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
64 changes: 64 additions & 0 deletions doc/keymaster-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,7 @@
"enum": [
"local",
"hyperswarm",
"BTC:mainnet",
"BTC:testnet4",
"BTC:signet"
]
Expand Down Expand Up @@ -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": {
Expand Down
10 changes: 10 additions & 0 deletions packages/keymaster/src/keymaster-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,16 @@ export default class KeymasterClient implements KeymasterInterface {
}
}

async updateDID(id: string, doc: DidCidDocument): Promise<boolean> {
try {
const response = await axios.put(`${this.API}/did/${id}`, { doc });
return response.data.ok;
}
catch (error) {
throwError(error);
}
}

async revokeDID(id: string): Promise<boolean> {
try {
const response = await axios.delete(`${this.API}/did/${id}`);
Expand Down
21 changes: 12 additions & 9 deletions packages/keymaster/src/keymaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,10 +1653,9 @@ export default class Keymaster implements KeymasterInterface {
validFrom?: string;
validUntil?: string;
claims?: Record<string, unknown>;
types?: string[];
} = {}
): Promise<VerifiableCredential> {
let { schema, validFrom, validUntil, claims, types } = options;
let { schema, validFrom, validUntil, claims } = options;

if (!validFrom) {
validFrom = new Date().toISOString();
Expand All @@ -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,
Expand All @@ -1682,16 +1681,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<string, unknown> } | null;
const schemaDoc = await this.getSchema(schemaDID) as { $credentialContext?: string[]; $credentialType?: string[]; properties?: Record<string, unknown> } | 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 = {
Expand All @@ -1700,7 +1703,7 @@ export default class Keymaster implements KeymasterInterface {
};
}

if (claims) {
if (claims && Object.keys(claims).length) {
vc.credentialSubject = {
id: subjectDID,
...claims,
Expand Down
3 changes: 2 additions & 1 deletion packages/keymaster/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,9 @@ export interface KeymasterInterface {
getName(name: string): Promise<string | null>;
removeName(name: string): Promise<boolean>;

// DID resolution
// DIDs
resolveDID(did: string, options?: ResolveDIDOptions): Promise<DidCidDocument>;
updateDID(id: string, doc: DidCidDocument): Promise<boolean>;

// Assets
createAsset(data: unknown, options?: CreateAssetOptions): Promise<string>;
Expand Down
4 changes: 2 additions & 2 deletions services/gatekeeper/client/src/KeymasterUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions services/keymaster/client/src/KeymasterUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions services/keymaster/server/src/keymaster-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,58 @@ 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.
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
*/
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:
Expand Down
32 changes: 32 additions & 0 deletions tests/keymaster/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
66 changes: 32 additions & 34 deletions tests/keymaster/credential.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,39 +294,13 @@ 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 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" }
Expand All @@ -344,25 +318,49 @@ describe('bindCredential', () => {
expect(vc.issuer).toBe(issuer);
});

it('should not duplicate VerifiableCredential when included in $credentialTypes', async () => {
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');

const schemaWithVC = {
const schemaWithoutTypes = {
"$schema": "http://json-schema.org/draft-07/schema#",
"$credentialTypes": ["VerifiableCredential", "CustomCredential"],
"type": "object",
"properties": {
"name": { "type": "string" }
}
};

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']);
});
});

Expand Down
Loading