@interop/verifier-core
v3.3.1
Published
For verifying Verifiable Credentials in the browser, Node.js, and React Native.
Readme
verifier-core (@interop/verifier-core)
Verifies W3C Verifiable Credentials in the browser, Node.js, and React Native.
Table of Contents
- Overview
- API
- Custom Suites
- Verbose mode and folded summaries
- Capturing timing data
- Pluggable clock (TimeService)
- Open Badges 3.0 verification (opt-in submodule)
- Credential recognition + two-pass verification
- Architecture
- Migration from earlier 1.0.0-beta.x
- Install
- Contribute
- License
Overview
Verifies the following versions of W3C Verifiable Credentials:
Supports both eddsa-rdfc-2022 Data Integrity Proof and ed25519-signature-2020 Linked Data Proof cryptosuites.
Verification runs an ordered pipeline of suites, each containing one or more checks:
| Suite | Phase | What it checks | Fatal? |
| --------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------- | ------ |
| Core | cryptographic | @context exists, VC context URI present, resolve issuers, credential ID valid, proof exists | Yes |
| Recognition | recognition | Pluggable credential-profile recognition; produces a normalized credential form (no-op when no recognizers configured) | No |
| Proof | cryptographic | Cryptographic signature verification | Yes |
| Status | cryptographic | Revocation/suspension via BitstringStatusList — sole owner of status verification | Yes |
| Registry | trust | Issuer DID lookup in known trust registries | No |
The Phase column drives the optional phases: filter on VerifierConfig
and per-call args, used for
two-pass verification.
Open Badges 3.0 verification lives in the opt-in submodule
@interop/verifier-core/openbadges
and is not part of the default suite list.
The result doesn't make a single "valid/invalid" judgment. It returns the
outcome of every check, letting consumers decide what matters for their use
case. A credential with a revoked status will fail (verified: false) — that's
an issuer-asserted state we can't ignore — but other distinctions, like an
unregistered issuer (the registry might just not be up to date), are surfaced as
non-fatal results for the consumer to weigh.
Trust Registries
Registry checks look up the credential's issuer DID in known registries. The DCC publishes a list of known registries:
https://digitalcredentials.github.io/dcc-known-registries/known-did-registries.jsonFetch and pass it to verification:
const response = await fetch(
'https://digitalcredentials.github.io/dcc-known-registries/known-did-registries.json'
);
const registries = await response.json();
const result = await verifyCredential({ credential, registries });[!CAUTION] The DCC registry list does not make claims about the registries it contains. It is a list of registries that the DCC knows about — it says nothing about the quality, meaning, or value of credentials issued by anyone in those registries.
API
verifyCredential
import { verifyCredential } from '@interop/verifier-core';
const result = await verifyCredential({
credential, // The VC to verify (any version, passed as unknown)
registries // Optional: issuer trust registries
});Options
interface VerifyCredentialOptions {
credential: unknown;
registries?: EntityIdentityRegistry[];
additionalSuites?: VerificationSuite[];
// Service overrides (otherwise sensible defaults are used):
httpGetService?: HttpGetService;
cacheService?: CacheService;
cryptoServices?: CryptoService[];
registryHandlers?: RegistryHandlerMap;
documentLoader?: DocumentLoader;
}Only credential is required. All other fields override sensible defaults
(security-document-loader, Ed25519 + EdDSA crypto suites, in-memory cache).
VerifyCredentialOptions is the type alias
VerifierConfig & VerifyCredentialCall, so callers building the options object
piece-by-piece can compose against either half.
Result
interface CredentialVerificationResult {
verified: boolean;
verifiableCredential: VerifiableCredential;
results: CheckResult[];
summary: SuiteSummary[];
}verified is true when no check returned a failure. By default (since v2.0.0)
results carries only failures and explicit <suite>.applies skips, while
summary provides the per-suite rollup (see
Verbose mode and folded summaries). Pass
verbose: true to receive every check that ran in results.
Each CheckResult contains a discriminated CheckOutcome:
type CheckOutcome =
| { status: 'success'; message: string }
| { status: 'failure'; problems: ProblemDetail[] }
| { status: 'skipped'; reason: string };Failures carry one or more ProblemDetail entries (inspired by
RFC 9457):
interface ProblemDetail {
type: string; // URI identifying the problem
title: string; // Short human-readable summary
detail: string; // Specific explanation of this occurrence
}Example: Successful verification
{
"verified": true,
"verifiableCredential": { "...parsed credential..." },
"results": [
{ "suite": "core", "check": "core.context-exists", "outcome": { "status": "success", "message": "Credential has a valid @context property." } },
{ "suite": "core", "check": "core.vc-context", "outcome": { "status": "success", "message": "..." } },
{ "suite": "core", "check": "core.credential-id", "outcome": { "status": "success", "message": "..." } },
{ "suite": "core", "check": "core.proof-exists", "outcome": { "status": "success", "message": "..." } },
{ "suite": "proof", "check": "proof.signature", "outcome": { "status": "success", "message": "Signature verified successfully." } },
{ "suite": "status", "check": "status.bitstring", "outcome": { "status": "success", "message": "Credential status is valid (not revoked or suspended)." } },
{ "suite": "registry", "check": "registry.issuer", "outcome": { "status": "success", "message": "Issuer found in registry: DCC Sandbox Registry" } }
]
}Example: Invalid signature (fatal)
An invalid signature is fatal — it means any part of the credential could have been tampered with, so subsequent checks within the proof suite stop. Other suites still run.
{
"verified": false,
"results": [
{
"suite": "core",
"check": "core.context-exists",
"outcome": { "status": "success", "message": "..." }
},
{
"suite": "core",
"check": "core.proof-exists",
"outcome": { "status": "success", "message": "..." }
},
{
"suite": "proof",
"check": "proof.signature",
"outcome": {
"status": "failure",
"problems": [
{
"type": "https://www.w3.org/TR/vc-data-model#INVALID_SIGNATURE",
"title": "Invalid Signature",
"detail": "The signature is not valid."
}
]
}
},
{
"suite": "status",
"check": "status.bitstring",
"outcome": { "status": "skipped", "reason": "..." }
}
]
}Example: Revoked credential (fatal, sourced from status)
When a credential's status list marks it revoked or suspended — or the verifier can't confidently evaluate the list (missing, expired, wrong type, signature invalid) — the status suite fails the credential. The proof check still passes on its own merits.
{
"verified": false,
"results": [
{
"suite": "core",
"check": "core.context-exists",
"outcome": { "status": "success", "message": "..." }
},
{
"suite": "proof",
"check": "proof.signature",
"outcome": {
"status": "success",
"message": "Signature verified successfully."
}
},
{
"suite": "status",
"check": "status.bitstring",
"outcome": {
"status": "failure",
"problems": [
{
"type": "https://www.w3.org/TR/vc-data-model#CREDENTIAL_REVOKED_OR_SUSPENDED",
"title": "Credential Revoked or Suspended",
"detail": "The credential has been revoked or suspended according to the status list."
}
]
}
}
]
}Example: Check skipped
Checks skip when they're irrelevant to the input. For instance, a credential
with no credentialStatus skips the status check:
{
"suite": "status",
"check": "status.bitstring",
"outcome": {
"status": "skipped",
"reason": "Credential has no credentialStatus."
}
}Error taxonomy
All failures use ProblemDetail with a type URI. Common error types:
| Type URI | Title | When |
| ------------------------------------- | ------------------------------------------------------------------ | -------------------------------------- |
| ...#PARSING_ERROR | Invalid JSON-LD / No VC Context / Invalid Credential ID / No Proof | Structural problems |
| ...#INVALID_SIGNATURE | Invalid Signature | Signature doesn't match content |
| ...#DID_WEB_UNRESOLVED | DID Web Unresolved | did:web document couldn't be fetched |
| ...#HTTP_ERROR | HTTP Error | Network error during signature check |
| ...#CREDENTIAL_REVOKED_OR_SUSPENDED | Credential Revoked or Suspended | Status list indicates revocation |
| ...#STATUS_LIST_NOT_FOUND | Status List Not Found | Status list URL unreachable |
| ...#STATUS_LIST_EXPIRED | Status List Expired | Status list VC has expired |
| ...#STATUS_LIST_SIGNATURE_ERROR | Status List Signature Error | Status list VC signature invalid |
| ...#ISSUER_NOT_REGISTERED | Issuer Not Registered | Issuer DID not in any registry |
| ...#REGISTRY_UNCHECKED | Registry Unchecked | Some registries couldn't be reached |
Problem types
Every built-in problem URI is also exported as a constant. Branch on the const map for type-safe checks:
import {
ProblemTypes,
type ProblemType
} from '@interop/verifier-core';
switch (problem.type as ProblemType) {
case ProblemTypes.INVALID_SIGNATURE:
// ...
break;
case ProblemTypes.CREDENTIAL_REVOKED_OR_SUSPENDED:
// ...
break;
case ProblemTypes.STATUS_LIST_NOT_FOUND:
// ...
break;
// ...
}ProblemDetail.type stays typed as string so custom suites can emit their own
problem URIs; the cast above is opt-in for callers that only need to branch on
built-in types. See the per-token JSDoc on ProblemTypes for which entries are
W3C-spec error identifiers (currently only PARSING_ERROR) and which are
synthesized placeholders we use until a wider problem-type vocabulary is
published.
verifyPresentation
import { verifyPresentation } from '@interop/verifier-core';
const result = await verifyPresentation({
presentation, // The VP to verify
challenge: 'abc123', // Optional: expected challenge
unsignedPresentation: false, // Optional: allow unsigned VP
registries // Optional: issuer trust registries
});Options
interface VerifyPresentationOptions {
presentation: unknown;
challenge?: string | null;
unsignedPresentation?: boolean;
registries?: EntityIdentityRegistry[];
additionalSuites?: VerificationSuite[];
// Service overrides (otherwise sensible defaults are used):
httpGetService?: HttpGetService;
cacheService?: CacheService;
cryptoServices?: CryptoService[];
registryHandlers?: RegistryHandlerMap;
documentLoader?: DocumentLoader;
}Like VerifyCredentialOptions, this is the alias
VerifierConfig & VerifyPresentationCall.
Result
interface PresentationVerificationResult {
verified: boolean;
verifiablePresentation: VerifiablePresentation;
presentationResults: CheckResult[];
credentialResults: CredentialVerificationResult[];
summary: SuiteSummary[];
// No allResults — use flattenPresentationResults(result)
}presentationResults and each embedded credentialResults[i].results follow
the same folded-by-default shape as verifyCredential's results. The
top-level summary rolls up the VP envelope's own suites; per-credential
rollups live on credentialResults[i].summary.
Presentation verification does two things:
- Verifies the VP itself — checks the presentation's signature (or skips if
unsignedPresentation: true). Results go inpresentationResults. - Verifies each embedded credential — extracts credentials from the VP and
runs
verifyCredentialon each. Results go incredentialResults.
verified is true only if both the presentation and all embedded credentials
pass.
The parsed VP is returned as verifiablePresentation, mirroring the wire-level
property name so callers (and downstream systems whose templates reach into
result objects by property path) can reach it without carrying the original
input separately.
A VP needn't be signed — it can simply package credentials together. Set
unsignedPresentation: true to skip the VP signature check.
Flattening results
When you want a single iterable view of every check that ran across the
presentation and its embedded credentials, use flattenPresentationResults:
import { flattenPresentationResults } from '@interop/verifier-core';
const result = await verifyPresentation({ presentation });
for (const entry of flattenPresentationResults(result)) {
if (entry.source === 'presentation') {
// entry.result is a CheckResult from VP-level verification
} else {
// entry.source === 'credential'
// entry.credentialIndex is the index into result.credentialResults
// entry.result is a CheckResult from that embedded VC
}
}Each entry preserves provenance — you always know whether a check applied to the presentation itself or to a specific embedded credential, by index.
createVerifier (batch / repeated verification)
verifyCredential and verifyPresentation are convenient one-shot wrappers —
each call builds a fresh verifier internally. When you'll perform more than one
verification, construct a Verifier with createVerifier(...) and reuse it for
better performance. The instance owns long-lived dependencies (HTTP, cache,
crypto services, document loader, registries), so issuer DID documents, status
list credentials, and JSON-LD contexts are fetched once and reused across calls.
Each verifier owns its own InMemoryCacheService by default; cache contents are
isolated from other verifiers in the same process. To share cache state across
verifiers, construct one cache adapter and pass it to each
createVerifier({ cacheService }).
import { createVerifier } from '@interop/verifier-core';
const verifier = createVerifier({ registries });
for (const credential of batch) {
const result = await verifier.verifyCredential({ credential });
// ... handle result ...
}The same Verifier is also used recursively when verifying a presentation, so
all credentials embedded in a VP share the verifier's caches automatically:
const result = await verifier.verifyPresentation({ presentation });Custom Suites
Extend the default pipeline with custom verification logic:
import {
verifyCredential,
VerificationSuite
} from '@interop/verifier-core';
const myCustomSuite: VerificationSuite = {
id: 'custom.expiry-policy',
name: 'Expiry Policy',
checks: [
{
id: 'custom.expiry-policy.grace-period',
name: 'Grace Period Check',
fatal: false,
execute: async (subject, context) => {
const credential = subject.verifiableCredential as any;
// Custom logic: allow 30-day grace period after expiration
return { status: 'success', message: 'Within grace period.' };
}
}
]
};
const result = await verifyCredential({
credential,
additionalSuites: [myCustomSuite]
});
// result.results includes checks from both default and custom suitesCustom suites run after the default suites. Each check receives the same
VerificationSubject and VerificationContext as built-in checks.
Verbose mode and folded summaries
Each CredentialVerificationResult and PresentationVerificationResult carries
a summary: SuiteSummary[] rollup of per-suite outcomes. By default (since
v2.0.0), results[] carries only failures and explicit <suite>.applies skips;
pass verbose: true to keep every check in results[].
const result = await verifier.verifyCredential({ credential });
// result.summary[i] = { id, phase, suite, status, verified, message, counts, ... }
// result.results[] = failures + explicit skips only
const verbose = await verifier.verifyCredential({ credential, verbose: true });
// verbose.results[] carries every check that ran, with .id populated.verbose is also accepted on createVerifier(...) as an instance default;
per-call values win when both are set.
See 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).
Capturing timing data
Every result can carry per-check, per-suite, and per-call TaskTiming data
(wall-clock startedAt / endedAt plus a monotonic durationMs). Off by
default; opt in via timing: true on createVerifier(...) or per-call. Mirrors
verbose's plumbing — per-call wins, and verifyPresentation propagates the
flag into embedded verifyCredential calls so the top-level result.timing is
an inclusive wrapper.
const verifier = createVerifier({ timing: true });
const result = await verifier.verifyCredential({ credential });
console.log(result.timing!.durationMs);
console.log(result.summary[0].timing!.durationMs);In verbose: false mode (the default), individual CheckResult.timing entries
fold away in results[], but the per-suite SuiteSummary.timing survives in
summary[] so suite-grain timing is never lost. See
docs/api/timing.md for the full reference, granularity
table, recipes, and a prompt-ready appendix.
Pluggable clock (TimeService)
verifier-core reads wall-clock and monotonic time through a small
TimeService interface. The default is RealTimeService (Date.now /
performance.now); pass { timeService: FakeTimeService() } on
createVerifier(...) in tests to make every TaskTiming field
exact-value-assertable. Both factories are exported from the package barrel. The
same abstraction will back upcoming work on credential expiration, signature
clock-skew windows, key rotation, and status-list freshness — see
docs/api/timing.md for the reference.
Open Badges 3.0 verification (opt-in submodule)
Open Badges 3.0 verification ships in
@interop/verifier-core/openbadges as an opt-in submodule. It is not
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.xand relied onobv3SchemaSuiterunning by default, you now need to opt in explicitly. The simplest migration is to addopenBadgesSuiteto your verify call.
Enabling OB verification
import { createVerifier } from '@interop/verifier-core';
import { openBadgesSuite } from '@interop/verifier-core/openbadges';
const verifier = createVerifier();
const result = await verifier.verifyCredential({
credential,
additionalSuites: [openBadgesSuite]
});Bundle variants
| Bundle | Contents | Network? |
| ------------------------- | ------------------------------------------------------------------ | --------------------------------------------- |
| openBadgesSuite | Semantic checks and AJV JSON Schema check (the default bundle) | Yes (schema fetch on first use; cached after) |
| openBadgesSemanticSuite | Cross-field semantic checks only | No |
| openBadgesSchemaSuite | AJV JSON Schema check only | Yes |
Pick openBadgesSemanticSuite when you want the OB-specific semantic checks
(OB_INVALID_RESULT_REFERENCE, OB_INVALID_ACHIEVED_LEVEL,
OB_MISSING_RESULT_STATUS, OB_UNKNOWN_ACHIEVEMENT_TYPE) but cannot afford a
network fetch on the first OB credential of a process.
Problem types
OB-specific problem URIs live on OpenBadgesProblemTypes (also exported as
Obv3ProblemTypes for symmetry with internal naming):
import {
openBadgesSuite,
OpenBadgesProblemTypes,
type OpenBadgesProblemType
} from '@interop/verifier-core/openbadges';
// In a result-handling callback...
switch (problem.type as OpenBadgesProblemType) {
case OpenBadgesProblemTypes.OB_INVALID_RESULT_REFERENCE:
// ...
break;
case OpenBadgesProblemTypes.OB_INVALID_ACHIEVED_LEVEL:
// ...
break;
case OpenBadgesProblemTypes.OB_MISSING_RESULT_STATUS:
// ...
break;
case OpenBadgesProblemTypes.OB_UNKNOWN_ACHIEVEMENT_TYPE:
// ...
break;
}The wire URIs follow the …#OB_* shape (e.g.
https://www.w3.org/TR/vc-data-model#OB_INVALID_ACHIEVED_LEVEL). Callers
upgrading from 1.0.0-beta.x who literal-matched
OBV3_INVALID_RESULT_REFERENCE need to update those literals to
OB_INVALID_RESULT_REFERENCE (or — preferred — switch to the
OpenBadgesProblemTypes constants).
Caller-augmented AchievementType vocabulary
The default obv3UnknownAchievementTypeCheck validates against the OB 3.0
§B.1.1 enumeration plus the spec-sanctioned ext: prefix. Issuers that mint
additional vocabulary tokens (without an ext: prefix) can compose a custom
check that adds those tokens to the accepted set:
import {
openBadgesSemanticSuite,
createObv3UnknownAchievementTypeCheck
} from '@interop/verifier-core/openbadges';
const customCheck = createObv3UnknownAchievementTypeCheck({
additionalKnownTypes: ['MyOrgInternalAchievementType']
});
const customSuite = {
...openBadgesSemanticSuite,
checks: openBadgesSemanticSuite.checks.map(c =>
c.id === 'schema.obv3.unknown-achievement-type' ? customCheck : c
)
};
const result = await verifier.verifyCredential({
credential,
additionalSuites: [customSuite]
});For version-pinned behavior, the OB_3_0_ACHIEVEMENT_TYPES set is exported
directly so callers can build their own check against an explicit OB version
rather than tracking the moving KNOWN_ACHIEVEMENT_TYPES alias.
Advanced: Credential recognition + two-pass verification
verifier-core ships a pluggable recognition pipeline. Recognizers (e.g.,
obv3p0Recognizer, obv3p0EndorsementRecognizer) parse a credential's
profile-specific shape and return a normalized form. The default
recognitionSuite runs them in registration order and surfaces the first
applies-true match on CredentialVerificationResult as
normalizedVerifiableCredential + recognizedProfile — so consumers can branch
on the recognized profile and reach a typed view of the credential without
re-parsing.
End-to-end Open Badges wiring
import { createVerifier } from '@interop/verifier-core';
import {
obv3p0Recognizer,
obv3p0EndorsementRecognizer,
openBadgesSuite
} from '@interop/verifier-core/openbadges';
import type { Obv3p0OpenBadgeCredential } from '@interop/verifier-core/openbadges';
const verifier = createVerifier({
recognizers: [obv3p0Recognizer, obv3p0EndorsementRecognizer]
});
// One pass: full crypto + recognition + OB semantic checks.
const presResult = await verifier.verifyPresentation({
presentation: vp,
additionalSuites: [openBadgesSuite]
});
for (const credResult of presResult.credentialResults) {
if (credResult.recognizedProfile === 'obv3p0.openbadge') {
const ob =
credResult.normalizedVerifiableCredential as Obv3p0OpenBadgeCredential;
console.log('Achievement:', ob.credentialSubject);
}
}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
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
pattern may be for you.
Two-pass verification
When a credential has already been cryptographically verified — e.g.
re-rendering a previously-verified credential, or running deeper semantic
analysis on demand — the phases: filter lets you re-run only the work you
need. Phases are 'cryptographic' | 'trust' | 'recognition' | 'semantic';
requesting 'semantic' automatically includes 'recognition' so semantic
checks can consume the normalized form.
// Pass 1 (default — every phase runs): full crypto + trust + recognition + semantic.
const fullResult = await verifier.verifyCredential({
credential,
additionalSuites: [openBadgesSuite]
});
// Pass 2: re-run only the semantic checks (recognition is auto-included).
const semanticOnly = await verifier.verifyCredential({
credential,
additionalSuites: [openBadgesSuite],
phases: ['semantic']
});
// semanticOnly.partial === true
// semanticOnly.results contains only the recognition.profile + OB semantic check entriespartial: true is set on the result whenever the consumer passed an explicit
phases: value, so downstream code can tell that the result covers only a
subset of the pipeline. The default (no phases:) leaves partial unset —
existing consumers see no change in result shape.
applies predicate contract
A VerificationSuite may declare an applies(subject, context) predicate. The
orchestrator uses it as follows:
- Implicit (default suite list): if
appliesreturns false, the suite is silently skipped — no entries inresults. - Explicit (suite passed via
additionalSuites): ifappliesreturns false, a synthetic<suite-id>.applies'skipped'CheckResultis emitted so the consumer sees their explicit request was acknowledged but not actionable.
This is what lets you queue openBadgesSuite against an arbitrary credential
and still get a clear signal in the result when the credential isn't an Open
Badge.
ProblemDetail.instance attribution
Failure-outcome ProblemDetail entries from semantic and envelope checks carry
an RFC 6901 JSON Pointer on
instance, locating the offending portion of the credential — aligned with
RFC 9457. For example, an OB
credential that names a result.achievedLevel not declared on the referenced
ResultDescription produces:
{
type: 'https://www.w3.org/TR/vc-data-model#OB_INVALID_ACHIEVED_LEVEL',
title: 'Invalid Achieved Level',
detail: 'Result entry at index 0 claims achievedLevel "urn:lvl:NOT_REAL", ...',
instance: '/credentialSubject/result/0/achievedLevel',
}UI surfaces can highlight the exact field by walking the pointer.
Architecture
For internal architecture details — verification pipeline, suite model, type
system, dependencies, and architectural direction — see
docs/architecture.md.
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 (@interop/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 viacreateVerifier(...)rather than reach for these directly.Result-shape changes:
CredentialVerificationResult.credential→verifiableCredential(s/\.credential\b/.verifiableCredential/on accessor sites).PresentationVerificationResult.allResultsremoved — replace withflattenPresentationResults(result).PresentationVerificationResult.verifiablePresentationadded — new field carrying the parsed VP.
Default cache isolation: two
createVerifier()calls without an explicitcacheServiceno longer share a process-wide cache. To preserve the previous "shared" behavior, construct oneInMemoryCacheServiceand pass it to each verifier explicitly.Legacy result types removed:
VerificationResponse,PresentationVerificationResponse, and friends were already removed fromindex.tsin1.0.0-beta.11; the type definitions are now gone too. Anyone who needed the old shape can pin1.0.0-beta.11or earlier.ProblemTypesconst map added: built-in problem URIs are now importable asProblemTypes.INVALID_SIGNATUREetc. Existing literal-string comparisons againstProblemDetail.typecontinue to work unchanged.OBv3 verification is opt-in — the OBv3 schema suite no longer runs by default. Add
openBadgesSuite(or one of its variants) viaadditionalSuiteson the verify call to restore previous behavior. See the Open Badges 3.0 verification section for details.OBv3 problem-type rename —
OBV3_INVALID_RESULT_REFERENCE(and other OB problems mirrored onProblemTypes) moved out of the core catalog intoOpenBadgesProblemTypesin the/openbadgessubmodule, and the wire URIs shifted from…#OBV3_*to…#OB_*. Callers comparing literal strings againstProblemDetail.typeneed to update the affected literals; callers using the constants should switch to the new module.
Install
Node.js 24+ is required.
NPM
npm install @interop/verifier-coreDevelopment
This repo uses pnpm.
git clone https://github.com/interop-alliance/verifier-core.git
cd verifier-core
pnpm install
pnpm test # lint + node tests (vitest)
pnpm run test-browser # playwright (run `pnpm exec playwright install chromium` first)Contribute
PRs accepted.
License
MIT License © 2025 Digital Credentials Consortium.
