Sign a container format (MXF)
The core signer handles every C2PA-native format directly. A container format with no C2PA byte layout of its own, such as an MXF broadcast master, is signed through a container plugin: a separately published component that teaches the SDK to author a manifest slot inside the container and hash it. A build that never signs MXF links zero of this code, so the plugin is opt-in.
MXF is the one container plugin published today: @limboai/verifox-sdk-issuer-container-mxf.
Install
Add the plugin alongside the issuer SDK:
npm install @limboai/verifox-sdk-issuer @limboai/verifox-sdk-issuer-container-mxf
The plugin peer-depends on @limboai/verifox-sdk-issuer: the SDK owns the host I/O contract the plugin runs against, so the two ship and version together.
Register the plugin
Import the plugin's module namespace and pass it in the array AssetSdk.create takes as its optional second argument. Omit the array for a c2pa-only signer; each plugin is tried in order on a format match.
import { SignerClient, AssetSdk } from "@limboai/verifox-sdk-issuer";
import * as mxf from "@limboai/verifox-sdk-issuer-container-mxf";
const client = await SignerClient.connect("https://api.trylimbo.com", {
headers: { authorization: `Bearer ${process.env.LIMBO_API_KEY}` },
});
const sdk = AssetSdk.create(client, [mxf]);
Sign a master
An MXF master runs to tens or hundreds of GB, so it is signed in place through a RangeSink, never loaded into memory. A RangeSink is the writable form of a RangeSource: synchronous readAt plus writeAt and setLen. The plugin authors the manifest slot, the SDK hashes the finalized file and submits it to the signer, then the SDK itself writes the signed manifest into the reserved slot. The sink is the signed output, so there is no separate embed step.
import { openSync, readSync, writeSync, ftruncateSync, fstatSync, closeSync } from "node:fs";
function rangeSink(path: string) {
const fd = openSync(path, "r+");
return {
get length() { return fstatSync(fd).size; },
readAt(offset: number, length: number) {
const buf = Buffer.alloc(length);
const n = readSync(fd, buf, 0, length, offset);
return new Uint8Array(buf.buffer, buf.byteOffset, n);
},
writeAt(offset: number, data: Uint8Array) { writeSync(fd, data, 0, data.length, offset); },
setLen(length: number) { ftruncateSync(fd, length); },
close() { closeSync(fd); },
};
}
const sink = rangeSink("master.mxf");
try {
const { sidecar, manifestId } = await sdk.sign(
sink,
"master.mxf",
{ origin: { sourceType: "digitalCapture" } },
{ storage: { mode: "embedded" }, format: "application/mxf" }, // ← RangeSink + format required
);
} finally {
sink.close();
}
master.mxf is now signed in place. The returned sidecar is the standalone manifest and manifestId its registry id, exactly as for a c2pa asset, useful for the remote and both storage modes, where the manifest is additionally indexed for recovery. See Soft binding for the durable ISCC a transcoded master is recovered by.
Container signing requires a writable RangeSink and an explicit format; in-memory Uint8Array bytes are rejected (there is nowhere to write the slot back). The master must be finalized (a closed footer) so the whole-file hash is stable; a still-growing capture cannot be signed.
How it works
The plugin is a native Node addon built from libMXF++ — not WebAssembly, since MXF masters are never a realistic browser workload. It never loads the essence into memory: it reserves an empty, fixed-size manifest slot inside the MXF, the SDK hashes the finalized bytes over the same sink, submits the hash to the signer, and the SDK itself writes the returned manifest into the reserved slot. The master streams throughout, so memory stays bounded regardless of its size. Verifying the result is the mirror of this: see Verify a container format.
See also
- Verify a container format (MXF): the verify-side plugin
- File streaming:
RangeSource/RangeSinkfor large files - Storage modes: embedded, remote, both
- Soft binding: recoverable code for a transcoded master