Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ca7a544
Move to verifier-core 2.0.0-beta.1
threeqube Oct 21, 2026
b3bee67
Drop the Open Badges schema copy the tests served
threeqube Oct 21, 2026
abf5115
Docs: verifier-core 2.0.0-beta.1
threeqube Oct 21, 2026
1255fc8
Review fixes for the verifier-core 2.0.0-beta.1 move
threeqube Oct 21, 2026
f77157b
Second review: a date is a finding only behind a held seal
threeqube Oct 21, 2026
14d3aac
Third review: no date behind a seal that didn't hold, in the row too
threeqube Oct 21, 2026
4efceab
Fourth review: comment, wrapping, and a wider expiry net
threeqube Oct 21, 2026
487b553
Test a credential with several seals against the real library
threeqube Oct 21, 2026
46a45f7
Read a status list someone else signed like the issuer's own
threeqube Oct 22, 2026
4d52dfd
Review: strict lists fail closed, and the issuer's own list is tested
threeqube Oct 22, 2026
43cc444
Review: an unconfirmed list signer is an unfinished check, not a stra…
threeqube Oct 22, 2026
6c842d4
Review: pin the right reason for a stranger's list and an unconfirmed…
threeqube Oct 22, 2026
1d41cea
Review: pin the action too, say why the row rule needs a held seal
threeqube Oct 22, 2026
ab12878
Run the consistency suite in four parts, under Vitest's time limit
threeqube Oct 23, 2026
66ab65f
Review: split the nameless check too, and name each part by its file
threeqube Oct 23, 2026
be205cd
Pause after each combination, and give the nameless tests 30 s
threeqube Oct 24, 2026
eea9e86
Review: comments that still said a stranger's list establishes nothing
threeqube Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ From the standup on 21 September 2026:
separated, and `skipped`-with-a-reason and a per-check `fatal` flag as
first-class, both of which had been hand-built here.

