@fun-xyz/fiat-contract 0.3.0 → 0.5.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 +25 -21
- package/dist/assert.d.ts +4 -12
- package/dist/{chunk-UWR2DRHB.mjs → chunk-3R5GGUAK.mjs} +5 -2
- package/dist/fixtures/index.d.ts +4 -6
- package/dist/index.js +42 -50
- package/dist/index.mjs +39 -50
- package/dist/schemas.d.ts +2 -8
- package/dist/table.d.ts +4 -2
- package/dist/table.js +6 -1
- package/dist/table.mjs +1 -1
- package/dist/types.d.ts +37 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ 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 envelopes (inlined as data — no filesystem, so React Native can bundle it) |
|
|
14
14
|
|
|
15
15
|
## Three entry points — production vs test-time
|
|
16
16
|
|
|
@@ -55,23 +55,26 @@ 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-envelope validation in dev/test
|
|
75
78
|
const owed = walkTable((entry) => entry.allowedTransitions); // what the adapter must emit
|
|
76
79
|
```
|
|
77
80
|
|
|
@@ -102,9 +105,9 @@ The backend *builds* envelopes, so its production use is entirely compile-time
|
|
|
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,9 +140,9 @@ 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({ env }: { env:
|
|
145
|
+
export function FiatScreen({ env }: { env: FiatStepResponse }) {
|
|
143
146
|
const page = computePage(stateKey(env.state), clientLocal); // client owns state → screen
|
|
144
147
|
const done = isTerminal(env.state); // table data, never inferred
|
|
145
148
|
|
|
@@ -160,9 +163,9 @@ 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(env:
|
|
168
|
+
function useTransitions(env: FiatStepResponse) {
|
|
166
169
|
return {
|
|
167
170
|
ctas: env.transitions.filter((t) => t.mode === 'SUBMIT'), // render buttons
|
|
168
171
|
poll: env.transitions.find((t) => t.mode === 'AWAIT'), // harness schedules
|
|
@@ -190,15 +193,16 @@ These import from the root, which carries zod. Test-time only.
|
|
|
190
193
|
|
|
191
194
|
### Validate an envelope 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 envelope =
|
|
201
|
-
// ^?
|
|
204
|
+
const envelope = assertFiatStepResponse(await res.json());
|
|
205
|
+
// ^? FiatStepResponse — state is a narrowable discriminated union from here on
|
|
202
206
|
if (envelope.state.kind === 'PAYMENT' && envelope.state.phase === 'INSTRUCT') {
|
|
203
207
|
render(envelope.state.instructions); // narrowed: instructions exists, quote does not
|
|
204
208
|
}
|
|
@@ -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
|
package/dist/assert.d.ts
CHANGED
|
@@ -10,19 +10,14 @@
|
|
|
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
|
+
envelope: 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.5.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-3R5GGUAK.mjs.map
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
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 envelope (`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-envelope shape.
|
|
16
16
|
*/
|
|
17
17
|
import type { StateKey } from '../table';
|
|
18
18
|
export type FixtureSource = 'FE_DOC' | 'CONTRACT_DOC';
|
|
@@ -25,8 +25,6 @@ export interface FixtureMeta {
|
|
|
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[];
|
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.5.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,7 +1765,6 @@ 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
1769
|
"The envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
|
|
1772
1770
|
]
|
|
@@ -1815,17 +1813,11 @@ var formatIssues = (error) => {
|
|
|
1815
1813
|
return path ? `${path}: ${issue.message ?? "invalid"}` : issue.message ?? "invalid";
|
|
1816
1814
|
});
|
|
1817
1815
|
};
|
|
1818
|
-
function
|
|
1819
|
-
const result =
|
|
1816
|
+
function assertFiatStepResponse(json) {
|
|
1817
|
+
const result = FiatStepResponseSchema.safeParse(json);
|
|
1820
1818
|
if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
|
|
1821
1819
|
return result.data;
|
|
1822
1820
|
}
|
|
1823
|
-
function assertEnvelopeFragment(json) {
|
|
1824
|
-
const result = StepResponseFragmentSchema.safeParse(json);
|
|
1825
|
-
if (!result.success)
|
|
1826
|
-
throw new ContractViolation("invalid envelope fragment", formatIssues(result.error));
|
|
1827
|
-
return result.data;
|
|
1828
|
-
}
|
|
1829
1821
|
function endpointMatches(template, actual) {
|
|
1830
1822
|
const pattern = template.split("/").map((segment) => segment.startsWith(":") ? "[^/]+" : escapeRegExp(segment)).join("/");
|
|
1831
1823
|
return new RegExp(`^${pattern}$`).test(actual);
|
|
@@ -1920,7 +1912,7 @@ function walkTable(fn) {
|
|
|
1920
1912
|
function assertFixture(id) {
|
|
1921
1913
|
const meta = fixtureMeta(id);
|
|
1922
1914
|
const json = loadFixture(id);
|
|
1923
|
-
const envelope =
|
|
1915
|
+
const envelope = assertFiatStepResponse(json);
|
|
1924
1916
|
if (stateKey(envelope.state) !== meta.stateKey) {
|
|
1925
1917
|
throw new ContractViolation(
|
|
1926
1918
|
`fixture ${id}: manifest says ${meta.stateKey}, envelope carries ${stateKey(envelope.state)}`
|
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-3R5GGUAK.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,7 +1272,6 @@ 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
1276
|
"The envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
|
|
1279
1277
|
]
|
|
@@ -1322,17 +1320,11 @@ var formatIssues = (error) => {
|
|
|
1322
1320
|
return path ? `${path}: ${issue.message ?? "invalid"}` : issue.message ?? "invalid";
|
|
1323
1321
|
});
|
|
1324
1322
|
};
|
|
1325
|
-
function
|
|
1326
|
-
const result =
|
|
1323
|
+
function assertFiatStepResponse(json) {
|
|
1324
|
+
const result = FiatStepResponseSchema.safeParse(json);
|
|
1327
1325
|
if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
|
|
1328
1326
|
return result.data;
|
|
1329
1327
|
}
|
|
1330
|
-
function assertEnvelopeFragment(json) {
|
|
1331
|
-
const result = StepResponseFragmentSchema.safeParse(json);
|
|
1332
|
-
if (!result.success)
|
|
1333
|
-
throw new ContractViolation("invalid envelope fragment", formatIssues(result.error));
|
|
1334
|
-
return result.data;
|
|
1335
|
-
}
|
|
1336
1328
|
function endpointMatches(template, actual) {
|
|
1337
1329
|
const pattern = template.split("/").map((segment) => segment.startsWith(":") ? "[^/]+" : escapeRegExp(segment)).join("/");
|
|
1338
1330
|
return new RegExp(`^${pattern}$`).test(actual);
|
|
@@ -1427,7 +1419,7 @@ function walkTable(fn) {
|
|
|
1427
1419
|
function assertFixture(id) {
|
|
1428
1420
|
const meta = fixtureMeta(id);
|
|
1429
1421
|
const json = loadFixture(id);
|
|
1430
|
-
const envelope =
|
|
1422
|
+
const envelope = assertFiatStepResponse(json);
|
|
1431
1423
|
if (stateKey(envelope.state) !== meta.stateKey) {
|
|
1432
1424
|
throw new ContractViolation(
|
|
1433
1425
|
`fixture ${id}: manifest says ${meta.stateKey}, envelope carries ${stateKey(envelope.state)}`
|
|
@@ -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
|
@@ -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
|
@@ -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
|
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.5.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
|
@@ -56,7 +56,7 @@ export type HttpVerb = 'GET' | 'POST';
|
|
|
56
56
|
* cannot express "no slash", so `GET /fiat/orders/${string}` also admits sub-paths. Precision at
|
|
57
57
|
* that level is the table's job (`allowedTransitions` + `endpointMatches`), not the type's.
|
|
58
58
|
*/
|
|
59
|
-
export type FiatEndpoint = 'GET /fiat/payment-methods' | 'POST /fiat/quote' | 'POST /fiat/auth' | 'POST /fiat/session' | 'POST /fiat/session/verify' | 'GET /fiat/kyc' | 'POST /fiat/kyc/form' | 'POST /fiat/kyc/document' | 'POST /fiat/instruments' | 'POST /fiat/orders' | `GET /fiat/orders/${string}` | `GET /fiat/orders/${string}/instructions` | `POST /fiat/orders/${string}/confirm-payment` | `POST /fiat/orders/${string}/reference` | `POST /fiat/orders/${string}/cancel` | `POST /fiat/orders/${string}/surface-result`;
|
|
59
|
+
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
62
|
* feeds analytics. It never drives sequencing (contract §The envelope).
|
|
@@ -64,7 +64,7 @@ export type FiatEndpoint = 'GET /fiat/payment-methods' | 'POST /fiat/quote' | 'P
|
|
|
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 envelope.
|
|
68
68
|
*/
|
|
69
69
|
export type FiatProvider = 'TRANSAK' | 'SWAPPED';
|
|
70
70
|
/**
|
|
@@ -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 {
|
|
@@ -449,7 +482,7 @@ export type FlowState = {
|
|
|
449
482
|
/**
|
|
450
483
|
* TODO(open-decision): `orderId` placement — envelope sibling (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;
|
|
@@ -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.5.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": {
|
|
@@ -78,4 +78,4 @@
|
|
|
78
78
|
]
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|