@fun-xyz/fiat-contract 0.4.0 → 0.6.0
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 +53 -49
- package/dist/assert.d.ts +5 -13
- package/dist/{chunk-VLQLWQPK.mjs → chunk-EHYDQU4T.mjs} +5 -2
- package/dist/fixtures/index.d.ts +9 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +50 -58
- package/dist/index.mjs +47 -58
- package/dist/schemas.d.ts +3 -9
- package/dist/table.d.ts +7 -5
- package/dist/table.js +6 -1
- package/dist/table.mjs +1 -1
- package/dist/types.d.ts +47 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# fiat-contract
|
|
2
2
|
|
|
3
|
-
The **published contract** between `fun-backend` (emits
|
|
3
|
+
The **published contract** between `fun-backend` (emits step responses) and `funkit`/`connect-core`
|
|
4
4
|
(renders them). Both repos test against it; neither owns it.
|
|
5
5
|
|
|
6
6
|
Four things, zero runtime logic beyond validation:
|
|
7
7
|
|
|
8
8
|
| File | What it is |
|
|
9
9
|
| --- | --- |
|
|
10
|
-
| `src/types.ts` | `FlowState` · `Transition` · `FailureReason` · `Surface` · `Instructions` · `FormDescriptor` · `OrderStatus` · `
|
|
10
|
+
| `src/types.ts` | `FlowState` · `Transition` · `FailureReason` · `Surface` · `Instructions` · `FormDescriptor` · `OrderStatus` · `FiatStepResponse` |
|
|
11
11
|
| `src/schemas.ts` | zod mirrors of every type — the single runtime validator |
|
|
12
12
|
| `src/table.ts` | the transition table **as data**: per state, its legal transition set, the states any call from it may return, and `terminal: boolean` |
|
|
13
|
-
| `src/assert.ts` + `src/fixtures/` | `
|
|
13
|
+
| `src/assert.ts` + `src/fixtures/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 16 recorded step responses (inlined as data — no filesystem, so React Native can bundle it) |
|
|
14
14
|
|
|
15
15
|
## Three entry points — production vs test-time
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ Metro before RN 0.79 ignores `exports` entirely — then greps the emitted bundl
|
|
|
29
29
|
schemas and executes it.
|
|
30
30
|
|
|
31
31
|
Source of truth: [Fiat Client Contract](https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc)
|
|
32
|
-
(§The
|
|
32
|
+
(§The fiat step response · §Conformance package · §split `InputSpec` — ACCEPTED) and
|
|
33
33
|
[Fiat Frontend — State Machine & Screen Map](https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3)
|
|
34
34
|
(✅ Decisions · per-screen State details · Event bindings per flow state).
|
|
35
35
|
Tracking: [Headless Fiat Onramp](https://linear.app/funxyz/project/headless-fiat-onramp-0147f4102399) ·
|
|
@@ -39,7 +39,7 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
|
|
|
39
39
|
|
|
40
40
|
1. **Errors are fields, never states.** Every fallible state carries `error?: FailureReason`.
|
|
41
41
|
Stay-on-screen ⇒ an error field. Change-screen ⇒ a different state (session expiry just returns
|
|
42
|
-
a `SESSION_AUTH`
|
|
42
|
+
a `SESSION_AUTH` step response — there is no error routing table).
|
|
43
43
|
2. **`params` = server literals · `inputs` = collected specs · `expects` = injected surface
|
|
44
44
|
results.** `body = {…params, …collected(inputs), …injected(expects)}`; a key collision across
|
|
45
45
|
the three is a contract violation, not last-write-wins. zod rejects a `FieldSpec` hiding in
|
|
@@ -55,27 +55,30 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
|
|
|
55
55
|
|
|
56
56
|
## Versioning — the package version IS the table version
|
|
57
57
|
|
|
58
|
-
`TABLE_VERSION` (exported from `src/table.ts`)
|
|
58
|
+
`TABLE_VERSION` (exported from `src/table.ts`) is derived from `package.json#version` at build
|
|
59
|
+
time — a version bump is one edit, in package.json, and the two can never drift. A test asserts
|
|
60
|
+
the wiring still resolves to the package version.
|
|
59
61
|
That version rides the capability handshake (`supportedProviders` / `supportedSurfaces` /
|
|
60
62
|
`supportedStepKinds` + table version), which is how BE↔FE skew stays explicit and designed-for:
|
|
61
63
|
the server never routes a flow into a state kind the installed SDK didn't declare.
|
|
62
64
|
|
|
63
65
|
Starting at **0.1.0**. Bump the minor for additive vocabulary (a new field, a new fixture); bump the
|
|
64
|
-
major when an existing shape changes meaning.
|
|
66
|
+
major when an existing shape changes meaning. Until 1.0 the contract is MVP-beta: renames and
|
|
67
|
+
removals also ride a minor, with no deprecated aliases kept — consumers pin and migrate in one step.
|
|
65
68
|
|
|
66
69
|
## How each repo consumes it
|
|
67
70
|
|
|
68
71
|
**`fun-backend`** — adapters compile their provider's real flow into this state machine.
|
|
69
72
|
|
|
70
73
|
```ts
|
|
71
|
-
import {
|
|
74
|
+
import { assertFiatStepResponse, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';
|
|
72
75
|
|
|
73
|
-
assertLegalEmission(state, transitions);
|
|
74
|
-
|
|
76
|
+
assertLegalEmission(state, transitions); // per emission: adapter conformance
|
|
77
|
+
assertFiatStepResponse(outgoing); // outgoing step-response validation in dev/test
|
|
75
78
|
const owed = walkTable((entry) => entry.allowedTransitions); // what the adapter must emit
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing-
|
|
81
|
+
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing step-response
|
|
79
82
|
validation, and the scheduled provider-sandbox drift run diffed against `src/fixtures`.
|
|
80
83
|
|
|
81
84
|
**`connect-core`** — the harness obeys `transitions` for sequencing and owns rendering.
|
|
@@ -88,7 +91,7 @@ FIXTURES.forEach(({ id }) => renderCold(loadFixture(id))); // stale-rule
|
|
|
88
91
|
```
|
|
89
92
|
|
|
90
93
|
Its own suites: exhaustive `computePage` table-walk, fixture-driven cold-render tests, harness units
|
|
91
|
-
(any-state handling, same-state error re-entry retains form values, one-live-
|
|
94
|
+
(any-state handling, same-state error re-entry retains form values, one-live-response focus gating,
|
|
92
95
|
idempotency-key reuse).
|
|
93
96
|
|
|
94
97
|
## Usage
|
|
@@ -98,13 +101,13 @@ so these examples cannot drift from the API.
|
|
|
98
101
|
|
|
99
102
|
### Production: a backend route handler
|
|
100
103
|
|
|
101
|
-
The backend *builds*
|
|
104
|
+
The backend *builds* step responses, so its production use is entirely compile-time — the types are the
|
|
102
105
|
verification. Runtime assertions stay behind a dev/test guard.
|
|
103
106
|
|
|
104
107
|
```ts
|
|
105
|
-
import type { FlowState,
|
|
108
|
+
import type { FlowState, FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
|
|
106
109
|
|
|
107
|
-
export async function createOrder(quoteRef: string): Promise<
|
|
110
|
+
export async function createOrder(quoteRef: string): Promise<FiatStepResponse> {
|
|
108
111
|
const state: FlowState = {
|
|
109
112
|
kind: 'PAYMENT',
|
|
110
113
|
phase: 'INSTRUCT',
|
|
@@ -137,19 +140,19 @@ production-safe: types erase, `./table` is 13.8 KB with no zod.
|
|
|
137
140
|
|
|
138
141
|
```ts
|
|
139
142
|
import { isTerminal, stateKey } from '@fun-xyz/fiat-contract/table';
|
|
140
|
-
import type {
|
|
143
|
+
import type { FiatStepResponse } from '@fun-xyz/fiat-contract/types';
|
|
141
144
|
|
|
142
|
-
export function FiatScreen({
|
|
143
|
-
const page = computePage(stateKey(
|
|
144
|
-
const done = isTerminal(
|
|
145
|
+
export function FiatScreen({ stepResponse }: { stepResponse: FiatStepResponse }) {
|
|
146
|
+
const page = computePage(stateKey(stepResponse.state), clientLocal); // client owns state → screen
|
|
147
|
+
const done = isTerminal(stepResponse.state); // table data, never inferred
|
|
145
148
|
|
|
146
|
-
if (
|
|
147
|
-
return render(page,
|
|
149
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
150
|
+
return render(page, stepResponse.state.instructions); // narrowed: instructions exists here
|
|
148
151
|
}
|
|
149
|
-
if (
|
|
150
|
-
return render(page, { status:
|
|
152
|
+
if (stepResponse.state.kind === 'ORDER' && stepResponse.state.phase === 'CREATED') {
|
|
153
|
+
return render(page, { status: stepResponse.state.status, done });
|
|
151
154
|
}
|
|
152
|
-
return render(page,
|
|
155
|
+
return render(page, stepResponse.state);
|
|
153
156
|
}
|
|
154
157
|
```
|
|
155
158
|
|
|
@@ -160,13 +163,13 @@ re-derive that rule locally.
|
|
|
160
163
|
### Production: the transitions loop
|
|
161
164
|
|
|
162
165
|
```ts
|
|
163
|
-
import type {
|
|
166
|
+
import type { FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
|
|
164
167
|
|
|
165
|
-
function useTransitions(
|
|
168
|
+
function useTransitions(stepResponse: FiatStepResponse) {
|
|
166
169
|
return {
|
|
167
|
-
ctas:
|
|
168
|
-
poll:
|
|
169
|
-
surface:
|
|
170
|
+
ctas: stepResponse.transitions.filter((t) => t.mode === 'SUBMIT'), // render buttons
|
|
171
|
+
poll: stepResponse.transitions.find((t) => t.mode === 'AWAIT'), // harness schedules
|
|
172
|
+
surface: stepResponse.transitions.find((t) => t.mode === 'CLIENT_SURFACE'), // harness mounts
|
|
170
173
|
};
|
|
171
174
|
}
|
|
172
175
|
|
|
@@ -188,19 +191,20 @@ Screens never inspect the array themselves; they receive `ctas` and bind labels
|
|
|
188
191
|
|
|
189
192
|
These import from the root, which carries zod. Test-time only.
|
|
190
193
|
|
|
191
|
-
### Validate
|
|
194
|
+
### Validate a step response at the boundary
|
|
192
195
|
|
|
193
|
-
`
|
|
194
|
-
problem. Use it on the way out of `fun-backend` (dev/test) and on the way in to
|
|
196
|
+
`assertFiatStepResponse` parses and returns a typed step response, or throws `ContractViolation`
|
|
197
|
+
listing every problem. Use it on the way out of `fun-backend` (dev/test) and on the way in to
|
|
198
|
+
`connect-core` tests.
|
|
195
199
|
|
|
196
200
|
```ts
|
|
197
|
-
import {
|
|
201
|
+
import { assertFiatStepResponse, ContractViolation } from '@fun-xyz/fiat-contract';
|
|
198
202
|
|
|
199
203
|
try {
|
|
200
|
-
const
|
|
201
|
-
// ^?
|
|
202
|
-
if (
|
|
203
|
-
render(
|
|
204
|
+
const stepResponse = assertFiatStepResponse(await res.json());
|
|
205
|
+
// ^? FiatStepResponse — state is a narrowable discriminated union from here on
|
|
206
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
207
|
+
render(stepResponse.state.instructions); // narrowed: instructions exists, quote does not
|
|
204
208
|
}
|
|
205
209
|
} catch (err) {
|
|
206
210
|
if (err instanceof ContractViolation) console.error(err.issues); // ['state.quote: Required', …]
|
|
@@ -261,16 +265,16 @@ TRANSITION_TABLE['PAYMENT/INSTRUCT'].mayReturn; // ['ORDER/CREATED']
|
|
|
261
265
|
|
|
262
266
|
### Render every fixture cold (stale-rule survival)
|
|
263
267
|
|
|
264
|
-
Any call may return any state, so every screen must render from a cold
|
|
265
|
-
context. The fixtures are the FE doc's own
|
|
268
|
+
Any call may return any state, so every screen must render from a cold step response with no
|
|
269
|
+
prior context. The fixtures are the FE doc's own responses, so this is a test against the spec.
|
|
266
270
|
|
|
267
271
|
```ts
|
|
268
272
|
import { FIXTURES, loadFixture, assertFixture } from '@fun-xyz/fiat-contract';
|
|
269
273
|
|
|
270
274
|
FIXTURES.forEach(({ id, stateKey, docRef }) => {
|
|
271
275
|
it(`${id} renders cold (${docRef})`, () => {
|
|
272
|
-
const {
|
|
273
|
-
expect(() => renderCold(
|
|
276
|
+
const { stepResponse } = assertFixture(id); // validated + emission-legality checked
|
|
277
|
+
expect(() => renderCold(stepResponse)).not.toThrow();
|
|
274
278
|
expect(computePage(stateKey)).toBeDefined();
|
|
275
279
|
});
|
|
276
280
|
});
|
|
@@ -280,7 +284,7 @@ loadFixture('screen-10-kyc-on-hold'); // raw JSON, fresh deep copy, `unknown`
|
|
|
280
284
|
|
|
281
285
|
### Use a schema directly
|
|
282
286
|
|
|
283
|
-
All 46 schemas are exported when you need to validate a fragment rather than a whole
|
|
287
|
+
All 46 schemas are exported when you need to validate a fragment rather than a whole step response.
|
|
284
288
|
They are typed `z.ZodType<T>`, so you get `.parse` / `.safeParse` / `.optional()` — not `.shape` or
|
|
285
289
|
`.extend`, deliberately.
|
|
286
290
|
|
|
@@ -313,9 +317,9 @@ pnpm add -D @fun-xyz/fiat-contract
|
|
|
313
317
|
```
|
|
314
318
|
|
|
315
319
|
```ts
|
|
316
|
-
import type {
|
|
317
|
-
import { isTerminal } from '@fun-xyz/fiat-contract/table';
|
|
318
|
-
import {
|
|
320
|
+
import type { FiatStepResponse } from '@fun-xyz/fiat-contract/types'; // production, 0 KB
|
|
321
|
+
import { isTerminal } from '@fun-xyz/fiat-contract/table'; // production, 13.8 KB, no zod
|
|
322
|
+
import { assertFiatStepResponse } from '@fun-xyz/fiat-contract'; // tests only, pulls zod
|
|
319
323
|
```
|
|
320
324
|
|
|
321
325
|
Public npm — no `.npmrc`, no token, no registry config. Releases fire from a `v*` tag
|
|
@@ -431,7 +435,7 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
431
435
|
|
|
432
436
|
| Open item | How 0.1.0 encodes it |
|
|
433
437
|
| --- | --- |
|
|
434
|
-
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional
|
|
438
|
+
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
|
|
435
439
|
| `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
|
|
436
440
|
| `[OQ7]` failure enumeration | The published `FailureReason` taxonomy only. Expired instructions, partial payment, per-rail cancel eligibility, and terminal-vs-escalating rejections are flagged unenumerated. |
|
|
437
441
|
| Screen 11 escalation trigger | Encoded as published (`SUBMIT GET /fiat/kyc` + `params: {tier}`) with the doc's own warning that a GET carrying params is not a real shape. |
|
|
@@ -439,11 +443,11 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
439
443
|
| Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
|
|
440
444
|
| `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
|
|
441
445
|
| `statusHistory` element shape | The documented minimum (`{status}`) — no invented timestamps. |
|
|
442
|
-
| `QR_IMAGE` instruction | In the union per §The
|
|
446
|
+
| `QR_IMAGE` instruction | In the union per §The fiat step response, flagged against OQ1's "deliberately not pre-declared". |
|
|
443
447
|
|
|
444
|
-
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented
|
|
448
|
+
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented step responses:
|
|
445
449
|
`FUN_AUTH` (shape owned by the auth spike) and `KYC/CAPTURE` (Screen 8 tombstone, dropped from v1).
|
|
446
|
-
Every other state has a recorded
|
|
450
|
+
Every other state has a recorded step response.
|
|
447
451
|
|
|
448
452
|
## Not decided here — needs a human
|
|
449
453
|
|
package/dist/assert.d.ts
CHANGED
|
@@ -3,26 +3,21 @@
|
|
|
3
3
|
* exactly two things: validate shapes (zod) and judge emissions against the table.
|
|
4
4
|
*
|
|
5
5
|
* Test *suites* live in the consumer repos next to the code they test:
|
|
6
|
-
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing-
|
|
6
|
+
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing step-response
|
|
7
7
|
* validation in dev/test, the scheduled provider drift run.
|
|
8
8
|
* - `connect-core`: exhaustive `computePage` table-walk, fixture-driven cold-render tests,
|
|
9
9
|
* harness units.
|
|
10
10
|
*/
|
|
11
11
|
import { type EndpointTemplate, type StateKey, type TableEntry } from './table';
|
|
12
12
|
import { type FixtureMeta } from './fixtures/index';
|
|
13
|
-
import type {
|
|
13
|
+
import type { FiatStepResponse, FlowState, Transition } from './types';
|
|
14
14
|
/** Thrown for anything the contract calls a violation — 4xx-class, never a user-facing state. */
|
|
15
15
|
export declare class ContractViolation extends Error {
|
|
16
16
|
readonly issues: readonly string[];
|
|
17
17
|
constructor(message: string, issues?: readonly string[]);
|
|
18
18
|
}
|
|
19
|
-
/** Validate a `/fiat/*` response
|
|
20
|
-
export declare function
|
|
21
|
-
/**
|
|
22
|
-
* Validate an envelope whose `provider` is absent — only doc excerpts look like this
|
|
23
|
-
* (see `src/fixtures`: fixtures marked `complete: false`). Never valid on the wire.
|
|
24
|
-
*/
|
|
25
|
-
export declare function assertEnvelopeFragment(json: unknown): Omit<StepResponse, 'provider'>;
|
|
19
|
+
/** Validate a `/fiat/*` step response. Returns the parsed response; throws on violation. */
|
|
20
|
+
export declare function assertFiatStepResponse(json: unknown): FiatStepResponse;
|
|
26
21
|
/** `"POST /fiat/orders/:id/cancel"` matches `"POST /fiat/orders/o_31c/cancel"`. */
|
|
27
22
|
export declare function endpointMatches(template: EndpointTemplate, actual: string): boolean;
|
|
28
23
|
export interface EmissionCheck {
|
|
@@ -54,10 +49,7 @@ export declare function assertLegalReturn(from: FlowState, to: FlowState): void;
|
|
|
54
49
|
export declare function walkTable<T>(fn: (entry: TableEntry, key: StateKey) => T): T[];
|
|
55
50
|
export interface ValidatedFixture {
|
|
56
51
|
meta: FixtureMeta;
|
|
57
|
-
|
|
58
|
-
envelope: Omit<StepResponse, 'provider'> & {
|
|
59
|
-
provider?: StepResponse['provider'];
|
|
60
|
-
};
|
|
52
|
+
stepResponse: FiatStepResponse;
|
|
61
53
|
}
|
|
62
54
|
/** Load one fixture, validate its shape, and assert its emission is legal for its state. */
|
|
63
55
|
export declare function assertFixture(id: string): ValidatedFixture;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var version = "0.6.0";
|
|
3
|
+
|
|
1
4
|
// src/table.ts
|
|
2
|
-
var TABLE_VERSION =
|
|
5
|
+
var TABLE_VERSION = version;
|
|
3
6
|
var TERMINAL_ORDER_STATUSES = [
|
|
4
7
|
"SETTLED",
|
|
5
8
|
"EXPIRED",
|
|
@@ -409,4 +412,4 @@ export {
|
|
|
409
412
|
tableEntry,
|
|
410
413
|
isTerminal
|
|
411
414
|
};
|
|
412
|
-
//# sourceMappingURL=chunk-
|
|
415
|
+
//# sourceMappingURL=chunk-EHYDQU4T.mjs.map
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — recorded fixtures + loader.
|
|
3
3
|
*
|
|
4
|
-
* Every `.json` file in this directory is
|
|
5
|
-
* doc's per-screen "State details"
|
|
4
|
+
* Every `.json` file in this directory is a step response copied **verbatim** from the docs — the
|
|
5
|
+
* FE doc's per-screen "State details" responses plus the contract doc's worked example. The one
|
|
6
6
|
* exception is `screen-04-order-review.json`, whose payload the doc publishes *by reference*
|
|
7
7
|
* ("quote: <same shape as Screen 1, refreshed>"): its quote is Screen 1's payload verbatim.
|
|
8
8
|
* They are
|
|
9
9
|
* already synthetic (`q_8f2`, `o_31c`, `eyJ…`, `"…"` placeholders); the redaction rule applies to
|
|
10
10
|
* fixtures too, so never replace a fake session token, bank field, or PII value with a real one.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* Every fixture is a full step response (`state` + `provider` + `transitions`). The contract doc's worked
|
|
13
|
+
* example elides `provider` in its prose; the three `worked-example-*` fixtures add
|
|
14
|
+
* `"provider": "TRANSAK"` so they validate as real responses — `provider` is always present on the
|
|
15
|
+
* wire, and the contract has no half-response shape.
|
|
16
16
|
*/
|
|
17
17
|
import type { StateKey } from '../table';
|
|
18
18
|
export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC';
|
|
@@ -20,18 +20,16 @@ export interface FixtureMeta {
|
|
|
20
20
|
id: string;
|
|
21
21
|
file: string;
|
|
22
22
|
source: FixtureSource;
|
|
23
|
-
/** Where in the source doc this
|
|
23
|
+
/** Where in the source doc this step response is published. */
|
|
24
24
|
docRef: string;
|
|
25
25
|
/** FE doc screen number, when the fixture comes from a screen card. */
|
|
26
26
|
screen?: number;
|
|
27
27
|
stateKey: StateKey;
|
|
28
|
-
/** `false` ⇒ the doc excerpt omits `provider`; validate as a fragment. */
|
|
29
|
-
complete: boolean;
|
|
30
28
|
notes?: readonly string[];
|
|
31
29
|
}
|
|
32
30
|
export declare const FIXTURES: readonly FixtureMeta[];
|
|
33
31
|
/**
|
|
34
|
-
* State entries with no published
|
|
32
|
+
* State entries with no published step response to record. Declared so missing coverage is visible
|
|
35
33
|
* instead of silent — the fixture test asserts coverage equals (all state keys − these).
|
|
36
34
|
*/
|
|
37
35
|
export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
@@ -40,7 +38,7 @@ export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
|
40
38
|
}[];
|
|
41
39
|
export declare const fixtureMeta: (id: string) => FixtureMeta;
|
|
42
40
|
/**
|
|
43
|
-
* Raw
|
|
41
|
+
* Raw step response — deliberately `unknown`, so callers validate before use.
|
|
44
42
|
*
|
|
45
43
|
* Reads from the generated `data.ts`: no filesystem, so this works under React Native, in a
|
|
46
44
|
* browser, and in both CJS and ESM output. A fresh deep copy each call, so a consumer mutating a
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fiat-contract — the published contract between `fun-backend` (emits
|
|
2
|
+
* fiat-contract — the published contract between `fun-backend` (emits step responses) and
|
|
3
3
|
* `funkit`/`connect-core` (renders them). Types + zod schemas + the transition table as data +
|
|
4
4
|
* recorded fixtures + assertion helpers. No runtime logic beyond validation and assertion.
|
|
5
5
|
*
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,6 @@ __export(index_exports, {
|
|
|
29
29
|
CryptoAmountSchema: () => CryptoAmountSchema,
|
|
30
30
|
DOCUMENTED_ENDPOINTS: () => DOCUMENTED_ENDPOINTS,
|
|
31
31
|
ENDPOINT_PATTERN: () => ENDPOINT_PATTERN,
|
|
32
|
-
EnvelopeSchema: () => EnvelopeSchema,
|
|
33
32
|
FIAT_CURRENCY_NAMES: () => FIAT_CURRENCY_NAMES,
|
|
34
33
|
FIELD_SPEC_TYPES: () => FIELD_SPEC_TYPES,
|
|
35
34
|
FIXTURES: () => FIXTURES,
|
|
@@ -40,6 +39,7 @@ __export(index_exports, {
|
|
|
40
39
|
FiatCurrencyCode: () => FiatCurrencyCode,
|
|
41
40
|
FiatEndpointSchema: () => FiatEndpointSchema,
|
|
42
41
|
FiatProviderSchema: () => FiatProviderSchema,
|
|
42
|
+
FiatStepResponseSchema: () => FiatStepResponseSchema,
|
|
43
43
|
FieldSpecSchema: () => FieldSpecSchema,
|
|
44
44
|
FlowStateSchema: () => FlowStateSchema,
|
|
45
45
|
FormDescriptorSchema: () => FormDescriptorSchema,
|
|
@@ -73,8 +73,6 @@ __export(index_exports, {
|
|
|
73
73
|
SelectOptionSchema: () => SelectOptionSchema,
|
|
74
74
|
SessionAuthStateSchema: () => SessionAuthStateSchema,
|
|
75
75
|
StatusHistoryEntrySchema: () => StatusHistoryEntrySchema,
|
|
76
|
-
StepResponseFragmentSchema: () => StepResponseFragmentSchema,
|
|
77
|
-
StepResponseSchema: () => StepResponseSchema,
|
|
78
76
|
SurfaceSchema: () => SurfaceSchema,
|
|
79
77
|
TABLE_VERSION: () => TABLE_VERSION,
|
|
80
78
|
TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
|
|
@@ -84,8 +82,7 @@ __export(index_exports, {
|
|
|
84
82
|
TransitionSchema: () => TransitionSchema,
|
|
85
83
|
TxSchema: () => TxSchema,
|
|
86
84
|
assertAllFixtures: () => assertAllFixtures,
|
|
87
|
-
|
|
88
|
-
assertEnvelopeFragment: () => assertEnvelopeFragment,
|
|
85
|
+
assertFiatStepResponse: () => assertFiatStepResponse,
|
|
89
86
|
assertFixture: () => assertFixture,
|
|
90
87
|
assertLegalEmission: () => assertLegalEmission,
|
|
91
88
|
assertLegalReturn: () => assertLegalReturn,
|
|
@@ -560,13 +557,32 @@ var $params = import_zod.z.record(import_zod.z.string(), JsonSchema).superRefine
|
|
|
560
557
|
var TransitionParamsSchema = $params;
|
|
561
558
|
var $inputs = import_zod.z.record(import_zod.z.string(), $fieldSpec);
|
|
562
559
|
var TransitionInputsSchema = $inputs;
|
|
560
|
+
var MAX_DELAY_MS = 2 ** 31 - 1;
|
|
561
|
+
var MIN_INTERVAL_MS = 500;
|
|
563
562
|
var $pollSpec = obj({
|
|
564
563
|
endpoint: $endpoint,
|
|
565
564
|
params: $params.optional(),
|
|
566
|
-
intervalMs: import_zod.z.number(),
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
565
|
+
intervalMs: import_zod.z.number().int().min(MIN_INTERVAL_MS).max(MAX_DELAY_MS),
|
|
566
|
+
// Not `.int()` — 1.5 is the recorded factor and a legitimate one. `.finite()` is load-bearing:
|
|
567
|
+
// zod 3's `z.number()` admits Infinity, and `Infinity >= 1` satisfies `.min(1)` on its own.
|
|
568
|
+
backoffFactor: import_zod.z.number().finite().min(1).optional(),
|
|
569
|
+
maxIntervalMs: import_zod.z.number().int().min(MIN_INTERVAL_MS).max(MAX_DELAY_MS).optional(),
|
|
570
|
+
giveUpAfterMs: import_zod.z.number().int().nonnegative().max(MAX_DELAY_MS).optional()
|
|
571
|
+
}).superRefine((spec, ctx) => {
|
|
572
|
+
if (spec.backoffFactor !== void 0 && spec.maxIntervalMs === void 0) {
|
|
573
|
+
ctx.addIssue({
|
|
574
|
+
code: "custom",
|
|
575
|
+
path: ["maxIntervalMs"],
|
|
576
|
+
message: "maxIntervalMs is required when backoffFactor is present \u2014 an unbounded backoff grows the interval past 2^31-1, where setTimeout truncates the delay to 0 and the poll hot-loops."
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (spec.maxIntervalMs !== void 0 && spec.maxIntervalMs < spec.intervalMs) {
|
|
580
|
+
ctx.addIssue({
|
|
581
|
+
code: "custom",
|
|
582
|
+
path: ["maxIntervalMs"],
|
|
583
|
+
message: `maxIntervalMs (${spec.maxIntervalMs}) is below intervalMs (${spec.intervalMs}) \u2014 a ceiling under the starting interval caps the poll at less than its own first delay.`
|
|
584
|
+
});
|
|
585
|
+
}
|
|
570
586
|
});
|
|
571
587
|
var PollSpecSchema = $pollSpec;
|
|
572
588
|
var $reportSpec = obj({
|
|
@@ -699,18 +715,13 @@ var $stepResponse = obj({
|
|
|
699
715
|
transitions: import_zod.z.array($transition),
|
|
700
716
|
orderId: import_zod.z.string().optional()
|
|
701
717
|
});
|
|
702
|
-
var
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
transitions: import_zod.z.array($transition),
|
|
707
|
-
orderId: import_zod.z.string().optional()
|
|
708
|
-
});
|
|
709
|
-
var StepResponseFragmentSchema = $stepResponseFragment;
|
|
710
|
-
var EnvelopeSchema = StepResponseSchema;
|
|
718
|
+
var FiatStepResponseSchema = $stepResponse;
|
|
719
|
+
|
|
720
|
+
// package.json
|
|
721
|
+
var version = "0.6.0";
|
|
711
722
|
|
|
712
723
|
// src/table.ts
|
|
713
|
-
var TABLE_VERSION =
|
|
724
|
+
var TABLE_VERSION = version;
|
|
714
725
|
var TERMINAL_ORDER_STATUSES = [
|
|
715
726
|
"SETTLED",
|
|
716
727
|
"EXPIRED",
|
|
@@ -1526,6 +1537,7 @@ var FIXTURE_DATA = {
|
|
|
1526
1537
|
"expiresAt": "2026-08-11T21:33:00Z"
|
|
1527
1538
|
}
|
|
1528
1539
|
},
|
|
1540
|
+
"provider": "TRANSAK",
|
|
1529
1541
|
"transitions": [
|
|
1530
1542
|
{
|
|
1531
1543
|
"id": "continue",
|
|
@@ -1547,6 +1559,7 @@ var FIXTURE_DATA = {
|
|
|
1547
1559
|
"kind": "SESSION_AUTH",
|
|
1548
1560
|
"channel": "EMAIL_OTP"
|
|
1549
1561
|
},
|
|
1562
|
+
"provider": "TRANSAK",
|
|
1550
1563
|
"transitions": [
|
|
1551
1564
|
{
|
|
1552
1565
|
"id": "verify",
|
|
@@ -1596,6 +1609,7 @@ var FIXTURE_DATA = {
|
|
|
1596
1609
|
"expiresAt": "\u2026"
|
|
1597
1610
|
}
|
|
1598
1611
|
},
|
|
1612
|
+
"provider": "TRANSAK",
|
|
1599
1613
|
"transitions": [
|
|
1600
1614
|
{
|
|
1601
1615
|
"id": "confirm_paid",
|
|
@@ -1614,8 +1628,7 @@ var FIXTURES = [
|
|
|
1614
1628
|
source: "FE_DOC",
|
|
1615
1629
|
docRef: "Screen 1 \xB7 Quote / review",
|
|
1616
1630
|
screen: 1,
|
|
1617
|
-
stateKey: "QUOTE"
|
|
1618
|
-
complete: true
|
|
1631
|
+
stateKey: "QUOTE"
|
|
1619
1632
|
},
|
|
1620
1633
|
{
|
|
1621
1634
|
id: "screen-02-session-auth",
|
|
@@ -1623,8 +1636,7 @@ var FIXTURES = [
|
|
|
1623
1636
|
source: "FE_DOC",
|
|
1624
1637
|
docRef: "Screen 2 \xB7 Verification code (OTP)",
|
|
1625
1638
|
screen: 2,
|
|
1626
|
-
stateKey: "SESSION_AUTH"
|
|
1627
|
-
complete: true
|
|
1639
|
+
stateKey: "SESSION_AUTH"
|
|
1628
1640
|
},
|
|
1629
1641
|
{
|
|
1630
1642
|
id: "screen-03-kyc-input-required",
|
|
@@ -1633,7 +1645,6 @@ var FIXTURES = [
|
|
|
1633
1645
|
docRef: "Screen 3 \xB7 Identity form (KYC)",
|
|
1634
1646
|
screen: 3,
|
|
1635
1647
|
stateKey: "KYC/INPUT_REQUIRED",
|
|
1636
|
-
complete: true,
|
|
1637
1648
|
notes: ["Carries the FORM_VALUES input spec \u2014 the no-double-carry case of the accepted split."]
|
|
1638
1649
|
},
|
|
1639
1650
|
{
|
|
@@ -1643,7 +1654,6 @@ var FIXTURES = [
|
|
|
1643
1654
|
docRef: "Screen 4 \xB7 Order review (bank rail)",
|
|
1644
1655
|
screen: 4,
|
|
1645
1656
|
stateKey: "ORDER/AWAITING_CONFIRMATION",
|
|
1646
|
-
complete: true,
|
|
1647
1657
|
notes: [
|
|
1648
1658
|
`The screen card publishes this payload by reference \u2014 "quote: <same shape as Screen 1, refreshed>" \u2014 so the quote here is Screen 1's payload verbatim. The numbers are not re-published as refreshed values, so they are unchanged; only the shape is contractual.`
|
|
1649
1659
|
]
|
|
@@ -1655,7 +1665,6 @@ var FIXTURES = [
|
|
|
1655
1665
|
docRef: "Screen 5 \xB7 Card payment",
|
|
1656
1666
|
screen: 5,
|
|
1657
1667
|
stateKey: "PAYMENT/CAPTURE",
|
|
1658
|
-
complete: true,
|
|
1659
1668
|
notes: [
|
|
1660
1669
|
"Capture-then-order topology (report \u2192 POST /fiat/orders, expects requestId).",
|
|
1661
1670
|
"TODO(open-decision): report target per card topology is a Transak ask (contract Flow A)."
|
|
@@ -1667,8 +1676,7 @@ var FIXTURES = [
|
|
|
1667
1676
|
source: "FE_DOC",
|
|
1668
1677
|
docRef: "Screen 9 \xB7 Verifying identity",
|
|
1669
1678
|
screen: 9,
|
|
1670
|
-
stateKey: "KYC/NO_ACTION_REQUIRED:IN_REVIEW"
|
|
1671
|
-
complete: true
|
|
1679
|
+
stateKey: "KYC/NO_ACTION_REQUIRED:IN_REVIEW"
|
|
1672
1680
|
},
|
|
1673
1681
|
{
|
|
1674
1682
|
id: "screen-10-kyc-on-hold",
|
|
@@ -1677,7 +1685,6 @@ var FIXTURES = [
|
|
|
1677
1685
|
docRef: "Screen 10 \xB7 Verification on hold",
|
|
1678
1686
|
screen: 10,
|
|
1679
1687
|
stateKey: "KYC/NO_ACTION_REQUIRED:ON_HOLD",
|
|
1680
|
-
complete: true,
|
|
1681
1688
|
notes: ["transitions: [] and NOT terminal \u2014 the fixture that proves terminality is table data."]
|
|
1682
1689
|
},
|
|
1683
1690
|
{
|
|
@@ -1687,7 +1694,6 @@ var FIXTURES = [
|
|
|
1687
1694
|
docRef: "Screen 11 \xB7 Verification unsuccessful",
|
|
1688
1695
|
screen: 11,
|
|
1689
1696
|
stateKey: "KYC/NO_ACTION_REQUIRED:REJECTED",
|
|
1690
|
-
complete: true,
|
|
1691
1697
|
notes: [
|
|
1692
1698
|
"TODO(open-decision): the escalation transition is a flagged placeholder \u2014 a GET carrying params is not a real request shape (FE doc Screen 11 + open question 7).",
|
|
1693
1699
|
"The terminal REJECTED variant (failureReason + transitions []) is described in prose, not published as JSON \u2014 no fixture."
|
|
@@ -1700,7 +1706,6 @@ var FIXTURES = [
|
|
|
1700
1706
|
docRef: "Screen 12 \xB7 Order in progress",
|
|
1701
1707
|
screen: 12,
|
|
1702
1708
|
stateKey: "PENDING_ORDER",
|
|
1703
|
-
complete: true,
|
|
1704
1709
|
notes: [
|
|
1705
1710
|
"TODO(open-decision): PENDING_ORDER removal proposed; Screen 12 frozen. Kept so 0.1.0 can validate the current shape."
|
|
1706
1711
|
]
|
|
@@ -1711,8 +1716,7 @@ var FIXTURES = [
|
|
|
1711
1716
|
source: "FE_DOC",
|
|
1712
1717
|
docRef: "Screen 13 \xB7 Success",
|
|
1713
1718
|
screen: 13,
|
|
1714
|
-
stateKey: "ORDER/CREATED"
|
|
1715
|
-
complete: true
|
|
1719
|
+
stateKey: "ORDER/CREATED"
|
|
1716
1720
|
},
|
|
1717
1721
|
{
|
|
1718
1722
|
id: "screen-14-order-failed",
|
|
@@ -1721,7 +1725,6 @@ var FIXTURES = [
|
|
|
1721
1725
|
docRef: "Screen 14 \xB7 Something went wrong",
|
|
1722
1726
|
screen: 14,
|
|
1723
1727
|
stateKey: "ORDER/CREATED",
|
|
1724
|
-
complete: true,
|
|
1725
1728
|
notes: ["Terminal status (FAILED) still carrying a recovery transition \u2014 retryable failureReason."]
|
|
1726
1729
|
},
|
|
1727
1730
|
{
|
|
@@ -1730,8 +1733,7 @@ var FIXTURES = [
|
|
|
1730
1733
|
source: "FE_DOC",
|
|
1731
1734
|
docRef: "Screen 15 \xB7 Refund on the way",
|
|
1732
1735
|
screen: 15,
|
|
1733
|
-
stateKey: "ORDER/CREATED"
|
|
1734
|
-
complete: true
|
|
1736
|
+
stateKey: "ORDER/CREATED"
|
|
1735
1737
|
},
|
|
1736
1738
|
{
|
|
1737
1739
|
id: "screen-16-blocked",
|
|
@@ -1740,7 +1742,6 @@ var FIXTURES = [
|
|
|
1740
1742
|
docRef: "Screen 16 \xB7 Not available",
|
|
1741
1743
|
screen: 16,
|
|
1742
1744
|
stateKey: "BLOCKED",
|
|
1743
|
-
complete: true,
|
|
1744
1745
|
notes: ["The only flatly terminal state entry in the table."]
|
|
1745
1746
|
},
|
|
1746
1747
|
{
|
|
@@ -1749,7 +1750,6 @@ var FIXTURES = [
|
|
|
1749
1750
|
source: "CONTRACT_DOC",
|
|
1750
1751
|
docRef: "\xA7Worked example, step 1 (method selection)",
|
|
1751
1752
|
stateKey: "QUOTE",
|
|
1752
|
-
complete: false,
|
|
1753
1753
|
notes: ["Carries inputs {email: {type: TEXT}} \u2014 the collected-spec half of the split."]
|
|
1754
1754
|
},
|
|
1755
1755
|
{
|
|
@@ -1757,8 +1757,7 @@ var FIXTURES = [
|
|
|
1757
1757
|
file: "worked-example-02-session-auth.json",
|
|
1758
1758
|
source: "CONTRACT_DOC",
|
|
1759
1759
|
docRef: "\xA7Worked example, step 2 (session challenge)",
|
|
1760
|
-
stateKey: "SESSION_AUTH"
|
|
1761
|
-
complete: false
|
|
1760
|
+
stateKey: "SESSION_AUTH"
|
|
1762
1761
|
},
|
|
1763
1762
|
{
|
|
1764
1763
|
id: "worked-example-04-payment-instruct",
|
|
@@ -1766,20 +1765,19 @@ var FIXTURES = [
|
|
|
1766
1765
|
source: "CONTRACT_DOC",
|
|
1767
1766
|
docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
|
|
1768
1767
|
stateKey: "PAYMENT/INSTRUCT",
|
|
1769
|
-
complete: false,
|
|
1770
1768
|
notes: [
|
|
1771
|
-
"The
|
|
1769
|
+
"The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
|
|
1772
1770
|
]
|
|
1773
1771
|
}
|
|
1774
1772
|
];
|
|
1775
1773
|
var FIXTURE_COVERAGE_GAPS = [
|
|
1776
1774
|
{
|
|
1777
1775
|
stateKey: "FUN_AUTH",
|
|
1778
|
-
reason: "No
|
|
1776
|
+
reason: "No step response exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
|
|
1779
1777
|
},
|
|
1780
1778
|
{
|
|
1781
1779
|
stateKey: "KYC/CAPTURE",
|
|
1782
|
-
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no
|
|
1780
|
+
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no step response was ever published. Kind retained as handshake-gated vocabulary."
|
|
1783
1781
|
}
|
|
1784
1782
|
];
|
|
1785
1783
|
var fixtureMeta = (id) => {
|
|
@@ -1815,15 +1813,9 @@ var formatIssues = (error) => {
|
|
|
1815
1813
|
return path ? `${path}: ${issue.message ?? "invalid"}` : issue.message ?? "invalid";
|
|
1816
1814
|
});
|
|
1817
1815
|
};
|
|
1818
|
-
function
|
|
1819
|
-
const result =
|
|
1820
|
-
if (!result.success) throw new ContractViolation("invalid
|
|
1821
|
-
return result.data;
|
|
1822
|
-
}
|
|
1823
|
-
function assertEnvelopeFragment(json) {
|
|
1824
|
-
const result = StepResponseFragmentSchema.safeParse(json);
|
|
1825
|
-
if (!result.success)
|
|
1826
|
-
throw new ContractViolation("invalid envelope fragment", formatIssues(result.error));
|
|
1816
|
+
function assertFiatStepResponse(json) {
|
|
1817
|
+
const result = FiatStepResponseSchema.safeParse(json);
|
|
1818
|
+
if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
|
|
1827
1819
|
return result.data;
|
|
1828
1820
|
}
|
|
1829
1821
|
function endpointMatches(template, actual) {
|
|
@@ -1920,14 +1912,14 @@ function walkTable(fn) {
|
|
|
1920
1912
|
function assertFixture(id) {
|
|
1921
1913
|
const meta = fixtureMeta(id);
|
|
1922
1914
|
const json = loadFixture(id);
|
|
1923
|
-
const
|
|
1924
|
-
if (stateKey(
|
|
1915
|
+
const stepResponse = assertFiatStepResponse(json);
|
|
1916
|
+
if (stateKey(stepResponse.state) !== meta.stateKey) {
|
|
1925
1917
|
throw new ContractViolation(
|
|
1926
|
-
`fixture ${id}: manifest says ${meta.stateKey},
|
|
1918
|
+
`fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
|
|
1927
1919
|
);
|
|
1928
1920
|
}
|
|
1929
|
-
assertLegalEmission(
|
|
1930
|
-
return { meta,
|
|
1921
|
+
assertLegalEmission(stepResponse.state, stepResponse.transitions);
|
|
1922
|
+
return { meta, stepResponse };
|
|
1931
1923
|
}
|
|
1932
1924
|
function assertAllFixtures() {
|
|
1933
1925
|
return FIXTURES.map((meta) => assertFixture(meta.id));
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isTerminal,
|
|
9
9
|
stateKey,
|
|
10
10
|
tableEntry
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EHYDQU4T.mjs";
|
|
12
12
|
|
|
13
13
|
// src/codes.ts
|
|
14
14
|
var COUNTRY_CURRENCY_LIST = [
|
|
@@ -468,13 +468,32 @@ var $params = z.record(z.string(), JsonSchema).superRefine((params, ctx) => {
|
|
|
468
468
|
var TransitionParamsSchema = $params;
|
|
469
469
|
var $inputs = z.record(z.string(), $fieldSpec);
|
|
470
470
|
var TransitionInputsSchema = $inputs;
|
|
471
|
+
var MAX_DELAY_MS = 2 ** 31 - 1;
|
|
472
|
+
var MIN_INTERVAL_MS = 500;
|
|
471
473
|
var $pollSpec = obj({
|
|
472
474
|
endpoint: $endpoint,
|
|
473
475
|
params: $params.optional(),
|
|
474
|
-
intervalMs: z.number(),
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
476
|
+
intervalMs: z.number().int().min(MIN_INTERVAL_MS).max(MAX_DELAY_MS),
|
|
477
|
+
// Not `.int()` — 1.5 is the recorded factor and a legitimate one. `.finite()` is load-bearing:
|
|
478
|
+
// zod 3's `z.number()` admits Infinity, and `Infinity >= 1` satisfies `.min(1)` on its own.
|
|
479
|
+
backoffFactor: z.number().finite().min(1).optional(),
|
|
480
|
+
maxIntervalMs: z.number().int().min(MIN_INTERVAL_MS).max(MAX_DELAY_MS).optional(),
|
|
481
|
+
giveUpAfterMs: z.number().int().nonnegative().max(MAX_DELAY_MS).optional()
|
|
482
|
+
}).superRefine((spec, ctx) => {
|
|
483
|
+
if (spec.backoffFactor !== void 0 && spec.maxIntervalMs === void 0) {
|
|
484
|
+
ctx.addIssue({
|
|
485
|
+
code: "custom",
|
|
486
|
+
path: ["maxIntervalMs"],
|
|
487
|
+
message: "maxIntervalMs is required when backoffFactor is present \u2014 an unbounded backoff grows the interval past 2^31-1, where setTimeout truncates the delay to 0 and the poll hot-loops."
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (spec.maxIntervalMs !== void 0 && spec.maxIntervalMs < spec.intervalMs) {
|
|
491
|
+
ctx.addIssue({
|
|
492
|
+
code: "custom",
|
|
493
|
+
path: ["maxIntervalMs"],
|
|
494
|
+
message: `maxIntervalMs (${spec.maxIntervalMs}) is below intervalMs (${spec.intervalMs}) \u2014 a ceiling under the starting interval caps the poll at less than its own first delay.`
|
|
495
|
+
});
|
|
496
|
+
}
|
|
478
497
|
});
|
|
479
498
|
var PollSpecSchema = $pollSpec;
|
|
480
499
|
var $reportSpec = obj({
|
|
@@ -607,15 +626,7 @@ var $stepResponse = obj({
|
|
|
607
626
|
transitions: z.array($transition),
|
|
608
627
|
orderId: z.string().optional()
|
|
609
628
|
});
|
|
610
|
-
var
|
|
611
|
-
var $stepResponseFragment = obj({
|
|
612
|
-
state: $flowState,
|
|
613
|
-
provider: $provider.optional(),
|
|
614
|
-
transitions: z.array($transition),
|
|
615
|
-
orderId: z.string().optional()
|
|
616
|
-
});
|
|
617
|
-
var StepResponseFragmentSchema = $stepResponseFragment;
|
|
618
|
-
var EnvelopeSchema = StepResponseSchema;
|
|
629
|
+
var FiatStepResponseSchema = $stepResponse;
|
|
619
630
|
|
|
620
631
|
// src/fixtures/data.ts
|
|
621
632
|
var FIXTURE_DATA = {
|
|
@@ -1033,6 +1044,7 @@ var FIXTURE_DATA = {
|
|
|
1033
1044
|
"expiresAt": "2026-08-11T21:33:00Z"
|
|
1034
1045
|
}
|
|
1035
1046
|
},
|
|
1047
|
+
"provider": "TRANSAK",
|
|
1036
1048
|
"transitions": [
|
|
1037
1049
|
{
|
|
1038
1050
|
"id": "continue",
|
|
@@ -1054,6 +1066,7 @@ var FIXTURE_DATA = {
|
|
|
1054
1066
|
"kind": "SESSION_AUTH",
|
|
1055
1067
|
"channel": "EMAIL_OTP"
|
|
1056
1068
|
},
|
|
1069
|
+
"provider": "TRANSAK",
|
|
1057
1070
|
"transitions": [
|
|
1058
1071
|
{
|
|
1059
1072
|
"id": "verify",
|
|
@@ -1103,6 +1116,7 @@ var FIXTURE_DATA = {
|
|
|
1103
1116
|
"expiresAt": "\u2026"
|
|
1104
1117
|
}
|
|
1105
1118
|
},
|
|
1119
|
+
"provider": "TRANSAK",
|
|
1106
1120
|
"transitions": [
|
|
1107
1121
|
{
|
|
1108
1122
|
"id": "confirm_paid",
|
|
@@ -1121,8 +1135,7 @@ var FIXTURES = [
|
|
|
1121
1135
|
source: "FE_DOC",
|
|
1122
1136
|
docRef: "Screen 1 \xB7 Quote / review",
|
|
1123
1137
|
screen: 1,
|
|
1124
|
-
stateKey: "QUOTE"
|
|
1125
|
-
complete: true
|
|
1138
|
+
stateKey: "QUOTE"
|
|
1126
1139
|
},
|
|
1127
1140
|
{
|
|
1128
1141
|
id: "screen-02-session-auth",
|
|
@@ -1130,8 +1143,7 @@ var FIXTURES = [
|
|
|
1130
1143
|
source: "FE_DOC",
|
|
1131
1144
|
docRef: "Screen 2 \xB7 Verification code (OTP)",
|
|
1132
1145
|
screen: 2,
|
|
1133
|
-
stateKey: "SESSION_AUTH"
|
|
1134
|
-
complete: true
|
|
1146
|
+
stateKey: "SESSION_AUTH"
|
|
1135
1147
|
},
|
|
1136
1148
|
{
|
|
1137
1149
|
id: "screen-03-kyc-input-required",
|
|
@@ -1140,7 +1152,6 @@ var FIXTURES = [
|
|
|
1140
1152
|
docRef: "Screen 3 \xB7 Identity form (KYC)",
|
|
1141
1153
|
screen: 3,
|
|
1142
1154
|
stateKey: "KYC/INPUT_REQUIRED",
|
|
1143
|
-
complete: true,
|
|
1144
1155
|
notes: ["Carries the FORM_VALUES input spec \u2014 the no-double-carry case of the accepted split."]
|
|
1145
1156
|
},
|
|
1146
1157
|
{
|
|
@@ -1150,7 +1161,6 @@ var FIXTURES = [
|
|
|
1150
1161
|
docRef: "Screen 4 \xB7 Order review (bank rail)",
|
|
1151
1162
|
screen: 4,
|
|
1152
1163
|
stateKey: "ORDER/AWAITING_CONFIRMATION",
|
|
1153
|
-
complete: true,
|
|
1154
1164
|
notes: [
|
|
1155
1165
|
`The screen card publishes this payload by reference \u2014 "quote: <same shape as Screen 1, refreshed>" \u2014 so the quote here is Screen 1's payload verbatim. The numbers are not re-published as refreshed values, so they are unchanged; only the shape is contractual.`
|
|
1156
1166
|
]
|
|
@@ -1162,7 +1172,6 @@ var FIXTURES = [
|
|
|
1162
1172
|
docRef: "Screen 5 \xB7 Card payment",
|
|
1163
1173
|
screen: 5,
|
|
1164
1174
|
stateKey: "PAYMENT/CAPTURE",
|
|
1165
|
-
complete: true,
|
|
1166
1175
|
notes: [
|
|
1167
1176
|
"Capture-then-order topology (report \u2192 POST /fiat/orders, expects requestId).",
|
|
1168
1177
|
"TODO(open-decision): report target per card topology is a Transak ask (contract Flow A)."
|
|
@@ -1174,8 +1183,7 @@ var FIXTURES = [
|
|
|
1174
1183
|
source: "FE_DOC",
|
|
1175
1184
|
docRef: "Screen 9 \xB7 Verifying identity",
|
|
1176
1185
|
screen: 9,
|
|
1177
|
-
stateKey: "KYC/NO_ACTION_REQUIRED:IN_REVIEW"
|
|
1178
|
-
complete: true
|
|
1186
|
+
stateKey: "KYC/NO_ACTION_REQUIRED:IN_REVIEW"
|
|
1179
1187
|
},
|
|
1180
1188
|
{
|
|
1181
1189
|
id: "screen-10-kyc-on-hold",
|
|
@@ -1184,7 +1192,6 @@ var FIXTURES = [
|
|
|
1184
1192
|
docRef: "Screen 10 \xB7 Verification on hold",
|
|
1185
1193
|
screen: 10,
|
|
1186
1194
|
stateKey: "KYC/NO_ACTION_REQUIRED:ON_HOLD",
|
|
1187
|
-
complete: true,
|
|
1188
1195
|
notes: ["transitions: [] and NOT terminal \u2014 the fixture that proves terminality is table data."]
|
|
1189
1196
|
},
|
|
1190
1197
|
{
|
|
@@ -1194,7 +1201,6 @@ var FIXTURES = [
|
|
|
1194
1201
|
docRef: "Screen 11 \xB7 Verification unsuccessful",
|
|
1195
1202
|
screen: 11,
|
|
1196
1203
|
stateKey: "KYC/NO_ACTION_REQUIRED:REJECTED",
|
|
1197
|
-
complete: true,
|
|
1198
1204
|
notes: [
|
|
1199
1205
|
"TODO(open-decision): the escalation transition is a flagged placeholder \u2014 a GET carrying params is not a real request shape (FE doc Screen 11 + open question 7).",
|
|
1200
1206
|
"The terminal REJECTED variant (failureReason + transitions []) is described in prose, not published as JSON \u2014 no fixture."
|
|
@@ -1207,7 +1213,6 @@ var FIXTURES = [
|
|
|
1207
1213
|
docRef: "Screen 12 \xB7 Order in progress",
|
|
1208
1214
|
screen: 12,
|
|
1209
1215
|
stateKey: "PENDING_ORDER",
|
|
1210
|
-
complete: true,
|
|
1211
1216
|
notes: [
|
|
1212
1217
|
"TODO(open-decision): PENDING_ORDER removal proposed; Screen 12 frozen. Kept so 0.1.0 can validate the current shape."
|
|
1213
1218
|
]
|
|
@@ -1218,8 +1223,7 @@ var FIXTURES = [
|
|
|
1218
1223
|
source: "FE_DOC",
|
|
1219
1224
|
docRef: "Screen 13 \xB7 Success",
|
|
1220
1225
|
screen: 13,
|
|
1221
|
-
stateKey: "ORDER/CREATED"
|
|
1222
|
-
complete: true
|
|
1226
|
+
stateKey: "ORDER/CREATED"
|
|
1223
1227
|
},
|
|
1224
1228
|
{
|
|
1225
1229
|
id: "screen-14-order-failed",
|
|
@@ -1228,7 +1232,6 @@ var FIXTURES = [
|
|
|
1228
1232
|
docRef: "Screen 14 \xB7 Something went wrong",
|
|
1229
1233
|
screen: 14,
|
|
1230
1234
|
stateKey: "ORDER/CREATED",
|
|
1231
|
-
complete: true,
|
|
1232
1235
|
notes: ["Terminal status (FAILED) still carrying a recovery transition \u2014 retryable failureReason."]
|
|
1233
1236
|
},
|
|
1234
1237
|
{
|
|
@@ -1237,8 +1240,7 @@ var FIXTURES = [
|
|
|
1237
1240
|
source: "FE_DOC",
|
|
1238
1241
|
docRef: "Screen 15 \xB7 Refund on the way",
|
|
1239
1242
|
screen: 15,
|
|
1240
|
-
stateKey: "ORDER/CREATED"
|
|
1241
|
-
complete: true
|
|
1243
|
+
stateKey: "ORDER/CREATED"
|
|
1242
1244
|
},
|
|
1243
1245
|
{
|
|
1244
1246
|
id: "screen-16-blocked",
|
|
@@ -1247,7 +1249,6 @@ var FIXTURES = [
|
|
|
1247
1249
|
docRef: "Screen 16 \xB7 Not available",
|
|
1248
1250
|
screen: 16,
|
|
1249
1251
|
stateKey: "BLOCKED",
|
|
1250
|
-
complete: true,
|
|
1251
1252
|
notes: ["The only flatly terminal state entry in the table."]
|
|
1252
1253
|
},
|
|
1253
1254
|
{
|
|
@@ -1256,7 +1257,6 @@ var FIXTURES = [
|
|
|
1256
1257
|
source: "CONTRACT_DOC",
|
|
1257
1258
|
docRef: "\xA7Worked example, step 1 (method selection)",
|
|
1258
1259
|
stateKey: "QUOTE",
|
|
1259
|
-
complete: false,
|
|
1260
1260
|
notes: ["Carries inputs {email: {type: TEXT}} \u2014 the collected-spec half of the split."]
|
|
1261
1261
|
},
|
|
1262
1262
|
{
|
|
@@ -1264,8 +1264,7 @@ var FIXTURES = [
|
|
|
1264
1264
|
file: "worked-example-02-session-auth.json",
|
|
1265
1265
|
source: "CONTRACT_DOC",
|
|
1266
1266
|
docRef: "\xA7Worked example, step 2 (session challenge)",
|
|
1267
|
-
stateKey: "SESSION_AUTH"
|
|
1268
|
-
complete: false
|
|
1267
|
+
stateKey: "SESSION_AUTH"
|
|
1269
1268
|
},
|
|
1270
1269
|
{
|
|
1271
1270
|
id: "worked-example-04-payment-instruct",
|
|
@@ -1273,20 +1272,19 @@ var FIXTURES = [
|
|
|
1273
1272
|
source: "CONTRACT_DOC",
|
|
1274
1273
|
docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
|
|
1275
1274
|
stateKey: "PAYMENT/INSTRUCT",
|
|
1276
|
-
complete: false,
|
|
1277
1275
|
notes: [
|
|
1278
|
-
"The
|
|
1276
|
+
"The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
|
|
1279
1277
|
]
|
|
1280
1278
|
}
|
|
1281
1279
|
];
|
|
1282
1280
|
var FIXTURE_COVERAGE_GAPS = [
|
|
1283
1281
|
{
|
|
1284
1282
|
stateKey: "FUN_AUTH",
|
|
1285
|
-
reason: "No
|
|
1283
|
+
reason: "No step response exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
|
|
1286
1284
|
},
|
|
1287
1285
|
{
|
|
1288
1286
|
stateKey: "KYC/CAPTURE",
|
|
1289
|
-
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no
|
|
1287
|
+
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no step response was ever published. Kind retained as handshake-gated vocabulary."
|
|
1290
1288
|
}
|
|
1291
1289
|
];
|
|
1292
1290
|
var fixtureMeta = (id) => {
|
|
@@ -1322,15 +1320,9 @@ var formatIssues = (error) => {
|
|
|
1322
1320
|
return path ? `${path}: ${issue.message ?? "invalid"}` : issue.message ?? "invalid";
|
|
1323
1321
|
});
|
|
1324
1322
|
};
|
|
1325
|
-
function
|
|
1326
|
-
const result =
|
|
1327
|
-
if (!result.success) throw new ContractViolation("invalid
|
|
1328
|
-
return result.data;
|
|
1329
|
-
}
|
|
1330
|
-
function assertEnvelopeFragment(json) {
|
|
1331
|
-
const result = StepResponseFragmentSchema.safeParse(json);
|
|
1332
|
-
if (!result.success)
|
|
1333
|
-
throw new ContractViolation("invalid envelope fragment", formatIssues(result.error));
|
|
1323
|
+
function assertFiatStepResponse(json) {
|
|
1324
|
+
const result = FiatStepResponseSchema.safeParse(json);
|
|
1325
|
+
if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
|
|
1334
1326
|
return result.data;
|
|
1335
1327
|
}
|
|
1336
1328
|
function endpointMatches(template, actual) {
|
|
@@ -1427,14 +1419,14 @@ function walkTable(fn) {
|
|
|
1427
1419
|
function assertFixture(id) {
|
|
1428
1420
|
const meta = fixtureMeta(id);
|
|
1429
1421
|
const json = loadFixture(id);
|
|
1430
|
-
const
|
|
1431
|
-
if (stateKey(
|
|
1422
|
+
const stepResponse = assertFiatStepResponse(json);
|
|
1423
|
+
if (stateKey(stepResponse.state) !== meta.stateKey) {
|
|
1432
1424
|
throw new ContractViolation(
|
|
1433
|
-
`fixture ${id}: manifest says ${meta.stateKey},
|
|
1425
|
+
`fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
|
|
1434
1426
|
);
|
|
1435
1427
|
}
|
|
1436
|
-
assertLegalEmission(
|
|
1437
|
-
return { meta,
|
|
1428
|
+
assertLegalEmission(stepResponse.state, stepResponse.transitions);
|
|
1429
|
+
return { meta, stepResponse };
|
|
1438
1430
|
}
|
|
1439
1431
|
function assertAllFixtures() {
|
|
1440
1432
|
return FIXTURES.map((meta) => assertFixture(meta.id));
|
|
@@ -1449,7 +1441,6 @@ export {
|
|
|
1449
1441
|
CryptoAmountSchema,
|
|
1450
1442
|
DOCUMENTED_ENDPOINTS,
|
|
1451
1443
|
ENDPOINT_PATTERN,
|
|
1452
|
-
EnvelopeSchema,
|
|
1453
1444
|
FIAT_CURRENCY_NAMES,
|
|
1454
1445
|
FIELD_SPEC_TYPES,
|
|
1455
1446
|
FIXTURES,
|
|
@@ -1460,6 +1451,7 @@ export {
|
|
|
1460
1451
|
FiatCurrencyCode,
|
|
1461
1452
|
FiatEndpointSchema,
|
|
1462
1453
|
FiatProviderSchema,
|
|
1454
|
+
FiatStepResponseSchema,
|
|
1463
1455
|
FieldSpecSchema,
|
|
1464
1456
|
FlowStateSchema,
|
|
1465
1457
|
FormDescriptorSchema,
|
|
@@ -1493,8 +1485,6 @@ export {
|
|
|
1493
1485
|
SelectOptionSchema,
|
|
1494
1486
|
SessionAuthStateSchema,
|
|
1495
1487
|
StatusHistoryEntrySchema,
|
|
1496
|
-
StepResponseFragmentSchema,
|
|
1497
|
-
StepResponseSchema,
|
|
1498
1488
|
SurfaceSchema,
|
|
1499
1489
|
TABLE_VERSION,
|
|
1500
1490
|
TERMINAL_ORDER_STATUSES,
|
|
@@ -1504,8 +1494,7 @@ export {
|
|
|
1504
1494
|
TransitionSchema,
|
|
1505
1495
|
TxSchema,
|
|
1506
1496
|
assertAllFixtures,
|
|
1507
|
-
|
|
1508
|
-
assertEnvelopeFragment,
|
|
1497
|
+
assertFiatStepResponse,
|
|
1509
1498
|
assertFixture,
|
|
1510
1499
|
assertLegalEmission,
|
|
1511
1500
|
assertLegalReturn,
|
package/dist/schemas.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* fiat-contract — zod schemas
|
|
3
3
|
*
|
|
4
4
|
* zod is the single runtime validator: TypeScript cannot validate at the boundary, and the
|
|
5
|
-
* table (`src/table.ts`) cannot reject a *malformed*
|
|
5
|
+
* table (`src/table.ts`) cannot reject a *malformed* step response — only an illegal *sequence*.
|
|
6
6
|
*
|
|
7
7
|
* Every object schema is `.strict()`: an undeclared key is a contract violation, not extra data.
|
|
8
8
|
*
|
|
@@ -27,11 +27,7 @@
|
|
|
27
27
|
* They are module-private, so none of it reaches the published declarations.
|
|
28
28
|
*/
|
|
29
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,
|
|
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
|
-
};
|
|
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, FiatStepResponse, Surface, Transition, TransitionInputs, TransitionParams, Tx } from './types';
|
|
35
31
|
export declare const JsonSchema: z.ZodType<JsonValue>;
|
|
36
32
|
export declare const HTTP_VERBS: readonly ["GET", "POST"];
|
|
37
33
|
/** `"POST /fiat/session/verify"`, `"GET /fiat/orders/o_31c"` — the verb rides the string. */
|
|
@@ -91,8 +87,6 @@ export declare const PaymentInstructStateSchema: z.ZodType<State<'PAYMENT', 'INS
|
|
|
91
87
|
export declare const OrderAwaitingConfirmationStateSchema: z.ZodType<State<'ORDER', 'AWAITING_CONFIRMATION'>>;
|
|
92
88
|
export declare const OrderCreatedStateSchema: z.ZodType<State<'ORDER', 'CREATED'>>;
|
|
93
89
|
export declare const FlowStateSchema: z.ZodType<FlowState>;
|
|
94
|
-
export declare const
|
|
95
|
-
export declare const StepResponseFragmentSchema: z.ZodType<EnvelopeFragment>;
|
|
96
|
-
export declare const EnvelopeSchema: z.ZodType<StepResponse>;
|
|
90
|
+
export declare const FiatStepResponseSchema: z.ZodType<FiatStepResponse>;
|
|
97
91
|
export {};
|
|
98
92
|
//# sourceMappingURL=schemas.d.ts.map
|
package/dist/table.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Rules the entries obey:
|
|
12
12
|
* - Terminality of `ORDER{CREATED}` rides `status`, because terminal outcomes are OrderStatus,
|
|
13
|
-
* never kinds (contract §The
|
|
13
|
+
* never kinds (contract §The fiat step response). The set is enumerated below as
|
|
14
14
|
* `TERMINAL_ORDER_STATUSES` — clients read it, they never derive it.
|
|
15
15
|
* - A terminal *state* carries no transitions. A terminal *status* still can: `FAILED` with a
|
|
16
16
|
* retryable `failureReason` carries the recovery CTA (FE doc Screen 14).
|
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
import type { FiatEndpoint, FlowState, OrderStatus, TransitionMode } from './types';
|
|
21
21
|
/**
|
|
22
22
|
* The package version IS the capability handshake's table version (contract §Conformance
|
|
23
|
-
* package → Versioning).
|
|
23
|
+
* package → Versioning). Derived from `package.json#version` at build time so the two can
|
|
24
|
+
* never drift — a version bump is one edit, in package.json. The test suite asserts the
|
|
25
|
+
* wiring still resolves to the package version.
|
|
24
26
|
*/
|
|
25
|
-
export declare const TABLE_VERSION
|
|
27
|
+
export declare const TABLE_VERSION: string;
|
|
26
28
|
/**
|
|
27
29
|
* State identity = kind + phase + the state's named discriminant (FE doc §Decisions).
|
|
28
30
|
* The table keys on kind + phase (+ `reason` for KYC, whose three reasons differ in terminality
|
|
@@ -30,7 +32,7 @@ export declare const TABLE_VERSION = "0.4.0";
|
|
|
30
32
|
*/
|
|
31
33
|
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
34
|
/**
|
|
33
|
-
* Terminal per the OrderStatus machine's monotonic ranks (contract §The
|
|
35
|
+
* Terminal per the OrderStatus machine's monotonic ranks (contract §The fiat step response).
|
|
34
36
|
*
|
|
35
37
|
* `CAPTURE_ORPHANED` is deliberately absent — see the TODO(open-decision) on the value itself.
|
|
36
38
|
* Non-terminal is the safe default for an unknown status: a client that wrongly believes the flow
|
|
@@ -46,7 +48,7 @@ export type TerminalOrderStatus = (typeof TERMINAL_ORDER_STATUSES)[number];
|
|
|
46
48
|
export type EndpointTemplate = FiatEndpoint;
|
|
47
49
|
export interface AllowedTransition {
|
|
48
50
|
/**
|
|
49
|
-
* The id observed in the docs'
|
|
51
|
+
* The id observed in the docs' step responses. Informational: ids are localization keys and the
|
|
50
52
|
* docs explicitly allow unknown ids (the client falls back to `labelFallback`), so
|
|
51
53
|
* `assertLegalEmission` matches on `mode` + `endpoint`, not on `id`.
|
|
52
54
|
*/
|
package/dist/table.js
CHANGED
|
@@ -30,7 +30,12 @@ __export(table_exports, {
|
|
|
30
30
|
tableEntry: () => tableEntry
|
|
31
31
|
});
|
|
32
32
|
module.exports = __toCommonJS(table_exports);
|
|
33
|
-
|
|
33
|
+
|
|
34
|
+
// package.json
|
|
35
|
+
var version = "0.6.0";
|
|
36
|
+
|
|
37
|
+
// src/table.ts
|
|
38
|
+
var TABLE_VERSION = version;
|
|
34
39
|
var TERMINAL_ORDER_STATUSES = [
|
|
35
40
|
"SETTLED",
|
|
36
41
|
"EXPIRED",
|
package/dist/table.mjs
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — types
|
|
3
3
|
*
|
|
4
|
-
* The published contract between `fun-backend` (emits
|
|
4
|
+
* The published contract between `fun-backend` (emits step responses) and
|
|
5
5
|
* `funkit`/`connect-core` (renders them). Zero runtime logic lives here.
|
|
6
6
|
*
|
|
7
7
|
* Sources of truth (do not extend this file from anywhere else):
|
|
8
8
|
* - Contract doc: Fiat Client Contract — Step-Driven Flow & Endpoint Schematics
|
|
9
9
|
* https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc
|
|
10
|
-
* §"The
|
|
10
|
+
* §"The fiat step response" · §"Conformance package — concrete spec"
|
|
11
11
|
* §"Proposal (2026-08-13): split InputSpec → params + inputs + expects" (ACCEPTED)
|
|
12
12
|
* - FE doc: Fiat Frontend — State Machine & Screen Map
|
|
13
13
|
* https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3
|
|
@@ -59,12 +59,12 @@ export type HttpVerb = 'GET' | 'POST';
|
|
|
59
59
|
export type FiatEndpoint = 'GET /fiat/payment-methods' | 'POST /fiat/quote' | 'POST /fiat/auth' | 'POST /fiat/payment-session' | '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`;
|
|
60
60
|
/**
|
|
61
61
|
* FiatProvider is metadata: it selects the embedded provider SDK for CLIENT_SURFACE steps and
|
|
62
|
-
* feeds analytics. It never drives sequencing (contract §The
|
|
62
|
+
* feeds analytics. It never drives sequencing (contract §The fiat step response).
|
|
63
63
|
* TODO(open-decision): the docs name only Transak as a v1 provider for the headless flow
|
|
64
64
|
* (contract §Decisions 3; Swapped-headless does not exist yet, Banxa is Tradeoff #4).
|
|
65
65
|
* `SWAPPED` added at fun-backend's request (2026-08-17) so its legacy fops payment-provider id
|
|
66
66
|
* has one shared vocabulary instead of a second fun-backend-local enum — it never appears as a
|
|
67
|
-
* `
|
|
67
|
+
* `FiatStepResponse.provider` value since Swapped's fops flow doesn't emit this response.
|
|
68
68
|
*/
|
|
69
69
|
export type FiatProvider = 'TRANSAK' | 'SWAPPED';
|
|
70
70
|
/**
|
|
@@ -101,7 +101,7 @@ export type FailureCategory = 'PAYMENT_DECLINED' | 'KYC_REJECTED' | 'PROVIDER_OU
|
|
|
101
101
|
* (the FE doc's Screen 11 note writes `SUPPORT` as shorthand — same value).
|
|
102
102
|
*/
|
|
103
103
|
export type Recovery = 'REQUOTE' | 'RETRY_PAYMENT' | 'CONTACT_SUPPORT' | 'NONE';
|
|
104
|
-
/** Three expiries, three recoveries (contract §The
|
|
104
|
+
/** Three expiries, three recoveries (contract §The fiat step response, FailureReason comment). */
|
|
105
105
|
export type ExpiredWhich = 'QUOTE' | 'PAYMENT_WINDOW' | 'INSTRUCTIONS';
|
|
106
106
|
/**
|
|
107
107
|
* `code?` is a copy key (e.g. `BAD_CODE`), not an error code to branch on.
|
|
@@ -121,7 +121,7 @@ export type FailureReason = {
|
|
|
121
121
|
code?: string;
|
|
122
122
|
};
|
|
123
123
|
/**
|
|
124
|
-
* Amounts are decimal strings in the docs'
|
|
124
|
+
* Amounts are decimal strings in the docs' step responses ("100.00") — never numbers.
|
|
125
125
|
* Named to pair with `CryptoAmount`: every use site is a `fiat:` field.
|
|
126
126
|
* `currency` is ISO 4217 — see `./codes`.
|
|
127
127
|
*/
|
|
@@ -172,7 +172,7 @@ export interface OrderRef {
|
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
174
|
* Payload of `ORDER{CREATED}` (FE doc Screens 7 · 13 · 14 · 15).
|
|
175
|
-
* `crypto` is optional because the Screen 14/15
|
|
175
|
+
* `crypto` is optional because the Screen 14/15 responses omit it (fiat-only failure/refund views).
|
|
176
176
|
*/
|
|
177
177
|
export interface OrderSummary {
|
|
178
178
|
orderId: string;
|
|
@@ -228,8 +228,8 @@ export interface InstructionField {
|
|
|
228
228
|
/**
|
|
229
229
|
* Payload of `PAYMENT{INSTRUCT}` — push rails. Values are PII-adjacent: never logged.
|
|
230
230
|
* TODO(open-decision): contract OQ1 lists `QR_IMAGE` as *deliberately not pre-declared* in the
|
|
231
|
-
* v1 vocabulary while §The
|
|
232
|
-
*
|
|
231
|
+
* v1 vocabulary while §The fiat step response's Instructions bullet enumerates it. Kept in the union
|
|
232
|
+
* per that section (and the conformance-package spec); handshake-gate it if OQ1 lands the
|
|
233
233
|
* other way.
|
|
234
234
|
* TODO(open-decision): contract OQ7 — expired instructions have no documented recovery and no
|
|
235
235
|
* re-issue endpoint exists.
|
|
@@ -311,12 +311,45 @@ export type FieldSpecType = FieldSpec['type'];
|
|
|
311
311
|
export type TransitionParams = Record<string, JsonValue>;
|
|
312
312
|
/** Client-collected specs — the harness renders + locally validates, then merges values in. */
|
|
313
313
|
export type TransitionInputs = Record<string, FieldSpec>;
|
|
314
|
+
/**
|
|
315
|
+
* Timing for an `AWAIT` poll.
|
|
316
|
+
*
|
|
317
|
+
* The numbers are bounded by `PollSpecSchema`, not by the type — TypeScript can't express a range.
|
|
318
|
+
* The bounds are part of the contract, enforced on the emitter by conformance tests, so a client
|
|
319
|
+
* can consume these fields directly and does not need to sanitize them:
|
|
320
|
+
*
|
|
321
|
+
* | field | bound |
|
|
322
|
+
* | --- | --- |
|
|
323
|
+
* | `intervalMs` | integer, `500 … 2^31-1` |
|
|
324
|
+
* | `backoffFactor` | finite, `>= 1` (**not** an integer — 1.5 is the recorded value) |
|
|
325
|
+
* | `maxIntervalMs` | integer, `500 … 2^31-1` |
|
|
326
|
+
* | `giveUpAfterMs` | integer, `0 … 2^31-1` |
|
|
327
|
+
*
|
|
328
|
+
* `2^31-1` is the ceiling because a larger `setTimeout` delay is silently truncated to 0, which
|
|
329
|
+
* hot-loops exactly as `intervalMs: 0` would.
|
|
330
|
+
*
|
|
331
|
+
* Two cross-field rules, both enforced by the schema:
|
|
332
|
+
*
|
|
333
|
+
* 1. **`maxIntervalMs` is required whenever `backoffFactor` is present.** A backoff with no ceiling
|
|
334
|
+
* reaches that same truncation by a slower route. The factor itself is deliberately not capped —
|
|
335
|
+
* only 1.5 has ever been recorded, so any cap would be arbitrary, whereas requiring the ceiling
|
|
336
|
+
* that already exists is not.
|
|
337
|
+
* 2. **`maxIntervalMs >= intervalMs`.** A ceiling below the starting interval caps the poll at less
|
|
338
|
+
* than its own first delay.
|
|
339
|
+
*
|
|
340
|
+
* Note there is deliberately no `giveUpAfterMs >= intervalMs` rule: a deadline shorter than the
|
|
341
|
+
* first interval means "give up without polling", which is coherent and legitimately emittable.
|
|
342
|
+
*/
|
|
314
343
|
export interface PollSpec {
|
|
315
344
|
endpoint: FiatEndpoint;
|
|
316
345
|
params?: TransitionParams;
|
|
346
|
+
/** Integer, `500 … 2^31-1`. */
|
|
317
347
|
intervalMs: number;
|
|
348
|
+
/** Finite, `>= 1`. Not constrained to an integer: 1.5 is legitimate. Requires `maxIntervalMs`. */
|
|
318
349
|
backoffFactor?: number;
|
|
350
|
+
/** Integer, `500 … 2^31-1`. Required when `backoffFactor` is present. */
|
|
319
351
|
maxIntervalMs?: number;
|
|
352
|
+
/** Integer, `0 … 2^31-1`. */
|
|
320
353
|
giveUpAfterMs?: number;
|
|
321
354
|
}
|
|
322
355
|
export interface ReportSpec {
|
|
@@ -404,7 +437,7 @@ export type FlowState = {
|
|
|
404
437
|
* empty transitions, NOT terminal.
|
|
405
438
|
* - `OrderStatus.ON_HOLD` = post-order compliance or amount mismatch, resolves to
|
|
406
439
|
* `PROCESSING` or `REFUNDED`.
|
|
407
|
-
* They cannot be confused in code — different types, different positions in the
|
|
440
|
+
* They cannot be confused in code — different types, different positions in the response —
|
|
408
441
|
* but they can be confused in conversation, so say which one you mean.
|
|
409
442
|
*/
|
|
410
443
|
reason: 'IN_REVIEW' | 'ON_HOLD' | 'REJECTED';
|
|
@@ -447,9 +480,9 @@ export type FlowState = {
|
|
|
447
480
|
kind: 'PAYMENT';
|
|
448
481
|
phase: 'INSTRUCT';
|
|
449
482
|
/**
|
|
450
|
-
* TODO(open-decision): `orderId` placement —
|
|
483
|
+
* TODO(open-decision): `orderId` placement — beside `state` (contract §Worked example,
|
|
451
484
|
* steps 4–5) vs inside state (FE doc v0, Screen 6). Both are accepted here; the FE doc's
|
|
452
|
-
* in-state placement is the current shape, `
|
|
485
|
+
* in-state placement is the current shape, `FiatStepResponse.orderId` covers the other.
|
|
453
486
|
*/
|
|
454
487
|
orderId?: string;
|
|
455
488
|
instructions: Instructions;
|
|
@@ -462,7 +495,7 @@ export type FlowState = {
|
|
|
462
495
|
} | {
|
|
463
496
|
kind: 'ORDER';
|
|
464
497
|
phase: 'CREATED';
|
|
465
|
-
/** Absent in the contract's terse worked-example
|
|
498
|
+
/** Absent in the contract's terse worked-example responses, present in every FE screen. */
|
|
466
499
|
order?: OrderSummary;
|
|
467
500
|
status: OrderStatus;
|
|
468
501
|
statusHistory?: StatusHistoryEntry[];
|
|
@@ -476,13 +509,11 @@ export type FlowStateKind = FlowState['kind'];
|
|
|
476
509
|
* Every `/fiat/*` response. `transitions: []` = nothing the wizard can do; whether the *flow*
|
|
477
510
|
* is over is read from the table (`terminal`), never from array emptiness (ON_HOLD is the proof).
|
|
478
511
|
*/
|
|
479
|
-
export interface
|
|
512
|
+
export interface FiatStepResponse {
|
|
480
513
|
state: FlowState;
|
|
481
514
|
provider: FiatProvider;
|
|
482
515
|
transitions: Transition[];
|
|
483
516
|
/** TODO(open-decision): `orderId` placement — see `PAYMENT{INSTRUCT}.orderId` above. */
|
|
484
517
|
orderId?: string;
|
|
485
518
|
}
|
|
486
|
-
/** The envelope as it appears in the docs' JSON — alias kept because both names are in use. */
|
|
487
|
-
export type Envelope = StepResponse;
|
|
488
519
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fun-xyz/fiat-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
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
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|