2.x is not on npm yet, so `package.json` pins a commit on
digitalcredentials/verifier-core `main`, d217b04 (#22). It is bundled into
`dist/`, so a host app never installs it.
Since 21 October 2026 `package.json` pins the published
`2.0.0-beta.1`, built from digitalcredentials/verifier-core `main`
e86bb79; before, 2.x was not on npm and it pinned a commit (d217b04,
#22). It is bundled into `dist/`, so a host app never installs it.
- **veri-good is a reference, not a foundation.** It was an experiment, it
targets a different setting (an issuer's own web page), and nobody uses it.
Worth learning from, not worth inheriting.
Expand All @@ -163,9 +164,10 @@ tested.** It was a 1.x problem. 2.x fetches with a bare `fetch(url)`, so
nothing is preflighted, and a page on localhost reads the withdrawal lists on
GitHub Pages correctly. The browser tests cover it against a second local
server that refuses preflights the way GitHub Pages does
(`test/browser/pages-like-server.js`). Separately, the tests now serve a copy
(`test/browser/pages-like-server.js`). Separately, the tests served a copy
of the Open Badges schema instead of fetching it, after one request took 52
seconds. The rest of this section is the 21 September record of 1.x, kept as
seconds; since 2.0.0-beta.1 the library bundles the schemas and fetches
none, and the copy is gone. The rest of this section is the 21 September record of 1.x, kept as
it was: it no longer describes how things stand.

**One real problem: fetching a status list fails in the browser.** The request
Expand Down Expand Up @@ -248,8 +250,10 @@ A first slice: a real credential in, real verification, one card out.
| `src/jsx.ts` | Tells a React host's JSX about `<verifier-credential>`; types only |
| `test/outcomes.test.ts` | The mapping, and the ways a good credential can be made to look bad |
| `test/fixtures.test.ts` | The demo's credentials are well-formed Open Badges, apart from the one built wrong on purpose |
| `test/did-web-outages.test.ts` | An issuer's website that won't serve its key reads as unreachable, not tampered (stopgap for verifier-core#65) |
| `test/consistency.test.ts` | 1,500 combinations asserting the headline and the breakdown can never disagree |
| `test/did-web-outages.test.ts` | An issuer's website that won't serve its key reads as unreachable, not tampered (verifier-core#65, fixed in 2.0.0-beta.1) |
| `test/consistency-combinations.ts` | Every combination of check results, and the rules by which the headline and the breakdown can never disagree |
| `test/consistency-1.test.ts` … `-4` | Those rules, and that a nameless issuer is never addressed by its identifier, over a quarter of the combinations each, so the files run side by side. A pause after each test keeps Vitest's 60-second limit from failing a long file on a slow CI machine |
| `test/consistency.test.ts` | The claim patterns themselves, a credential both withdrawn and expired, verification that stopped early, and a check that every part above has its file |
| `test/browser/states.spec.ts` | Drives the component in a real browser, locally or against a published copy |
| `test/browser/link.spec.ts` | The demo page opened from a link to a credential |
| `test/browser/support.ts` | Page setup both browser specs share |
Expand Down Expand Up @@ -308,10 +312,12 @@ found across four reviews of the first two pull requests, and six were that —
a clean verdict above a row reporting a problem, or a headline claiming in
prose what the breakdown said was never checked.

`test/consistency.test.ts` walks 1,500 combinations and asserts both: that the
severities agree, and that no verdict claims the credential is unchanged or
not withdrawn unless the row it rests on actually reported. Run it if you
touch either function.
The consistency suite (`test/consistency-combinations.ts`, run in parts by
`test/consistency-*.test.ts`) walks every combination of check results and
asserts both: that the severities agree, and that no verdict claims the
credential is unchanged or not withdrawn unless the row it rests on actually
reported. Run it (`npx vitest run test/consistency`) if you touch either
function.

Two rules fall out of it, both learned the hard way:

Expand Down
1 change: 0 additions & 1 deletion dev/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const SITUATIONS: Situation[] = [
{ label: 'Built wrong', file: 'malformed', registries: LOCAL_REGISTRY, note: 'genuine, but missing a field its standard requires' },
{ label: 'Changed', file: 'tampered', registries: LOCAL_REGISTRY, note: 'altered after issuing' },
{ label: 'Not their seal', file: 'not-their-seal', registries: LOCAL_REGISTRY, note: "names the issuer, signed with someone else's key" },
{ label: 'Untrusted list', file: 'list-not-issuers', registries: LOCAL_REGISTRY, note: "its withdrawal list isn't signed by the issuer" },
{ label: 'No signature', file: 'unsigned', registries: LOCAL_REGISTRY, note: 'nothing to check' },
];

Expand Down
4 changes: 2 additions & 2 deletions docs/planning/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ non-blocking severity; it is **not published to npm**, so anything depending on
it depends on a git reference until it lands upstream.

*Since then (October 2026):* the fork's work has landed upstream, as 2.x on
`digitalcredentials/verifier-core`'s main. 2.x is still not on npm (the latest
there is 1.0.0-beta.11), so verifier-plugin depends on it by git commit. This
`digitalcredentials/verifier-core`'s main, and was published to npm as
2.0.0-beta.1 on 21 October, which verifier-plugin now depends on. This
document still describes 1.0.0-beta.11.

A note on wording: the library's own code calls each individual check a
Expand Down
8 changes: 7 additions & 1 deletion docs/planning/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ hasn't been tampered with", when the signature held), and what to do, in
plain words: "We couldn't check this right now. Something went wrong while
we were checking, so we can't tell you yet whether it's valid. Try again in
a little while." Friendly, but never comforting about what we don't know.
consistency.test.ts holds every verdict to this.
The consistency suite holds every verdict to this.

The advice has to match the cause, though. For a **withdrawn** credential the
issuer already made their decision, so the useful advice is to ask for a
Expand Down Expand Up @@ -536,6 +536,12 @@ stays green, and the reassurance can be said. Only the Dates row reports a
problem. (Established from the library's code, 30 September 2026, and pinned by
a test that alters an expired credential and checks it reads as tampered.)

*Update, 21 October 2026:* verifier-core 2.0.0-beta.1 checks the dates in a
check of their own, which runs only once the seal has verified, so an expired
credential no longer fails the signature at all. The rows read as above. And
expiry no longer stops the checks after it, so an expired credential now also
reports whether it was deactivated and who issued it.

**The name at the top follows the same rule.** When the seal didn't hold —
an altered credential, or one with no seal at all — the issuer's name in the
card's header carries "(unconfirmed)", as it already does for an issuer no
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"@digitalcredentials/security-document-loader": "^6.0.0",
"@digitalcredentials/vc": "^9.0.1",
"@digitalcredentials/vc-bitstring-status-list": "^1.0.0",
"@digitalcredentials/verifier-core": "github:digitalcredentials/verifier-core#d217b042371096e84b8cd0046fbe79a667683d8a",
"@digitalcredentials/verifier-core": "2.0.0-beta.1",
"@playwright/test": "^1.63.0",
"@types/react": "^19.3.0",
"oxlint": "^1.75.0",
Expand Down
3 changes: 0 additions & 3 deletions scripts/build-site.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ run('npx', ['vite', 'build', '--mode', 'site'], { SITE_BASE: url.pathname });
run('node', ['scripts/make-fixtures.js'], {
FIXTURES_OUT: join(SITE, 'fixtures'),
FIXTURES_STATUS_LIST_URL: new URL('fixtures/status-list.json', url).href,
// Empty, whatever the shell has: the published credentials name no schema,
// like the demo's own.
FIXTURES_SCHEMA_URL: '',
});

const commit = process.env.GITHUB_SHA ?? execFileSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT }).toString().trim();
Expand Down
25 changes: 6 additions & 19 deletions scripts/make-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
*
* FIXTURES_STATUS_LIST_URL where the withdrawal list will be served
* FIXTURES_OUT the folder to write into
* FIXTURES_SCHEMA_URL a schema for the credentials to name; unset or
* empty, they name none (see below)
*/

import { writeFile, mkdir } from 'node:fs/promises';
Expand Down Expand Up @@ -43,12 +41,6 @@ const OUT =
*/
const STATUS_LIST_URL =
process.env.FIXTURES_STATUS_LIST_URL ?? 'http://localhost:5180/fixtures/status-list.json';
/**
* Unset, the credentials name no schema, and verifier-core falls back to the
* Open Badges schema on purl.imsglobal.org. The cross-site browser tests set
* it to a copy on their own server: see test/browser/pages-like-server.js.
*/
const SCHEMA_URL = process.env.FIXTURES_SCHEMA_URL;
const WITHDRAWN_INDEX = 42;
const LIST_LENGTH = 131072;

Expand Down Expand Up @@ -96,10 +88,6 @@ const baseCredential = (issuerDid, overrides = {}) => ({
criteria: { narrative: 'Completed all coursework and the final assessment.' },
},
},
// `JsonSchema`, the VC 2.0 context's own type. The Open Badges one,
// 1EdTechJsonSchemaValidator2019, signs but then fails the signature check
// ("Safe mode validation error") under the VC 2.0 context.
...(SCHEMA_URL && { credentialSchema: [{ id: SCHEMA_URL, type: 'JsonSchema' }] }),
...overrides.credential,
});

