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/components/EmbeddedIcon/EmbeddedIcon.svelte b/src/components/EmbeddedIcon/EmbeddedIcon.svelte index cf93a929e..12980b70f 100644 --- a/src/components/EmbeddedIcon/EmbeddedIcon.svelte +++ b/src/components/EmbeddedIcon/EmbeddedIcon.svelte @@ -3,28 +3,18 @@ --> diff --git a/src/lib/asset.ts b/src/lib/asset.ts index 0d3ce7334..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 any).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 any)?.trust_source || 'none', + trustSource: ((ingredient as Ingredient & { trust_source?: string }) + ?.trust_source || 'none') as AssetData['trustSource'], }; if (thumbnail?.dispose) { @@ -332,15 +341,18 @@ export async function resultToAssetMap({ async function getManifestData( manifest: Manifest | null | undefined, - validationResult: ValidationStatusResult + validationResult: ValidationStatusResult, ): Promise { if (!manifest) { return null; } - function formattedGeneratorInfo(claim_generator: any) { + function formattedGeneratorInfo( + claim_generator: NonNullable[number], + ): NonNullable[number] { const version = claim_generator?.version; claim_generator.version = version?.replace(/\([^()]*\)/g, ''); + return claim_generator; } @@ -356,11 +368,9 @@ export async function resultToAssetMap({ const claimGenerator: ClaimGeneratorDisplayInfo = { label: claimGeneratorLabel, - icon: claimGeneratorInfo?.icon ?? null, + icon: (claimGeneratorInfo?.icon as Thumbnail | null | undefined) ?? null, }; - // Extract Organization (O) from the native X.509 certificate subject tree - let organization: string | undefined = undefined; const safeSignatureInfo = manifest.signature_info ? { ...manifest.signature_info } : null; @@ -370,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, @@ -383,9 +391,18 @@ export async function resultToAssetMap({ ); if (editsAndActivity) { - const actionsAssertion = manifest.assertions?.['c2pa.actions']; + 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 as any)?.data?.metadata?.['com.adobe.inference']; + !!actionsAssertion?.metadata?.['com.adobe.inference']; const filteredEditsAndActivity = editsAndActivity.filter( (value) => !!value.label, @@ -411,19 +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: any) => 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']; } - const actions = (actionsAssertion as any)?.data?.actions || (actionsAssertion as any)?.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 @@ -431,8 +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.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/crjson.ts b/src/lib/crjson.ts index 6f1d2efa5..9356c20dc 100644 --- a/src/lib/crjson.ts +++ b/src/lib/crjson.ts @@ -1,115 +1,135 @@ -/** - * crJSON (Content Credentials JSON) - native format from C2PA Reader.crjson(). - * This is the canonical format for reports: stored, downloaded, and passed through the app. - * Legacy (Reader.json()) format is converted to crJSON only when received from the packaged SDK. - */ +// 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 - return Array.isArray(o?.manifests) && o.manifests.length > 0 && o['@context'] != null + const o = obj as Record; + + 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 ?? {} - return Object.entries(assertions).map(([label, data]) => ({ label, data })) +export function getAssertionsList( + m: CrJsonManifestEntry, +): CrJsonAssertionItem[] { + const assertions = m.assertions ?? {}; + + 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 - if (assertionLabel === 'c2pa.ingredient' || (d?.document_id != null && d?.instance_id != null)) { + const d = data as Record; + + 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; } /** @@ -117,72 +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(', ') - return '' + + if (parts.length > 0) return parts.join(', '); + + 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 - return { alg, common_name, organization, issuer, time } + if (!alg && !common_name && !issuer && !time) return undefined; + + 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 ?? {} - return assertions[label] +export function getAssertionDataByLabel( + m: CrJsonManifestEntry, + label: string, +): unknown { + const assertions = m.assertions ?? {}; + + return assertions[label]; } /** @@ -190,21 +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) + ); } /** @@ -212,20 +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])); } } } @@ -233,61 +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 - if (manifests.length > 0 && activeStatus && typeof activeStatus === 'object') { + const activeStatus = validationResults.activeManifest ?? validationResults; + + 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/sdk.ts b/src/lib/sdk.ts index bfdbae7a0..82d625923 100644 --- a/src/lib/sdk.ts +++ b/src/lib/sdk.ts @@ -15,6 +15,7 @@ export const getSdk = pMemoize(createSdk); async function loadTrustResource(file: string): Promise { const res = await fetch(`/trust/${file}`); + return res.text(); } @@ -26,6 +27,7 @@ async function getOfficialAnchors(): Promise { 'https://raw.githubusercontent.com/c2pa-org/conformance-public/refs/heads/main/trust-list/C2PA-TSA-TRUST-LIST.pem', ].map(async (url) => { const res = await fetch(url); + return res.text(); }), ) @@ -64,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/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 f4ee52327..1398c02ea 100644 --- a/src/lib/selectors/doNotTrain.ts +++ b/src/lib/selectors/doNotTrain.ts @@ -3,17 +3,34 @@ 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']; - if (trainingAssertions) { - const entry = (trainingAssertions as any)?.data?.entries?.find((e: any) => - e.use === 'notAllowed' && (e.c2pa_manifest === true || e.c2pa_manifest === 'true') - ); - return !!entry; + const trainingAssertionEntry = assertions.find( + (a) => a.label === 'c2pa.training-mining', + ); + + if (trainingAssertionEntry) { + type TrainingEntry = { use: string }; + type TrainingMining = { entries?: Record }; + const trainingData = trainingAssertionEntry.data as + | TrainingMining + | undefined; + const entries = trainingData?.entries; + const hasDoNotTrain = + entries != null && + Object.values(entries).some((e) => e.use === 'notAllowed'); + + return hasDoNotTrain; } // Fallback: Check c2pa.actions for specific 'not_trained' markers - const actionsAssertion = manifest.assertions?.['c2pa.actions']; - const actions = (actionsAssertion as any)?.data?.actions || []; - return actions.some((a: any) => a.action === 'c2pa.not_trained'); + 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 3bc822e9a..b06d1d655 100644 --- a/src/lib/selectors/editsAndActivity.ts +++ b/src/lib/selectors/editsAndActivity.ts @@ -1,3 +1,5 @@ +// Copyright 2021-2024 Adobe, Copyright 2026 The C2PA Contributors + import type { Manifest } from '@contentauth/c2pa-web'; export interface TranslatedDictionaryCategory { @@ -9,22 +11,40 @@ export interface TranslatedDictionaryCategory { export async function selectEditsAndActivity( manifest: Manifest, - locale: string + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _locale: string, ): Promise { // Handle Legacy SDK (Map), Native SDK (Array), and crJSON (Object) - let actionsAssertion; + type ActionItem = { label?: string; action: string }; + 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: any) => 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 as any)?.data?.actions || (actionsAssertion as any)?.actions || []; + + const actions = + actionsAssertion?.data?.actions || actionsAssertion?.actions || []; const uniqueActionTypes = new Set(); - actions.forEach((a: any) => uniqueActionTypes.add(a.action)); + actions.forEach((a) => uniqueActionTypes.add(a.action)); const results: TranslatedDictionaryCategory[] = []; @@ -33,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 2347ec642..ce18087ce 100644 --- a/src/lib/selectors/generativeInfo.ts +++ b/src/lib/selectors/generativeInfo.ts @@ -1,46 +1,62 @@ // 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; type: string; } +type GenActionItem = { + label?: string; + action?: string; + digitalSourceType?: string; + softwareAgent?: string; + parameters?: { digitalSourceType?: string }; +}; + +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: any) => a.label === 'c2pa.actions' || a.label === 'c2pa.actions.v2') - : (manifest.assertions?.['c2pa.actions.v2'] || manifest.assertions?.['c2pa.actions']); - - const actions = (actionsAssertion as any)?.data?.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 || []; + return actions - .filter((a: any) => { + .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: any) => { + .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'; - + const typeSlug = + typeof rawType === 'string' + ? (rawType.split('/').pop() ?? 'legacy') + : 'legacy'; + return { softwareAgent: a.softwareAgent || 'Unknown', - type: typeSlug + type: typeSlug, }; }); } + 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[]; @@ -50,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[]) { @@ -80,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 9f6193c96..3284011ec 100644 --- a/src/lib/selectors/producer.ts +++ b/src/lib/selectors/producer.ts @@ -7,25 +7,52 @@ interface ProducerInfo { 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 any)?.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 }; } } // 2. Check XMP producer/creator - const xmp = (manifest.assertions?.['stds.xmp'] as any)?.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 }; } // 3. Fallback to certificate subject common name const commonName = manifest.signature_info?.common_name; + if (commonName) { return { name: commonName }; } diff --git a/src/lib/selectors/reviewRatings.ts b/src/lib/selectors/reviewRatings.ts index d30415f5d..e97acfb48 100644 --- a/src/lib/selectors/reviewRatings.ts +++ b/src/lib/selectors/reviewRatings.ts @@ -13,8 +13,14 @@ export function selectReviewRatings(manifest: Manifest) { }, [], ); + 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 any)?.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 d835d7391..f69a6b19f 100644 --- a/src/lib/selectors/socialAccounts.ts +++ b/src/lib/selectors/socialAccounts.ts @@ -14,10 +14,16 @@ 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({ '@id': vcData.id || '', @@ -29,19 +35,42 @@ export function selectSocialAccounts(manifest: Manifest): SocialAccount[] { } // Also check standard CreativeWork assertions for "sameAs" social URLs - const creativeWork = (manifest.assertions?.['stds.schema-org.CreativeWork'] as any)?.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.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/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 ee884e028..0e2de0e6c 100644 --- a/src/lib/selectors/web3Info.ts +++ b/src/lib/selectors/web3Info.ts @@ -17,8 +17,17 @@ declare module '@contentauth/c2pa-web' { } } +type CryptoAddressData = Record; + export function selectWeb3(manifest: Manifest): [string, string[]][] { - const cryptoEntries = (manifest.assertions?.['adobe.crypto.addresses'] as any)?.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 64faf5e15..2fe18c19f 100644 --- a/src/lib/selectors/website.ts +++ b/src/lib/selectors/website.ts @@ -17,10 +17,22 @@ declare module '@contentauth/c2pa-web' { } } +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 any)?.data?.references?.[0]?.reference?.uri ?? - (manifest.assertions?.['stds.schema-org.CreativeWork'] as any)?.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/AssetInfo/AssetInfoIssuerDate.svelte b/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte index 9259b6d9b..8b1ca8798 100644 --- a/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte +++ b/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte @@ -5,12 +5,11 @@ import Description from '$src/components/typography/Description.svelte'; import type { ManifestData } from '$src/lib/asset'; import { _ } from 'svelte-i18n'; - import AssetInfoDate from './AssetInfoDate.svelte'; export let manifestData: ManifestData | null; - export let trustSource: 'official' | 'legacy' | 'none' = 'none'; - $: date = manifestData?.date; + export const trustSource: 'official' | 'legacy' | 'none' = 'none'; $: 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/AboutSection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte index 8dc14aaa6..a1ec784a9 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte @@ -12,9 +12,29 @@ export let trustSource: 'official' | 'legacy' | 'none' = 'none'; // Extract extended X.509 fields (Catching various Rust/JS SDK naming conventions) - $: sigInfo = manifestData.signatureInfo as any; - $: orgUnit = sigInfo?.organization_unit || sigInfo?.organizational_unit || sigInfo?.organizationUnit || sigInfo?.org_unit || sigInfo?.ou; - $: country = sigInfo?.country || sigInfo?.country_name || sigInfo?.countryName || sigInfo?.c; + 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; @@ -22,13 +42,12 @@ {$_('sidebar.verify.about')} {#if manifestData.signatureInfo?.common_name || manifestData.signatureInfo?.issuer} - + {country} + {trustSource} /> {/if} {#if manifestData.date} diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/IssuedBySection.svelte index 21159186c..63c199e61 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()}`; })(); @@ -41,24 +42,28 @@ {issuer} {:else} - {issuer || commonName} + {issuer || commonName} {/if} {#if trustSource === 'official'} {:else if trustSource === 'legacy'} - Legacy trust + Legacy trust {/if} @@ -71,6 +76,7 @@ {#if showTooltip} (showTooltip = !showTooltip)} >
+ {@html $_('sidebar.verify.about.issuedby.tooltip')}
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}