Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/w3c-context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,26 @@ The package includes the following pre-cached contexts:

#### TrustVC Business Contexts
- `https://trustvc.io/context/transferable-records-context.json` - Transferable Records
- `https://trustvc.io/context/obligation-records-context.json` - Obligation Records (BoE)
- `https://trustvc.io/context/render-method-context.json` - Render Methods
- `https://trustvc.io/context/render-method-context-v2.json` - Render Methods v2
- `https://trustvc.io/context/attachments-context.json` - Attachments
- `https://trustvc.io/context/qrcode-context.json` - QR Code
- `https://trustvc.io/context/opencerts-context.json` - OpenCerts Certificate
- `https://trustvc.io/context/bill-of-exchange.json` - Bill of Exchange

- `https://trustvc.io/context/bill-of-lading.json` - Bill of Lading
- `https://trustvc.io/context/bill-of-lading-carrier.json` - Bill of Lading Carrier
- `https://trustvc.io/context/coo.json` - Certificate of Origin
- `https://trustvc.io/context/invoice.json` - Invoice
- `https://trustvc.io/context/promissory-note.json` - Promissory Note
- `https://trustvc.io/context/warehouse-receipt.json` - Warehouse Receipt

### Samples

- `samples/obligation-credential-subject.sample.json` - Example Obligation / BoE credential subject payload
- `samples/bill-of-exchange.md` - Form label → property map for finalized BoE fields
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Context Constants

You can import URL constants for type safety:
Expand Down

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions packages/w3c-context/src/context/bill-of-exchange.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"@context": {
"@version": 1.1,
"@protected": true,

"electronicDocumentIdentifier": {
"@id": "https://trustvc.io/context/terms#electronicDocumentIdentifier"
},
"referenceNumber": {
"@id": "https://trustvc.io/context/terms#referenceNumber"
},
"amountInFigures": {
"@id": "https://trustvc.io/context/terms#amountInFigures"
},
"amountInWords": {
"@id": "https://trustvc.io/context/terms#amountInWords"
},
"currencyCode": {
"@id": "https://trustvc.io/context/terms#currencyCode"
},
"blDate": {
"@id": "https://trustvc.io/context/terms#blDate"
},
"placeOfIssue": {
"@id": "https://trustvc.io/context/terms#placeOfIssue"
},
"dateOfIssue": {
"@id": "https://trustvc.io/context/terms#dateOfIssue"
},
"tenor": {
"@id": "https://trustvc.io/context/terms#tenor"
},
"payee": {
"@id": "https://trustvc.io/context/terms#payee"
},
"drawnUnder": {
"@id": "https://trustvc.io/context/terms#drawnUnder"
},
"drawnUnderDate": {
"@id": "https://trustvc.io/context/terms#drawnUnderDate"
},
"issuedBy": {
"@id": "https://trustvc.io/context/terms#issuedBy"
},
"drawee": {
"@id": "https://trustvc.io/context/terms#drawee",
"@context": {
"@protected": true,
"name": {
"@id": "https://trustvc.io/context/terms#partyName"
},
"address": {
"@id": "https://trustvc.io/context/terms#partyAddress"
},
"authorisedSignatoryName": {
"@id": "https://trustvc.io/context/terms#authorisedSignatoryName"
},
"signature": {
"@id": "https://trustvc.io/context/terms#signature"
}
}
},
"drawer": {
"@id": "https://trustvc.io/context/terms#drawer",
"@context": {
"@protected": true,
"name": {
"@id": "https://trustvc.io/context/terms#partyName"
},
"address": {
"@id": "https://trustvc.io/context/terms#partyAddress"
},
"authorisedSignatoryName": {
"@id": "https://trustvc.io/context/terms#authorisedSignatoryName"
},
"signature": {
"@id": "https://trustvc.io/context/terms#signature"
}
}
}
}
}
32 changes: 32 additions & 0 deletions packages/w3c-context/src/context/obligation-records-context.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"@context": {
"@version": 1.1,
"@protected": true,
"TransferableRecords": {
"@id": "https://example.com/terms#TransferableRecords",
"@type": "@id",
"@context": {
"@protected": true,
"tokenId": {
"@id": "https://example.com/terms#tokenId"
},
"tokenNetwork": {
"@id": "https://example.com/terms#tokenNetwork",
"@context": {
"@protected": true,
"chain": {
"@id": "https://example.com/terms#chain"
},
"chainId": {
"@id": "https://example.com/terms#chainId",
"@type": "http://www.w3.org/2001/XMLSchema#integer"
}
}
},
"obligationRegistry": {
"@id": "https://trustvc.io/context/terms#obligationRegistry"
}
}
}
}
}
11 changes: 11 additions & 0 deletions packages/w3c-context/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DocumentLoader, DocumentLoaderObject } from './types';
import jsonldSignatures from 'jsonld-signatures';
import attachmentsContext from '../context/attachments-context.json';
import bbsV1 from '../context/bbs-v1.json';
import boeContext from '../context/bill-of-exchange.json';
import bolContext from '../context/bill-of-lading.json';
import bolcContext from '../context/bill-of-lading-carrier.json';
import cooContext from '../context/coo.json';
Expand All @@ -21,6 +22,7 @@ import renderContext from '../context/render-method-context.json';
import renderContextV2 from '../context/render-method-context-v2.json';
import statusList2021V1 from '../context/status-list-2021-v1.json';
import trContext from '../context/transferable-records-context.json';
import obligationRecordsContext from '../context/obligation-records-context.json';
import warehouseReceiptContext from '../context/warehouse-receipt.json';
import { Document } from './types';