Expand Down Expand Up @@ -206,8 +194,8 @@ const main = async () => {
// required — the same omission in Nate Otto's 22 September screenshot.
// Nothing here is fraudulent: the signature is real, the dates are fine,
// and verifier-core still reports the credential as verified, because the
// schema result is filed under `additionalInformation` and never reaches
// `verified`. It is the issuer's mistake and only the issuer's to fix.
// schema check is not fatal and never reaches `verified`. It is the
// issuer's mistake and only the issuer's to fix.
await write(
'malformed',
await sign(
Expand Down Expand Up @@ -294,11 +282,10 @@ const main = async () => {
);

// --- withdrawal list not the issuer's -------------------------------------
// Not marked on it: the case verifier-core reports. A mark would end its
// status suite at the withdrawal check, which is fatal, before the check
// that compares the list's signer runs — so a stranger's "withdrawn" still
// reads as the issuer's, and nothing in the result says otherwise (raised
// upstream). outcomes.ts handles that case too, once the library reports it.
// Clear on a list signed by another key. Read like the issuer's own by
// default, with the library's mismatch warning in the Developer view; only
// `requireIssuerSignedStatusList` refuses it. Not a demo button, since it
// reads like "Not withdrawn"; the browser tests load it directly.
await write(
'list-not-issuers',
await sign(
Expand Down
8 changes: 3 additions & 5 deletions src/developer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const groupChecks = (r: VerificationResponse): CheckGroup[] => {
let home: CheckGroup | undefined;
for (const group of groups) {
const id = group.suite!.id;
if (check.id && belongsTo(check.id, id) && (!home || id.length > home.suite!.id.length)) {
if (belongsTo(check.id, id) && (!home || id.length > home.suite!.id.length)) {
home = group;
}
}
Expand All @@ -52,11 +52,9 @@ export const groupChecks = (r: VerificationResponse): CheckGroup[] => {
/**
* What to call a check inside its group: the part of its id after the suite's,
* so `cryptographic.core.proof-exists` reads `proof-exists` under
* `cryptographic.core`. Outside a group it keeps its whole id — or, for a
* hand-built result with no id, the deprecated `suite` and `check` it carries.
* `cryptographic.core`. Outside a group it keeps its whole id.
*/
export const checkName = (check: CheckResult, suiteId?: string): string => {
const id = check.id ?? [check.suite, check.check].filter(Boolean).join('.');
export const checkName = ({ id }: CheckResult, suiteId?: string): string => {
if (!suiteId || id === suiteId || !belongsTo(id, suiteId)) return id;
return id.slice(suiteId.length + 1);
};
Loading
Loading