@fun-xyz/fiat-contract 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * fiat-contract — zod schemas
3
+ *
4
+ * zod is the single runtime validator: TypeScript cannot validate at the boundary, and the
5
+ * table (`src/table.ts`) cannot reject a *malformed* envelope — only an illegal *sequence*.
6
+ *
7
+ * Every object schema is `.strict()`: an undeclared key is a contract violation, not extra data.
8
+ *
9
+ * The accepted params/inputs/expects split is enforced structurally here — `params` values are
10
+ * `JsonValue` literals and are *rejected* if they carry a `FieldSpec`-shaped `type`, while `inputs`
11
+ * values must be `FieldSpec` objects. The old double-duty bug is unrepresentable, not discouraged.
12
+ *
13
+ * ## Why every export is annotated `z.ZodType<T>`
14
+ *
15
+ * Left un-annotated, tsc bakes the *structural* zod type into the emitted `.d.ts`
16
+ * (`ZodObject<…, $strip>`, `z.core.$ZodEnum<…>`, object-form `ZodEnum`). Those internals differ
17
+ * between zod majors, so declarations built against zod 4 resolve to errors — or, under the
18
+ * `skipLibCheck: true` that both consumer repos use, silently to `any` — for a consumer on zod 3.
19
+ * Annotating with `z.ZodType<T>` makes the declarations name nothing but `z.ZodType`, which exists
20
+ * with the same 1-argument form in every supported major.
21
+ *
22
+ * The cost: consumers get `.parse` / `.safeParse` / `.optional()`, not `.shape` / `.extend`. That
23
+ * is deliberate — this is a contract to validate against, not a base to compose from.
24
+ *
25
+ * The internal `$`-prefixed consts keep their inferred structural types so the mirror assertions at
26
+ * the bottom of this file still prove the schemas match the hand-written types in `types.ts`.
27
+ * They are module-private, so none of it reaches the published declarations.
28
+ */
29
+ import { z } from 'zod';
30
+ import type { CryptoAmount, FailureReason, FeeLine, FiatEndpoint, FieldSpec, FlowState, FormDescriptor, FormField, FormFieldType, Instructions, InstructionField, Instrument, JsonValue, FiatAmount, OrderRef, OrderStatus, OrderSummary, PollSpec, FiatProvider, Quote, Recovery, Refund, ReportSpec, SelectOption, StatusHistoryEntry, StepResponse, Surface, Transition, TransitionInputs, TransitionParams, Tx } from './types';
31
+ /** The envelope as doc excerpts show it: `provider` elided. Never valid on the wire. */
32
+ export type EnvelopeFragment = Omit<StepResponse, 'provider'> & {
33
+ provider?: FiatProvider;
34
+ };
35
+ export declare const JsonSchema: z.ZodType<JsonValue>;
36
+ export declare const HTTP_VERBS: readonly ["GET", "POST"];
37
+ /** `"POST /fiat/session/verify"`, `"GET /fiat/orders/o_31c"` — the verb rides the string. */
38
+ export declare const ENDPOINT_PATTERN: RegExp;
39
+ export declare const FiatEndpointSchema: z.ZodType<FiatEndpoint>;
40
+ export declare const FiatProviderSchema: z.ZodType<FiatProvider>;
41
+ export declare const OrderStatusSchema: z.ZodType<OrderStatus>;
42
+ export declare const RecoverySchema: z.ZodType<Recovery>;
43
+ export declare const FailureReasonSchema: z.ZodType<FailureReason>;
44
+ export declare const FiatAmountSchema: z.ZodType<FiatAmount>;
45
+ export declare const CryptoAmountSchema: z.ZodType<CryptoAmount>;
46
+ export declare const FeeLineSchema: z.ZodType<FeeLine>;
47
+ export declare const QuoteSchema: z.ZodType<Quote>;
48
+ export declare const InstrumentSchema: z.ZodType<Instrument>;
49
+ export declare const OrderRefSchema: z.ZodType<OrderRef>;
50
+ export declare const OrderSummarySchema: z.ZodType<OrderSummary>;
51
+ export declare const StatusHistoryEntrySchema: z.ZodType<StatusHistoryEntry>;
52
+ export declare const TxSchema: z.ZodType<Tx>;
53
+ export declare const RefundSchema: z.ZodType<Refund>;
54
+ export declare const SurfaceSchema: z.ZodType<Surface>;
55
+ export declare const InstructionFieldSchema: z.ZodType<InstructionField>;
56
+ export declare const InstructionsSchema: z.ZodType<Instructions>;
57
+ export declare const SelectOptionSchema: z.ZodType<SelectOption>;
58
+ export declare const FormFieldTypeSchema: z.ZodType<FormFieldType>;
59
+ export declare const FormFieldSchema: z.ZodType<FormField>;
60
+ export declare const FormDescriptorSchema: z.ZodType<FormDescriptor>;
61
+ export declare const FIELD_SPEC_TYPES: readonly ["TEXT", "DATE", "COUNTRY", "FILE", "SELECT", "HOSTED_LINK", "FORM_VALUES"];
62
+ export declare const FieldSpecSchema: z.ZodType<FieldSpec>;
63
+ /**
64
+ * True when a `params` value is wearing a `FieldSpec`'s clothes — i.e. the exact defect the
65
+ * accepted split exists to kill (a spec object spread into a request body as if it were a value).
66
+ */
67
+ export declare const looksLikeFieldSpec: (value: unknown) => boolean;
68
+ export declare const TransitionParamsSchema: z.ZodType<TransitionParams>;
69
+ export declare const TransitionInputsSchema: z.ZodType<TransitionInputs>;
70
+ export declare const PollSpecSchema: z.ZodType<PollSpec>;
71
+ export declare const ReportSpecSchema: z.ZodType<ReportSpec>;
72
+ export declare const TransitionSchema: z.ZodType<Transition>;
73
+ export declare const AuthChallengeSchema: z.ZodType<Record<string, JsonValue>>;
74
+ /** `Extract` keeps each per-state export pinned to the union member it validates. */
75
+ type State<K extends FlowState['kind'], P = never> = [P] extends [never] ? Extract<FlowState, {
76
+ kind: K;
77
+ }> : Extract<FlowState, {
78
+ kind: K;
79
+ phase: P;
80
+ }>;
81
+ export declare const QuoteStateSchema: z.ZodType<State<'QUOTE'>>;
82
+ export declare const FunAuthStateSchema: z.ZodType<State<'FUN_AUTH'>>;
83
+ export declare const SessionAuthStateSchema: z.ZodType<State<'SESSION_AUTH'>>;
84
+ export declare const KycCaptureStateSchema: z.ZodType<State<'KYC', 'CAPTURE'>>;
85
+ export declare const KycInputRequiredStateSchema: z.ZodType<State<'KYC', 'INPUT_REQUIRED'>>;
86
+ export declare const KycNoActionRequiredStateSchema: z.ZodType<State<'KYC', 'NO_ACTION_REQUIRED'>>;
87
+ export declare const PendingOrderStateSchema: z.ZodType<State<'PENDING_ORDER'>>;
88
+ export declare const BlockedStateSchema: z.ZodType<State<'BLOCKED'>>;
89
+ export declare const PaymentCaptureStateSchema: z.ZodType<State<'PAYMENT', 'CAPTURE'>>;
90
+ export declare const PaymentInstructStateSchema: z.ZodType<State<'PAYMENT', 'INSTRUCT'>>;
91
+ export declare const OrderAwaitingConfirmationStateSchema: z.ZodType<State<'ORDER', 'AWAITING_CONFIRMATION'>>;
92
+ export declare const OrderCreatedStateSchema: z.ZodType<State<'ORDER', 'CREATED'>>;
93
+ export declare const FlowStateSchema: z.ZodType<FlowState>;
94
+ export declare const StepResponseSchema: z.ZodType<StepResponse>;
95
+ export declare const StepResponseFragmentSchema: z.ZodType<EnvelopeFragment>;
96
+ export declare const EnvelopeSchema: z.ZodType<StepResponse>;
97
+ export {};
98
+ //# sourceMappingURL=schemas.d.ts.map
@@ -0,0 +1,97 @@
1
+ /**
2
+ * fiat-contract — the transition table, as data. **This file IS the published contract.**
3
+ *
4
+ * Per state entry:
5
+ * (a) `allowedTransitions` — the legal transition set the state may carry
6
+ * (b) `mayReturn` — the legal states any call from it may return
7
+ * (source: FE doc §"Event bindings per flow state", column 4 "May receive back")
8
+ * (c) `terminal` — enumerated, never inferred. Empty transitions ≠ terminal: `KYC ON_HOLD`
9
+ * carries `transitions: []` and is NOT terminal.
10
+ *
11
+ * Rules the entries obey:
12
+ * - Terminality of `ORDER{CREATED}` rides `status`, because terminal outcomes are OrderStatus,
13
+ * never kinds (contract §The envelope). The set is enumerated below as
14
+ * `TERMINAL_ORDER_STATUSES` — clients read it, they never derive it.
15
+ * - A terminal *state* carries no transitions. A terminal *status* still can: `FAILED` with a
16
+ * retryable `failureReason` carries the recovery CTA (FE doc Screen 14).
17
+ * - The stale rule means any call may return any state. `mayReturn` is the conformance
18
+ * assertion on the *typical* server sequence — the client never uses it to gate rendering.
19
+ */
20
+ import type { FiatEndpoint, FlowState, OrderStatus, TransitionMode } from './types';
21
+ /**
22
+ * The package version IS the capability handshake's table version (contract §Conformance
23
+ * package → Versioning). Must equal `package.json#version` — asserted in the test suite.
24
+ */
25
+ export declare const TABLE_VERSION = "0.2.1";
26
+ /**
27
+ * State identity = kind + phase + the state's named discriminant (FE doc §Decisions).
28
+ * The table keys on kind + phase (+ `reason` for KYC, whose three reasons differ in terminality
29
+ * and in what they may carry).
30
+ */
31
+ export type StateKey = 'QUOTE' | 'FUN_AUTH' | 'SESSION_AUTH' | 'KYC/CAPTURE' | 'KYC/INPUT_REQUIRED' | 'KYC/NO_ACTION_REQUIRED:IN_REVIEW' | 'KYC/NO_ACTION_REQUIRED:ON_HOLD' | 'KYC/NO_ACTION_REQUIRED:REJECTED' | 'PENDING_ORDER' | 'BLOCKED' | 'PAYMENT/CAPTURE' | 'PAYMENT/INSTRUCT' | 'ORDER/AWAITING_CONFIRMATION' | 'ORDER/CREATED';
32
+ /**
33
+ * Terminal per the OrderStatus machine's monotonic ranks (contract §The envelope).
34
+ *
35
+ * `CAPTURE_ORPHANED` is deliberately absent — see the TODO(open-decision) on the value itself.
36
+ * Non-terminal is the safe default for an unknown status: a client that wrongly believes the flow
37
+ * is over stops polling and the order silently strands, whereas one that keeps polling lets the
38
+ * reconciler resolve it. Revisit when the backend publishes its rank map.
39
+ */
40
+ export declare const TERMINAL_ORDER_STATUSES: readonly ["SETTLED", "EXPIRED", "CANCELLED", "FAILED", "REFUNDED"];
41
+ export type TerminalOrderStatus = (typeof TERMINAL_ORDER_STATUSES)[number];
42
+ /**
43
+ * Endpoint template as written in the docs — `:id` stands for an order id, so
44
+ * `"POST /fiat/orders/:id/cancel"` matches the emitted `"POST /fiat/orders/o_31c/cancel"`.
45
+ */
46
+ export type EndpointTemplate = FiatEndpoint;
47
+ export interface AllowedTransition {
48
+ /**
49
+ * The id observed in the docs' envelopes. Informational: ids are localization keys and the
50
+ * docs explicitly allow unknown ids (the client falls back to `labelFallback`), so
51
+ * `assertLegalEmission` matches on `mode` + `endpoint`, not on `id`.
52
+ */
53
+ id: string;
54
+ mode: TransitionMode;
55
+ endpoint: EndpointTemplate;
56
+ /** When this entry is emitted, if it is conditional. */
57
+ when?: string;
58
+ note?: string;
59
+ }
60
+ export interface TableEntry {
61
+ key: StateKey;
62
+ kind: FlowState['kind'];
63
+ phase?: string;
64
+ /** KYC `NO_ACTION_REQUIRED` only. */
65
+ reason?: 'IN_REVIEW' | 'ON_HOLD' | 'REJECTED';
66
+ /** FE doc screen numbers that render this state. */
67
+ screens: readonly number[];
68
+ /**
69
+ * `SPECIFIED` — the docs enumerate this state's legal transitions.
70
+ * `UNSPECIFIED` — they do not; `assertLegalEmission` cannot judge emissions here and says so.
71
+ */
72
+ docStatus: 'SPECIFIED' | 'UNSPECIFIED';
73
+ allowedTransitions: readonly AllowedTransition[];
74
+ /** Absent when the docs publish no bindings row for this state. */
75
+ mayReturn?: readonly StateKey[];
76
+ /** Flow-terminal. Enumerated here so no SDK infers it. */
77
+ terminal: boolean;
78
+ /** `ORDER{CREATED}` only: the statuses that end the flow. */
79
+ terminalStatuses?: readonly OrderStatus[];
80
+ notes?: readonly string[];
81
+ }
82
+ export declare const TRANSITION_TABLE: Readonly<Record<StateKey, TableEntry>>;
83
+ export declare const STATE_KEYS: StateKey[];
84
+ /**
85
+ * The `/fiat/*` surface as enumerated in contract §Endpoints. Informational data — emissions are
86
+ * checked against each state's `allowedTransitions`, not against this list.
87
+ */
88
+ export declare const DOCUMENTED_ENDPOINTS: readonly ["GET /fiat/payment-methods", "POST /fiat/quote", "POST /fiat/auth", "POST /fiat/session", "POST /fiat/session/verify", "GET /fiat/kyc", "POST /fiat/kyc/form", "POST /fiat/kyc/document", "POST /fiat/instruments", "POST /fiat/orders", "GET /fiat/orders/:id", "GET /fiat/orders/:id/instructions", "POST /fiat/orders/:id/confirm-payment", "POST /fiat/orders/:id/reference", "POST /fiat/orders/:id/cancel", "POST /fiat/orders/:id/surface-result"];
89
+ /** State → its table key. The one place kind/phase/reason is flattened. */
90
+ export declare function stateKey(state: FlowState): StateKey;
91
+ export declare const tableEntry: (state: FlowState) => TableEntry;
92
+ /**
93
+ * Flow-terminal, read from the table — never inferred from `transitions.length`.
94
+ * `ORDER{CREATED}` is terminal when its status is in the entry's `terminalStatuses`.
95
+ */
96
+ export declare function isTerminal(state: FlowState): boolean;
97
+ //# sourceMappingURL=table.d.ts.map
package/dist/table.js ADDED
@@ -0,0 +1,432 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/table.ts
21
+ var table_exports = {};
22
+ __export(table_exports, {
23
+ DOCUMENTED_ENDPOINTS: () => DOCUMENTED_ENDPOINTS,
24
+ STATE_KEYS: () => STATE_KEYS,
25
+ TABLE_VERSION: () => TABLE_VERSION,
26
+ TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
27
+ TRANSITION_TABLE: () => TRANSITION_TABLE,
28
+ isTerminal: () => isTerminal,
29
+ stateKey: () => stateKey,
30
+ tableEntry: () => tableEntry
31
+ });
32
+ module.exports = __toCommonJS(table_exports);
33
+ var TABLE_VERSION = "0.2.1";
34
+ var TERMINAL_ORDER_STATUSES = [
35
+ "SETTLED",
36
+ "EXPIRED",
37
+ "CANCELLED",
38
+ "FAILED",
39
+ "REFUNDED"
40
+ ];
41
+ var KYC_ANY = [
42
+ "KYC/CAPTURE",
43
+ "KYC/INPUT_REQUIRED",
44
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
45
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
46
+ "KYC/NO_ACTION_REQUIRED:REJECTED"
47
+ ];
48
+ var TRANSITION_TABLE = {
49
+ QUOTE: {
50
+ key: "QUOTE",
51
+ kind: "QUOTE",
52
+ screens: [1],
53
+ docStatus: "SPECIFIED",
54
+ allowedTransitions: [
55
+ {
56
+ id: "continue",
57
+ mode: "SUBMIT",
58
+ endpoint: "POST /fiat/session",
59
+ when: "no valid provider session token (own-tables check)"
60
+ },
61
+ {
62
+ id: "continue",
63
+ mode: "SUBMIT",
64
+ endpoint: "POST /fiat/orders",
65
+ when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
66
+ },
67
+ {
68
+ id: "continue",
69
+ mode: "SUBMIT",
70
+ endpoint: "POST /fiat/auth",
71
+ when: "no ambient Fun identity (non-fomo surfaces only)"
72
+ }
73
+ ],
74
+ mayReturn: [
75
+ "SESSION_AUTH",
76
+ ...KYC_ANY,
77
+ "PENDING_ORDER",
78
+ "ORDER/AWAITING_CONFIRMATION",
79
+ "ORDER/CREATED",
80
+ "BLOCKED"
81
+ ],
82
+ terminal: false,
83
+ notes: [
84
+ "Re-quote on amount change is a client-local event, not a transition.",
85
+ 'If the PENDING_ORDER removal lands, QUOTE additionally returns PAYMENT{INSTRUCT} / ORDER{CREATED} directly. TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER".'
86
+ ]
87
+ },
88
+ FUN_AUTH: {
89
+ key: "FUN_AUTH",
90
+ kind: "FUN_AUTH",
91
+ screens: [],
92
+ docStatus: "UNSPECIFIED",
93
+ allowedTransitions: [],
94
+ terminal: false,
95
+ notes: [
96
+ "TODO(open-decision): shape and transitions owned by the auth spike (contract \xA7Auth & ref binding, item 5).",
97
+ "Never renders in fomo \u2014 identity is ambient, so the FE doc gives it no screen and no bindings row."
98
+ ]
99
+ },
100
+ SESSION_AUTH: {
101
+ key: "SESSION_AUTH",
102
+ kind: "SESSION_AUTH",
103
+ screens: [2],
104
+ docStatus: "SPECIFIED",
105
+ allowedTransitions: [
106
+ { id: "verify", mode: "SUBMIT", endpoint: "POST /fiat/session/verify" }
107
+ ],
108
+ mayReturn: [
109
+ "SESSION_AUTH",
110
+ ...KYC_ANY,
111
+ "ORDER/AWAITING_CONFIRMATION",
112
+ "PENDING_ORDER"
113
+ ],
114
+ terminal: false,
115
+ notes: [
116
+ "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
117
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
118
+ ]
119
+ },
120
+ "KYC/CAPTURE": {
121
+ key: "KYC/CAPTURE",
122
+ kind: "KYC",
123
+ phase: "CAPTURE",
124
+ screens: [8],
125
+ docStatus: "SPECIFIED",
126
+ allowedTransitions: [
127
+ {
128
+ id: "capture",
129
+ mode: "CLIENT_SURFACE",
130
+ endpoint: "GET /fiat/kyc",
131
+ note: 'report target per FE bindings ("POST surface report \u2192 GET /fiat/kyc") and contract Flow C.'
132
+ }
133
+ ],
134
+ mayReturn: [
135
+ "KYC/INPUT_REQUIRED",
136
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
137
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
138
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
139
+ "ORDER/AWAITING_CONFIRMATION"
140
+ ],
141
+ terminal: false,
142
+ notes: [
143
+ "Dropped from v1 (no UK headless-module coverage) \u2014 FE Screen 8 is a tombstone. Kind retained as vocabulary; the handshake (supportedStepKinds) keeps the backend from routing v1 SDKs here."
144
+ ]
145
+ },
146
+ "KYC/INPUT_REQUIRED": {
147
+ key: "KYC/INPUT_REQUIRED",
148
+ kind: "KYC",
149
+ phase: "INPUT_REQUIRED",
150
+ screens: [3],
151
+ docStatus: "SPECIFIED",
152
+ allowedTransitions: [
153
+ { id: "submit_round", mode: "SUBMIT", endpoint: "POST /fiat/kyc/form" }
154
+ ],
155
+ mayReturn: [
156
+ "KYC/INPUT_REQUIRED",
157
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
158
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
159
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
160
+ "ORDER/AWAITING_CONFIRMATION"
161
+ ],
162
+ terminal: false,
163
+ notes: [
164
+ "One POST per round; conditional requirements may open another round.",
165
+ "FILE fields stream to POST /fiat/kyc/document \u2014 an upload path the harness owns, not a transition.",
166
+ "Hosted KYC links arrive as HOSTED_LINK form fields, not as a Surface."
167
+ ]
168
+ },
169
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW": {
170
+ key: "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
171
+ kind: "KYC",
172
+ phase: "NO_ACTION_REQUIRED",
173
+ reason: "IN_REVIEW",
174
+ screens: [9],
175
+ docStatus: "SPECIFIED",
176
+ allowedTransitions: [{ id: "poll", mode: "AWAIT", endpoint: "GET /fiat/kyc" }],
177
+ mayReturn: [
178
+ "KYC/NO_ACTION_REQUIRED:IN_REVIEW",
179
+ "KYC/INPUT_REQUIRED",
180
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD",
181
+ "KYC/NO_ACTION_REQUIRED:REJECTED",
182
+ "ORDER/AWAITING_CONFIRMATION"
183
+ ],
184
+ terminal: false,
185
+ notes: [
186
+ "Documented poll shape: intervalMs 3000, backoffFactor 1.5, maxIntervalMs 10000, giveUpAfterMs 90000.",
187
+ "Pre-order giveUp destination is a design gap (FE doc Screen 9) \u2014 client-local, no contract impact."
188
+ ]
189
+ },
190
+ "KYC/NO_ACTION_REQUIRED:ON_HOLD": {
191
+ key: "KYC/NO_ACTION_REQUIRED:ON_HOLD",
192
+ kind: "KYC",
193
+ phase: "NO_ACTION_REQUIRED",
194
+ reason: "ON_HOLD",
195
+ screens: [10],
196
+ docStatus: "SPECIFIED",
197
+ allowedTransitions: [],
198
+ mayReturn: KYC_ANY,
199
+ terminal: false,
200
+ notes: [
201
+ "THE proof that terminality is not array emptiness: transitions [] and NOT terminal. Re-entry after retryAfter re-derives."
202
+ ]
203
+ },
204
+ "KYC/NO_ACTION_REQUIRED:REJECTED": {
205
+ key: "KYC/NO_ACTION_REQUIRED:REJECTED",
206
+ kind: "KYC",
207
+ phase: "NO_ACTION_REQUIRED",
208
+ reason: "REJECTED",
209
+ screens: [11],
210
+ docStatus: "SPECIFIED",
211
+ allowedTransitions: [
212
+ {
213
+ id: "escalate",
214
+ mode: "SUBMIT",
215
+ endpoint: "GET /fiat/kyc",
216
+ when: "an escalation round is offered",
217
+ note: "TODO(open-decision): placeholder shape \u2014 the FE doc flags that a GET carrying params is not a real request shape, and OQ7 owns which rejections escalate. FE doc open question 7."
218
+ }
219
+ ],
220
+ mayReturn: KYC_ANY,
221
+ terminal: false,
222
+ notes: [
223
+ "Terminal *variant*: failureReason {KYC_REJECTED, retryable: false, recovery: CONTACT_SUPPORT} + transitions [] \u2014 renders in place, never routes through ORDER{CREATED, FAILED}. Because escalation is also legal here, the entry is not flatly terminal.",
224
+ "TODO(open-decision): contract OQ7 \u2014 terminal vs escalating rejections are not enumerated."
225
+ ]
226
+ },
227
+ PENDING_ORDER: {
228
+ key: "PENDING_ORDER",
229
+ kind: "PENDING_ORDER",
230
+ screens: [12],
231
+ docStatus: "SPECIFIED",
232
+ allowedTransitions: [
233
+ { id: "resume", mode: "SUBMIT", endpoint: "GET /fiat/orders/:id" },
234
+ { id: "cancel", mode: "SUBMIT", endpoint: "POST /fiat/orders/:id/cancel" }
235
+ ],
236
+ mayReturn: ["ORDER/CREATED", "QUOTE"],
237
+ terminal: false,
238
+ notes: [
239
+ 'TODO(open-decision): removal proposed (contract \xA7"Proposal: remove PENDING_ORDER"); FE Screen 12 is frozen until it lands. The two-call resume-or-cancel shape is why transitions is an array.',
240
+ "SUBMIT is a user-fired request, not a POST: GET /fiat/orders/:id under SUBMIT is legal."
241
+ ]
242
+ },
243
+ BLOCKED: {
244
+ key: "BLOCKED",
245
+ kind: "BLOCKED",
246
+ screens: [16],
247
+ docStatus: "SPECIFIED",
248
+ allowedTransitions: [],
249
+ mayReturn: [],
250
+ terminal: true,
251
+ notes: [
252
+ "Region/compliance block \u2014 fires from anywhere (contract \xA7Global transitions). retryAfter renders a dated comeback; null = none.",
253
+ "TODO(open-decision): v1 scope assumed yes for FCA/UK \u2014 product confirm (FE doc open question 4)."
254
+ ]
255
+ },
256
+ "PAYMENT/CAPTURE": {
257
+ key: "PAYMENT/CAPTURE",
258
+ kind: "PAYMENT",
259
+ phase: "CAPTURE",
260
+ screens: [5],
261
+ docStatus: "SPECIFIED",
262
+ allowedTransitions: [
263
+ {
264
+ id: "capture",
265
+ mode: "CLIENT_SURFACE",
266
+ endpoint: "POST /fiat/orders",
267
+ when: "capture-then-order topology (Transak headless cards: the component yields requestId, an input to createOrder)",
268
+ note: 'TODO(open-decision): report target per card topology \u2014 Transak ask (contract Flow A: "confirm before freeze").'
269
+ },
270
+ {
271
+ id: "capture",
272
+ mode: "CLIENT_SURFACE",
273
+ endpoint: "POST /fiat/orders/:id/surface-result",
274
+ when: "the order already exists (order-then-capture topology)",
275
+ note: 'Endpoint per contract \xA7Endpoints ("client reports CLIENT_SURFACE outcome; backend reconciles vs webhooks").'
276
+ }
277
+ ],
278
+ mayReturn: ["ORDER/CREATED", "PAYMENT/CAPTURE", "QUOTE"],
279
+ terminal: false,
280
+ notes: [
281
+ "Decline re-entry: same state, fresh surface.session, error {category: PAYMENT_DECLINED, retryable: true, recovery: RETRY_PAYMENT}.",
282
+ "The Surface is carried in state, never in the transition (\u03942, single-carry)."
283
+ ]
284
+ },
285
+ "PAYMENT/INSTRUCT": {
286
+ key: "PAYMENT/INSTRUCT",
287
+ kind: "PAYMENT",
288
+ phase: "INSTRUCT",
289
+ screens: [6],
290
+ docStatus: "SPECIFIED",
291
+ allowedTransitions: [
292
+ {
293
+ id: "confirm_paid",
294
+ mode: "SUBMIT",
295
+ endpoint: "POST /fiat/orders/:id/confirm-payment"
296
+ },
297
+ {
298
+ id: "cancel",
299
+ mode: "SUBMIT",
300
+ endpoint: "POST /fiat/orders/:id/cancel",
301
+ when: "in-wizard cancel placement wins",
302
+ note: "TODO(open-decision): cancel placement \u2014 in-wizard vs an out-of-wizard pending-orders surface (FE doc open questions 1+2). Per-rail cancel eligibility is contract OQ7."
303
+ }
304
+ ],
305
+ mayReturn: ["ORDER/CREATED"],
306
+ terminal: false,
307
+ notes: [
308
+ "No instructions re-issue endpoint exists; expired-instructions recovery is unenumerated. TODO(open-decision): contract OQ7.",
309
+ "Async instructions (PREPARING) are deliberately not in v1 \u2014 Transak returns bank details synchronously."
310
+ ]
311
+ },
312
+ "ORDER/AWAITING_CONFIRMATION": {
313
+ key: "ORDER/AWAITING_CONFIRMATION",
314
+ kind: "ORDER",
315
+ phase: "AWAITING_CONFIRMATION",
316
+ screens: [4],
317
+ docStatus: "SPECIFIED",
318
+ allowedTransitions: [{ id: "confirm", mode: "SUBMIT", endpoint: "POST /fiat/orders" }],
319
+ mayReturn: [
320
+ "PAYMENT/CAPTURE",
321
+ "PAYMENT/INSTRUCT",
322
+ "ORDER/CREATED",
323
+ "QUOTE",
324
+ "PENDING_ORDER"
325
+ ],
326
+ terminal: false,
327
+ notes: [
328
+ "Bank rails only \u2014 card rails skip it, the pay sheet was the confirmation.",
329
+ "Carries a refreshed quote: the price may have drifted during KYC."
330
+ ]
331
+ },
332
+ "ORDER/CREATED": {
333
+ key: "ORDER/CREATED",
334
+ kind: "ORDER",
335
+ phase: "CREATED",
336
+ screens: [7, 13, 14, 15],
337
+ docStatus: "SPECIFIED",
338
+ allowedTransitions: [
339
+ {
340
+ id: "poll",
341
+ mode: "AWAIT",
342
+ endpoint: "GET /fiat/orders/:id",
343
+ when: "status is non-terminal",
344
+ note: "Documented giveUpAfterMs 300000 \u2014 on give-up the wizard exits and the durable order takes over."
345
+ },
346
+ {
347
+ id: "retry_payment",
348
+ mode: "SUBMIT",
349
+ endpoint: "POST /fiat/orders",
350
+ when: "failureReason.recovery === RETRY_PAYMENT"
351
+ },
352
+ {
353
+ id: "requote",
354
+ mode: "SUBMIT",
355
+ endpoint: "POST /fiat/quote",
356
+ when: "failureReason.recovery === REQUOTE"
357
+ },
358
+ {
359
+ id: "cancel",
360
+ mode: "SUBMIT",
361
+ endpoint: "POST /fiat/orders/:id/cancel",
362
+ when: "in-wizard cancel placement wins and the rail allows it",
363
+ note: "TODO(open-decision): cancel placement (FE doc open questions 1+2); per-rail eligibility is contract OQ7."
364
+ }
365
+ ],
366
+ mayReturn: ["ORDER/CREATED"],
367
+ terminal: false,
368
+ terminalStatuses: TERMINAL_ORDER_STATUSES,
369
+ notes: [
370
+ "Flow-terminality here rides status \u2208 terminalStatuses, not the state kind and not array emptiness.",
371
+ "A terminal status may still carry a recovery transition: FAILED + retryable failureReason emits it (FE doc Screen 14); non-retryable emits [].",
372
+ "ON_HOLD is a non-terminal status (resolves to PROCESSING or REFUNDED) \u2014 and is NOT the same thing as the KYC ON_HOLD reason; see the note on that field.",
373
+ "CAPTURE_ORPHANED is non-terminal pending the backend rank map (added from review 2026-08-14).",
374
+ "TODO(open-decision): contract OQ7 \u2014 under/over-payment is the biggest unenumerated branch."
375
+ ]
376
+ }
377
+ };
378
+ var STATE_KEYS = Object.keys(TRANSITION_TABLE);
379
+ var DOCUMENTED_ENDPOINTS = [
380
+ "GET /fiat/payment-methods",
381
+ "POST /fiat/quote",
382
+ "POST /fiat/auth",
383
+ "POST /fiat/session",
384
+ "POST /fiat/session/verify",
385
+ "GET /fiat/kyc",
386
+ "POST /fiat/kyc/form",
387
+ "POST /fiat/kyc/document",
388
+ "POST /fiat/instruments",
389
+ "POST /fiat/orders",
390
+ "GET /fiat/orders/:id",
391
+ "GET /fiat/orders/:id/instructions",
392
+ "POST /fiat/orders/:id/confirm-payment",
393
+ "POST /fiat/orders/:id/reference",
394
+ "POST /fiat/orders/:id/cancel",
395
+ "POST /fiat/orders/:id/surface-result"
396
+ ];
397
+ function stateKey(state) {
398
+ switch (state.kind) {
399
+ case "QUOTE":
400
+ return "QUOTE";
401
+ case "FUN_AUTH":
402
+ return "FUN_AUTH";
403
+ case "SESSION_AUTH":
404
+ return "SESSION_AUTH";
405
+ case "PENDING_ORDER":
406
+ return "PENDING_ORDER";
407
+ case "BLOCKED":
408
+ return "BLOCKED";
409
+ case "KYC": {
410
+ if (state.phase === "CAPTURE") return "KYC/CAPTURE";
411
+ if (state.phase === "INPUT_REQUIRED") return "KYC/INPUT_REQUIRED";
412
+ return `KYC/NO_ACTION_REQUIRED:${state.reason}`;
413
+ }
414
+ case "PAYMENT":
415
+ return state.phase === "CAPTURE" ? "PAYMENT/CAPTURE" : "PAYMENT/INSTRUCT";
416
+ case "ORDER":
417
+ return state.phase === "AWAITING_CONFIRMATION" ? "ORDER/AWAITING_CONFIRMATION" : "ORDER/CREATED";
418
+ default: {
419
+ const exhaustive = state;
420
+ throw new Error(`unknown FlowState: ${JSON.stringify(exhaustive)}`);
421
+ }
422
+ }
423
+ }
424
+ var tableEntry = (state) => TRANSITION_TABLE[stateKey(state)];
425
+ function isTerminal(state) {
426
+ const entry = tableEntry(state);
427
+ if (entry.terminalStatuses && state.kind === "ORDER" && state.phase === "CREATED") {
428
+ return entry.terminalStatuses.includes(state.status);
429
+ }
430
+ return entry.terminal;
431
+ }
432
+ //# sourceMappingURL=table.js.map
package/dist/table.mjs ADDED
@@ -0,0 +1,21 @@
1
+ import {
2
+ DOCUMENTED_ENDPOINTS,
3
+ STATE_KEYS,
4
+ TABLE_VERSION,
5
+ TERMINAL_ORDER_STATUSES,
6
+ TRANSITION_TABLE,
7
+ isTerminal,
8
+ stateKey,
9
+ tableEntry
10
+ } from "./chunk-IUYT5BR2.mjs";
11
+ export {
12
+ DOCUMENTED_ENDPOINTS,
13
+ STATE_KEYS,
14
+ TABLE_VERSION,
15
+ TERMINAL_ORDER_STATUSES,
16
+ TRANSITION_TABLE,
17
+ isTerminal,
18
+ stateKey,
19
+ tableEntry
20
+ };
21
+ //# sourceMappingURL=table.mjs.map