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
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,6 @@ jspm_packages/
/.vscode
.cursor

# local development notes
docs/plans

# Output
dist

Expand Down
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ not run after a fatal failure.
- Pure `foldCheckResults` helper and `computeId` namespace builder, both
exported from the package barrel.
- New consumer doc at `docs/api/verification-results.md` covering the result
shape, the `id` namespace, a UI rendering recipe, and a prompt-ready appendix
for downstream UIs.
shape, the `id` namespace, and a UI rendering recipe.
- `timing?: boolean` flag on `VerifierConfig`, `VerifyCredentialCall`, and
`VerifyPresentationCall`. When true, every `CheckResult`, every
`SuiteSummary`, and every top-level
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,8 +849,8 @@ embeds its suite's phase, so re-tagging a built-in suite would rename every
check in it: changing either is a breaking change.

See [`docs/api/verification-results.md`](./docs/api/verification-results.md) for
the full reference (phase model, `SuiteSummary` fields, `id` namespace, UI
rendering recipe, and a prompt-ready appendix for downstream UIs).
the full reference (phase model, `SuiteSummary` fields, `id` namespace, and a UI
rendering recipe).

## Capturing timing data

Expand All @@ -871,7 +871,7 @@ console.log(result.summary[0].timing!.durationMs);
Per-check timings ride on every row of `results[]`, and the per-suite rollup
lives on `summary[i].timing`. See
[`docs/api/timing.md`](./docs/api/timing.md) for the full reference, granularity
table, recipes, and a prompt-ready appendix.
table, and recipes.

## Pluggable clock (`TimeService`)

Expand Down Expand Up @@ -1044,9 +1044,9 @@ for (const credResult of presResult.credentialResults) {
}
```

This is the recommended advanced integration shape for services like
`dcc-transaction-service` that verify a presentation up front and then inspect
each embedded credential's profile to drive downstream business logic. If
This is the recommended advanced integration shape for a service that verifies
a presentation up front and then inspects each embedded credential's profile to
drive downstream business logic. If
performance matters, you're verifying high volumes of credentials, you want to
show partial results to a user for verification in progress, or you have
specific requirements for the verification process, this advanced integration
Expand Down
80 changes: 2 additions & 78 deletions docs/api/timing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
> Consumer reference for the optional `timing` flag and the
> `TaskTiming` data carried on every result when it is enabled.
>
> **Audience.** Performance / observability work in
> `dcc-transaction-service`, `skills-verifier`, and any future
> client building on `verifier-core`. The §9 LLM-prompt
> appendix is written to be copy-pasted as the seed of an LLM
> prompt for client-side instrumentation code-gen.
> **Audience.** Performance and observability work in any client
> building on `verifier-core`.

## Table of Contents

Expand All @@ -19,7 +16,6 @@
6. [Parse-failure behavior](#parse-failure-behavior)
7. [Recipes](#recipes)
8. [`TimeService` reference](#timeservice-reference)
9. [Prompt-ready appendix](#prompt-ready-appendix)

## Overview

Expand Down Expand Up @@ -245,75 +241,3 @@ verifier built on a `FakeTimeService` decides both at the pinned
moment. Signature clock-skew windows and key rotation belong
there too. Read it from `VerificationContext.timeService` inside
`execute()`.

## Prompt-ready appendix

> Drop this section verbatim into an LLM prompt for client-side
> instrumentation code-gen.

`@digitalcredentials/verifier-core` v2 emits optional
`TaskTiming` data on every result when constructed with
`timing: true`:

```ts
import { createVerifier } from '@digitalcredentials/verifier-core';
const verifier = createVerifier({ timing: true });
const result = await verifier.verifyPresentation({ presentation });
```

Every result then carries a `timing: TaskTiming` field at three
levels:

```ts
interface TaskTiming {
startedAt: string; // ISO 8601 wall-clock
endedAt: string; // ISO 8601 wall-clock
durationMs: number; // monotonic, fractional ms
events?: ReadonlyArray<TaskTiming & { name: string }>; // reserved
}

