diff --git a/e2e/c2pa-migration-test.spec.ts b/e2e/c2pa-migration-test.spec.ts index fb3eed5be..942002178 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: true })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: true }), + ).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: true })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: true }), + ).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: true })).toBeVisible({ + await expect( + page.getByText('Content Credentials', { exact: true }), + ).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/locales/en-US.json b/locales/en-US.json index 58a3fba2d..7db10dabd 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -125,6 +125,13 @@ "sidebar.verify.about.issuedby.tooltip": "The \"Conformant\" badge indicates products that have met the requirements of the C2PA Conformance Program for correctness and security of implementation, and are issued certificates from the C2PA Trust List. The \"Legacy trust\" badge is for products with certificates on the frozen Interim Trust List. The correctness and security of those products have not been evaluated by the C2PA Conformance Program. Learn more at c2pa.org/conformance", "sidebar.verify.about.issuedby.tooltip.link": "See certificate information", "sidebar.verify.about.issuedon": "Issued on", + "sidebar.verify.cawg.role.contributor": "Contributor", + "sidebar.verify.cawg.role.creator": "Creator", + "sidebar.verify.cawg.role.editor": "Editor", + "sidebar.verify.cawg.role.producer": "Producer", + "sidebar.verify.cawg.role.publisher": "Publisher", + "sidebar.verify.cawg.role.sponsor": "Sponsor", + "sidebar.verify.cawg.role.translator": "Translator", "sidebar.verify.advanced": "Advanced", "sidebar.verify.advanced.show": "Show all data", "sidebar.verify.asset.date.on": "on", diff --git a/playwright.config.ts b/playwright.config.ts index bff08269d..7b2e32d6a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -3,7 +3,6 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import testImageConfig from './e2e/c2pa-test-image-service.config'; - export const port = parseInt( (process.env.HOST_PORT as string | undefined) ?? '4173', 10, diff --git a/src/lib/asset.ts b/src/lib/asset.ts index 4033553ef..a58c9f4f2 100644 --- a/src/lib/asset.ts +++ b/src/lib/asset.ts @@ -16,7 +16,10 @@ interface ExtendedIngredient extends Ingredient { trustSource?: string; } import { selectDoNotTrain } from './selectors/doNotTrain'; -import { selectEditsAndActivity, type TranslatedDictionaryCategory } from './selectors/editsAndActivity'; +import { + selectEditsAndActivity, + type TranslatedDictionaryCategory, +} from './selectors/editsAndActivity'; import debug from 'debug'; import { selectExif } from './exif'; @@ -34,6 +37,7 @@ import { selectModelsFromIngredient, type GenerativeInfo, } from './selectors/generativeInfo'; +import { selectCawgRoles, type RoleEntry } from './selectors/cawgRoles'; import { selectReviewRatings } from './selectors/reviewRatings'; import { selectValidationResult, @@ -93,6 +97,7 @@ export type ManifestData = { doNotTrain: ReturnType; web3Accounts: [string, string[]][]; website: string | null; + cawgRoles: RoleEntry[]; autoDubInfo: AutoDubInfo | null; isCapturedMedia: boolean; }; @@ -145,7 +150,7 @@ export async function resultToAssetMap({ }): Promise { const assetMap: AssetDataMap = {}; const disposers: (() => void)[] = []; - + const activeManifestLabel = manifestStore?.active_manifest ?? ''; const allLabels = Object.keys(manifestStore?.manifests ?? {}); @@ -157,7 +162,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 || undefined; @@ -189,7 +197,9 @@ export async function resultToAssetMap({ containingManifestLabel: string, ): Promise { const blob = ref?.identifier - ? thumbnails.get(toAbsoluteIdentifier(ref.identifier, containingManifestLabel)) + ? thumbnails.get( + toAbsoluteIdentifier(ref.identifier, containingManifestLabel), + ) : undefined; if (!blob) { @@ -207,10 +217,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); @@ -262,7 +272,10 @@ export async function resultToAssetMap({ const manifest = manifestStore.manifests?.[activeManifestLabel]; if (!manifest) throw new Error('Active manifest not found'); - let thumbnail = await lookupThumbnail(manifest.thumbnail, activeManifestLabel); + let thumbnail = await lookupThumbnail( + manifest.thumbnail, + activeManifestLabel, + ); if ( !thumbnail.info && @@ -271,10 +284,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 = { @@ -292,7 +305,8 @@ export async function resultToAssetMap({ manifestData: await getManifestData(manifest, rootValidationResult), dataType: null, validationResult: rootValidationResult, - trustSource: ((manifest as Manifest & { trust_source?: string }).trust_source || 'none') as 'legacy' | 'none' | 'official', + trustSource: ((manifest as Manifest & { trust_source?: string }) + .trust_source || 'none') as 'legacy' | 'none' | 'official', }; if (thumbnail?.dispose) { @@ -311,8 +325,12 @@ export async function resultToAssetMap({ id: string, containingManifestLabel: string, ): Promise { - const ingredientManifestLabel = ingredient.active_manifest || (ingredient as ExtendedIngredient).activeManifest; - const ingredientManifest = ingredientManifestLabel ? manifestStore.manifests?.[ingredientManifestLabel] : null; + const ingredientManifestLabel = + ingredient.active_manifest || + (ingredient as ExtendedIngredient).activeManifest; + const ingredientManifest = ingredientManifestLabel + ? manifestStore.manifests?.[ingredientManifestLabel] + : null; /** * The code prioritizes the signed ingredient's own c2pa.thumbnail.claim, if present, @@ -329,19 +347,33 @@ export async function resultToAssetMap({ * signed ingredient's own claim thumbnail is still shown when present (untrusted * state is flagged in the UI). */ - const containingManifestUntrusted = (runtimeValidationStatuses[containingManifestLabel] ?? []) - .some((s) => s.code.includes('signingCredential.untrusted') || s.code.includes('signingCredential.invalid')); + const containingManifestUntrusted = ( + runtimeValidationStatuses[containingManifestLabel] ?? [] + ).some( + (s) => + s.code.includes('signingCredential.untrusted') || + s.code.includes('signingCredential.invalid'), + ); - const thumbnail = ingredientManifestLabel && ingredientManifest?.thumbnail - ? await lookupThumbnail(ingredientManifest.thumbnail, ingredientManifestLabel) - : !containingManifestUntrusted - ? await lookupThumbnail(ingredient.thumbnail, containingManifestLabel) - : await loadThumbnail(undefined, undefined); + const thumbnail = + ingredientManifestLabel && ingredientManifest?.thumbnail + ? await lookupThumbnail( + ingredientManifest.thumbnail, + ingredientManifestLabel, + ) + : !containingManifestUntrusted + ? await lookupThumbnail(ingredient.thumbnail, containingManifestLabel) + : await loadThumbnail(undefined, undefined); const activeManifestValidationResults = - (ingredient.validation_results?.activeManifest || (ingredient as ExtendedIngredient).validationResults?.activeManifest) ?? undefined; - - const validationStatus = ingredient.validation_status || (ingredient as ExtendedIngredient).validationStatus || []; + (ingredient.validation_results?.activeManifest || + (ingredient as ExtendedIngredient).validationResults?.activeManifest) ?? + undefined; + + const validationStatus = + ingredient.validation_status || + (ingredient as ExtendedIngredient).validationStatus || + []; let validationResult = selectValidationResult( validationStatus, activeManifestValidationResults, @@ -359,19 +391,24 @@ export async function resultToAssetMap({ title: ingredient.title ?? null, thumbnail: thumbnail.info, mimeType: ingredient.format || '', - children: (showChildren && ingredientManifest?.ingredients && ingredientManifestLabel) - ? await processIngredients( - ingredientManifest.ingredients, - manifestStore, - runtimeValidationStatuses, - id, - ingredientManifestLabel, - ) - : [], + children: + showChildren && + ingredientManifest?.ingredients && + ingredientManifestLabel + ? await processIngredients( + ingredientManifest.ingredients, + manifestStore, + runtimeValidationStatuses, + id, + ingredientManifestLabel, + ) + : [], manifestData: await getManifestData(ingredientManifest, validationResult), dataType: getIngredientDataType(ingredient), validationResult, - trustSource: ((ingredient as ExtendedIngredient)?.trust_source || (ingredient as ExtendedIngredient)?.trustSource || 'none') as 'legacy' | 'none' | 'official', + trustSource: ((ingredient as ExtendedIngredient)?.trust_source || + (ingredient as ExtendedIngredient)?.trustSource || + 'none') as 'legacy' | 'none' | 'official', }; if (thumbnail?.dispose) { @@ -385,7 +422,7 @@ export async function resultToAssetMap({ async function getManifestData( manifest: Manifest | null | undefined, - validationResult: ValidationStatusResult + validationResult: ValidationStatusResult, ): Promise { if (!manifest) { return null; @@ -406,7 +443,9 @@ export async function resultToAssetMap({ } const claimGeneratorInfo = manifest?.claim_generator_info?.[0] - ? formattedGeneratorInfo(manifest.claim_generator_info[0] as GeneratorInfoShape) + ? formattedGeneratorInfo( + manifest.claim_generator_info[0] as GeneratorInfoShape, + ) : null; const claimGeneratorLabel = @@ -417,7 +456,9 @@ export async function resultToAssetMap({ const claimGenerator: ClaimGeneratorDisplayInfo = { label: claimGeneratorLabel, - icon: (claimGeneratorInfo?.icon ? { identifier: claimGeneratorInfo.icon } : null) as unknown as Thumbnail | null, + icon: (claimGeneratorInfo?.icon + ? { identifier: claimGeneratorInfo.icon } + : null) as unknown as Thumbnail | null, }; // Extract Organization (O) from the native X.509 certificate subject tree @@ -430,9 +471,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, editsAndActivityForLocale: async (locale) => { @@ -451,8 +490,13 @@ export async function resultToAssetMap({ } const assertionsArr = (manifest.assertions || []) as unknown[]; type AssItem = { label?: string; data?: unknown }; - const actionsAss = assertionsArr.find((a: unknown) => (a as AssItem).label === 'c2pa.actions' || (a as AssItem).label === 'c2pa.actions.v2') as InferenceAssertion | undefined; - const hasInference = !!actionsAss?.data?.metadata?.['com.adobe.inference']; + const actionsAss = assertionsArr.find( + (a: unknown) => + (a as AssItem).label === 'c2pa.actions' || + (a as AssItem).label === 'c2pa.actions.v2', + ) as InferenceAssertion | undefined; + const hasInference = + !!actionsAss?.data?.metadata?.['com.adobe.inference']; const filteredEditsAndActivity = editsAndActivity.filter( (value) => !!value.label, @@ -473,26 +517,48 @@ export async function resultToAssetMap({ reviewRatings: selectReviewRatings(manifest), web3Accounts: selectWeb3(manifest), website: selectWebsite(manifest), + cawgRoles: selectCawgRoles(manifest), autoDubInfo: selectAutoDubInfo(manifest), 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: unknown) => (a as { label?: string }).label === 'c2pa.actions.v2' || (a as { label?: string }).label === 'c2pa.actions'); + actionsAssertion = manifest.assertions.find( + (a: unknown) => + (a as { label?: string }).label === 'c2pa.actions.v2' || + (a as { label?: string }).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 ActionEntry = { action: string; digitalSourceType?: string; parameters?: { digitalSourceType?: string } }; - type AssertionBlock = { data?: { actions?: ActionEntry[] }; actions?: ActionEntry[] }; - const actions = (actionsAssertion as AssertionBlock)?.data?.actions || (actionsAssertion as AssertionBlock)?.actions || []; + type ActionEntry = { + action: string; + digitalSourceType?: string; + parameters?: { digitalSourceType?: string }; + }; + type AssertionBlock = { + data?: { actions?: ActionEntry[] }; + actions?: ActionEntry[]; + }; + const actions = + (actionsAssertion as AssertionBlock)?.data?.actions || + (actionsAssertion as AssertionBlock)?.actions || + []; if (actions.length !== 1) return false; // 3. First and only action must be c2pa.created @@ -500,9 +566,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 ee9a15194..cc45ca5de 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,55 +315,67 @@ 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; @@ -305,12 +384,14 @@ function legacyManifestToCrJsonEntry(label: string, m: Record): const entry: CrJsonManifestEntry = { label, assertions, - } + }; if (claim) entry.claim = claim as Record; if (signature) entry.signature = signature; - if (validationResults) entry.validationResults = validationResults as CrJsonValidationResults; - if (validationStatus) entry.validationStatus = validationStatus as Record; + if (validationResults) + entry.validationResults = validationResults as CrJsonValidationResults; + if (validationStatus) + entry.validationStatus = validationStatus as Record; return entry; } diff --git a/src/lib/exif.ts b/src/lib/exif.ts index adc5a8212..7eb6b2fa8 100644 --- a/src/lib/exif.ts +++ b/src/lib/exif.ts @@ -28,8 +28,6 @@ export interface ExifTags { 'exif:offsettimeoriginal'?: string; } - - function findExifValue(exif: ExifTags, locations: string[]) { return ( locations @@ -209,23 +207,22 @@ export function selectExif(manifest: Manifest): ExifSummary | null { a !== null && 'label' in a && typeof (a as Record)['label'] === 'string' && - (a as Record)['label'] === 'stds.exif' + (a as Record)['label'] === 'stds.exif', ); } else if (assertions && typeof assertions === 'object') { assertion = (assertions as Record)['stds.exif']; } const assertionData = (assertion as { data?: unknown })?.data; - const exif: ExifTags = (Array.isArray(assertionData) ? assertionData : [assertionData]).reduce( - (acc, exif) => { - const caseInsensitiveData = mapKeys(exif?.data, (_, key) => { - return key.toLowerCase(); - }); + const exif: ExifTags = ( + Array.isArray(assertionData) ? assertionData : [assertionData] + ).reduce((acc, exif) => { + const caseInsensitiveData = mapKeys(exif?.data, (_, key) => { + return key.toLowerCase(); + }); - return merge({}, acc, caseInsensitiveData); - }, - {}, - ); + return merge({}, acc, caseInsensitiveData); + }, {}); if (Object.keys(exif).length > 0) { dbg('Got EXIF tags', exif); diff --git a/src/lib/resolveThumbnails.ts b/src/lib/resolveThumbnails.ts index 78de569a6..53d859647 100644 --- a/src/lib/resolveThumbnails.ts +++ b/src/lib/resolveThumbnails.ts @@ -28,7 +28,9 @@ export async function resolveThumbnails( ): Promise> { const refs = new Map(); - for (const [label, manifest] of Object.entries(manifestStore.manifests || {})) { + for (const [label, manifest] of Object.entries( + manifestStore.manifests || {}, + )) { if (manifest.thumbnail?.identifier && manifest.thumbnail.format) { refs.set( toAbsoluteIdentifier(manifest.thumbnail.identifier, label), @@ -52,7 +54,10 @@ export async function resolveThumbnails( try { const bytes = await reader.resourceToBytes(identifier); - return [identifier, new Blob([new Uint8Array(bytes)], { type: format })]; + return [ + identifier, + new Blob([new Uint8Array(bytes)], { type: format }), + ]; } catch (err) { console.warn(`Failed to resolve thumbnail ${identifier}:`, err); 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/autoDubInfo.ts b/src/lib/selectors/autoDubInfo.ts index eb1adbd5c..e929a06cf 100644 --- a/src/lib/selectors/autoDubInfo.ts +++ b/src/lib/selectors/autoDubInfo.ts @@ -24,32 +24,36 @@ export function selectAutoDubInfo(manifest: Manifest): AutoDubInfo | null { a !== null && 'label' in a && typeof (a as Record)['label'] === 'string' && - (a as Record)['label'] === 'c2pa.actions.v2' + (a as Record)['label'] === 'c2pa.actions.v2', ); } else if (assertions && typeof assertions === 'object') { - actionAssertion = (assertions as Record)['c2pa.actions.v2']; + actionAssertion = (assertions as Record)[ + 'c2pa.actions.v2' + ]; } if (!actionAssertion) { return null; } - type ActionItem = { - action: string; - changes?: Array<{ region?: Array<{ type?: string; item?: { value?: string } }> }>; + type ActionItem = { + action: string; + changes?: Array<{ + region?: Array<{ type?: string; item?: { value?: string } }>; + }>; parameters?: unknown; }; type ActionsAssertion = { data?: { actions?: ActionItem[] } }; - const dubbedAction = (actionAssertion as ActionsAssertion).data?.actions?.find( - ({ action }) => action === 'c2pa.dubbed', - ); - const translatedAction = (actionAssertion as ActionsAssertion).data?.actions?.find( - ({ action }) => action === 'c2pa.translated', - ); - const editedAction = (actionAssertion as ActionsAssertion).data?.actions?.find( - ({ action }) => action === 'c2pa.edited', - ); + const dubbedAction = ( + actionAssertion as ActionsAssertion + ).data?.actions?.find(({ action }) => action === 'c2pa.dubbed'); + const translatedAction = ( + actionAssertion as ActionsAssertion + ).data?.actions?.find(({ action }) => action === 'c2pa.translated'); + const editedAction = ( + actionAssertion as ActionsAssertion + ).data?.actions?.find(({ action }) => action === 'c2pa.edited'); if (dubbedAction) { const dubbedRegionOfInterest = dubbedAction.changes?.find( diff --git a/src/lib/selectors/cawgRoles.spec.ts b/src/lib/selectors/cawgRoles.spec.ts new file mode 100644 index 000000000..a892fb1e9 --- /dev/null +++ b/src/lib/selectors/cawgRoles.spec.ts @@ -0,0 +1,96 @@ +// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors + +import { describe, expect, it } from 'vitest'; +import type { Manifest } from '@contentauth/c2pa-web'; +import { selectCawgRoles } from './cawgRoles'; + +/** + * Build a minimal Manifest-like object with the given assertions and + * (optionally) top-level signature_info. Cast to Manifest since selectCawgRoles + * only reads `assertions` and `signature_info`. + */ +function makeManifest( + assertions: Array<{ label?: string; data?: unknown }>, + signatureInfo?: { issuer?: string }, +): Manifest { + return { + assertions, + signature_info: signatureInfo, + } as unknown as Manifest; +} + +describe('lib/selectors/cawgRoles', () => { + describe('selectCawgRoles()', () => { + it('returns an empty array when there are no assertions', () => { + expect(selectCawgRoles(makeManifest([]))).toEqual([]); + }); + + it('returns an empty array when no CAWG assertions are present', () => { + const manifest = makeManifest([ + { label: 'c2pa.actions.v2', data: { actions: [] } }, + ]); + expect(selectCawgRoles(manifest)).toEqual([]); + }); + + it('extracts named roles from cawg.identity signer_payload.role using the issuer', () => { + const manifest = makeManifest([ + { + label: 'cawg.identity', + data: { + signer_payload: { + role: ['cawg.editor', 'cawg.sponsor'], + }, + signature_info: { + issuer: 'Google LLC', + }, + }, + }, + ]); + + expect(selectCawgRoles(manifest)).toEqual([ + { role: 'editor', name: 'Google LLC' }, + { role: 'sponsor', name: 'Google LLC' }, + ]); + }); + + it('falls back to the manifest signature_info issuer for identity roles', () => { + const manifest = makeManifest( + [ + { + label: 'cawg.identity', + data: { + signer_payload: { + role: ['cawg.producer'], + }, + }, + }, + ], + { issuer: 'Manifest Signer' }, + ); + + expect(selectCawgRoles(manifest)).toEqual([ + { role: 'producer', name: 'Manifest Signer' }, + ]); + }); + + it('ignores unknown roles in signer_payload.role', () => { + const manifest = makeManifest([ + { + label: 'cawg.identity', + data: { + signer_payload: { + role: ['cawg.unknown', 'cawg.translator'], + }, + signature_info: { + issuer: 'Google LLC', + }, + }, + }, + ]); + + expect(selectCawgRoles(manifest)).toEqual([ + { role: 'translator', name: 'Google LLC' }, + ]); + }); + }); +}); diff --git a/src/lib/selectors/cawgRoles.ts b/src/lib/selectors/cawgRoles.ts new file mode 100644 index 000000000..48d04beeb --- /dev/null +++ b/src/lib/selectors/cawgRoles.ts @@ -0,0 +1,70 @@ +// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors + +import type { Manifest } from '@contentauth/c2pa-web'; + +export interface RoleEntry { + role: string; + name: string; +} + +/** + * Named roles carried by the `cawg.identity` assertion's `signer_payload.role` array. + */ +const CAWG_ROLE_MAP: Record = { + 'cawg.creator': 'creator', + 'cawg.contributor': 'contributor', + 'cawg.editor': 'editor', + 'cawg.producer': 'producer', + 'cawg.publisher': 'publisher', + 'cawg.sponsor': 'sponsor', + 'cawg.translator': 'translator', +}; + +/** + * Extracts CAWG named roles from the `cawg.identity` assertion. + * + * Reads `signer_payload.role[]` (array of `cawg.*` role strings); the role + * holder's name is taken from the identity issuer (falling back to the manifest signer). + * + * Note: assertion labels may include JUMBF hash suffixes (e.g. `cawg.identity#xyz`), + * so labels are matched with `startsWith`. + */ +export function selectCawgRoles(manifest: Manifest): RoleEntry[] { + const assertionsArray = (manifest.assertions || []) as unknown[]; + type AssertionItem = { label?: string; data?: unknown }; + + const roles: RoleEntry[] = []; + + // Source: cawg.identity signer_payload.role[] + const identityAssertion = assertionsArray.find((a: unknown) => + (a as AssertionItem).label?.startsWith('cawg.identity'), + ) as AssertionItem | undefined; + + if (identityAssertion?.data && typeof identityAssertion.data === 'object') { + const data = identityAssertion.data as Record; + const signerPayload = data.signer_payload as + | Record + | undefined; + const signerRoles = Array.isArray(signerPayload?.role) + ? (signerPayload?.role as unknown[]) + : []; + + const signatureInfo = data.signature_info as + | Record + | undefined; + const name = + (signatureInfo?.issuer as string | undefined) || + manifest.signature_info?.issuer || + ''; + + for (const raw of signerRoles) { + const role = CAWG_ROLE_MAP[String(raw)]; + + if (role && name) { + roles.push({ role, name }); + } + } + } + + return roles; +} diff --git a/src/lib/selectors/doNotTrain.ts b/src/lib/selectors/doNotTrain.ts index b3bfa7684..d879fe137 100644 --- a/src/lib/selectors/doNotTrain.ts +++ b/src/lib/selectors/doNotTrain.ts @@ -7,21 +7,25 @@ export function selectDoNotTrain(manifest: Manifest): boolean { type AssertionItem = { label?: string; data?: unknown }; // 1. Search for modern c2pa.training-mining assertion array block item - const trainingAss = assertionsArray.find((a: unknown) => (a as AssertionItem).label === 'c2pa.training-mining') as AssertionItem | undefined; + const trainingAss = assertionsArray.find( + (a: unknown) => (a as AssertionItem).label === 'c2pa.training-mining', + ) as AssertionItem | undefined; if (trainingAss) { // The c2pa.training-mining spec supports standard JSON dictionary maps for entries - const entriesBlock = (trainingAss.data as Record | undefined)?.entries; - + const entriesBlock = ( + trainingAss.data as Record | undefined + )?.entries; + if (entriesBlock && typeof entriesBlock === 'object') { const entries = entriesBlock as Record>; - + // Audit all known standard generative AI and data-mining blockers const blockers = [ entries['c2pa.ai_generative_training'], entries['c2pa.ai_inference'], entries['c2pa.ai_training'], - entries['c2pa.data_mining'] + entries['c2pa.data_mining'], ]; for (let i = 0; i < blockers.length; i++) { @@ -33,11 +37,18 @@ export function selectDoNotTrain(manifest: Manifest): boolean { } // 2. Fallback: Search c2pa.actions array item for historical 'c2pa.not_trained' action markers - const actionsAss = assertionsArray.find((a: unknown) => (a as AssertionItem).label === 'c2pa.actions' || (a as AssertionItem).label === 'c2pa.actions.v2') as AssertionItem | undefined; - + const actionsAss = assertionsArray.find( + (a: unknown) => + (a as AssertionItem).label === 'c2pa.actions' || + (a as AssertionItem).label === 'c2pa.actions.v2', + ) as AssertionItem | undefined; + if (actionsAss) { type ActionEntry = { action: string }; - const actions = ((actionsAss.data as Record | undefined)?.actions || (actionsAss as Record | undefined)?.actions || []) as ActionEntry[]; + const actions = ((actionsAss.data as Record | undefined) + ?.actions || + (actionsAss as Record | undefined)?.actions || + []) as ActionEntry[]; for (let i = 0; i < actions.length; i++) { if (actions[i].action === 'c2pa.not_trained') { diff --git a/src/lib/selectors/editsAndActivity.ts b/src/lib/selectors/editsAndActivity.ts index 9496d8ab1..ad9c9550f 100644 --- a/src/lib/selectors/editsAndActivity.ts +++ b/src/lib/selectors/editsAndActivity.ts @@ -12,11 +12,14 @@ 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[]; + }; const assertions = manifest.assertions; let actionsAssertion: unknown; @@ -27,7 +30,9 @@ export async function selectEditsAndActivity( a !== null && 'label' in a && typeof (a as Record)['label'] === 'string' && - ['c2pa.actions', 'c2pa.actions.v2'].includes((a as Record)['label'] as string) + ['c2pa.actions', 'c2pa.actions.v2'].includes( + (a as Record)['label'] as string, + ), ); } else if (assertions && typeof assertions === 'object') { actionsAssertion = @@ -50,66 +55,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 23a3bf4d2..230204895 100644 --- a/src/lib/selectors/generativeInfo.ts +++ b/src/lib/selectors/generativeInfo.ts @@ -1,9 +1,6 @@ // Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors -import type { - Ingredient, - Manifest, -} from '@contentauth/c2pa-web'; +import type { Ingredient, Manifest } from '@contentauth/c2pa-web'; interface SdkGenerativeInfo { softwareAgent: string; @@ -12,42 +9,69 @@ interface SdkGenerativeInfo { function sdkSelectGenerativeInfo(manifest: Manifest): SdkGenerativeInfo[] { // Handle both native SDK array structures and crJSON maps - type C2paActionItem = { action: string; digitalSourceType?: string; softwareAgent?: string; parameters?: { digitalSourceType?: string } }; - type AssertionValue = { label?: string; data?: { actions?: C2paActionItem[] }; actions?: C2paActionItem[] }; + type C2paActionItem = { + action: string; + digitalSourceType?: string; + softwareAgent?: string; + parameters?: { digitalSourceType?: string }; + }; + type AssertionValue = { + label?: string; + data?: { actions?: C2paActionItem[] }; + actions?: C2paActionItem[]; + }; const isArray = Array.isArray(manifest.assertions); const assertionsArray = (manifest.assertions || []) as unknown[]; - const actionsAssertion = isArray - ? assertionsArray.find((a: unknown) => (a as AssertionValue).label === 'c2pa.actions' || (a as AssertionValue).label === 'c2pa.actions.v2') - : ((manifest.assertions as unknown as Record)?.[ 'c2pa.actions.v2' ] || (manifest.assertions as unknown as Record)?.[ 'c2pa.actions' ]); - - const actions = (actionsAssertion as AssertionValue)?.data?.actions || (actionsAssertion as AssertionValue)?.actions || []; - + const actionsAssertion = isArray + ? assertionsArray.find( + (a: unknown) => + (a as AssertionValue).label === 'c2pa.actions' || + (a as AssertionValue).label === 'c2pa.actions.v2', + ) + : (manifest.assertions as unknown as Record)?.[ + 'c2pa.actions.v2' + ] || + (manifest.assertions as unknown as Record)?.[ + 'c2pa.actions' + ]; + + const actions = + (actionsAssertion as AssertionValue)?.data?.actions || + (actionsAssertion as AssertionValue)?.actions || + []; + return actions .filter((a: C2paActionItem) => { // 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: C2paActionItem) => { 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'; + let agentName = 'Unknown'; if (a.softwareAgent) { if (typeof a.softwareAgent === 'string') { agentName = a.softwareAgent; - } else if (typeof a.softwareAgent === 'object' && (a.softwareAgent as Record).name) { - agentName = (a.softwareAgent as Record).name as string; + } else if ( + typeof a.softwareAgent === 'object' && + (a.softwareAgent as Record).name + ) { + agentName = (a.softwareAgent as Record) + .name as string; } } return { softwareAgent: agentName, - type: typeSlug || 'legacy' + type: typeSlug || 'legacy', }; }); } @@ -94,8 +118,9 @@ export function selectGenerativeType(generativeInfo: SdkGenerativeInfo[]) { } export function selectModelsFromIngredient(ingredient: Ingredient) { - const typesArray = ((ingredient as Record).dataTypes || []) as Array<{ type: string }>; - + const typesArray = ((ingredient as Record).dataTypes || + []) as Array<{ type: string }>; + return typesArray.filter((dataType: { type: string }) => startsWith('c2pa.types.model', dataType.type), ); diff --git a/src/lib/selectors/reviewRatings.ts b/src/lib/selectors/reviewRatings.ts index ff38c94b4..e47aeba6c 100644 --- a/src/lib/selectors/reviewRatings.ts +++ b/src/lib/selectors/reviewRatings.ts @@ -22,13 +22,25 @@ export function selectReviewRatings(manifest: Manifest) { metadata?: { reviewRatings?: ReviewRating[]; }; + actions?: Array<{ + parameters?: { + reviewRatings?: ReviewRating[]; + }; + }>; } const assertionsArray = (manifest.assertions || []) as unknown[]; type AssItem = { label?: string; data?: unknown }; - - const actionsAss = assertionsArray.find((a: unknown) => (a as AssItem).label === 'c2pa.actions' || (a as AssItem).label === 'c2pa.actions.v2') as InferenceAssertion | undefined; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const actionRatings = actionsAss?.data?.metadata?.reviewRatings || actionsAss?.metadata?.reviewRatings || (actionsAss as any)?.actions?.[0]?.parameters?.reviewRatings || []; + + const actionsAss = assertionsArray.find( + (a: unknown) => + (a as AssItem).label === 'c2pa.actions' || + (a as AssItem).label === 'c2pa.actions.v2', + ) as InferenceAssertion | undefined; + const actionRatings = + actionsAss?.data?.metadata?.reviewRatings || + actionsAss?.metadata?.reviewRatings || + actionsAss?.actions?.[0]?.parameters?.reviewRatings || + []; const reviewRatings = [...ingredientRatings, ...actionRatings]; return { 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 46c6641ec..8fee06703 100644 --- a/src/lib/selectors/validationResult.ts +++ b/src/lib/selectors/validationResult.ts @@ -1,6 +1,9 @@ // Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors -import type { ValidationStatus as SdkValidationStatus, StatusCodes } from '@contentauth/c2pa-web'; +import type { + ValidationStatus as SdkValidationStatus, + StatusCodes, +} from '@contentauth/c2pa-web'; import { difference } from 'lodash'; export type ValidationStatus = SdkValidationStatus; @@ -122,7 +125,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]; @@ -139,9 +142,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. @@ -150,16 +154,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'; @@ -167,7 +175,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 5635217b5..1a39988ad 100644 --- a/src/lib/selectors/web3Info.ts +++ b/src/lib/selectors/web3Info.ts @@ -24,8 +24,13 @@ export function selectWeb3(manifest: Manifest): [string, string[]][] { const assertionsArray = (manifest.assertions || []) as unknown[]; type AssItem = { label?: string; data?: unknown }; - const cryptoAss = assertionsArray.find((a: unknown) => (a as AssItem).label === 'adobe.crypto.addresses') as CryptoAssertion | undefined; - const cryptoEntries = cryptoAss?.data || (cryptoAss as unknown as Record)?.entries || {}; + const cryptoAss = assertionsArray.find( + (a: unknown) => (a as AssItem).label === 'adobe.crypto.addresses', + ) as CryptoAssertion | undefined; + const cryptoEntries = + cryptoAss?.data || + (cryptoAss as unknown as Record)?.entries || + {}; 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 83d46c604..924223ff2 100644 --- a/src/lib/selectors/website.ts +++ b/src/lib/selectors/website.ts @@ -27,15 +27,22 @@ export function selectWebsite(manifest: Manifest): string | null { data?: { url?: string; }; + url?: string; } const assertionsArray = (manifest.assertions || []) as unknown[]; type AssItem = { label?: string; data?: unknown }; - const assetRefAss = assertionsArray.find((a: unknown) => (a as AssItem).label === 'c2pa.asset-ref') as AssetRefAssertion | undefined; - const creativeWorkAss = assertionsArray.find((a: unknown) => (a as AssItem).label === 'stds.schema-org.CreativeWork') as CreativeWorkAssertion | undefined; + const assetRefAss = assertionsArray.find( + (a: unknown) => (a as AssItem).label === 'c2pa.asset-ref', + ) as AssetRefAssertion | undefined; + const creativeWorkAss = assertionsArray.find( + (a: unknown) => (a as AssItem).label === 'stds.schema-org.CreativeWork', + ) as CreativeWorkAssertion | undefined; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const site = (assetRefAss?.data?.references?.[0]?.reference?.uri || creativeWorkAss?.data?.url || (creativeWorkAss as any)?.url || null) as string | null; + const site = (assetRefAss?.data?.references?.[0]?.reference?.uri || + creativeWorkAss?.data?.url || + creativeWorkAss?.url || + null) as string | null; 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 6e0c70aa7..7618ed156 100644 --- a/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte +++ b/src/routes/verify/components/AssetInfo/AssetInfoIssuerDate.svelte @@ -12,5 +12,7 @@
- {#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 a3ba3b7d2..bd4473f47 100644 --- a/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte +++ b/src/routes/verify/components/DetailedInfo/AboutSection/AboutSection.svelte @@ -5,6 +5,7 @@ import CollapsibleSection from '$src/components/SidebarSection/CollapsibleSection.svelte'; import type { ManifestData } from '$src/lib/asset'; import { _ } from 'svelte-i18n'; + import CAWGRolesSection from './CAWGRolesSection.svelte'; import IssuedBySection from './IssuedBySection.svelte'; import IssuedOnSection from './IssuedOnSection.svelte'; @@ -12,10 +13,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,16 +43,18 @@ {$_('sidebar.verify.about')} {#if manifestData.signatureInfo?.common_name || manifestData.signatureInfo?.issuer} - + {trustSource} /> {/if} {#if manifestData.date} {/if} + {#if manifestData.cawgRoles?.length} + + {/if} diff --git a/src/routes/verify/components/DetailedInfo/AboutSection/CAWGRolesSection.svelte b/src/routes/verify/components/DetailedInfo/AboutSection/CAWGRolesSection.svelte new file mode 100644 index 000000000..efc1e3cb3 --- /dev/null +++ b/src/routes/verify/components/DetailedInfo/AboutSection/CAWGRolesSection.svelte @@ -0,0 +1,23 @@ + + +{#each roles as { role, name } (role)} + + + {$_(`sidebar.verify.cawg.role.${role}`, { + default: role.charAt(0).toUpperCase() + role.slice(1), + })} + + + {name} + + + +{/each} 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}