Security
@sealpixel/render treats the original and the edit log as untrusted input. It does not know who sent them. Authenticate and authorize the caller in your own code before you call render().
Limits before the decode
The renderer checks three limits before it decodes a pixel:
limits.maxBytes: the size of the original.limits.maxMegapixels: the width and the height from the file header.limits.maxLogBytes: the size of the edit log, as UTF-8 bytes of its canonical JSON.
A file whose header claims a large size fails with TOO_LARGE, even when its pixels would not decode. Set maxMegapixels to the size that your memory allows, see Memory.
The log belongs to the original
The editor writes the sha256 and the size of the original into log.source. The renderer compares both with the original that it receives:
- A different sha256 is
SOURCE_MISMATCH. The check runs when the log has a hash andverifySourceHashis on, which is the default. Keep the hash in the log that you store: without it, only the size is compared. - A different decoded size is
SOURCE_MISMATCH, with or without the hash.
So a log never renders on other pixels than the ones it was written for.
The log is parsed, not trusted
Every op passes the schema of its plugin. An op of a plugin that is not in plugins is UNKNOWN_PLUGIN, and a malformed log is INVALID_EDIT_LOG. Give the renderer the same pluginOptions as the editor. Then every constraint that the editor enforces is enforced again: a disallowed preset, a sticker above max, a locked watermark that the log removed. A violation is OPTION_VIOLATION.
Bounded work per render
The render is one synchronous call into the core, and a signal cannot interrupt it while it runs. The core bounds the work instead, with the same limits in the browser, on the server and in the CLI:
- Every stage's output stays within 268 megapixels and 65535 pixels per side.
- One render does at most 32 times the work of one pass over its largest image, and never less than for a 4-megapixel image.
- A text is at most 10,000 characters.
A plan above these limits is TOO_LARGE before the core draws it. The limits are part of the core, not options, so the browser and the server accept the same documents.
Assets
The renderer loads nothing. It fetches no URL and reads no file, also not the URLs in plugin declarations or in log.assets. The host passes the bytes of every font and image, so the host decides where they come from.
When a declaration carries a sha256, the renderer compares it with the sha256 of the bytes. Bytes with another hash are SOURCE_MISMATCH, and an asset that was not passed is MISSING_ASSET. A plugin declaration wins over an entry in log.assets with the same id, so a log cannot replace the hash of a plugin's font.
Each render starts with an empty asset table in the core. A render cannot use bytes that an earlier render put under the same id.
What the renderer does not do
It has no caller identity, no rate limit and no timeout of its own. It opens no port and writes nothing to storage. Those parts belong to the code around it.