From 793ae1ce8fa820b8025c65268f949df8e0200d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karlo=20Ben=C4=8Di=C4=87?= Date: Tue, 7 Jan 2025 16:35:05 +0100 Subject: [PATCH] add jsdoc --- src/cose/Signature.ts | 29 ++++++ src/manifest/AssertionStore.ts | 47 +++++++++ src/manifest/Claim.ts | 56 +++++++++++ src/manifest/Manifest.ts | 95 ++++++++++++++++--- src/manifest/assertions/Assertion.ts | 52 +++++++++- .../assertions/IngredientAssertion.ts | 22 +++++ 6 files changed, 286 insertions(+), 15 deletions(-) diff --git a/src/cose/Signature.ts b/src/cose/Signature.ts index 36c78b1..41c2cb5 100644 --- a/src/cose/Signature.ts +++ b/src/cose/Signature.ts @@ -31,10 +31,21 @@ export class Signature { public paddingLength = 0; private validatedTimestamp: Date | undefined; + /** + * Gets the validated timestamp or falls back to unverified timestamp + * @returns Date object representing the timestamp, or undefined if no timestamp exists + */ public get timestamp() { return this.validatedTimestamp ?? this.getTimestampWithoutVerification(); } + /** + * Reads a signature from JUMBF data + * @param content - The JUMBF content to parse + * @returns A new Signature instance + * @throws MalformedContentError if content is malformed + * @throws ValidationError if algorithm is unsupported + */ public static readFromJUMBFData(content: unknown) { const signature = new Signature(); const rawContent = content as CoseSignature; @@ -99,6 +110,11 @@ export class Signature { return signature; } + /** + * Writes the signature data to JUMBF format + * @returns CoseSignature array containing the signature data + * @throws Error if certificate or algorithm is missing + */ public writeJUMBFData(): CoseSignature { if (!this.certificate) throw new Error('Signature is missing certificate'); if (!this.algorithm) throw new Error('Signature is missing algorithm'); @@ -131,6 +147,13 @@ export class Signature { ]; } + /** + * Signs the provided payload and optionally adds a timestamp + * @param privateKey - Private key in PKCS#8 format + * @param payload - Data to be signed + * @param timestampProvider - Optional provider for RFC3161 timestamp + * @throws Error if protected bucket, algorithm or certificate is missing + */ public async sign( privateKey: Uint8Array, payload: Uint8Array, @@ -290,6 +313,12 @@ export class Signature { return undefined; } + /** + * Validates the signature against a payload + * @param payload - The payload to validate against + * @param sourceBox - Optional JUMBF box for error context + * @returns Promise resolving to ValidationResult + */ public async validate(payload: Uint8Array, sourceBox?: JUMBF.IBox): Promise { if (!this.certificate || !this.rawProtectedBucket || !this.signature || !this.algorithm) { return ValidationResult.error(ValidationStatusCode.SigningCredentialInvalid, sourceBox); diff --git a/src/manifest/AssertionStore.ts b/src/manifest/AssertionStore.ts index f76c133..53df61d 100644 --- a/src/manifest/AssertionStore.ts +++ b/src/manifest/AssertionStore.ts @@ -22,6 +22,13 @@ export class AssertionStore implements ManifestComponent { public assertions: Assertion[] = []; public sourceBox: JUMBF.SuperBox | undefined; + /** + * Reads an assertion store from a JUMBF box + * @param box - The JUMBF box to read from + * @param claim - The claim this assertion store belongs to + * @returns A new AssertionStore instance + * @throws ValidationError if the box is invalid + */ public static read(box: JUMBF.SuperBox, claim: Claim): AssertionStore { const assertionStore = new AssertionStore(); assertionStore.sourceBox = box; @@ -40,6 +47,13 @@ export class AssertionStore implements ManifestComponent { return assertionStore; } + /** + * Reads an assertion from a JUMBF box + * @param box - The JUMBF box to read from + * @param claim - The claim this assertion belongs to + * @returns The created Assertion instance + * @throws ValidationError if the box is invalid + */ private static readAssertion(box: JUMBF.IBox, claim: Claim): Assertion { if (!(box instanceof JUMBF.SuperBox)) throw new ValidationError(ValidationStatusCode.AssertionMissing, box, 'Assertion is not a SuperBox'); @@ -82,6 +96,11 @@ export class AssertionStore implements ManifestComponent { return assertion; } + /** + * Generates a JUMBF box containing the assertion store + * @param claim - The claim this assertion store belongs to + * @returns The generated JUMBF box + */ public generateJUMBFBox(claim: Claim): JUMBF.SuperBox { const box = new JUMBF.SuperBox(); box.descriptionBox = new JUMBF.DescriptionBox(); @@ -93,20 +112,37 @@ export class AssertionStore implements ManifestComponent { return box; } + /** + * Gets all hard binding assertions from the store + * @returns Array of assertions that are considered hard bindings + */ public getHardBindings() { return this.assertions.filter( assertion => assertion.label && AssertionLabels.hardBindings.includes(assertion.label), ); } + /** + * Gets assertions by their label + * @param label - The label to filter by + * @returns Array of assertions matching the label + */ public getAssertionsByLabel(label: string) { return this.assertions.filter(assertion => assertion.label === label); } + /** + * Gets all action assertions from the store + * @returns Array of ActionAssertion objects + */ public getActionAssertions() { return this.assertions.filter(assertion => assertion instanceof ActionAssertion); } + /** + * Gets all thumbnail assertions from the store + * @returns Array of thumbnail assertions (both claim and ingredient thumbnails) + */ public getThumbnailAssertions() { return this.assertions.filter( assertion => @@ -115,11 +151,22 @@ export class AssertionStore implements ManifestComponent { ); } + /** + * Gets the bytes representation of the assertion store + * @param claim - The claim this assertion store belongs to + * @param rebuild - Whether to rebuild the JUMBF box before getting bytes + * @returns Uint8Array of bytes or undefined if no source box exists + */ public getBytes(claim: Claim, rebuild = false) { if (rebuild) this.generateJUMBFBox(claim); return this.sourceBox?.toBuffer(); } + /** + * Gets ingredient assertions filtered by relationship type + * @param relationship - The relationship type to filter by + * @returns Array of IngredientAssertion objects matching the relationship + */ public getIngredientsByRelationship(relationship: RelationshipType): IngredientAssertion[] { return this.assertions.filter( (a): a is IngredientAssertion => a instanceof IngredientAssertion && a.relationship === relationship, diff --git a/src/manifest/Claim.ts b/src/manifest/Claim.ts index 1609d12..c77b39d 100644 --- a/src/manifest/Claim.ts +++ b/src/manifest/Claim.ts @@ -31,19 +31,37 @@ export class Claim implements ManifestComponent { public claimGeneratorInfo?: string; public versionReason?: string; + /** + * Gets the version of the claim + * @returns The claim version + */ public get version(): ClaimVersion { return this._version; } + /** + * Sets the version of the claim and updates the label accordingly + * @param value - The new claim version + */ public set version(value: ClaimVersion) { this._version = value; this._label = this.version === ClaimVersion.V2 ? 'c2pa.claim.v2' : 'c2pa.claim'; } + /** + * Gets the label for this claim + * @returns The claim label string + */ public get label(): string { return this._label; } + /** + * Reads a claim from a JUMBF box + * @param box - The JUMBF box to read from + * @returns A new Claim instance + * @throws ValidationError if the box is invalid or has unsupported algorithm + */ public static read(box: JUMBF.SuperBox) { if (!box.contentBoxes.length || !(box.contentBoxes[0] instanceof JUMBF.CBORBox)) throw new ValidationError(ValidationStatusCode.ClaimCBORInvalid, box, 'Claim has invalid content box'); @@ -94,6 +112,11 @@ export class Claim implements ManifestComponent { return claim; } + /** + * Generates a JUMBF box containing the claim + * @returns The generated JUMBF box + * @throws Error if required fields are missing + */ public generateJUMBFBox(): JUMBF.SuperBox { if (!this.instanceID) throw new Error('Claim: missing instanceID'); if (!this.signatureRef) throw new Error('Claim: missing signature'); @@ -154,6 +177,11 @@ export class Claim implements ManifestComponent { return this.sourceBox; } + /** + * Maps a raw hash algorithm string to internal HashAlgorithm type + * @param alg - The raw hash algorithm string + * @returns The mapped HashAlgorithm or undefined if not supported + */ public static mapHashAlgorithm(alg: raw.HashAlgorithm | undefined): HashAlgorithm | undefined { switch (alg) { case 'sha256': @@ -167,6 +195,11 @@ export class Claim implements ManifestComponent { } } + /** + * Maps internal HashAlgorithm type to raw hash algorithm string + * @param alg - The HashAlgorithm to map + * @returns The raw hash algorithm string or undefined if not supported + */ public static reverseMapHashAlgorithm(alg: HashAlgorithm | undefined): raw.HashAlgorithm | undefined { switch (alg) { case 'SHA-256': @@ -180,6 +213,12 @@ export class Claim implements ManifestComponent { } } + /** + * Maps a raw HashedURI to internal HashedURI type + * @param hashedURI - The raw HashedURI to map + * @returns The mapped HashedURI + * @throws ValidationError if algorithm is unsupported + */ public mapHashedURI(hashedURI: raw.HashedURI): HashedURI { const algorithm = Claim.mapHashAlgorithm(hashedURI.alg) ?? this.defaultAlgorithm; if (!algorithm) throw new ValidationError(ValidationStatusCode.AlgorithmUnsupported, hashedURI.url); @@ -191,6 +230,11 @@ export class Claim implements ManifestComponent { }; } + /** + * Maps internal HashedURI type to raw HashedURI + * @param hashedURI - The HashedURI to map + * @returns The raw HashedURI + */ public reverseMapHashedURI(hashedURI: HashedURI): raw.HashedURI { if (hashedURI.algorithm === this.defaultAlgorithm) { // don't store the algorithm redundantly if it's the default @@ -207,11 +251,23 @@ export class Claim implements ManifestComponent { } } + /** + * Gets the bytes representation of the claim + * @param claim - The claim to get bytes for + * @param rebuild - Whether to rebuild the JUMBF box before getting bytes + * @returns Uint8Array of bytes or undefined if no source box exists + */ public getBytes(claim: Claim, rebuild = false): Uint8Array | undefined { if (rebuild) this.generateJUMBFBox(); return (this.sourceBox?.contentBoxes[0] as JUMBF.CBORBox | undefined)?.rawContent; } + /** + * Generates a URN for the claim based on its version + * For v1: urn:uuid:{uuid} + * For v2: urn:c2pa:{uuid}[:{generatorInfo}][:{versionReason}] + * @returns The generated URN string + */ public getURN(): string { const uuid = uuidv4({ random: Crypto.getRandomValues(16) }); diff --git a/src/manifest/Manifest.ts b/src/manifest/Manifest.ts index ee782c8..5ac003f 100644 --- a/src/manifest/Manifest.ts +++ b/src/manifest/Manifest.ts @@ -38,6 +38,16 @@ export class Manifest implements ManifestComponent { public constructor(public readonly parentStore: ManifestStore) {} + /** + * Initializes a new manifest with the specified parameters + * @param claimVersion - The version of the claim to create + * @param assetFormat - The format of the asset this manifest is for + * @param instanceID - Unique identifier for this manifest instance + * @param defaultHashAlgorithm - Default hashing algorithm to use + * @param certificate - X.509 certificate for signing + * @param signingAlgorithm - Algorithm to use for signing + * @param chainCertificates - Optional chain of certificates + */ public initialize( claimVersion: ClaimVersion, assetFormat: string | undefined, @@ -63,8 +73,11 @@ export class Manifest implements ManifestComponent { /** * Reads a manifest from a JUMBF box - * @param box Source JUMBF box - * @param parentStore The manifest store this manifest is located in + * @param box - Source JUMBF box + * @param parentStore - The manifest store this manifest is located in + * @returns A new Manifest instance or undefined if box type is not recognized + * @throws ValidationError if the box is invalid + * @throws MalformedContentError if manifest structure is invalid */ public static read(box: JUMBF.SuperBox, parentStore: ManifestStore): Manifest | undefined { if (!box.descriptionBox) throw new MalformedContentError('Manifest box is missing a description box'); @@ -127,6 +140,11 @@ export class Manifest implements ManifestComponent { } } + /** + * Generates a JUMBF box containing the manifest + * @returns The generated JUMBF box + * @throws Error if required fields are missing + */ public generateJUMBFBox(): JUMBF.SuperBox { // TODO: Here, we never assign this.sourceBox and leave it as is, to ensure we read back unmodified bytes when // re-signing an existing manifest. But in other classes, we do assign this.sourceBox within generateJUMBFBox(). @@ -153,8 +171,9 @@ export class Manifest implements ManifestComponent { /** * Resolves a JUMBF URL to a manifest component - * @param url JUMBF URL - * @param sameManifestOnly Should the component be located in this manifest only? + * @param url - JUMBF URL + * @param sameManifestOnly - Should the component be located in this manifest only? + * @returns The resolved ManifestComponent or undefined if not found */ public getComponentByURL(url?: string, sameManifestOnly = false): ManifestComponent | undefined { const m = url?.match(/^self#jumbf=(.+)$/); @@ -185,8 +204,9 @@ export class Manifest implements ManifestComponent { /** * Retrieves an Assertion from a hashed reference (without validating the hash) - * @param assertion Assertion reference - * @param sameManifestOnly Should the assertion be located in this manifest only? + * @param assertion - Assertion reference as HashedURI or string + * @param sameManifestOnly - Should the assertion be located in this manifest only? + * @returns The referenced Assertion or undefined if not found */ private getAssertion(assertion: HashedURI | string, sameManifestOnly?: boolean): Assertion | undefined { const component = this.getComponentByURL( @@ -202,7 +222,9 @@ export class Manifest implements ManifestComponent { } /** - * Validates that a hashed reference is valid, i.e. the referenced component exists and the hash matches + * Validates that a hashed reference is valid + * @param reference - The hashed reference to validate + * @returns Promise resolving to true if the hash matches, false otherwise */ private async validateHashedReference(reference: HashedURI): Promise { const referencedComponent = this.getComponentByURL(reference.uri); @@ -215,6 +237,8 @@ export class Manifest implements ManifestComponent { /** * Calculates the hash for the hashed reference based on the referenced component + * @param reference - The hashed reference to update + * @throws Error if reference is invalid or manifest has no claim */ public async updateHashedReference(reference: HashedURI): Promise { if (!this.claim) throw new Error('Manifest must have a claim'); @@ -226,8 +250,9 @@ export class Manifest implements ManifestComponent { } /** - * Verifies a the manifest's claim's validity - * @param asset Asset for validation of bindings + * Verifies the manifest's claim's validity + * @param asset - Asset for validation of bindings + * @returns Promise resolving to ValidationResult */ public async validate(asset: Asset): Promise { const result = new ValidationResult(); @@ -316,6 +341,10 @@ export class Manifest implements ManifestComponent { return result; } + /** + * Validates assertions in a standard manifest + * @returns ValidationResult containing any validation errors or successes + */ private validateStandardManifestAssertions(): ValidationResult { const result = new ValidationResult(); @@ -336,6 +365,10 @@ export class Manifest implements ManifestComponent { return result; } + /** + * Validates assertions in an update manifest + * @returns ValidationResult containing any validation errors or successes + */ private validateUpdateManifestAssertions(): ValidationResult { const result = new ValidationResult(); @@ -561,6 +594,12 @@ export class Manifest implements ManifestComponent { return result; } + /** + * Validates assertions in a standard manifest + * @param assertionReference - Reference to the assertion being validated + * @param assertion - The assertion to validate + * @returns ValidationResult containing validation status + */ private validateStandardMandatoryActions( assertionReference: HashedURI, assertion: ActionAssertion, @@ -581,7 +620,10 @@ export class Manifest implements ManifestComponent { } /** - * Appends an assertion to the manifest's assertion store and adds a reference to the claim. + * Appends an assertion to the manifest's assertion store and adds a reference to the claim + * @param assertion - The assertion to add + * @param hashAlgorithm - Optional hash algorithm to use for the reference + * @throws Error if manifest has no claim or assertion store */ public addAssertion(assertion: Assertion, hashAlgorithm: HashAlgorithm | undefined = undefined): void { if (!this.claim) throw new Error('Manifest does not have claim'); @@ -594,6 +636,10 @@ export class Manifest implements ManifestComponent { /** * Creates a hashed reference to an assertion. The hash is left empty and will be calculated * during sign(). + * @param assertion - The assertion to reference + * @param hashAlgorithm - Optional hash algorithm to use + * @returns HashedURI reference to the assertion + * @throws Error if manifest has no assertion store */ public createAssertionReference( assertion: Assertion, @@ -606,6 +652,10 @@ export class Manifest implements ManifestComponent { /** * Creates a hashed reference to a ManifestComponent. The hash is left empty and will be calculated * during sign(). + * @param label - The label of the component to reference + * @param hashAlgorithm - Optional hash algorithm to use + * @returns HashedURI reference to the component + * @throws Error if manifest has no claim or missing algorithm */ public createHashedReference(label: string, hashAlgorithm: HashAlgorithm | undefined = undefined): HashedURI { // TODO: It would be better to pass in a ManifestComponent here instead of the label and have the @@ -629,9 +679,10 @@ export class Manifest implements ManifestComponent { } /** - * Prepares the manifest for signing and fills in the signature using the provided private key - * @param privateKey Private key in PKCS#8 format - * @param timestampProvider An optional timestamp provider to add an RFC3161 timestamp + * Prepares the manifest for signing and fills in the signature + * @param privateKey - Private key in PKCS#8 format + * @param timestampProvider - An optional timestamp provider to add an RFC3161 timestamp + * @throws Error if manifest has no claim or signature */ public async sign(privateKey: Uint8Array, timestampProvider?: TimestampProvider): Promise { if (!this.claim) throw new Error('Manifest does not have claim'); @@ -646,6 +697,11 @@ export class Manifest implements ManifestComponent { await this.signature.sign(privateKey, this.claim.getBytes(this.claim, true)!, timestampProvider); } + /** + * Gets the bytes representation of the manifest + * @param claim - Optional claim parameter + * @returns Uint8Array of bytes or undefined if no source box exists + */ public getBytes(claim?: Claim): Uint8Array | undefined { if (!claim && !this.claim) { return undefined; @@ -653,12 +709,20 @@ export class Manifest implements ManifestComponent { return this.sourceBox?.toBuffer(); } + /** + * Validates relationships between manifests + * @returns Promise resolving to ValidationResult + */ private async validateManifestRelationships(): Promise { // TODO: Manifest relationship validation needs to be revisited // Current validation is too strict and fails valid Adobe test files return new ValidationResult(); } + /** + * Validates all ingredients in the manifest + * @returns Promise resolving to ValidationResult + */ private async validateIngredients(): Promise { const result = new ValidationResult(); const ingredients = this.assertions?.getAssertionsByLabel(AssertionLabels.ingredient) ?? []; @@ -671,6 +735,11 @@ export class Manifest implements ManifestComponent { return result; } + /** + * Validates a single ingredient + * @param ingredient - The ingredient assertion to validate + * @returns Promise resolving to ValidationResult + */ private async validateSingleIngredient(ingredient: IngredientAssertion): Promise { const result = new ValidationResult(); result.merge(await ingredient.validate(this)); diff --git a/src/manifest/assertions/Assertion.ts b/src/manifest/assertions/Assertion.ts index 340abd7..d42f859 100644 --- a/src/manifest/assertions/Assertion.ts +++ b/src/manifest/assertions/Assertion.ts @@ -13,13 +13,18 @@ export abstract class Assertion implements ManifestComponent { public uuid?: Uint8Array; public sourceBox: JUMBF.SuperBox | undefined; + /** + * Gets the full label including the optional index + * @returns The full label string + */ public get fullLabel() { return this.labelSuffix !== undefined ? `${this.label}__${this.labelSuffix}` : this.label; } /** - * the label in the JUMBF box contains both the actual assertion type identifier - * and an optional index, this utility method splits the two + * Splits the label in the JUMBF box into the actual assertion type identifier and an optional index + * @param label - The label to split + * @returns An object containing the label and the optional index */ public static splitLabel(label: string): { label: string; index?: number } { const match = /^(.+)__(\d+)$/.exec(label); @@ -33,6 +38,12 @@ export abstract class Assertion implements ManifestComponent { } } + /** + * Reads an assertion from a JUMBF box + * @param box - The JUMBF box to read from + * @param claim - The claim this assertion belongs to + * @throws ValidationError if the box is invalid + */ public readFromJUMBF(box: JUMBF.SuperBox, claim: Claim): void { if (!box.descriptionBox?.label) throw new ValidationError(ValidationStatusCode.AssertionCBORInvalid, box, 'Assertion is missing label'); @@ -48,8 +59,18 @@ export abstract class Assertion implements ManifestComponent { this.readContentFromJUMBF(box.contentBoxes[0], claim); } + /** + * Reads the assertion content from a JUMBF box + * @param box - The JUMBF box to read from + * @param claim - The claim this assertion belongs to + */ public abstract readContentFromJUMBF(box: JUMBF.IBox, claim: Claim): void; + /** + * Generates a JUMBF box for this assertion + * @param claim - Optional claim this assertion belongs to + * @returns The generated JUMBF box + */ public generateJUMBFBox(claim?: Claim): JUMBF.SuperBox { const box = new JUMBF.SuperBox(); @@ -63,17 +84,39 @@ export abstract class Assertion implements ManifestComponent { return box; } + /** + * Generates a JUMBF box for the assertion content + * @param claim - Optional claim this assertion belongs to + * @returns The generated JUMBF box + */ public abstract generateJUMBFBoxForContent(claim?: Claim): JUMBF.IBox; + /** + * Validates the assertion against an asset + * @param asset - The asset to validate against + * @returns Promise resolving to ValidationResult + */ public async validateAgainstAsset(asset: Asset): Promise { return new ValidationResult(); } + /** + * Gets the bytes representation of the assertion + * @param claim - The claim this assertion belongs to + * @param rebuild - Whether to rebuild the JUMBF box + * @returns Uint8Array of bytes or undefined if no source box + */ public getBytes(claim: Claim, rebuild = false) { if (rebuild) this.generateJUMBFBox(claim); return this.sourceBox?.toBuffer(); } + /** + * Reads the assertion data from a JUMBF box + * @param box - The JUMBF box to read from + * @returns The assertion data + * @throws ValidationError if the box is not a CBOR box + */ protected static readAssertionData(box: JUMBF.IBox): unknown { if (!(box instanceof JUMBF.CBORBox)) { throw new ValidationError(ValidationStatusCode.AssertionCBORInvalid, box, 'Expected CBOR box'); @@ -81,6 +124,11 @@ export abstract class Assertion implements ManifestComponent { return box.content; } + /** + * Validates the assertion against a manifest + * @param manifest - The manifest to validate against + * @returns Promise resolving to ValidationResult + */ public async validate(manifest: Manifest): Promise { return new ValidationResult(); } diff --git a/src/manifest/assertions/IngredientAssertion.ts b/src/manifest/assertions/IngredientAssertion.ts index b136fc6..47e787a 100644 --- a/src/manifest/assertions/IngredientAssertion.ts +++ b/src/manifest/assertions/IngredientAssertion.ts @@ -70,6 +70,12 @@ export class IngredientAssertion extends Assertion { public data?: HashedURI; public description?: string; + /** + * Reads the content of this assertion from a JUMBF box + * @param box - The JUMBF box to read from + * @param claim - The claim this assertion belongs to + * @throws ValidationError if the box is invalid + */ public readContentFromJUMBF(box: JUMBF.IBox, claim: Claim): void { if (!(box instanceof JUMBF.CBORBox) || !this.uuid || !BinaryHelper.bufEqual(this.uuid, raw.UUIDs.cborAssertion)) throw new ValidationError( @@ -114,6 +120,12 @@ export class IngredientAssertion extends Assertion { if (content.description) this.description = content.description; } + /** + * Generates a JUMBF box containing this assertion's content + * @param claim - The claim this assertion belongs to + * @returns The generated JUMBF box + * @throws Error if required fields are missing + */ public generateJUMBFBoxForContent(claim: Claim): JUMBF.IBox { if (!this.relationship) throw new Error('Assertion has no relationship'); @@ -140,6 +152,11 @@ export class IngredientAssertion extends Assertion { return box; } + /** + * Validates an ingredient assertion against a manifest + * @param manifest - The manifest containing this ingredient + * @returns Promise resolving to ValidationResult + */ public override async validate(manifest: Manifest): Promise { const result = await super.validate(manifest); @@ -203,6 +220,11 @@ export class IngredientAssertion extends Assertion { return result; } + /** + * Validates a single ingredient's manifest reference + * @param manifest - The manifest containing this ingredient + * @throws Error if validation fails + */ private async validateIngredient(manifest: Manifest): Promise { if (!this.activeManifest) { throw new Error('No active manifest reference');