ADR 0015: Publish one redirect action shape
Context
Four schemas carried the same concept — open this provider URL, optionally before a deadline, optionally polling for the outcome. The URL field was namedurl three times and connect_url once; expiry and polling appeared
or vanished per shape; and the approval flavour carried a discriminator
with a single value, which cannot discriminate. The agent-purchase domain
adds a fifth producer, and adding it to four bespoke shapes would deepen
the divergence.
Decision
OneRedirectAction: a kind discriminator whose closed vocabulary names
every producer — kyc_session, card_reveal, grant_approval, connect
— plus url (required), expires_at, and poll_after_ms. Nullability is
decided per field, not inherited from the loosest producer: a null
expires_at means the provider communicated no expiry, true today only of
the KYC producer; a null poll_after_ms means no polling applies.
Some kinds guarantee more, and both the model and the published schema
description say so, so the contract cannot promise less than the service
delivers: card_reveal, grant_approval, and connect actions always
carry an expiry — the connection service refuses an upstream session
without a usable one — and a grant_approval action a positive poll
interval. A guarantee table keyed by kind makes an undecided new kind an
import-time failure. The service refuses to emit an incomplete approval
action as a handled provider error rather than constructing an invalid
one.
PerfloConnectionView keeps status and account_hint as its own fields
and embeds RedirectAction | None — null when there is nothing to open:
the connection is settled, or its pending provider session is no longer
readable. That is the nullable connect_url it replaces.
ConfirmationIntentView.step_up_url stays outside this shape by intent:
it targets the wrapper’s own login route, not a provider hosted page, and
its expiry belongs to the intent rather than the URL.
OperationView.action_required is the grant_approval flavour. The KYC
and reveal session routes return the action directly. One projection
helper validates the hosted URL and builds the shape, with each producer
supplying its own error detail.
A future approval flavour — the agent domain’s spending-session activation
— becomes its own kind, inheriting the approval guarantees only if live
captures show the provider supplies an expiry and poll interval.
Consequences
KycSessionView, RevealSessionView, and PerfloApprovalAction leave the
contract; a test pins that exactly one redirect schema exists. Clients can
branch on kind rather than on which endpoint produced the object, and the
connection dialog reads the action’s url where it read connect_url.
Adding a producer is one new kind member, one guarantee-table entry —
which the import-time check makes mandatory — and one projection site.
Rejected alternatives
Flattening the connection view into a bare redirect: the connection’sstatus and account_hint are facts about the connection, not the
redirect, and a settled connection has no redirect at all.
Inheriting the loosest nullability everywhere: it would erase the expiry
and polling guarantees the reveal and approval flows rely on, and the
service already refuses to emit less.
Keeping the single-value type discriminator and widening it later: the
fifth producer forces the widening anyway; widening once, here, is cheaper
than widening a bespoke shape there.