From 4643a1498261d5cd8a30a2333e32921cd27c8cc7 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Tue, 3 Feb 2026 17:29:07 -0500 Subject: [PATCH 1/6] Added updateDID to keymaster interface --- packages/keymaster/src/keymaster-client.ts | 10 +++++ packages/keymaster/src/types.ts | 3 +- .../keymaster/server/src/keymaster-api.ts | 45 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/packages/keymaster/src/keymaster-client.ts b/packages/keymaster/src/keymaster-client.ts index f2baf4d..c47d279 100644 --- a/packages/keymaster/src/keymaster-client.ts +++ b/packages/keymaster/src/keymaster-client.ts @@ -424,6 +424,16 @@ export default class KeymasterClient implements KeymasterInterface { } } + async updateDID(id: string, doc: DidCidDocument): Promise { + try { + const response = await axios.put(`${this.API}/did/${id}`, { doc }); + return response.data.ok; + } + catch (error) { + throwError(error); + } + } + async revokeDID(id: string): Promise { try { const response = await axios.delete(`${this.API}/did/${id}`); diff --git a/packages/keymaster/src/types.ts b/packages/keymaster/src/types.ts index 5bf26dc..b4ef5f8 100644 --- a/packages/keymaster/src/types.ts +++ b/packages/keymaster/src/types.ts @@ -311,8 +311,9 @@ export interface KeymasterInterface { getName(name: string): Promise; removeName(name: string): Promise; - // DID resolution + // DIDs resolveDID(did: string, options?: ResolveDIDOptions): Promise; + updateDID(id: string, doc: DidCidDocument): Promise; // Assets createAsset(data: unknown, options?: CreateAssetOptions): Promise; diff --git a/services/keymaster/server/src/keymaster-api.ts b/services/keymaster/server/src/keymaster-api.ts index 7fc6ca0..4fa3f4e 100644 --- a/services/keymaster/server/src/keymaster-api.ts +++ b/services/keymaster/server/src/keymaster-api.ts @@ -933,6 +933,51 @@ v1router.delete('/did/:id', async (req, res) => { } }); +/** + * @swagger + * /did/{id}: + * put: + * summary: Update a DID document. + * description: Updates the DID document with the provided data. + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: string + * description: The DID to update. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * doc: + * type: object + * description: The DID document fields to update. + * responses: + * 200: + * description: Indicates whether the DID was successfully updated. + * content: + * application/json: + * schema: + * type: object + * properties: + * ok: + * type: boolean + * 500: + * description: Internal server error. + */ +v1router.put('/did/:id', async (req, res) => { + try { + const ok = await keymaster.updateDID(req.params.id, req.body.doc); + res.json({ ok }); + } catch (error: any) { + res.status(500).send({ error: error.toString() }); + } +}); + /** * @swagger * /ids/current: From e1ae9f9223e3cd5884dac7c33f528ef3b26d00c8 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Tue, 3 Feb 2026 18:04:07 -0500 Subject: [PATCH 2/6] Improved coverage --- tests/keymaster/client.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/keymaster/client.test.ts b/tests/keymaster/client.test.ts index c0df783..dd116e6 100644 --- a/tests/keymaster/client.test.ts +++ b/tests/keymaster/client.test.ts @@ -1033,6 +1033,38 @@ describe('revokeDID', () => { }); }); +describe('updateDID', () => { + const mockDID = 'did:example:1234567890abcdefghi'; + const mockDoc = { didDocumentData: { schema: { title: 'Test Schema' } } }; + + it('should update DID', async () => { + nock(KeymasterURL) + .put(`${Endpoints.did}/${mockDID}`) + .reply(200, { ok: true }); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + const ok = await keymaster.updateDID(mockDID, mockDoc); + + expect(ok).toBe(true); + }); + + it('should throw exception on updateDID server error', async () => { + nock(KeymasterURL) + .put(`${Endpoints.did}/${mockDID}`) + .reply(500, ServerError); + + const keymaster = await KeymasterClient.create({ url: KeymasterURL }); + + try { + await keymaster.updateDID(mockDID, mockDoc); + throw new ExpectedExceptionError(); + } + catch (error: any) { + expect(error.message).toBe(ServerError.message); + } + }); +}); + describe('createAsset', () => { const mockAsset = { id: 'asset1', data: 'some data' }; const mockDID = 'did:example:1234567890abcd'; From c515ec0de18581f2d27cd12cca37a31c1cd8e7d9 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 4 Feb 2026 10:23:35 -0500 Subject: [PATCH 3/6] Added $credentialContext support --- packages/keymaster/src/keymaster.ts | 16 ++++++++++------ services/gatekeeper/client/src/KeymasterUI.js | 4 ++-- services/keymaster/client/src/KeymasterUI.js | 4 ++-- tests/keymaster/credential.test.ts | 14 ++++++-------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/keymaster/src/keymaster.ts b/packages/keymaster/src/keymaster.ts index 269443a..b474ae1 100644 --- a/packages/keymaster/src/keymaster.ts +++ b/packages/keymaster/src/keymaster.ts @@ -1682,16 +1682,20 @@ export default class Keymaster implements KeymasterInterface { // If schema provided, add credentialSchema and generate claims from schema if (schema) { const schemaDID = await this.lookupDID(schema); - const schemaDoc = await this.getSchema(schemaDID) as { $credentialTypes?: string[]; properties?: Record } | null; + const schemaDoc = await this.getSchema(schemaDID) as { $credentialContext?: string[]; $credentialType?: string[]; properties?: Record } | null; if (!claims && schemaDoc) { claims = this.generateSchema(schemaDoc); } - // If schema has $credentialTypes, add them to credential types (avoiding duplicates) - if (schemaDoc?.$credentialTypes) { - const newTypes = schemaDoc.$credentialTypes.filter(t => !vc.type.includes(t)); - vc.type.push(...newTypes); + // If schema has $credentialContext, use it for the credential context + if (schemaDoc?.$credentialContext?.length) { + vc["@context"] = schemaDoc.$credentialContext; + } + + // If schema has $credentialType, use it for the credential type + if (schemaDoc?.$credentialType?.length) { + vc.type = schemaDoc.$credentialType; } vc.credentialSchema = { @@ -1700,7 +1704,7 @@ export default class Keymaster implements KeymasterInterface { }; } - if (claims) { + if (claims && Object.keys(claims).length) { vc.credentialSubject = { id: subjectDID, ...claims, diff --git a/services/gatekeeper/client/src/KeymasterUI.js b/services/gatekeeper/client/src/KeymasterUI.js index cb108ae..f66db9f 100644 --- a/services/gatekeeper/client/src/KeymasterUI.js +++ b/services/gatekeeper/client/src/KeymasterUI.js @@ -989,8 +989,8 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { let name = null; // Priority 1: last $credentialType - if (schema.$credentialTypes && Array.isArray(schema.$credentialTypes) && schema.$credentialTypes.length > 0) { - name = schema.$credentialTypes[schema.$credentialTypes.length - 1]; + if (schema.$credentialType && Array.isArray(schema.$credentialType) && schema.$credentialType.length > 0) { + name = schema.$credentialType[schema.$credentialType.length - 1]; } // Priority 2: schema title else if (schema.title) { diff --git a/services/keymaster/client/src/KeymasterUI.js b/services/keymaster/client/src/KeymasterUI.js index cb108ae..f66db9f 100644 --- a/services/keymaster/client/src/KeymasterUI.js +++ b/services/keymaster/client/src/KeymasterUI.js @@ -989,8 +989,8 @@ function KeymasterUI({ keymaster, title, challengeDID, onWalletUpload }) { let name = null; // Priority 1: last $credentialType - if (schema.$credentialTypes && Array.isArray(schema.$credentialTypes) && schema.$credentialTypes.length > 0) { - name = schema.$credentialTypes[schema.$credentialTypes.length - 1]; + if (schema.$credentialType && Array.isArray(schema.$credentialType) && schema.$credentialType.length > 0) { + name = schema.$credentialType[schema.$credentialType.length - 1]; } // Priority 2: schema title else if (schema.title) { diff --git a/tests/keymaster/credential.test.ts b/tests/keymaster/credential.test.ts index 9021eaa..90a3306 100644 --- a/tests/keymaster/credential.test.ts +++ b/tests/keymaster/credential.test.ts @@ -320,13 +320,13 @@ describe('bindCredential', () => { expect(issued.proof!.proofPurpose).toBe('assertionMethod'); }); - it('should auto-derive credential types from schema $credentialTypes', async () => { + it('should override credential types from schema $credentialType', async () => { const issuer = await keymaster.createId('ChessClub'); const member = await keymaster.createId('Member'); const membershipSchema = { "$schema": "http://json-schema.org/draft-07/schema#", - "$credentialTypes": ["DTGCredential", "MembershipCredential"], + "$credentialType": ["VerifiableCredential", "DTGCredential", "MembershipCredential"], "type": "object", "properties": { "memberSince": { "type": "string" } @@ -344,13 +344,12 @@ describe('bindCredential', () => { expect(vc.issuer).toBe(issuer); }); - it('should not duplicate VerifiableCredential when included in $credentialTypes', async () => { + it('should use default types when schema has no $credentialType', async () => { await keymaster.createId('Issuer'); const subject = await keymaster.createId('Subject'); - const schemaWithVC = { + const schemaWithoutTypes = { "$schema": "http://json-schema.org/draft-07/schema#", - "$credentialTypes": ["VerifiableCredential", "CustomCredential"], "type": "object", "properties": { "name": { "type": "string" } @@ -358,11 +357,10 @@ describe('bindCredential', () => { }; await keymaster.setCurrentId('Issuer'); - const schemaDid = await keymaster.createSchema(schemaWithVC); + const schemaDid = await keymaster.createSchema(schemaWithoutTypes); const vc = await keymaster.bindCredential(subject, { schema: schemaDid }); - expect(vc.type).toEqual(['VerifiableCredential', 'CustomCredential']); - expect(vc.type.filter(t => t === 'VerifiableCredential')).toHaveLength(1); + expect(vc.type).toEqual(['VerifiableCredential']); }); }); From 6f0f33b855a0ad893995ae3916fa6e2621850a28 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 4 Feb 2026 12:03:14 -0500 Subject: [PATCH 4/6] Improved coverage --- packages/keymaster/src/keymaster.ts | 5 ++- tests/keymaster/credential.test.ts | 52 ++++++++++++++--------------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/keymaster/src/keymaster.ts b/packages/keymaster/src/keymaster.ts index b474ae1..8ad2e12 100644 --- a/packages/keymaster/src/keymaster.ts +++ b/packages/keymaster/src/keymaster.ts @@ -1653,10 +1653,9 @@ export default class Keymaster implements KeymasterInterface { validFrom?: string; validUntil?: string; claims?: Record; - types?: string[]; } = {} ): Promise { - let { schema, validFrom, validUntil, claims, types } = options; + let { schema, validFrom, validUntil, claims } = options; if (!validFrom) { validFrom = new Date().toISOString(); @@ -1670,7 +1669,7 @@ export default class Keymaster implements KeymasterInterface { "https://www.w3.org/ns/credentials/v2", "https://www.w3.org/ns/credentials/examples/v2" ], - type: ["VerifiableCredential", ...(types || [])], + type: ["VerifiableCredential"], issuer: id.did, validFrom, validUntil, diff --git a/tests/keymaster/credential.test.ts b/tests/keymaster/credential.test.ts index 90a3306..9c06a8f 100644 --- a/tests/keymaster/credential.test.ts +++ b/tests/keymaster/credential.test.ts @@ -294,32 +294,6 @@ describe('bindCredential', () => { expect(vc.credentialSubject!.email).toEqual(expect.any(String)); }); - it('should create a credential with semantic types instead of schema', async () => { - const issuer = await keymaster.createId('ChessClub'); - const member = await keymaster.createId('Bob'); - - await keymaster.setCurrentId('ChessClub'); - const vc = await keymaster.bindCredential(member, { - types: ['DTGCredential', 'MembershipCredential'], - validUntil: '2027-01-06T10:00:00Z', - }); - - expect(vc.type).toContain('VerifiableCredential'); - expect(vc.type).toContain('DTGCredential'); - expect(vc.type).toContain('MembershipCredential'); - expect(vc.credentialSchema).toBeUndefined(); - expect(vc.issuer).toBe(issuer); - expect(vc.credentialSubject!.id).toBe(member); - expect(vc.validUntil).toBe('2027-01-06T10:00:00Z'); - - const did = await keymaster.issueCredential(vc); - const issued = await keymaster.decryptJSON(did) as VerifiableCredential; - - expect(issued.type).toEqual(vc.type); - expect(issued.proof).toBeDefined(); - expect(issued.proof!.proofPurpose).toBe('assertionMethod'); - }); - it('should override credential types from schema $credentialType', async () => { const issuer = await keymaster.createId('ChessClub'); const member = await keymaster.createId('Member'); @@ -344,6 +318,32 @@ describe('bindCredential', () => { expect(vc.issuer).toBe(issuer); }); + it('should override credential context from schema $credentialContext', async () => { + const issuer = await keymaster.createId('ContextOrg'); + const member = await keymaster.createId('ContextMember'); + + const schemaWithContext = { + "$schema": "http://json-schema.org/draft-07/schema#", + "$credentialContext": [ + "https://example.org/credentials/v3", + "https://example.org/credentials/membership/v1" + ], + "type": "object", + "properties": { + "role": { "type": "string" } + } + }; + + await keymaster.setCurrentId('ContextOrg'); + const schemaDid = await keymaster.createSchema(schemaWithContext); + const vc = await keymaster.bindCredential(member, { schema: schemaDid }); + + expect(vc['@context']).toEqual(schemaWithContext.$credentialContext); + expect(vc.credentialSchema).toBeDefined(); + expect(vc.credentialSchema!.id).toBe(schemaDid); + expect(vc.issuer).toBe(issuer); + }); + it('should use default types when schema has no $credentialType', async () => { await keymaster.createId('Issuer'); const subject = await keymaster.createId('Subject'); From b348ded326d19856197a0539990e9523377c07a0 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 4 Feb 2026 12:18:22 -0500 Subject: [PATCH 5/6] Update services/keymaster/server/src/keymaster-api.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- services/keymaster/server/src/keymaster-api.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/services/keymaster/server/src/keymaster-api.ts b/services/keymaster/server/src/keymaster-api.ts index 4fa3f4e..e367b88 100644 --- a/services/keymaster/server/src/keymaster-api.ts +++ b/services/keymaster/server/src/keymaster-api.ts @@ -968,6 +968,13 @@ v1router.delete('/did/:id', async (req, res) => { * type: boolean * 500: * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string */ v1router.put('/did/:id', async (req, res) => { try { From e244612290ad6c5a13283e3a479ed70a274735af Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Wed, 4 Feb 2026 12:19:30 -0500 Subject: [PATCH 6/6] Updated API doc --- doc/keymaster-api.json | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/doc/keymaster-api.json b/doc/keymaster-api.json index 6355633..073c0c2 100644 --- a/doc/keymaster-api.json +++ b/doc/keymaster-api.json @@ -911,6 +911,7 @@ "enum": [ "local", "hyperswarm", + "BTC:mainnet", "BTC:testnet4", "BTC:signet" ] @@ -1013,6 +1014,69 @@ } } } + }, + "put": { + "summary": "Update a DID document.", + "description": "Updates the DID document with the provided data.", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + }, + "description": "The DID to update." + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "doc": { + "type": "object", + "description": "The DID document fields to update." + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates whether the DID was successfully updated.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean" + } + } + } + } + } + }, + "500": { + "description": "Internal server error.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } + } + } + } } }, "/ids/current": {