Expand All @@ -35,12 +37,15 @@ export const MULTIKEY_V1_URL = 'https://w3id.org/security/multikey/v1';
export const STATUS_LIST_2021_CREDENTIAL_URL = 'https://w3id.org/vc/status-list/2021/v1';

export const TR_CONTEXT_URL = 'https://trustvc.io/context/transferable-records-context.json';
export const OBLIGATION_RECORDS_CONTEXT_URL =
'https://trustvc.io/context/obligation-records-context.json';
export const RENDER_CONTEXT_URL = 'https://trustvc.io/context/render-method-context.json';
export const RENDER_CONTEXT_V2_URL = 'https://trustvc.io/context/render-method-context-v2.json';
export const ATTACHMENTS_CONTEXT_URL = 'https://trustvc.io/context/attachments-context.json';
export const QRCODE_CONTEXT_URL = 'https://trustvc.io/context/qrcode-context.json';
export const OPENCERTS_CONTEXT_URL = 'https://trustvc.io/context/opencerts-context.json';

export const BOE_CONTEXT_URL = 'https://trustvc.io/context/bill-of-exchange.json';
export const BOL_CONTEXT_URL = 'https://trustvc.io/context/bill-of-lading.json';
export const BOLC_CONTEXT_URL = 'https://trustvc.io/context/bill-of-lading-carrier.json';
export const COO_CONTEXT_URL = 'https://trustvc.io/context/coo.json';
Expand All @@ -64,6 +69,10 @@ export const trContexts: { [key: string]: Document } = {
[TR_CONTEXT_URL]: trContext,
};

export const obligationRecordsContexts: { [key: string]: Document } = {
[OBLIGATION_RECORDS_CONTEXT_URL]: obligationRecordsContext,
};

export const renderContexts: { [key: string]: Document } = {
[RENDER_CONTEXT_URL]: renderContext,
};
Expand All @@ -81,6 +90,7 @@ export const qrCodeContexts: { [key: string]: Document } = {
};

