@delegus/core
v0.3.4
Published
Pure Delegus v0.3 protocol engine: Grant/Proof parsing, canonicalization, checks P1-P21 with budgets (P21/T7) and dependency checks D1-D7 under delegus-base-v3, receipt assembly and signing. The frozen v0.2 profile (delegus-base-v1, P1-P20) is unchanged.
Readme
@delegus/core
The pure Delegus v0.2 protocol engine (spec §15 step 1) and its conformance
suite (step 2). Implements docs/delegus-spec-v0.2-final.md and nothing else.
Zero runtime dependencies; Node ≥ 22 built-ins only (node:crypto Ed25519 and
SHA-256, node:zlib for status-list bitstrings).
Invariants
- No I/O. DID document snapshot, status-list credential snapshot,
profile document, trust-configuration descriptor, and the clock are inputs.
The three service seams (T1–T3 lookup, revocation-state read, replay
SET NX) are injected hooks; core owns the order they run in. - Fail closed. Unknown, missing, ambiguous, or unavailable → DENY. Unknown
header parameters, claims, members and constraint values are rejected at
parse time; unavailable or throwing hooks yield
SERVICE_UNAVAILABLE. - Deterministic. Same inputs + same snapshots + same hook answers → byte-identical receipt (JCS payload, Ed25519).
- Canonical order (§5.4):
P1 → T1 → T2 → T3 → P2 → P3 → P4 → P5 → [status read] → P6 → P7 → P8 … P14 → [replay SET NX] → T5 → P15 … P20.reasonis the firstfalse. Replay is consumed only when P1–P14 are all true; the status read happens only after P3.
Layout
src/
jcs.ts RFC 8785 canonicalization
hash.ts sha256:<64 lowercase hex> over exact bytes (§6.3)
htu.ts §4.3 htu canonical form (RFC 9449 §4.3 + RFC 3986 §6.2.2)
json.ts strict JSON parser (duplicate keys, unsafe integers rejected)
base64url.ts canonical base64url; base58.ts base58btc; time.ts RFC 3339 UTC
did.ts did:web / did:key (Ed25519) syntax and key decoding
jws.ts compact JWS parse/create, Ed25519 sign/verify
grant.ts Grant parse + P1 proof.ts Proof parse + P8
diddoc.ts P2 key lookup status.ts P6 status-list credential
ir.ts the Authority IR: compileGrantV1 (P1 → IR), constraint nodes, capabilityKey
capability.ts §3.3 vocabulary, P15 (Action) and P16–P20 over IR nodes (authorize / authorizeIR)
evaluate.ts P1–P20 with the T-check and internal-operation seams
receipt.ts §6 signing, verification, offline re-verification
memory.ts in-memory hooks and signer for tests/dev
test/
*.test.ts unit tests fixtures/ fixed keys, builders, scenario
vectors/ conformance suite (see test/vectors/README.md)npm run typecheck # tsc --noEmit
npm test # node --test
npm run vectors:update # regenerate test/vectors/cases and setsPublic API
import { evaluate, verify, signReceipt, verifyReceiptSignature, reverifyReceipt } from "@delegus/core";
// Live /verify: evaluate (P/T checks in order) and sign.
const receipt = await verify({
grant, proof, action, // as received; action = parsed JSON body field
relyingParty: "did:web:…", // from the RP API key
evaluatedAt: new Date().toISOString(), // now, recorded as evaluated_at
receiptId: "drc_<ULID>",
proofWindowSeconds: 60, // RP-configurable, 1..300
service: { did: "did:web:dev.delegus.ai", statusListKeys: { "did:web:dev.delegus.ai#status-1": rawPk } },
profile: { document: profileBytes }, // published §5.2 text → profile_hash
trustConfig: { version: "trust_0042", descriptor: descriptorBytes },
hooks: { // ServiceHooks — Postgres/Redis in apps/api
issuer(issuerDid, kid) { … }, // → { registered, domainVerified, keyCompromised, didDocument:{bytes,resolvedAt} } | { registered:false } | "unavailable"
statusList(principalDid, url) { … }, // → { credential, checkedAt } | "unavailable" (read status_ver first)
replay(rpDid, jti, ttlMs) { … }, // → "ok" | "replayed" | "unavailable" (SET … NX PX ttlMs)
},
}, receiptSigner); // { kid: "did:web:<domain>#receipt-<n>", sign(bytes) } — KMS ED25519_SHA_512 RAW compatible
// Offline (§6.4): now = the receipt's signed evaluated_at.
const result = await reverifyReceipt({ receipt, receiptKeys, statusListKeys, grant, proof, action,
evidence: { didDocument, statusListCredential, profileDocument, trustConfigDescriptor }, proofWindowSeconds });Also exported: parseGrant, parseProof, validateAction, authorize,
globMatch, verifyStatusList, findVerificationKey, canonicalize,
sha256Utf8/sha256Bytes, normalizeHtu/isCanonicalHtu, parseJson,
DID and JWS helpers, MemoryService/MemorySigner (and MemoryConsumableService, which adds the in-memory consumption snapshot and atomic spend seams of delegus-base-v2/v3), and the vocabulary
constants in types.ts. Service-side misuse (bad clock, bad receipt id, bad
window, non-string grant/proof) throws TypeError; party inputs never throw.
Receipt shape
As §6.2, with these choices where the spec is silent:
protocol.checksandtrust.checksalways list every check (true | false | "skipped");resultisPASSiff alltrue, elseFAIL.evidence.issuer_resolutionis present once the issuer lookup succeeded;evidence.statusonce the status read returned a credential (valid_fromonly if the credential parsed).request_hashis present whenever the submitted action canonicalizes.- After P3 passes:
principal.verificationis"domain"when T2 is true,"unverified"otherwise;authorityis the Capability that authorized the action, ornullon DENY. - Before P3 passes:
principal,agent,grant_id,authority,expires_atare omitted (§6.4).
Readings of the spec (candidate errata)
Each is the fail-closed reading; none changes the protocol.
htumust be presented in canonical form. §4.3 defines the canonical form and §5.1 gives/verifynothing to comparehtuagainst, so P8 requires the claim to equal its own §4.3 normalization (no query, no fragment, lowercase scheme/host, default port removed, percent-encoding normalized).normalizeHtuimplements the transformation for SDKs.jtialphabet. §4.2 makes 22–64 ASCII characters a MUST and base64url a SHOULD; the engine requires the base64url alphabet, the only verifiable proxy for the ≥128-bit MUST.- Unknown constraint keys pass P1 and fail P17 (so P17 is reachable);
unknown Capability
actionvalues, unknown JOSE header parameters, unknown top-level claims and unknown object members fail P1. P17 evaluates the candidate Capabilities (those whoseactionequalsaction.type), not the whole Grant. merchantCategoryhas no counterpart in the §4.3 action wire fields and cannot be evaluated; it is treated as unknown at P17.api:callmethod mismatch has no reason code;methodsis applied together withresourcesat P20 (RESOURCE_NOT_AUTHORIZED). P18 and P19 are vacuouslytrueforapi:call.- Prerequisites for "skipped". T2, T3 and P2 need T1; P3 needs P2; P4–P20 need P3 (nothing about a Grant is evaluated until it is authentic); P9–P14 need P8; T5 needs P1–P14; P15 needs P14; P16–P20 chain.
SERVICE_UNAVAILABLEat the status read is reported as P6falsewith that reason (§7), mirroring T5's two reasons; an unavailable registry is T1falsewith that reason.- Status-list credential shape (P6): VC 2.0
vc+jwtsigned by a Delegusdid:web:<domain>#<kid>key,idequal to the Grant'sstatusListCredential,credentialSubject.type BitstringStatusList,statusPurpose revocation,encodedList=u+ base64url(GZIP), bit index MSB-first,validFrom ≤ now, optionalvalidUntil > now. An index outside the list is not "0" (P6 false). - DID document profile (P2):
idequals the issuer; ≤ 5verificationMethodentries; exactly one withid == kid,type Multikey,controller == issuer, Ed25519publicKeyMultibase.assertionMethodis not required (not in the spec). - Times. RFC 3339 UTC
…Zonly, at most millisecond precision;nbf/expmust equalvalidFrom/validUntilexactly (integer seconds).evaluated_atcarries milliseconds; P13 compares in ms. - Amounts.
amountandmaxAmountare non-negative safe integers. A float action canonicalizes per RFC 8785 (so P14 compares like with like) and is then rejected at P15. - Grant size is the compact JWS length in bytes, ≤ 8192. No size limit is imposed on the Proof (the API bounds the request body).
- The Proof window is not in the receipt;
reverifyReceipttakes it as an input (default 60 s). Recording it in the receipt would make P13 reproducible without out-of-band knowledge. did:websyntax: lowercase DNS name, optional%3Aport, optional path segments.did:keymust be canonical base58btc with the0xed01multicodec prefix. The Proofkidmust equal<iss>#<multibase>.- Empty
audienceparses (P1) and denies at P7; emptyresourcesparses and denies at P20;constraintsis required for both action types (maxAmount+currency;methods).
License
Apache-2.0. See LICENSE. The Delegus service (apps/api) is not part of this package and is not open source.
@delegus/core/testing
A separate entry point with the fixed test identities and builders the core
test suite, @delegus/conformance and @delegus/test share: keys from the
seeds 0x01 to 0x07, did:web:acme.example and did:web:test.delegus.example,
and buildGrant, buildProof, buildStatusList and scenario(). Test use
only: nothing in it is a production identity, and importing @delegus/core
alone never loads it.
Plainly: the published package's ./testing subpath carries the fixed test
keys, private seeds included (the 32-byte seeds 0x01 to 0x07, the same
ones the conformance vectors use). They are public by design and were never
secret. They are never production keys: the production DID document
(did:web:delegus.ai) cannot carry them, and a production verifier trusts no
receipt, status list or Grant signed with them.
