@burnt-labs/provider-registry-types
v0.6.0
Published
Types, zod schemas and JSON Schema for the Burnt provider registry artifact served at api.provider-registry.burnt.com.
Keywords
Readme
@burnt-labs/provider-registry-types
Types, zod schemas and JSON Schema for the provider registry artifact served at
https://api.provider-registry.burnt.com.
npm install @burnt-labs/provider-registry-types zodimport {
RegistryArtifactSchema,
SUPPORTED_ARTIFACT_SCHEMA_VERSION,
type RawProvider,
type ProofPlan,
type ProviderHostGroup,
type EndpointHostBinding,
} from '@burnt-labs/provider-registry-types';
const artifact = RegistryArtifactSchema.parse(await res.json());The generated JSON Schema is available at
@burnt-labs/provider-registry-types/schema.json for consumers that do not use
zod (the Rust verifier, CI conformance jobs).
zod compatibility
zod is a peer dependency, and the supported range is
^3.25.0 || ^4.0.0.
zod 3.25 ships the whole of zod 4 at the zod/v4 subpath, which is what this
package imports — so a consumer pinned to zod 3.25 gets the same types and the
same runtime behaviour as one on zod 4, without migrating. That matters because
the largest consumer, EarnOS's data-ops, exports zod schemas to ~20 workers
and cannot move versions for this package alone.
Declarations are emitted against the floor of that range rather than the
newest zod, and this is load-bearing rather than incidental: zod 4.4.3 gave
ZodDiscriminatedUnion a second type parameter that zod 3.25's bundled v4 does
not have, so declarations built against 4.4.3 fail on 3.25 with TS2707. The
older, narrower generic satisfies both. npm run check:consumer-compat in the
source repo packs this tarball and compiles a consumer against every version in
the range, so the constraint is checked rather than remembered.
Its root validates a registry artifact — the bytes at
/.well-known/provider-registry/providers.json — so compiling the document and
handing it an artifact is the whole job. The other shapes are $ref targets
rather than alternative roots:
{ "$ref": "https://burnt.com/schemas/provider-registry.schema.json#/$defs/ClaimOutputArtifact" }The root deliberately asserts something. A schema document carrying only
$defs has no root assertions, and an empty schema accepts every instance —
{} and null included — while looking exactly like a validation step that
passed. The generator refuses to emit one.
Why this exists
Consumers live in separate repositories with no shared build — EarnOS, the Satya verifier, the Expo SDK. Before this package their only options were hand-written types or a vendored copy of a schema from another repo, and both rot silently. A vendored schema validated against a vendored fixture detects nothing: both sides of the comparison are frozen.
Two partial descriptions existed and neither was both authoritative and accurate:
- the service's own OpenAPI typed the envelope but declared
endpoints: unknown[]and omittedproofPlansentirely; tls-app-attest/contracts/provider_template.schema.jsontyped the body but setadditionalProperties: falsewhile the service served three keys it never declared, so it rejected 16 of 51 live providers.
This package is generated from the zod the service validates with, so the types and the served bytes cannot disagree.
Contract rules
- Objects are additive-safe. Unknown keys pass through, so a new upstream field never breaks a consumer. Verified live: the registry went from 51 to 52 providers during development and the schema accepted the new one unchanged.
- Every field a consumer reads is declared. The catchall is for fields we do not interpret, never a hiding place for ones we do.
- SDK-interpreted nested fields are strict.
authremains additive-safe, whileauth.submitSignal[]entries and their request/response rules reject unknown keys, enforce the Expo SDK vocabulary, require unique IDs and canonical HTTPS URLs, cap every trimmed matcher string at 2048 characters, validate JSON-path grammar, and cap the provider at 32 login-cover regexes totaling 8192 characters before publication. - Fail closed on the rollback floor. Reject an artifact whose
registryVersionis belowminimumAllowedRegistryVersion; the floor moves when a provider is disabled, so an older cached artifact can present a revoked provider as active. - Publication is not readiness.
metadata.isSatyaReadyis a curated devtool tag, not a verdict, andtemplateGovernance.providerActivationStateis a separate axis again. A disabled provider remains in the artifact — filter on the field, never on absence.
Contract additions
Next — dynamic host binding
Declares tlsIdentityPolicy.hostGroups, endpoint hostBinding, and
replayCapturedUrl. A pinned-set binding references a named exact-host group; the
Devtool semantic validator (not JSON Schema alone) enforces per-host pins, path and regex
constraints, group limits, and proof-plan host intersection. Consumers must continue to fail
closed on unknown host-binding modes.
0.3.0 — typed auth.submitSignal
Introduced the typed singleton login-submit signal contract. It was superseded by the 0.4.0 array contract before the feature was enabled in the production registry.
Breaking changes
0.4.0 — multiple login submit signals
RawProvider.auth and its presentation-only login submit signals are now typed in
the Zod and JSON Schema contracts. submitSignal is an ordered array of 1–16 entries;
each requires a unique stable ID and canonical HTTPS URL, and supports URL-bound request
rules, request bodyContains/bodyRegex, response bodyContains/bodyRegex, an optional
navigation regex, a 1000–15000 ms timeout, and response-status handling. Unknown direct
auth siblings still pass through for forward compatibility; the SDK-interpreted entries
are strict. Regexes must compile, contain no backreferences, and pass bounded static
backtracking analysis; anchor body regexes and use bodyContains for substring checks.
Adding, editing, or reordering auth changes policyHash because the policy projection
copies it verbatim.
This replaces the 0.3.0 singleton submitSignal object with an array whose entries
require id and url.
0.2.0 — session_stored → sessionStored
The registry served this key snake_case while every other key was camelCase, and
while provider-policy's validation rule declared the camelCase spelling — so
the rule matched nothing and the field was unvalidated on write for as long as
it existed. The wire format was renamed to the spelling the rule, this schema's
docs, and the verifier's policy-hash projection all already used.
Consumers reading provider.session_stored must read provider.sessionStored.
It is not dual-served: the old spelling is rejected on write, so an artifact
published after the migration carries only the new one.
The field is part of policyHash, so affected providers' hashes changed with
the rename. Verify against a freshly fetched artifact rather than a cached one.
Do not edit src/schema.ts
It is generated. Edit
backend/src/schemas/provider-registry.schema.ts in this repo and run:
npm run generate:types-packageCI regenerates and fails on a dirty tree, and a scheduled job validates the live artifact against this schema.
The generated dist/schema.json is a structural interchange schema. Publishing must use
RawProviderSchema and the Devtool policy validator as the authoritative semantic gate for
refinements JSON Schema cannot preserve or express fully, including canonical login-submit
URLs, URL-rule binding, unique signal IDs, safe regular expressions, and well-formed Unicode.
