From 9d670f1e2fa57e7ac860b45ca636ae0ce89cf8c4 Mon Sep 17 00:00:00 2001 From: Nate Otto Date: Tue, 20 Oct 2026 18:29:50 -0500 Subject: [PATCH] docs: rewrite the 2.0.0 changelog and migration guide against 1.0.0-beta.11 The 2.0.0 entry and the README migration section described changes between states that were never released. Both now compare against the last release, 1.0.0-beta.11. The entry is headed Unreleased and lists the breaking changes, the new API grouped by area, and what was removed. The migration section maps the 1.x options, credential result and presentation result to 2.0 field by field, with one before-and-after example. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 450 ++++++++++++--------------------------------------- README.md | 158 ++++++++++++------ 2 files changed, 216 insertions(+), 392 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d18a7b..b6e8111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,356 +1,116 @@ # @digitalcredentials/verifier-core CHANGELOG -## 2.0.0 - Month XX 2026 - -Verifier results now carry a per-suite `summary: SuiteSummary[]` rollup -alongside `results[]`, which lists every check that ran, was skipped, or was -not run after a fatal failure. +## 2.0.0 - Unreleased + +A complete rewrite relative to 1.0.0-beta.11. Verification now runs as suites of +named checks, every check that ran is reported, and the result carries an +overall `verified` verdict. `verifyCredential` and `verifyPresentation` keep +their names, but their options and results have changed: see +[Migration from earlier 1.0.0-beta.x](README.md#migration-from-earlier-100-betax) +for a field-by-field map. + +### Breaking + +- **New result shapes.** `verifyCredential` returns a + `CredentialVerificationResult` (`verified`, `verifiableCredential`, `results`, + `summary`) in place of `{ credential, errors, log, additionalInformation }`. + `verifyPresentation` returns a `PresentationVerificationResult` (`verified`, + `verifiablePresentation`, `presentationResults`, `credentialResults`, + `summary`) in place of `{ presentationResult, credentialResults, errors }`. + Failures are `CheckResult` rows carrying `ProblemDetail`s; nothing is reported + through a top-level `errors` array. +- **`verified` is a verdict.** 1.x reported each step and left the conclusion to + the caller. In 2.0 a credential that is expired, not yet valid, revoked or + suspended has `verified: false`, alongside a bad signature. An issuer that is + not in any registry is a warning: `verified` stays `true`. +- **`knownDIDRegistries` is replaced by + `registries: EntityIdentityRegistry[]`**, and `reloadIssuerRegistry` is + removed. The DCC known-registries list can be passed as `registries` + unchanged. Registry matches move from `log[registered_issuer].matchingIssuers` + to the `trust.registry.issuer` result's `outcome.payload.matches`. +- **`challenge` defaults to `null`** (was `'meaningless'`). A supplied challenge + is always enforced, whatever proof purpose the presentation claims. +- **`unsignedPresentation: true` only accepts a presentation that has no + proof.** A proof that is present is always verified. +- **No JSON Schema validation by default.** 1.x validated every + `credentialSchema` a credential listed, and Open Badges 3.0 credentials, and + reported it in `additionalInformation`. 2.0 runs no JSON Schema check unless + you add `openBadgesSchemaSuite` or `openBadgesSuite` from + `@digitalcredentials/verifier-core/openbadges`, and those apply only to + `OpenBadgeCredential`. +- **Node.js 24 or later** (`engines.node` was `>=18`), because the dependencies + require it. ### Added -- Default verification support for the `ecdsa-rdfc-2019` Data Integrity - cryptosuite (Multikey; P-256 `zDna…` and P-384 `z82L…` did:key/did:web - verification methods), wired via `@interop/ecdsa-signature` + - `@interop/ecdsa-multikey`. `defaultCryptoSuites()` now registers three suites: - Ed25519Signature2020, EdDSA RDFC-2022, and ECDSA RDFC-2019. -- `SuiteSummary` type and `summary: SuiteSummary[]` field on - `CredentialVerificationResult` and `PresentationVerificationResult`. A - summary has no `verified` field: the only verdict is the result-level - `verified` ("no fatal check failed"). `status` comes from the suite's - counts, so a non-fatal failure shows as `status: 'failure'` or `'mixed'` - inside a result that is still `verified: true`. Each suite whose check - failed fatally carries `fatalFailureAt` — more than one can, since some - fatal checks do not halt — and a result is unverified exactly when any of - its summaries has it; UIs should colour those suites as errors and other - failing suites as warnings. -- Required `id: string` field on `CheckResult` — dot-separated - `..` namespace, set by the suite runner on every - row it emits (including not-run rows and synthetic `.applies` - rows), and by the parse-failure path on its one row. A suite with no - `phase` produces ids beginning `unknown.`. Check ids and the built-in - suites' phase tags are fixed for 2.x; since an id embeds its suite's - phase, changing either is a breaking change. -- 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, and a UI rendering recipe. -- `timing?: boolean` flag on `VerifierConfig`, `VerifyCredentialCall`, and - `VerifyPresentationCall`. When true, every `CheckResult`, every - `SuiteSummary`, and every top-level - `Credential|PresentationVerificationResult` carries a `timing: TaskTiming` - field describing wall-clock start/end and monotonic duration. Per-call wins - over the instance default; propagates from `verifyPresentation` into embedded - `verifyCredential` calls. See `docs/api/timing.md`. -- `TaskTiming` interface (`startedAt`, `endedAt`, `durationMs`, optional - recursive `events`). The reserved `events` field is forward-compatible with - future sub-event capture from inside a single check. -- `TimeService` interface plus `RealTimeService` and `FakeTimeService` - factories. New optional `timeService` on `VerifierConfig` (defaults to - `RealTimeService()`). Available on `VerificationContext.timeService` to any - check that needs to ask "what time is it?": `proof.validity-period` judges - credential validity dates against it and `status.bitstring` judges status-list - freshness against it, so a verifier built on a `FakeTimeService` decides both - at the pinned moment. Signature clock-skew and key rotation are still ahead. -- `CREDENTIAL_EXPIRED` and `CREDENTIAL_NOT_YET_VALID` on `ProblemTypes`, under - the same URI namespace as the other synthesized types. -- `proof.validity-period` check (`cryptographic.proof.validity-period`), run - after `proof.signature` for credentials. It judges `validFrom` / `validUntil` - (v2) or `issuanceDate` / `expirationDate` (v1) against the verification - context's `TimeService`, and reports `CREDENTIAL_EXPIRED` / - `CREDENTIAL_NOT_YET_VALID`. -- Optional `halts?: boolean` on `VerificationCheck` and `CheckResult`. It - defaults to `fatal`; a fatal check with `halts: false` still makes `verified` - false, but the checks after it still run. `proof.validity-period` and - `status.bitstring` set it. -- `VERIFICATION_METHOD_ERROR` on `ProblemTypes`, under the same URI namespace. - Reported by `proof.signature` when the proof's verification method cannot be - found, its controller has not authorized it for the proof purpose, or the - credential's issuer is not that controller. -- `PROOF_UNSUPPORTED` on `ProblemTypes`, under the same URI namespace: a proof - this verifier did not check, because no registered suite supports its - cryptosuite or it was made for a different proof purpose. -- `proof.unsupported-proofs` check (`cryptographic.proof.unsupported-proofs`), - non-fatal, run after `proof.signature` for credentials and presentations. It - fails with one `PROOF_UNSUPPORTED` problem per proof in a proof set that could - not be checked, as a warning beside the proofs that verified; otherwise it - succeeds ("Every proof was checked."). Every credential and presentation - result now has this one more row. -- Optional `now?: Date` on `CryptoVerifyOptions`, filled by the calling check - from the verification context's `TimeService` and passed through to - `@digitalcredentials/vc` by `DataIntegrityCryptoService`. Custom - `CryptoService` implementations may ignore it; omitting it means "now" as the - adapter's own library sees it. -- `RegistryCheckPayload` on the `registry.issuer` outcome, on success and on - failure alike: `matches`, one per registry that listed the issuer, each - `{ registry: { name, type, url? }, entity? }`, and `uncheckedRegistries`, - each `{ name, type, url? }`. The registry's `url` is where it is read from — - the list URL for `dcc-legacy` and `vc-recognition`, the trust anchor's entity - configuration for `oidf`. The lookup stops at the first registry that - matches, so `matches` normally holds at most one entry; only a lookup run - with `exhaustive` returns more. The outcome's `message` is unchanged, and is - display text only — read the payload rather than parsing it. -- `RegistryEntity` on a match: the registry's own record of the issuer - (`name`, `url`, `logo`, and the untouched registry entry as `raw`). Because - it comes from the registry rather than from the credential, it is the display - name to prefer. Every registry handler fills it in. For `oidf` it is the - `metadata` of the trust anchor's subordinate statement — `name` from - `federation_entity.organization_name`, `url` from `homepage_uri`, `logo` from - `logo_uri`, and the whole verified `metadata` as `raw` — read only from a - statement that has just verified, on a cache hit as well as on a fresh fetch. - An OIDF `logo_uri` may be a `data:` URI holding the image itself rather than - a link to it, so `entity.logo` can be large, and it is cached along with the - rest of the lookup. For `vc-recognition` it is the recognised - `credentialSubject` entry — `name`, `url` and `logo` from the entry's members - of those names, each taken only where the entry's value is a string, and the - whole entry as `raw` — read only from a recognition credential that has - verified, on a cache hit as well as on a fresh fetch. -- `payload?: unknown` on `failure` outcomes of `CheckOutcome`. Additive — - `success` already allowed one. -- `RegistryCheckPayload`, `RegistryMatch`, `RegistryReference`, - `RegistryEntity` and `HandlerResult` are exported from the package entry, so - a custom handler passed via `registryHandlers` can return an `entity` on its - `found` result and have the lookup pass it through. -- `domain` on `verifyPresentation`, checked alongside `challenge`. -- `BuiltinHttpGetService({ timeoutMs, maxBytes })`: the deadline (default 10 s) - and body cap (default 5 MB) of the built-in HTTP service. - -### Changed - -- **`results[]` shape**: every check that ran, was skipped (with its reason), - or was not run after a fatal failure (reason starting `Not run: `). A check - that passed and a check that never ran are now distinguishable. -- `flattenPresentationResults` semantically unchanged. -- **Status list credential proofs** are verified through the `cryptoServices` - configured on `createVerifier`, not through a second, non-injectable suite - list. Consumers who inject custom `CryptoService`s now get them applied to - BitstringStatusListCredentials fetched during a status check as well as to - presentation and credential proofs. -- **Tooling / packaging** (infrastructure aligned with - `isomorphic-lib-template`, no library behavior change): build is a single-pass - `tsc` under `moduleResolution: Bundler`; tests run on **vitest** (Node) + - **playwright** (browser, replacing karma); lint/format on eslint flat config + - prettier 3; package manager is **pnpm**. `engines.node` raised to `>=24`. - `exports` now declare `react-native` / `import` conditions for `.` and - `./openbadges`, and the package is marked `sideEffects: false`. -- **`RegistryLookupResult` carries `matches` and registry references in place - of its lists of names.** `matchingRegistries: string[]` is replaced by - `matches: RegistryMatch[]`, and `uncheckedRegistries: string[]` by - `uncheckedRegistries: RegistryReference[]`. A lookup result cached in the old - shape — one with no `matches` array — is treated as a cache miss, not an - error: the issuer is looked up again and the new shape is cached over it. No - cache needs clearing on upgrade. -- `HttpGetResult.body` from the built-in HTTP service is parsed JSON whenever - the body is valid JSON, whatever its content type. Previously only JSON - content types were parsed, so a `text/plain` status list or DID document - arrived as a string. - -### Fixed - -- **A proof set no longer passes when only one of its proofs verifies** - ([#67](https://github.com/digitalcredentials/verifier-core/issues/67)). A - credential, presentation or status list credential carrying several proofs - reported `proof.signature` success, and `verified: true`, when any one proof - verified, because the signature libraries accept a set on its first good - proof. Each proof is now verified on its own, and `proof.signature` fails when - any proof a registered suite can check fails, naming it: "Proof 2 of 2 - (DataIntegrityProof, eddsa-rdfc-2022): …". A status list with one broken proof - is a bad list signature (`STATUS_LIST_SIGNATURE_ERROR`), and an expired - credential with one broken proof reports the broken proof rather than reading - as authentic and merely expired. A proof in a cryptosuite this verifier does - not support, or made for another proof purpose, is no longer silently ignored: - beside a proof that verified it is a warning on the new - `proof.unsupported-proofs` check; when no proof could be checked, - `proof.signature` fails with `PROOF_UNSUPPORTED` in place of - `INVALID_SIGNATURE`. -- **A did:web issuer whose DID document cannot be fetched is no longer reported - as a bad signature** - ([#65](https://github.com/digitalcredentials/verifier-core/issues/65)). A DID - document answered with a 404 or another non-2xx status, or one whose host - could not be reached, made `proof.signature` fail with `INVALID_SIGNATURE`, - exactly what a tampered credential reports. It now fails with - `DID_WEB_UNRESOLVED`, as the error taxonomy already said, or with - `HTTP_ERROR` when the key that could not be fetched is not the issuer's. Still - fatal. -- **A key or controller mismatch is no longer reported as `INVALID_SIGNATURE` - with the detail "Verification error(s).", exactly what a tampered credential - reported.** A verification method that is missing, unauthorized by its - controller for the proof purpose, or controlled by someone other than the - issuer now fails with `VERIFICATION_METHOD_ERROR` ("Verification Method - Error"): the issuer's key setup is wrong, which is not the same accusation as - "the content was altered". Still fatal. -- **A failed signature check now reports the signature library's own message.** - Every failure took its `detail` from the `VerificationError` wrapper - ("Verification error(s).") rather than from the errors it wraps; a tampered - credential now reads "Invalid signature.". -- **An expired or not-yet-valid credential is no longer reported as - `INVALID_SIGNATURE` / "Invalid Signature", the same verdict as a forgery.** - `@digitalcredentials/vc` reads validity dates only after the proof verifies, - so a date failure is positive evidence the credential is authentic. - `proof.signature` now reads a date-only rejection as a verified signature, and - the new `proof.validity-period` check reports `CREDENTIAL_EXPIRED` - ("Credential Expired") when `validUntil` / `expirationDate` has passed, and - `CREDENTIAL_NOT_YET_VALID` ("Credential Not Yet Valid") when `validFrom` / - `issuanceDate` is still in the future. Both are fatal, so `verified` is - `false`, but neither halts: an expired credential still reports whether it - was revoked and whether its issuer is registered. The crypto service - recognises a date failure only from `@digitalcredentials/vc`'s own - unwrapped error and exact wording, so a tampered credential that is also - expired is still reported as a bad signature. -- **A revoked or suspended credential no longer hides its issuer.** - `status.bitstring` stays fatal but no longer halts, so `status.list-issuer`, - `trust.registry.issuer` and any caller-added suites still run after a - revocation, a suspension, or a status list that could not be trusted. - `status.list-issuer` compares only lists whose proof verified, and now - reports `STATUS_LIST_ISSUER_MISMATCH` for a list that marks the credential - revoked, too: previously a revocation read from a list signed by someone - else was reported as a plain revocation. -- **Valid credentials no longer fail as `INVALID_SIGNATURE` because the verifier - rewrote them.** `verifyCredential` / `verifyPresentation` previously ran the - suites against the Zod parse output rather than the document the issuer - signed. Zod's `.passthrough()` does not extend into nested object schemas, so - `IssuerObjectSchema` deleted every key on `issuer.image` except `id` and - `type` — including `caption`, which Open Badges 3.0 §B.1.13 defines and real - issuers populate. Dropping a signed key changes the canonicalized N-Quads and - the proof check then fails. The parse result is now discarded and the caller's - original object is verified and returned as `result.verifiableCredential`, - which also fixes consumers that re-verify that field in a second pass. -- `issuer.image.type` accepts an array (`['Image']`) as well as a string. The - previous string-only union failed the entire credential parse. -- Verifiable presentation `holder` now accepts an object with an `id` as well as - a URL string, per VCDM 2.0 (supersedes upstream #22). -- **Security: `unsignedPresentation: true` no longer skips a presentation proof - that is present.** The flag was forwarded to `@digitalcredentials/vc`, which - reads it as "skip the presentation proof", so a VP with a tampered holder - signature verified whenever the caller set it. A proof that is present is now - always verified. The flag only accepts a VP that has no proof, and that VP's - `proof.signature` is `skipped` rather than failing with "No Applicable Crypto - Service". Without the flag, a VP with no proof fails `proof.signature` as - `Presentation Not Signed`. `CryptoVerifyOptions` no longer carries - `unsignedPresentation`, because a `CryptoService` is only asked to verify - proofs that exist. -- **Security: a supplied `challenge` is now always enforced.** Previously a VP - whose proof claimed `assertionMethod` was verified without checking it, so a - replayed presentation verified. With no challenge supplied, an - `authentication` VP now verifies on its signature instead of failing against - the placeholder challenge `meaningless`. The VP proof check no longer - re-verifies embedded credentials. -- **Security: the built-in HTTP service no longer fetches whatever a credential - names.** It refuses non-https URLs, `localhost` and loopback, private, - link-local and unspecified IP literals, on the first URL and on every redirect - hop (at most five). Each request has a 10 s deadline and a 5 MB body cap. The - body is read once, so a malformed JSON response no longer fails with - `Body is unusable`, and errors name only the URL and the reason: response text - no longer reaches problem details. This covers the built-in service only; an - injected `httpGetService` or `documentLoader` keeps its own policy. -- **Security: OIDF registry membership is now verified.** Previously both entity - statements were payload-decoded without checking their signatures, and any - statement with a `metadata` object counted as registered. The trust anchor's - entity configuration and the issuer's subordinate statement are now - signature-verified (ES256 or EdDSA), with `iss`, `sub` and `exp` checked; a - statement that does not verify reports the registry as unchecked. OIDF lookups - now work in browsers and React Native (no `Buffer`). A lookup that included an - unreachable registry is cached for 60 s instead of 1 h, and a recognition - credential for at most 1 h instead of until its `validUntil`. `registries: []` - skips the registry check instead of reporting the issuer as not registered. - When every registry is unchecked, the result says registration could not be - determined (`REGISTRY_UNCHECKED` only). dcc-legacy lookups ignore prototype - keys such as `__proto__`, and registry cache keys can no longer collide. -- **The OBv3 schema check no longer fetches and compiles its schema on every - verification.** The four published OB 3.0 schemas are now fetched once per - `cacheService` (cached for 24 h under `schema:`) and compiled once per - verifier, so verifiers that share a `cacheService` share the fetch. Only a - successfully fetched JSON object is cached. Any other `credentialSchema` URL - is still fetched and compiled on every verification, so an issuer cannot grow - the cache. The docs previously claimed the schema was cached after first use; - it was not. -- **Security: a status list signed by someone other than the credential's issuer - is now reported.** Previously a validly signed status list from any DID - decided revocation without any signal, so whoever controlled the list's host - could un-revoke a credential by serving a list signed with their own key. A - new non-fatal check, `status.list-issuer`, runs after `status.bitstring` and - reports `STATUS_LIST_ISSUER_MISMATCH` when a list's `issuer` id differs from - the credential's. It is a warning, so `verified` is unchanged: some status - services sign every list with a DID of their own rather than the issuer's. - Consumers that need the binding enforced should treat - `STATUS_LIST_ISSUER_MISMATCH` as decisive. Each status list is still fetched - once per verification. The crypto service no longer passes - `verifyMatchingIssuers` to `@digitalcredentials/vc`, which never read it. The - status check also now checks every `BitstringStatusListEntry` for `revocation` - or `suspension`, not only the first `credentialStatus` entry, and ignores - other entries (legacy types, unknown types, other purposes) without fetching - their lists; a set `refresh` or `message` bit no longer reads as revoked. Each - set bit is its own problem, `CREDENTIAL_REVOKED` or `CREDENTIAL_SUSPENDED`, - which replace `CREDENTIAL_REVOKED_OR_SUSPENDED`. An expired or not-yet-valid - status list is reported as `STATUS_LIST_EXPIRED` or - `STATUS_LIST_NOT_YET_VALID` instead of as a bad signature, and a list that is - not a `BitstringStatusListCredential` as `STATUS_LIST_TYPE_ERROR`; neither - classification could fire before. -- **"Issuer not registered" no longer claims every registry was checked.** When - an issuer was not found but some registries could not be reached (for example, - a registry that sends no CORS headers, read from a browser), - `ISSUER_NOT_REGISTERED` said the issuer "was not found in any known DID - registry". It now says how many registries answered — "was not found in the 5 - registries that could be checked; 4 could not be checked." The - `REGISTRY_UNCHECKED` problem that names the unreachable registries, and the - success message, are unchanged. +- **`createVerifier(config)`** builds a reusable `Verifier`. Reuse one for + repeated verification so DID documents, status lists, contexts and registry + lookups are cached across calls. `verifyCredential` / `verifyPresentation` are + one-shot wrappers around it. +- **Suites and checks.** The default suites — `coreSuite`, `recognitionSuite`, + `proofSuite`, `statusSuite`, `registrySuite` — are exported, and callers can + add their own `VerificationSuite`s through `additionalSuites`, or run a subset + with `phases`. Every result row has a stable id, `..` + (for example `cryptographic.proof.signature`); ids and phases are fixed for + 2.x. A fatal check makes `verified` false; most fatal checks also stop later + checks, but validity dates and status do not, so an expired credential still + reports revocation and its issuer. +- **Per-suite `summary: SuiteSummary[]`** beside `results[]`, which lists every + check that ran, was skipped (with its reason) or was not run. Helpers: + `flattenPresentationResults`, `foldCheckResults`, `computeId`. See + [`docs/api/verification-results.md`](docs/api/verification-results.md). +- **`ProblemTypes`**: problem URIs for every failure the verifier reports, among + them `CREDENTIAL_EXPIRED` and `CREDENTIAL_NOT_YET_VALID` (distinct from + `INVALID_SIGNATURE`), `VERIFICATION_METHOD_ERROR`, `DID_WEB_UNRESOLVED`, + `PROOF_UNSUPPORTED`, `CREDENTIAL_REVOKED` / `CREDENTIAL_SUSPENDED`, + `STATUS_LIST_ISSUER_MISMATCH`, `ISSUER_NOT_REGISTERED` and + `REGISTRY_UNCHECKED`. +- **Proofs.** `ecdsa-rdfc-2019` (P-256 and P-384 Multikey) alongside + Ed25519Signature2020 and `eddsa-rdfc-2022`. Each proof in a proof set is + verified on its own and all must pass; a proof in an unsupported cryptosuite + is reported by `proof.unsupported-proofs` as a warning. `domain` on + `verifyPresentation`, checked alongside `challenge`. +- **Status.** Every revocation and suspension `BitstringStatusListEntry` is + checked, the status list's proof is verified with the configured crypto + services, and the non-fatal `status.list-issuer` check reports a list signed + by someone other than the credential's issuer. +- **Registries.** `oidf` (OpenID Federation, with signature-verified entity + statements), `dcc-legacy` and `vc-recognition` registries. The + `trust.registry.issuer` result carries a `RegistryCheckPayload`: `matches`, + each with the registry's own record of the issuer as `entity` (`name`, `url`, + `logo`, `raw`), and the `uncheckedRegistries` that could not be reached. + Custom lookups through `registryHandlers`. +- **Pluggable services**, each with a default: `CacheService` + (`InMemoryCacheService`), `HttpGetService` (`BuiltinHttpGetService`, which + fetches https only, refuses local and private addresses, and has a 10 s + deadline and 5 MB body cap — see + [Network policy](README.md#network-policy-of-the-built-in-http-service)), + `CryptoService` (`DataIntegrityCryptoService`) and `TimeService` + (`RealTimeService`, `FakeTimeService`), which decides "now" for validity dates + and status-list freshness. +- **`timing`** option: attaches a `TaskTiming` to every check, summary and + result. See [`docs/api/timing.md`](docs/api/timing.md). +- **Open Badges 3.0** in the opt-in submodule + `@digitalcredentials/verifier-core/openbadges`: `openBadgesSemanticSuite` + (Zod-based checks, no network), `openBadgesSchemaSuite` (JSON Schema, with the + published schemas cached), `openBadgesSuite` (both), the `obv3p0Recognizer` / + `obv3p0EndorsementRecognizer` recognizers, the Zod schemas and + `OpenBadgesProblemTypes`. +- **Credential recognition**: `recognizers` parse a credential into a known + profile and report it as `recognizedProfile` and + `normalizedVerifiableCredential`. +- `parseCredential` / `parsePresentation` and the `VerifiableCredential` / + `VerifiablePresentation` types, and TypeScript types for the whole API + exported from the package entry. +- Package `exports` add the `./openbadges` subpath and `react-native` / `import` + conditions, and the package is marked `sideEffects: false`. ### Removed -- `CheckResult.check` — the namespaced `CheckResult.id` is the identifier. - `CheckResult.suite` stays: it is how rows are grouped, it matches - `SuiteSummary.suite`, and it cannot be recovered from `id`, because a suite - id may itself contain dots. -- `cryptoSuites` and `verifyBitstringStatusListCredential` on - `VerificationContext`. Both were `@internal` and neither was reachable through - `VerifierConfig`, so only code constructing a `VerificationContext` directly - (test helpers) is affected. Tests that used - `verifyBitstringStatusListCredential: false` should inject a permissive - `CryptoService` instead. - -### Migration - -- **Code that read `CheckResult.check`** reads `CheckResult.id` instead. The id - is namespaced — `..` — so a match on the old local - id such as `'proof.signature'` becomes a match on - `'cryptographic.proof.signature'`. -- To adopt the new shape: read `result.summary[]` for the per-suite rollup; read - `result.results[]` for per-check detail; use - `r.id.startsWith(summary.id + '.')` to find the detail rows under a summary - entry, narrowing by `r.outcome.status` when you want only the failures. -- **Registry matches.** 1.x returned the registry's record of the issuer at the - top level of the verification result, as `matchingIssuers[]`, each - `{ issuer: { federation_entity: { organization_name, homepage_uri, logo_uri?, - … } }, registry: { name, type, url } }`. 2.0 returns the same information as - the `payload` of the `registry.issuer` check's outcome, a - `RegistryCheckPayload` whose `matches[]` entries are - `{ registry: { name, type, url? }, entity? }`. What 1.x called `issuer` - (its `federation_entity`) is now `entity`: - - | 1.x | 2.0 | - |-----|-----| - | `matchingIssuers[]` | `payload.matches[]` | - | `…issuer.federation_entity.organization_name` | `…entity.name` | - | `…issuer.federation_entity.homepage_uri` | `…entity.url` | - | `…issuer.federation_entity.logo_uri` | `…entity.logo` | - | `…registry` | `…registry` (`name`, `type`, `url`) | - - The whole registry entry is also available, unmodified, as `entity.raw`: - for `oidf` that is the verified subordinate statement's whole `metadata`, - which is where 1.x read `issuer` from, and for `vc-recognition` it is the - recognised `credentialSubject` entry. Every registry type populates - `entity`. The payload is on the - `registry.issuer` row in `results[]` whether the check passed or failed. -- **Reading registry names out of the message.** Code that parsed the - `registry.issuer` message — "Issuer found in registry: A. 1 registries could - not be checked: B" — should read `payload.matches[].registry.name` and - `payload.uncheckedRegistries[].name` instead. The message is unchanged, but - it joins names with `, `, so a registry whose own name contains `, ` cannot - be recovered from it. -- **Custom `LookupIssuers` or `registryHandlers`.** A custom `LookupIssuers` - must now return `matches: RegistryMatch[]` rather than - `matchingRegistries: string[]`, and `uncheckedRegistries` as references - rather than names. A custom handler's `found` result is unchanged unless it - wants to supply an `entity`, which is optional. +- The `knownDIDRegistries` and `reloadIssuerRegistry` options. +- The 1.x result fields `credential`, `errors`, `log` and + `additionalInformation`, and `presentationResult` on presentations. +- The `@digitalcredentials/issuer-registry-client` dependency. ## 1.0.0-beta.11 - December 15 2025 diff --git a/README.md b/README.md index 1de477b..b7d33e7 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,10 @@ A custom handler passed via `registryHandlers` can supply an `entity` on its ### verifyCredential ```typescript -import { verifyCredential } from '@digitalcredentials/verifier-core'; +import { + verifyCredential, + type RegistryCheckPayload +} from '@digitalcredentials/verifier-core'; const result = await verifyCredential({ credential, // The VC to verify (any version, passed as unknown) @@ -1004,7 +1007,7 @@ part of the default suite list; consumers that want OB checks pass `openBadgesSuite` (or one of the bundled variants) via `additionalSuites` on a verify call. -> [!IMPORTANT] If you were on `1.0.0-beta.x` and relied on `obv3SchemaSuite` +> [!IMPORTANT] If you were on `1.0.0-beta.x` and relied on its schema check > running by default, you now need to opt in explicitly. The simplest migration > is to add `openBadgesSuite` to your verify call. @@ -1234,51 +1237,112 @@ system, dependencies, and architectural direction — see ## Migration from earlier 1.0.0-beta.x -This release tightens the public API surface. The following changes may require -small migrations: - -- **Demoted from `index.ts`** — these symbols remain reachable via their module - paths (`@digitalcredentials/verifier-core/dist/...`) but are no longer part of - the published 1.0 surface: `runSuites`, `createRegistryLookup`, - `DEFAULT_TTL_MS`, `parseCacheControlMaxAge`, `resolveTtl`, - `ttlFromValidUntil`, `documentLoaderFromHttpGet`, `fetchJsonFromHttpGet`, - `extractCredentialsFrom`, `registryKeyHash`. Most callers should build - verifiers via `createVerifier(...)` rather than reach for these directly. - -- **Result-shape changes:** - - `CredentialVerificationResult.credential` → `verifiableCredential` - (`s/\.credential\b/.verifiableCredential/` on accessor sites). - - `PresentationVerificationResult.allResults` removed — replace with - `flattenPresentationResults(result)`. - - `PresentationVerificationResult.verifiablePresentation` added — new field - carrying the VP you passed in, unchanged. - -- **Default cache isolation:** two `createVerifier()` calls without an explicit - `cacheService` no longer share a process-wide cache. To preserve the previous - "shared" behavior, construct one `InMemoryCacheService` and pass it to each - verifier explicitly. - -- **Legacy result types removed:** `VerificationResponse`, - `PresentationVerificationResponse`, and friends were already removed from - `index.ts` in `1.0.0-beta.11`; the type definitions are now gone too. Anyone - who needed the old shape can pin `1.0.0-beta.11` or earlier. - -- **`ProblemTypes` const map added:** built-in problem URIs are now importable - as `ProblemTypes.INVALID_SIGNATURE` etc. Existing literal-string comparisons - against `ProblemDetail.type` continue to work unchanged. - -- **OBv3 verification is opt-in** — the OBv3 schema suite no longer runs by - default. Add `openBadgesSuite` (or one of its variants) via `additionalSuites` - on the verify call to restore previous behavior. See the - [Open Badges 3.0 verification](#open-badges-30-verification-opt-in-submodule) - section for details. - -- **OBv3 problem-type rename** — `OBV3_INVALID_RESULT_REFERENCE` (and other OB - problems mirrored on `ProblemTypes`) moved out of the core catalog into - `OpenBadgesProblemTypes` in the `/openbadges` submodule, and the wire URIs - shifted from `…#OBV3_*` to `…#OB_*`. Callers comparing literal strings against - `ProblemDetail.type` need to update the affected literals; callers using the - constants should switch to the new module. +2.0.0 is a rewrite. This section maps the last 1.x release, 1.0.0-beta.11, to +2.0.0. `verifyCredential` and `verifyPresentation` keep their names; their +options and results change. Check ids below are the `CheckResult.id` values (see +[verification results](docs/api/verification-results.md)). + +### Options + +| 1.0.0-beta.11 | 2.0.0 | +| ------------------------------------- | --------------------------------------------------------------------------------------- | +| `knownDIDRegistries` | `registries: EntityIdentityRegistry[]` — the DCC known-registries list works unchanged | +| `reloadIssuerRegistry` | Removed. Lookups are cached in the `cacheService`; reuse a `createVerifier` instance | +| `challenge` (default `'meaningless'`) | `challenge` (default `null`). A supplied challenge is always enforced | +| `unsignedPresentation` | Same name; it now only accepts a presentation with no proof | +| — | New: `domain`, `additionalSuites`, `phases`, `recognizers`, `timing`, service overrides | + +### Credential result + +| 1.0.0-beta.11 (`VerificationResponse`) | 2.0.0 (`CredentialVerificationResult`) | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| — | `verified`: `false` when any fatal check failed (bad signature, expired, revoked, …) | +| `credential` | `verifiableCredential` — the object you passed in, unchanged | +| `errors[]` (fatal: unparseable, bad signature) | `results[]` rows with `fatal: true` and `outcome.status: 'failure'`; details in `outcome.problems` | +| `log[valid_signature]` | `cryptographic.proof.signature` | +| `log[expiration]` | `cryptographic.proof.validity-period` (`CREDENTIAL_EXPIRED`, `CREDENTIAL_NOT_YET_VALID`) | +| `log[revocation_status]` | `cryptographic.status.bitstring` (`CREDENTIAL_REVOKED`, `CREDENTIAL_SUSPENDED`) | +| `log[registered_issuer].valid` | `trust.registry.issuer` `outcome.status` (`success` / `failure`; non-fatal) | +| `log[registered_issuer].matchingIssuers[]` | `trust.registry.issuer` `outcome.payload.matches[]`; `.issuer` → `.entity.raw`, with `.entity.name` / `.url` / `.logo` | +| `log[registered_issuer].uncheckedRegistries[]` | `trust.registry.issuer` `outcome.payload.uncheckedRegistries[]` | +| `additionalInformation[schema_check]` | No default equivalent. The opt-in Open Badges schema suite reports a `schema.obv3.json` row | +| — | `summary[]`, one rollup per suite | + +A `log` step's `valid: true` becomes `outcome.status: 'success'`; `valid: false` +becomes `'failure'` with `outcome.problems`. A check that did not apply is +`'skipped'` with a `reason`. + +### Presentation result + +| 1.0.0-beta.11 (`PresentationVerificationResponse`) | 2.0.0 (`PresentationVerificationResult`) | +| -------------------------------------------------- | --------------------------------------------------------------------------- | +| — | `verified`: the presentation and every credential in it passed | +| — | `verifiablePresentation` — the object you passed in, unchanged | +| `presentationResult.signature: 'valid'` | `presentationResults` row `cryptographic.proof.signature` with `'success'` | +| `presentationResult.signature: 'invalid'` | that row with `'failure'` | +| `presentationResult.signature: 'unsigned'` | that row with `'skipped'`, its `reason` saying the presentation is unsigned | +| `credentialResults[]` | `credentialResults[]`, each a `CredentialVerificationResult` | +| `errors[]` | `verified: false`, with the failing rows in `presentationResults` | + +`flattenPresentationResults(result)` lists every row of a presentation and its +credentials in one array. + +### Before and after + +1.0.0-beta.11: + +```javascript +const result = await verifyCredential({ credential, knownDIDRegistries }); +if (result.errors) { + console.log('fatal:', result.errors[0].message); +} else { + const step = id => result.log.find(s => s.id === id); + console.log('signature ok:', step('valid_signature').valid); + console.log('not expired:', step('expiration').valid); + const match = step('registered_issuer').matchingIssuers[0]; + console.log('issuer:', match?.issuer.federation_entity?.organization_name); +} +``` + +2.0.0: + +```typescript +import { + verifyCredential, + type RegistryCheckPayload +} from '@digitalcredentials/verifier-core'; + +const result = await verifyCredential({ credential, registries }); +const row = (id: string) => result.results.find(r => r.id === id); +console.log('verified:', result.verified); +console.log( + 'signature ok:', + row('cryptographic.proof.signature')?.outcome.status === 'success' +); +console.log( + 'not expired:', + row('cryptographic.proof.validity-period')?.outcome.status === 'success' +); +const registry = row('trust.registry.issuer')?.outcome; +const payload = + registry && registry.status !== 'skipped' + ? (registry.payload as RegistryCheckPayload | undefined) + : undefined; +console.log('issuer:', payload?.matches[0]?.entity?.name); +for (const r of result.results) { + if (r.outcome.status === 'failure') + console.log(r.id, r.outcome.problems[0].title); +} +``` + +### Open Badges + +1.x checked Open Badges 3.0 credentials, and any listed `credentialSchema`, +against JSON Schema by default. 2.0 does not. Add `openBadgesSuite` from +`@digitalcredentials/verifier-core/openbadges` to `additionalSuites` to check +Open Badges credentials (see +[Open Badges 3.0 verification](#open-badges-30-verification-opt-in-submodule)). +JSON Schema validation of other credential types is not included. ## Install