From 65869192e5d2ae7f234752e3c95b25938a2ea048 Mon Sep 17 00:00:00 2001 From: rvlb Date: Fri, 28 Nov 2025 16:35:15 -0300 Subject: [PATCH 1/4] Add VCI snapshot download support --- src/shc/directory.ts | 26 +++++ test/shc/directory.test.ts | 228 ++++++++++++++++++++++--------------- 2 files changed, 161 insertions(+), 93 deletions(-) diff --git a/src/shc/directory.ts b/src/shc/directory.ts index 8853a8d..f57ea65 100644 --- a/src/shc/directory.ts +++ b/src/shc/directory.ts @@ -27,6 +27,32 @@ export class Directory { return this.issuerInfo } + /** + * Fetch a snapshot of the VCI Directory published by The Commons Project + * and build a {@link Directory} from it. + * + * This helper fetches a well-known VCI snapshot JSON file and delegates to + * `Directory.fromJSON` to produce a `Directory` instance. If the snapshot + * cannot be retrieved (non-2xx response) the function throws an Error. + * + * @returns A {@link Directory} populated from the VCI snapshot + * @throws Error when the VCI snapshot HTTP fetch returns a non-OK status + * @example + * const directory = await Directory.fromVCI() + */ + static async fromVCI(): Promise { + const vciSnapshotResponse = await fetch( + 'https://raw.githubusercontent.com/the-commons-project/vci-directory/main/logs/vci_snapshot.json' + ) + if (!vciSnapshotResponse.ok) { + throw new Error( + `Failed to fetch VCI Directory snapshot with status ${vciSnapshotResponse.status}` + ) + } + const vciDirectoryJson = await vciSnapshotResponse.json() + return Directory.fromJSON(vciDirectoryJson) + } + /** * Build a Directory from a parsed JSON object matching the published * directory schema. diff --git a/test/shc/directory.test.ts b/test/shc/directory.test.ts index 2e12f7b..b70f067 100644 --- a/test/shc/directory.test.ts +++ b/test/shc/directory.test.ts @@ -2,112 +2,154 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Directory } from '../../src/shc/directory' import type { DirectoryJSON } from '../../src/shc/types' -describe('Directory', () => { - const ISS_URL = 'https://example.com/issuer' - - afterEach(() => { - vi.restoreAllMocks() - }) - - it('should create a directory from JSON', () => { - const directoryJson = { - directory: 'https://example.com/keystore/directory.json', - issuerInfo: [ +const SAMPLE_DIRECTORY_JSON = { + directory: 'https://example.com/keystore/directory.json', + issuerInfo: [ + { + issuer: { + iss: 'https://example.com/issuer', + name: 'Example Issuer 1', + }, + keys: [ { - issuer: { - iss: 'https://example.com/issuer', - name: 'Example Issuer 1', - }, - keys: [ - { - kty: 'EC', - kid: 'kid-1-simple', - }, - { - kty: 'EC', - kid: 'kid-2-simple', - }, - ], - crls: [ - { - kid: 'kid-2-simple', - method: 'rid', - ctr: 1, - rids: ['revoked-1'], - }, - ], + kty: 'EC', + kid: 'kid-1-simple', }, { - issuer: { - iss: 'https://example.com/issuer2', - name: 'Example Issuer 2', - }, - keys: [ - { - kty: 'EC', - kid: 'kid-A-simple', - }, - ], + kty: 'EC', + kid: 'kid-2-simple', }, + ], + crls: [ { - issuer: { - iss: 'https://example.com/issuer3', - name: 'Example Issuer 3', - }, - keys: [ - { - kty: 'EC', - kid: 'kid-C-simple', - }, - ], + kid: 'kid-2-simple', + method: 'rid', + ctr: 1, + rids: ['revoked-1'], }, + ], + }, + { + issuer: { + iss: 'https://example.com/issuer2', + name: 'Example Issuer 2', + }, + keys: [ { - issuer: { - iss: 'https://example.com/issuer4', - name: 'Example Issuer 4', - website: 'https://example.com/issuer4', - }, - keys: [ - { - kty: 'EC', - kid: 'kid-D-simple', - }, - ], - crls: [ - { - kid: 'kid-D-simple', - method: 'rid', - ctr: 1, - rids: ['revoked-2'], - }, - ], + kty: 'EC', + kid: 'kid-A-simple', }, ], - } - const directory = Directory.fromJSON(directoryJson as DirectoryJSON) - const issuers = directory.getIssuerInfo() - expect(issuers).toHaveLength(4) + }, + { + issuer: { + iss: 'https://example.com/issuer3', + name: 'Example Issuer 3', + }, + keys: [ + { + kty: 'EC', + kid: 'kid-C-simple', + }, + ], + }, + { + issuer: { + iss: 'https://example.com/issuer4', + name: 'Example Issuer 4', + website: 'https://example.com/issuer4', + }, + keys: [ + { + kty: 'EC', + kid: 'kid-D-simple', + }, + ], + crls: [ + { + kid: 'kid-D-simple', + method: 'rid', + ctr: 1, + rids: ['revoked-2'], + }, + ], + }, + ], +} + +function assertDirectoryFromSampleJson(directory: Directory) { + const issuers = directory.getIssuerInfo() + expect(issuers).toHaveLength(4) + + const issuer1 = issuers[0]! + expect(issuer1.iss).toEqual('https://example.com/issuer') + expect(issuer1.keys).toHaveLength(2) + const crls1 = issuer1.crls! + expect(crls1).toHaveLength(1) + expect(crls1[0]!.kid).toEqual('kid-2-simple') + + const issuer2 = issuers.find(i => i.iss === 'https://example.com/issuer2')! + expect(issuer2).toBeDefined() + expect(issuer2.keys).toHaveLength(1) + + const issuer3 = issuers.find(i => i.iss === 'https://example.com/issuer3')! + expect(issuer3).toBeDefined() + expect(issuer3.keys).toHaveLength(1) + + const issuer4 = issuers.find(i => i.iss === 'https://example.com/issuer4')! + expect(issuer4).toBeDefined() + expect(issuer4.keys).toHaveLength(1) + const crls4 = issuer4.crls! + expect(crls4).toHaveLength(1) +} - const issuer1 = issuers[0]! - expect(issuer1.iss).toEqual('https://example.com/issuer') - expect(issuer1.keys).toHaveLength(2) - const crls1 = issuer1.crls! - expect(crls1).toHaveLength(1) - expect(crls1[0]!.kid).toEqual('kid-2-simple') +describe('Directory', () => { + const ISS_URL = 'https://example.com/issuer' - const issuer2 = issuers.find(i => i.iss === 'https://example.com/issuer2')! - expect(issuer2).toBeDefined() - expect(issuer2.keys).toHaveLength(1) + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should create a directory from the VCI snapshot', async () => { + const originalFetch = globalThis.fetch + const fetchMock = vi.fn().mockImplementation((url: string) => { + if (url.includes('vci_snapshot.json')) { + return Promise.resolve({ + ok: true, + json: async () => SAMPLE_DIRECTORY_JSON, + }) + } + + return Promise.resolve({ ok: false, status: 404 }) + }) + ;(globalThis as any).fetch = fetchMock - const issuer3 = issuers.find(i => i.iss === 'https://example.com/issuer3')! - expect(issuer3).toBeDefined() - expect(issuer3.keys).toHaveLength(1) + const directory = await Directory.fromVCI() + assertDirectoryFromSampleJson(directory) - const issuer4 = issuers.find(i => i.iss === 'https://example.com/issuer4')! - expect(issuer4).toBeDefined() - expect(issuer4.keys).toHaveLength(1) - const crls4 = issuer4.crls! - expect(crls4).toHaveLength(1) + ;(globalThis as any).fetch = originalFetch + }) + + it('should throw when VCI snapshot fetch fails', async () => { + const originalFetch = globalThis.fetch + const fetchMock = vi.fn().mockImplementation((url: string) => { + if (url.includes('vci_snapshot.json')) { + return Promise.resolve({ ok: false, status: 500 }) + } + return Promise.resolve({ ok: false, status: 404 }) + }) + ;(globalThis as any).fetch = fetchMock + + await expect(Directory.fromVCI()).rejects.toThrow( + 'Failed to fetch VCI Directory snapshot with status 500' + ) + + ;(globalThis as any).fetch = originalFetch + }) + + it('should create a directory from JSON', () => { + const directory = Directory.fromJSON(SAMPLE_DIRECTORY_JSON as DirectoryJSON) + assertDirectoryFromSampleJson(directory) }) it('should handle missing or invalid values when building directory using fromJSON', () => { From b22fc50f6afb0b16bf7933103b32704670a8a8e6 Mon Sep 17 00:00:00 2001 From: rvlb Date: Fri, 28 Nov 2025 17:07:41 -0300 Subject: [PATCH 2/4] Add VCI to readme --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 4e7d4c2..91765f7 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,25 @@ console.log('Resolved FHIR resources:', resolved.fhirResources); This example above demonstrates the complete lifecycle: SHL generation, content addition, manifest builder persistence in server-side database, manifest serving, and client-side resolution. In a real application, you would implement persistence for the manifest builder state and serve the manifest endpoint from your backend server. +### Directories +#### Building a Directory from VCI Snapshot +`Directory.fromVCI` is a convenience helper that fetches the VCI Directory snapshot published by The Commons Project and returns a Directory instance built from that snapshot. + +Use it when you want a quick, canonical directory of issuer metadata (JWKS + CRLs) without manually assembling or maintaining a directory JSON. + +```typescript +import { Directory } from 'kill-the-clipboard' + +// top-level async context or inside an async function +try { + const directory = await Directory.fromVCI() + const issuers = directory.getIssuerInfo() + console.log('Loaded issuers:', issuers.length) +} catch (err) { + console.error('Failed to load VCI Directory:', err) +} +``` + ### Error Handling The library provides granular error handling with specific error codes for different failure scenarios. Check each method documentation for the specific errors that can be thrown. From 4eb485b8eb9971910222ad50c9545c8b5af112a8 Mon Sep 17 00:00:00 2001 From: rvlb Date: Fri, 5 Dec 2025 15:51:12 -0300 Subject: [PATCH 3/4] Update next.js version --- demo/medplum-shl/package.json | 2 +- demo/shl/package.json | 2 +- pnpm-lock.yaml | 207 +++++++--------------------------- 3 files changed, 45 insertions(+), 166 deletions(-) diff --git a/demo/medplum-shl/package.json b/demo/medplum-shl/package.json index 6799634..c89744d 100644 --- a/demo/medplum-shl/package.json +++ b/demo/medplum-shl/package.json @@ -30,7 +30,7 @@ "@tabler/icons-react": "^3.34.1", "argon2": "^0.44.0", "kill-the-clipboard": "workspace:../..", - "next": "15.4.6", + "next": "15.5.7", "react": "19.1.1", "react-dom": "19.1.1", "uuid": "^11.1.0" diff --git a/demo/shl/package.json b/demo/shl/package.json index dee8fd8..082b723 100644 --- a/demo/shl/package.json +++ b/demo/shl/package.json @@ -32,7 +32,7 @@ "@tabler/icons-react": "^3.34.1", "argon2": "^0.44.0", "kill-the-clipboard": "workspace:../..", - "next": "15.5.3", + "next": "15.5.7", "react": "19.1.1", "react-dom": "19.1.1", "uuid": "^11.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5f15da..2b17f04 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,8 +130,8 @@ importers: specifier: workspace:../.. version: link:../.. next: - specifier: 15.4.6 - version: 15.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 15.5.7 + version: 15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: specifier: 19.1.1 version: 19.1.1 @@ -219,8 +219,8 @@ importers: specifier: workspace:../.. version: link:../.. next: - specifier: 15.5.3 - version: 15.5.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 15.5.7 + version: 15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: specifier: 19.1.1 version: 19.1.1 @@ -1233,11 +1233,8 @@ packages: '@neon-rs/load@0.0.4': resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - '@next/env@15.4.6': - resolution: {integrity: sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==} - - '@next/env@15.5.3': - resolution: {integrity: sha512-RSEDTRqyihYXygx/OJXwvVupfr9m04+0vH8vyy0HfZ7keRto6VX9BbEk0J2PUk0VGy6YhklJUSrgForov5F9pw==} + '@next/env@15.5.7': + resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} '@next/eslint-plugin-next@15.4.6': resolution: {integrity: sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==} @@ -1245,98 +1242,50 @@ packages: '@next/eslint-plugin-next@15.5.3': resolution: {integrity: sha512-SdhaKdko6dpsSr0DldkESItVrnPYB1NS2NpShCSX5lc7SSQmLZt5Mug6t2xbiuVWEVDLZSuIAoQyYVBYp0dR5g==} - '@next/swc-darwin-arm64@15.4.6': - resolution: {integrity: sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==} + '@next/swc-darwin-arm64@15.5.7': + resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@15.5.3': - resolution: {integrity: sha512-nzbHQo69+au9wJkGKTU9lP7PXv0d1J5ljFpvb+LnEomLtSbJkbZyEs6sbF3plQmiOB2l9OBtN2tNSvCH1nQ9Jg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@15.4.6': - resolution: {integrity: sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-darwin-x64@15.5.3': - resolution: {integrity: sha512-w83w4SkOOhekJOcA5HBvHyGzgV1W/XvOfpkrxIse4uPWhYTTRwtGEM4v/jiXwNSJvfRvah0H8/uTLBKRXlef8g==} + '@next/swc-darwin-x64@15.5.7': + resolution: {integrity: sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.6': - resolution: {integrity: sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==} + '@next/swc-linux-arm64-gnu@15.5.7': + resolution: {integrity: sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-gnu@15.5.3': - resolution: {integrity: sha512-+m7pfIs0/yvgVu26ieaKrifV8C8yiLe7jVp9SpcIzg7XmyyNE7toC1fy5IOQozmr6kWl/JONC51osih2RyoXRw==} + '@next/swc-linux-arm64-musl@15.5.7': + resolution: {integrity: sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.6': - resolution: {integrity: sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@15.5.3': - resolution: {integrity: sha512-u3PEIzuguSenoZviZJahNLgCexGFhso5mxWCrrIMdvpZn6lkME5vc/ADZG8UUk5K1uWRy4hqSFECrON6UKQBbQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@15.4.6': - resolution: {integrity: sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-gnu@15.5.3': - resolution: {integrity: sha512-lDtOOScYDZxI2BENN9m0pfVPJDSuUkAD1YXSvlJF0DKwZt0WlA7T7o3wrcEr4Q+iHYGzEaVuZcsIbCps4K27sA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@15.4.6': - resolution: {integrity: sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==} + '@next/swc-linux-x64-gnu@15.5.7': + resolution: {integrity: sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.3': - resolution: {integrity: sha512-9vWVUnsx9PrY2NwdVRJ4dUURAQ8Su0sLRPqcCCxtX5zIQUBES12eRVHq6b70bbfaVaxIDGJN2afHui0eDm+cLg==} + '@next/swc-linux-x64-musl@15.5.7': + resolution: {integrity: sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.6': - resolution: {integrity: sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==} + '@next/swc-win32-arm64-msvc@15.5.7': + resolution: {integrity: sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@15.5.3': - resolution: {integrity: sha512-1CU20FZzY9LFQigRi6jM45oJMU3KziA5/sSG+dXeVaTm661snQP6xu3ykGxxwU5sLG3sh14teO/IOEPVsQMRfA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@15.4.6': - resolution: {integrity: sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@next/swc-win32-x64-msvc@15.5.3': - resolution: {integrity: sha512-JMoLAq3n3y5tKXPQwCK5c+6tmwkuFDa2XAxz8Wm4+IVthdBZdZGh+lmiLUHg9f9IDwIQpUjp+ysd6OkYTyZRZw==} + '@next/swc-win32-x64-msvc@15.5.7': + resolution: {integrity: sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -3370,29 +3319,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - next@15.4.6: - resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - next@15.5.3: - resolution: {integrity: sha512-r/liNAx16SQj4D+XH/oI1dlpv9tdKJ6cONYPwwcCC46f2NjpaRWY+EKCzULfgQYV6YKXjHBchff2IZBSlZmJNw==} + next@15.5.7: + resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -5552,9 +5480,7 @@ snapshots: '@neon-rs/load@0.0.4': {} - '@next/env@15.4.6': {} - - '@next/env@15.5.3': {} + '@next/env@15.5.7': {} '@next/eslint-plugin-next@15.4.6': dependencies: @@ -5564,52 +5490,28 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.4.6': - optional: true - - '@next/swc-darwin-arm64@15.5.3': - optional: true - - '@next/swc-darwin-x64@15.4.6': - optional: true - - '@next/swc-darwin-x64@15.5.3': - optional: true - - '@next/swc-linux-arm64-gnu@15.4.6': + '@next/swc-darwin-arm64@15.5.7': optional: true - '@next/swc-linux-arm64-gnu@15.5.3': + '@next/swc-darwin-x64@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.4.6': + '@next/swc-linux-arm64-gnu@15.5.7': optional: true - '@next/swc-linux-arm64-musl@15.5.3': + '@next/swc-linux-arm64-musl@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.4.6': + '@next/swc-linux-x64-gnu@15.5.7': optional: true - '@next/swc-linux-x64-gnu@15.5.3': + '@next/swc-linux-x64-musl@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.4.6': + '@next/swc-win32-arm64-msvc@15.5.7': optional: true - '@next/swc-linux-x64-musl@15.5.3': - optional: true - - '@next/swc-win32-arm64-msvc@15.4.6': - optional: true - - '@next/swc-win32-arm64-msvc@15.5.3': - optional: true - - '@next/swc-win32-x64-msvc@15.4.6': - optional: true - - '@next/swc-win32-x64-msvc@15.5.3': + '@next/swc-win32-x64-msvc@15.5.7': optional: true '@nodelib/fs.scandir@2.1.5': @@ -7919,32 +7821,9 @@ snapshots: natural-compare@1.4.0: {} - next@15.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): - dependencies: - '@next/env': 15.4.6 - '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001743 - postcss: 8.4.31 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - styled-jsx: 5.1.6(react@19.1.1) - optionalDependencies: - '@next/swc-darwin-arm64': 15.4.6 - '@next/swc-darwin-x64': 15.4.6 - '@next/swc-linux-arm64-gnu': 15.4.6 - '@next/swc-linux-arm64-musl': 15.4.6 - '@next/swc-linux-x64-gnu': 15.4.6 - '@next/swc-linux-x64-musl': 15.4.6 - '@next/swc-win32-arm64-msvc': 15.4.6 - '@next/swc-win32-x64-msvc': 15.4.6 - sharp: 0.34.4 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - next@15.5.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + next@15.5.7(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: - '@next/env': 15.5.3 + '@next/env': 15.5.7 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001743 postcss: 8.4.31 @@ -7952,14 +7831,14 @@ snapshots: react-dom: 19.1.1(react@19.1.1) styled-jsx: 5.1.6(react@19.1.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.3 - '@next/swc-darwin-x64': 15.5.3 - '@next/swc-linux-arm64-gnu': 15.5.3 - '@next/swc-linux-arm64-musl': 15.5.3 - '@next/swc-linux-x64-gnu': 15.5.3 - '@next/swc-linux-x64-musl': 15.5.3 - '@next/swc-win32-arm64-msvc': 15.5.3 - '@next/swc-win32-x64-msvc': 15.5.3 + '@next/swc-darwin-arm64': 15.5.7 + '@next/swc-darwin-x64': 15.5.7 + '@next/swc-linux-arm64-gnu': 15.5.7 + '@next/swc-linux-arm64-musl': 15.5.7 + '@next/swc-linux-x64-gnu': 15.5.7 + '@next/swc-linux-x64-musl': 15.5.7 + '@next/swc-win32-arm64-msvc': 15.5.7 + '@next/swc-win32-x64-msvc': 15.5.7 sharp: 0.34.4 transitivePeerDependencies: - '@babel/core' From efab59b4044e78fa2506b322afb156b13039e4cc Mon Sep 17 00:00:00 2001 From: rvlb Date: Fri, 5 Dec 2025 16:00:55 -0300 Subject: [PATCH 4/4] Improve directory readme --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 91765f7..fc666ad 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,16 @@ console.log('Resolved FHIR resources:', resolved.fhirResources); This example above demonstrates the complete lifecycle: SHL generation, content addition, manifest builder persistence in server-side database, manifest serving, and client-side resolution. In a real application, you would implement persistence for the manifest builder state and serve the manifest endpoint from your backend server. ### Directories +A `Directory` is a lightweight, serializable collection of issuer metadata used by this library: each entry contains an issuer URL, its JWK descriptors (public keys) and optional CRL entries (revoked resource ids). + +It provides a local/cached source of JWKS + CRL data so verification and lookup code can resolve public keys and revocation information without hitting network endpoints repeatedly. + +It can be built through the following methods: + +1. Build from a JSON manifest: `Directory.fromJSON` +2. Build by fetching from issuer endpoints: `Directory.fromURLs` +3. Build by loading the VCI snapshot: `Directory.fromVCI` + #### Building a Directory from VCI Snapshot `Directory.fromVCI` is a convenience helper that fetches the VCI Directory snapshot published by The Commons Project and returns a Directory instance built from that snapshot.