Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
26 changes: 26 additions & 0 deletions src/shc/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Directory> {
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.
Expand Down
228 changes: 135 additions & 93 deletions test/shc/directory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down