From 3eb913149449db8af60b63f5b153370276bc97a7 Mon Sep 17 00:00:00 2001 From: Andy Parsons Date: Thu, 30 Apr 2026 13:28:44 -0400 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20fix=20all=20ESLint=20errors=20(103?= =?UTF-8?q?=20=E2=86=92=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused imports and variables across 17 files - Replace `any` types with specific inline types for C2PA assertion accesses, validation status objects, and trust-annotated manifest types - Fix unused export prop in AssetInfoIssuerDate (let → const) - Fix svelte-ignore → eslint-disable-next-line for {@html} in IssuedBySection - Auto-fixed padding, prefer-const, and shorthand attribute violations Co-Authored-By: Claude Sonnet 4.6 --- .../EmbeddedIcon/EmbeddedIcon.svelte | 2 +- src/lib/asset.ts | 23 ++++-- src/lib/crjson.ts | 30 +++++-- src/lib/sdk.ts | 2 + src/lib/selectors/doNotTrain.ts | 14 +++- src/lib/selectors/editsAndActivity.ts | 18 ++-- src/lib/selectors/generativeInfo.ts | 30 +++++-- src/lib/selectors/producer.ts | 12 ++- src/lib/selectors/reviewRatings.ts | 3 +- src/lib/selectors/socialAccounts.ts | 5 +- src/lib/selectors/web3Info.ts | 4 +- src/lib/selectors/website.ts | 7 +- .../AssetInfo/AssetInfoIssuerDate.svelte | 4 +- .../AboutSection/AboutSection.svelte | 5 +- .../AboutSection/IssuedBySection.svelte | 2 + .../verify/components/TreeView/TreeL1.svelte | 1 - src/routes/verify/stores/c2paReader.ts | 82 ++++++++----------- 17 files changed, 147 insertions(+), 97 deletions(-) diff --git a/src/components/EmbeddedIcon/EmbeddedIcon.svelte b/src/components/EmbeddedIcon/EmbeddedIcon.svelte index cf93a929e..2c89a618b 100644 --- a/src/components/EmbeddedIcon/EmbeddedIcon.svelte +++ b/src/components/EmbeddedIcon/EmbeddedIcon.svelte @@ -3,7 +3,7 @@ --> diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte index 8dc14aaa6..de003cbf5 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte @@ -12,7 +12,8 @@ export let trustSource: 'official' | 'legacy' | 'none' = 'none'; // Extract extended X.509 fields (Catching various Rust/JS SDK naming conventions) - $: sigInfo = manifestData.signatureInfo as any; + type ExtendedSigInfo = { organization_unit?: string; organizational_unit?: string; organizationUnit?: string; org_unit?: string; ou?: string; country?: string; country_name?: string; countryName?: string; c?: string }; + $: sigInfo = manifestData.signatureInfo as ExtendedSigInfo; $: orgUnit = sigInfo?.organization_unit || sigInfo?.organizational_unit || sigInfo?.organizationUnit || sigInfo?.org_unit || sigInfo?.ou; $: country = sigInfo?.country || sigInfo?.country_name || sigInfo?.countryName || sigInfo?.c; @@ -26,7 +27,7 @@ commonName={manifestData.signatureInfo?.common_name} issuer={manifestData.signatureInfo?.issuer} organizationalUnit={orgUnit} - country={country} + {country} {trustSource} /> {/if} diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte index 21159186c..9f9d060e7 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte @@ -22,6 +22,7 @@ if (issuer) params.set('o', issuer); if (organizationalUnit) params.set('ou', organizationalUnit); if (country) params.set('c', country); + return `https://spec.c2pa.org/conformance-explorer/?${params.toString()}`; })(); @@ -71,6 +72,7 @@ {#if showTooltip} (showTooltip = !showTooltip)} >
+ {@html $_('sidebar.verify.about.issuedby.tooltip')}
diff --git a/src/routes/verify/components/TreeView/TreeL1.svelte b/src/routes/verify/components/TreeView/TreeL1.svelte index 94e0e6902..f8d110f7a 100644 --- a/src/routes/verify/components/TreeView/TreeL1.svelte +++ b/src/routes/verify/components/TreeView/TreeL1.svelte @@ -32,7 +32,6 @@ $: removeL1 = transformScale === 0.125 ? true : false; $: scale = 0.5 / transformScale; $: L1margin = transformScale >= 0.25 ? 0.5 : transformScale / 0.25; - $: trustSource = $assetStore.trustSource;
; @@ -33,32 +33,11 @@ const mimeTypeCorrections = { 'image/dng': 'image/x-adobe-dng', }; -import { legacyToCrJson, type CrJson } from '$lib/crjson'; - -// Helper to extract all manifest labels that failed trust validation in a given crJSON store -function getUntrustedManifestLabels(store: CrJson): Set { - const untrustedLabels = new Set(); - const isTrustError = (s: any) => s.code.includes('signingCredential'); - - // 1. Check Root - const rootFailures = store.validationResults?.activeManifest?.failure || []; - const activeLabel = store.manifests[0]?.label; - if (activeLabel && rootFailures.some(isTrustError)) { - untrustedLabels.add(activeLabel); - } - - // 2. Check EVERY manifest in the store (Active + Ingredients) - for (const manifest of store.manifests) { - // Check the manifest object itself - const mFailures = manifest.validationResults?.failure || []; - const mStatuses = manifest.validationStatus || []; - if (mFailures.some(isTrustError) || mStatuses.some(isTrustError)) { - untrustedLabels.add(manifest.label); - } - } - - return untrustedLabels; -} +type ValidationStatus = { code: string }; +type TrustSource = 'official' | 'legacy' | 'none'; +type TrustedIngredient = { trust_source?: TrustSource; active_manifest?: string; validation_status?: ValidationStatus[] }; +type TrustedManifest = { trust_source?: TrustSource; ingredients?: TrustedIngredient[] }; +type ValidationDelta = { ingredientAssertionURI?: string; validationDeltas?: { failure?: ValidationStatus[] } }; export function createC2paReader(): C2paReaderStore { let dispose: () => void; @@ -112,7 +91,7 @@ export function createC2paReader(): C2paReaderStore { // PASS 1: Validate against Official Trust List const officialSettings = await getOfficialToolkitSettings(); - let reader = await sdk.reader.fromBlob(source.type || 'application/octet-stream', source, officialSettings); + const reader = await sdk.reader.fromBlob(source.type || 'application/octet-stream', source, officialSettings); if (!reader) { throw new Error('No C2PA manifest found in this file'); @@ -139,18 +118,17 @@ export function createC2paReader(): C2paReaderStore { reader.free(); currentReader = legacyReader; - const isTrustError = (s: any) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid'); - const isCryptoValid = (s: any) => s.code.includes('signingCredential.trusted') || s.code.includes('claimSignature.validated'); + const isTrustError = (s: ValidationStatus) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid'); // 4a. Tag the Active Manifest - const p1ActiveV3 = rawManifestStore.validation_results?.activeManifest?.failure || []; - const p1ActiveV2 = rawManifestStore.manifests?.[rawManifestStore.active_manifest || '']?.validation_status || []; + const p1ActiveV3 = (rawManifestStore.validation_results?.activeManifest?.failure || []) as ValidationStatus[]; + const p1ActiveV2 = (rawManifestStore.manifests?.[rawManifestStore.active_manifest || '']?.validation_status || []) as ValidationStatus[]; const wasActiveUntrusted = p1ActiveV3.some(isTrustError) || p1ActiveV2.some(isTrustError); const isFinalTrusted = finalStore.validation_state === 'Trusted'; if (finalStore.manifests && finalStore.active_manifest && finalStore.manifests[finalStore.active_manifest]) { - const activeMan = finalStore.manifests[finalStore.active_manifest]; - + const activeMan = finalStore.manifests[finalStore.active_manifest] as unknown as TrustedManifest; + // If the root is Trusted, default to official, then downgrade if Pass 1 failed. if (isFinalTrusted) { activeMan.trust_source = wasActiveUntrusted ? 'legacy' : 'official'; @@ -160,32 +138,32 @@ export function createC2paReader(): C2paReaderStore { } // 4b. Tag ALL Ingredients across the entire provenance tree - const p1Deltas = rawManifestStore.validation_results?.ingredientDeltas || []; - - Object.entries(finalStore.manifests || {}).forEach(([label, manifest]: [string, any]) => { + const p1Deltas = (rawManifestStore.validation_results?.ingredientDeltas || []) as ValidationDelta[]; + + (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { const p1Manifest = rawManifestStore.manifests?.[label]; - + if (manifest.ingredients && p1Manifest?.ingredients) { - manifest.ingredients.forEach((ingredient: any, index: number) => { + manifest.ingredients.forEach((ingredient, index) => { // ZERO TRUST DEFAULT: Unverified assets get no trust credentials ingredient.trust_source = 'none'; if (ingredient.active_manifest) { - const p1Ing = p1Manifest.ingredients[index]; + const p1Ing = p1Manifest.ingredients?.[index]; const subLabel = ingredient.active_manifest; const p1SubManifest = subLabel ? rawManifestStore.manifests?.[subLabel] : null; // 1. Check the Ingredient Pointer (V2 standard) - const p1V2_ing = p1Ing?.validation_status || []; + const p1V2_ing = (p1Ing?.validation_status || []) as ValidationStatus[]; // 2. Check the actual Sub-Manifest directly (V2 + V3 standards) // NOTE: In V3, the sub-manifest's own results are stored under .activeManifest - const p1V2_sub = p1SubManifest?.validation_status || []; - const p1V3_sub = p1SubManifest?.validation_results?.activeManifest?.failure || []; + const p1V2_sub = (p1SubManifest?.validation_status || []) as ValidationStatus[]; + const p1V3_sub = (p1SubManifest?.validation_results?.activeManifest?.failure || []) as ValidationStatus[]; // 3. Check the Root Deltas (V3 standard) - const p1Delta = p1Deltas.find((d: any) => - d.ingredientAssertionURI?.includes(label) && + const p1Delta = p1Deltas.find((d) => + d.ingredientAssertionURI?.includes(label) && d.ingredientAssertionURI?.includes('c2pa.ingredient') ); const p1V3_delta = p1Delta?.validationDeltas?.failure || []; @@ -212,12 +190,13 @@ export function createC2paReader(): C2paReaderStore { // Fallback: If root is Trusted, entire tree is official. const isTrusted = finalStore.validation_state === 'Trusted'; - Object.entries(finalStore.manifests || {}).forEach(([label, manifest]: [string, any]) => { + (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { if (label === finalStore.active_manifest) { manifest.trust_source = isTrusted ? 'official' : 'none'; } + if (manifest.ingredients) { - manifest.ingredients.forEach((ing: any) => { + manifest.ingredients.forEach((ing) => { ing.trust_source = (isTrusted && ing.active_manifest) ? 'official' : 'none'; }); } @@ -226,12 +205,13 @@ export function createC2paReader(): C2paReaderStore { } else { // Pass 1 had no trust issues (Could be Trusted or Hard Invalid) const isTrusted = finalStore.validation_state === 'Trusted'; - Object.entries(finalStore.manifests || {}).forEach(([label, manifest]: [string, any]) => { + (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { if (label === finalStore.active_manifest) { manifest.trust_source = isTrusted ? 'official' : 'none'; } + if (manifest.ingredients) { - manifest.ingredients.forEach((ing: any) => { + manifest.ingredients.forEach((ing) => { ing.trust_source = (isTrusted && ing.active_manifest) ? 'official' : 'none'; }); } @@ -239,7 +219,8 @@ export function createC2paReader(): C2paReaderStore { } const { assetMap, dispose: assetMapDisposer } = - await resultToAssetMap({ manifestStore: finalStore, source }); + await resultToAssetMap({ manifestStore: finalStore, source }); + dispose = () => { assetMapDisposer(); currentReader.free(); @@ -279,5 +260,6 @@ export function createC2paReader(): C2paReaderStore { async function hasLegacyCredentials(source: Blob | File): Promise { const legacySdk = await getLegacySdk(); const legacyResult = await legacySdk.processImage(source); + return legacyResult.exists; } From b59613da0ade04c511ca3abd54a6ba452f6bbac4 Mon Sep 17 00:00:00 2001 From: Andy Parsons Date: Fri, 1 May 2026 10:56:41 -0400 Subject: [PATCH 2/4] =?UTF-8?q?chore:=20fix=20all=20svelte-check=20TypeScr?= =?UTF-8?q?ipt=20errors=20(35=20=E2=86=92=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix type errors exposed after ESLint was fixed: replace string-indexed assertion lookups with array .find() by label, add proper type annotations for implicit-any parameters, and remove invalid c2pa module augmentation (package is @contentauth/c2pa-web). Co-Authored-By: Claude Sonnet 4.6 --- .../EmbeddedIcon/EmbeddedIcon.svelte | 10 - src/lib/asset.ts | 141 ++++--- src/lib/crjson.ts | 367 +++++++++++------- src/lib/exif.ts | 28 +- src/lib/selectors/autoDubInfo.ts | 35 +- src/lib/selectors/doNotTrain.ts | 28 +- src/lib/selectors/editsAndActivity.ts | 159 ++++++-- src/lib/selectors/generativeInfo.ts | 65 ++-- src/lib/selectors/producer.ts | 29 +- src/lib/selectors/reviewRatings.ts | 9 +- src/lib/selectors/socialAccounts.ts | 46 ++- src/lib/selectors/validationResult.ts | 38 +- src/lib/selectors/web3Info.ts | 11 +- src/lib/selectors/website.ts | 17 +- .../AboutSection/AboutSection.svelte | 34 +- src/routes/verify/stores/c2paReader.ts | 168 ++++++-- src/routes/verify/stores/verifyStore.ts | 6 +- 17 files changed, 816 insertions(+), 375 deletions(-) diff --git a/src/components/EmbeddedIcon/EmbeddedIcon.svelte b/src/components/EmbeddedIcon/EmbeddedIcon.svelte index 2c89a618b..12980b70f 100644 --- a/src/components/EmbeddedIcon/EmbeddedIcon.svelte +++ b/src/components/EmbeddedIcon/EmbeddedIcon.svelte @@ -4,27 +4,17 @@ diff --git a/src/lib/asset.ts b/src/lib/asset.ts index 512ef8135..659a22fe6 100644 --- a/src/lib/asset.ts +++ b/src/lib/asset.ts @@ -7,7 +7,10 @@ import type { ResourceRef as Thumbnail, } from '@contentauth/c2pa-web'; import { selectDoNotTrain } from './selectors/doNotTrain'; -import { selectEditsAndActivity, type TranslatedDictionaryCategory } from './selectors/editsAndActivity'; +import { + selectEditsAndActivity, + type TranslatedDictionaryCategory, +} from './selectors/editsAndActivity'; import { selectProducer } from './selectors/producer'; import { selectSocialAccounts } from './selectors/socialAccounts'; import debug from 'debug'; @@ -132,7 +135,7 @@ export async function resultToAssetMap({ }): Promise { const assetMap: AssetDataMap = {}; const disposers: (() => void)[] = []; - + const activeManifestLabel = manifestStore?.active_manifest ?? ''; const allLabels = Object.keys(manifestStore?.manifests ?? {}); const runtimeValidationStatuses = manifestStore?.validation_status @@ -143,7 +146,10 @@ export async function resultToAssetMap({ ) : {}; - dbg('Runtime validation statuses by manifest label', runtimeValidationStatuses); + dbg( + 'Runtime validation statuses by manifest label', + runtimeValidationStatuses, + ); const activeManifestValidationResults = manifestStore?.validation_results?.activeManifest; @@ -152,7 +158,7 @@ export async function resultToAssetMap({ runtimeValidationStatuses[activeManifestLabel] ?? []; const rootValidationResult = selectValidationResult( rootValidationStatuses, - activeManifestValidationResults, + activeManifestValidationResults ?? undefined, ); const { hasError, hasOtgp } = rootValidationResult ?? {}; const isManifest = source.type === MANIFEST_STORE_MIME_TYPE; @@ -173,10 +179,10 @@ export async function resultToAssetMap({ if (!isManifest && (!manifestStore || hasError || hasOtgp)) { // Fallback for raw files: render the original source file directly const url = URL.createObjectURL(source); - const thumbnail = await loadThumbnail( - source.type, - { url, dispose: () => URL.revokeObjectURL(url) }, - ); + const thumbnail = await loadThumbnail(source.type, { + url, + dispose: () => URL.revokeObjectURL(url), + }); if (thumbnail?.dispose) { disposers.push(thumbnail.dispose); @@ -224,14 +230,12 @@ export async function resultToAssetMap({ runtimeValidationStatuses: ManifestLabelValidationStatusMap, id: string, ): Promise { - const manifest = manifestStore.manifests?.[manifestStore.active_manifest || '']; + const manifest = + manifestStore.manifests?.[manifestStore.active_manifest || '']; if (!manifest) throw new Error('Active manifest not found'); // 0.17.x SDK dropped internal thumbnail generation. Pass undefined to skip WASM fetch. - let thumbnail = await loadThumbnail( - manifest.thumbnail?.format, - undefined - ); + let thumbnail = await loadThumbnail(manifest.thumbnail?.format, undefined); if ( !thumbnail.info && @@ -240,10 +244,10 @@ export async function resultToAssetMap({ ) { // Fallback for active manifest: render the original source file directly const url = URL.createObjectURL(source); - thumbnail = await loadThumbnail( - source.type, - { url, dispose: () => URL.revokeObjectURL(url) } - ); + thumbnail = await loadThumbnail(source.type, { + url, + dispose: () => URL.revokeObjectURL(url), + }); } const asset = { @@ -260,7 +264,8 @@ export async function resultToAssetMap({ manifestData: await getManifestData(manifest, rootValidationResult), dataType: null, validationResult: rootValidationResult, - trustSource: (manifest as Manifest & { trust_source?: string }).trust_source || 'none', + trustSource: ((manifest as Manifest & { trust_source?: string }) + .trust_source || 'none') as AssetData['trustSource'], }; if (thumbnail?.dispose) { @@ -279,7 +284,9 @@ export async function resultToAssetMap({ id: string, ): Promise { const ingredientManifestLabel = ingredient.active_manifest; - const ingredientManifest = ingredientManifestLabel ? manifestStore.manifests?.[ingredientManifestLabel] : null; + const ingredientManifest = ingredientManifestLabel + ? manifestStore.manifests?.[ingredientManifestLabel] + : null; // 0.17.x SDK dropped internal thumbnail generation. Skip WASM fetch for ingredients. const thumbnail = await loadThumbnail( @@ -292,7 +299,7 @@ export async function resultToAssetMap({ let validationResult = selectValidationResult( ingredient.validation_status || [], - activeManifestValidationResults, + activeManifestValidationResults ?? undefined, ); if (!validationResult.hasError && ingredientManifestLabel) { @@ -307,18 +314,20 @@ export async function resultToAssetMap({ title: ingredient.title ?? null, thumbnail: thumbnail.info, mimeType: ingredient.format || '', - children: (showChildren && ingredientManifest?.ingredients) - ? await processIngredients( - ingredientManifest.ingredients, - manifestStore, - runtimeValidationStatuses, - id, - ) - : [], + children: + showChildren && ingredientManifest?.ingredients + ? await processIngredients( + ingredientManifest.ingredients, + manifestStore, + runtimeValidationStatuses, + id, + ) + : [], manifestData: await getManifestData(ingredientManifest, validationResult), dataType: getIngredientDataType(ingredient), validationResult, - trustSource: (ingredient as Ingredient & { trust_source?: string })?.trust_source || 'none', + trustSource: ((ingredient as Ingredient & { trust_source?: string }) + ?.trust_source || 'none') as AssetData['trustSource'], }; if (thumbnail?.dispose) { @@ -332,15 +341,15 @@ export async function resultToAssetMap({ async function getManifestData( manifest: Manifest | null | undefined, - validationResult: ValidationStatusResult + validationResult: ValidationStatusResult, ): Promise { if (!manifest) { return null; } - type ClaimGeneratorEntry = { name?: string; version?: string | null; icon?: Thumbnail | null }; - - function formattedGeneratorInfo(claim_generator: ClaimGeneratorEntry): ClaimGeneratorEntry { + function formattedGeneratorInfo( + claim_generator: NonNullable[number], + ): NonNullable[number] { const version = claim_generator?.version; claim_generator.version = version?.replace(/\([^()]*\)/g, ''); @@ -359,7 +368,7 @@ export async function resultToAssetMap({ const claimGenerator: ClaimGeneratorDisplayInfo = { label: claimGeneratorLabel, - icon: claimGeneratorInfo?.icon ?? null, + icon: (claimGeneratorInfo?.icon as Thumbnail | null | undefined) ?? null, }; const safeSignatureInfo = manifest.signature_info @@ -371,9 +380,7 @@ export async function resultToAssetMap({ } return { - date: safeSignatureInfo?.time - ? new Date(safeSignatureInfo.time) - : null, + date: safeSignatureInfo?.time ? new Date(safeSignatureInfo.time) : null, claimGenerator, signatureInfo: safeSignatureInfo, producer: selectProducer(manifest)?.name ?? null, @@ -384,9 +391,18 @@ export async function resultToAssetMap({ ); if (editsAndActivity) { - const actionsAssertion = manifest.assertions?.['c2pa.actions'] as { data?: { metadata?: Record } } | undefined; + type ActionsData = { metadata?: Record }; + const assertionsArr = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const actionsEntry = assertionsArr.find( + (a) => a.label === 'c2pa.actions', + ); + const actionsAssertion = actionsEntry?.data as + | ActionsData + | undefined; const hasInference = - !!actionsAssertion?.data?.metadata?.['com.adobe.inference']; + !!actionsAssertion?.metadata?.['com.adobe.inference']; const filteredEditsAndActivity = editsAndActivity.filter( (value) => !!value.label, @@ -412,22 +428,42 @@ export async function resultToAssetMap({ isCapturedMedia: (() => { // 1. Must be a still image or audio file (Fallback to assuming true if SDK omits format) const format = manifest.format || 'image/jpeg'; - if (!format.startsWith('image/') && !format.startsWith('audio/')) return false; + if (!format.startsWith('image/') && !format.startsWith('audio/')) + return false; // 2. Must have exactly one action in the manifest history let actionsAssertion; if (manifest.assertions instanceof Map) { - actionsAssertion = manifest.assertions.get('c2pa.actions.v2')?.[0] || manifest.assertions.get('c2pa.actions')?.[0] || manifest.assertions.get('c2pa.actions.v2') || manifest.assertions.get('c2pa.actions'); + actionsAssertion = + manifest.assertions.get('c2pa.actions.v2')?.[0] || + manifest.assertions.get('c2pa.actions')?.[0] || + manifest.assertions.get('c2pa.actions.v2') || + manifest.assertions.get('c2pa.actions'); } else if (Array.isArray(manifest.assertions)) { - actionsAssertion = manifest.assertions.find((a: { label?: string }) => a.label === 'c2pa.actions.v2' || a.label === 'c2pa.actions'); + actionsAssertion = manifest.assertions.find( + (a: { label?: string }) => + a.label === 'c2pa.actions.v2' || a.label === 'c2pa.actions', + ); } else { - actionsAssertion = manifest.assertions?.['c2pa.actions.v2'] || manifest.assertions?.['c2pa.actions']; + actionsAssertion = + manifest.assertions?.['c2pa.actions.v2'] || + manifest.assertions?.['c2pa.actions']; } - type C2paActionItem = { action: string; digitalSourceType?: string; parameters?: { digitalSourceType?: string } }; - type AssertionValue = { data?: { actions?: C2paActionItem[] }; actions?: C2paActionItem[] }; - const actions = (actionsAssertion as AssertionValue)?.data?.actions || (actionsAssertion as AssertionValue)?.actions || []; + type C2paActionItem = { + action: string; + digitalSourceType?: string; + parameters?: { digitalSourceType?: string }; + }; + type AssertionValue = { + data?: { actions?: C2paActionItem[] }; + actions?: C2paActionItem[]; + }; + const actions = + (actionsAssertion as AssertionValue)?.data?.actions || + (actionsAssertion as AssertionValue)?.actions || + []; if (actions.length !== 1) return false; // 3. First and only action must be c2pa.created @@ -435,9 +471,16 @@ export async function resultToAssetMap({ if (action.action !== 'c2pa.created') return false; // 4. Digital source type must be a standard captured media URI (including computational) - const sourceType = action.digitalSourceType || action.parameters?.digitalSourceType || ''; - - return sourceType.includes('digitalCapture') || sourceType.includes('compositeCapture') || sourceType.includes('computationalCapture'); + const sourceType = + action.digitalSourceType || + action.parameters?.digitalSourceType || + ''; + + return ( + sourceType.includes('digitalCapture') || + sourceType.includes('compositeCapture') || + sourceType.includes('computationalCapture') + ); })(), }; } diff --git a/src/lib/crjson.ts b/src/lib/crjson.ts index 4eaaa8f29..9356c20dc 100644 --- a/src/lib/crjson.ts +++ b/src/lib/crjson.ts @@ -1,117 +1,135 @@ // Copyright 2021-2024 Adobe, Copyright 2026 The C2PA Contributors - /** Validation status entry in crJSON (code, optional url, explanation) */ export interface CrJsonValidationStatus { - code: string - url?: string - explanation?: string + code: string; + url?: string; + explanation?: string; } /** activeManifest block inside validationResults */ export interface CrJsonActiveManifestStatus { - success?: CrJsonValidationStatus[] - informational?: CrJsonValidationStatus[] - failure?: CrJsonValidationStatus[] + success?: CrJsonValidationStatus[]; + informational?: CrJsonValidationStatus[]; + failure?: CrJsonValidationStatus[]; } /** validationResults in crJSON (camelCase). Document-level has activeManifest; per-manifest has status codes directly. */ export interface CrJsonValidationResults { - activeManifest?: CrJsonActiveManifestStatus - success?: CrJsonValidationStatus[] - informational?: CrJsonValidationStatus[] - failure?: CrJsonValidationStatus[] - [key: string]: unknown + activeManifest?: CrJsonActiveManifestStatus; + success?: CrJsonValidationStatus[]; + informational?: CrJsonValidationStatus[]; + failure?: CrJsonValidationStatus[]; + [key: string]: unknown; } /** Single manifest entry in crJSON manifests array */ export interface CrJsonManifestEntry { - label: string - assertions: Record - claim?: Record - 'claim.v2'?: Record - signature?: Record - status?: Record - [key: string]: unknown + label: string; + assertions: Record; + claim?: Record; + 'claim.v2'?: Record; + signature?: Record; + status?: Record; + [key: string]: unknown; } /** Root crJSON structure from Reader.crjson() */ export interface CrJson { - '@context'?: Record - manifests: CrJsonManifestEntry[] - validationResults?: CrJsonValidationResults - jsonGenerator?: Record - [key: string]: unknown + '@context'?: Record; + manifests: CrJsonManifestEntry[]; + validationResults?: CrJsonValidationResults; + jsonGenerator?: Record; + [key: string]: unknown; } /** Assertion as list item: { label, data } from crJSON manifest.assertions object */ export interface CrJsonAssertionItem { - label: string - data: unknown + label: string; + data: unknown; } /** Ingredient derived from crJSON manifest.assertions (c2pa.ingredient entries) */ export interface CrJsonIngredientItem { - title?: string - format?: string - document_id?: unknown - instance_id?: unknown - relationship?: string - active_manifest?: string - [key: string]: unknown + title?: string; + format?: string; + document_id?: unknown; + instance_id?: unknown; + relationship?: string; + active_manifest?: string; + [key: string]: unknown; } /** Signature info read from crJSON manifest.signature */ export interface CrJsonSignatureInfo { - alg: string - common_name: string - organization?: string - issuer: string - time: string + alg: string; + common_name: string; + organization?: string; + issuer: string; + time: string; } /** Claim info read from crJSON manifest.claim or manifest['claim.v2'] */ export interface CrJsonClaimInfo { - claim_generator?: string - claim_generator_info: Array<{ name?: string; version?: string; [key: string]: unknown }> - instance_id?: string + claim_generator?: string; + claim_generator_info: Array<{ + name?: string; + version?: string; + [key: string]: unknown; + }>; + instance_id?: string; } /** Detect if parsed JSON is crJSON format */ export function isCrJson(obj: unknown): obj is CrJson { - const o = obj as Record + const o = obj as Record; - return Array.isArray(o?.manifests) && o.manifests.length > 0 && o['@context'] != null + return ( + Array.isArray(o?.manifests) && + o.manifests.length > 0 && + o['@context'] != null + ); } /** Read assertions as list from crJSON manifest.assertions (object → array of { label, data }) */ -export function getAssertionsList(m: CrJsonManifestEntry): CrJsonAssertionItem[] { - const assertions = m.assertions ?? {} +export function getAssertionsList( + m: CrJsonManifestEntry, +): CrJsonAssertionItem[] { + const assertions = m.assertions ?? {}; - return Object.entries(assertions).map(([label, data]) => ({ label, data })) + return Object.entries(assertions).map(([label, data]) => ({ label, data })); } /** Read ingredients from crJSON manifest.assertions (c2pa.ingredient and entries with document_id/instance_id) */ -export function getIngredientsFromManifest(m: CrJsonManifestEntry): CrJsonIngredientItem[] { - const assertions = m.assertions ?? {} - const out: CrJsonIngredientItem[] = [] +export function getIngredientsFromManifest( + m: CrJsonManifestEntry, +): CrJsonIngredientItem[] { + const assertions = m.assertions ?? {}; + const out: CrJsonIngredientItem[] = []; for (const [assertionLabel, data] of Object.entries(assertions)) { - const d = data as Record + const d = data as Record; - if (assertionLabel === 'c2pa.ingredient' || (d?.document_id != null && d?.instance_id != null)) { + if ( + assertionLabel === 'c2pa.ingredient' || + (d?.document_id != null && d?.instance_id != null) + ) { out.push({ title: (d.title ?? d.dc_title ?? assertionLabel) as string, format: (d.format ?? d.dc_format ?? '') as string, document_id: d.document_id, instance_id: d.instance_id, - relationship: (d.relationship ?? d['dc:relationship']) as string | undefined, - active_manifest: (d.active_manifest ?? d.activeManifest) as string | undefined - }) + relationship: (d.relationship ?? d['dc:relationship']) as + | string + | undefined, + active_manifest: (d.active_manifest ?? d.activeManifest) as + | string + | undefined, + }); } } - return out + return out; } /** @@ -119,78 +137,97 @@ export function getIngredientsFromManifest(m: CrJsonManifestEntry): CrJsonIngred * c2pa-rs crJSON uses DN component objects { CN, O, OU, L, ST, C }; extract string or format. */ function certFieldToString(value: unknown): string { - if (value == null) return '' - if (typeof value === 'string') return value - if (typeof value !== 'object' || Array.isArray(value)) return '' - const obj = value as Record + if (value == null) return ''; + if (typeof value === 'string') return value; + if (typeof value !== 'object' || Array.isArray(value)) return ''; + const obj = value as Record; // DN components: prefer CN for common name; for full display join key=value - const cn = obj.CN ?? obj.cn - if (cn != null && typeof cn === 'string') return cn - const parts: string[] = [] - const order = ['CN', 'O', 'OU', 'L', 'ST', 'C'] + const cn = obj.CN ?? obj.cn; + if (cn != null && typeof cn === 'string') return cn; + const parts: string[] = []; + const order = ['CN', 'O', 'OU', 'L', 'ST', 'C']; for (const key of order) { - const v = obj[key] ?? obj[key.toLowerCase()] - if (v != null && typeof v === 'string') parts.push(`${key}=${v}`) + const v = obj[key] ?? obj[key.toLowerCase()]; + if (v != null && typeof v === 'string') parts.push(`${key}=${v}`); } - if (parts.length > 0) return parts.join(', ') + if (parts.length > 0) return parts.join(', '); - return '' + return ''; } /** Read signature display info from crJSON manifest.signature */ -export function getSignatureInfo(m: CrJsonManifestEntry): CrJsonSignatureInfo | undefined { - const sig = m.signature as Record | undefined - if (!sig || typeof sig !== 'object') return undefined +export function getSignatureInfo( + m: CrJsonManifestEntry, +): CrJsonSignatureInfo | undefined { + const sig = m.signature as Record | undefined; + if (!sig || typeof sig !== 'object') return undefined; // crJSON from c2pa-rs: certificateInfo (camelCase), subject/issuer are DN objects { CN, O, ... } - const certInfo = (sig.certificateInfo ?? sig.certificate_info ?? {}) as Record - const tsInfo = (sig.timeStampInfo ?? sig.time_stamp_info ?? sig.timeStamp ?? {}) as Record - const alg = (sig.algorithm ?? sig.alg ?? '') as string - - const subjectObj = typeof certInfo.subject === 'object' && certInfo.subject !== null ? certInfo.subject as Record : {}; - + const certInfo = (sig.certificateInfo ?? + sig.certificate_info ?? + {}) as Record; + const tsInfo = (sig.timeStampInfo ?? + sig.time_stamp_info ?? + sig.timeStamp ?? + {}) as Record; + const alg = (sig.algorithm ?? sig.alg ?? '') as string; + + const subjectObj = + typeof certInfo.subject === 'object' && certInfo.subject !== null + ? (certInfo.subject as Record) + : {}; + const common_name = certFieldToString(certInfo.subject) || (typeof certInfo.common_name === 'string' ? certInfo.common_name : '') || - (typeof certInfo.commonName === 'string' ? certInfo.commonName : '') - + (typeof certInfo.commonName === 'string' ? certInfo.commonName : ''); + const organization = (subjectObj.O ?? subjectObj.o) as string | undefined; - const issuer = certFieldToString(certInfo.issuer) || (typeof certInfo.issuer === 'string' ? certInfo.issuer : '') - const timeRaw = tsInfo.timestamp ?? sig.time ?? sig.timestamp - const time = typeof timeRaw === 'string' ? timeRaw : '' - + const issuer = + certFieldToString(certInfo.issuer) || + (typeof certInfo.issuer === 'string' ? certInfo.issuer : ''); + const timeRaw = tsInfo.timestamp ?? sig.time ?? sig.timestamp; + const time = typeof timeRaw === 'string' ? timeRaw : ''; + // Return undefined if no meaningful signature data (avoids empty section) - if (!alg && !common_name && !issuer && !time) return undefined + if (!alg && !common_name && !issuer && !time) return undefined; - return { alg, common_name, organization, issuer, time } + return { alg, common_name, organization, issuer, time }; } /** Read claim info from crJSON manifest.claim or manifest['claim.v2'] */ export function getClaimInfo(m: CrJsonManifestEntry): CrJsonClaimInfo { - const claim = (m.claim ?? m['claim.v2']) as Record | undefined - const cgi = claim?.claim_generator_info + const claim = (m.claim ?? m['claim.v2']) as + | Record + | undefined; + const cgi = claim?.claim_generator_info; const cgiArray = Array.isArray(cgi) ? cgi : cgi != null ? [cgi] : claim?.claim_generator != null ? [{ name: String(claim.claim_generator) }] - : [] + : []; return { claim_generator: claim?.claim_generator as string | undefined, claim_generator_info: cgiArray as CrJsonClaimInfo['claim_generator_info'], - instance_id: (claim?.instanceID ?? claim?.instance_id) as string | undefined - } + instance_id: (claim?.instanceID ?? claim?.instance_id) as + | string + | undefined, + }; } /** Get assertion data by label from crJSON manifest.assertions */ -export function getAssertionDataByLabel(m: CrJsonManifestEntry, label: string): unknown { - const assertions = m.assertions ?? {} +export function getAssertionDataByLabel( + m: CrJsonManifestEntry, + label: string, +): unknown { + const assertions = m.assertions ?? {}; - return assertions[label] + return assertions[label]; } /** @@ -198,25 +235,50 @@ export function getAssertionDataByLabel(m: CrJsonManifestEntry, label: string): * - Document-level (legacy/SDK): report.validationResults.activeManifest * - Per-manifest (c2pa-rs crJSON): report.manifests[0].validationResults (status codes directly) */ -export function getActiveManifestValidationStatus(report: CrJson): CrJsonActiveManifestStatus | undefined { - const docLevel = report.validationResults?.activeManifest - - if (docLevel && (docLevel.success?.length ?? 0) + (docLevel.failure?.length ?? 0) + (docLevel.informational?.length ?? 0) > 0) { - return docLevel +export function getActiveManifestValidationStatus( + report: CrJson, +): CrJsonActiveManifestStatus | undefined { + const docLevel = report.validationResults?.activeManifest; + + if ( + docLevel && + (docLevel.success?.length ?? 0) + + (docLevel.failure?.length ?? 0) + + (docLevel.informational?.length ?? 0) > + 0 + ) { + return docLevel; } - const firstManifest = report.manifests?.[0] - const perManifest = firstManifest?.validationResults as CrJsonValidationResults | undefined - - if (perManifest && (perManifest.success?.length ?? 0) + (perManifest.failure?.length ?? 0) + (perManifest.informational?.length ?? 0) > 0) { + const firstManifest = report.manifests?.[0]; + const perManifest = firstManifest?.validationResults as + | CrJsonValidationResults + | undefined; + + if ( + perManifest && + (perManifest.success?.length ?? 0) + + (perManifest.failure?.length ?? 0) + + (perManifest.informational?.length ?? 0) > + 0 + ) { return { success: perManifest.success, informational: perManifest.informational, - failure: perManifest.failure - } + failure: perManifest.failure, + }; } - return docLevel ?? (perManifest ? { success: perManifest.success, informational: perManifest.informational, failure: perManifest.failure } : undefined) + return ( + docLevel ?? + (perManifest + ? { + success: perManifest.success, + informational: perManifest.informational, + failure: perManifest.failure, + } + : undefined) + ); } /** @@ -224,23 +286,28 @@ export function getActiveManifestValidationStatus(report: CrJson): CrJsonActiveM * Use only when receiving legacy format; native path is already crJSON. */ export function legacyToCrJson(legacy: Record): CrJson { - const manifestsObj = legacy.manifests as Record> | undefined - const activeLabel = legacy.active_manifest as string | undefined - const validationResults = (legacy.validation_results ?? legacy.validationResults) as CrJsonValidationResults | undefined + const manifestsObj = legacy.manifests as + | Record> + | undefined; + const activeLabel = legacy.active_manifest as string | undefined; + const validationResults = (legacy.validation_results ?? + legacy.validationResults) as CrJsonValidationResults | undefined; - const manifests: CrJsonManifestEntry[] = [] + const manifests: CrJsonManifestEntry[] = []; if (manifestsObj && typeof manifestsObj === 'object') { - const labels = Object.keys(manifestsObj) + const labels = Object.keys(manifestsObj); // Put active manifest first (crJSON convention) if (activeLabel && manifestsObj[activeLabel]) { - manifests.push(legacyManifestToCrJsonEntry(activeLabel, manifestsObj[activeLabel])) + manifests.push( + legacyManifestToCrJsonEntry(activeLabel, manifestsObj[activeLabel]), + ); } for (const label of labels) { if (label !== activeLabel && manifestsObj[label]) { - manifests.push(legacyManifestToCrJsonEntry(label, manifestsObj[label])) + manifests.push(legacyManifestToCrJsonEntry(label, manifestsObj[label])); } } } @@ -248,66 +315,82 @@ export function legacyToCrJson(legacy: Record): CrJson { const cr: CrJson = { '@context': { '@vocab': 'https://contentcredentials.org/crjson', - extras: 'https://contentcredentials.org/crjson/extras' + extras: 'https://contentcredentials.org/crjson/extras', }, - manifests - } + manifests, + }; if (validationResults && typeof validationResults === 'object') { - cr.validationResults = validationResults + cr.validationResults = validationResults; // Propagate into first manifest so per-manifest readers (and c2pa-rs-style crJSON) see it - const activeStatus = validationResults.activeManifest ?? validationResults + const activeStatus = validationResults.activeManifest ?? validationResults; - if (manifests.length > 0 && activeStatus && typeof activeStatus === 'object') { + if ( + manifests.length > 0 && + activeStatus && + typeof activeStatus === 'object' + ) { manifests[0].validationResults = { success: (activeStatus as CrJsonActiveManifestStatus).success, - informational: (activeStatus as CrJsonActiveManifestStatus).informational, - failure: (activeStatus as CrJsonActiveManifestStatus).failure - } + informational: (activeStatus as CrJsonActiveManifestStatus) + .informational, + failure: (activeStatus as CrJsonActiveManifestStatus).failure, + }; } } - return cr + return cr; } -function legacyManifestToCrJsonEntry(label: string, m: Record): CrJsonManifestEntry { - const assertionsArray = (m.assertions ?? []) as Array<{ label: string; data: unknown }> - const assertions: Record = {} +function legacyManifestToCrJsonEntry( + label: string, + m: Record, +): CrJsonManifestEntry { + const assertionsArray = (m.assertions ?? []) as Array<{ + label: string; + data: unknown; + }>; + const assertions: Record = {}; for (const a of assertionsArray) { - if (a?.label != null) assertions[a.label] = a.data + if (a?.label != null) assertions[a.label] = a.data; } - const claim = m.claim_generator_info != null || m.instance_id != null - ? { - claim_generator: m.claim_generator, - claim_generator_info: m.claim_generator_info, - instanceID: m.instanceID ?? m.instance_id - } - : undefined - const sig = m.signature_info as Record | undefined + const claim = + m.claim_generator_info != null || m.instance_id != null + ? { + claim_generator: m.claim_generator, + claim_generator_info: m.claim_generator_info, + instanceID: m.instanceID ?? m.instance_id, + } + : undefined; + const sig = m.signature_info as Record | undefined; const signature = sig ? { algorithm: sig.alg ?? sig.algorithm, certificateInfo: { subject: sig.common_name ?? sig.subject, - issuer: sig.issuer + issuer: sig.issuer, }, - timeStampInfo: sig.time ? { timestamp: sig.time } : undefined + timeStampInfo: sig.time ? { timestamp: sig.time } : undefined, } - : undefined - + : undefined; + // The original Conformance Tool implementation dropped validation data for ingredients. // We explicitly rescue both V2 and V3 validation states here. - const validationResults = m.validation_results ?? m.validationResults; - const validationStatus = m.validation_status ?? m.validationStatus; + const validationResults = (m.validation_results ?? m.validationResults) as + | CrJsonValidationResults + | undefined; + const validationStatus = (m.validation_status ?? m.validationStatus) as + | Record + | undefined; return { label, assertions, ...(claim && { claim: claim as Record }), ...(signature && { signature }), - ...(validationResults && { validationResults: validationResults as CrJsonValidationResults }), - ...(validationStatus && { validationStatus: validationStatus as Record }) - } + ...(validationResults != null && { validationResults }), + ...(validationStatus != null && { validationStatus }), + }; } diff --git a/src/lib/exif.ts b/src/lib/exif.ts index 5053375a3..bb788fd9a 100644 --- a/src/lib/exif.ts +++ b/src/lib/exif.ts @@ -28,11 +28,7 @@ export interface ExifTags { 'exif:offsettimeoriginal'?: string; } -declare module 'c2pa' { - interface ExtendedAssertions { - 'stds.exif': ExifTags; - } -} +// Module augmentation removed - 'c2pa' package not used directly; using '@contentauth/c2pa-web' function findExifValue(exif: ExifTags, locations: string[]) { return ( @@ -203,17 +199,23 @@ export function parseDateTime(exif: ExifTags): Date | null { } export function selectExif(manifest: Manifest): ExifSummary | null { - const assertion = manifest.assertions?.['stds.exif']; - const exif: ExifTags = (Array.isArray(assertion) ? assertion : [assertion]).reduce( - (acc, exif) => { - const caseInsensitiveData = mapKeys(exif?.data, (_, key) => { - return key.toLowerCase(); - }); + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const exifAssertions = assertions.filter((a) => a.label === 'stds.exif'); + const exif: ExifTags = exifAssertions.reduce( + (acc, assertionItem) => { + const caseInsensitiveData = mapKeys( + assertionItem?.data as Record, + (_, key) => { + return key.toLowerCase(); + }, + ); return merge({}, acc, caseInsensitiveData); }, - {}, - ); + {} as Record, + ) as ExifTags; if (Object.keys(exif).length > 0) { dbg('Got EXIF tags', exif); diff --git a/src/lib/selectors/autoDubInfo.ts b/src/lib/selectors/autoDubInfo.ts index 867b58e7b..d5b54fc83 100644 --- a/src/lib/selectors/autoDubInfo.ts +++ b/src/lib/selectors/autoDubInfo.ts @@ -13,38 +13,55 @@ export interface AutoDubInfo { translatedData: TranslatedActionDataParams | null; } +type ActionItem = { + action: string; + changes?: Array<{ + region?: Array<{ type: string; item?: { value: string } }>; + }>; + parameters?: unknown; +}; +type ActionAssertionData = { actions?: ActionItem[] }; + export function selectAutoDubInfo(manifest: Manifest): AutoDubInfo | null { - const actionAssertion = manifest.assertions?.['c2pa.actions.v2']; + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const actionAssertionEntry = assertions.find( + (a) => a.label === 'c2pa.actions.v2', + ); + const actionAssertion = actionAssertionEntry?.data as + | ActionAssertionData + | undefined; - if (!actionAssertion) { + if (!actionAssertion?.actions) { return null; } - const dubbedAction = actionAssertion.data.actions.find( + const dubbedAction = actionAssertion.actions.find( ({ action }) => action === 'c2pa.dubbed', ); - const translatedAction = actionAssertion.data.actions.find( + const translatedAction = actionAssertion.actions.find( ({ action }) => action === 'c2pa.translated', ); - const editedAction = actionAssertion.data.actions.find( + const editedAction = actionAssertion.actions.find( ({ action }) => action === 'c2pa.edited', ); if (dubbedAction) { const dubbedRegionOfInterest = dubbedAction.changes?.find( - (change) => !!change?.region, + (change: { region?: unknown }) => !!change?.region, )?.region; const dubbedIdentified = dubbedRegionOfInterest?.find( (region: Record) => region.type === 'identified', - )?.item.value; + )?.item?.value; const hasLipsRoi = dubbedIdentified === 'lips'; const editedRegionOfInterest = editedAction?.changes?.find( - (change) => !!change?.region, + (change: { region?: unknown }) => !!change?.region, )?.region; const editedIdentified = editedRegionOfInterest?.find( (region: Record) => region.type === 'identified', - )?.item.value; + )?.item?.value; const hasTranscriptRoi = editedIdentified === 'transcript'; const translatedLanguageData = translatedAction?.parameters ?? null; diff --git a/src/lib/selectors/doNotTrain.ts b/src/lib/selectors/doNotTrain.ts index 45c8ea95a..7dc11623c 100644 --- a/src/lib/selectors/doNotTrain.ts +++ b/src/lib/selectors/doNotTrain.ts @@ -3,23 +3,35 @@ import type { Manifest } from '@contentauth/c2pa-web'; export function selectDoNotTrain(manifest: Manifest): boolean { + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + // Check for the explicit do not train/mine assertion - const trainingAssertions = manifest.assertions?.['c2pa.training-mining']; + const trainingAssertionEntry = assertions.find( + (a) => a.label === 'c2pa.training-mining', + ); - if (trainingAssertions) { + if (trainingAssertionEntry) { type TrainingEntry = { use: string; c2pa_manifest: boolean | string }; - type TrainingMining = { data?: { entries?: TrainingEntry[] } }; - const entry = (trainingAssertions as TrainingMining)?.data?.entries?.find((e: TrainingEntry) => - e.use === 'notAllowed' && (e.c2pa_manifest === true || e.c2pa_manifest === 'true') + type TrainingMining = { entries?: TrainingEntry[] }; + const trainingData = trainingAssertionEntry.data as + | TrainingMining + | undefined; + const entry = trainingData?.entries?.find( + (e: TrainingEntry) => + e.use === 'notAllowed' && + (e.c2pa_manifest === true || e.c2pa_manifest === 'true'), ); return !!entry; } // Fallback: Check c2pa.actions for specific 'not_trained' markers - type ActionsAssertion = { data?: { actions?: Array<{ action: string }> } }; - const actionsAssertion = manifest.assertions?.['c2pa.actions'] as ActionsAssertion | undefined; - const actions = actionsAssertion?.data?.actions ?? []; + type ActionsAssertion = { actions?: Array<{ action: string }> }; + const actionsEntry = assertions.find((a) => a.label === 'c2pa.actions'); + const actionsAssertion = actionsEntry?.data as ActionsAssertion | undefined; + const actions = actionsAssertion?.actions ?? []; return actions.some((a) => a.action === 'c2pa.not_trained'); } diff --git a/src/lib/selectors/editsAndActivity.ts b/src/lib/selectors/editsAndActivity.ts index 862770d49..b06d1d655 100644 --- a/src/lib/selectors/editsAndActivity.ts +++ b/src/lib/selectors/editsAndActivity.ts @@ -12,22 +12,36 @@ export interface TranslatedDictionaryCategory { export async function selectEditsAndActivity( manifest: Manifest, // eslint-disable-next-line @typescript-eslint/no-unused-vars - _locale: string + _locale: string, ): Promise { // Handle Legacy SDK (Map), Native SDK (Array), and crJSON (Object) type ActionItem = { label?: string; action: string }; - type ActionsAssertion = { data?: { actions?: ActionItem[] }; actions?: ActionItem[] }; + type ActionsAssertion = { + data?: { actions?: ActionItem[] }; + actions?: ActionItem[]; + }; let actionsAssertion: ActionsAssertion | undefined; if (manifest.assertions instanceof Map) { - actionsAssertion = manifest.assertions.get('c2pa.actions.v2')?.[0] || manifest.assertions.get('c2pa.actions')?.[0] || manifest.assertions.get('c2pa.actions.v2') || manifest.assertions.get('c2pa.actions'); + actionsAssertion = + manifest.assertions.get('c2pa.actions.v2')?.[0] || + manifest.assertions.get('c2pa.actions')?.[0] || + manifest.assertions.get('c2pa.actions.v2') || + manifest.assertions.get('c2pa.actions'); } else if (Array.isArray(manifest.assertions)) { - actionsAssertion = manifest.assertions.find((a: { label?: string }) => a.label === 'c2pa.actions' || a.label === 'c2pa.actions.v2'); + const found = manifest.assertions.find( + (a: { label?: string }) => + a.label === 'c2pa.actions' || a.label === 'c2pa.actions.v2', + ); + actionsAssertion = found as ActionsAssertion | undefined; } else { - actionsAssertion = manifest.assertions?.['c2pa.actions.v2'] || manifest.assertions?.['c2pa.actions']; + actionsAssertion = + manifest.assertions?.['c2pa.actions.v2'] || + manifest.assertions?.['c2pa.actions']; } - const actions = actionsAssertion?.data?.actions || actionsAssertion?.actions || []; + const actions = + actionsAssertion?.data?.actions || actionsAssertion?.actions || []; const uniqueActionTypes = new Set(); actions.forEach((a) => uniqueActionTypes.add(a.action)); @@ -39,66 +53,161 @@ export async function selectEditsAndActivity( for (const action of uniqueActionTypes) { switch (action) { case 'c2pa.created': - results.push({ id: action, label: 'Created', description: 'The asset was created.', icon: `${baseUrl}/new-item-dark.svg` }); + results.push({ + id: action, + label: 'Created', + description: 'The asset was created.', + icon: `${baseUrl}/new-item-dark.svg`, + }); break; case 'c2pa.edited': - results.push({ id: action, label: 'Edited', description: 'The asset was modified.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Edited', + description: 'The asset was modified.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.color_adjustments': case 'c2pa.adjustedColor': - results.push({ id: action, label: 'Color adjustments', description: 'Changes made to tone, saturation, or exposure.', icon: `${baseUrl}/color-palette-dark.svg` }); + results.push({ + id: action, + label: 'Color adjustments', + description: 'Changes made to tone, saturation, or exposure.', + icon: `${baseUrl}/color-palette-dark.svg`, + }); break; case 'c2pa.cropped': - results.push({ id: action, label: 'Cropped', description: 'The asset was cropped.', icon: `${baseUrl}/crop-dark.svg` }); + results.push({ + id: action, + label: 'Cropped', + description: 'The asset was cropped.', + icon: `${baseUrl}/crop-dark.svg`, + }); break; case 'c2pa.filtered': - results.push({ id: action, label: 'Filtered', description: 'Appearance changed with filters and effects.', icon: `${baseUrl}/properties-dark.svg` }); + results.push({ + id: action, + label: 'Filtered', + description: 'Appearance changed with filters and effects.', + icon: `${baseUrl}/properties-dark.svg`, + }); break; case 'c2pa.resized': - results.push({ id: action, label: 'Resized', description: 'The asset was resized.', icon: `${baseUrl}/resize-dark.svg` }); + results.push({ + id: action, + label: 'Resized', + description: 'The asset was resized.', + icon: `${baseUrl}/resize-dark.svg`, + }); break; case 'c2pa.orientation': - results.push({ id: action, label: 'Orientation changed', description: 'The asset was rotated or flipped.', icon: `${baseUrl}/rotate-left-outline-dark.svg` }); + results.push({ + id: action, + label: 'Orientation changed', + description: 'The asset was rotated or flipped.', + icon: `${baseUrl}/rotate-left-outline-dark.svg`, + }); break; case 'c2pa.placed': - results.push({ id: action, label: 'Imported', description: 'Other assets were combined into this one.', icon: `${baseUrl}/import-dark.svg` }); + results.push({ + id: action, + label: 'Imported', + description: 'Other assets were combined into this one.', + icon: `${baseUrl}/import-dark.svg`, + }); break; case 'c2pa.drawing': - results.push({ id: action, label: 'Drawing', description: 'Digital painting or drawing was added.', icon: `${baseUrl}/draw-dark.svg` }); + results.push({ + id: action, + label: 'Drawing', + description: 'Digital painting or drawing was added.', + icon: `${baseUrl}/draw-dark.svg`, + }); break; case 'c2pa.converted': case 'c2pa.transcoded': - results.push({ id: action, label: 'Format converted', description: 'The file format was changed or transcoded.', icon: `${baseUrl}/export-dark.svg` }); + results.push({ + id: action, + label: 'Format converted', + description: 'The file format was changed or transcoded.', + icon: `${baseUrl}/export-dark.svg`, + }); break; case 'c2pa.deleted': case 'c2pa.removed': - results.push({ id: action, label: 'Content removed', description: 'Content was deleted or removed from the asset.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Content removed', + description: 'Content was deleted or removed from the asset.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.dubbed': case 'c2pa.translated': - results.push({ id: action, label: 'Audio/Text modified', description: 'Audio tracks or text were dubbed or translated.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Audio/Text modified', + description: 'Audio tracks or text were dubbed or translated.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.published': case 'c2pa.repackaged': - results.push({ id: action, label: 'Published', description: 'The asset was published or repackaged.', icon: `${baseUrl}/export-dark.svg` }); + results.push({ + id: action, + label: 'Published', + description: 'The asset was published or repackaged.', + icon: `${baseUrl}/export-dark.svg`, + }); break; case 'c2pa.watermarked': - results.push({ id: action, label: 'Watermarked', description: 'A watermark was added.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Watermarked', + description: 'A watermark was added.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.redacted': - results.push({ id: action, label: 'Redacted', description: 'Information was redacted from the manifest.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Redacted', + description: 'Information was redacted from the manifest.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.edited.metadata': - results.push({ id: action, label: 'Metadata changed', description: 'Metadata was edited.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Metadata changed', + description: 'Metadata was edited.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; case 'c2pa.opened': - results.push({ id: action, label: 'Opened', description: 'Opened a pre-existing file.', icon: `${baseUrl}/folder-open-outline-dark.svg` }); + results.push({ + id: action, + label: 'Opened', + description: 'Opened a pre-existing file.', + icon: `${baseUrl}/folder-open-outline-dark.svg`, + }); break; case 'c2pa.saved': - results.push({ id: action, label: 'Saved', description: 'Saved the file.', icon: `${baseUrl}/export-dark.svg` }); + results.push({ + id: action, + label: 'Saved', + description: 'Saved the file.', + icon: `${baseUrl}/export-dark.svg`, + }); break; case 'c2pa.unknown': - results.push({ id: action, label: 'Unknown action', description: 'An unknown edit or activity occurred.', icon: `${baseUrl}/actions-dark.svg` }); + results.push({ + id: action, + label: 'Unknown action', + description: 'An unknown edit or activity occurred.', + icon: `${baseUrl}/actions-dark.svg`, + }); break; } } diff --git a/src/lib/selectors/generativeInfo.ts b/src/lib/selectors/generativeInfo.ts index 12f65f1a0..ce18087ce 100644 --- a/src/lib/selectors/generativeInfo.ts +++ b/src/lib/selectors/generativeInfo.ts @@ -1,10 +1,6 @@ // Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors -import type { - DataType, - Ingredient, - Manifest, -} from '@contentauth/c2pa-web'; +import type { AssetType, Ingredient, Manifest } from '@contentauth/c2pa-web'; interface SdkGenerativeInfo { softwareAgent: string; @@ -22,29 +18,37 @@ type GenActionItem = { type GenActionsAssertion = { data?: { actions?: GenActionItem[] } }; function sdkSelectGenerativeInfo(manifest: Manifest): SdkGenerativeInfo[] { - // Handle both native SDK array structures and crJSON maps - const isArray = Array.isArray(manifest.assertions); - const actionsAssertion = isArray - ? manifest.assertions.find((a: { label?: string }) => a.label === 'c2pa.actions' || a.label === 'c2pa.actions.v2') - : (manifest.assertions?.['c2pa.actions.v2'] || manifest.assertions?.['c2pa.actions']); + // Handle native SDK array structure (assertions is always ManifestAssertion[]) + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const actionsAssertion = assertions.find( + (a: { label?: string }) => + a.label === 'c2pa.actions' || a.label === 'c2pa.actions.v2', + ); - const actions = (actionsAssertion as GenActionsAssertion)?.data?.actions || []; + const actions = + (actionsAssertion as GenActionsAssertion)?.data?.actions || []; return actions .filter((a) => { // For created/edited actions, inspect the IPTC digitalSourceType for AI definitions - const sourceType = a.digitalSourceType || a.parameters?.digitalSourceType || ''; + const sourceType = + a.digitalSourceType || a.parameters?.digitalSourceType || ''; return sourceType.toLowerCase().includes('algorithmicmedia'); }) .map((a) => { const rawType = a.digitalSourceType || a.parameters?.digitalSourceType; // The UI expects the IPTC slug, not the full absolute URI - const typeSlug = typeof rawType === 'string' ? (rawType.split('/').pop() ?? 'legacy') : 'legacy'; + const typeSlug = + typeof rawType === 'string' + ? (rawType.split('/').pop() ?? 'legacy') + : 'legacy'; return { softwareAgent: a.softwareAgent || 'Unknown', - type: typeSlug + type: typeSlug, }; }); } @@ -52,7 +56,7 @@ function sdkSelectGenerativeInfo(manifest: Manifest): SdkGenerativeInfo[] { import { filter, flow, uniqBy } from 'lodash/fp'; import startsWith from 'lodash/startsWith'; -type SoftwareAgent = SdkGenerativeInfo['softwareAgent']; +export type SoftwareAgent = string | { name: string; version?: string }; export interface GenerativeInfo { softwareAgents: SoftwareAgent[]; @@ -62,21 +66,21 @@ export interface GenerativeInfo { export interface CustomModel { name: string; - dataTypes: DataType[]; + dataTypes: AssetType[]; } export function selectGenerativeSoftwareAgents( generativeInfo: SdkGenerativeInfo[], ): SoftwareAgent[] { - const softwareAgents: SoftwareAgent[] = generativeInfo.map((assertion) => { + const softwareAgents: string[] = generativeInfo.map((assertion) => { return assertion?.softwareAgent; }); // if there are undefined software agents remove them from the array - return flow<[SoftwareAgent[]], SoftwareAgent[], SoftwareAgent[]>( - filter((x) => !!x?.name || x), - uniqBy((x) => x?.name || x), - )(softwareAgents); + return flow<[string[]], string[], string[]>( + filter((x: string) => !!x), + uniqBy((x: string) => x), + )(softwareAgents) as SoftwareAgent[]; } export function selectGenerativeType(generativeInfo: SdkGenerativeInfo[]) { @@ -92,22 +96,25 @@ export function selectGenerativeType(generativeInfo: SdkGenerativeInfo[]) { export function selectModelsFromIngredient(ingredient: Ingredient) { return ( - ingredient.dataTypes?.filter((dataType: { type: string }) => + ingredient.data_types?.filter((dataType: { type: string }) => startsWith('c2pa.types.model', dataType.type), ) ?? [] ); } export function selectCustomModels(manifest: Manifest): CustomModel[] { - return (manifest.ingredients || []).reduce((acc, ingredient) => { - const dataTypes = selectModelsFromIngredient(ingredient); + return (manifest.ingredients || []).reduce( + (acc, ingredient) => { + const dataTypes = selectModelsFromIngredient(ingredient); - if (dataTypes.length > 0) { - return [...acc, { name: ingredient.title, dataTypes } as CustomModel]; - } + if (dataTypes.length > 0) { + return [...acc, { name: ingredient.title, dataTypes } as CustomModel]; + } - return acc; - }, []); + return acc; + }, + [], + ); } export function selectGenerativeInfo(manifest: Manifest) { diff --git a/src/lib/selectors/producer.ts b/src/lib/selectors/producer.ts index ec8f737cd..3284011ec 100644 --- a/src/lib/selectors/producer.ts +++ b/src/lib/selectors/producer.ts @@ -7,15 +7,31 @@ interface ProducerInfo { url?: string; } -type CreativeWorkAssertion = { data?: { author?: { name?: string; url?: string; sameAs?: string | string[] } | Array<{ name?: string; url?: string }> } }; +type CreativeWorkAssertion = { + data?: { + author?: + | { name?: string; url?: string; sameAs?: string | string[] } + | Array<{ name?: string; url?: string }>; + }; +}; type XmpAssertion = { data?: { 'dc:creator'?: string | string[] } }; export function selectProducer(manifest: Manifest): ProducerInfo | null { + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + // 1. Check CreativeWork schema - const creativeWork = (manifest.assertions?.['stds.schema-org.CreativeWork'] as CreativeWorkAssertion)?.data; + const creativeWorkEntry = assertions.find( + (a) => a.label === 'stds.schema-org.CreativeWork', + ); + const creativeWork = + (creativeWorkEntry?.data as CreativeWorkAssertion['data']) ?? undefined; if (creativeWork?.author) { - const author = Array.isArray(creativeWork.author) ? creativeWork.author[0] : creativeWork.author; + const author = Array.isArray(creativeWork.author) + ? creativeWork.author[0] + : creativeWork.author; if (author?.name) { return { name: author.name, url: author.url }; @@ -23,10 +39,13 @@ export function selectProducer(manifest: Manifest): ProducerInfo | null { } // 2. Check XMP producer/creator - const xmp = (manifest.assertions?.['stds.xmp'] as XmpAssertion)?.data; + const xmpEntry = assertions.find((a) => a.label === 'stds.xmp'); + const xmp = (xmpEntry?.data as XmpAssertion['data']) ?? undefined; if (xmp?.['dc:creator']) { - const creator = Array.isArray(xmp['dc:creator']) ? xmp['dc:creator'][0] : xmp['dc:creator']; + const creator = Array.isArray(xmp['dc:creator']) + ? xmp['dc:creator'][0] + : xmp['dc:creator']; return { name: creator }; } diff --git a/src/lib/selectors/reviewRatings.ts b/src/lib/selectors/reviewRatings.ts index b95d65cb3..e97acfb48 100644 --- a/src/lib/selectors/reviewRatings.ts +++ b/src/lib/selectors/reviewRatings.ts @@ -13,9 +13,14 @@ export function selectReviewRatings(manifest: Manifest) { }, [], ); - type ActionsReviewAssertion = { data?: { metadata?: { reviewRatings?: ReviewRating[] } } }; + type ActionsReviewData = { metadata?: { reviewRatings?: ReviewRating[] } }; + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const actionsEntry = assertions.find((a) => a.label === 'c2pa.actions'); const actionRatings = - (manifest.assertions?.['c2pa.actions'] as ActionsReviewAssertion)?.data?.metadata?.reviewRatings ?? []; + (actionsEntry?.data as ActionsReviewData | undefined)?.metadata + ?.reviewRatings ?? []; const reviewRatings = [...ingredientRatings, ...actionRatings]; return { diff --git a/src/lib/selectors/socialAccounts.ts b/src/lib/selectors/socialAccounts.ts index b4ff40947..f69a6b19f 100644 --- a/src/lib/selectors/socialAccounts.ts +++ b/src/lib/selectors/socialAccounts.ts @@ -14,10 +14,15 @@ export function selectSocialAccounts(manifest: Manifest): SocialAccount[] { // Look through verified credentials if present const credentials = manifest.credentials || []; - + for (const cred of credentials) { // Simplified mapping logic for standard social media VC schemas - const vcData = cred.credentialSubject || {}; + const credRecord = cred as Record; + type VcData = { + id?: string; + account?: { service?: string; identifier?: string }; + }; + const vcData = (credRecord.credentialSubject as VcData) || {}; if (vcData?.account?.service && vcData?.account?.identifier) { accounts.push({ @@ -30,21 +35,42 @@ export function selectSocialAccounts(manifest: Manifest): SocialAccount[] { } // Also check standard CreativeWork assertions for "sameAs" social URLs - type CreativeWorkAssertion = { data?: { author?: { sameAs?: string | string[] } } }; - const creativeWork = (manifest.assertions?.['stds.schema-org.CreativeWork'] as CreativeWorkAssertion)?.data; + type CreativeWorkData = { author?: { sameAs?: string | string[] } }; + const assertionsArr = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const creativeWorkEntry = assertionsArr.find( + (a) => a.label === 'stds.schema-org.CreativeWork', + ); + const creativeWork = creativeWorkEntry?.data as CreativeWorkData | undefined; if (creativeWork?.author?.sameAs) { - const urls = Array.isArray(creativeWork.author.sameAs) - ? creativeWork.author.sameAs + const urls = Array.isArray(creativeWork.author.sameAs) + ? creativeWork.author.sameAs : [creativeWork.author.sameAs]; - + for (const url of urls) { if (url.includes('twitter.com') || url.includes('x.com')) { - accounts.push({ '@id': url, '@type': 'Organization', name: url.split('/').pop() || url, identifier: 'twitter' }); + accounts.push({ + '@id': url, + '@type': 'Organization', + name: url.split('/').pop() || url, + identifier: 'twitter', + }); } else if (url.includes('instagram.com')) { - accounts.push({ '@id': url, '@type': 'Organization', name: url.split('/').pop() || url, identifier: 'instagram' }); + accounts.push({ + '@id': url, + '@type': 'Organization', + name: url.split('/').pop() || url, + identifier: 'instagram', + }); } else if (url.includes('linkedin.com')) { - accounts.push({ '@id': url, '@type': 'Organization', name: url.split('/').pop() || url, identifier: 'linkedin' }); + accounts.push({ + '@id': url, + '@type': 'Organization', + name: url.split('/').pop() || url, + identifier: 'linkedin', + }); } } } diff --git a/src/lib/selectors/validationResult.ts b/src/lib/selectors/validationResult.ts index 25d69d80f..933750c97 100644 --- a/src/lib/selectors/validationResult.ts +++ b/src/lib/selectors/validationResult.ts @@ -3,9 +3,12 @@ import type { ManifestStore } from '@contentauth/c2pa-web'; import { difference } from 'lodash'; -export type ValidationStatus = ManifestStore['validation_status'][0]; -export type ValidationResults = - ManifestStore['validation_results']['activeManifest']; +export type ValidationStatus = NonNullable< + ManifestStore['validation_status'] +>[number]; +export type ValidationResults = NonNullable< + NonNullable['activeManifest'] +>; export type ValidationStatusCode = 'valid' | 'invalid' | 'unrecognized'; export type ValidationStatusResult = ReturnType; @@ -123,7 +126,7 @@ export function selectValidationResult( // Combine V2 failures (from validationStatus) and V3 failures (from validationResults) const v3Failures = validationResults?.failure || []; const v2Failures = validationStatus.filter( - (status) => !SUCCESS_CODES.includes(status.code) + (status) => !SUCCESS_CODES.includes(status.code), ); const allFailures = [...v3Failures, ...v2Failures]; @@ -140,9 +143,10 @@ export function selectValidationResult( const allCodes = [...allV3Codes, ...validationStatus]; const hasUntrustedTimestamp = allCodes.some( - c => c.code.toLowerCase().includes('timestamp') - && !c.code.toLowerCase().includes('timestamp.trusted') - && !c.code.toLowerCase().includes('timestamp.validated') + (c) => + c.code.toLowerCase().includes('timestamp') && + !c.code.toLowerCase().includes('timestamp.trusted') && + !c.code.toLowerCase().includes('timestamp.validated'), ); // Determine the specific types of failures present. @@ -151,16 +155,20 @@ export function selectValidationResult( // because the cert is untrusted); treat it the same way when both are present. // - Timestamp codes: an untrusted/mismatched timestamp suppresses the date display but must // not downgrade the badge from valid to invalid. - const isTimestampCode = (code: string) => code.toLowerCase().startsWith('timestamp'); - const hasUntrusted = allFailures.some(f => f.code === UNTRUSTED_SIGNER_ERROR_CODE); - const hasOtgp = allFailures.some(f => f.code === OTGP_ERROR_CODE); + const isTimestampCode = (code: string) => + code.toLowerCase().startsWith('timestamp'); + const hasUntrusted = allFailures.some( + (f) => f.code === UNTRUSTED_SIGNER_ERROR_CODE, + ); + const hasOtgp = allFailures.some((f) => f.code === OTGP_ERROR_CODE); const hasOtherErrors = allFailures.some( - f => f.code !== UNTRUSTED_SIGNER_ERROR_CODE - && f.code !== OTGP_ERROR_CODE - && !isTimestampCode(f.code) + (f) => + f.code !== UNTRUSTED_SIGNER_ERROR_CODE && + f.code !== OTGP_ERROR_CODE && + !isTimestampCode(f.code) && // general.error is a signature-validation side-effect of an untrusted cert; only count // it as a hard error when signingCredential.untrusted is absent. - && !(f.code === GENERAL_ERROR_CODE && hasUntrusted) + !(f.code === GENERAL_ERROR_CODE && hasUntrusted), ); let statusCode: ValidationStatusCode = 'valid'; @@ -168,7 +176,7 @@ export function selectValidationResult( // If there are explicit errors (other than just being untrusted), it's invalid (Red) if (hasOtherErrors || hasOtgp) { statusCode = 'invalid'; - } + } // If the only issue is an untrusted signer, it's unrecognized (Orange) else if (hasUntrusted) { statusCode = 'unrecognized'; diff --git a/src/lib/selectors/web3Info.ts b/src/lib/selectors/web3Info.ts index 7527924a9..0e2de0e6c 100644 --- a/src/lib/selectors/web3Info.ts +++ b/src/lib/selectors/web3Info.ts @@ -17,10 +17,17 @@ declare module '@contentauth/c2pa-web' { } } -type CryptoAddressAssertion = { data?: Record }; +type CryptoAddressData = Record; export function selectWeb3(manifest: Manifest): [string, string[]][] { - const cryptoEntries = (manifest.assertions?.['adobe.crypto.addresses'] as CryptoAddressAssertion)?.data ?? {}; + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const cryptoEntry = assertions.find( + (a) => a.label === 'adobe.crypto.addresses', + ); + const cryptoEntries = + (cryptoEntry?.data as CryptoAddressData | undefined) ?? {}; return (Object.entries(cryptoEntries) as [string, string[]][]).filter( ([type, [address]]) => address && ['solana', 'ethereum'].includes(type), diff --git a/src/lib/selectors/website.ts b/src/lib/selectors/website.ts index 8119bee9e..2fe18c19f 100644 --- a/src/lib/selectors/website.ts +++ b/src/lib/selectors/website.ts @@ -17,13 +17,22 @@ declare module '@contentauth/c2pa-web' { } } -type AssetRefAssertion = { data?: { references?: Array<{ reference?: { uri?: string } }> } }; -type CreativeWorkAssertion = { data?: { url?: string } }; +type AssetRefData = { references?: Array<{ reference?: { uri?: string } }> }; +type CreativeWorkData = { url?: string }; export function selectWebsite(manifest: Manifest): string | null { + const assertions = Array.isArray(manifest.assertions) + ? manifest.assertions + : []; + const assetRefEntry = assertions.find((a) => a.label === 'c2pa.asset-ref'); + const creativeWorkEntry = assertions.find( + (a) => a.label === 'stds.schema-org.CreativeWork', + ); + const site = - (manifest.assertions?.['c2pa.asset-ref'] as AssetRefAssertion)?.data?.references?.[0]?.reference?.uri ?? - (manifest.assertions?.['stds.schema-org.CreativeWork'] as CreativeWorkAssertion)?.data?.url; + (assetRefEntry?.data as AssetRefData | undefined)?.references?.[0] + ?.reference?.uri ?? + (creativeWorkEntry?.data as CreativeWorkData | undefined)?.url; return site && isSecureUrl(site) ? site : null; } diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte index de003cbf5..a1ec784a9 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte @@ -12,10 +12,29 @@ export let trustSource: 'official' | 'legacy' | 'none' = 'none'; // Extract extended X.509 fields (Catching various Rust/JS SDK naming conventions) - type ExtendedSigInfo = { organization_unit?: string; organizational_unit?: string; organizationUnit?: string; org_unit?: string; ou?: string; country?: string; country_name?: string; countryName?: string; c?: string }; + type ExtendedSigInfo = { + organization_unit?: string; + organizational_unit?: string; + organizationUnit?: string; + org_unit?: string; + ou?: string; + country?: string; + country_name?: string; + countryName?: string; + c?: string; + }; $: sigInfo = manifestData.signatureInfo as ExtendedSigInfo; - $: orgUnit = sigInfo?.organization_unit || sigInfo?.organizational_unit || sigInfo?.organizationUnit || sigInfo?.org_unit || sigInfo?.ou; - $: country = sigInfo?.country || sigInfo?.country_name || sigInfo?.countryName || sigInfo?.c; + $: orgUnit = + sigInfo?.organization_unit || + sigInfo?.organizational_unit || + sigInfo?.organizationUnit || + sigInfo?.org_unit || + sigInfo?.ou; + $: country = + sigInfo?.country || + sigInfo?.country_name || + sigInfo?.countryName || + sigInfo?.c; @@ -23,13 +42,12 @@ {$_('sidebar.verify.about')} {#if manifestData.signatureInfo?.common_name || manifestData.signatureInfo?.issuer} - + {trustSource} /> {/if} {#if manifestData.date} diff --git a/src/routes/verify/stores/c2paReader.ts b/src/routes/verify/stores/c2paReader.ts index 040fad77f..bddfba1b8 100644 --- a/src/routes/verify/stores/c2paReader.ts +++ b/src/routes/verify/stores/c2paReader.ts @@ -1,7 +1,12 @@ // Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors import { resultToAssetMap, type AssetDataMap } from '$lib/asset'; -import { getLegacySdk, getSdk, getOfficialToolkitSettings, getLegacyToolkitSettings } from '$lib/sdk'; +import { + getLegacySdk, + getSdk, + getOfficialToolkitSettings, + getLegacyToolkitSettings, +} from '$lib/sdk'; import type { Loadable } from '$lib/types'; import { somethingWentWrong, @@ -35,9 +40,19 @@ const mimeTypeCorrections = { type ValidationStatus = { code: string }; type TrustSource = 'official' | 'legacy' | 'none'; -type TrustedIngredient = { trust_source?: TrustSource; active_manifest?: string; validation_status?: ValidationStatus[] }; -type TrustedManifest = { trust_source?: TrustSource; ingredients?: TrustedIngredient[] }; -type ValidationDelta = { ingredientAssertionURI?: string; validationDeltas?: { failure?: ValidationStatus[] } }; +type TrustedIngredient = { + trust_source?: TrustSource; + active_manifest?: string; + validation_status?: ValidationStatus[]; +}; +type TrustedManifest = { + trust_source?: TrustSource; + ingredients?: TrustedIngredient[]; +}; +type ValidationDelta = { + ingredientAssertionURI?: string; + validationDeltas?: { failure?: ValidationStatus[] }; +}; export function createC2paReader(): C2paReaderStore { let dispose: () => void; @@ -91,7 +106,11 @@ export function createC2paReader(): C2paReaderStore { // PASS 1: Validate against Official Trust List const officialSettings = await getOfficialToolkitSettings(); - const reader = await sdk.reader.fromBlob(source.type || 'application/octet-stream', source, officialSettings); + const reader = await sdk.reader.fromBlob( + source.type || 'application/octet-stream', + source, + officialSettings, + ); if (!reader) { throw new Error('No C2PA manifest found in this file'); @@ -108,39 +127,71 @@ export function createC2paReader(): C2paReaderStore { // 2. Run Pass 2 ONLY if the official pass wasn't perfectly trusted if (needsLegacyPass) { const legacySettings = await getLegacyToolkitSettings(); - const legacyReader = await sdk.reader.fromBlob(source.type || 'application/octet-stream', source, legacySettings); + const legacyReaderOrNull = await sdk.reader.fromBlob( + source.type || 'application/octet-stream', + source, + legacySettings, + ); + + if (!legacyReaderOrNull) { + throw new Error('Legacy reader returned null'); + } + + const legacyReader = legacyReaderOrNull; const legacyStore = await legacyReader.manifestStore(); // 3. ONLY ADOPT Pass 2 if it actually solved the problem (State is now Trusted/Valid) - if (legacyStore.validation_state === 'Trusted' || legacyStore.validation_state === 'Valid') { + if ( + legacyStore.validation_state === 'Trusted' || + legacyStore.validation_state === 'Valid' + ) { finalStore = legacyStore; - + reader.free(); currentReader = legacyReader; - const isTrustError = (s: ValidationStatus) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid'); + const isTrustError = (s: ValidationStatus) => + s.code.includes('signingCredential.untrusted') || + s.code.includes('signingCredential.invalid'); // 4a. Tag the Active Manifest - const p1ActiveV3 = (rawManifestStore.validation_results?.activeManifest?.failure || []) as ValidationStatus[]; - const p1ActiveV2 = (rawManifestStore.manifests?.[rawManifestStore.active_manifest || '']?.validation_status || []) as ValidationStatus[]; - const wasActiveUntrusted = p1ActiveV3.some(isTrustError) || p1ActiveV2.some(isTrustError); + const p1ActiveV3 = (rawManifestStore.validation_results + ?.activeManifest?.failure || []) as ValidationStatus[]; + const p1ActiveV2 = (rawManifestStore.manifests?.[ + rawManifestStore.active_manifest || '' + ]?.validation_status || []) as ValidationStatus[]; + const wasActiveUntrusted = + p1ActiveV3.some(isTrustError) || p1ActiveV2.some(isTrustError); const isFinalTrusted = finalStore.validation_state === 'Trusted'; - - if (finalStore.manifests && finalStore.active_manifest && finalStore.manifests[finalStore.active_manifest]) { - const activeMan = finalStore.manifests[finalStore.active_manifest] as unknown as TrustedManifest; + + if ( + finalStore.manifests && + finalStore.active_manifest && + finalStore.manifests[finalStore.active_manifest] + ) { + const activeMan = finalStore.manifests[ + finalStore.active_manifest + ] as unknown as TrustedManifest; // If the root is Trusted, default to official, then downgrade if Pass 1 failed. if (isFinalTrusted) { - activeMan.trust_source = wasActiveUntrusted ? 'legacy' : 'official'; + activeMan.trust_source = wasActiveUntrusted + ? 'legacy' + : 'official'; } else { activeMan.trust_source = 'none'; } } // 4b. Tag ALL Ingredients across the entire provenance tree - const p1Deltas = (rawManifestStore.validation_results?.ingredientDeltas || []) as ValidationDelta[]; - - (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { + const p1Deltas = (rawManifestStore.validation_results + ?.ingredientDeltas || []) as ValidationDelta[]; + + ( + Object.entries(finalStore.manifests || {}) as Array< + [string, TrustedManifest] + > + ).forEach(([label, manifest]) => { const p1Manifest = rawManifestStore.manifests?.[label]; if (manifest.ingredients && p1Manifest?.ingredients) { @@ -151,53 +202,73 @@ export function createC2paReader(): C2paReaderStore { if (ingredient.active_manifest) { const p1Ing = p1Manifest.ingredients?.[index]; const subLabel = ingredient.active_manifest; - const p1SubManifest = subLabel ? rawManifestStore.manifests?.[subLabel] : null; + const p1SubManifest = subLabel + ? rawManifestStore.manifests?.[subLabel] + : null; // 1. Check the Ingredient Pointer (V2 standard) - const p1V2_ing = (p1Ing?.validation_status || []) as ValidationStatus[]; + const p1V2_ing = (p1Ing?.validation_status || + []) as ValidationStatus[]; // 2. Check the actual Sub-Manifest directly (V2 + V3 standards) // NOTE: In V3, the sub-manifest's own results are stored under .activeManifest - const p1V2_sub = (p1SubManifest?.validation_status || []) as ValidationStatus[]; - const p1V3_sub = (p1SubManifest?.validation_results?.activeManifest?.failure || []) as ValidationStatus[]; + const p1V2_sub = (p1SubManifest?.validation_status || + []) as ValidationStatus[]; + type ManifestValidationResults = { + activeManifest?: { failure?: ValidationStatus[] }; + }; + const p1SubValResults = ( + p1SubManifest as + | { validation_results?: ManifestValidationResults } + | undefined + )?.validation_results; + const p1V3_sub = (p1SubValResults?.activeManifest + ?.failure || []) as ValidationStatus[]; // 3. Check the Root Deltas (V3 standard) - const p1Delta = p1Deltas.find((d) => - d.ingredientAssertionURI?.includes(label) && - d.ingredientAssertionURI?.includes('c2pa.ingredient') + const p1Delta = p1Deltas.find( + (d) => + d.ingredientAssertionURI?.includes(label) && + d.ingredientAssertionURI?.includes('c2pa.ingredient'), ); const p1V3_delta = p1Delta?.validationDeltas?.failure || []; // AGGREGATE: Did ANY layer in the sub-manifest chain fail Trust in Pass 1? - const hasTrustError = - p1V3_delta.some(isTrustError) || - p1V2_ing.some(isTrustError) || - p1V2_sub.some(isTrustError) || + const hasTrustError = + p1V3_delta.some(isTrustError) || + p1V2_ing.some(isTrustError) || + p1V2_sub.some(isTrustError) || p1V3_sub.some(isTrustError); if (isFinalTrusted) { - // If the Root is trusted, the chain is intact. + // If the Root is trusted, the chain is intact. // It only earns 'Official' if it explicitly cleared the local error gauntlet. - ingredient.trust_source = hasTrustError ? 'legacy' : 'official'; + ingredient.trust_source = hasTrustError + ? 'legacy' + : 'official'; } } }); } }); - } else { legacyReader.free(); - + // Fallback: If root is Trusted, entire tree is official. const isTrusted = finalStore.validation_state === 'Trusted'; - (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { + ( + Object.entries(finalStore.manifests || {}) as Array< + [string, TrustedManifest] + > + ).forEach(([label, manifest]) => { if (label === finalStore.active_manifest) { manifest.trust_source = isTrusted ? 'official' : 'none'; } if (manifest.ingredients) { manifest.ingredients.forEach((ing) => { - ing.trust_source = (isTrusted && ing.active_manifest) ? 'official' : 'none'; + ing.trust_source = + isTrusted && ing.active_manifest ? 'official' : 'none'; }); } }); @@ -205,22 +276,29 @@ export function createC2paReader(): C2paReaderStore { } else { // Pass 1 had no trust issues (Could be Trusted or Hard Invalid) const isTrusted = finalStore.validation_state === 'Trusted'; - (Object.entries(finalStore.manifests || {}) as Array<[string, TrustedManifest]>).forEach(([label, manifest]) => { + ( + Object.entries(finalStore.manifests || {}) as Array< + [string, TrustedManifest] + > + ).forEach(([label, manifest]) => { if (label === finalStore.active_manifest) { manifest.trust_source = isTrusted ? 'official' : 'none'; } if (manifest.ingredients) { manifest.ingredients.forEach((ing) => { - ing.trust_source = (isTrusted && ing.active_manifest) ? 'official' : 'none'; + ing.trust_source = + isTrusted && ing.active_manifest ? 'official' : 'none'; }); } }); } - const { assetMap, dispose: assetMapDisposer } = - await resultToAssetMap({ manifestStore: finalStore, source }); - + const { assetMap, dispose: assetMapDisposer } = await resultToAssetMap({ + manifestStore: finalStore, + source, + }); + dispose = () => { assetMapDisposer(); currentReader.free(); @@ -235,10 +313,14 @@ export function createC2paReader(): C2paReaderStore { const errStr = String(e); const errName = (e as Record)?.name; - if (errName === 'InvalidMimeTypeError' || errStr.includes('Unsupported format')) { + if ( + errName === 'InvalidMimeTypeError' || + errStr.includes('Unsupported format') + ) { toast.trigger(unsupportedFileType()); } else if ( - (errName === 'C2pa(PrereleaseError)' || errStr.includes('Prerelease')) && + (errName === 'C2pa(PrereleaseError)' || + errStr.includes('Prerelease')) && (await hasLegacyCredentials(source)) ) { openModal(LegacyCredentialModal); diff --git a/src/routes/verify/stores/verifyStore.ts b/src/routes/verify/stores/verifyStore.ts index 8c959881e..94bbe46e6 100644 --- a/src/routes/verify/stores/verifyStore.ts +++ b/src/routes/verify/stores/verifyStore.ts @@ -150,7 +150,11 @@ export function createVerifyStore(): VerifyStore { } dbg('Reading C2PA source', source); - c2paReader.read(source); + + if (typeof source !== 'string') { + c2paReader.read(source); + } + dbg('Setting selected source', incomingSource); selectedSource.set(incomingSource); }, From 945e5315fa061073c6b56559bd59f6f128801335 Mon Sep 17 00:00:00 2001 From: Andy Parsons Date: Fri, 1 May 2026 13:13:32 -0400 Subject: [PATCH 3/4] fix: correct doNotTrain entries lookup (object not array) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit entries in c2pa.training-mining is a Record, not an array — use Object.values().some() instead of .find(). Co-Authored-By: Claude Sonnet 4.6 --- src/lib/selectors/doNotTrain.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/selectors/doNotTrain.ts b/src/lib/selectors/doNotTrain.ts index 7dc11623c..1398c02ea 100644 --- a/src/lib/selectors/doNotTrain.ts +++ b/src/lib/selectors/doNotTrain.ts @@ -13,18 +13,17 @@ export function selectDoNotTrain(manifest: Manifest): boolean { ); if (trainingAssertionEntry) { - type TrainingEntry = { use: string; c2pa_manifest: boolean | string }; - type TrainingMining = { entries?: TrainingEntry[] }; + type TrainingEntry = { use: string }; + type TrainingMining = { entries?: Record }; const trainingData = trainingAssertionEntry.data as | TrainingMining | undefined; - const entry = trainingData?.entries?.find( - (e: TrainingEntry) => - e.use === 'notAllowed' && - (e.c2pa_manifest === true || e.c2pa_manifest === 'true'), - ); + const entries = trainingData?.entries; + const hasDoNotTrain = + entries != null && + Object.values(entries).some((e) => e.use === 'notAllowed'); - return !!entry; + return hasDoNotTrain; } // Fallback: Check c2pa.actions for specific 'not_trained' markers From 954a83f6c5f32e4731cfc0a1411588b152ec2dcd Mon Sep 17 00:00:00 2001 From: Andy Parsons Date: Fri, 1 May 2026 15:26:37 -0400 Subject: [PATCH 4/4] chore: apply Prettier formatting across TS and Svelte files Reformats long lines in e2e tests, spec files, sdk.ts, and Svelte components to comply with the project's print-width lint rules. Co-Authored-By: Claude Sonnet 4.6 --- e2e/c2pa-migration-test.spec.ts | 45 ++++++++++++++----- src/lib/crjson.spec.ts | 38 ++++++++++++---- src/lib/sdk.ts | 9 ++-- src/lib/selectors/validationResult.spec.ts | 19 ++++---- .../AssetInfo/AssetInfoIssuerDate.svelte | 3 +- .../AboutSection/IssuedBySection.svelte | 22 +++++---- .../ContentSummarySection.svelte | 17 ++++--- .../DetailedInfo/DetailedInfo.svelte | 11 ++++- 8 files changed, 115 insertions(+), 49 deletions(-) diff --git a/e2e/c2pa-migration-test.spec.ts b/e2e/c2pa-migration-test.spec.ts index 629e79bbb..08db696ef 100644 --- a/e2e/c2pa-migration-test.spec.ts +++ b/e2e/c2pa-migration-test.spec.ts @@ -45,12 +45,18 @@ function collectPageErrors(page: import('@playwright/test').Page) { } test.describe('c2pa-web SDK migration — trust badge rendering', () => { - test('loads a conformant fixture image without browser errors', async ({ page }) => { + test('loads a conformant fixture image without browser errors', async ({ + page, + }) => { const { consoleErrors, pageErrors } = collectPageErrors(page); - await page.goto(`/?source=${encodeURIComponent(`${FIXTURES_BASE}/CAICAI.jpg`)}`); + await page.goto( + `/?source=${encodeURIComponent(`${FIXTURES_BASE}/CAICAI.jpg`)}`, + ); - await expect(page.getByText('Content Credentials', { exact: false })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: false }), + ).toBeVisible({ timeout: 20000, }); @@ -58,18 +64,26 @@ test.describe('c2pa-web SDK migration — trust badge rendering', () => { expect(consoleErrors).toHaveLength(0); }); - test('does not show an unrecognized-issuer banner for a conformant fixture image', async ({ page }) => { + test('does not show an unrecognized-issuer banner for a conformant fixture image', async ({ + page, + }) => { const { consoleErrors, pageErrors } = collectPageErrors(page); - await page.goto(`/?source=${encodeURIComponent(`${FIXTURES_BASE}/CAICAI.jpg`)}`); + await page.goto( + `/?source=${encodeURIComponent(`${FIXTURES_BASE}/CAICAI.jpg`)}`, + ); - await expect(page.getByText('Content Credentials', { exact: false })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: false }), + ).toBeVisible({ timeout: 20000, }); // The orange "issuer couldn't be recognized" banner must not appear for an image signed // by a conformant implementation. - await expect(page.getByText("issuer couldn't be recognized", { exact: false })).toBeHidden(); + await expect( + page.getByText("issuer couldn't be recognized", { exact: false }), + ).toBeHidden(); expect(pageErrors).toHaveLength(0); expect(consoleErrors).toHaveLength(0); @@ -78,9 +92,14 @@ test.describe('c2pa-web SDK migration — trust badge rendering', () => { // Requires a locally-available legacy-signed image. Set TEST_LEGACY_IMAGE_PATH to an // absolute path on disk to run this test; it is skipped when the variable is unset so that // CI passes without the proprietary asset. - test('shows a "Legacy trust" badge for a legacy-signed image', async ({ page }) => { + test('shows a "Legacy trust" badge for a legacy-signed image', async ({ + page, + }) => { const legacyImagePath = process.env.TEST_LEGACY_IMAGE_PATH; - test.skip(!legacyImagePath, 'TEST_LEGACY_IMAGE_PATH not set — skipping legacy trust test'); + test.skip( + !legacyImagePath, + 'TEST_LEGACY_IMAGE_PATH not set — skipping legacy trust test', + ); const { consoleErrors, pageErrors } = collectPageErrors(page); @@ -90,11 +109,15 @@ test.describe('c2pa-web SDK migration — trust badge rendering', () => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion await page.setInputFiles('input[type="file"]', legacyImagePath!); - await expect(page.getByText('Content Credentials', { exact: false })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: false }), + ).toBeVisible({ timeout: 20000, }); - await expect(page.getByText('Legacy trust', { exact: true })).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('Legacy trust', { exact: true })).toBeVisible({ + timeout: 10000, + }); expect(pageErrors).toHaveLength(0); expect(consoleErrors).toHaveLength(0); diff --git a/src/lib/crjson.spec.ts b/src/lib/crjson.spec.ts index 1bb39d489..b64b5ff0f 100644 --- a/src/lib/crjson.spec.ts +++ b/src/lib/crjson.spec.ts @@ -14,7 +14,9 @@ import { type CrJsonValidationResults, } from './crjson'; -const makeManifest = (overrides: Partial = {}): CrJsonManifestEntry => ({ +const makeManifest = ( + overrides: Partial = {}, +): CrJsonManifestEntry => ({ label: 'urn:test:manifest', assertions: {}, ...overrides, @@ -51,11 +53,16 @@ describe('lib/crjson', () => { describe('getAssertionsList()', () => { it('converts assertions object to label/data pairs', () => { const m = makeManifest({ - assertions: { 'c2pa.actions': { actions: [] }, 'stds.schema-org.CreativeWork': { name: 'test' } }, + assertions: { + 'c2pa.actions': { actions: [] }, + 'stds.schema-org.CreativeWork': { name: 'test' }, + }, }); const list = getAssertionsList(m); expect(list).toHaveLength(2); - expect(list.find((a) => a.label === 'c2pa.actions')?.data).toEqual({ actions: [] }); + expect(list.find((a) => a.label === 'c2pa.actions')?.data).toEqual({ + actions: [], + }); }); it('returns empty array for a manifest with no assertions', () => { @@ -65,13 +72,21 @@ describe('lib/crjson', () => { describe('getAssertionDataByLabel()', () => { it('returns data for a known label', () => { - const m = makeManifest({ assertions: { 'c2pa.actions': { actions: [{ action: 'c2pa.created' }] } } }); - const data = getAssertionDataByLabel(m, 'c2pa.actions') as { actions: unknown[] }; + const m = makeManifest({ + assertions: { + 'c2pa.actions': { actions: [{ action: 'c2pa.created' }] }, + }, + }); + const data = getAssertionDataByLabel(m, 'c2pa.actions') as { + actions: unknown[]; + }; expect(data.actions).toHaveLength(1); }); it('returns undefined for an unknown label', () => { - expect(getAssertionDataByLabel(makeManifest(), 'does.not.exist')).toBeUndefined(); + expect( + getAssertionDataByLabel(makeManifest(), 'does.not.exist'), + ).toBeUndefined(); }); }); @@ -162,7 +177,11 @@ describe('lib/crjson', () => { failure: [{ code: 'signingCredential.untrusted', url: 'Cose_Sign1' }], }; const report = makeReport({ - manifests: [makeManifest({ validationResults: perManifestValidation } as unknown as Partial)], + manifests: [ + makeManifest({ + validationResults: perManifestValidation, + } as unknown as Partial), + ], }); const status = getActiveManifestValidationStatus(report); expect(status?.failure).toHaveLength(1); @@ -177,7 +196,10 @@ describe('lib/crjson', () => { manifests: { 'urn:legacy:manifest': { assertions: [ - { label: 'c2pa.actions', data: { actions: [{ action: 'c2pa.created' }] } }, + { + label: 'c2pa.actions', + data: { actions: [{ action: 'c2pa.created' }] }, + }, ], claim_generator_info: [{ name: 'LegacyApp', version: '0.9' }], signature_info: { diff --git a/src/lib/sdk.ts b/src/lib/sdk.ts index fa7118641..82d625923 100644 --- a/src/lib/sdk.ts +++ b/src/lib/sdk.ts @@ -66,9 +66,12 @@ async function createLegacyToolkitSettings(): Promise { }; } -export const getOfficialToolkitSettings = pMemoize(createOfficialToolkitSettings, { - maxAge: 1000 * ALLOWED_LIST_CACHE_SECS, -}); +export const getOfficialToolkitSettings = pMemoize( + createOfficialToolkitSettings, + { + maxAge: 1000 * ALLOWED_LIST_CACHE_SECS, + }, +); export const getLegacyToolkitSettings = pMemoize(createLegacyToolkitSettings, { maxAge: 1000 * ALLOWED_LIST_CACHE_SECS, diff --git a/src/lib/selectors/validationResult.spec.ts b/src/lib/selectors/validationResult.spec.ts index ce0a81ce6..004b84626 100644 --- a/src/lib/selectors/validationResult.spec.ts +++ b/src/lib/selectors/validationResult.spec.ts @@ -243,14 +243,17 @@ describe('lib/selectors/validationResult', () => { it('should detect an untrusted timestamp in v3 validationResults', () => { expect( - selectValidationResult( - [], - { - success: [{ code: 'signingCredential.trusted', url: 'Cose_Sign1' }], - informational: [{ code: 'timeStamp.mismatch', url: 'Cose_Sign1', explanation: 'timestamp mismatch' }], - failure: [], - }, - ), + selectValidationResult([], { + success: [{ code: 'signingCredential.trusted', url: 'Cose_Sign1' }], + informational: [ + { + code: 'timeStamp.mismatch', + url: 'Cose_Sign1', + explanation: 'timestamp mismatch', + }, + ], + failure: [], + }), ).toEqual({ hasError: false, hasOtgp: false, diff --git a/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte b/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte index d5423e628..8b1ca8798 100644 --- a/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte +++ b/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte @@ -11,4 +11,5 @@ $: issuer = manifestData?.signatureInfo?.issuer; -{#if issuer}{$_('sidebar.verify.about.issuedby')} {issuer}{/if} +{#if issuer}{$_('sidebar.verify.about.issuedby')} {issuer}{/if} diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte index 9f9d060e7..63c199e61 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte @@ -42,24 +42,28 @@ {issuer}
{:else} - {issuer || commonName} + {issuer || commonName} {/if} {#if trustSource === 'official'} {:else if trustSource === 'legacy'} - Legacy trust + Legacy trust {/if} diff --git a/src/routes/verify/components/DetailedInfo/ContentSummarySection/ContentSummarySection.svelte b/src/routes/verify/components/DetailedInfo/ContentSummarySection/ContentSummarySection.svelte index fce7e809b..74f872d72 100644 --- a/src/routes/verify/components/DetailedInfo/ContentSummarySection/ContentSummarySection.svelte +++ b/src/routes/verify/components/DetailedInfo/ContentSummarySection/ContentSummarySection.svelte @@ -40,7 +40,10 @@ icon?: string; }; - type ContentSummaryData = ContentSummary | ContentSummaryTranslated | ContentSummaryText; + type ContentSummaryData = + | ContentSummary + | ContentSummaryTranslated + | ContentSummaryText; export interface ContentSummarySectionProps { contentSummaryData: ContentSummaryData | null; @@ -186,8 +189,8 @@ if (isCapturedMedia) { return { contentSummaryData: { - text: 'This is captured media.' - } + text: 'This is captured media.', + }, }; } @@ -250,13 +253,13 @@
{'text' in contentSummaryData - ? contentSummaryData.text - : (hasLocaleData(contentSummaryData) + >{'text' in contentSummaryData + ? contentSummaryData.text + : hasLocaleData(contentSummaryData) ? $_(contentSummaryData.key, { values: { ...formatLocaleData(contentSummaryData.locale) }, }) - : $_(contentSummaryData.key))} + : $_(contentSummaryData.key)}
{/if} diff --git a/src/routes/verify/components/DetailedInfo/DetailedInfo.svelte b/src/routes/verify/components/DetailedInfo/DetailedInfo.svelte index b6a2f3847..bc4a49478 100644 --- a/src/routes/verify/components/DetailedInfo/DetailedInfo.svelte +++ b/src/routes/verify/components/DetailedInfo/DetailedInfo.svelte @@ -89,7 +89,10 @@ {#if $assetData} {title} - + {/if}