Lockspire.Protocol.DcrPolicy
(lockspire v1.0.0)
Copy Markdown
Resolves the effective DCR policy for an inbound RFC 7591 client registration request as the intersection of:
- ServerPolicy DCR allowlists (the operator-configured envelope)
- InitialAccessToken policy_overrides (when an IAT was redeemed; nil otherwise)
- Inbound RFC 7591 client metadata (what the registrant requested)
Intersection-only: the resolver never widens any allowlist. IAT overrides are assumed
already-narrowed to ⊆ server allowlist at IAT-mint time (Phase 28 admin path enforces
this). If an out-of-allowlist override slips through (e.g. policy was tightened after
IAT mint), MapSet.intersection/2 naturally drops it — never widens.
Returns:
{:ok, %Resolved{}} — the effective policy bound to this request
{:error, :invalid_client_metadata, %{field, reason, allowed}} — first inbound value
not in the server allowlist; field names the offending axis (e.g. `:scope`,
`:grant_types`, `:redirect_uri_scheme`, `:redirect_uri_host`,
`:token_endpoint_auth_method`).Note on redirect_uris data carriage: the Resolved substruct exposes the deduplicated
allowed_redirect_uri_schemes and allowed_redirect_uri_hosts axes (the resolver's
job is bound-checking, not data carriage). The validated redirect_uris list itself is
NOT carried on Resolved.t(). Phase 26's intake validator and Phase 27's controller
take the original inbound["redirect_uris"] list directly once the resolver returns
:ok.
Mirrors Lockspire.Protocol.ParPolicy shape (the only existing resolver precedent in
the repo).
Empty allowlist semantics (operator UX hazard)
Migration A defaults every dcr_allowed_* axis to [] (Postgres DEFAULT '{}'). A host
app that runs the migration but never runs an operator-mint script will, by default,
reject every DCR request with {:error, :invalid_client_metadata, %{reason: :not_in_allowlist, allowed: []}}. The empty allowed list tells the
registrant "no values are allowed" — not "the operator hasn't configured DCR yet."
This is intended secure-by-default behaviour (D-06: operator must explicitly populate)
but is operator-confusing if paired with registration_policy != :disabled. Phase 26's
intake validator should detect "registrationpolicy != :disabled AND every
dcr_allowed* is []" and emit a more informative error (reason: :dcr_unconfigured)
to disambiguate from a legitimate per-axis allowlist rejection. Until then, operators
diagnosing "every DCR request returns :not_in_allowlist with allowed: []" should check
the server-policy DCR fields are populated.
Summary
Types
Functions
@spec resolve(Lockspire.Domain.ServerPolicy.t(), map() | keyword() | nil, map()) :: {:ok, struct()} | {:error, :invalid_client_metadata, error_detail()}