Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
94824ca
feat: implement explicit credential selection in proof controller
rmlearney-digicatapult Dec 11, 2025
e4fc1c0
version bump
rmlearney-digicatapult Dec 11, 2025
c7b0da4
one-liner
rmlearney-digicatapult Dec 11, 2025
2160443
chore: update integration test instructions to match README
rmlearney-digicatapult Dec 11, 2025
e19e478
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
770c1a7
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
938342c
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
1d625be
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
5ef7855
Update README.md
rmlearney-digicatapult Dec 11, 2025
b805fd3
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
568a7e1
fix: enhance proof controller validation, logging and tests
rmlearney-digicatapult Dec 11, 2025
ae33840
refactor: secure logging and remove unused endpoint
rmlearney-digicatapult Dec 11, 2025
c298134
install latest versions of npm in dockerfile
rmlearney-digicatapult Dec 11, 2025
f10d2ef
Update src/controllers/v1/proofs/ProofController.ts
rmlearney-digicatapult Dec 11, 2025
6ef2729
Update docs/explicit-credential-selection.md
rmlearney-digicatapult Dec 11, 2025
4015034
Update package.json
rmlearney-digicatapult Dec 11, 2025
2dd1365
version bump
rmlearney-digicatapult Dec 11, 2025
bc56fbf
refactor(proofs): optimize validation and redaction in acceptRequest
rmlearney-digicatapult Dec 11, 2025
cccac33
refactor(proofs): remove any type usage in acceptRequest
rmlearney-digicatapult Dec 11, 2025
79b5f9b
feat(scripts): add credential existence check to maker-accept-proof-f…
rmlearney-digicatapult Dec 11, 2025
b38c402
fix: relax AcceptProofProposalOptions validation for TSOA
rmlearney-digicatapult Dec 11, 2025
63961f1
fix(proofs): address PR review comments
rmlearney-digicatapult Dec 11, 2025
45a250d
fix: consolidate logging and add revealed status test
rmlearney-digicatapult Dec 11, 2025
133439a
test: verify acceptProofStub is not called in validation tests
rmlearney-digicatapult Dec 11, 2025
5ed1727
remove npm install
rmlearney-digicatapult Dec 12, 2025
8760b50
simplify validation logic
rmlearney-digicatapult Dec 12, 2025
0802f08
address test review
rmlearney-digicatapult Dec 12, 2025
2c14804
address readme comment
rmlearney-digicatapult Dec 12, 2025
54fc5ce
feat: enrich proof webhook with proposal message and add getProposalM…
rmlearney-digicatapult Dec 12, 2025
2446432
comment
rmlearney-digicatapult Dec 12, 2025
5e6984b
rename anoncreds variables
rmlearney-digicatapult Dec 12, 2025
7de8734
commented code
rmlearney-digicatapult Dec 12, 2025
71b09a2
only metadata logging at info level
rmlearney-digicatapult Dec 12, 2025
8d619d3
revert event handler and webhook test changes
rmlearney-digicatapult Dec 12, 2025
cc504d2
comment
rmlearney-digicatapult Dec 12, 2025
693daf1
improve proofcontroller tests
rmlearney-digicatapult Dec 12, 2025
1148f03
feat: enforce strict attribute disclosure validation in proof acceptance
rmlearney-digicatapult Dec 12, 2025
46d24e9
fix test expectation text
rmlearney-digicatapult Dec 12, 2025
c2e1302
refactor(proofs): separate proof content from record endpoint
rmlearney-digicatapult Dec 13, 2025
a882a3e
Merge branch 'main' into feature/credential-selection
rmlearney-digicatapult Dec 13, 2025
4585917
Merge main into feature/credential-selection: keep v0.17.0 but accept…
rmlearney-digicatapult Dec 17, 2025
66c3a56
Refactor ProofController, improve types, and add webhook noise reduction
rmlearney-digicatapult Dec 18, 2025
18c194d
Update explicit credential selection docs with new endpoints
rmlearney-digicatapult Dec 18, 2025
622650c
feat: enhance proof controller error messages and update tests for ex…
rmlearney-digicatapult Dec 18, 2025
8eafe6a
linting
rmlearney-digicatapult Dec 18, 2025
b18ae55
docker-compose-testnet one liner
rmlearney-digicatapult Dec 18, 2025
0a0c0b9
fix: resolve integration test failures and 500 error in ProofController
rmlearney-digicatapult Dec 18, 2025
3a43b83
docs: add client-side helper example for credential selection
rmlearney-digicatapult Dec 18, 2025
9c44a87
change log levels
rmlearney-digicatapult Dec 18, 2025
c74456d
improve documentation
rmlearney-digicatapult Dec 18, 2025
8a98f33
fix(test): correct getCredentialsForRequest mock structure in proof u…
rmlearney-digicatapult Dec 18, 2025
a6920ad
linting
rmlearney-digicatapult Dec 18, 2025
c3c83e5
fix(test): add polling and increase timeout for flaky proof verificat…
rmlearney-digicatapult Dec 19, 2025
881e2d6
increase timeout for flaky IPFS test
rmlearney-digicatapult Dec 19, 2025
08e43b7
Merge branch 'main' into feature/credential-selection
rmlearney-digicatapult Dec 19, 2025
524d998
refactor(scripts): make CLI parameters optional and improve workflow …
rmlearney-digicatapult Dec 19, 2025
f168f01
renames
rmlearney-digicatapult Dec 19, 2025
b8c69bf
remove script dependency from e2e test
rmlearney-digicatapult Dec 19, 2025
6e5aa37
feat: add endpoint to retrieve matching credentials for proof request
rmlearney-digicatapult Jan 8, 2026
a4f99a4
open IPFS gateway port for integration test runner
rmlearney-digicatapult Jan 8, 2026
1fbfafe
Merge branch 'main' into feature/credential-selection
rmlearney-digicatapult Jan 8, 2026
07baa87
unit test new endpoint
rmlearney-digicatapult Jan 8, 2026
4fd3fdf
remove unnecessary
rmlearney-digicatapult Jan 8, 2026
1149531
e2e test now uses new endpoint
rmlearney-digicatapult Jan 8, 2026
cf57e29
Merge branch 'main' into feature/credential-selection
rmlearney-digicatapult Jan 9, 2026
05b5ceb
remove test expectation
rmlearney-digicatapult Jan 9, 2026
4f84d22
remove ipfs gateway from integration test
rmlearney-digicatapult Jan 9, 2026
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
101 changes: 52 additions & 49 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bob-listener.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { spawn } from 'node:child_process'
const PORT = process.env.PORT || 3003
const AUTO_CONNECT_TO_OEM_SCRIPT_PATH = './scripts/maker-connect-to-oem.ts'
const PROPOSE_PROOF_SCRIPT_PATH = './scripts/maker-propose-proof-to-oem.ts'
const ACCEPT_PROOF_SCRIPT_PATH = './scripts/maker-accept-proof-from-oem.ts'
const ACCEPT_PROOF_SCRIPT_PATH = './scripts/maker-respond-proof-request.ts'
const TRIGGER_STATES = (process.env.TRIGGER_STATES || 'offer-received,credential-issued')
.split(',')
.map((s) => s.trim().toLowerCase())
Expand All @@ -15,7 +15,7 @@ const { log, error } = console
const app = express()
app.use(express.json({ type: '*/*' }))

