@fun-xyz/fiat-contract 0.5.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 +33 -33
- package/dist/assert.d.ts +2 -2
- package/dist/{chunk-3R5GGUAK.mjs → chunk-EHYDQU4T.mjs} +2 -2
- package/dist/fixtures/index.d.ts +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +10 -10
- package/dist/index.mjs +10 -10
- package/dist/schemas.d.ts +1 -1
- package/dist/table.d.ts +3 -3
- package/dist/table.js +1 -1
- package/dist/table.mjs +1 -1
- package/dist/types.d.ts +12 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fiat-contract
|
|
2
2
|
|
|
3
|
-
The **published contract** between `fun-backend` (emits
|
|
3
|
+
The **published contract** between `fun-backend` (emits step responses) and `funkit`/`connect-core`
|
|
4
4
|
(renders them). Both repos test against it; neither owns it.
|
|
5
5
|
|
|
6
6
|
Four things, zero runtime logic beyond validation:
|
|
@@ -10,7 +10,7 @@ Four things, zero runtime logic beyond validation:
|
|
|
10
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/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 16 recorded
|
|
13
|
+
| `src/assert.ts` + `src/fixtures/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 16 recorded step responses (inlined as data — no filesystem, so React Native can bundle it) |
|
|
14
14
|
|
|
15
15
|
## Three entry points — production vs test-time
|
|
16
16
|
|
|
@@ -29,7 +29,7 @@ Metro before RN 0.79 ignores `exports` entirely — then greps the emitted bundl
|
|
|
29
29
|
schemas and executes it.
|
|
30
30
|
|
|
31
31
|
Source of truth: [Fiat Client Contract](https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc)
|
|
32
|
-
(§The
|
|
32
|
+
(§The fiat step response · §Conformance package · §split `InputSpec` — ACCEPTED) and
|
|
33
33
|
[Fiat Frontend — State Machine & Screen Map](https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3)
|
|
34
34
|
(✅ Decisions · per-screen State details · Event bindings per flow state).
|
|
35
35
|
Tracking: [Headless Fiat Onramp](https://linear.app/funxyz/project/headless-fiat-onramp-0147f4102399) ·
|
|
@@ -39,7 +39,7 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
|
|
|
39
39
|
|
|
40
40
|
1. **Errors are fields, never states.** Every fallible state carries `error?: FailureReason`.
|
|
41
41
|
Stay-on-screen ⇒ an error field. Change-screen ⇒ a different state (session expiry just returns
|
|
42
|
-
a `SESSION_AUTH`
|
|
42
|
+
a `SESSION_AUTH` step response — there is no error routing table).
|
|
43
43
|
2. **`params` = server literals · `inputs` = collected specs · `expects` = injected surface
|
|
44
44
|
results.** `body = {…params, …collected(inputs), …injected(expects)}`; a key collision across
|
|
45
45
|
the three is a contract violation, not last-write-wins. zod rejects a `FieldSpec` hiding in
|
|
@@ -74,11 +74,11 @@ removals also ride a minor, with no deprecated aliases kept — consumers pin an
|
|
|
74
74
|
import { assertFiatStepResponse, assertLegalEmission, walkTable } from '@fun-xyz/fiat-contract';
|
|
75
75
|
|
|
76
76
|
assertLegalEmission(state, transitions); // per emission: adapter conformance
|
|
77
|
-
assertFiatStepResponse(outgoing); // outgoing-
|
|
77
|
+
assertFiatStepResponse(outgoing); // outgoing step-response validation in dev/test
|
|
78
78
|
const owed = walkTable((entry) => entry.allowedTransitions); // what the adapter must emit
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing-
|
|
81
|
+
Its own suites: adapter conformance (fixture-driven + property-generated states), outgoing step-response
|
|
82
82
|
validation, and the scheduled provider-sandbox drift run diffed against `src/fixtures`.
|
|
83
83
|
|
|
84
84
|
**`connect-core`** — the harness obeys `transitions` for sequencing and owns rendering.
|
|
@@ -91,7 +91,7 @@ FIXTURES.forEach(({ id }) => renderCold(loadFixture(id))); // stale-rule
|
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Its own suites: exhaustive `computePage` table-walk, fixture-driven cold-render tests, harness units
|
|
94
|
-
(any-state handling, same-state error re-entry retains form values, one-live-
|
|
94
|
+
(any-state handling, same-state error re-entry retains form values, one-live-response focus gating,
|
|
95
95
|
idempotency-key reuse).
|
|
96
96
|
|
|
97
97
|
## Usage
|
|
@@ -101,7 +101,7 @@ so these examples cannot drift from the API.
|
|
|
101
101
|
|
|
102
102
|
### Production: a backend route handler
|
|
103
103
|
|
|
104
|
-
The backend *builds*
|
|
104
|
+
The backend *builds* step responses, so its production use is entirely compile-time — the types are the
|
|
105
105
|
verification. Runtime assertions stay behind a dev/test guard.
|
|
106
106
|
|
|
107
107
|
```ts
|
|
@@ -142,17 +142,17 @@ production-safe: types erase, `./table` is 13.8 KB with no zod.
|
|
|
142
142
|
import { isTerminal, stateKey } from '@fun-xyz/fiat-contract/table';
|
|
143
143
|
import type { FiatStepResponse } from '@fun-xyz/fiat-contract/types';
|
|
144
144
|
|
|
145
|
-
export function FiatScreen({
|
|
146
|
-
const page = computePage(stateKey(
|
|
147
|
-
const done = isTerminal(
|
|
145
|
+
export function FiatScreen({ stepResponse }: { stepResponse: FiatStepResponse }) {
|
|
146
|
+
const page = computePage(stateKey(stepResponse.state), clientLocal); // client owns state → screen
|
|
147
|
+
const done = isTerminal(stepResponse.state); // table data, never inferred
|
|
148
148
|
|
|
149
|
-
if (
|
|
150
|
-
return render(page,
|
|
149
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
150
|
+
return render(page, stepResponse.state.instructions); // narrowed: instructions exists here
|
|
151
151
|
}
|
|
152
|
-
if (
|
|
153
|
-
return render(page, { status:
|
|
152
|
+
if (stepResponse.state.kind === 'ORDER' && stepResponse.state.phase === 'CREATED') {
|
|
153
|
+
return render(page, { status: stepResponse.state.status, done });
|
|
154
154
|
}
|
|
155
|
-
return render(page,
|
|
155
|
+
return render(page, stepResponse.state);
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -165,11 +165,11 @@ re-derive that rule locally.
|
|
|
165
165
|
```ts
|
|
166
166
|
import type { FiatStepResponse, Transition } from '@fun-xyz/fiat-contract/types';
|
|
167
167
|
|
|
168
|
-
function useTransitions(
|
|
168
|
+
function useTransitions(stepResponse: FiatStepResponse) {
|
|
169
169
|
return {
|
|
170
|
-
ctas:
|
|
171
|
-
poll:
|
|
172
|
-
surface:
|
|
170
|
+
ctas: stepResponse.transitions.filter((t) => t.mode === 'SUBMIT'), // render buttons
|
|
171
|
+
poll: stepResponse.transitions.find((t) => t.mode === 'AWAIT'), // harness schedules
|
|
172
|
+
surface: stepResponse.transitions.find((t) => t.mode === 'CLIENT_SURFACE'), // harness mounts
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -191,7 +191,7 @@ Screens never inspect the array themselves; they receive `ctas` and bind labels
|
|
|
191
191
|
|
|
192
192
|
These import from the root, which carries zod. Test-time only.
|
|
193
193
|
|
|
194
|
-
### Validate
|
|
194
|
+
### Validate a step response at the boundary
|
|
195
195
|
|
|
196
196
|
`assertFiatStepResponse` parses and returns a typed step response, or throws `ContractViolation`
|
|
197
197
|
listing every problem. Use it on the way out of `fun-backend` (dev/test) and on the way in to
|
|
@@ -201,10 +201,10 @@ listing every problem. Use it on the way out of `fun-backend` (dev/test) and on
|
|
|
201
201
|
import { assertFiatStepResponse, ContractViolation } from '@fun-xyz/fiat-contract';
|
|
202
202
|
|
|
203
203
|
try {
|
|
204
|
-
const
|
|
204
|
+
const stepResponse = assertFiatStepResponse(await res.json());
|
|
205
205
|
// ^? FiatStepResponse — state is a narrowable discriminated union from here on
|
|
206
|
-
if (
|
|
207
|
-
render(
|
|
206
|
+
if (stepResponse.state.kind === 'PAYMENT' && stepResponse.state.phase === 'INSTRUCT') {
|
|
207
|
+
render(stepResponse.state.instructions); // narrowed: instructions exists, quote does not
|
|
208
208
|
}
|
|
209
209
|
} catch (err) {
|
|
210
210
|
if (err instanceof ContractViolation) console.error(err.issues); // ['state.quote: Required', …]
|
|
@@ -265,16 +265,16 @@ TRANSITION_TABLE['PAYMENT/INSTRUCT'].mayReturn; // ['ORDER/CREATED']
|
|
|
265
265
|
|
|
266
266
|
### Render every fixture cold (stale-rule survival)
|
|
267
267
|
|
|
268
|
-
Any call may return any state, so every screen must render from a cold
|
|
269
|
-
context. The fixtures are the FE doc's own
|
|
268
|
+
Any call may return any state, so every screen must render from a cold step response with no
|
|
269
|
+
prior context. The fixtures are the FE doc's own responses, so this is a test against the spec.
|
|
270
270
|
|
|
271
271
|
```ts
|
|
272
272
|
import { FIXTURES, loadFixture, assertFixture } from '@fun-xyz/fiat-contract';
|
|
273
273
|
|
|
274
274
|
FIXTURES.forEach(({ id, stateKey, docRef }) => {
|
|
275
275
|
it(`${id} renders cold (${docRef})`, () => {
|
|
276
|
-
const {
|
|
277
|
-
expect(() => renderCold(
|
|
276
|
+
const { stepResponse } = assertFixture(id); // validated + emission-legality checked
|
|
277
|
+
expect(() => renderCold(stepResponse)).not.toThrow();
|
|
278
278
|
expect(computePage(stateKey)).toBeDefined();
|
|
279
279
|
});
|
|
280
280
|
});
|
|
@@ -284,7 +284,7 @@ loadFixture('screen-10-kyc-on-hold'); // raw JSON, fresh deep copy, `unknown`
|
|
|
284
284
|
|
|
285
285
|
### Use a schema directly
|
|
286
286
|
|
|
287
|
-
All 46 schemas are exported when you need to validate a fragment rather than a whole
|
|
287
|
+
All 46 schemas are exported when you need to validate a fragment rather than a whole step response.
|
|
288
288
|
They are typed `z.ZodType<T>`, so you get `.parse` / `.safeParse` / `.optional()` — not `.shape` or
|
|
289
289
|
`.extend`, deliberately.
|
|
290
290
|
|
|
@@ -435,7 +435,7 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
435
435
|
|
|
436
436
|
| Open item | How 0.1.0 encodes it |
|
|
437
437
|
| --- | --- |
|
|
438
|
-
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional
|
|
438
|
+
| `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
|
|
439
439
|
| `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
|
|
440
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. |
|
|
441
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. |
|
|
@@ -443,11 +443,11 @@ The current documented shape is what ships; none of these are settled here.
|
|
|
443
443
|
| Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
|
|
444
444
|
| `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
|
|
445
445
|
| `statusHistory` element shape | The documented minimum (`{status}`) — no invented timestamps. |
|
|
446
|
-
| `QR_IMAGE` instruction | In the union per §The
|
|
446
|
+
| `QR_IMAGE` instruction | In the union per §The fiat step response, flagged against OQ1's "deliberately not pre-declared". |
|
|
447
447
|
|
|
448
|
-
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented
|
|
448
|
+
Two fixture gaps are declared in `FIXTURE_COVERAGE_GAPS` rather than filled with invented step responses:
|
|
449
449
|
`FUN_AUTH` (shape owned by the auth spike) and `KYC/CAPTURE` (Screen 8 tombstone, dropped from v1).
|
|
450
|
-
Every other state has a recorded
|
|
450
|
+
Every other state has a recorded step response.
|
|
451
451
|
|
|
452
452
|
## Not decided here — needs a human
|
|
453
453
|
|
package/dist/assert.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* exactly two things: validate shapes (zod) and judge emissions against the table.
|
|
4
4
|
*
|
|
5
5
|
* Test *suites* live in the consumer repos next to the code they test:
|
|
6
|
-
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing-
|
|
6
|
+
* - `fun-backend`: adapter conformance (every emission ∈ the legal set), outgoing step-response
|
|
7
7
|
* validation in dev/test, the scheduled provider drift run.
|
|
8
8
|
* - `connect-core`: exhaustive `computePage` table-walk, fixture-driven cold-render tests,
|
|
9
9
|
* harness units.
|
|
@@ -49,7 +49,7 @@ export declare function assertLegalReturn(from: FlowState, to: FlowState): void;
|
|
|
49
49
|
export declare function walkTable<T>(fn: (entry: TableEntry, key: StateKey) => T): T[];
|
|
50
50
|
export interface ValidatedFixture {
|
|
51
51
|
meta: FixtureMeta;
|
|
52
|
-
|
|
52
|
+
stepResponse: FiatStepResponse;
|
|
53
53
|
}
|
|
54
54
|
/** Load one fixture, validate its shape, and assert its emission is legal for its state. */
|
|
55
55
|
export declare function assertFixture(id: string): ValidatedFixture;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.6.0";
|
|
3
3
|
|
|
4
4
|
// src/table.ts
|
|
5
5
|
var TABLE_VERSION = version;
|
|
@@ -412,4 +412,4 @@ export {
|
|
|
412
412
|
tableEntry,
|
|
413
413
|
isTerminal
|
|
414
414
|
};
|
|
415
|
-
//# sourceMappingURL=chunk-
|
|
415
|
+
//# sourceMappingURL=chunk-EHYDQU4T.mjs.map
|
package/dist/fixtures/index.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — recorded fixtures + loader.
|
|
3
3
|
*
|
|
4
|
-
* Every `.json` file in this directory is
|
|
5
|
-
* doc's per-screen "State details"
|
|
4
|
+
* Every `.json` file in this directory is a step response copied **verbatim** from the docs — the
|
|
5
|
+
* FE doc's per-screen "State details" responses plus the contract doc's worked example. The one
|
|
6
6
|
* exception is `screen-04-order-review.json`, whose payload the doc publishes *by reference*
|
|
7
7
|
* ("quote: <same shape as Screen 1, refreshed>"): its quote is Screen 1's payload verbatim.
|
|
8
8
|
* They are
|
|
9
9
|
* already synthetic (`q_8f2`, `o_31c`, `eyJ…`, `"…"` placeholders); the redaction rule applies to
|
|
10
10
|
* fixtures too, so never replace a fake session token, bank field, or PII value with a real one.
|
|
11
11
|
*
|
|
12
|
-
* Every fixture is a full
|
|
12
|
+
* Every fixture is a full step response (`state` + `provider` + `transitions`). The contract doc's worked
|
|
13
13
|
* example elides `provider` in its prose; the three `worked-example-*` fixtures add
|
|
14
14
|
* `"provider": "TRANSAK"` so they validate as real responses — `provider` is always present on the
|
|
15
|
-
* wire, and the contract has no half-
|
|
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,7 +20,7 @@ export interface FixtureMeta {
|
|
|
20
20
|
id: string;
|
|
21
21
|
file: string;
|
|
22
22
|
source: FixtureSource;
|
|
23
|
-
/** Where in the source doc this
|
|
23
|
+
/** Where in the source doc this step response is published. */
|
|
24
24
|
docRef: string;
|
|
25
25
|
/** FE doc screen number, when the fixture comes from a screen card. */
|
|
26
26
|
screen?: number;
|
|
@@ -29,7 +29,7 @@ export interface FixtureMeta {
|
|
|
29
29
|
}
|
|
30
30
|
export declare const FIXTURES: readonly FixtureMeta[];
|
|
31
31
|
/**
|
|
32
|
-
* State entries with no published
|
|
32
|
+
* State entries with no published step response to record. Declared so missing coverage is visible
|
|
33
33
|
* instead of silent — the fixture test asserts coverage equals (all state keys − these).
|
|
34
34
|
*/
|
|
35
35
|
export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
@@ -38,7 +38,7 @@ export declare const FIXTURE_COVERAGE_GAPS: readonly {
|
|
|
38
38
|
}[];
|
|
39
39
|
export declare const fixtureMeta: (id: string) => FixtureMeta;
|
|
40
40
|
/**
|
|
41
|
-
* Raw
|
|
41
|
+
* Raw step response — deliberately `unknown`, so callers validate before use.
|
|
42
42
|
*
|
|
43
43
|
* Reads from the generated `data.ts`: no filesystem, so this works under React Native, in a
|
|
44
44
|
* browser, and in both CJS and ESM output. A fresh deep copy each call, so a consumer mutating a
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* fiat-contract — the published contract between `fun-backend` (emits
|
|
2
|
+
* fiat-contract — the published contract between `fun-backend` (emits step responses) and
|
|
3
3
|
* `funkit`/`connect-core` (renders them). Types + zod schemas + the transition table as data +
|
|
4
4
|
* recorded fixtures + assertion helpers. No runtime logic beyond validation and assertion.
|
|
5
5
|
*
|
package/dist/index.js
CHANGED
|
@@ -718,7 +718,7 @@ var $stepResponse = obj({
|
|
|
718
718
|
var FiatStepResponseSchema = $stepResponse;
|
|
719
719
|
|
|
720
720
|
// package.json
|
|
721
|
-
var version = "0.
|
|
721
|
+
var version = "0.6.0";
|
|
722
722
|
|
|
723
723
|
// src/table.ts
|
|
724
724
|
var TABLE_VERSION = version;
|
|
@@ -1766,18 +1766,18 @@ var FIXTURES = [
|
|
|
1766
1766
|
docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
|
|
1767
1767
|
stateKey: "PAYMENT/INSTRUCT",
|
|
1768
1768
|
notes: [
|
|
1769
|
-
"The
|
|
1769
|
+
"The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
|
|
1770
1770
|
]
|
|
1771
1771
|
}
|
|
1772
1772
|
];
|
|
1773
1773
|
var FIXTURE_COVERAGE_GAPS = [
|
|
1774
1774
|
{
|
|
1775
1775
|
stateKey: "FUN_AUTH",
|
|
1776
|
-
reason: "No
|
|
1776
|
+
reason: "No step response exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
|
|
1777
1777
|
},
|
|
1778
1778
|
{
|
|
1779
1779
|
stateKey: "KYC/CAPTURE",
|
|
1780
|
-
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no
|
|
1780
|
+
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no step response was ever published. Kind retained as handshake-gated vocabulary."
|
|
1781
1781
|
}
|
|
1782
1782
|
];
|
|
1783
1783
|
var fixtureMeta = (id) => {
|
|
@@ -1815,7 +1815,7 @@ var formatIssues = (error) => {
|
|
|
1815
1815
|
};
|
|
1816
1816
|
function assertFiatStepResponse(json) {
|
|
1817
1817
|
const result = FiatStepResponseSchema.safeParse(json);
|
|
1818
|
-
if (!result.success) throw new ContractViolation("invalid
|
|
1818
|
+
if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
|
|
1819
1819
|
return result.data;
|
|
1820
1820
|
}
|
|
1821
1821
|
function endpointMatches(template, actual) {
|
|
@@ -1912,14 +1912,14 @@ function walkTable(fn) {
|
|
|
1912
1912
|
function assertFixture(id) {
|
|
1913
1913
|
const meta = fixtureMeta(id);
|
|
1914
1914
|
const json = loadFixture(id);
|
|
1915
|
-
const
|
|
1916
|
-
if (stateKey(
|
|
1915
|
+
const stepResponse = assertFiatStepResponse(json);
|
|
1916
|
+
if (stateKey(stepResponse.state) !== meta.stateKey) {
|
|
1917
1917
|
throw new ContractViolation(
|
|
1918
|
-
`fixture ${id}: manifest says ${meta.stateKey},
|
|
1918
|
+
`fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
|
|
1919
1919
|
);
|
|
1920
1920
|
}
|
|
1921
|
-
assertLegalEmission(
|
|
1922
|
-
return { meta,
|
|
1921
|
+
assertLegalEmission(stepResponse.state, stepResponse.transitions);
|
|
1922
|
+
return { meta, stepResponse };
|
|
1923
1923
|
}
|
|
1924
1924
|
function assertAllFixtures() {
|
|
1925
1925
|
return FIXTURES.map((meta) => assertFixture(meta.id));
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isTerminal,
|
|
9
9
|
stateKey,
|
|
10
10
|
tableEntry
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EHYDQU4T.mjs";
|
|
12
12
|
|
|
13
13
|
// src/codes.ts
|
|
14
14
|
var COUNTRY_CURRENCY_LIST = [
|
|
@@ -1273,18 +1273,18 @@ var FIXTURES = [
|
|
|
1273
1273
|
docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
|
|
1274
1274
|
stateKey: "PAYMENT/INSTRUCT",
|
|
1275
1275
|
notes: [
|
|
1276
|
-
"The
|
|
1276
|
+
"The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
|
|
1277
1277
|
]
|
|
1278
1278
|
}
|
|
1279
1279
|
];
|
|
1280
1280
|
var FIXTURE_COVERAGE_GAPS = [
|
|
1281
1281
|
{
|
|
1282
1282
|
stateKey: "FUN_AUTH",
|
|
1283
|
-
reason: "No
|
|
1283
|
+
reason: "No step response exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
|
|
1284
1284
|
},
|
|
1285
1285
|
{
|
|
1286
1286
|
stateKey: "KYC/CAPTURE",
|
|
1287
|
-
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no
|
|
1287
|
+
reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no step response was ever published. Kind retained as handshake-gated vocabulary."
|
|
1288
1288
|
}
|
|
1289
1289
|
];
|
|
1290
1290
|
var fixtureMeta = (id) => {
|
|
@@ -1322,7 +1322,7 @@ var formatIssues = (error) => {
|
|
|
1322
1322
|
};
|
|
1323
1323
|
function assertFiatStepResponse(json) {
|
|
1324
1324
|
const result = FiatStepResponseSchema.safeParse(json);
|
|
1325
|
-
if (!result.success) throw new ContractViolation("invalid
|
|
1325
|
+
if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
|
|
1326
1326
|
return result.data;
|
|
1327
1327
|
}
|
|
1328
1328
|
function endpointMatches(template, actual) {
|
|
@@ -1419,14 +1419,14 @@ function walkTable(fn) {
|
|
|
1419
1419
|
function assertFixture(id) {
|
|
1420
1420
|
const meta = fixtureMeta(id);
|
|
1421
1421
|
const json = loadFixture(id);
|
|
1422
|
-
const
|
|
1423
|
-
if (stateKey(
|
|
1422
|
+
const stepResponse = assertFiatStepResponse(json);
|
|
1423
|
+
if (stateKey(stepResponse.state) !== meta.stateKey) {
|
|
1424
1424
|
throw new ContractViolation(
|
|
1425
|
-
`fixture ${id}: manifest says ${meta.stateKey},
|
|
1425
|
+
`fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
|
|
1426
1426
|
);
|
|
1427
1427
|
}
|
|
1428
|
-
assertLegalEmission(
|
|
1429
|
-
return { meta,
|
|
1428
|
+
assertLegalEmission(stepResponse.state, stepResponse.transitions);
|
|
1429
|
+
return { meta, stepResponse };
|
|
1430
1430
|
}
|
|
1431
1431
|
function assertAllFixtures() {
|
|
1432
1432
|
return FIXTURES.map((meta) => assertFixture(meta.id));
|
package/dist/schemas.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* fiat-contract — zod schemas
|
|
3
3
|
*
|
|
4
4
|
* zod is the single runtime validator: TypeScript cannot validate at the boundary, and the
|
|
5
|
-
* table (`src/table.ts`) cannot reject a *malformed*
|
|
5
|
+
* table (`src/table.ts`) cannot reject a *malformed* step response — only an illegal *sequence*.
|
|
6
6
|
*
|
|
7
7
|
* Every object schema is `.strict()`: an undeclared key is a contract violation, not extra data.
|
|
8
8
|
*
|
package/dist/table.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Rules the entries obey:
|
|
12
12
|
* - Terminality of `ORDER{CREATED}` rides `status`, because terminal outcomes are OrderStatus,
|
|
13
|
-
* never kinds (contract §The
|
|
13
|
+
* never kinds (contract §The fiat step response). The set is enumerated below as
|
|
14
14
|
* `TERMINAL_ORDER_STATUSES` — clients read it, they never derive it.
|
|
15
15
|
* - A terminal *state* carries no transitions. A terminal *status* still can: `FAILED` with a
|
|
16
16
|
* retryable `failureReason` carries the recovery CTA (FE doc Screen 14).
|
|
@@ -32,7 +32,7 @@ export declare const TABLE_VERSION: string;
|
|
|
32
32
|
*/
|
|
33
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';
|
|
34
34
|
/**
|
|
35
|
-
* Terminal per the OrderStatus machine's monotonic ranks (contract §The
|
|
35
|
+
* Terminal per the OrderStatus machine's monotonic ranks (contract §The fiat step response).
|
|
36
36
|
*
|
|
37
37
|
* `CAPTURE_ORPHANED` is deliberately absent — see the TODO(open-decision) on the value itself.
|
|
38
38
|
* Non-terminal is the safe default for an unknown status: a client that wrongly believes the flow
|
|
@@ -48,7 +48,7 @@ export type TerminalOrderStatus = (typeof TERMINAL_ORDER_STATUSES)[number];
|
|
|
48
48
|
export type EndpointTemplate = FiatEndpoint;
|
|
49
49
|
export interface AllowedTransition {
|
|
50
50
|
/**
|
|
51
|
-
* The id observed in the docs'
|
|
51
|
+
* The id observed in the docs' step responses. Informational: ids are localization keys and the
|
|
52
52
|
* docs explicitly allow unknown ids (the client falls back to `labelFallback`), so
|
|
53
53
|
* `assertLegalEmission` matches on `mode` + `endpoint`, not on `id`.
|
|
54
54
|
*/
|
package/dist/table.js
CHANGED
package/dist/table.mjs
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* fiat-contract — types
|
|
3
3
|
*
|
|
4
|
-
* The published contract between `fun-backend` (emits
|
|
4
|
+
* The published contract between `fun-backend` (emits step responses) and
|
|
5
5
|
* `funkit`/`connect-core` (renders them). Zero runtime logic lives here.
|
|
6
6
|
*
|
|
7
7
|
* Sources of truth (do not extend this file from anywhere else):
|
|
8
8
|
* - Contract doc: Fiat Client Contract — Step-Driven Flow & Endpoint Schematics
|
|
9
9
|
* https://app.notion.com/p/3b9fc3b2a002815eb270fa4c818268cc
|
|
10
|
-
* §"The
|
|
10
|
+
* §"The fiat step response" · §"Conformance package — concrete spec"
|
|
11
11
|
* §"Proposal (2026-08-13): split InputSpec → params + inputs + expects" (ACCEPTED)
|
|
12
12
|
* - FE doc: Fiat Frontend — State Machine & Screen Map
|
|
13
13
|
* https://app.notion.com/p/3bbfc3b2a00281c994c2cebd17b1d6d3
|
|
@@ -59,12 +59,12 @@ export type HttpVerb = 'GET' | 'POST';
|
|
|
59
59
|
export type FiatEndpoint = 'GET /fiat/payment-methods' | 'POST /fiat/quote' | 'POST /fiat/auth' | 'POST /fiat/payment-session' | 'POST /fiat/session' | 'POST /fiat/session/verify' | 'GET /fiat/kyc' | 'POST /fiat/kyc/form' | 'POST /fiat/kyc/document' | 'POST /fiat/instruments' | 'POST /fiat/orders' | `GET /fiat/orders/${string}` | `GET /fiat/orders/${string}/instructions` | `POST /fiat/orders/${string}/confirm-payment` | `POST /fiat/orders/${string}/reference` | `POST /fiat/orders/${string}/cancel` | `POST /fiat/orders/${string}/surface-result`;
|
|
60
60
|
/**
|
|
61
61
|
* FiatProvider is metadata: it selects the embedded provider SDK for CLIENT_SURFACE steps and
|
|
62
|
-
* feeds analytics. It never drives sequencing (contract §The
|
|
62
|
+
* feeds analytics. It never drives sequencing (contract §The fiat step response).
|
|
63
63
|
* TODO(open-decision): the docs name only Transak as a v1 provider for the headless flow
|
|
64
64
|
* (contract §Decisions 3; Swapped-headless does not exist yet, Banxa is Tradeoff #4).
|
|
65
65
|
* `SWAPPED` added at fun-backend's request (2026-08-17) so its legacy fops payment-provider id
|
|
66
66
|
* has one shared vocabulary instead of a second fun-backend-local enum — it never appears as a
|
|
67
|
-
* `FiatStepResponse.provider` value since Swapped's fops flow doesn't emit this
|
|
67
|
+
* `FiatStepResponse.provider` value since Swapped's fops flow doesn't emit this response.
|
|
68
68
|
*/
|
|
69
69
|
export type FiatProvider = 'TRANSAK' | 'SWAPPED';
|
|
70
70
|
/**
|
|
@@ -101,7 +101,7 @@ export type FailureCategory = 'PAYMENT_DECLINED' | 'KYC_REJECTED' | 'PROVIDER_OU
|
|
|
101
101
|
* (the FE doc's Screen 11 note writes `SUPPORT` as shorthand — same value).
|
|
102
102
|
*/
|
|
103
103
|
export type Recovery = 'REQUOTE' | 'RETRY_PAYMENT' | 'CONTACT_SUPPORT' | 'NONE';
|
|
104
|
-
/** Three expiries, three recoveries (contract §The
|
|
104
|
+
/** Three expiries, three recoveries (contract §The fiat step response, FailureReason comment). */
|
|
105
105
|
export type ExpiredWhich = 'QUOTE' | 'PAYMENT_WINDOW' | 'INSTRUCTIONS';
|
|
106
106
|
/**
|
|
107
107
|
* `code?` is a copy key (e.g. `BAD_CODE`), not an error code to branch on.
|
|
@@ -121,7 +121,7 @@ export type FailureReason = {
|
|
|
121
121
|
code?: string;
|
|
122
122
|
};
|
|
123
123
|
/**
|
|
124
|
-
* Amounts are decimal strings in the docs'
|
|
124
|
+
* Amounts are decimal strings in the docs' step responses ("100.00") — never numbers.
|
|
125
125
|
* Named to pair with `CryptoAmount`: every use site is a `fiat:` field.
|
|
126
126
|
* `currency` is ISO 4217 — see `./codes`.
|
|
127
127
|
*/
|
|
@@ -172,7 +172,7 @@ export interface OrderRef {
|
|
|
172
172
|
}
|
|
173
173
|
/**
|
|
174
174
|
* Payload of `ORDER{CREATED}` (FE doc Screens 7 · 13 · 14 · 15).
|
|
175
|
-
* `crypto` is optional because the Screen 14/15
|
|
175
|
+
* `crypto` is optional because the Screen 14/15 responses omit it (fiat-only failure/refund views).
|
|
176
176
|
*/
|
|
177
177
|
export interface OrderSummary {
|
|
178
178
|
orderId: string;
|
|
@@ -228,8 +228,8 @@ export interface InstructionField {
|
|
|
228
228
|
/**
|
|
229
229
|
* Payload of `PAYMENT{INSTRUCT}` — push rails. Values are PII-adjacent: never logged.
|
|
230
230
|
* TODO(open-decision): contract OQ1 lists `QR_IMAGE` as *deliberately not pre-declared* in the
|
|
231
|
-
* v1 vocabulary while §The
|
|
232
|
-
*
|
|
231
|
+
* v1 vocabulary while §The fiat step response's Instructions bullet enumerates it. Kept in the union
|
|
232
|
+
* per that section (and the conformance-package spec); handshake-gate it if OQ1 lands the
|
|
233
233
|
* other way.
|
|
234
234
|
* TODO(open-decision): contract OQ7 — expired instructions have no documented recovery and no
|
|
235
235
|
* re-issue endpoint exists.
|
|
@@ -437,7 +437,7 @@ export type FlowState = {
|
|
|
437
437
|
* empty transitions, NOT terminal.
|
|
438
438
|
* - `OrderStatus.ON_HOLD` = post-order compliance or amount mismatch, resolves to
|
|
439
439
|
* `PROCESSING` or `REFUNDED`.
|
|
440
|
-
* They cannot be confused in code — different types, different positions in the
|
|
440
|
+
* They cannot be confused in code — different types, different positions in the response —
|
|
441
441
|
* but they can be confused in conversation, so say which one you mean.
|
|
442
442
|
*/
|
|
443
443
|
reason: 'IN_REVIEW' | 'ON_HOLD' | 'REJECTED';
|
|
@@ -480,7 +480,7 @@ export type FlowState = {
|
|
|
480
480
|
kind: 'PAYMENT';
|
|
481
481
|
phase: 'INSTRUCT';
|
|
482
482
|
/**
|
|
483
|
-
* TODO(open-decision): `orderId` placement —
|
|
483
|
+
* TODO(open-decision): `orderId` placement — beside `state` (contract §Worked example,
|
|
484
484
|
* steps 4–5) vs inside state (FE doc v0, Screen 6). Both are accepted here; the FE doc's
|
|
485
485
|
* in-state placement is the current shape, `FiatStepResponse.orderId` covers the other.
|
|
486
486
|
*/
|
|
@@ -495,7 +495,7 @@ export type FlowState = {
|
|
|
495
495
|
} | {
|
|
496
496
|
kind: 'ORDER';
|
|
497
497
|
phase: 'CREATED';
|
|
498
|
-
/** Absent in the contract's terse worked-example
|
|
498
|
+
/** Absent in the contract's terse worked-example responses, present in every FE screen. */
|
|
499
499
|
order?: OrderSummary;
|
|
500
500
|
status: OrderStatus;
|
|
501
501
|
statusHistory?: StatusHistoryEntry[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fun-xyz/fiat-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Published conformance contract for Fun's headless fiat onramp: FlowState/Transition types, zod schemas, the transition table as data, recorded fixtures, assertion helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|