export const templateContexts: { [key: string]: Document } = {
[BOE_CONTEXT_URL]: boeContext,
[BOL_CONTEXT_URL]: bolContext,
[BOLC_CONTEXT_URL]: bolcContext,
[COO_CONTEXT_URL]: cooContext,
Expand Down Expand Up @@ -112,6 +122,7 @@ export async function getDocumentLoader(
[
contexts,
trContexts,
obligationRecordsContexts,
renderContexts,
renderContextsV2,
attachmentsContexts,
Expand Down
14 changes: 14 additions & 0 deletions packages/w3c-credential-status/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ export type TransferableRecordsCredentialStatus = Omit<GeneralCredentialStatus,
tokenRegistry: string;
};

/**
* Obligation Records (BoE) credential status. Same TransferableRecords type string,
* but keyed by obligationRegistry instead of tokenRegistry.
*/
export type ObligationRecordsCredentialStatus = Omit<GeneralCredentialStatus, 'type'> & {
type: 'TransferableRecords';
tokenId: string;
tokenNetwork: {
chain: string;
chainId: string | number;
};
obligationRegistry: string;
};

export type RawCredentialStatusVC = {
'@context': string | string[];
type: string[];
Expand Down
56 changes: 56 additions & 0 deletions packages/w3c-credential-status/src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { describe, expect, it } from 'vitest';
import { CredentialStatusPurpose, VCBitstringCredentialSubject } from './types';
import {
assertCredentialStatusType,
assertObligationRecords,
assertStatusListIndex,
assertStatusPurposeMatches,
assertTransferableRecords,
fetchCredentialStatusVC,
} from './utils';

Expand Down Expand Up @@ -55,6 +57,60 @@ describe('utils.ts', () => {
});
});

describe('assertTransferableRecords chainId', () => {
const base = {
type: 'TransferableRecords' as const,
tokenId: 'abc',
tokenRegistry: '0x123',
tokenNetwork: { chain: 'sepolia', chainId: 11155111 as string | number },
};

it('accepts integer number and numeric string chainId', () => {
expect(() => assertTransferableRecords(base)).not.toThrow();
expect(() =>
assertTransferableRecords({
...base,
tokenNetwork: { chain: 'sepolia', chainId: '11155111' },
}),
).not.toThrow();
});

it.each([
['', 'empty string'],
['abc', 'non-numeric'],
['1.5', 'fractional'],
[NaN, 'NaN'],
[Infinity, 'Infinity'],
[1.5, 'fractional number'],
])('rejects invalid chainId (%s / %s)', (chainId: string | number, _label: string) => {
expect(() =>
assertTransferableRecords({
...base,
tokenNetwork: { chain: 'sepolia', chainId },
}),
).toThrow(/credentialStatus\.tokenNetwork\.chainId" must be an integer/);
});
});

describe('assertObligationRecords chainId', () => {
const base = {
type: 'TransferableRecords' as const,
tokenId: 'abc',
obligationRegistry: '0x456',
tokenNetwork: { chain: 'sepolia', chainId: 11155111 as string | number },
};

it('accepts integer chainId and rejects invalid values', () => {
expect(() => assertObligationRecords(base)).not.toThrow();
expect(() =>
assertObligationRecords({
...base,
tokenNetwork: { chain: 'sepolia', chainId: '' },
}),
).toThrow(/credentialStatus\.tokenNetwork\.chainId" must be an integer/);
});
});

describe('fetchCredentialStatusVC', () => {
it('should fetch a credential status VC successfully', async () => {
const vc = await fetchCredentialStatusVC(
Expand Down
50 changes: 49 additions & 1 deletion packages/w3c-credential-status/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CredentialStatusPurpose } from './BitstringStatusList/types';
import {
BitstringStatusListCredentialStatus,
GeneralCredentialStatus,
ObligationRecordsCredentialStatus,
SignedCredentialStatusVC,
TransferableRecordsCredentialStatus,
VCBitstringCredentialSubject,
Expand Down Expand Up @@ -126,6 +127,23 @@ const _assertStatusListCredentialStatus = (
export const assertStatusList2021Entry = _assertStatusListCredentialStatus;
export const assertBitstringStatusListEntry = _assertStatusListCredentialStatus;

/**
* Asserts that `chainId` is an integer (number or whole numeric string).
* Rejects empty, non-numeric, non-finite, and fractional values.
* @param {string | number} chainId - Chain id from credentialStatus.tokenNetwork.
* @param {string} name - Property name used in the error message.
*/
export const assertIntegerChainId = (chainId: string | number, name: string): void => {
const valid =
typeof chainId === 'number'
? Number.isSafeInteger(chainId)
: typeof chainId === 'string' && chainId.trim() !== '' && /^[+-]?\d+$/.test(chainId.trim());

if (!valid) {
throw new TypeError(`"${name}" must be an integer.`);
}
};

export const assertTransferableRecords = (
credentialStatus: TransferableRecordsCredentialStatus,
mode: 'sign' | 'verify' = 'verify',
Expand All @@ -148,7 +166,37 @@ export const assertTransferableRecords = (

isString(tokenRegistry, 'credentialStatus.tokenRegistry');
isString(chain, 'credentialStatus.tokenNetwork.chain');
isNumber(Number(chainId), 'credentialStatus.tokenNetwork.chainId');
assertIntegerChainId(chainId, 'credentialStatus.tokenNetwork.chainId');
};

/**
* Asserts an Obligation Records (BoE) credential status.
* @param {ObligationRecordsCredentialStatus} credentialStatus - Obligation credential status.
* @param {'sign' | 'verify'} mode - Validation mode.
*/
export const assertObligationRecords = (
credentialStatus: ObligationRecordsCredentialStatus,
mode: 'sign' | 'verify' = 'verify',
): void => {
const {
type,
tokenId,
tokenNetwork: { chain, chainId },
obligationRegistry,
} = credentialStatus;
assertCredentialStatusType(type);

if (tokenId && mode === 'sign') {
throw new Error(
`"tokenId" is a generated field and should not be included in the credential status.`,
);
} else if (mode === 'verify') {
isString(tokenId, 'credentialStatus.tokenId');
}

isString(obligationRegistry, 'credentialStatus.obligationRegistry');
isString(chain, 'credentialStatus.tokenNetwork.chain');
assertIntegerChainId(chainId, 'credentialStatus.tokenNetwork.chainId');
};

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/w3c-vc/src/lib/helper/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ describe('helper', () => {
expect(() => _checkCredentialStatus({} as any)).toThrowError('Invalid type');
});

it('routes to Obligation Records when obligationRegistry is present even if not a string', () => {
const assertObligation = vi
.spyOn(w3c_credential_status, 'assertObligationRecords')
.mockImplementation(() => {
throw new TypeError('"credentialStatus.obligationRegistry" must be a string.');
});
const assertTransferable = vi.spyOn(w3c_credential_status, 'assertTransferableRecords');

expect(() =>
_checkCredentialStatus({
type: 'TransferableRecords',
obligationRegistry: 123,
tokenRegistry: '0xabc',
tokenNetwork: { chain: 'sepolia', chainId: 11155111 },
} as any),
).toThrow(/obligationRegistry" must be a string/);

expect(assertObligation).toHaveBeenCalled();
expect(assertTransferable).not.toHaveBeenCalled();
});

describe('_checkCredentialStatus', () => {
it('should return true if the credential status is valid', () => {
vi.spyOn(w3c_credential_status, 'assertStatusList2021Entry').mockResolvedValue();
Expand Down
Loading
Loading