@molecule/api-screenshots-molecule
Provider bond · screenshots · API (Node) · v1.1.1 · Apache-2.0
molecule.dev hosted screenshot capture — URL or HTML to image, billed to your molecule project, no vendor account
npm install @molecule/api-screenshots-moleculenpm · Source on GitHub · Implements @molecule/api-screenshots
How it works
@molecule/api-screenshots-molecule is a provider bond on the API (Node) side: it implements the screenshots core interface (@molecule/api-screenshots) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-screenshots, @molecule/api-secrets
Secrets: MOLECULE_API_KEY
Reference
molecule.dev hosted screenshot capture — URL or HTML to image, billed to your molecule project, no vendor account
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Type
provider
Installation
npm install @molecule/api-screenshots-molecule @molecule/api-screenshots @molecule/api-secrets
API
Interfaces
ScreenshotResult
The response the hosted screenshot service returns.
interface ScreenshotResult {
/** The image, base64-encoded — no `data:` URL prefix. */
data: string
/** MIME type of the returned image (`image/png` or `image/jpeg`). */
mimeType: string
/** The image's width in pixels. */
width: number
/** The image's height in pixels. */
height: number
/** Size in bytes. */
bytes: number
}
MoleculeScreenshotsConfig
Options for createProvider(). Every field falls back to its env var.
interface MoleculeScreenshotsConfig {
/**
* A molecule project API key (`mk_…`) or the in-sandbox token (`mbk_…`),
* scope `broker` or `broker:screenshots`. Defaults to
* `process.env.MOLECULE_API_KEY`.
*/
apiKey?: string
/**
* The hosted services base URL. Defaults to
* `process.env.MOLECULE_SERVICES_URL`, then
* `https://api.molecule.dev/api/v1/services`.
*/
servicesUrl?: string
/** Per-call timeout in milliseconds. Defaults to `60_000` — a capture renders a page. */
timeoutMs?: number
}
Classes
MoleculeServiceError
Error thrown for a refused or failed hosted-service call.
class MoleculeServiceError extends Error {
/** HTTP status from molecule.dev. */
readonly status: number
/** Stable error key from molecule.dev. */
readonly errorKey: string | undefined
/**
* Create the error.
*
* @param message - Human-readable message.
* @param status - HTTP status.
* @param errorKey - Stable error key, when the service sent one.
* @param cause - The underlying error, if any.
*/
constructor(message: string, status: number, errorKey?: string, cause?: unknown) {
super(message, cause === undefined ? undefined : { cause })
this.name = 'MoleculeServiceError'
this.status = status
this.errorKey = errorKey
}
}
MoleculeScreenshotProvider
Screenshot provider backed by molecule.dev's hosted service.
The renderer fetches HTTP(S) subresources from PUBLIC addresses only — loopback, private-network and link-local targets are refused server-side — so a user-supplied URL cannot reach your network from molecule's render machine.
class MoleculeScreenshotProvider implements ScreenshotProvider {
readonly name = 'molecule'
private readonly apiKey: string
private readonly servicesUrl: string
private readonly timeoutMs: number
/**
* Create the provider.
*
* @param config - Options; each falls back to its env var.
*/
constructor(config: MoleculeScreenshotsConfig = {}) {
this.apiKey = config.apiKey ?? process.env.MOLECULE_API_KEY ?? ''
this.servicesUrl = resolveServicesUrl(
config.servicesUrl ?? process.env.MOLECULE_SERVICES_URL ?? DEFAULT_SERVICES_URL,
)
this.timeoutMs = config.timeoutMs ?? 60_000
}
/**
* Captures one image on the hosted service and returns the decoded bytes.
* Prefer `captureWithMeta` when the pixel dimensions matter — it is
* the same single round trip.
*
* @param source - What to capture (URL or HTML — exactly one).
* @param options - Capture options (viewport, format, scale, …).
* @returns The image bytes.
*/
async capture(source: CaptureSource, options?: ScreenshotOptions): Promise<Buffer> {
const result = await this.captureWithMeta(source, options)
return Buffer.from(result.data, 'base64')
}
/**
* Captures one image on the hosted service, returning the bytes together
* with their pixel dimensions and MIME type.
*
* @param source - What to capture (URL or HTML — exactly one).
* @param options - Capture options (viewport, format, scale, …).
* @returns The image plus its metadata.
*/
async captureWithMeta(
source: CaptureSource,
options?: ScreenshotOptions,
): Promise<ScreenshotResult> {
if (typeof source !== 'object' || source === null) {
throw new MoleculeServiceError(
'Pass exactly one of "url" or "html".',
400,
'hostedServices.error.invalidInput',
)
}
const { url, html } = source
if ((typeof url === 'string') === (typeof html === 'string')) {
throw new MoleculeServiceError(
'Pass exactly one of "url" or "html".',
400,
'hostedServices.error.invalidInput',
)
}
if (url !== undefined) {
if (url.length === 0 || url.length > SCREENSHOT_SERVICE_LIMITS.maxUrlChars) {
throw new MoleculeServiceError(
`"url" must be 1-${SCREENSHOT_SERVICE_LIMITS.maxUrlChars} characters.`,
400,
'hostedServices.error.invalidInput',
)
}
try {
const parsed = new URL(url)
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('scheme')
} catch (_error) {
// Intentional: the parse failure becomes the field error below.
throw new MoleculeServiceError(
'"url" must be an absolute http(s) URL.',
400,
'hostedServices.error.invalidInput',
)
}
}
if (html !== undefined) {
if (html.length === 0) {
throw new MoleculeServiceError(
'"html" must be non-empty text.',
400,
'hostedServices.error.invalidInput',
)
}
if (html.length > SCREENSHOT_SERVICE_LIMITS.maxHtmlChars) {
throw new MoleculeServiceError(
`"html" is ${html.length} characters; the limit is ${SCREENSHOT_SERVICE_LIMITS.maxHtmlChars}.`,
413,
'hostedServices.error.inputTooLarge',
)
}
}
if (options !== undefined) {
if (typeof options !== 'object' || options === null) {
throw new MoleculeServiceError(
'"options" must be an object.',
400,
'hostedServices.error.invalidInput',
)
}
const o = options as Record<string, unknown>
const int = (v: unknown): boolean => typeof v === 'number' && Number.isInteger(v)
for (const key of ['width', 'height'] as const) {
const v = o[key]
if (
v !== undefined &&
(!int(v) ||
(v as number) < SCREENSHOT_SERVICE_LIMITS.minViewport ||
(v as number) > SCREENSHOT_SERVICE_LIMITS.maxViewport)
) {
throw new MoleculeServiceError(
`"options.${key}" must be an integer from ${SCREENSHOT_SERVICE_LIMITS.minViewport} to ${SCREENSHOT_SERVICE_LIMITS.maxViewport}.`,
400,
'hostedServices.error.invalidInput',
)
}
}
if (
o.scale !== undefined &&
(typeof o.scale !== 'number' ||
!Number.isFinite(o.scale) ||
o.scale < 1 ||
o.scale > SCREENSHOT_SERVICE_LIMITS.maxScale)
) {
throw new MoleculeServiceError(
`"options.scale" must be a number from 1 to ${SCREENSHOT_SERVICE_LIMITS.maxScale}.`,
400,
'hostedServices.error.invalidInput',
)
}
if (
o.quality !== undefined &&
(!int(o.quality) ||
(o.quality as number) < 1 ||
(o.quality as number) > SCREENSHOT_SERVICE_LIMITS.maxQuality)
) {
throw new MoleculeServiceError(
`"options.quality" must be an integer from 1 to ${SCREENSHOT_SERVICE_LIMITS.maxQuality}.`,
400,
'hostedServices.error.invalidInput',
)
}
if (
o.delayMs !== undefined &&
(!int(o.delayMs) ||
(o.delayMs as number) < 0 ||
(o.delayMs as number) > SCREENSHOT_SERVICE_LIMITS.maxDelayMs)
) {
throw new MoleculeServiceError(
`"options.delayMs" must be an integer from 0 to ${SCREENSHOT_SERVICE_LIMITS.maxDelayMs}.`,
400,
'hostedServices.error.invalidInput',
)
}
if (o.fullPage !== undefined && typeof o.fullPage !== 'boolean') {
throw new MoleculeServiceError(
'"options.fullPage" must be a boolean.',
400,
'hostedServices.error.invalidInput',
)
}
if (o.format !== undefined && o.format !== 'png' && o.format !== 'jpeg') {
throw new MoleculeServiceError(
'"options.format" must be "png" or "jpeg".',
400,
'hostedServices.error.invalidInput',
)
}
const width = (o.width as number | undefined) ?? 1280
const height = (o.height as number | undefined) ?? 800
const scale = (o.scale as number | undefined) ?? 1
const captureHeight =
o.fullPage === true ? SCREENSHOT_SERVICE_LIMITS.maxFullPageHeight : height
if (width * captureHeight * scale * scale > SCREENSHOT_SERVICE_LIMITS.maxPixels) {
throw new MoleculeServiceError(
`The capture is ${width}×${captureHeight} at scale ${scale} — over the ${SCREENSHOT_SERVICE_LIMITS.maxPixels}-pixel budget. Reduce the viewport or the scale.`,
400,
'hostedServices.error.invalidInput',
)
}
}
return this.request('screenshots/capture', {
...(url !== undefined ? { url } : { html }),
...(options !== undefined ? { options } : {}),
})
}
/** One POST to the hosted service. */
private async request(path: string, body: Record<string, unknown>): Promise<ScreenshotResult> {
if (!this.apiKey) {
throw new MoleculeServiceError(
'MOLECULE_API_KEY is not set. Create a project API key on molecule.dev (or with `mlcl apikey create`) and put it in the API .env.',
401,
'hostedServices.error.tokenInvalid',
)
}
const response = await fetch(`${this.servicesUrl}/${path}`, {
method: 'POST',
headers: { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(this.timeoutMs),
})
const text = await response.text()
let data: unknown
try {
data = text ? JSON.parse(text) : {}
} catch (error) {
throw new MoleculeServiceError(
`molecule.dev returned ${response.status} with a non-JSON body.`,
response.status,
undefined,
error,
)
}
if (!response.ok) {
// … truncated — the full declaration is in node_modules/@molecule/api-screenshots-molecule/dist/provider.d.ts
Functions
createProvider(config)
Create a hosted screenshot provider.
function createProvider(config?: MoleculeScreenshotsConfig): ScreenshotProvider
config— Options; each falls back to its env var.
Returns: A ScreenshotProvider backed by molecule.dev.
Constants
provider
The provider implementation (wire with setProvider).
const provider: ScreenshotProvider
DEFAULT_SERVICES_URL
Default hosted services base URL.
const DEFAULT_SERVICES_URL: 'https://api.molecule.dev/api/v1/services'
SCREENSHOT_SERVICE_LIMITS
Per-request limits of the hosted service (mirrored for local refusal).
const SCREENSHOT_SERVICE_LIMITS: {
readonly minViewport: 16
readonly maxViewport: 3840
readonly maxScale: 3
readonly maxQuality: 100
readonly maxDelayMs: 5000
readonly maxUrlChars: 2048
readonly maxHtmlChars: 2000000
readonly maxFullPageHeight: 20000
readonly maxPixels: 25000000
readonly maxResultBytes: number
}
screenshotsMoleculeSecretDefinitions
Secret definitions required by the molecule.dev hosted screenshot bond.
const screenshotsMoleculeSecretDefinitions: SecretDefinition[]
Core Interface
Implements @molecule/api-screenshots interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-screenshots'
import { provider } from '@molecule/api-screenshots-molecule'
export function setupScreenshotsMolecule(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-secrets^1.0.1@molecule/api-screenshots>=1.0.0
Environment Variables
MOLECULE_API_KEY(required) — molecule.dev project API key- Setup: Create one for your project with
mlcl apikey create --project <id> --name <name>(keys start with mk_). - Get it here: https://www.molecule.dev
- Example:
mk_...
- Setup: Create one for your project with
Runtime Dependencies
@molecule/api-screenshots@molecule/api-secrets