@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.
- package/README.md +454 -0
- package/dist/assert.d.ts +66 -0
- package/dist/chunk-IUYT5BR2.mjs +412 -0
- package/dist/chunk-KIQOUUEZ.mjs +1 -0
- package/dist/fixtures/data.d.ts +8 -0
- package/dist/fixtures/index.d.ts +55 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1665 -0
- package/dist/index.mjs +1253 -0
- package/dist/schemas.d.ts +98 -0
- package/dist/table.d.ts +97 -0
- package/dist/table.js +432 -0
- package/dist/table.mjs +21 -0
- package/dist/types.d.ts +486 -0
- package/dist/types.js +19 -0
- package/dist/types.mjs +2 -0
- package/package.json +81 -0
- package/table.d.ts +1 -0
- package/table.js +5 -0
- package/types.d.ts +1 -0
- package/types.js +3 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fiat-contract — types
|
|
3
|
+
*
|
|
4
|
+
* The published contract between `fun-backend` (emits envelopes) and
|
|
5
|
+
* `funkit`/`connect-core` (renders them). Zero runtime logic lives here.
|
|
6
|
+
*
|
|
7
|
+
* Sources of truth (do not extend this file from anywhere else):
|
|
8
|
+
* - Contract doc: Fiat Client Contract — Step-Driven Flow & Endpoint Schematics
|
|
9
|
+
* https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc
|
|
10
|
+
* §"The envelope" · §"Conformance package — concrete spec"
|
|
11
|
+
* §"Proposal (2026-08-13): split InputSpec → params + inputs + expects" (ACCEPTED)
|
|
12
|
+
* - FE doc: Fiat Frontend — State Machine & Screen Map
|
|
13
|
+
* https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3
|
|
14
|
+
* §"✅ Decisions" · per-screen "State details" · §"Event bindings per flow state"
|
|
15
|
+
*
|
|
16
|
+
* Litmus rules encoded here:
|
|
17
|
+
* - errors are fields, never states (`error?: FailureReason` on every fallible state)
|
|
18
|
+
* - `params` = server literals · `inputs` = collected FieldSpecs · `expects` = injected surface results
|
|
19
|
+
* - terminality comes from the table (`src/table.ts`), never from `transitions.length`
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* JSON value. `params` carry these — opaque literals the client spreads verbatim.
|
|
23
|
+
* Named `JsonValue` rather than `Json` because this package's surface is re-exported wholesale;
|
|
24
|
+
* a bare `Json` is a generic name to put in a consumer's import scope for no benefit.
|
|
25
|
+
*/
|
|
26
|
+
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
27
|
+
[key: string]: JsonValue;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The harness parses the verb off the endpoint string — SUBMIT is not "POST".
|
|
31
|
+
*
|
|
32
|
+
* Defined locally on purpose: neither the TS lib nor `@types/node` publishes an HTTP-method union
|
|
33
|
+
* (`http.METHODS` is a runtime `string[]`), and the one in `fun-backend`'s `backend-utils`
|
|
34
|
+
* (`GET | PATCH | POST | PUT | DELETE`) is not exported from that package's root — and depending on
|
|
35
|
+
* the backend would invert this package's direction anyway. Deliberately two members, not five: the
|
|
36
|
+
* `/fiat/*` surface uses only these, and a wider union would let the backend emit a verb the
|
|
37
|
+
* harness has no path for.
|
|
38
|
+
*/
|
|
39
|
+
export type HttpVerb = 'GET' | 'POST';
|
|
40
|
+
/**
|
|
41
|
+
* Endpoint strings as emitted: `"POST /fiat/session/verify"`, `"GET /fiat/orders/o_31c"`.
|
|
42
|
+
*
|
|
43
|
+
* A **bounded** union of the documented `/fiat/*` surface (contract §Endpoints), not the open
|
|
44
|
+
* `` `${HttpVerb} /fiat/${string}` `` it started as: bounded catches a typo'd endpoint family
|
|
45
|
+
* (`POST /fiat/orderz`) at compile time, and measured *cheaper* than the open pattern
|
|
46
|
+
* (77,680 vs 77,688 types across this package).
|
|
47
|
+
*
|
|
48
|
+
* On the fun-backend#3984 hazard (unbounded pattern prefixes OOM'd every build at 1.1M types):
|
|
49
|
+
* that blowup came from pattern types keying **mapped types**, where every `keyof` and indexed
|
|
50
|
+
* access re-expands across all members. This type is only ever a field value — never in `keyof`,
|
|
51
|
+
* never a `Record` key, never a mapped-type key — so assignability is a single match per member.
|
|
52
|
+
* Keep it that way: `Record<FiatEndpoint, …>` is the shape that would make this expensive.
|
|
53
|
+
*
|
|
54
|
+
* Per-order paths stay patterns because the id is runtime data (`o_31c`); TS template literals
|
|
55
|
+
* cannot express "no slash", so `GET /fiat/orders/${string}` also admits sub-paths. Precision at
|
|
56
|
+
* that level is the table's job (`allowedTransitions` + `endpointMatches`), not the type's.
|
|
57
|
+
*/
|
|
58
|
+
export type FiatEndpoint = '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/${string}` | `GET /fiat/orders/${string}/instructions` | `POST /fiat/orders/${string}/confirm-payment` | `POST /fiat/orders/${string}/reference` | `POST /fiat/orders/${string}/cancel` | `POST /fiat/orders/${string}/surface-result`;
|
|
59
|
+
/**
|
|
60
|
+
* FiatProvider is metadata: it selects the embedded provider SDK for CLIENT_SURFACE steps and
|
|
61
|
+
* feeds analytics. It never drives sequencing (contract §The envelope).
|
|
62
|
+
* TODO(open-decision): the docs name only Transak as a v1 provider for the headless flow
|
|
63
|
+
* (contract §Decisions 3; Swapped-headless does not exist yet, Banxa is Tradeoff #4).
|
|
64
|
+
* `SWAPPED` added at fun-backend's request (2026-08-17) so its legacy fops payment-provider id
|
|
65
|
+
* has one shared vocabulary instead of a second fun-backend-local enum — it never appears as a
|
|
66
|
+
* `StepResponse.provider` value since Swapped's fops flow doesn't emit this envelope.
|
|
67
|
+
*/
|
|
68
|
+
export type FiatProvider = 'TRANSAK' | 'SWAPPED';
|
|
69
|
+
/**
|
|
70
|
+
* The order status vocabulary. Monotonic ranks, no backwards moves. Terminal outcomes are
|
|
71
|
+
* OrderStatus, never FlowState kinds.
|
|
72
|
+
*
|
|
73
|
+
* **This package is the source of truth** (Charles, review 2026-08-14): `fun-backend` should
|
|
74
|
+
* reference these values for the `fiat_orders` table rather than declaring its own copy, so the
|
|
75
|
+
* enum cannot drift between the wire contract and the database.
|
|
76
|
+
*
|
|
77
|
+
* The enum mirrors the backend's real statuses as closely as possible; presentation mapping is the
|
|
78
|
+
* client's job (Jeremy, same thread) — the FE may collapse several of these into one visual state,
|
|
79
|
+
* but the contract does not pre-collapse them.
|
|
80
|
+
*/
|
|
81
|
+
export type OrderStatus = 'CREATING' | 'AWAITING_PAYMENT' | 'PAYMENT_VERIFYING' | 'PROCESSING' | 'PENDING_DELIVERY' | 'ON_HOLD' | 'CRYPTO_SENT' | 'SETTLED' | 'EXPIRED' | 'CANCELLED' | 'FAILED' | 'REFUNDED'
|
|
82
|
+
/**
|
|
83
|
+
* A capture that never became an order — the pull-rail orphan (contract Flow A's
|
|
84
|
+
* capture-then-order topology: the component yields a `requestId`, then `createOrder` runs).
|
|
85
|
+
*
|
|
86
|
+
* Added from Charles's review (2026-08-14): it exists in the backend's status set and was missing
|
|
87
|
+
* here. Encoded as a first-class value rather than pre-mapped to `PROCESSING`, because the enum
|
|
88
|
+
* should mirror the source of truth and let the client collapse it for display.
|
|
89
|
+
*
|
|
90
|
+
* TODO(open-decision): its semantics, its position in the monotonic ranks, and its recovery are
|
|
91
|
+
* not published in either doc — `domains/fiat` has not landed in `fun-backend` yet. Treated as
|
|
92
|
+
* NON-terminal here (see `TERMINAL_ORDER_STATUSES`), which is the safe default: a client that
|
|
93
|
+
* wrongly thinks the flow is over stops polling, while one that keeps polling lets the reconciler
|
|
94
|
+
* resolve it. Confirm against the backend's rank map when it lands.
|
|
95
|
+
*/
|
|
96
|
+
| 'CAPTURE_ORPHANED';
|
|
97
|
+
export type FailureCategory = 'PAYMENT_DECLINED' | 'KYC_REJECTED' | 'PROVIDER_OUTAGE' | 'USER_ERROR' | 'CANCELLED' | 'EXPIRED';
|
|
98
|
+
/**
|
|
99
|
+
* Recovery affordance the client renders. Contract spelling is `CONTACT_SUPPORT`
|
|
100
|
+
* (the FE doc's Screen 11 note writes `SUPPORT` as shorthand — same value).
|
|
101
|
+
*/
|
|
102
|
+
export type Recovery = 'REQUOTE' | 'RETRY_PAYMENT' | 'CONTACT_SUPPORT' | 'NONE';
|
|
103
|
+
/** Three expiries, three recoveries (contract §The envelope, FailureReason comment). */
|
|
104
|
+
export type ExpiredWhich = 'QUOTE' | 'PAYMENT_WINDOW' | 'INSTRUCTIONS';
|
|
105
|
+
/**
|
|
106
|
+
* `code?` is a copy key (e.g. `BAD_CODE`), not an error code to branch on.
|
|
107
|
+
* TODO(open-decision): contract OQ7 — the failure/partial-payment enumeration is incomplete.
|
|
108
|
+
* This is the taxonomy as published; new categories land with OQ7.
|
|
109
|
+
*/
|
|
110
|
+
export type FailureReason = {
|
|
111
|
+
category: Exclude<FailureCategory, 'EXPIRED'>;
|
|
112
|
+
retryable: boolean;
|
|
113
|
+
recovery: Recovery;
|
|
114
|
+
code?: string;
|
|
115
|
+
} | {
|
|
116
|
+
category: 'EXPIRED';
|
|
117
|
+
which: ExpiredWhich;
|
|
118
|
+
retryable: boolean;
|
|
119
|
+
recovery: Recovery;
|
|
120
|
+
code?: string;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Amounts are decimal strings in the docs' envelopes ("100.00") — never numbers.
|
|
124
|
+
* Named to pair with `CryptoAmount`: every use site is a `fiat:` field.
|
|
125
|
+
*/
|
|
126
|
+
export interface FiatAmount {
|
|
127
|
+
currency: string;
|
|
128
|
+
amount: string;
|
|
129
|
+
}
|
|
130
|
+
export interface CryptoAmount {
|
|
131
|
+
currency: string;
|
|
132
|
+
network: string;
|
|
133
|
+
amount: string;
|
|
134
|
+
}
|
|
135
|
+
/** `id` is the client's localization lookup key; `labelFallback` renders for unknown ids. */
|
|
136
|
+
export interface FeeLine {
|
|
137
|
+
id: string;
|
|
138
|
+
labelFallback: string;
|
|
139
|
+
amount: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `expiresAt` is BE-stamped, not a provider passthrough (contract §Quote semantics).
|
|
143
|
+
* All v1 quotes are estimates as a global fact, not a field.
|
|
144
|
+
* `paymentMethod` is an agnostic category (`card`, `apple_pay`, `bank_transfer`, …) — the docs
|
|
145
|
+
* leave the set open, so it stays `string` rather than a fabricated enum.
|
|
146
|
+
*/
|
|
147
|
+
export interface Quote {
|
|
148
|
+
quoteRef: string;
|
|
149
|
+
fiat: FiatAmount;
|
|
150
|
+
crypto: CryptoAmount;
|
|
151
|
+
rate: string;
|
|
152
|
+
totalFee: string;
|
|
153
|
+
feeBreakdown: FeeLine[];
|
|
154
|
+
paymentMethod: string;
|
|
155
|
+
expiresAt: string;
|
|
156
|
+
}
|
|
157
|
+
/** Deferred to the credit-card iteration (FE doc port ledger) — optional wherever it appears. */
|
|
158
|
+
export interface Instrument {
|
|
159
|
+
brandLabel: string;
|
|
160
|
+
last4: string;
|
|
161
|
+
}
|
|
162
|
+
/** Payload of `PENDING_ORDER` (contract: `OrderRef`). */
|
|
163
|
+
export interface OrderRef {
|
|
164
|
+
orderId: string;
|
|
165
|
+
fiat: FiatAmount;
|
|
166
|
+
crypto: CryptoAmount;
|
|
167
|
+
method: string;
|
|
168
|
+
createdAt: string;
|
|
169
|
+
status: OrderStatus;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Payload of `ORDER{CREATED}` (FE doc Screens 7 · 13 · 14 · 15).
|
|
173
|
+
* `crypto` is optional because the Screen 14/15 envelopes omit it (fiat-only failure/refund views).
|
|
174
|
+
*/
|
|
175
|
+
export interface OrderSummary {
|
|
176
|
+
orderId: string;
|
|
177
|
+
fiat: FiatAmount;
|
|
178
|
+
crypto?: CryptoAmount;
|
|
179
|
+
method: string;
|
|
180
|
+
createdAt: string;
|
|
181
|
+
instrument?: Instrument;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* TODO(open-decision): the element shape of `statusHistory` is not defined in either doc
|
|
185
|
+
* (FE doc Screen 7 lists the key; the FE sketch renders `<StatusTimeline history={…}/>`).
|
|
186
|
+
* Encoding the documented minimum — the status only — rather than inventing timestamps.
|
|
187
|
+
*/
|
|
188
|
+
export interface StatusHistoryEntry {
|
|
189
|
+
status: OrderStatus;
|
|
190
|
+
}
|
|
191
|
+
export interface Tx {
|
|
192
|
+
chainId: number;
|
|
193
|
+
hash: string;
|
|
194
|
+
}
|
|
195
|
+
export interface Refund {
|
|
196
|
+
currency: string;
|
|
197
|
+
amount: string;
|
|
198
|
+
expectedBy: string;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Carried as *data* in CAPTURE phases only (`PAYMENT{CAPTURE}` · `KYC{CAPTURE}`).
|
|
202
|
+
* Δ2: the CLIENT_SURFACE transition carries just `report` — single-carry, no duplication.
|
|
203
|
+
* `session` values are secrets: never log them (FE doc §Decisions, redaction).
|
|
204
|
+
*/
|
|
205
|
+
export type Surface = {
|
|
206
|
+
kind: 'URL_REDIRECT';
|
|
207
|
+
url: string;
|
|
208
|
+
} | {
|
|
209
|
+
kind: 'URL_EMBED';
|
|
210
|
+
url: string;
|
|
211
|
+
} | {
|
|
212
|
+
kind: 'PCI_COMPONENT';
|
|
213
|
+
session: string;
|
|
214
|
+
} | {
|
|
215
|
+
kind: 'PAY_SHEET';
|
|
216
|
+
session: string;
|
|
217
|
+
} | {
|
|
218
|
+
kind: 'ACH_COMPONENT';
|
|
219
|
+
session: string;
|
|
220
|
+
};
|
|
221
|
+
export interface InstructionField {
|
|
222
|
+
id: string;
|
|
223
|
+
labelFallback: string;
|
|
224
|
+
value: string;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Payload of `PAYMENT{INSTRUCT}` — push rails. Values are PII-adjacent: never logged.
|
|
228
|
+
* TODO(open-decision): contract OQ1 lists `QR_IMAGE` as *deliberately not pre-declared* in the
|
|
229
|
+
* v1 vocabulary while §The envelope's Instructions bullet enumerates it. Kept in the union per
|
|
230
|
+
* the envelope section (and the conformance-package spec); handshake-gate it if OQ1 lands the
|
|
231
|
+
* other way.
|
|
232
|
+
* TODO(open-decision): contract OQ7 — expired instructions have no documented recovery and no
|
|
233
|
+
* re-issue endpoint exists.
|
|
234
|
+
*/
|
|
235
|
+
export type Instructions = {
|
|
236
|
+
kind: 'DEEPLINK';
|
|
237
|
+
url: string;
|
|
238
|
+
/** TODO(open-decision): element shape undocumented (contract: "UPI deeplink ×4 app variants"). */
|
|
239
|
+
appVariants: string[];
|
|
240
|
+
} | {
|
|
241
|
+
kind: 'QR_IMAGE';
|
|
242
|
+
url: string;
|
|
243
|
+
} | {
|
|
244
|
+
kind: 'BANK_FIELDS';
|
|
245
|
+
fields: InstructionField[];
|
|
246
|
+
expiresAt?: string;
|
|
247
|
+
} | {
|
|
248
|
+
kind: 'REFERENCE';
|
|
249
|
+
code: string;
|
|
250
|
+
expiresAt: string;
|
|
251
|
+
};
|
|
252
|
+
export interface SelectOption {
|
|
253
|
+
id: string;
|
|
254
|
+
labelFallback: string;
|
|
255
|
+
}
|
|
256
|
+
/** Field types inside a KYC form round. A new field TYPE is a client release (handshake-gated). */
|
|
257
|
+
export type FormFieldType = 'TEXT' | 'DATE' | 'SELECT' | 'COUNTRY' | 'FILE' | 'HOSTED_LINK';
|
|
258
|
+
export interface FormField {
|
|
259
|
+
id: string;
|
|
260
|
+
type: FormFieldType;
|
|
261
|
+
labelFallback: string;
|
|
262
|
+
required: boolean;
|
|
263
|
+
regex?: string;
|
|
264
|
+
/** SELECT only. */
|
|
265
|
+
options?: SelectOption[];
|
|
266
|
+
/** HOSTED_LINK only. */
|
|
267
|
+
url?: string;
|
|
268
|
+
}
|
|
269
|
+
/** Payload of `KYC{INPUT_REQUIRED}`. One uniform renderer walks `fields`. */
|
|
270
|
+
export interface FormDescriptor {
|
|
271
|
+
formId: string;
|
|
272
|
+
fields: FormField[];
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* A `FieldSpec` describes something the **client collects**. It never carries a value.
|
|
276
|
+
* `FORM_VALUES` = "collect the values of the form already carried in `state.form`" — the
|
|
277
|
+
* transition never re-carries the descriptor (the double-carry disease Δ2 killed).
|
|
278
|
+
*
|
|
279
|
+
* Note: the contract's illustrative list reads `'TEXT' | 'NUMBER' | …`; the conformance-package
|
|
280
|
+
* spec closes the set at the seven below, so `NUMBER` is not declared here.
|
|
281
|
+
*/
|
|
282
|
+
export type FieldSpec = {
|
|
283
|
+
type: 'TEXT';
|
|
284
|
+
length?: number;
|
|
285
|
+
regex?: string;
|
|
286
|
+
labelFallback?: string;
|
|
287
|
+
} | {
|
|
288
|
+
type: 'DATE';
|
|
289
|
+
labelFallback?: string;
|
|
290
|
+
} | {
|
|
291
|
+
type: 'COUNTRY';
|
|
292
|
+
labelFallback?: string;
|
|
293
|
+
} | {
|
|
294
|
+
type: 'FILE';
|
|
295
|
+
labelFallback?: string;
|
|
296
|
+
} | {
|
|
297
|
+
type: 'SELECT';
|
|
298
|
+
options?: SelectOption[];
|
|
299
|
+
labelFallback?: string;
|
|
300
|
+
} | {
|
|
301
|
+
type: 'HOSTED_LINK';
|
|
302
|
+
url: string;
|
|
303
|
+
labelFallback?: string;
|
|
304
|
+
} | {
|
|
305
|
+
type: 'FORM_VALUES';
|
|
306
|
+
};
|
|
307
|
+
export type FieldSpecType = FieldSpec['type'];
|
|
308
|
+
/** Server literals — spread into the body verbatim, opaque to the client. */
|
|
309
|
+
export type TransitionParams = Record<string, JsonValue>;
|
|
310
|
+
/** Client-collected specs — the harness renders + locally validates, then merges values in. */
|
|
311
|
+
export type TransitionInputs = Record<string, FieldSpec>;
|
|
312
|
+
export interface PollSpec {
|
|
313
|
+
endpoint: FiatEndpoint;
|
|
314
|
+
params?: TransitionParams;
|
|
315
|
+
intervalMs: number;
|
|
316
|
+
backoffFactor?: number;
|
|
317
|
+
maxIntervalMs?: number;
|
|
318
|
+
giveUpAfterMs?: number;
|
|
319
|
+
}
|
|
320
|
+
export interface ReportSpec {
|
|
321
|
+
endpoint: FiatEndpoint;
|
|
322
|
+
params?: TransitionParams;
|
|
323
|
+
/**
|
|
324
|
+
* Names the harness copies out of the surface result into the body (e.g. `["requestId"]`).
|
|
325
|
+
* Declared so conformance can assert the surface actually yields them.
|
|
326
|
+
*/
|
|
327
|
+
expects?: string[];
|
|
328
|
+
}
|
|
329
|
+
export type TransitionMode = 'SUBMIT' | 'AWAIT' | 'CLIENT_SURFACE';
|
|
330
|
+
/**
|
|
331
|
+
* Body assembly, harness-owned, one rule:
|
|
332
|
+
* `body = {…params, …collected(inputs), …injected(expects)}`
|
|
333
|
+
* A key collision across the three sources is a contract violation (see `assertLegalEmission`),
|
|
334
|
+
* never last-write-wins.
|
|
335
|
+
*
|
|
336
|
+
* `id` keys localization; `labelFallback` renders for ids the client doesn't recognize and rides
|
|
337
|
+
* SUBMIT entries only — the user-visible ones.
|
|
338
|
+
*/
|
|
339
|
+
export type Transition = {
|
|
340
|
+
id: string;
|
|
341
|
+
mode: 'SUBMIT';
|
|
342
|
+
endpoint: FiatEndpoint;
|
|
343
|
+
params?: TransitionParams;
|
|
344
|
+
inputs?: TransitionInputs;
|
|
345
|
+
labelFallback?: string;
|
|
346
|
+
} | {
|
|
347
|
+
id: string;
|
|
348
|
+
mode: 'AWAIT';
|
|
349
|
+
poll: PollSpec;
|
|
350
|
+
} | {
|
|
351
|
+
id: string;
|
|
352
|
+
mode: 'CLIENT_SURFACE';
|
|
353
|
+
report: ReportSpec;
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* TODO(open-decision): shape owned by the auth spike (contract §Auth & ref binding, item 5).
|
|
357
|
+
* `FUN_AUTH` never renders in fomo (identity is ambient), so nothing in either doc constrains
|
|
358
|
+
* this beyond the key name. Charles's review (2026-08-14) questions whether the kind is needed at
|
|
359
|
+
* all, pending the Transak Auth Reliance answer — if reliance lands, provider-side session auth is
|
|
360
|
+
* unreachable for fomo users and this kind may go with it. Kept for now: removing a kind is
|
|
361
|
+
* cheap, adding one back costs a client release (handshake-gated).
|
|
362
|
+
*/
|
|
363
|
+
export type AuthChallenge = Record<string, JsonValue>;
|
|
364
|
+
/**
|
|
365
|
+
* The semantic state. Three rules keep this union honest:
|
|
366
|
+
* 1. sub-states are payload discriminants, never new kinds
|
|
367
|
+
* 2. terminal outcomes are OrderStatus, never kinds
|
|
368
|
+
* 3. every fallible state carries `error?: FailureReason`
|
|
369
|
+
*/
|
|
370
|
+
export type FlowState = {
|
|
371
|
+
kind: 'QUOTE';
|
|
372
|
+
quote: Quote;
|
|
373
|
+
error?: FailureReason;
|
|
374
|
+
} | {
|
|
375
|
+
kind: 'FUN_AUTH';
|
|
376
|
+
challenge: AuthChallenge;
|
|
377
|
+
error?: FailureReason;
|
|
378
|
+
} | {
|
|
379
|
+
kind: 'SESSION_AUTH';
|
|
380
|
+
channel: 'EMAIL_OTP';
|
|
381
|
+
error?: FailureReason;
|
|
382
|
+
}
|
|
383
|
+
/** Dropped from v1 (no UK module coverage) — kind retained, handshake-gated. */
|
|
384
|
+
| {
|
|
385
|
+
kind: 'KYC';
|
|
386
|
+
phase: 'CAPTURE';
|
|
387
|
+
surface: Surface;
|
|
388
|
+
error?: FailureReason;
|
|
389
|
+
} | {
|
|
390
|
+
kind: 'KYC';
|
|
391
|
+
phase: 'INPUT_REQUIRED';
|
|
392
|
+
form: FormDescriptor;
|
|
393
|
+
error?: FailureReason;
|
|
394
|
+
} | {
|
|
395
|
+
kind: 'KYC';
|
|
396
|
+
phase: 'NO_ACTION_REQUIRED';
|
|
397
|
+
/**
|
|
398
|
+
* Note the deliberate name collision (Charles's nit, review 2026-08-14): `ON_HOLD` appears
|
|
399
|
+
* here as a KYC reason *and* in `OrderStatus`. They are different things and both names come
|
|
400
|
+
* from their own doc, so neither is renamed:
|
|
401
|
+
* - `KYC{NO_ACTION_REQUIRED, ON_HOLD}` = FCA cooling-off, pre-order, `retryAfter` set,
|
|
402
|
+
* empty transitions, NOT terminal.
|
|
403
|
+
* - `OrderStatus.ON_HOLD` = post-order compliance or amount mismatch, resolves to
|
|
404
|
+
* `PROCESSING` or `REFUNDED`.
|
|
405
|
+
* They cannot be confused in code — different types, different positions in the envelope —
|
|
406
|
+
* but they can be confused in conversation, so say which one you mean.
|
|
407
|
+
*/
|
|
408
|
+
reason: 'IN_REVIEW' | 'ON_HOLD' | 'REJECTED';
|
|
409
|
+
retryAfter?: string;
|
|
410
|
+
/**
|
|
411
|
+
* Terminal REJECTED variant carries this (FE doc Screen 11: `KYC_REJECTED`,
|
|
412
|
+
* `retryable: false`, `recovery: CONTACT_SUPPORT`) and renders in place — it never routes
|
|
413
|
+
* through `ORDER{CREATED, FAILED}`.
|
|
414
|
+
* TODO(open-decision): contract OQ7 — which rejections escalate vs terminate.
|
|
415
|
+
*/
|
|
416
|
+
failureReason?: FailureReason;
|
|
417
|
+
error?: FailureReason;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* TODO(open-decision): removal proposed (contract §"Proposal: remove PENDING_ORDER").
|
|
421
|
+
* Shipped in 0.1.0 so the current shape is representable; FE Screen 12 is frozen until the
|
|
422
|
+
* decision lands. The removal targets the wizard-interstitial *kind*, not the pending-order
|
|
423
|
+
* concept (which may re-land as an out-of-wizard PENDING_ORDERS view).
|
|
424
|
+
*/
|
|
425
|
+
| {
|
|
426
|
+
kind: 'PENDING_ORDER';
|
|
427
|
+
order: OrderRef;
|
|
428
|
+
error?: FailureReason;
|
|
429
|
+
} | {
|
|
430
|
+
kind: 'BLOCKED';
|
|
431
|
+
/**
|
|
432
|
+
* Observed value: `REGION_UNSUPPORTED` (FE doc Screen 16). The contract types this as
|
|
433
|
+
* `string`; no enum is published, so none is invented here.
|
|
434
|
+
*/
|
|
435
|
+
reason: string;
|
|
436
|
+
/** `null` renders "no comeback date" (FE doc Screen 16 emits it explicitly). */
|
|
437
|
+
retryAfter?: string | null;
|
|
438
|
+
error?: FailureReason;
|
|
439
|
+
} | {
|
|
440
|
+
kind: 'PAYMENT';
|
|
441
|
+
phase: 'CAPTURE';
|
|
442
|
+
surface: Surface;
|
|
443
|
+
error?: FailureReason;
|
|
444
|
+
} | {
|
|
445
|
+
kind: 'PAYMENT';
|
|
446
|
+
phase: 'INSTRUCT';
|
|
447
|
+
/**
|
|
448
|
+
* TODO(open-decision): `orderId` placement — envelope sibling (contract §Worked example,
|
|
449
|
+
* steps 4–5) vs inside state (FE doc v0, Screen 6). Both are accepted here; the FE doc's
|
|
450
|
+
* in-state placement is the current shape, `StepResponse.orderId` covers the other.
|
|
451
|
+
*/
|
|
452
|
+
orderId?: string;
|
|
453
|
+
instructions: Instructions;
|
|
454
|
+
error?: FailureReason;
|
|
455
|
+
} | {
|
|
456
|
+
kind: 'ORDER';
|
|
457
|
+
phase: 'AWAITING_CONFIRMATION';
|
|
458
|
+
quote: Quote;
|
|
459
|
+
error?: FailureReason;
|
|
460
|
+
} | {
|
|
461
|
+
kind: 'ORDER';
|
|
462
|
+
phase: 'CREATED';
|
|
463
|
+
/** Absent in the contract's terse worked-example envelopes, present in every FE screen. */
|
|
464
|
+
order?: OrderSummary;
|
|
465
|
+
status: OrderStatus;
|
|
466
|
+
statusHistory?: StatusHistoryEntry[];
|
|
467
|
+
tx?: Tx;
|
|
468
|
+
refund?: Refund;
|
|
469
|
+
failureReason?: FailureReason;
|
|
470
|
+
error?: FailureReason;
|
|
471
|
+
};
|
|
472
|
+
export type FlowStateKind = FlowState['kind'];
|
|
473
|
+
/**
|
|
474
|
+
* Every `/fiat/*` response. `transitions: []` = nothing the wizard can do; whether the *flow*
|
|
475
|
+
* is over is read from the table (`terminal`), never from array emptiness (ON_HOLD is the proof).
|
|
476
|
+
*/
|
|
477
|
+
export interface StepResponse {
|
|
478
|
+
state: FlowState;
|
|
479
|
+
provider: FiatProvider;
|
|
480
|
+
transitions: Transition[];
|
|
481
|
+
/** TODO(open-decision): `orderId` placement — see `PAYMENT{INSTRUCT}.orderId` above. */
|
|
482
|
+
orderId?: string;
|
|
483
|
+
}
|
|
484
|
+
/** The envelope as it appears in the docs' JSON — alias kept because both names are in use. */
|
|
485
|
+
export type Envelope = StepResponse;
|
|
486
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
7
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
8
|
+
for (let key of __getOwnPropNames(from))
|
|
9
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
10
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
11
|
+
}
|
|
12
|
+
return to;
|
|
13
|
+
};
|
|
14
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var types_exports = {};
|
|
18
|
+
module.exports = __toCommonJS(types_exports);
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
package/dist/types.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fun-xyz/fiat-contract",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Published conformance contract for Fun's headless fiat onramp: FlowState/Transition types, zod schemas, the transition table as data, recorded fixtures, assertion helpers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/fun-xyz/fiat-contract.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.mjs",
|
|
19
|
+
"require": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./types": {
|
|
22
|
+
"types": "./dist/types.d.ts",
|
|
23
|
+
"import": "./dist/types.mjs",
|
|
24
|
+
"require": "./dist/types.js"
|
|
25
|
+
},
|
|
26
|
+
"./table": {
|
|
27
|
+
"types": "./dist/table.d.ts",
|
|
28
|
+
"import": "./dist/table.mjs",
|
|
29
|
+
"require": "./dist/table.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"table.js",
|
|
36
|
+
"table.d.ts",
|
|
37
|
+
"types.js",
|
|
38
|
+
"types.d.ts",
|
|
39
|
+
"README.md",
|
|
40
|
+
"!dist/**/*.map"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"generate:fixtures": "node scripts/generate-fixture-data.mjs",
|
|
47
|
+
"typegen": "tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
48
|
+
"build": "rm -rf dist && npm run typegen && node build.mjs",
|
|
49
|
+
"prepublishOnly": "npm run build",
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"check": "npm run typecheck && npm run test"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"zod": "^3.22.0 || ^4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"zod": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@types/node": "^20.19.0",
|
|
64
|
+
"esbuild": "^0.25.0",
|
|
65
|
+
"typescript": "^5.9.3",
|
|
66
|
+
"vitest": "^3.2.4",
|
|
67
|
+
"zod": "^4.4.3"
|
|
68
|
+
},
|
|
69
|
+
"module": "./dist/index.mjs",
|
|
70
|
+
"sideEffects": false,
|
|
71
|
+
"typesVersions": {
|
|
72
|
+
"*": {
|
|
73
|
+
"types": [
|
|
74
|
+
"./dist/types.d.ts"
|
|
75
|
+
],
|
|
76
|
+
"table": [
|
|
77
|
+
"./dist/table.d.ts"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/table.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/table';
|
package/table.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Compat stub: resolvers that do not read `package.json#exports` (Metro before RN 0.79, where
|
|
2
|
+
// `unstable_enablePackageExports` defaults to false) resolve `@fun-xyz/fiat-contract/table` to this
|
|
3
|
+
// file by path instead. Verified: without it, Metro fails with "Unable to resolve module".
|
|
4
|
+
// Anything that does read `exports` never sees this file — it goes straight to dist/.
|
|
5
|
+
module.exports = require('./dist/table.js');
|
package/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/types';
|
package/types.js
ADDED