diff --git a/packages/core/src/vault/credentials.ts b/packages/core/src/vault/credentials.ts index b07da8b..a7a5ccd 100644 --- a/packages/core/src/vault/credentials.ts +++ b/packages/core/src/vault/credentials.ts @@ -110,6 +110,10 @@ export interface CredentialFilter { } export interface CredVaultQueryParams extends CredVaultCallerParams, CredentialFilter { + /** Only credentials whose subject — the identity the agent presents them as — + * is this DID. Narrows the filter; does not satisfy it on its own, since one + * identity's credentials can be the whole vault. */ + subjectDid?: string; /** Also return archived credentials. A **modifier, not a filter** — it does * not satisfy the at-least-one-filter requirement. */ includeArchived?: boolean; @@ -160,6 +164,7 @@ export async function credVaultQuery( ...(params.issuerDid ? { issuerDid: params.issuerDid } : {}), ...(params.purpose ? { purpose: params.purpose } : {}), ...(params.status ? { status: params.status } : {}), + ...(params.subjectDid ? { subjectDid: params.subjectDid } : {}), ...(params.includeArchived ? { includeArchived: true } : {}), ...(params.includeDeleted ? { includeDeleted: true } : {}), }, diff --git a/packages/core/tests/vault.ops.mjs b/packages/core/tests/vault.ops.mjs index 9e79572..4e62c36 100644 --- a/packages/core/tests/vault.ops.mjs +++ b/packages/core/tests/vault.ops.mjs @@ -13,6 +13,7 @@ import { generateSigningIdentity, VtaSession, decodeDigestMultibase, + credVaultQuery, } from "../dist/index.js"; /** A TrustTaskChannel that records the envelope + opts and returns a canned @@ -216,3 +217,24 @@ test("an attachment digest that is not a sha2-256 multihash is refused", () => { // invisible at the one place it matters. assert.throws(() => decodeDigestMultibase("deadbeef"), /unsupported multibase prefix/); }); + +test("credVaultQuery carries subjectDid beside the filter, and it alone is no filter", async () => { + const ch = captureChannel({ credentials: [{ id: "c1", types: ["VerifiableCredential"], status: "valid" }] }); + const res = await credVaultQuery(ch, { + holder, + service, + type: "EmploymentCredential", + status: "valid", + subjectDid: "did:web:member.example", + }); + assert.equal(ch.sent[0].envelope.type, "https://trusttasks.org/spec/vault/credentials/query/0.1"); + assert.deepEqual(ch.sent[0].envelope.payload, { + type: "EmploymentCredential", + status: "valid", + subjectDid: "did:web:member.example", + }); + assert.deepEqual(res.map((c) => c.id), ["c1"]); + + await assert.rejects(credVaultQuery(ch, { holder, service, subjectDid: "did:web:member.example" })); + assert.equal(ch.sent.length, 1, "refused before sending"); +}); diff --git a/packages/extension/src/directory-page-offscreen.ts b/packages/extension/src/directory-page-offscreen.ts index 2866c5c..6f54fd1 100644 --- a/packages/extension/src/directory-page-offscreen.ts +++ b/packages/extension/src/directory-page-offscreen.ts @@ -258,7 +258,17 @@ export async function runDirectoryPageOp( const asked = credentialAskFor(fields, op.fields); if (!asked.ok) throw new Error(asked.error); const { ask } = asked; - const held = await credVaultQuery(sender, { holder, service, type: ask.credentialType, status: "valid" }); + // Only credentials the agent would present as the member: the community + // accepts a presentation only from its member, so one bound to another + // key (a separate SD-JWT holder key) would be refused after the member + // chose it. + const held = await credVaultQuery(sender, { + holder, + service, + type: ask.credentialType, + status: "valid", + subjectDid: op.member, + }); const candidates = held .filter((c) => !c.lifecycle || c.lifecycle === "active") .filter((c) => !ask.issuers || (c.issuerDid !== undefined && ask.issuers.includes(c.issuerDid)))