diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9f5b759..4f181c7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,6 +5,16 @@ module.exports = { extends: 'standard-with-typescript', parserOptions: { project: './tsconfig.json' + }, + rules: { + '@typescript-eslint/strict-boolean-expressions': 'off' + } + }, + { + files: ['test/**/*.spec.ts'], + extends: 'standard-with-typescript', + parserOptions: { + project: './tsconfig.spec.json' } } ] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 06c3297..84fe92e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -20,27 +20,27 @@ jobs: run: npm run test-node env: CI: true - test-karma: - runs-on: ubuntu-latest -# needs: [lint] - timeout-minutes: 10 - strategy: - matrix: - node-version: [16.x] - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - name: Run karma tests - run: npm run test-karma +# test-karma: +# runs-on: ubuntu-latest +## needs: [lint] +# timeout-minutes: 10 +# strategy: +# matrix: +# node-version: [16.x] +# steps: +# - uses: actions/checkout@v2 +# - name: Use Node.js ${{ matrix.node-version }} +# uses: actions/setup-node@v1 +# with: +# node-version: ${{ matrix.node-version }} +# - run: npm install +# - name: Run karma tests +# run: npm run test-karma lint: runs-on: ubuntu-latest strategy: matrix: - node-version: [16.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f05b46..095aeee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # issuer-registry-client Changelog +## 2.0.0 - + +### Changed +- **BREAKING**: Refactor the client to use a more streamlined API, `client.load(config)` + and `client.didEntry(did)` + ## 1.0.0 - 2022-11-30 ### Added - - Initial commit. diff --git a/README.md b/README.md index cd538bc..deb7e3d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,28 @@ spec is being incubated in the W3C CCG. Assumes the loaded registries are public, accessible via `https`. +Note that an individual DID can appear in multiple registries. +Because of that, the _order_ of registries loaded matters. For example, given +a registry: + +```js +const knownRegistries = [{ + "name": "DCC Pilot Registry", + "url": "https://digitalcredentials.github.io/issuer-registry/registry.json" + }, + { + "name": "DCC Sandbox Registry", + "url": "https://digitalcredentials.github.io/sandbox-registry/registry.json" + }] +``` + +If an issuer DID is contained in both of those registries, the issuer entry +(that contains the issuer name and URL) will come from the _first_ registry, +"DCC Pilot Registry". + +In other words, verifiers and other implementers must order the registries +_in order of most authoritative to least_. + ## Install - Node.js 18+ is recommended. @@ -76,21 +98,40 @@ const knownRegistries = [ ] ``` -You can now fetch them and query them +You can now: ```js -import { loadRegistries } from '@digitalcredentials/issuer-registry-client' +import { RegistryClient } from '@digitalcredentials/issuer-registry-client' -// Load the registries from the web (typically done at app startup). -const registries = loadRegistries(knownRegistries) +const registries = new RegistryClient() -registries.contains('did:example:123') -// { result: false } +// Load the registries from the web (typically done at app startup). +await registries.load({ config: knownRegistries }) + +// You can now query to see if a DID is known in any registry +console.log(registries.didEntry('did:key:z6MkpLDL3RoAoMRTwTgo3rs39ZwssfaPKtGdZw7AGRN7CK4W')) +/** +DidMapRegistryEntry { + name: 'My University', + url: 'https://digitalcredentials.mit.edu', + location: 'Cambridge, MA, USA', + inRegistries: Set(2) { + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json', + rawContents: [Object] + }, + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json', + rawContents: [Object] + } + } +} + */ -// or -registries.contains('did:example:456') -// This DID is contained in one registry, DCC Registry -// { result: true, names: ['DCC Registry'] } +registries.didEntry('did:example:does-not-exist') +// undefined ``` ## Contribute diff --git a/declarations.d.ts b/declarations.d.ts deleted file mode 100644 index 8cb2932..0000000 --- a/declarations.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.json'; diff --git a/karma.conf.js b/karma.conf.js index be847b0..75a4eda 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -11,6 +11,7 @@ module.exports = function (config) { ], karmaTypescriptConfig: { + tsconfig: './tsconfig.spec.json', reports: {} // Disables the code coverage report }, diff --git a/package.json b/package.json index 943d513..cb83cd8 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "scripts": { "build": "npm run clear && tsc -d && tsc -p tsconfig.esm.json", "clear": "rimraf dist/*", - "lint": "ts-standard --fix", + "lint": "ts-standard --fix --project tsconfig.spec.json", "prepare": "npm run build", "rebuild": "npm run clear && npm run build", - "test": "npm run lint && npm run test-node && npm run test-karma", + "test": "npm run lint && npm run test-node", "test-karma": "karma start karma.conf.js", "test-node": "cross-env NODE_ENV=test TS_NODE_PROJECT=tsconfig.spec.json TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\" }' mocha -r ts-node/register --project tsconfig.spec.json 'test/*.ts'" }, "files": [ "dist", + "CHANGELOG.md", "README.md", "LICENSE.md" ], @@ -43,7 +44,10 @@ "rimraf": "^5.0.1", "ts-node": "^10.9.1", "ts-standard": "^12.0.2", - "typescript": "^5.1.6" + "typescript": "5.2.2" + }, + "resolutions": { + "@typescript-eslint/typescript-estree": "^6.1.6" }, "publishConfig": { "access": "public" diff --git a/src/Registry.ts b/src/Registry.ts deleted file mode 100644 index bf4e095..0000000 --- a/src/Registry.ts +++ /dev/null @@ -1,30 +0,0 @@ -/*! - * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. - */ - -import { RegistryRaw } from './types'; - -export class Registry implements RegistryRaw { - entries; - meta; - name; - - constructor(registry: RegistryRaw) { - this.entries = registry.entries; - this.meta = registry.meta; - this.name = registry.name; - } - - public isInRegistry(issuerKey: string): boolean { - return issuerKey in this.entries; - } - - public entryFor(issuerKey: string): Entry { - if (!this.isInRegistry(issuerKey)) { - throw new Error(`${issuerKey} not found in registry.`); - } - - return this.entries[issuerKey]; - } -} - diff --git a/src/RegistryCollection.ts b/src/RegistryCollection.ts deleted file mode 100644 index 1390cf2..0000000 --- a/src/RegistryCollection.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RemoteRegistryConfig } from './types/registry'; -import { Registry } from './Registry'; - -export class RegistryCollection { - configs: RemoteRegistryConfig[]; - registries: Array> = []; - - constructor(configs: RemoteRegistryConfig[]) { - this.configs = configs; - } - - public async fetchRegistries(): Promise { - const allRegistries = await Promise.all(this.configs.map(async ({ url, name }) => { - try { - const response = await fetch(url); - if (!response.ok) throw new Error(`Received ${response.status} from ${url}`); - - const data = await response.json(); - return new Registry({ - meta: data.meta, - entries: data.registry, - name, - }); - } catch (err) { - console.log(`Could not fetch registry "${name}" at ${url}`); - } - })); - - this.registries = allRegistries.filter(Boolean) as Array>; - } - - public isInRegistryCollection(key: string): boolean { - return this.registries.some(registry => registry.isInRegistry(key)); - } - - public registriesFor(key: string): Array> { - return this.registries.filter(registry => registry.isInRegistry(key)); - } -} diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 0000000..61bd22e --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1 @@ +declare module '@digitalcredentials/http-client' diff --git a/src/index.ts b/src/index.ts index 3ca3a4a..767367d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,125 @@ /*! - * Copyright (c) 2022 Digital Credentials Consortium. All rights reserved. + * Copyright (c) 2023 Digital Credentials Consortium. All rights reserved. */ -import 'isomorphic-fetch'; +import { httpClient } from '@digitalcredentials/http-client' -export async function loadRegistries (config: any): Promise { - return { result: true } +/** + * Example registry: + * @example + * ``` + * { + * "name": "DCC Sandbox Registry", + * "url": "https://digitalcredentials.github.io/sandbox-registry/registry.json" + * } + * ``` + */ +export class KnownDidRegistry { + public name: string + public url: string + + constructor (name: string, url: string) { + this.name = name + this.url = url + } +} + +const DID_MAP_REGISTRY_SPEC_V01 = '0.1.0' + +export class DidMapRegistry extends KnownDidRegistry { + public version: string = DID_MAP_REGISTRY_SPEC_V01 + + /** + * @example rawContents + * { + * did:key:z6MkpLDL3RoAoMRTwTgo3rs39ZwssfaPKtGdZw7AGRN7CK4W: { + * name: "(Example) My University", + * location: "Cambridge, MA, USA", + * url: "https://digitalcredentials.mit.edu" + * } + * } + */ + public rawContents?: any + + constructor ({ name, url, rawContents }: { name: string, url: string, rawContents: any[] }) { + super(name, url) + this.rawContents = rawContents + } +} + +/** + * @example + * ``` + * // did:key:z6MkpLDL3RoAoMRTwTgo3rs39ZwssfaPKtGdZw7AGRN7CK4W + * { + * name: "(Example) My University", + * location: "Cambridge, MA, USA", + * url: "https://digitalcredentials.mit.edu", + * inRegistries: ["DCC Community Registry", "DCC Sandbox Registry"] + * } + * ``` + */ +export class DidMapRegistryEntry { + public name: string + public url: string + public location?: string + inRegistries?: Set = new Set() + + constructor ({ name, url, location }: { name: string, url: string, location: string }) { + this.name = name + this.url = url + this.location = location + } } -export { Registry } from './lib'; -export { registryCollections, loadRegistryCollections } from './registryCollections'; -export * from './types'; +export class RegistryClient { + public registries?: DidMapRegistry[] + public didMap: Map = new Map() + + /** + * @example + * ``` + * const config = [ + * { + * "name": "DCC Sandbox Registry", + * "url": "https://digitalcredentials.github.io/sandbox-registry/registry.json" + * } + * ] + * const client = new RegistryClient() + * await client.load({ config }) + * ``` + * @param registries - Config object with a list of registries to load + */ + async load ({ config }: { config: any }): Promise { + // Clear previous DID map and entries + this.didMap = new Map() + this.registries = config as DidMapRegistry[] + + await Promise.all(this.registries.map(async (registry) => { + try { + // fetch registry contents + const contents: any = await httpClient.get(registry.url) + registry.rawContents = contents.data.registry + // discard contents.meta, not needed at this point + + // cycle through each DID in the registry, add to DID Map + for (const did in registry.rawContents) { + const entry = new DidMapRegistryEntry(registry.rawContents[did]) + const existingEntry = this.didMap.get(did) + if (existingEntry != null) { + existingEntry.inRegistries?.add(registry) + } else { + entry.inRegistries?.add(registry) + this.didMap.set(did, entry) + } + } + } catch (e) { + console.log(`Could not load registry from url "${registry.url}":`, e) + // do nothing; no DIDs are added from that registry + } + })) + } + + didEntry (did: string): DidMapRegistryEntry | undefined { + return this.didMap.get(did) + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index 59b618a..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '../Registry'; -export * from '../RegistryCollection'; diff --git a/src/registryCollections.ts b/src/registryCollections.ts deleted file mode 100644 index 8b63573..0000000 --- a/src/registryCollections.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { RegistryCollection } from './lib'; -import { IssuerDidEntry } from './types'; - -// import registryCollectionsConfig from './config/registryCollections.json'; - -const registryCollectionsConfig: any = {issuerDid: ''} - -export const registryCollections = { - issuerDid: new RegistryCollection(registryCollectionsConfig.issuerDid), -}; - -export async function loadRegistryCollections(): Promise { - await Promise.all(Object.values(registryCollections).map(async (collection) => - collection.fetchRegistries() - )); -} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 585b667..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './registry'; diff --git a/src/types/registry.ts b/src/types/registry.ts deleted file mode 100644 index e208fd9..0000000 --- a/src/types/registry.ts +++ /dev/null @@ -1,21 +0,0 @@ -type RegistryMetadata = { - created: string; - updated: string; -} - -export type RegistryRaw = { - readonly meta: RegistryMetadata; - readonly entries: Record; - readonly name: string; -} - -export type RemoteRegistryConfig = { - name: string; - url: string; -} - -export type IssuerDidEntry = { - name: string; - location?: string; - url: string; -} diff --git a/test/registryClient.spec.ts b/test/registryClient.spec.ts new file mode 100644 index 0000000..c0ec1d7 --- /dev/null +++ b/test/registryClient.spec.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai' +import { RegistryClient } from '../src' + +const knownIssuers = [ + { + name: 'DCC Sandbox Registry', + url: 'https://digitalcredentials.github.io/sandbox-registry/registry.json' + }, + { + name: 'DCC Community Registry', + url: 'https://digitalcredentials.github.io/community-registry/registry.json' + } +] + +describe('registry client', () => { + it('loads registries', async () => { + const client = new RegistryClient() + + await client.load({ config: knownIssuers }) + + const entry = client + .didEntry('did:key:z6MkpLDL3RoAoMRTwTgo3rs39ZwssfaPKtGdZw7AGRN7CK4W') + expect(entry?.inRegistries?.size).to.equal(2) + + expect(client.didEntry('did:example:invalid')).to.equal(undefined) + }) +}) diff --git a/test/registryCollections.spec.ts b/test/registryCollections.spec.ts deleted file mode 100644 index 9dd4f63..0000000 --- a/test/registryCollections.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect } from 'chai' -import { loadRegistryCollections, registryCollections } from '../src' - -import { loadRegistries } from '../src' - -const knownIssuers = [ - { - "name": "DCC Sandbox Registry", - "url": "https://digitalcredentials.github.io/sandbox-registry/registry.json" - } -] - -describe('registry client', () => { - it('loads registries', async () => { - const registries = await loadRegistries(knownIssuers) - - console.log(registries) - }) -}) - diff --git a/tsconfig.spec.json b/tsconfig.spec.json index e947de9..5bf0e78 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -19,8 +19,9 @@ }, "include": [ "src/**/*", + "test/**/*.spec.ts", ".eslintrc.js", "karma.conf.js" ], - "exclude": ["node_modules", "dist", "test"] + "exclude": ["node_modules", "dist"] }