Repository navigation
v0.2.1 — P0 auth contract hardening; auth-provider image rename
A patch number for work that includes contract hardening, deliberately: while
the version is 0.x this file already states that even minor releases may
change the public API, so patch-vs-minor carries no compatibility promise here.
What the number does buy is a stable consumer pin — provin.oss's quickstart
tracks the moving v0.2 tag, and cutting v0.3 would have forced that pin to
move for no benefit before the first announcement.
Changed
-
The published auth-provider image is
ghcr.io/provin-line/auth-provider, no
longerauth-auth-provider. That name came from concatenating anauth-
prefix onto a component already calledauth-provider— it said nothing
twice.Breaking for anyone pinning the old name, done now precisely because
nobody can be: the packages became public shortly before the rename and no
release had been announced. GHCR has no rename and no alias, so the operation
is publish-the-right-name and delete-the-wrong-one. -
One component name now derives every published identifier, with each
namespace adding only the prefix its own context does not already supply:component provider | policy-verifier directory packages/create-${c} inside this repo — "auth" is given by where the file is npm create-auth-${c} scope is provin-line, not auth image auth-${c} org is provin-line, not authSo directories lose a prefix their location already implies
(create-auth-provider→create-provider,auth-provider-did→
provider-did,auth-provider-dplaax-module→provider-dplaax-module) and
two npm names gain one that was missing
(create-auth-policy-verifier,auth-policy-verifier-dplaax-module). This
deliberately breaks the directory↔package-name mirror; pnpm resolves by the
nameinpackage.json.Every package is unpublished on npm, which is the only reason this is a
refactor rather than a breaking change.The same conflation was live inside both generators, and there it was not
cosmetic: one list served as both npm names and directory paths, so after the
rename a generated scaffold would have carried a git spec pointing at a
packages/path that no longer exists — an install failure, not a cosmetic
oddity.
P0 auth contract work (dplaax.spec): hardens DID resolution, tightens
verification-method selection, adds an OWNER-path login transcript
(validated but not yet wired in), a pluggable nonce store, six signed token
claims with an explicit outcome split, and a conformance vector runner. See
README.md § P0 Auth Contract for the full,
accuracy-checked surface — including what does not work yet (OWNER
contracts are fail-closed).
Added
- Resolver hardening for
DplaaxDidResolver's DID-document fetch: a finite
timeout and response-body cap enforced while streaming (never after full
buffering), bounded concurrency via a shared semaphore, strict RFC 8259
JSON decoding (rejects duplicate object keys and trailing data; preserves
unknown members — including__proto__, kept as an own property, not a
prototype write — rather than stripping them), byte-exactidequality
against the requested DID, and redirect refusal (redirect: "error") so
the serving connection is always the requested URL, never a redirect
target. Rules:auth.resolve.resource-floor,auth.resolve.id-equality,
auth.resolve.unknown-member,auth.resolve.origin-pin,
auth.resolve.failure-mapping. - Verification-method selection hardening in
@provin-line/auth-provider-did:
exact-idmatch with a controller check for the OWNER path, duplicate
verificationMethodid rejection on every path, and a relationship-array
check that trusts only string references — an embedded object whose
idhappens to match is rejected rather than silently accepted, closing
a path that could otherwise smuggle a method intoauthentication
inline. JWS-based verifiers (ed25519_jws,es256_jws,es256k_jws) now
surface the protected header'skidand the signed payload's
verification_methodonParsedMessage. Rules:auth.grant.exact-method,
auth.method.relationship,auth.method.string-reference-only,
auth.forky.authentication-login. - A versioned OWNER-path login transcript (
login-transcript-v1) and its
validator,validateOwnerLogin: binds all ten transcript fields, checks
the transcript's audience against the configured allowlist, enforces a
domain-separation tag so a transcript signed for a different purpose
cannot be replayed as a login, and enforces the three-way match between
the JWSkid, the transcript'sverification_method, and the
resolver-selected method id. Built and unit-tested, but not yet called
from the DID grant's request handler — see "OWNER contract fail-closed
stopgap" under Security, below. Rules:auth.transcript.bound-fields,
auth.transcript.audience-required,auth.transcript.domain-separation,
auth.grant.kid-match. - A pluggable
NonceStoreinterface for the DID grant's replay protection,
with an in-memory default (swept periodically,.unref()'d so it never
blocks process shutdown on its own). Deployments needing cross-replica
replay protection can inject a shared-store implementation. - Six signed claims on every minted DID-grant token —
auth_contract_id,
verification_method,did_document_snapshot(sha256:<64-hex>),
lifecycle_state_ref,lifecycle_freshness_ref, and
authorization_scope(alwaysAUTHORIZATION_AT_ISSUANCE_WITH_MAX_AGE@1,
the only scope this package mints) — assembled from a single bound
evaluation input per request instead of being re-derived piecemeal, plus
an explicit outcome split: resolver outage maps to HTTP 503
temporarily_unavailable, resolver/method rejection maps to HTTP 400
invalid_grant, and neither outcome mints a token. Rules:
auth.token.signed-claims,auth.token.issuance-vs-request,
auth.resolve.single-input-binding. - A conformance vector runner (
integration/conformance) that loads
dplaax.spec-shaped vectors and executes them against the real
resolve/grant code paths, fails closed (throws) on a vector naming a rule
with no registered executor rather than silently skipping it, and carries
aDPLAAX_SPEC_DIR-gated drift check against the upstream spec's vector
bytes. Rule:auth.contract.normative-sot.
Security
- The DID grant's audience allowlist, revocation-latency bound, and
LEGACY-contract TTL cap are now fail-closed configuration: an
empty/absentallowedAudiences, a missingrevocationLatencyBoundSec, or
anoauth.accessToken.expiresInthat exceeds either bound now throws at
grant construction instead of silently accepting any audience or an
unbounded token lifetime (previously, an empty/absent audience allowlist
meant "accept any audience"). Selecting anOWNER_*authContractalso
requires an explicitownerMigrationRatified: trueopt-in at the
config-schema level. Rules:auth.token.lifetime-bound,
auth.legacy.did-login,auth.migration.enable-gate. - OWNER contract fail-closed stopgap:
createDidGrantnow throws at
construction time ifauthContractis set to
OWNER_AUTHENTICATION_LOGIN@1orOWNER_ASSERTION_CONTROL_LOGIN@1. The
OWNER validation path (validateOwnerLogin, above) is implemented but not
yet wired into the request handler, which would otherwise mint a token
labeledOWNER_*while only performing LEGACY (relationship-blind)
validation — a token that misrepresents its own assurance level. This
guard closes that gap until the OWNER path is wired into the handler; see
README.md § P0 Auth Contract.