// Per-check (every row in results[] when timing: true):
result.results[i].timing
// Per-suite (always present in summary[] when timing: true):
result.summary[i].timing
// Top-level inclusive wrapper:
result.timing
// In verifyPresentation results, each embedded VC also has:
result.credentialResults[i].timing
```

Invariants (always hold when `timing: true`):

1. `endedAt >= startedAt` for every `TaskTiming`.
2. `durationMs >= 0` for every `TaskTiming`.
3. Top-level `result.timing.durationMs >= max(s.timing.durationMs for s in result.summary)`.
4. `verifyPresentation` top-level `result.timing.durationMs >= cr.timing.durationMs` for every `cr in result.credentialResults`.
5. Per-suite `timing.durationMs == sum(child timing.durationMs)`.
6. Per-suite `timing.startedAt == min(child startedAt)`,
per-suite `timing.endedAt == max(child endedAt)`.

Use `durationMs` for any sort / sum / comparison (it comes
from a monotonic clock); use `startedAt` / `endedAt` only for
log correlation and human-readable timelines.

A "verification time breakdown" UI from a presentation result:

```ts
function buildBreakdown(result: PresentationVerificationResult) {
const totalMs = result.timing!.durationMs;
return {
totalMs,
presentationOnlyMs: result.summary
.reduce((a, s) => a + (s.timing?.durationMs ?? 0), 0),
credentials: result.credentialResults.map(cr => ({
id: cr.verifiableCredential.id,
totalMs: cr.timing!.durationMs,
bySuite: cr.summary.map(s => ({
suite: s.id,
durationMs: s.timing!.durationMs,
fatalFailureAt: s.fatalFailureAt,
})),
})),
};
}
```
230 changes: 2 additions & 228 deletions docs/api/verification-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
> `id` namespace, the `SuiteSummary` rollup (new in v2.0.0), and a UI
> rendering recipe.
>
> **Audience.** UI/integration teams building on top of `verifier-core`,
> primarily `dcc-transaction-service`. The §8 prompt-ready appendix is
> written to be copy-pasted as the seed of an LLM prompt.
> **Audience.** UI and integration teams building on top of
> `verifier-core`.
>
> **See also.** For per-check / per-suite / per-call timing data
> on every result, see [`timing.md`](./timing.md).
Expand All @@ -23,7 +22,6 @@
5. [`id` namespace reference](#id-namespace-reference)
6. [UI rendering recipe](#ui-rendering-recipe)
7. [Backwards compatibility](#backwards-compatibility)
8. [Prompt-ready appendix](#prompt-ready-appendix)

## Overview

Expand Down Expand Up @@ -548,227 +546,3 @@ Lazy-expand them — most users only want the verdict + per-phase status.
against the summary
rollup, narrowing by `outcome.status`. See §6 step 3 for the
canonical pattern.

## Prompt-ready appendix

Copy the fenced block below as the seed of an LLM prompt for the
dcc-transaction-service UI implementation.

> Given the JSON shape and `SuiteSummary` type below, produce a React
> component that renders a presentation verification result. Group
> per-credential summaries by phase
> (`cryptographic` → `trust` → `recognition` → `semantic` → `unknown`).
> Show failure detail rows (from `results[]` filtered by `id` prefix
> **and** `outcome.status === 'failure'`) under the failing phase row,
> lazy-expanded. Colour a suite red only when it has `fatalFailureAt`;
> a suite with failures but no `fatalFailureAt` is a warning (amber),
> because non-fatal failures leave `verified` true.
>
> The result is the return value of
> `verifier.verifyPresentation({ presentation })` from
> `@digitalcredentials/verifier-core` (v2.0.0+). `results[]` carries
> every check — passes, skips with a reason, and rows not run after a
> fatal failure — so a row's `outcome.status` is what distinguishes
> them.
>
> `verified` means "no fatal check failed" (and, for a presentation,
> that every embedded credential is itself verified). The advisory
> (non-fatal) checks are `trust.registry.issuer`,
> `recognition.profile`, `cryptographic.status.list-issuer`, the Open
> Badges checks (the semantic `schema.obv3.*` checks and the Open
> Badges JSON-schema check), and any caller-supplied check without
> `fatal`. A credential whose only failing check is advisory is
> `verified: true` with `status: 'failure'` (or `'mixed'`) and no
> `fatalFailureAt` on that suite — render the failure rows as warnings,
> but do not render the verdict as failed.

---

**JSON example.**

```jsonc
{
"verified": false,
"verifiablePresentation": { /* the VP you passed in, unchanged */ },
"presentationResults": [
{ "id": "cryptographic.core.vp-structure", /* success */ },
{ "id": "cryptographic.proof.signature", /* success */ }
],
"summary": [
{ "id": "cryptographic.proof", "phase": "cryptographic",
"suite": "proof", "status": "success",
"message": "1 of 1 check passed",
"counts": { "passed": 1, "failed": 0, "skipped": 0 } }
],
"credentialResults": [
{
// Only advisory checks failed, so the verdict stays true.
"verified": true,
"verifiableCredential": { /* VC with an unregistered issuer */ },
"recognizedProfile": "obv3p0.openbadge",
"results": [
{ "id": "cryptographic.core.context-exists", /* success */ },
{ "id": "cryptographic.core.vc-context", /* success */ },
{ "id": "cryptographic.core.vc-structure", /* success */ },
{ "id": "cryptographic.core.credential-id", /* success */ },
{ "id": "cryptographic.core.proof-exists", /* success */ },
{ "id": "recognition.profile", /* success */ },
{ "id": "cryptographic.proof.signature", /* success */ },
{ "id": "trust.registry.issuer",
"suite": "registry",
"outcome": { "status": "failure", "problems": [
{ "type": "https://www.w3.org/TR/vc-data-model#ISSUER_NOT_REGISTERED",
"title": "Issuer Not Registered",
"detail": "Issuer DID not in any registry." }
],
"payload": { "matches": [], "uncheckedRegistries": [] }},
"fatal": false }
],
"summary": [
{ "id": "cryptographic.core", "phase": "cryptographic",
"suite": "core", "status": "success",
"message": "5 of 5 checks passed",
"counts": { "passed": 5, "failed": 0, "skipped": 0 } },
{ "id": "recognition", "phase": "recognition",
"suite": "recognition", "status": "success",
"message": "1 of 1 check passed",
"counts": { "passed": 1, "failed": 0, "skipped": 0 } },
{ "id": "cryptographic.proof", "phase": "cryptographic",
"suite": "proof", "status": "success",
"message": "1 of 1 check passed",
"counts": { "passed": 1, "failed": 0, "skipped": 0 } },
// Non-fatal failure: status "failure", but no fatalFailureAt
// and the credential is still verified. Render as a warning.
{ "id": "trust.registry", "phase": "trust",
"suite": "registry", "status": "failure",
"message": "1 of 1 check failed",
"counts": { "passed": 0, "failed": 1, "skipped": 0 } }
]
},
{
// A fatal failure: this is what makes a verdict false.
"verified": false,
"verifiableCredential": { /* VC with a broken signature */ },
"recognizedProfile": "obv3p0.openbadge",
"results": [
{ "id": "cryptographic.core.context-exists", /* success */ },
{ "id": "cryptographic.core.vc-context", /* success */ },
{ "id": "cryptographic.core.vc-structure", /* success */ },
{ "id": "cryptographic.core.credential-id", /* success */ },
{ "id": "cryptographic.core.proof-exists", /* success */ },
{ "id": "recognition.profile", /* success */ },
{ "id": "cryptographic.proof.signature",
"suite": "proof",
"outcome": { "status": "failure", "problems": [
{ "type": "https://www.w3.org/TR/vc-data-model#INVALID_SIGNATURE",
"title": "Invalid Signature",
"detail": "The credential's signature did not verify." }
]},
"fatal": true },
{ "id": "cryptographic.status.bitstring",
"suite": "status",
"outcome": { "status": "skipped",
"reason": "Not run: proof.signature failed" } },
{ "id": "cryptographic.status.list-issuer",
"suite": "status",
"outcome": { "status": "skipped",
"reason": "Not run: proof.signature failed" } },
{ "id": "trust.registry.issuer",
"suite": "registry",
"outcome": { "status": "skipped",
"reason": "Not run: proof.signature failed" } }
],
"summary": [
{ "id": "cryptographic.core", "phase": "cryptographic",
"suite": "core", "status": "success",
"message": "5 of 5 checks passed",
"counts": { "passed": 5, "failed": 0, "skipped": 0 } },
{ "id": "recognition", "phase": "recognition",
"suite": "recognition", "status": "success",
"message": "1 of 1 check passed",
"counts": { "passed": 1, "failed": 0, "skipped": 0 } },
// The fatal failure: the one summary with fatalFailureAt.
{ "id": "cryptographic.proof", "phase": "cryptographic",
"suite": "proof", "status": "failure",
"message": "1 of 1 check failed",
"counts": { "passed": 0, "failed": 1, "skipped": 0 },
"fatalFailureAt": "cryptographic.proof.signature" },
{ "id": "cryptographic.status", "phase": "cryptographic",
"suite": "status", "status": "skipped",
"message": "status not run: proof.signature failed",
"counts": { "passed": 0, "failed": 0, "skipped": 2 } },
{ "id": "trust.registry", "phase": "trust",
"suite": "registry", "status": "skipped",
"message": "registry not run: proof.signature failed",
"counts": { "passed": 0, "failed": 0, "skipped": 1 } }
]
}
]
}
```

**`SuiteSummary` type.**

```ts
type SuiteSummaryPhase =
| 'cryptographic'
| 'trust'
| 'recognition'
| 'semantic'
| 'unknown';

