Repository navigation
Move to verifier-core 2.0.0-beta.1 - #31
Conversation
Nate's 2.0.0-beta.1 is on npm, built from verifier-core main e86bb79, so depend on the published package rather than a commit on GitHub. What changed upstream, and what follows here: - Validity dates have their own check, cryptographic.proof.validity-period, which runs once the signature has verified. Expiry is read from it, the seal is just the signature passing, and the Dates row now has that id. Expiry and a withdrawal no longer stop the checks after them, so an expired credential now reports its withdrawal list and its issuer, and a "withdrawn" mark on a list someone else signed is caught. - The registry check carries what it found as data. Names are read from it, not parsed out of the message, so a name with a comma survives. - verifier-core#65 is fixed: an issuer whose did:web document won't load reads DID_WEB_UNRESOLVED. The namingDidWebOutages stopgap goes; its end-to-end tests now run the library unaided, and pass. - openBadgesSchemaSuite moved to /openbadges/schema and validates against bundled schemas. `verbose` and CheckResult.check are gone. No wording changes. One new outcome code, dates_unchecked, for a signature that passed with no dates check reported: it says COULD_NOT_CHECK, so "Verified" never sits above a Dates row reading "not checked". The consistency suite gains the dates as a dimension (38,880 combinations). A browser test that counted rows one tick after a re-check now counts them as the check starts: with nothing to fetch, the check finishes in a tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verifier-core fetched the Open Badges schema from purl.imsglobal.org on every verification until 2.0.0-beta.1, so the browser tests served a copy, and the cross-site tests' credentials named one on the second server. It now validates against schemas bundled with it and fetches nothing. Removed, as support.ts said to once that happened: the copy and its route, the second server's copy, FIXTURES_SCHEMA_URL in make-fixtures.js and its empty override in build-site.js. The watch on purl.imsglobal.org stays, now with no exception: any request there fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's library note, its account of the schema copy and two rows of its file table; the inventory's note on what is on npm; and an update to requirements §5 on why an expired credential's seal still holds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dates that went unchecked while the seal held now lead over the issuer
verdicts. A dates check that throws reports CHECK_ERROR, not a date, and
"Genuine, but we can't confirm who issued it" sat above a Dates row
reading "not checked". The consistency suite gains the rule that would
have caught it, and outcomes.test.ts pins the outcome.
- The halted reason names the check's id within its suite ("Not run:
proof.signature failed"), as the library writes it; the consistency
fixture and halted()'s note had the full id.
- The browser tests refuse requests to purl.imsglobal.org again, rather
than leaving one to the network before failing; the cross-site tests
drop the refusal, as before, and keep the watch.
- Three comments that described the stopgap, the schema fetch, or a
fatal failure ending only its own suite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Expiry and a start date to come count only while the seal held. Until 2.0.0-beta.1 that came free, since the dates were judged inside the signature check; now an expired dates row beside an unchecked seal read "Expired on …" as fact. - dates_unchecked yields to the withdrawal list's unknowns, whose words are more specific and say whose it is to fix, and still leads over who issued it. - The seal is tested once: below that branch it held, so the repeated seal conditions and the unreachable signature_unchecked tail go. Tests: the "date nothing vouched for" test now reports an expiry and checks it isn't believed, rather than skipping the check. The consistency suite gains a dates check that threw, a rule that a date is reported only behind a held seal, one that an expiry the library didn't report never appears (the end date now tested), and an allow-list for unchecked dates. Each net was mutation-checked: 2517, 1, 226 and 9019 failures for the four regressions it guards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Dates row believes the dates check only while the seal held, as the headline now does: behind an unheld seal it reads "not checked", "in date" included, as it did before 2.0.0-beta.1. - Comments: the expired branch's seal always holds now; dates_unchecked's ordering note says what the withdrawal unknowns give that it doesn't. Tests: consistency rules that the Dates row reads "not checked" behind an unheld seal, and that no verdict but "Expired" talks of expiry (7021 and 11600 failures under mutation); unit tests for a start date and an "in date" behind a missing seal, and for dates_unchecked yielding to a withdrawal check that couldn't finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expired branch's comment no longer says its reassurance can be absent; the dates_unchecked note is re-wrapped. "No verdict but Expired talks of expiry" now reads the action too, and matches "run out", "end date" and "renew" as well as "expired" (caught a "renewed" action under signature_unchecked by mutation). A duplicate assertion goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Try it: https://digitalcredentials.github.io/verifier-plugin/pr-31/ Updates on every push to this PR, and goes away when it closes. The browser tests run against this copy too. |
verifier-core#67, fixed in 2.0.0-beta.1: each seal in a proof set is checked and all must hold. The demo's verified credential, given a second copy of its seal with one character changed, reads as tampered whichever of the two is broken; with two good copies it holds. Making isTampered return false fails both tampered cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DCC's own status service (status-list-lambda) signs every list with its own key, and the batch issuer signs each batch with a fresh did:key, so their lists never match their credentials' issuer. Since #22 we refused such a list, so every batch-issued credential read "We couldn't check whether this copy is still active", and with 2.0.0-beta.1 a deactivated one did too, instead of "deactivated". Nate Otto, 22 October 2026: a mismatch is a warning, not recommended practice; show it as one "until we get our productionized issuer capabilities set up in the next couple months". So by default the list is read like any other, with the same words, and the mismatch is the library's warning in the Developer view. The strict rule stays behind OutcomeOptions.requireIssuerSignedStatusList, for when DCC's issuers sign their own lists. Meanwhile we can't notice a list re-signed by someone else, which for these credentials the check couldn't tell from the normal case anyway. One switch, believedChecks: summarise and listChecks both act on the checks less the list-issuer one unless asked, so they can't disagree. Tests: the strict tests run with the setting on; new ones show a list someone else signed reads exactly as the issuer's own, clear, deactivated or on hold. The consistency suite runs those combinations both ways, with rules that by default they read exactly as the issuer's own list and, strict, believe nothing. Mutation: ignoring the setting fails 14568 tests, dropping the default 14571, the rows not following 14571. Demo: "Untrusted list" goes, since it would read like "Not withdrawn"; its fixture stays, and the browser test loads it directly and checks Verified, with "Failed: list-issuer" in the Developer view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- With requireIssuerSignedStatusList, a list that was read is believed only when the library confirmed its signer: a list-issuer check that errored, was skipped or is missing now counts as someone else's, rather than the issuer's by default. (Off by default, and so it was before.) - The consistency suite's lists the issuer signed now carry the passing list-issuer check the library reports for them, and run both ways, with the rule that strict reads them exactly as the default does. - Unit tests: strict still believes the issuer's own list; believes nothing when the signer check errored, was skipped, or is absent. - Comments: the Developer view shows the mismatch as the library's "Failed: list-issuer", not a warning; the setting is ours to flip, not a property of the element; one ordering note qualified. Mutation: failing open fails 3 tests (the three written for it); refusing the issuer's own list under strict fails 14569. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nger Under requireIssuerSignedStatusList, a list whose signer check errored, was skipped or is missing had a mismatch made up for it, so it read as someone else's list: "may not be from the issuer", "let them know", though nothing was wrong at the issuer's end and a retry may well work. Now its withdrawal answer is set aside, as if that check hadn't finished, which reads "we couldn't finish that check" with a Status row of "not checked". A mismatch the library reports keeps the stranger's wording. Nothing is added to the checks: only what the library reported. The consistency suite gains lists whose signer check threw (passed+errored, revoked+errored), run both ways: by default they read as the issuer's own; strict, they settle nothing. The lists the issuer signed are one constant. Unit tests cover a clear, deactivated and on-hold list with an errored signer check; the "skipped" case, which the library can't produce for a list it read, goes. Mutation: strict believing an unconfirmed list fails 9716 tests; the default refusing one fails 9712. Unit suite: 95,984 tests, 25s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… one The consistency suite now says why, not only that, a list is refused under requireIssuerSignedStatusList: a stranger's list reads "may not be from the issuer" with its Status row; a signer nobody could confirm reads "we couldn't finish that check" with "not checked". Putting back the old made-up mismatch for an unconfirmed signer now fails 2100 combinations. It also gains suspended+errored, and the Rev doc says what +errored is. The missing-signer unit test pins its wording and row, not just its code. Not changed: "Try again" for a read list with no signer check. The library's status suite always runs that check after a list it read, so the case can't arise with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The strict-lists rule now checks the action as well as the reason: a stranger's list tells the earner to let the issuer know, an unconfirmed signer to try again (giving the stranger's case "Try again" fails 1008 combinations). Its Status-row half is limited to a held seal on purpose, and now says so: behind a broken seal that row is about the seal (#32). A unit test covers a skipped signer check, which the docs already said confirms nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Following Nate's recommendation: a status list signed by someone other than the issuer is now read like the issuer's own, and flagged for developers Smoke-testing this PR showed that DCC's status service (status-list-lambda) signs every list with its own key, and batch issuer signs each batch with a fresh did:key — so batch-issued credentials' lists are never signed by their issuer. Since #22 the plugin refused such lists, so those credentials read "We couldn't check whether this copy is still active", and with this PR a deactivated one would have too. Per Nate (22 Oct: a mismatch is a warning, not recommended practice; "display it as a warning and carry on until we get our productionized issuer capabilities set up"), commit 46a45f7 and its review fixes change that:
Checked: 105,697 unit tests (the consistency suite runs both settings), browser tests, medium reviews with tests on each commit, and a smoke comparison against the previous preview: exactly the four expected cases changed (the deactivated vc-test-fixtures credentials now read "deactivated", and the removed demo button). @ottonomy — does this match what you meant? @jchartrand — FYI: no earner-facing wording changed, but this reverses the #22 rule. |
Whether a credential has a status list, and which entry of it is its own, are the credential's claims. Behind a seal that didn't hold they vouch for nothing, yet the Status row said "the issuer set up no way to deactivate this" for a tampered credential with no status entry, which removing that entry would buy. It now reads "not checked", as the Dates row does since #31. The same holds for the headline: "deactivated" and "on hold" are believed only behind a held seal. The library halts after a failed seal, so it can't produce either today, but the rule must not depend on that. notWithdrawn needs no guard: everything that reads it already stands behind a held seal. A test that pinned "withdrawn" behind a missing seal, to check the action named nobody, now pins signature_unchecked and still checks that. The consistency suite gains the rule that the Status row reads "not checked" behind an unheld seal. Mutation: dropping the row guard fails 20782 tests, the withdrawn guard 1853, the on-hold guard 1852. Closes #32 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vitest fails any test file that runs longer than 60 seconds, whatever its
results ("Timeout calling onTaskUpdate"). The consistency suite grew to
105,499 tests in one file, and CI machines vary about threefold in speed:
it passed in 34 s on #31 and failed twice at 70 s on #33, every test
passing. Reproduced locally by slowing it past 60 s; two 40 s files run
back to back on one worker pass, so the limit is per file.
The combinations, their builders and the agreement rules move unchanged
to consistency-combinations.ts. consistency-1 to -4 each run every fourth
combination (26,358 each, 105,432 in all). consistency.test.ts keeps the
claim patterns, the nameless-issuer check (whole, about 10 s) and the
stopped-early cases, and gains one test that every part has its file, so
a lost file can't drop a quarter of the combinations unnoticed.
Checked: 105,698 tests (105,697 plus that one); four deliberate breaks
fail exactly as many tests as on the single file (29,147, 14,571, 1,010,
14,573); the guard fails with a part missing or one extra; the slowdown
that failed the single file at 73 s passes split, files run one at a
time (each at most 17 s). The whole run takes about 10 s, not 25.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Vitest also fails any one test past 5 seconds. The five nameless-issuer tests each looped over all 105,432 combinations, about 2 s apiece beside the parts; under CPU load (10 busy processes, the parts about 2.3 times slower) three of them timed out. They now run in each part over its quarter: 20 tests of about 0.45 s, all passing under that load (slowest 0.79 s). The total goes from 105,698 to 105,713. - Two part files could call the same part and the guard would pass, a quarter of the combinations silently unrun. Each part now takes its number from its own file name, so the files carry no number to repeat; a name outside consistency-1 to -4 stops its file with an error. - Comments that pointed "below" or "above" at code now in the other file say which file; the README rows say where the nameless check went. Checked: lint; 105,713 tests pass; with 10 busy processes alongside, all pass; an extra consistency-5 fails its file and the guard; ignoring the setting still fails 29,147 tests and the stranger told "Try again" 1,010, as on the single file; naming a nameless issuer by its identifier fails all 20 nameless tests, 5 in each part. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In CI the four parts took 16 to 27 s each (23 October 2026), two to four times a laptop's, as they share fewer processors: only about 2x clear of the 60 s limit. That limit turns out not to be the file's length. A worker fails the run, every test passing, when a message to the main Vitest process waits 60 s for its reply to be read, and synchronous tests never let it read one. A pause after each test (one setImmediate) lets it, however long a file runs: one part slowed to 74 s passes with the pause and fails without, and a 70 s probe file of 1,000 slow tests does the same. A microtask doesn't (the reviewer checked). The nameless-issuer tests each loop over a part's combinations and took up to 2.0 s in CI, against Vitest's 5 s limit per test; they now have 30 s. Slowed to about 7 s each, they pass with it and fail without. Cost: about 8 to 15% per part file locally (7.1 to 8 s, was 6.6 to 7 s), about 1 s on the whole run. 105,713 tests pass; lint passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether a credential has a status list, and which entry of it is its own, are the credential's claims. Behind a seal that didn't hold they vouch for nothing, yet the Status row said "the issuer set up no way to deactivate this" for a tampered credential with no status entry, which removing that entry would buy. It now reads "not checked", as the Dates row does since #31. The same holds for the headline: "deactivated" and "on hold" are believed only behind a held seal. The library halts after a failed seal, so it can't produce either today, but the rule must not depend on that. notWithdrawn needs no guard: everything that reads it already stands behind a held seal. A test that pinned "withdrawn" behind a missing seal, to check the action named nobody, now pins signature_unchecked and still checks that. The consistency suite gains the rule that the Status row reads "not checked" behind an unheld seal. Mutation: dropping the row guard fails 20782 tests, the withdrawn guard 1853, the on-hold guard 1852. Closes #32 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the final review of the whole branch (24 October 2026). Comments only: the built JavaScript is byte-identical; the .d.ts files differ in these comments. - isWithdrawn, notWithdrawn and PROBLEM.revoked said a list someone else signed establishes nothing. That is now true only with requireIssuerSignedStatusList; by default it is read like the issuer's. - CHECK.statusListIssuer said the check runs only for lists that loaded. It gives one answer for all of a credential's lists, compares only those whose signature it can establish, and is skipped if any couldn't be. - OutcomeOptions gains what to know before turning the strict rule on: one answer for all lists means a stranger's second list sets aside the issuer's own "deactivated", which then reads "couldn't check". Cautious, not wrong; found by a probe in review. - A consistency comment described verifier-plugin#32's fix as if it were already here; the README credited the split, not the pause, with the 60-second limit. Checked: lint, typecheck, 105,713 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether a credential has a status list, and which entry of it is its own, are the credential's claims. Behind a seal that didn't hold they vouch for nothing, yet the Status row said "the issuer set up no way to deactivate this" for a tampered credential with no status entry, which removing that entry would buy. It now reads "not checked", as the Dates row does since #31. The same holds for the headline: "deactivated" and "on hold" are believed only behind a held seal. The library halts after a failed seal, so it can't produce either today, but the rule must not depend on that. notWithdrawn needs no guard: everything that reads it already stands behind a held seal. A test that pinned "withdrawn" behind a missing seal, to check the action named nobody, now pins signature_unchecked and still checks that. The consistency suite gains the rule that the Status row reads "not checked" behind an unheld seal. Mutation: dropping the row guard fails 20782 tests, the withdrawn guard 1853, the on-hold guard 1852. Closes #32 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post-merge check (29 October 2026)Checked on the live demo and the
The wallet still pins an earlier build (a4ca3e6), so none of this reaches it until that pin is bumped. |

