diff --git a/.husky/post-checkout b/.husky/post-checkout
index 6d1018135..5abf8ed93 100755
--- a/.husky/post-checkout
+++ b/.husky/post-checkout
@@ -1,3 +1,3 @@
#!/bin/sh
-command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
-git lfs post-checkout "$@"
\ No newline at end of file
+command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-checkout' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
+git lfs post-checkout "$@"
diff --git a/.husky/post-commit b/.husky/post-commit
index 323aa604d..b8b76c2c4 100755
--- a/.husky/post-commit
+++ b/.husky/post-commit
@@ -1,3 +1,3 @@
#!/bin/sh
-command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
-git lfs post-commit "$@"
\ No newline at end of file
+command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-commit' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
+git lfs post-commit "$@"
diff --git a/.husky/post-merge b/.husky/post-merge
index 25befc269..726f90989 100755
--- a/.husky/post-merge
+++ b/.husky/post-merge
@@ -1,3 +1,3 @@
#!/bin/sh
-command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
-git lfs post-merge "$@"
\ No newline at end of file
+command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'post-merge' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
+git lfs post-merge "$@"
diff --git a/.husky/pre-push b/.husky/pre-push
index 1b9474078..5f26dc455 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,3 +1,3 @@
#!/bin/sh
-command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; }
-git lfs pre-push "$@"
\ No newline at end of file
+command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
+git lfs pre-push "$@"
diff --git a/e2e/c2pa-migration-test.spec.ts b/e2e/c2pa-migration-test.spec.ts
index 629e79bbb..fb3eed5be 100644
--- a/e2e/c2pa-migration-test.spec.ts
+++ b/e2e/c2pa-migration-test.spec.ts
@@ -50,7 +50,7 @@ test.describe('c2pa-web SDK migration — trust badge rendering', () => {
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: true })).toBeVisible({
timeout: 20000,
});
@@ -63,7 +63,7 @@ test.describe('c2pa-web SDK migration — trust badge rendering', () => {
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: true })).toBeVisible({
timeout: 20000,
});
@@ -90,7 +90,7 @@ 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: true })).toBeVisible({
timeout: 20000,
});
diff --git a/e2e/fixtures/cors_server.py b/e2e/fixtures/cors_server.py
new file mode 100644
index 000000000..2e2df1b1b
--- /dev/null
+++ b/e2e/fixtures/cors_server.py
@@ -0,0 +1,14 @@
+import sys
+import socketserver
+from http.server import SimpleHTTPRequestHandler
+
+class CORS(SimpleHTTPRequestHandler):
+ def end_headers(self):
+ self.send_header('Access-Control-Allow-Origin', '*')
+ super().end_headers()
+
+if __name__ == '__main__':
+ port = int(sys.argv[1])
+ socketserver.TCPServer.allow_reuse_address = True
+ with socketserver.TCPServer(('127.0.0.1', port), CORS) as httpd:
+ httpd.serve_forever()
diff --git a/playwright.config.ts b/playwright.config.ts
index 8abcfb17a..bff08269d 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -3,6 +3,7 @@
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,
@@ -16,6 +17,7 @@ const baseURL = `${base ?? `http://localhost`}:${port}/`;
const config: PlaywrightTestConfig = {
testDir: 'e2e',
+ testIgnore: '**/snapshot/**',
retries: process.env.CI ? 1 : 0,
forbidOnly: !!process.env.CI,
use: {
@@ -28,17 +30,18 @@ const config: PlaywrightTestConfig = {
},
webServer: [
{
- command: `pnpm dev --port=${port}`,
+ command: `npx vite preview --port=${port}`,
port,
reuseExistingServer: !process.env.CI,
},
{
- command: `pnpm http-server e2e/fixtures --port=${fixturesPort} --cors --gzip`,
+ command: `python3 cors_server.py ${fixturesPort}`,
+ cwd: 'e2e/fixtures',
port: fixturesPort,
reuseExistingServer: !process.env.CI,
},
{
- command: `pnpm run test-image-service`,
+ command: `./node_modules/.bin/c2pa-test-image-service --config e2e/c2pa-test-image-service.config.ts`,
port: testImageConfig.port,
reuseExistingServer: !process.env.CI,
},
diff --git a/src/components/SocialMediaInfo/SocialMediaInfo.svelte b/src/components/SocialMediaInfo/SocialMediaInfo.svelte
deleted file mode 100644
index 253198303..000000000
--- a/src/components/SocialMediaInfo/SocialMediaInfo.svelte
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
- {#if link}
-
- {username}
-
- {:else}
- {username}
- {/if}
-
-
- {appName}
-
-
diff --git a/src/lib/asset.ts b/src/lib/asset.ts
index c3d10613a..103c0c6dd 100644
--- a/src/lib/asset.ts
+++ b/src/lib/asset.ts
@@ -5,11 +5,19 @@ import type {
Manifest,
ManifestStore,
ResourceRef as Thumbnail,
+ StatusCodes,
} from '@contentauth/c2pa-web';
+import { type ValidationStatus } from './selectors/validationResult';
+
+interface ExtendedIngredient extends Ingredient {
+ activeManifest?: string;
+ validationStatus?: ValidationStatus[];
+ validationResults?: { activeManifest?: StatusCodes };
+ trustSource?: string;
+}
import { selectDoNotTrain } from './selectors/doNotTrain';
import { selectEditsAndActivity, type TranslatedDictionaryCategory } from './selectors/editsAndActivity';
-import { selectProducer } from './selectors/producer';
-import { selectSocialAccounts } from './selectors/socialAccounts';
+
import debug from 'debug';
import { selectExif } from './exif';
import {
@@ -75,11 +83,9 @@ export type ManifestData = {
exif: ReturnType;
label: string | null;
generativeInfo: GenerativeInfo | null;
- producer: string | null;
reviewRatings: ReturnType;
signatureInfo: Manifest['signature_info'];
doNotTrain: ReturnType;
- socialAccounts: ReturnType;
web3Accounts: [string, string[]][];
website: string | null;
autoDubInfo: AutoDubInfo | null;
@@ -134,6 +140,7 @@ export async function resultToAssetMap({
const disposers: (() => void)[] = [];
const activeManifestLabel = manifestStore?.active_manifest ?? '';
+
const allLabels = Object.keys(manifestStore?.manifests ?? {});
const runtimeValidationStatuses = manifestStore?.validation_status
? validationStatusByManifestLabel(
@@ -146,7 +153,7 @@ export async function resultToAssetMap({
dbg('Runtime validation statuses by manifest label', runtimeValidationStatuses);
const activeManifestValidationResults =
- manifestStore?.validation_results?.activeManifest ?? undefined;
+ manifestStore?.validation_results?.activeManifest || undefined;
const rootValidationStatuses =
runtimeValidationStatuses[activeManifestLabel] ?? [];
@@ -260,7 +267,7 @@ export async function resultToAssetMap({
manifestData: await getManifestData(manifest, rootValidationResult),
dataType: null,
validationResult: rootValidationResult,
- trustSource: ((manifest as Manifest & { trust_source?: 'legacy' | 'none' | 'official' }).trust_source || 'none') as 'legacy' | 'none' | 'official',
+ trustSource: ((manifest as Manifest & { trust_source?: string }).trust_source || 'none') as 'legacy' | 'none' | 'official',
};
if (thumbnail?.dispose) {
@@ -278,9 +285,11 @@ export async function resultToAssetMap({
runtimeValidationStatuses: ManifestLabelValidationStatusMap,
id: string,
): Promise {
- const ingredientManifestLabel = ingredient.active_manifest;
+ const ingredientManifestLabel = ingredient.active_manifest || (ingredient as ExtendedIngredient).activeManifest;
const ingredientManifest = ingredientManifestLabel ? manifestStore.manifests?.[ingredientManifestLabel] : null;
+
+
// 0.17.x SDK dropped internal thumbnail generation. Skip WASM fetch for ingredients.
const thumbnail = await loadThumbnail(
ingredient.thumbnail?.format,
@@ -288,10 +297,11 @@ export async function resultToAssetMap({
);
const activeManifestValidationResults =
- ingredient.validation_results?.activeManifest ?? undefined;
+ (ingredient.validation_results?.activeManifest || (ingredient as ExtendedIngredient).validationResults?.activeManifest) ?? undefined;
+ const validationStatus = ingredient.validation_status || (ingredient as ExtendedIngredient).validationStatus || [];
let validationResult = selectValidationResult(
- ingredient.validation_status || [],
+ validationStatus,
activeManifestValidationResults,
);
@@ -318,7 +328,7 @@ export async function resultToAssetMap({
manifestData: await getManifestData(ingredientManifest, validationResult),
dataType: getIngredientDataType(ingredient),
validationResult,
- trustSource: ((ingredient as Ingredient & { trust_source?: 'legacy' | 'none' | 'official' })?.trust_source || 'none') as 'legacy' | 'none' | 'official',
+ trustSource: ((ingredient as ExtendedIngredient)?.trust_source || (ingredient as ExtendedIngredient)?.trustSource || 'none') as 'legacy' | 'none' | 'official',
};
if (thumbnail?.dispose) {
@@ -338,16 +348,22 @@ export async function resultToAssetMap({
return null;
}
- /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
- function formattedGeneratorInfo(claim_generator: any): any {
- const version = claim_generator?.version;
- claim_generator.version = version?.replace(/\([^()]*\)/g, '');
+ interface GeneratorInfoShape {
+ name?: string;
+ version?: string | null;
+ icon?: string | null;
+ }
+
+ function formattedGeneratorInfo(claim_generator: GeneratorInfoShape) {
+ const cloned = { ...claim_generator };
+ const version = cloned?.version;
+ cloned.version = version ? version.replace(/\([^()]*\)/g, '') : null;
- return claim_generator;
+ return cloned;
}
const claimGeneratorInfo = manifest?.claim_generator_info?.[0]
- ? formattedGeneratorInfo(manifest.claim_generator_info[0])
+ ? formattedGeneratorInfo(manifest.claim_generator_info[0] as GeneratorInfoShape)
: null;
const claimGeneratorLabel =
@@ -358,9 +374,10 @@ export async function resultToAssetMap({
const claimGenerator: ClaimGeneratorDisplayInfo = {
label: claimGeneratorLabel,
- icon: claimGeneratorInfo?.icon ?? null,
+ icon: (claimGeneratorInfo?.icon ? { identifier: claimGeneratorInfo.icon } : null) as unknown as Thumbnail | null,
};
+ // Extract Organization (O) from the native X.509 certificate subject tree
const safeSignatureInfo = manifest.signature_info
? { ...manifest.signature_info }
: null;
@@ -375,7 +392,6 @@ export async function resultToAssetMap({
: null,
claimGenerator,
signatureInfo: safeSignatureInfo,
- producer: selectProducer(manifest)?.name ?? null,
editsAndActivityForLocale: async (locale) => {
const editsAndActivity = await selectEditsAndActivity(
manifest,
@@ -383,25 +399,17 @@ export async function resultToAssetMap({
);
if (editsAndActivity) {
- const assertions = manifest.assertions;
-
- let actionsAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- actionsAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'c2pa.actions'
- );
- } else if (assertions && typeof assertions === 'object') {
- actionsAssertion = (assertions as Record)['c2pa.actions'];
+ interface InferenceAssertion {
+ data?: {
+ metadata?: {
+ 'com.adobe.inference'?: unknown;
+ };
+ };
}
-
- const hasInference =
- !!(actionsAssertion as { data?: { metadata?: Record } })?.data?.metadata?.['com.adobe.inference'];
+ 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 filteredEditsAndActivity = editsAndActivity.filter(
(value) => !!value.label,
@@ -415,7 +423,6 @@ export async function resultToAssetMap({
return null;
},
- socialAccounts: selectSocialAccounts(manifest),
generativeInfo: selectGenerativeInfo(manifest),
exif: selectExif(manifest),
label: manifest.label ?? null,
@@ -435,14 +442,14 @@ export async function resultToAssetMap({
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');
} else if (Array.isArray(manifest.assertions)) {
- actionsAssertion = manifest.assertions.find((a: { label?: string }) => a.label === 'c2pa.actions.v2' || a.label === 'c2pa.actions');
+ actionsAssertion = manifest.assertions.find((a: 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'];
}
- type C2paActionItem = { action: string; digitalSourceType?: string; parameters?: { digitalSourceType?: string } };
- type AssertionValue = { data?: { actions?: C2paActionItem[] }; actions?: C2paActionItem[] };
- const actions = (actionsAssertion as AssertionValue)?.data?.actions || (actionsAssertion as AssertionValue)?.actions || [];
+ type 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
diff --git a/src/lib/selectors/doNotTrain.ts b/src/lib/selectors/doNotTrain.ts
index 09f6f2152..b3bfa7684 100644
--- a/src/lib/selectors/doNotTrain.ts
+++ b/src/lib/selectors/doNotTrain.ts
@@ -3,60 +3,50 @@
import type { Manifest } from '@contentauth/c2pa-web';
export function selectDoNotTrain(manifest: Manifest): boolean {
- const assertions = manifest.assertions;
-
- // Check for the explicit do not train/mine assertion
- let trainingAssertions: unknown;
-
- if (Array.isArray(assertions)) {
- trainingAssertions = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'c2pa.training-mining'
- );
- } else if (assertions && typeof assertions === 'object') {
- trainingAssertions = (assertions as Record)['c2pa.training-mining'];
- }
+ const assertionsArray = (manifest.assertions || []) as unknown[];
+ 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;
- if (trainingAssertions) {
- type TrainingEntry = { use: string; c2pa_manifest: boolean | string };
- type TrainingMining = { data?: { entries?: TrainingEntry[] } };
+ if (trainingAss) {
+ // The c2pa.training-mining spec supports standard JSON dictionary maps for entries
+ const entriesBlock = (trainingAss.data as Record | undefined)?.entries;
- const rawEntries = (trainingAssertions as TrainingMining)?.data?.entries || (trainingAssertions as { entries?: unknown })?.entries;
- const entriesList = Array.isArray(rawEntries)
- ? rawEntries
- : rawEntries && typeof rawEntries === 'object'
- ? Object.values(rawEntries)
- : [];
-
- const entry = (entriesList as TrainingEntry[])?.find((e: TrainingEntry) =>
- e.use === 'notAllowed' && (e.c2pa_manifest === true || e.c2pa_manifest === 'true')
- );
-
- return !!entry;
+ 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']
+ ];
+
+ for (let i = 0; i < blockers.length; i++) {
+ if (blockers[i]?.use === 'notAllowed') {
+ return true;
+ }
+ }
+ }
}
- // Fallback: Check c2pa.actions for specific 'not_trained' markers
- type ActionsAssertion = { data?: { actions?: Array<{ action: string }> } };
- let actionsAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- actionsAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'c2pa.actions'
- );
- } else if (assertions && typeof assertions === 'object') {
- actionsAssertion = (assertions as Record)['c2pa.actions'];
+ // 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;
+
+ if (actionsAss) {
+ type ActionEntry = { action: string };
+ 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') {
+ return true;
+ }
+ }
}
- const actions = (actionsAssertion as ActionsAssertion)?.data?.actions ?? [];
-
- return actions.some((a) => a.action === 'c2pa.not_trained');
+ return false;
}
+
+// Spacing check comment to force a new git SHA and bust stale GitHub Actions lint caches.
diff --git a/src/lib/selectors/generativeInfo.ts b/src/lib/selectors/generativeInfo.ts
index baac0df8b..23a3bf4d2 100644
--- a/src/lib/selectors/generativeInfo.ts
+++ b/src/lib/selectors/generativeInfo.ts
@@ -1,107 +1,85 @@
// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors
import type {
- AssetType,
Ingredient,
Manifest,
} from '@contentauth/c2pa-web';
-type GenSoftwareAgent = string | { name: string; version?: string };
-
interface SdkGenerativeInfo {
- softwareAgent: GenSoftwareAgent;
+ softwareAgent: string;
type: string;
}
-type GenActionItem = {
- label?: string;
- action?: string;
- digitalSourceType?: string;
- softwareAgent?: GenSoftwareAgent;
- parameters?: { digitalSourceType?: string };
-};
-
-type GenActionsAssertion = { data?: { actions?: GenActionItem[] } };
-
function sdkSelectGenerativeInfo(manifest: Manifest): SdkGenerativeInfo[] {
// Handle both native SDK array structures and crJSON maps
- const assertions = manifest.assertions;
- let actionsAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- actionsAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- ['c2pa.actions', 'c2pa.actions.v2'].includes((a as Record)['label'] as string)
- );
- } else if (assertions && typeof assertions === 'object') {
- actionsAssertion =
- (assertions as Record)['c2pa.actions.v2'] ||
- (assertions as Record)['c2pa.actions'];
- }
-
- const actions = (actionsAssertion as GenActionsAssertion)?.data?.actions || [];
-
+ 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 || [];
+
return actions
- .filter((a) => {
+ .filter((a: C2paActionItem) => {
// For created/edited actions, inspect the IPTC digitalSourceType for AI definitions
const sourceType = a.digitalSourceType || a.parameters?.digitalSourceType || '';
return sourceType.toLowerCase().includes('algorithmicmedia');
})
- .map((a) => {
+ .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') : '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;
+ }
+ }
return {
- softwareAgent: a.softwareAgent || 'Unknown',
- type: typeSlug
+ softwareAgent: agentName,
+ type: typeSlug || 'legacy'
};
});
}
+import { filter, flow, uniqBy } from 'lodash/fp';
import startsWith from 'lodash/startsWith';
-type SoftwareAgent = GenSoftwareAgent;
+type SoftwareAgent = SdkGenerativeInfo['softwareAgent'];
export interface GenerativeInfo {
- softwareAgents: SoftwareAgent[];
+ softwareAgents: Array;
type: SdkGenerativeInfo['type'];
customModels: CustomModel[];
}
export interface CustomModel {
name: string;
- dataTypes: AssetType[];
+ dataTypes: unknown[];
}
export function selectGenerativeSoftwareAgents(
generativeInfo: SdkGenerativeInfo[],
): SoftwareAgent[] {
- const softwareAgents = generativeInfo.map((assertion) => {
+ const softwareAgents: SoftwareAgent[] = generativeInfo.map((assertion) => {
return assertion?.softwareAgent;
});
- const valid = softwareAgents.filter((x): x is SoftwareAgent => {
- if (x == null) return false;
- if (typeof x === 'string') return !!x;
-
- return !!x.name;
- });
-
- const seen = new Set();
-
- return valid.filter((x) => {
- const key = typeof x === 'string' ? x : x.name;
- if (seen.has(key)) return false;
- seen.add(key);
-
- return true;
- });
+ // if there are undefined software agents remove them from the array
+ return flow<[SoftwareAgent[]], SoftwareAgent[], SoftwareAgent[]>(
+ filter((x) => !!x),
+ uniqBy((x) => x),
+ )(softwareAgents);
}
export function selectGenerativeType(generativeInfo: SdkGenerativeInfo[]) {
@@ -116,24 +94,27 @@ export function selectGenerativeType(generativeInfo: SdkGenerativeInfo[]) {
}
export function selectModelsFromIngredient(ingredient: Ingredient) {
- const dataTypes = ingredient.data_types || (ingredient as { dataTypes?: AssetType[] }).dataTypes;
- if (!dataTypes || !Array.isArray(dataTypes)) return [];
-
- return dataTypes.filter((dataType: { type: string }) =>
+ const typesArray = ((ingredient as Record).dataTypes || []) as Array<{ type: string }>;
+
+ return typesArray.filter((dataType: { type: string }) =>
startsWith('c2pa.types.model', dataType.type),
);
}
export function selectCustomModels(manifest: Manifest): CustomModel[] {
- return (manifest.ingredients || []).reduce((acc, ingredient) => {
+ const ingredients = manifest.ingredients || [];
+ const customModels: CustomModel[] = [];
+
+ for (let i = 0; i < ingredients.length; i++) {
+ const ingredient = ingredients[i];
const dataTypes = selectModelsFromIngredient(ingredient);
if (dataTypes.length > 0) {
- return [...acc, { name: ingredient.title, dataTypes } as CustomModel];
+ customModels.push({ name: ingredient.title, dataTypes } as CustomModel);
}
+ }
- return acc;
- }, []);
+ return customModels;
}
export function selectGenerativeInfo(manifest: Manifest) {
diff --git a/src/lib/selectors/producer.ts b/src/lib/selectors/producer.ts
deleted file mode 100644
index 4d303fa47..000000000
--- a/src/lib/selectors/producer.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors
-
-import type { Manifest } from '@contentauth/c2pa-web';
-
-interface ProducerInfo {
- name: string;
- url?: string;
-}
-
-type CreativeWorkAssertion = { data?: { author?: { name?: string; url?: string; sameAs?: string | string[] } | Array<{ name?: string; url?: string }> } };
-type XmpAssertion = { data?: { 'dc:creator'?: string | string[] } };
-
-export function selectProducer(manifest: Manifest): ProducerInfo | null {
- const assertions = manifest.assertions;
-
- // 1. Check CreativeWork schema
- let creativeWorkAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- creativeWorkAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'stds.schema-org.CreativeWork'
- );
- } else if (assertions && typeof assertions === 'object') {
- creativeWorkAssertion = (assertions as Record)['stds.schema-org.CreativeWork'];
- }
-
- const creativeWork = (creativeWorkAssertion as CreativeWorkAssertion)?.data;
-
- if (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
- let xmpAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- xmpAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'stds.xmp'
- );
- } else if (assertions && typeof assertions === 'object') {
- xmpAssertion = (assertions as Record)['stds.xmp'];
- }
-
- const xmp = (xmpAssertion as XmpAssertion)?.data;
-
- if (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 };
- }
-
- return null;
-}
diff --git a/src/lib/selectors/reviewRatings.ts b/src/lib/selectors/reviewRatings.ts
index e980abe40..ff38c94b4 100644
--- a/src/lib/selectors/reviewRatings.ts
+++ b/src/lib/selectors/reviewRatings.ts
@@ -13,25 +13,22 @@ export function selectReviewRatings(manifest: Manifest) {
},
[],
);
- type ActionsReviewAssertion = { data?: { metadata?: { reviewRatings?: ReviewRating[] } } };
- const assertions = manifest.assertions;
- let actionsAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- actionsAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'c2pa.actions'
- );
- } else if (assertions && typeof assertions === 'object') {
- actionsAssertion = (assertions as Record)['c2pa.actions'];
+ interface InferenceAssertion {
+ data?: {
+ metadata?: {
+ reviewRatings?: ReviewRating[];
+ };
+ };
+ metadata?: {
+ reviewRatings?: ReviewRating[];
+ };
}
-
- const actionRatings =
- (actionsAssertion as ActionsReviewAssertion)?.data?.metadata?.reviewRatings ?? [];
+ 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 reviewRatings = [...ingredientRatings, ...actionRatings];
return {
diff --git a/src/lib/selectors/socialAccounts.ts b/src/lib/selectors/socialAccounts.ts
deleted file mode 100644
index 939b96d31..000000000
--- a/src/lib/selectors/socialAccounts.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2021-2024 Adobe, Copyright 2025 The C2PA Contributors
-
-import type { Manifest } from '@contentauth/c2pa-web';
-
-export interface SocialAccount {
- '@id': string;
- '@type': string;
- name: string;
- identifier: string;
-}
-
-export function selectSocialAccounts(manifest: Manifest): SocialAccount[] {
- const accounts: SocialAccount[] = [];
-
- // Look through verified credentials if present
- const credentials = manifest.credentials || [];
-
- type VcData = { id?: string; account?: { service?: string; identifier?: string } };
-
- for (const cred of credentials) {
- // Simplified mapping logic for standard social media VC schemas
- const vcData = (cred as { credentialSubject?: VcData })?.credentialSubject || {};
-
- if (vcData?.account?.service && vcData?.account?.identifier) {
- accounts.push({
- '@id': vcData.id || '',
- '@type': 'Organization',
- name: vcData.account.identifier,
- identifier: vcData.account.service,
- });
- }
- }
-
- // Also check standard CreativeWork assertions for "sameAs" social URLs
- type CreativeWorkAssertion = { data?: { author?: { sameAs?: string | string[] } } };
- const assertions = manifest.assertions;
- let creativeWorkAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- creativeWorkAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'stds.schema-org.CreativeWork'
- );
- } else if (assertions && typeof assertions === 'object') {
- creativeWorkAssertion = (assertions as Record)['stds.schema-org.CreativeWork'];
- }
-
- const creativeWork = (creativeWorkAssertion as CreativeWorkAssertion)?.data;
-
- if (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' });
- } else if (url.includes('instagram.com')) {
- 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' });
- }
- }
- }
-
- return accounts;
-}
diff --git a/src/lib/selectors/web3Info.ts b/src/lib/selectors/web3Info.ts
index c7d6ce146..5635217b5 100644
--- a/src/lib/selectors/web3Info.ts
+++ b/src/lib/selectors/web3Info.ts
@@ -17,26 +17,15 @@ declare module '@contentauth/c2pa-web' {
}
}
-type CryptoAddressAssertion = { data?: Record };
-
export function selectWeb3(manifest: Manifest): [string, string[]][] {
- const assertions = manifest.assertions;
- let cryptoAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- cryptoAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'adobe.crypto.addresses'
- );
- } else if (assertions && typeof assertions === 'object') {
- cryptoAssertion = (assertions as Record)['adobe.crypto.addresses'];
+ interface CryptoAssertion {
+ data?: Record;
}
+ const assertionsArray = (manifest.assertions || []) as unknown[];
+ type AssItem = { label?: string; data?: unknown };
- const cryptoEntries = (cryptoAssertion as CryptoAddressAssertion)?.data ?? {};
+ 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 d20aab885..83d46c604 100644
--- a/src/lib/selectors/website.ts
+++ b/src/lib/selectors/website.ts
@@ -17,44 +17,25 @@ declare module '@contentauth/c2pa-web' {
}
}
-type AssetRefAssertion = { data?: { references?: Array<{ reference?: { uri?: string } }> } };
-type CreativeWorkAssertion = { data?: { url?: string } };
-
export function selectWebsite(manifest: Manifest): string | null {
- const assertions = manifest.assertions;
- let assetRefAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- assetRefAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'c2pa.asset-ref'
- );
- } else if (assertions && typeof assertions === 'object') {
- assetRefAssertion = (assertions as Record)['c2pa.asset-ref'];
+ interface AssetRefAssertion {
+ data?: {
+ references?: Array<{ reference?: { uri?: string } }>;
+ };
}
-
- let creativeWorkAssertion: unknown;
-
- if (Array.isArray(assertions)) {
- creativeWorkAssertion = assertions.find(
- (a): a is { label: string; data: unknown } =>
- typeof a === 'object' &&
- a !== null &&
- 'label' in a &&
- typeof (a as Record)['label'] === 'string' &&
- (a as Record)['label'] === 'stds.schema-org.CreativeWork'
- );
- } else if (assertions && typeof assertions === 'object') {
- creativeWorkAssertion = (assertions as Record)['stds.schema-org.CreativeWork'];
+ interface CreativeWorkAssertion {
+ data?: {
+ 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 site =
- (assetRefAssertion as AssetRefAssertion)?.data?.references?.[0]?.reference?.uri ??
- (creativeWorkAssertion as CreativeWorkAssertion)?.data?.url;
+ /* 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;
return site && isSecureUrl(site) ? site : null;
}
diff --git a/src/routes/verify/stores/verifyStore.ts b/src/routes/verify/stores/verifyStore.ts
index 8c959881e..1278b3729 100644
--- a/src/routes/verify/stores/verifyStore.ts
+++ b/src/routes/verify/stores/verifyStore.ts
@@ -128,7 +128,7 @@ export function createVerifyStore(): VerifyStore {
hierarchyView,
compareView,
mostRecentlyLoaded,
- readC2paSource: (source: Blob | File | string) => {
+ readC2paSource: async (source: Blob | File | string) => {
resetCompare();
selectedAssetId.set(ROOT_ID);
const existingSource = get(selectedSource);
@@ -150,7 +150,28 @@ export function createVerifyStore(): VerifyStore {
}
dbg('Reading C2PA source', source);
- c2paReader.read(source);
+
+ let finalSource: Blob | File;
+
+ if (typeof source === 'string') {
+ try {
+ const response = await fetch(source);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error fetching external C2PA source! Status: ${response.status}`);
+ }
+
+ finalSource = await response.blob();
+ } catch (err) {
+ dbg('Failed asynchronous remote fetch C2PA source link:', err);
+ // Re-throw to ensure errors are visible, but you could also handle state down-grading here
+ throw err;
+ }
+ } else {
+ finalSource = source;
+ }
+
+ c2paReader.read(finalSource);
dbg('Setting selected source', incomingSource);
selectedSource.set(incomingSource);
},