// Handle credential events, in our demo this is the credential sent from Alice(MoD) to Bob(Maker)
// Handle credential events, in our demo this is the credential sent from Alice(Requester) to Bob(Maker)
app.post('/credentials', (req, res) => {
const cred = req.body || {}
credentialRecordId = cred.id
Expand Down
5 changes: 5 additions & 0 deletions docker-compose-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
# Run stack and integration tests with:
#
# export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" &&
# docker compose \
# -f docker-compose-testnet.yml \
# -f docker-compose-integration-tests.yml \
# up --build --exit-code-from integration-tests
#
# Or as a one-liner:
# export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" && docker compose -f docker-compose-testnet.yml -f docker-compose-integration-tests.yml up --build --exit-code-from integration-tests
#

services:
integration-tests:
Expand All @@ -22,6 +26,7 @@ services:
condition: service_healthy
environment:
- NODE_ENV=test
- NODE_EXTRA_CA_CERTS=/rootCA.pem
- ALICE_BASE_URL=http://alice:3000
- ALICE_WS_PORT=5003
- ALICE_DID
Expand Down
6 changes: 3 additions & 3 deletions docker-compose-testnet.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# docker compose -f docker-compose-testnet.yml up --build -d
# export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" && docker compose -f docker-compose-testnet.yml up --build -d

##################################################################################
## Docker-Compose for a 3-agent testnet + 3-node private IPFS cluster ############
Expand Down Expand Up @@ -93,8 +93,8 @@ services:
- POSTGRES_PORT=5432
- POSTGRES_USERNAME=postgres
- POSTGRES_PASSWORD=postgres
- AUTO_ACCEPT_CREDENTIALS=always
- AUTO_ACCEPT_PROOFS=always
- AUTO_ACCEPT_CREDENTIALS=never
- AUTO_ACCEPT_PROOFS=contentApproved
- DID_WEB_ENABLED=true
- DID_WEB_USE_DEV_CERT=true
- DID_WEB_PORT=8443
Expand Down
169 changes: 169 additions & 0 deletions docs/explicit-credential-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Explicit Credential Selection

When responding to a Proof Request, an agent may hold multiple credentials that satisfy the request's requirements (e.g., multiple credentials from the same issuer, or multiple credentials with the same attribute names). By default, the agent might select any valid credential, which can lead to incorrect data being shared if a specific credential was intended.

The **Explicit Credential Selection** feature allows a client (controller) to specify exactly which credential ID should be used for each requested attribute or predicate.

## The Problem

1. **Ambiguity**: If a user has two "Employee ID" credentials, the agent doesn't know which one to present.
2. **Complexity**: The underlying Credo-TS `acceptRequest` method requires a highly complex `proofFormats` object containing deep metadata (`schemaId`, `credDefId`, `revocationRegistryId`, raw values, etc.) to specify a credential. Constructing this object on the client side is difficult and error-prone.

## The Solution: Simplified Proof Formats

The `veritable-cloudagent` API supports a **Simplified Proof Format**. Clients can provide a lightweight object specifying only the `credentialId` and `revealed` status for each attribute. The agent's controller then "hydrates" this object by fetching the full credential details from the wallet before passing it to the core agent.

### API Usage

**Endpoint**: `POST /v1/proofs/{proofRecordId}/accept-request`

**Body**:

```json
{
"autoAcceptProof": "contentApproved",
"proofFormats": {
"anoncreds": {
"attributes": {
"attribute_name_1": {
"credentialId": "credential-uuid-1",
"revealed": true
},
"attribute_name_2": {
"credentialId": "credential-uuid-1",
"revealed": true
}
},
"predicates": {
"predicate_name_1": {
"credentialId": "credential-uuid-2"
}
}
}
}
}
```

### How it Works

The `ProofController` employs a three-path logic flow to handle proof acceptance:

1. **Auto-Selection (No `proofFormats` provided)**
* If the client omits `proofFormats`, the agent automatically selects the best available credentials that satisfy the request.
* This is useful for simple use cases where ambiguity is not a concern.

2. **Hydration (Simplified `proofFormats` provided)**
* The controller detects the simplified format (missing `credentialInfo`).
* It queries the agent for all valid credentials for this proof request.
* It matches the `credentialId` provided by the client with the available credentials.
* It populates the required `credentialInfo` (Schema ID, Cred Def ID, etc.) into the request object.
* **Validation**:
* If attributes are missing in the simplified format, the API returns a `400 Bad Request`.
* If no matching credentials are found to satisfy the request, the API returns a `404 Not Found`.
Comment thread
rmlearney-digicatapult marked this conversation as resolved.

3. **Pass-Through (Full `proofFormats` provided)**
* If the client provides the full, complex Credo-TS proof format, the controller passes it directly to the agent.
* This supports advanced use cases where the client needs full control over the cryptographic metadata.

### Example Scenario

**Scenario**: A Manufacturer (Bob) needs to prove the `tdp_reference` of a specific product to an OEM (Charlie). Bob has multiple credentials for different products.

1. **Charlie** sends a Proof Request for `tdp_reference`.
2. **Bob's Client** inspects the request and identifies that `credential-123` contains the correct TDP reference.
3. **Bob's Client** calls `accept-request` with:

```json
{
"proofFormats": {
"anoncreds": {
"attributes": {
"tdp_reference": {
"credentialId": "credential-123",
"revealed": true
}
},
"predicates": {}
}
}
}
```

4. **Bob's Agent** looks up `credential-123`, retrieves its full metadata, and generates a proof using that specific credential.

## Retrieving Proof Context

To construct the `accept-request` body, the client often needs to know what attributes are being requested and what credentials are available.

### 1. Get Proof Record with Content

To avoid making multiple calls, you can fetch the proof record along with its content (request and presentation data) in a single request.

**Endpoint**: `GET /v1/proofs/{proofRecordId}?includeContent=true`

**Response**:
Returns the `ProofExchangeRecord` with an additional `content` property containing the raw proof format data.

### 2. Get Simplified Proof Content

The raw proof content can be complex to parse. You can request a simplified view that flattens the structure into simple key-value pairs.

**Endpoint**: `GET /v1/proofs/{proofRecordId}/content?view=simplified`

**Response**:

```json
{
"attribute_name_1": "value_1",
"attribute_name_2": "value_2"
}
```

This is particularly useful for displaying the values of a received proof (Verifier role) or understanding what is being requested (Prover role) without navigating the deep AnonCreds structure.

## Developer Experience

While the simplified format is much cleaner than the raw format, constructing the nested JSON object can still be verbose. It is recommended to use a helper function in your client application to generate this structure.

### Client-Side Helper Example (TypeScript)

> **Important**: The `credentialId` used in the selection payload must be the **Wallet Credential ID** (the ID of the credential stored in the wallet), NOT the **Credential Exchange Record ID** (the ID of the record tracking the issuance process).
>
> In Credo-TS, you can access this via `credentialRecord.credentials[0].credentialRecordId`.

```typescript
/**
* Helper to construct the simplified proof format payload.
*
* @param selections A map where the key is the attribute name and the value is the credential ID to use.
* @returns The formatted payload ready for the API.
*/
function createSelection(selections: Record<string, string>) {
const attributes: Record<string, { credentialId: string; revealed: boolean }> = {};

for (const [key, credId] of Object.entries(selections)) {
attributes[key] = { credentialId: credId, revealed: true };
}

return {
proofFormats: {
anoncreds: {
attributes
}
}
};
}

// Usage Example:
// 1. User selects specific credentials for the requested attributes
const userSelections = {
'name': 'credential-uuid-123',
'email': 'credential-uuid-456'
};

// 2. Generate the payload
const payload = createSelection(userSelections);

// 3. Send to API
await client.acceptProofRequest(proofRecordId, payload);
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@digicatapult/veritable-cloudagent",
"version": "0.16.21",
"version": "0.17.0",
"main": "build/index",
"type": "module",
"types": "build/index",
Expand All @@ -25,7 +25,7 @@
"scripts": {
"depcheck": "depcheck",
"lint": "eslint .",
"lint:fix": "npm run lint -- -- fix",
"lint:fix": "eslint . --fix",
Comment thread
rmlearney-digicatapult marked this conversation as resolved.
"check": "npm run tsoa:build && tsc --noEmit",
"clean": "rimraf -rf ./build",
"build": "npm run clean && npm run tsoa:build && swc ./src -d ./build --strip-leading-paths --copy-files",
Expand Down
Empty file modified scripts/connection-setup.sh
100644 → 100755
Empty file.
42 changes: 35 additions & 7 deletions scripts/issue-credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,21 @@ const connectionParser = z.array(
})
)
interface ParsedArgs {
credDefId: string
credDefId?: string
oemDid: string
makerDid: string
baseUrl: string
}
function printUsageAndExit(code: number): never {
process.stderr.write(
'Usage: issue-credential --oem <oemDid> --maker <makerDid> --cred-def-id <credDefId> [--base-url <url>]\n'
'Usage: issue-credential --oem <oemDid> --maker <makerDid> [--cred-def-id <credDefId>] [--base-url <url>]\n'
)
process.exit(code)
}

function parseArgs(argv: string[]): ParsedArgs {
const args = argv.slice(2)
const parsed: ParsedArgs = {
credDefId: '',
baseUrl: 'http://localhost:3000',
oemDid: 'did:web:charlie%3A8443',
makerDid: 'did:web:bob%3A8443',
Expand Down Expand Up @@ -61,14 +60,42 @@ function parseArgs(argv: string[]): ParsedArgs {
}

async function main() {
const { credDefId, oemDid, makerDid, baseUrl } = parseArgs(process.argv)
const { credDefId: argsCredDefId, oemDid, makerDid, baseUrl } = parseArgs(process.argv)
const log = (msg: string, extra?: unknown) => {
process.stderr.write(`${msg}${extra ? ' ' + JSON.stringify(extra) : ''}\n`)
}
if (credDefId.length === 0) {
process.stderr.write('--cred-def-id is required\n')
printUsageAndExit(1)

let credDefId = argsCredDefId

if (!credDefId) {
log(`Looking for credential definitions at ${baseUrl}...`)
const credDefsResponse = await fetch(`${baseUrl}/v1/credential-definitions?createdLocally=true`, {
method: 'GET',
headers: { accept: 'application/json' },
})

if (!credDefsResponse.ok) {
throw new Error(
`Failed to retrieve credential definitions: ${credDefsResponse.status} ${credDefsResponse.statusText}`
)
}

const credDefs = await credDefsResponse.json()

if (!Array.isArray(credDefs) || credDefs.length === 0) {
throw new Error('No credential definitions found on the agent.')
}

// Pick the most recent one (assuming the list is ordered or we just take the last one)
const targetCredDef = credDefs[credDefs.length - 1]
credDefId = targetCredDef.id
log(`Found credential definition: ${credDefId}`)
}

if (!credDefId) {
throw new Error('Could not determine credential definition ID')
}

// get connectionId for bob on alice
const connections = await fetch(`${baseUrl}/v1/connections`, {
method: 'GET',
Expand Down Expand Up @@ -195,6 +222,7 @@ async function main() {
}

log('Credential offer sent to maker. Awaiting acceptance...')
process.exit(0)
}

main().catch((e) => process.stderr.write((e as Error).stack + '\n') && process.exit(1))
Loading