interface SuiteSummary {
id: string;
phase: SuiteSummaryPhase;
suite: string;
status: 'success' | 'failure' | 'skipped' | 'mixed';
message: string;
counts: { passed: number; failed: number; skipped: number };
fatalFailureAt?: string;
}
```

**Grouping snippet.**

```ts
const PHASE_ORDER: SuiteSummary['phase'][] = [
'cryptographic',
'trust',
'recognition',
'semantic',
'unknown',
];

function groupByPhase(cr: CredentialVerificationResult) {
const groups = new Map<SuiteSummary['phase'], SuiteSummary[]>();
for (const phase of PHASE_ORDER) groups.set(phase, []);
for (const s of cr.summary) groups.get(s.phase)!.push(s);
return groups;
}

function failureDetail(
cr: CredentialVerificationResult,
failing: SuiteSummary,
) {
return cr.results.filter(
r => r.id.startsWith(failing.id + '.') && r.outcome.status === 'failure',
);
}
```

**Layout reference.**

```
Presentation: ✗ (1 of 2 credentials verified)
├─ VP signature: ✓
├─ Credential 1 (obv3p0.openbadge): ✓ — verified, with advisory failures
│ ├─ cryptographic ✓
│ ├─ trust ✗ (1 of 1 check failed — advisory)
│ │ └─ trust.registry.issuer — Issuer Not Registered
│ └─ recognition ✓
└─ Credential 2 (obv3p0.openbadge): ✗
├─ cryptographic ✗ (1 of 1 check failed)
│ └─ cryptographic.proof.signature — Invalid Signature
├─ trust — (not run after fatal)
└─ recognition ✓
```
Loading
Loading