Your Credentials
+Your Credentials
Add New Credential
-Add New Credential
+- Download Test Credential -
+Download Test Credential
+
+
+
+ {isExpanded ? : }
+
{credentialType}
- {hasCredential &&
- ` (${
- version === "unknown" ? version + " version" : "v" + version
- })`}
+ {hasCredential && ` (${version === 'unknown' ? version + ' version' : 'v' + version})`}
+ {extensionCredentialType && (
+
+ {extensionCredentialType}
+ {extensionVersion === 'unknown' ? 'unknown' : ` (v${extensionVersion})`}
+
+ )}
-
- {hasCredential && proofType !== "none" && (
+
+ {hasCredential && proofType !== 'none' && (
{proofType} proof
@@ -115,15 +97,11 @@ const TestGroup = ({
{isExpanded && (
-
+
{steps.map((step) => (
))}
- {!hasCredential && (
-
- Upload a credential to begin validation
-
- )}
+ {!hasCredential && Upload a credential to begin validation
}
)}
@@ -136,39 +114,35 @@ const TestStepItem = ({ step }: { step: TestStep }) => {
const shouldShowDetails =
step.details &&
((step.details.errors && step.details.errors.length > 0) ||
- (step.details.additionalProperties &&
- Object.keys(step.details.additionalProperties).length > 0));
+ (step.details.additionalProperties && Object.keys(step.details.additionalProperties).length > 0));
return (
-
-
-
+
+
+
{step.name}
{step.details &&
- step.id === "schema" &&
- (step.details.errors?.[0]?.message === "Failed to fetch schema" ? (
- Failed to load schema
+ (step.id === 'schema' || step.id === 'extension-schema') &&
+ (step.details.errors?.[0]?.message === 'Failed to fetch schema' ? (
+ Failed to load schema
) : shouldShowDetails ? (
-
-
+
Validation Details
-
+
{step.details.errors && step.details.errors.length > 0 ? (
-
+
) : (
-
+
⚠️ Additional properties found in credential
)}
@@ -181,23 +155,17 @@ const TestStepItem = ({ step }: { step: TestStep }) => {
);
};
-const StatusIcon = ({
- status,
- size = "default",
-}: {
- status: TestStep["status"];
- size?: "sm" | "default";
-}) => {
- const sizeClass = size === "sm" ? "h-3 w-3" : "h-4 w-4";
+const StatusIcon = ({ status, size = 'default' }: { status: TestStep['status']; size?: 'sm' | 'default' }) => {
+ const sizeClass = size === 'sm' ? 'h-3 w-3' : 'h-4 w-4';
switch (status) {
- case "success":
+ case 'success':
return ;
- case "failure":
+ case 'failure':
return ;
- case "in-progress":
+ case 'in-progress':
return ;
- case "missing":
+ case 'missing':
return ;
default:
return ;
@@ -218,52 +186,58 @@ export function TestResults({
const validatedCredentialsRef = useRef({});
const previousCredentialsRef = useRef(credentials);
- const initializeTestSteps = (credential?: {
- original: any;
- decoded: Credential;
- }) => {
+ const initializeTestSteps = (credential?: { original: any; decoded: Credential }) => {
if (!credential) {
return [
{
- id: "proof-type",
- name: "Proof Type Detection",
- status: "missing",
+ id: 'proof-type',
+ name: 'Proof Type Detection',
+ status: 'missing',
},
{
- id: "verification",
- name: "Credential Verification",
- status: "missing",
+ id: 'verification',
+ name: 'Credential Verification',
+ status: 'missing',
},
{
- id: "schema",
- name: "Schema Validation",
- status: "missing",
+ id: 'schema',
+ name: 'Schema Validation',
+ status: 'missing',
},
];
}
- return [
+ const steps = [
{
- id: "proof-type",
- name: "Proof Type Detection",
- status: "success",
+ id: 'proof-type',
+ name: 'Proof Type Detection',
+ status: 'success',
details: {
- type: isEnvelopedProof(credential.original)
- ? "enveloping"
- : "embedded",
+ type: isEnvelopedProof(credential.original) ? 'enveloping' : 'embedded',
},
},
{
- id: "verification",
- name: "Credential Verification",
- status: "pending",
+ id: 'verification',
+ name: 'Credential Verification',
+ status: 'pending',
},
{
- id: "schema",
- name: "Schema Validation",
- status: "pending",
+ id: 'schema',
+ name: 'Schema Validation',
+ status: 'pending',
},
];
+
+ const extension = detectExtension(credential.decoded);
+
+ if (extension) {
+ steps.push({
+ id: 'extension-schema',
+ name: 'Extension Schema Validation',
+ status: 'pending',
+ });
+ }
+ return steps;
};
// First useEffect for initializing test steps
@@ -300,79 +274,66 @@ export function TestResults({
// Set in-progress state
setTestResults((prev) => ({
...prev,
- [type as CredentialType]: prev[type as CredentialType]?.map(
- (step) =>
- step.id === "verification" || step.id === "schema"
- ? { ...step, status: "in-progress" }
- : step
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'verification' || step.id === 'schema' ? { ...step, status: 'in-progress' } : step,
),
}));
// Verification
- const verificationResult = await verifyCredential(
- credential.original
- );
+ const verificationResult = await verifyCredential(credential.original);
setTestResults((prev) => ({
...prev,
- [type as CredentialType]: prev[type as CredentialType]?.map(
- (step) =>
- step.id === "verification"
- ? {
- ...step,
- status: verificationResult.verified
- ? "success"
- : "failure",
- details: verificationResult,
- }
- : step
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'verification'
+ ? {
+ ...step,
+ status: verificationResult.verified ? 'success' : 'failure',
+ details: verificationResult,
+ }
+ : step,
),
}));
if (!verificationResult.verified) {
const errorMessage =
- typeof verificationResult.error === "object"
- ? verificationResult.error.message ||
- "The credential could not be verified"
- : verificationResult.error ||
- "The credential could not be verified";
+ typeof verificationResult.error === 'object'
+ ? verificationResult.error.message || 'The credential could not be verified'
+ : verificationResult.error || 'The credential could not be verified';
- toast.error("Credential verification failed", {
+ toast.error('Credential verification failed', {
description: errorMessage,
});
}
+ // Store reference to validated credential
+ validatedCredentialsRef.current[credentialType] = {
+ credential: {
+ original: credential.original,
+ decoded: credential.decoded,
+ },
+ validated: true,
+ };
+
+ const extension = detectExtension(credential.decoded);
+
// Schema validation
try {
- const validationResult = await validateCredentialSchema(
- credential.decoded
- );
+ const validationResult = await validateCredentialSchema(credential.decoded);
setTestResults((prev) => ({
...prev,
- [type as CredentialType]: prev[type as CredentialType]?.map(
- (step) =>
- step.id === "schema"
- ? {
- ...step,
- status: validationResult.valid ? "success" : "failure",
- details: validationResult,
- }
- : step
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'schema'
+ ? {
+ ...step,
+ status: validationResult.valid ? 'success' : 'failure',
+ details: validationResult,
+ }
+ : step,
),
}));
- // Store reference to validated credential
- validatedCredentialsRef.current[credentialType] = {
- credential: {
- original: credential.original,
- decoded: credential.decoded,
- },
- validated: true,
- };
-
- if (validationResult.valid) {
- if (
- !validatedCredentialsRef.current[credentialType]?.confettiShown
- ) {
+ if (!extension && validationResult.valid) {
+ if (!validatedCredentialsRef.current[credentialType]?.confettiShown) {
confetti({
particleCount: 200,
spread: 90,
@@ -386,34 +347,91 @@ export function TestResults({
}
}
} catch (error) {
- console.log("Schema validation error:", error);
- toast.error("Failed to fetch schema. Please try again.");
+ console.log('Schema validation error:', error);
+ toast.error('Failed to fetch schema. Please try again.');
// Only update the schema validation step
setTestResults((prev) => ({
...prev,
- [type as CredentialType]: prev[type as CredentialType]?.map(
- (step) =>
- step.id === "schema"
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'schema'
+ ? {
+ ...step,
+ status: 'failure',
+ details: {
+ errors: [
+ {
+ keyword: 'schema',
+ message: 'Failed to fetch schema',
+ instancePath: '',
+ },
+ ],
+ },
+ }
+ : step,
+ ),
+ }));
+ }
+
+ // Extension schema validation
+ if (extension) {
+ try {
+ const extensionValidationResult = await validateExtension(credential.decoded);
+ setTestResults((prev) => ({
+ ...prev,
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'extension-schema'
? {
...step,
- status: "failure",
+ status: extensionValidationResult.valid ? 'success' : 'failure',
+ details: extensionValidationResult,
+ }
+ : step,
+ ),
+ }));
+ if (extensionValidationResult.valid) {
+ if (!validatedCredentialsRef.current[credentialType]?.confettiShown) {
+ confetti({
+ particleCount: 200,
+ spread: 90,
+ origin: { y: 0.7 },
+ });
+
+ validatedCredentialsRef.current[credentialType] = {
+ ...validatedCredentialsRef.current[credentialType]!,
+ confettiShown: true,
+ };
+ }
+ }
+ } catch (error) {
+ console.log('Extension schema validation error:', error);
+ toast.error('Failed to fetch extension schema. Please try again.');
+
+ // Only update the schema validation step
+ setTestResults((prev) => ({
+ ...prev,
+ [type as CredentialType]: prev[type as CredentialType]?.map((step) =>
+ step.id === 'extension-schema'
+ ? {
+ ...step,
+ status: 'failure',
details: {
errors: [
{
- keyword: "schema",
- message: "Failed to fetch schema",
- instancePath: "",
+ keyword: 'schema',
+ message: 'Failed to fetch extension schema',
+ instancePath: '',
},
],
},
}
- : step
- ),
- }));
+ : step,
+ ),
+ }));
+ }
}
} catch (error) {
- console.log("Error processing credential:", error);
+ console.log('Error processing credential:', error);
}
};
@@ -422,32 +440,28 @@ export function TestResults({
}, [credentials]);
const toggleGroup = (groupId: string) => {
- setExpandedGroups((prev) =>
- prev.includes(groupId)
- ? prev.filter((id) => id !== groupId)
- : [...prev, groupId]
- );
+ setExpandedGroups((prev) => (prev.includes(groupId) ? prev.filter((id) => id !== groupId) : [...prev, groupId]));
};
return (
-
+
{ALL_CREDENTIAL_TYPES.map((type) => {
const credential = credentials[type];
const steps = testResults[type] || [];
const hasCredential = !!credential;
- const proofType = credential
- ? isEnvelopedProof(credential.original)
- ? "enveloping"
- : "embedded"
- : "none";
+ const extension = hasCredential ? detectExtension(credential.decoded) : null;
+ const version = hasCredential ? extension?.core?.version || detectVersion(credential.decoded) : 'unknown';
+ const extensionCredentialType = extension?.extension?.type;
+ const extensionVersion = extension?.extension?.version;
+ const proofType = credential ? (isEnvelopedProof(credential.original) ? 'enveloping' : 'embedded') : 'none';
return (
toggleGroup(type)}
@@ -463,19 +477,17 @@ export function TestResults({
// Helper function to extract version
function extractVersion(credential: Credential): string {
try {
- if (!credential["@context"] || !Array.isArray(credential["@context"])) {
- return "unknown";
+ if (!credential['@context'] || !Array.isArray(credential['@context'])) {
+ return 'unknown';
}
- const contextUrl = credential["@context"].find(
+ const contextUrl = credential['@context'].find(
(ctx) =>
- typeof ctx === "string" &&
- (ctx.includes("vocabulary.uncefact.org") ||
- ctx.includes("test.uncefact.org"))
+ typeof ctx === 'string' && (ctx.includes('vocabulary.uncefact.org') || ctx.includes('test.uncefact.org')),
);
- return contextUrl?.match(/\/(\d+\.\d+\.\d+)\//)?.[1] || "unknown";
+ return contextUrl?.match(/\/(\d+\.\d+\.\d+)\//)?.[1] || 'unknown';
} catch {
- return "unknown";
+ return 'unknown';
}
}
diff --git a/packages/untp-playground/src/lib/credentialService.ts b/packages/untp-playground/src/lib/credentialService.ts
index 6273dda2..f6b30d80 100644
--- a/packages/untp-playground/src/lib/credentialService.ts
+++ b/packages/untp-playground/src/lib/credentialService.ts
@@ -1,5 +1,5 @@
-import type { Credential } from "@/types/credential";
-import { jwtDecode } from "jwt-decode";
+import type { Credential } from '@/types/credential';
+import { jwtDecode } from 'jwt-decode';
export function decodeEnvelopedCredential(credential: any): Credential {
if (!isEnvelopedProof(credential)) {
@@ -7,43 +7,42 @@ export function decodeEnvelopedCredential(credential: any): Credential {
}
try {
- const jwtPart = credential.id.split(",")[1];
+ const jwtPart = credential.id.split(',')[1];
if (!jwtPart) {
return credential;
}
return jwtDecode(jwtPart);
} catch (error) {
- console.log("Error processing enveloped credential:", error);
+ console.log('Error processing enveloped credential:', error);
return credential;
}
}
export function detectCredentialType(credential: Credential): string {
const types = [
- "DigitalProductPassport",
- "DigitalConformityCredential",
- "DigitalFacilityRecord",
- "DigitalIdentityAnchor",
- "DigitalTraceabilityEvent",
+ 'DigitalProductPassport',
+ 'DigitalLivestockPassport',
+ 'DigitalConformityCredential',
+ 'DigitalFacilityRecord',
+ 'DigitalIdentityAnchor',
+ 'DigitalTraceabilityEvent',
];
- return credential.type.find((t) => types.includes(t)) || "Unknown";
+ return credential.type.find((t) => types.includes(t)) || 'Unknown';
}
-export function detectVersion(credential: Credential): string {
- const contextUrl = credential["@context"].find((ctx) =>
- ctx.includes("vocabulary.uncefact.org")
- );
+export function detectVersion(credential: Credential, domain?: string): string {
+ const contextUrl = credential['@context'].find((ctx) => ctx.includes(domain || 'test.uncefact.org'));
- if (!contextUrl) return "unknown";
+ if (!contextUrl) return 'unknown';
- const versionMatch = contextUrl.match(/\/(\d+\.\d+\.\d+)\//);
- return versionMatch ? versionMatch[1] : "unknown";
+ const versionMatch = contextUrl.match(/(\d+\.\d+\.\d+)/);
+ return versionMatch ? versionMatch[1] : 'unknown';
}
export function isEnvelopedProof(credential: any): boolean {
const normalizedCredential = credential.verifiableCredential || credential;
- return normalizedCredential.type === "EnvelopedVerifiableCredential";
+ return normalizedCredential.type === 'EnvelopedVerifiableCredential';
}
diff --git a/packages/untp-playground/src/lib/schemaValidation.ts b/packages/untp-playground/src/lib/schemaValidation.ts
index 09523554..bebfd61d 100644
--- a/packages/untp-playground/src/lib/schemaValidation.ts
+++ b/packages/untp-playground/src/lib/schemaValidation.ts
@@ -1,5 +1,6 @@
import addFormats from 'ajv-formats';
import Ajv2020 from 'ajv/dist/2020';
+import { detectCredentialType, detectVersion } from './credentialService';
const ajv = new Ajv2020({
allErrors: true,
@@ -10,29 +11,129 @@ addFormats(ajv);
const schemaCache = new Map();
-const SCHEMA_URLS = {
- DigitalProductPassport: 'https://test.uncefact.org/vocabulary/untp/dpp/untp-dpp-schema-0.5.0.json',
- DigitalConformityCredential: 'https://test.uncefact.org/vocabulary/untp/dcc/untp-dcc-schema-0.5.0.json',
- DigitalTraceabilityEvent: 'https://test.uncefact.org/vocabulary/untp/dte/untp-dte-schema-0.5.0.json',
- DigitalFacilityRecord: 'https://test.uncefact.org/vocabulary/untp/dfr/untp-dfr-schema-0.5.0.json',
- DigitalIdentityAnchor: 'https://test.uncefact.org/vocabulary/untp/dia/untp-dia-schema-0.2.1.json',
+interface CoreVersion {
+ type: string;
+ version: string;
+}
+
+interface ExtensionVersion {
+ version: string;
+ schema: string;
+ core: CoreVersion;
+}
+
+interface ExtensionConfig {
+ domain: string;
+ versions: ExtensionVersion[];
+}
+
+const EXTENSION_VERSIONS: Record = {
+ DigitalLivestockPassport: {
+ domain: 'aatp.foodagility.com',
+ versions: [
+ {
+ version: '0.4.0',
+ schema: 'https://aatp.foodagility.com/assets/files/aatp-dlp-schema-0.4.0-9c0ad2b1ca6a9e497dedcfd8b87f35f1.json',
+ core: { type: 'DigitalProductPassport', version: '0.5.0' },
+ },
+ ],
+ },
+};
+
+const schemaURLConstructor = (type: string, version: string) => {
+ const shortCredentialTypes: Record = {
+ DigitalProductPassport: 'dpp',
+ DigitalConformityCredential: 'dcc',
+ DigitalTraceabilityEvent: 'dte',
+ DigitalFacilityRecord: 'dfr',
+ DigitalIdentityAnchor: 'dia',
+ };
+ return `https://test.uncefact.org/vocabulary/untp/${shortCredentialTypes[type]}/untp-${shortCredentialTypes[type]}-schema-${version}.json`;
+};
+
+const findExtensionSchemaURL = (type: string, version: string) => {
+ return EXTENSION_VERSIONS[type].versions.find((v) => v.version === version)?.schema;
};
export async function validateCredentialSchema(credential: any): Promise<{
valid: boolean;
errors?: any[];
}> {
- try {
- const credentialType = credential.type.find((t: string) => Object.keys(SCHEMA_URLS).includes(t));
+ const extension = detectExtension(credential);
+ const credentialType = extension ? extension.core.type : detectCredentialType(credential);
+
+ if (credentialType === 'Unknown') {
+ throw new Error('Unsupported credential type');
+ }
+
+ const version = extension?.core?.version || detectVersion(credential);
+
+ if (!version) {
+ throw new Error('Unsupported version');
+ }
+
+ const schemaUrl = schemaURLConstructor(credentialType, version);
+
+ if (extension?.core.type === 'DigitalProductPassport' && extension?.core.version === '0.5.0') {
+ const relaxFunction = (schema: any) => {
+ delete schema?.properties?.type?.const;
+ delete schema?.properties?.type?.items?.enum;
+ delete schema?.properties?.['@context']?.const;
+ delete schema?.properties?.['@context']?.items?.enum;
+ return schema;
+ };
+ return validateCredentialOnSchemaUrl(credential, schemaUrl, relaxFunction);
+ }
+
+ return validateCredentialOnSchemaUrl(credential, schemaUrl);
+}
+
+export async function validateExtension(credential: any): Promise<{
+ valid: boolean;
+ errors?: any[];
+}> {
+ const extension = detectExtension(credential);
+ if (!extension) {
+ throw new Error('Unknown extension');
+ }
- if (!credentialType) {
- throw new Error('Unsupported credential type');
+ const schemaUrl = findExtensionSchemaURL(extension.extension.type, extension.extension.version);
+
+ if (!schemaUrl) {
+ throw new Error('Unsupported extension version');
+ }
+
+ return validateCredentialOnSchemaUrl(credential, schemaUrl);
+}
+
+export function detectExtension(credential: any):
+ | {
+ core: { type: string; version: string };
+ extension: { type: string; version: string };
}
+ | undefined {
+ const credentialType = detectCredentialType(credential);
+ const extension = EXTENSION_VERSIONS[credentialType];
+ if (!extension) {
+ return undefined;
+ }
+ const version = detectVersion(credential, extension.domain);
+ const extensionVersion = extension.versions.find((v) => v.version === version);
+ if (!extensionVersion) {
+ return undefined;
+ }
- const schemaUrl = SCHEMA_URLS[credentialType as keyof typeof SCHEMA_URLS];
+ return {
+ core: extensionVersion.core,
+ extension: { type: credentialType, version },
+ };
+}
+async function validateCredentialOnSchemaUrl(credential: any, schemaUrl: string, relaxFunction?: (schema: any) => any) {
+ try {
if (!schemaCache.has(schemaUrl)) {
- const proxyUrl = `/untp-playground/api/schema?url=${encodeURIComponent(schemaUrl)}`;
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_PATH || '';
+ const proxyUrl = `${baseUrl}/api/schema?url=${encodeURIComponent(schemaUrl)}`;
const schemaResponse = await fetch(proxyUrl);
if (!schemaResponse.ok) {
@@ -43,7 +144,11 @@ export async function validateCredentialSchema(credential: any): Promise<{
schemaCache.set(schemaUrl, schema);
}
- const schema = schemaCache.get(schemaUrl);
+ let schema = schemaCache.get(schemaUrl);
+ if (relaxFunction) {
+ schema = relaxFunction(schema);
+ }
+
const validate = ajv.compile(schema);
const isValid = validate(credential);
const errors = validate.errors || [];
{credentialType} - {hasCredential && - ` (${ - version === "unknown" ? version + " version" : "v" + version - })`} + {hasCredential && ` (${version === 'unknown' ? version + ' version' : 'v' + version})`}
+ {extensionCredentialType && ( ++ {extensionCredentialType} + {extensionVersion === 'unknown' ? 'unknown' : ` (v${extensionVersion})`} +
+ )}- Upload a credential to begin validation -
- )} + {!hasCredential &&Upload a credential to begin validation
}⚠️ Additional properties found in credential