Skip to content

Validate Open Badges against bundled schemas chosen by type, never credentialSchema - #81

Merged
ottonomy merged 5 commits into
docs/changelog-2-0from
fix/bundled-ob-schemas
Oct 21, 2026
Merged

ottonomy merged 5 commits into
docs/changelog-2-0from
fix/bundled-ob-schemas

Conversation

@ottonomy

Copy link
Copy Markdown
Contributor

Stacked on #80 — review and merge that first.

Why

The Open Badges JSON Schema check validated a credential against credentialSchema[0].id whenever one was present, before choosing by type, so an Open Badges credential naming a permissive schema of its own passed the check. It also fetched every schema it used, including URLs the issuer chose. 2.0.0 is unreleased, so this is the moment to fix both, and to move the export, which would be breaking later.

What changed

  • Bundled schemas, no fetching. The four credential schemas in the Open Badges 3.0 spec appendix are vendored as TypeScript modules. schema.obv3.json validates against the one for the credential's type and VC version:

    Credential Schema
    OpenBadgeCredential, VC 2.0 E.2.1.2 AchievementCredential
    OpenBadgeCredential, VC 1.1 E.2.1.34 AchievementCredentialv1p1
    EndorsementCredential, VC 2.0 E.2.1.7 EndorsementCredential
    EndorsementCredential, VC 1.1 E.2.1.36 EndorsementCredentialv1p1

    VC 1.1 is no longer checked against the either-version "Any" schema. Schemas compile once per process (Ajv2020, strictTuples: false). The check no longer uses fetchJson or cacheService, and the cached schema loader is removed.

  • credentialSchema never chooses the schema. A new non-fatal check, schema.obv3.declared, reports a declared schema that does not fit: OB_DECLARED_SCHEMA_MISMATCH (a known OB schema for the other type or VC version) or OB_DECLARED_SCHEMA_NOT_CHECKED (an unknown URL — not fetched). Neither check changes verified.

  • BREAKING: new subpath @digitalcredentials/verifier-core/openbadges/schema exports openBadgesSuite, openBadgesSchemaSuite, obv3SchemaCheck and obv3DeclaredSchemaCheck. /openbadges keeps the semantic suite, recognizers, Zod schemas and problem types, and no longer loads AJV (relevant for Metro and for CSPs without unsafe-eval).

  • Endorsements now reach the schema suites; in openBadgesSuite the semantic checks skip them. openBadgesSemanticSuite stays OpenBadgeCredential-only.

  • Refresh is hand-run: node scripts/refresh-ob-schemas.mjs [--fixtures]. Nothing in build, test or CI fetches. Tests pin each schema's SHA-256 and compare it with the purl-published schema after normalising draft 2020-12 tuple syntax.

  • Docs: README Open Badges section and migration note, CHANGELOG 2.0.0, docs/architecture.md, docs/api/verification-results.md.