Nate published verifier-core 2.0.0-beta.1 (built from verifier-core main
e86bb79, 35 commits after our pind217b04). This moves the plugin onto the published package and adapts to what changed. It follows verifier-core#63, #64, #65 and #67, all closed by that release.What changes for the earner
No wording changes.
namingDidWebOutagesstopgap is removed. Its end-to-end tests now run the library on its own, and pass.dates_unchecked, using the existing "We couldn't check this right now" words. It applies only when the seal held but the dates check didn't report or crashed, which the library doesn't do today.What changes in the code
cryptographic.proof.validity-periodcheck, not the signature check. A date counts only while the seal held, in the headline and in the Dates row. The Dates row's id is nowcryptographic.proof.validity-period.openBadgesSchemaSuitemoves to/openbadges/schemaand validates against schemas bundled with the library. The tests' copy of the schema and its plumbing are removed. Requests to purl.imsglobal.org are still refused and fail a test.verboseoption and theCheckResult.checkfield are gone.OutcomeOptions.requireIssuerSignedStatusList(off) keeps the Move to verifier-core main, and say what its new results mean #22 rule for later. One helper,believedChecks, decides which checks the headline and the rows both act on, so they can't disagree.Validation (local)
Reviews
Each review was sized by what the commit touches. Every round included the tests, because
outcomes.tsand the consistency suite were involved.CheckResult.haltskept because it mirrors the library's shapeunroutepattern is as on main and guarded by the preflight probe test, andhalted()never branches on which check haltedMutation checks on the status-list commits, as measured at each commit (several nets have grown since; the final review below re-ran them): setting ignored 14,568; default dropped 14,571; rows not following the headline 14,571; strict refusing the issuer's own list 14,569; strict believing an unconfirmed list 9,716; default refusing one 9,712; the old made-up mismatch put back 2,100; a stranger's list told "Try again" 1,008; strict failing open 3.
Final review (24 October 2026)
Three independent reviewers, each with its own experiments:
src/outcomes.ts; all but one fail between 1 and 29,147 tests, among them a pass above an error row (10,226), a date believed behind a broken seal (15,613), a stranger's list believed in strict mode (14,574), expired-and-deactivated headlined "Expired" (562), a "couldn't check" that leans positive (21,844), and a registry named to the earner (4,204). The exception: the did:web outage's own wording isn't in the combinations, so a positive-leaning rewording of it would pass. That predates this PR; it is the first follow-up.Follow-ups, not in this PR: the did:web and HTTP-error outcomes in the combinations; strict mode with two status lists; "couldn't load the standard to check it against", which gives the wrong reason now that the schemas are bundled (on main too); a few rare result shapes for the combinations (tried: no contradiction).
The consistency suite and Vitest's time limits
With the status-list settings the consistency suite reached 105,499 tests in one file, and CI started failing with every test passing: Vitest's "Timeout calling onTaskUpdate" (a test worker's message to the main process waits 60 s for a reply it can't read while synchronous tests run back to back). It passed at 34 s and failed twice at 70 s on #33. ab12878 and 66ab65f run the combinations from four files; be205cd adds a pause after each test, which removes that limit however long a file runs (a part slowed to 74 s passes with it and fails without), and gives the five nameless-issuer tests 30 s instead of Vitest's 5 s per test. Outside the tests only the README, one planning-doc line and one comment change: the built plugin is byte-identical. Breaking the code fails exactly as many tests as on the single file (e.g. 29,147 and 1,010).
Several seals on one credential (#67)
Tested against the real library in 487b553. The demo's verified credential with a second copy of its seal reads as tampered when either copy is broken (first or second), and holds when both are good. Not covered: seals in two different cryptosuites, where the library treats one it cannot check as a warning. That's rare for our issuers.
Status lists signed by someone else
Smoke-testing this PR showed that DCC's status service (status-list-lambda) signs every list with one
ISSUER_SEED, and batch-issuer-backend signs each batch with a fresh did:key. So a batch-issued credential's list is never signed by its issuer. Since #22 the plugin refused such lists: those credentials read "We couldn't check whether this copy is still active", and with 2.0.0-beta.1 a deactivated one would have too.Nate (22 Oct): a mismatch is a warning, not recommended practice; "display it as a warning and carry on until we get our productionized issuer capabilities set up in the next couple months". 46a45f7 and its review fixes do that:
OutcomeOptions.requireIssuerSignedStatusList(off), to switch back on once DCC's issuers sign their own lists. Strict mode still believes the issuer's own list, and reads an unconfirmed signer (that check errored, was skipped or is missing) as "we couldn't finish that check". One limit, documented inOutcomeOptionsfor whoever turns it on: the library gives one signer answer for all of a credential's lists, so where a credential has two, one the issuer's and one someone else's, strict mode reads even the issuer's own "deactivated" as "couldn't check". Cautious, not wrong.Smoke tests
87aad6c= main + this PR up to 487b553) against the live demo (main,37b6fd9), on all 13 demo credentials and 9 vc-test-fixtures links. 9 differed, all as intended: Expired, Not yet valid, Withdrawn and Suspended (and the expired fixtures) now report the issuer and "How it was built" rows instead of "not checked"; and the vc-test-fixtures revoked credentials, whose shared list is signed bydid:key:z6MknNQD…, read "couldn't check", which led to the change above.main,37b6fd9) against pr-31 (92e8165), expectations written first: exactly the predicted 10 of 22 cases differ, each in the predicted way (the four above with real issuer and "How it was built" rows; the two revoked-and-expired fixtures now leading "The issuer has deactivated this copy"; the revoked fixture's rows; two expired fixtures' Issuer rows; and "Untrusted list" only on live).Tested in the wallet
24 October 2026. The wallet's own verification slot (
src/plugins/Verifier.tsx, with its registry list), on a fresh install of lcw-front-endmainfrom its lockfile, with this PR and #33 packaged exactly as the release workflow packages them, against today's release build:npm run build(type check and production build) passes.a4ca3e6), so the wallet changes when its pin is bumped, not when this merges. How the deployed wallet installs isn't in its repository, so that part is unverified.Worth knowing
releasebuild.Open question for reviewers
The registry check now also gives the registry's own name for the issuer (
entity.name), which the credential cannot alter. Should the verification box show that name instead of the credential's own when a registry matches? This PR doesn't do it, because it would change what earners read.🤖 Generated with Claude Code