Skip to main content

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 named url 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

One RedirectAction: 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’s status 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.