Worth knowing

  • The spec schemas reject some JSON-LD-equivalent forms the Zod checks accept, e.g. image.type: ['Image']. That is upstream schema behaviour; a test pins it and the README documents it.
  • The schemas carry no licence text; mechanical copying is assumed permitted for now.
  • Consumers importing openBadgesSuite from /openbadges (e.g. transaction-service's suite resolver) change that import when they move to 2.0.

Validation

  • pnpm run build: pass.
  • pnpm run test (lint + 1032 Node tests, up from 1003): pass.
  • pnpm exec tsc -p tsconfig.dev.json: the same 6 pre-existing errors as the base, none new.
  • Mutation checks: the drift test fails on an edited purl fixture; the no-AJV test fails when an AJV export is put back into /openbadges.
  • npm pack --dry-run: ships dist/openbadges/schema/**, no test fixtures or scripts.

Deferred

  • A generic, opt-in "validate against the issuer's declared credentialSchema" suite.
  • AJV standalone validators for CSP-strict / Hermes consumers (~10× the size of the schema JSON).
  • Depending on a 1EdTech-published schema package, if one appears.

🤖 Generated with Claude Code

ottonomy and others added 5 commits October 21, 2026 00:12
Adds the four credential schemas embedded in the Open Badges 3.0
specification appendix as TypeScript modules under
`src/openbadges/schema/json-schema/`: AchievementCredential (E.2.1.2),
AchievementCredentialv1p1 (E.2.1.34), EndorsementCredential (E.2.1.7) and
EndorsementCredentialv1p1 (E.2.1.36). The VC 1.1-only `v1p1` schemas are
published only in the spec, so the spec is the source for all four.
Nothing uses them yet.

- `scripts/refresh-ob-schemas.mjs` regenerates the modules by hand from
  the spec HTML, recording the source, fetch date and SHA-256. With
  `--fixtures` it also downloads the purl-published twins. Nothing in
  build, test or CI fetches.
- Tests pin each module's `$id` and SHA-256, check each compiles with
  Ajv2020 without strict-mode output, and compare each with its purl twin
  (committed under `test/openbadges/schema-files/purl/`) after rewriting
  draft 2020-12 tuple syntax to 2019-09 form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… credentialSchema

`obv3SchemaCheck` validated a credential against `credentialSchema[0].id`
whenever one was present, before choosing by type, so an Open Badges
credential naming a permissive schema of its own passed the check. It
also fetched every schema it used.

The check now validates against the vendored Open Badges 3.0 schema for
the credential's type and VC version, and never reads `credentialSchema`:

- OpenBadgeCredential: E.2.1.2 AchievementCredential (VC 2.0) or
  E.2.1.34 AchievementCredentialv1p1 (VC 1.1).
- EndorsementCredential: E.2.1.7 EndorsementCredential (VC 2.0) or
  E.2.1.36 EndorsementCredentialv1p1 (VC 1.1).

VC 1.1 credentials are no longer checked against the either-version
"Any" schema. Each schema is compiled once per process with Ajv2020
(`strictTuples: false`), and nothing is fetched or cached, so the check
no longer uses `fetchJson` or `cacheService`.

- The check moves to `src/openbadges/schema/`; `src/suites/schema/`,
  its cached schema loader and the internal `schema.obv3` suite are
  removed.
- Success and failure messages name the spec section of the schema used.
- Tests cover each schema, the VC 1.1-only error set, a regression test
  for the `credentialSchema` bypass with zero HTTP requests, the
  upstream schema's rejection of `image.type: ['Image']`, and compiling
  once across verifiers. Specs that served a stand-in schema over a fake
  HTTP service now run against the real one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the credential

The Open Badges JSON Schema check no longer follows `credentialSchema`,
so a declaration that disagrees with the credential would go unseen. A
new non-fatal check, `schema.obv3.declared`, compares each declared
entry with the schema the credential is validated against:

- `OB_DECLARED_SCHEMA_MISMATCH` — a known Open Badges 3.0 schema URL
  for the other OB credential type, or one that does not accept the
  credential's VC version.
- `OB_DECLARED_SCHEMA_NOT_CHECKED` — a URL that is not a known Open
  Badges 3.0 schema, even alongside one that fits. It is not fetched.

Known URLs are the eight purl-published schemas (`json/` and `json-ld/`,
strict and "Any") and the four `$id`s of the schemas embedded in the
spec. A declared URL fits when it is for the credential's OB type and
accepts its VC version. Each problem's `instance` points at the entry.
The check succeeds when every entry fits and is skipped when nothing is
declared; it never changes `verified`.

`obv3DeclaredSchemaCheck` runs after `obv3SchemaCheck` in
`openBadgesSchemaSuite` and `openBadgesSuite`, and is exported from
`/openbadges`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and apply them to endorsements

BREAKING: `openBadgesSuite`, `openBadgesSchemaSuite`, `obv3SchemaCheck`
and `obv3DeclaredSchemaCheck` are no longer exported from
`@digitalcredentials/verifier-core/openbadges`. Import them from the new
`@digitalcredentials/verifier-core/openbadges/schema` entry point.

Everything that loads AJV and the bundled Open Badges 3.0 schemas now
sits behind its own subpath, so importing `/openbadges` for the semantic
suite, recognizers, Zod schemas or problem types loads neither. That
matters where bundlers do not tree-shake (Metro), and under a Content
Security Policy that forbids `unsafe-eval`.

`openBadgesSchemaSuite` and `openBadgesSuite` now apply to
EndorsementCredential as well as OpenBadgeCredential, so an endorsement
is validated against its schema. In `openBadgesSuite` the semantic
checks skip an endorsement. `openBadgesSemanticSuite` still applies to
OpenBadgeCredential only.

- Tests: both subpaths resolve through `package.json#exports` and
  expose only their own symbols; `/openbadges` imports with every AJV
  module mocked to throw; endorsements pass E.2.1.7 through both
  schema suites with the semantic checks skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… subpath

- README: the Open Badges section splits the two subpaths, adds an
  "Import from" and "Applies to" table, and replaces the fetch-and-cache
  paragraph with the bundled schemas, the `schema.obv3.declared` check,
  the CSP note, the `image.type: ['Image']` caveat and the maintainers'
  refresh script. Import examples use `/openbadges/schema` for
  `openBadgesSuite`; the migration section says so, and that
  `credentialSchema` is no longer followed. Schemas leave the list of
  URLs verification fetches.
- CHANGELOG 2.0.0: the JSON Schema breaking change and the Open Badges
  and package `exports` entries describe the new subpath, endorsements
  and the declared-schema check.
- `docs/architecture.md`: source and test trees, `scripts/`, the
  `openbadges/schema/` → `openbadges/` dependency direction, the new
  problem types, the AJV dependency row and the known gap.
- `docs/api/verification-results.md`: `schema.obv3.declared` in the id
  listings and the subpath in the phase table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ottonomy
ottonomy added this pull request to stack #82 October 21, 2026 05:40
@ottonomy
ottonomy removed this pull request from stack #82 October 21, 2026 05:40
@ottonomy
ottonomy added this pull request to stack #77 October 21, 2026 05:41
@ottonomy ottonomy mentioned this pull request Oct 21, 2026
@ottonomy
ottonomy merged commit e86bb79 into main Oct 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant