@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 CHANGED
@@ -1,16 +1,16 @@
1
1
  # fiat-contract
2
2
 
3
- The **published contract** between `fun-backend` (emits envelopes) and `funkit`/`connect-core`
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` · `StepResponse` |
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/` | `assertEnvelope` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 16 recorded envelopes (inlined as data — no filesystem, so React Native can bundle it) |
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 envelope · §Conformance package · §split `InputSpec` — ACCEPTED) and
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` envelope — there is no error routing table).
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`) must equal `package.json#version`; a test asserts it.
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 { assertEnvelope, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';
74
+ import { assertFiatStepResponse, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';
72
75
 
73
- assertLegalEmission(state, transitions); // per emission: adapter conformance
74
- assertEnvelope(outgoing); // outgoing-envelope validation in dev/test
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-envelope
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-envelope focus gating,
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* envelopes, so its production use is entirely compile-time — the types are the
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, StepResponse, Transition } from '@fun-xyz/fiat-contract/types';
108
+ import type { FlowState, FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
106
109
 
107
- export async function createOrder(quoteRef: string): Promise<StepResponse> {
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 { StepResponse } from '@fun-xyz/fiat-contract/types';
143
+ import type { FiatStepResponse } from '@fun-xyz/fiat-contract/types';
141
144
 
142
- export function FiatScreen({ env }: { env: StepResponse }) {
143
- const page = computePage(stateKey(env.state), clientLocal); // client owns state → screen
144
- const done = isTerminal(env.state); // table data, never inferred
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 (env.state.kind === 'PAYMENT' && env.state.phase === 'INSTRUCT') {
147
- return render(page, env.state.instructions); // narrowed: instructions exists here
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 (env.state.kind === 'ORDER' && env.state.phase === 'CREATED') {
150
- return render(page, { status: env.state.status, done });
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, env.state);
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 { StepResponse, Transition } from '@fun-xyz/fiat-contract/types';
166
+ import type { FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
164
167
 
165
- function useTransitions(env: StepResponse) {
168
+ function useTransitions(stepResponse: FiatStepResponse) {
166
169
  return {
167
- ctas: env.transitions.filter((t) => t.mode === 'SUBMIT'), // render buttons
168
- poll: env.transitions.find((t) => t.mode === 'AWAIT'), // harness schedules
169
- surface: env.transitions.find((t) => t.mode === 'CLIENT_SURFACE'), // harness mounts
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 an envelope at the boundary
194
+ ### Validate a step response at the boundary
192
195
 
193
- `assertEnvelope` parses and returns a typed envelope, or throws `ContractViolation` listing every
194
- problem. Use it on the way out of `fun-backend` (dev/test) and on the way in to `connect-core` tests.
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 { assertEnvelope, ContractViolation } from '@fun-xyz/fiat-contract';
201
+ import { assertFiatStepResponse, ContractViolation } from '@fun-xyz/fiat-contract';
198
202
 
199
203
  try {
200
- const envelope = assertEnvelope(await res.json());
201
- // ^? StepResponse — state is a narrowable discriminated union from here on
202
- if (envelope.state.kind === 'PAYMENT' && envelope.state.phase === 'INSTRUCT') {
203
- render(envelope.state.instructions); // narrowed: instructions exists, quote does not
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 envelope with no prior
265
- context. The fixtures are the FE doc's own envelopes, so this is a test against the spec.
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 { envelope } = assertFixture(id); // validated + emission-legality checked
273
- expect(() => renderCold(envelope)).not.toThrow();
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 envelope.
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 { StepResponse } from '@fun-xyz/fiat-contract/types'; // production, 0 KB
317
- import { isTerminal } from '@fun-xyz/fiat-contract/table'; // production, 13.8 KB, no zod
318
- import { assertEnvelope } from '@fun-xyz/fiat-contract'; // tests only, pulls zod
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 as an envelope sibling (contract worked example). One fixture of each. |
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 envelope, flagged against OQ1's "deliberately not pre-declared". |
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 envelopes:
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 envelope.
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-envelope
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 { FlowState, StepResponse, Transition } from './types';
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 envelope. Returns the parsed envelope; throws on violation. */
20
- export declare function assertEnvelope(json: unknown): StepResponse;
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
- /** `provider` is absent on fragment fixtures (`meta.complete === false`). */
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 = "0.4.0";
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-VLQLWQPK.mjs.map
415
+ //# sourceMappingURL=chunk-EHYDQU4T.mjs.map
@@ -1,18 +1,18 @@
1
1
  /**
2
2
  * fiat-contract — recorded fixtures + loader.
3
3
  *
4
- * Every `.json` file in this directory is an envelope copied **verbatim** from the docs — the FE
5
- * doc's per-screen "State details" envelopes plus the contract doc's worked example. The one
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
- * Two fixture shapes exist:
13
- * - `complete: true` a full envelope (`state` + `provider` + `transitions`) ⇒ `assertEnvelope`
14
- * - `complete: false` a doc excerpt that omits `provider` `assertEnvelopeFragment`
15
- * (`provider` is always present on the wire; the contract doc's worked example just elides it)
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 envelope is published. */
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 envelope to record. Declared so missing coverage is visible
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 envelope — deliberately `unknown`, so callers validate before use.
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 envelopes) and
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
- assertEnvelope: () => assertEnvelope,
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
- backoffFactor: import_zod.z.number().optional(),
568
- maxIntervalMs: import_zod.z.number().optional(),
569
- giveUpAfterMs: import_zod.z.number().optional()
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 StepResponseSchema = $stepResponse;
703
- var $stepResponseFragment = obj({
704
- state: $flowState,
705
- provider: $provider.optional(),
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 = "0.4.0";
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 envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
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 envelope 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."
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 envelope was ever published. Kind retained as handshake-gated vocabulary."
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 assertEnvelope(json) {
1819
- const result = StepResponseSchema.safeParse(json);
1820
- if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
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 envelope = meta.complete ? assertEnvelope(json) : assertEnvelopeFragment(json);
1924
- if (stateKey(envelope.state) !== meta.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}, envelope carries ${stateKey(envelope.state)}`
1918
+ `fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
1927
1919
  );
1928
1920
  }
1929
- assertLegalEmission(envelope.state, envelope.transitions);
1930
- return { meta, envelope };
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-VLQLWQPK.mjs";
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
- backoffFactor: z.number().optional(),
476
- maxIntervalMs: z.number().optional(),
477
- giveUpAfterMs: z.number().optional()
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 StepResponseSchema = $stepResponse;
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 envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
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 envelope 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."
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 envelope was ever published. Kind retained as handshake-gated vocabulary."
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 assertEnvelope(json) {
1326
- const result = StepResponseSchema.safeParse(json);
1327
- if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
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 envelope = meta.complete ? assertEnvelope(json) : assertEnvelopeFragment(json);
1431
- if (stateKey(envelope.state) !== meta.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}, envelope carries ${stateKey(envelope.state)}`
1425
+ `fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
1434
1426
  );
1435
1427
  }
1436
- assertLegalEmission(envelope.state, envelope.transitions);
1437
- return { meta, envelope };
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
- assertEnvelope,
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* envelope — only an illegal *sequence*.
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, StepResponse, Surface, Transition, TransitionInputs, TransitionParams, Tx } from './types';
31
- /** The envelope as doc excerpts show it: `provider` elided. Never valid on the wire. */
32
- export type EnvelopeFragment = Omit<StepResponse, 'provider'> & {
33
- provider?: FiatProvider;
34
- };
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 StepResponseSchema: z.ZodType<StepResponse>;
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 envelope). The set is enumerated below as
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). Must equal `package.json#version` asserted in the test suite.
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 = "0.4.0";
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 envelope).
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' envelopes. Informational: ids are localization keys and the
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
- var TABLE_VERSION = "0.4.0";
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
@@ -7,7 +7,7 @@ import {
7
7
  isTerminal,
8
8
  stateKey,
9
9
  tableEntry
10
- } from "./chunk-VLQLWQPK.mjs";
10
+ } from "./chunk-EHYDQU4T.mjs";
11
11
  export {
12
12
  DOCUMENTED_ENDPOINTS,
13
13
  STATE_KEYS,
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 envelopes) and
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 envelope" · §"Conformance package — concrete spec"
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 envelope).
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
- * `StepResponse.provider` value since Swapped's fops flow doesn't emit this envelope.
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 envelope, FailureReason comment). */
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' envelopes ("100.00") — never numbers.
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 envelopes omit it (fiat-only failure/refund views).
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 envelope's Instructions bullet enumerates it. Kept in the union per
232
- * the envelope section (and the conformance-package spec); handshake-gate it if OQ1 lands the
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 envelope
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 — envelope sibling (contract §Worked example,
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, `StepResponse.orderId` covers the other.
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 envelopes, present in every FE screen. */
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 StepResponse {
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.4.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": {