@fun-xyz/fiat-contract 0.5.0 → 0.7.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/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).
@@ -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 envelope).
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' envelopes. Informational: ids are localization keys and the
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
  */
@@ -84,10 +84,45 @@ export interface TableEntry {
84
84
  export declare const TRANSITION_TABLE: Readonly<Record<StateKey, TableEntry>>;
85
85
  export declare const STATE_KEYS: StateKey[];
86
86
  /**
87
- * The `/fiat/*` surface as enumerated in contract §Endpoints. Informational data — emissions are
88
- * checked against each state's `allowedTransitions`, not against this list.
87
+ * The `/fiat/*` surface. Informational data — emissions are checked against each state's
88
+ * `allowedTransitions`, not against this list.
89
+ *
90
+ * Sourced from contract §Endpoints, with one deliberate divergence: `POST /fiat/payment-session` is
91
+ * named by the table (QUOTE's capture-first entry) while the doc's endpoint table still lists no
92
+ * session-minting call for that rail.
93
+ */
94
+ export declare const DOCUMENTED_ENDPOINTS: readonly ["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/:id", "GET /fiat/orders/:id/instructions", "POST /fiat/orders/:id/confirm-payment", "POST /fiat/orders/:id/reference", "POST /fiat/orders/:id/cancel", "POST /fiat/orders/:id/surface-result"];
95
+ /**
96
+ * Documented endpoints no flow state offers, and why. Declared so an endpoint nothing can reach is
97
+ * visible instead of silent — the table test asserts the unoffered set equals exactly these.
98
+ */
99
+ export declare const UNOFFERED_ENDPOINTS: readonly {
100
+ endpoint: EndpointTemplate;
101
+ reason: string;
102
+ }[];
103
+ /**
104
+ * States entered out-of-band rather than by returning from a call. Declared so an unreachable state
105
+ * is visible instead of silent — the table test asserts reachability from QUOTE equals (all state
106
+ * keys − these).
107
+ */
108
+ export declare const UNREACHABLE_STATES: readonly {
109
+ stateKey: StateKey;
110
+ reason: string;
111
+ }[];
112
+ /**
113
+ * The live answer to what `UNOFFERED_ENDPOINTS` declares: documented endpoints that no state's
114
+ * `allowedTransitions` names. The table test asserts the two agree, so a new orphan and a stale
115
+ * declaration both fail.
116
+ */
117
+ export declare function unofferedEndpoints(): readonly EndpointTemplate[];
118
+ /**
119
+ * The live answer to what `UNREACHABLE_STATES` declares: states no `mayReturn` path reaches from
120
+ * `QUOTE`, the flow's entry. The table test asserts the two agree.
121
+ *
122
+ * `mayReturn` is the only edge set the contract publishes, so it is the only graph there is to walk
123
+ * — a state absent from every one of them cannot be arrived at by following the contract.
89
124
  */
90
- export declare const DOCUMENTED_ENDPOINTS: readonly ["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/:id", "GET /fiat/orders/:id/instructions", "POST /fiat/orders/:id/confirm-payment", "POST /fiat/orders/:id/reference", "POST /fiat/orders/:id/cancel", "POST /fiat/orders/:id/surface-result"];
125
+ export declare function unreachableStates(from?: StateKey): readonly StateKey[];
91
126
  /** State → its table key. The one place kind/phase/reason is flattened. */
92
127
  export declare function stateKey(state: FlowState): StateKey;
93
128
  export declare const tableEntry: (state: FlowState) => TableEntry;
package/dist/table.js CHANGED
@@ -25,14 +25,18 @@ __export(table_exports, {
25
25
  TABLE_VERSION: () => TABLE_VERSION,
26
26
  TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
27
27
  TRANSITION_TABLE: () => TRANSITION_TABLE,
28
+ UNOFFERED_ENDPOINTS: () => UNOFFERED_ENDPOINTS,
29
+ UNREACHABLE_STATES: () => UNREACHABLE_STATES,
28
30
  isTerminal: () => isTerminal,
29
31
  stateKey: () => stateKey,
30
- tableEntry: () => tableEntry
32
+ tableEntry: () => tableEntry,
33
+ unofferedEndpoints: () => unofferedEndpoints,
34
+ unreachableStates: () => unreachableStates
31
35
  });
32
36
  module.exports = __toCommonJS(table_exports);
33
37
 
34
38
  // package.json
35
- var version = "0.5.0";
39
+ var version = "0.7.0";
36
40
 
37
41
  // src/table.ts
38
42
  var TABLE_VERSION = version;
@@ -63,22 +67,46 @@ var TRANSITION_TABLE = {
63
67
  endpoint: "POST /fiat/session",
64
68
  when: "no valid provider session token (own-tables check)"
65
69
  },
70
+ {
71
+ id: "open_session",
72
+ mode: "SUBMIT",
73
+ endpoint: "POST /fiat/payment-session",
74
+ when: "returning user, session + KYC valid, capture-first rail \u2014 the method authorises before any order exists",
75
+ note: "Returns PAYMENT{CAPTURE} carrying the Surface. Without this entry the capture-first rail has no legal way in: a transition-following client takes POST /fiat/orders instead, and the backend refuses that when the provider declares the capture-first capability and no authorisation is present. Named open_session per the Fiat Contract 0.7 doc, which adds quote-time-minted surface rows beside it."
76
+ },
66
77
  {
67
78
  id: "continue",
68
79
  mode: "SUBMIT",
69
80
  endpoint: "POST /fiat/orders",
70
- when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
81
+ when: "returning user, session + KYC valid, order-first rail \u2014 the quote screen doubles as review",
82
+ note: "Narrowed to the order-first rail: on a capture-first method the order cannot be created before the payment is authorised, so the sibling payment-session entry is the one that applies."
71
83
  },
72
84
  {
73
85
  id: "continue",
74
86
  mode: "SUBMIT",
75
87
  endpoint: "POST /fiat/auth",
76
88
  when: "no ambient Fun identity (non-fomo surfaces only)"
89
+ },
90
+ {
91
+ id: "capture",
92
+ mode: "CLIENT_SURFACE",
93
+ endpoint: "POST /fiat/orders",
94
+ when: "capture-then-order rail whose session is minted at quote time (inline pay sheet)",
95
+ note: "The 0.7 row that makes the single-screen Apple Pay design legal: the surface rides QUOTE, the provider component IS the primary CTA, and its result creates the order. Apple policy forbids a visible step between an Apple-branded tap and the sheet, and TransakApplePay exposes no programmatic trigger, so a two-step via payment-session cannot serve this rail. Requires state.surface (\u03942)."
96
+ },
97
+ {
98
+ id: "capture",
99
+ mode: "CLIENT_SURFACE",
100
+ endpoint: "POST /fiat/orders/:id/surface-result",
101
+ when: "order-then-capture rail whose provider order was created with the quote-time session",
102
+ note: "Same quote-time placement, other topology: the order already exists, so the surface reports against it rather than creating one."
77
103
  }
78
104
  ],
79
105
  mayReturn: [
80
106
  "SESSION_AUTH",
81
107
  ...KYC_ANY,
108
+ "PAYMENT/CAPTURE",
109
+ "PAYMENT/INSTRUCT",
82
110
  "PENDING_ORDER",
83
111
  "ORDER/AWAITING_CONFIRMATION",
84
112
  "ORDER/CREATED",
@@ -86,8 +114,10 @@ var TRANSITION_TABLE = {
86
114
  ],
87
115
  terminal: false,
88
116
  notes: [
89
- "Re-quote on amount change is a client-local event, not a transition.",
90
- 'If the PENDING_ORDER removal lands, QUOTE additionally returns PAYMENT{INSTRUCT} / ORDER{CREATED} directly. TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER".'
117
+ "Re-quote on amount change is a client-local event, not a transition. A new quote carries a new surface; the client remounts the adapter rather than reusing the old session.",
118
+ "A surface on QUOTE and a SUBMIT to the same endpoint may not coexist \u2014 two paths to one order. assertLegalEmission enforces it; the server picks one per rail.",
119
+ `Both PAYMENT states are reachable directly from QUOTE: the FE doc canvas draws S1 \u2192 S5 (card, capture-first) and S1 \u2192 S6 (bank). This entry was first ported from the FE doc's bindings table, whose "May receive back" column omits both \u2014 the canvas is the correct source of the two.`,
120
+ 'TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER" would additionally have QUOTE return ORDER{CREATED} for an active order. That decision does not gate the PAYMENT edges above.'
91
121
  ]
92
122
  },
93
123
  FUN_AUTH: {
@@ -385,6 +415,7 @@ var DOCUMENTED_ENDPOINTS = [
385
415
  "GET /fiat/payment-methods",
386
416
  "POST /fiat/quote",
387
417
  "POST /fiat/auth",
418
+ "POST /fiat/payment-session",
388
419
  "POST /fiat/session",
389
420
  "POST /fiat/session/verify",
390
421
  "GET /fiat/kyc",
@@ -399,6 +430,54 @@ var DOCUMENTED_ENDPOINTS = [
399
430
  "POST /fiat/orders/:id/cancel",
400
431
  "POST /fiat/orders/:id/surface-result"
401
432
  ];
433
+ var UNOFFERED_ENDPOINTS = [
434
+ {
435
+ endpoint: "GET /fiat/payment-methods",
436
+ reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
437
+ },
438
+ {
439
+ endpoint: "POST /fiat/kyc/document",
440
+ reason: "FILE fields stream to it directly; the harness owns that upload path and it is not a transition (see KYC/INPUT_REQUIRED notes)."
441
+ },
442
+ {
443
+ endpoint: "POST /fiat/instruments",
444
+ reason: "No state offers it and no published doc explains why. TODO(open-decision): saved instruments may simply be unreached in v1 \u2014 confirm, or wire it to the state that should offer it."
445
+ },
446
+ {
447
+ endpoint: "GET /fiat/orders/:id/instructions",
448
+ reason: "PAYMENT{INSTRUCT} carries its instructions in state and offers no fetch. TODO(open-decision): that state also notes no re-issue endpoint exists, which sits oddly beside this one being published \u2014 confirm or retire it."
449
+ },
450
+ {
451
+ endpoint: "POST /fiat/orders/:id/reference",
452
+ reason: "The bank-reference submission is offered by no state. TODO(open-decision): PAYMENT{INSTRUCT} is the plausible home \u2014 confirm or wire it."
453
+ }
454
+ ];
455
+ var UNREACHABLE_STATES = [
456
+ {
457
+ stateKey: "FUN_AUTH",
458
+ reason: "No bindings row published (docStatus UNSPECIFIED) and never rendered in fomo, where identity is ambient. Entered out-of-band, not by a mayReturn edge."
459
+ }
460
+ ];
461
+ function unofferedEndpoints() {
462
+ const offered = /* @__PURE__ */ new Set();
463
+ for (const key of STATE_KEYS) {
464
+ for (const allowed of TRANSITION_TABLE[key].allowedTransitions) offered.add(allowed.endpoint);
465
+ }
466
+ return DOCUMENTED_ENDPOINTS.filter((endpoint) => !offered.has(endpoint));
467
+ }
468
+ function unreachableStates(from = "QUOTE") {
469
+ const seen = /* @__PURE__ */ new Set([from]);
470
+ const queue = [from];
471
+ while (queue.length > 0) {
472
+ for (const target of TRANSITION_TABLE[queue.pop()].mayReturn ?? []) {
473
+ if (!seen.has(target)) {
474
+ seen.add(target);
475
+ queue.push(target);
476
+ }
477
+ }
478
+ }
479
+ return STATE_KEYS.filter((key) => !seen.has(key));
480
+ }
402
481
  function stateKey(state) {
403
482
  switch (state.kind) {
404
483
  case "QUOTE":
package/dist/table.mjs CHANGED
@@ -4,18 +4,26 @@ import {
4
4
  TABLE_VERSION,
5
5
  TERMINAL_ORDER_STATUSES,
6
6
  TRANSITION_TABLE,
7
+ UNOFFERED_ENDPOINTS,
8
+ UNREACHABLE_STATES,
7
9
  isTerminal,
8
10
  stateKey,
9
- tableEntry
10
- } from "./chunk-3R5GGUAK.mjs";
11
+ tableEntry,
12
+ unofferedEndpoints,
13
+ unreachableStates
14
+ } from "./chunk-OGNYHIOE.mjs";
11
15
  export {
12
16
  DOCUMENTED_ENDPOINTS,
13
17
  STATE_KEYS,
14
18
  TABLE_VERSION,
15
19
  TERMINAL_ORDER_STATUSES,
16
20
  TRANSITION_TABLE,
21
+ UNOFFERED_ENDPOINTS,
22
+ UNREACHABLE_STATES,
17
23
  isTerminal,
18
24
  stateKey,
19
- tableEntry
25
+ tableEntry,
26
+ unofferedEndpoints,
27
+ unreachableStates
20
28
  };
21
29
  //# sourceMappingURL=table.mjs.map
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,14 +59,16 @@ 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
- * `FiatStepResponse.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
+ * `BANXA` added at 0.7 (Provider Landscape: Tier 1). A vendor name here is a routing key only —
69
+ * it never decides which fields exist, which is what keeps provider churn out of this package.
68
70
  */
69
- export type FiatProvider = 'TRANSAK' | 'SWAPPED';
71
+ export type FiatProvider = 'TRANSAK' | 'SWAPPED' | 'BANXA';
70
72
  /**
71
73
  * The order status vocabulary. Monotonic ranks, no backwards moves. Terminal outcomes are
72
74
  * OrderStatus, never FlowState kinds.
@@ -101,7 +103,7 @@ export type FailureCategory = 'PAYMENT_DECLINED' | 'KYC_REJECTED' | 'PROVIDER_OU
101
103
  * (the FE doc's Screen 11 note writes `SUPPORT` as shorthand — same value).
102
104
  */
103
105
  export type Recovery = 'REQUOTE' | 'RETRY_PAYMENT' | 'CONTACT_SUPPORT' | 'NONE';
104
- /** Three expiries, three recoveries (contract §The envelope, FailureReason comment). */
106
+ /** Three expiries, three recoveries (contract §The fiat step response, FailureReason comment). */
105
107
  export type ExpiredWhich = 'QUOTE' | 'PAYMENT_WINDOW' | 'INSTRUCTIONS';
106
108
  /**
107
109
  * `code?` is a copy key (e.g. `BAD_CODE`), not an error code to branch on.
@@ -121,7 +123,7 @@ export type FailureReason = {
121
123
  code?: string;
122
124
  };
123
125
  /**
124
- * Amounts are decimal strings in the docs' envelopes ("100.00") — never numbers.
126
+ * Amounts are decimal strings in the docs' step responses ("100.00") — never numbers.
125
127
  * Named to pair with `CryptoAmount`: every use site is a `fiat:` field.
126
128
  * `currency` is ISO 4217 — see `./codes`.
127
129
  */
@@ -172,7 +174,7 @@ export interface OrderRef {
172
174
  }
173
175
  /**
174
176
  * 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).
177
+ * `crypto` is optional because the Screen 14/15 responses omit it (fiat-only failure/refund views).
176
178
  */
177
179
  export interface OrderSummary {
178
180
  orderId: string;
@@ -199,27 +201,45 @@ export interface Refund {
199
201
  amount: string;
200
202
  expectedBy: string;
201
203
  }
204
+ /** What the user sees, and therefore where it can be placed. */
205
+ export type SurfaceKind = 'URL_REDIRECT' | 'URL_EMBED' | 'PCI_COMPONENT' | 'PAY_SHEET' | 'ACH_COMPONENT';
202
206
  /**
203
- * Carried as *data* in CAPTURE phases only (`PAYMENT{CAPTURE}` · `KYC{CAPTURE}`).
204
- * Δ2: the CLIENT_SURFACE transition carries just `report`single-carry, no duplication.
205
- * `session` values are secrets: never log them (FE doc §Decisions, redaction).
207
+ * Identity vendors Fun verifies into. Its own axis rather than a `FiatProvider` value, because
208
+ * Fun is the KYC-of-record in its own tenantan identity surface is never the onramp's to mint.
209
+ * `SUMSUB` alone at 0.7; adding a value later is additive, and a vendor with no installed adapter
210
+ * lands on the client's existing unsupported screen.
206
211
  */
207
- export type Surface = {
208
- kind: 'URL_REDIRECT';
209
- url: string;
210
- } | {
211
- kind: 'URL_EMBED';
212
- url: string;
213
- } | {
214
- kind: 'PCI_COMPONENT';
215
- session: string;
216
- } | {
217
- kind: 'PAY_SHEET';
218
- session: string;
219
- } | {
220
- kind: 'ACH_COMPONENT';
221
- session: string;
222
- };
212
+ export type KYCProvider = 'SUMSUB';
213
+ /**
214
+ * Which adapter renders the surface. Can differ from `FiatStepResponse.provider`: an identity
215
+ * surface is Fun's to mint, not the onramp's.
216
+ *
217
+ * A routing key only — it never narrows the shape of `params`.
218
+ */
219
+ export type SurfaceProvider = FiatProvider | KYCProvider;
220
+ /**
221
+ * Carried as data in `QUOTE` (inline in the entry screen's primary slot) and in the CAPTURE
222
+ * phases (a screen of its own). Δ2 unchanged: the CLIENT_SURFACE transition carries only
223
+ * `report`, so a surface is never described twice.
224
+ *
225
+ * `params` is the adapter's vocabulary — opaque here, narrowed at the adapter boundary, and
226
+ * honoured verbatim. A provider's own `environment` value lives here and is never normalised:
227
+ * Transak's is `LOCAL | DEVELOPMENT | STAGING | PRODUCTION`, Banxa's is `sandbox | production`,
228
+ * and URL surfaces carry none, so a Fun-owned enum could only be lossy.
229
+ *
230
+ * `params` values are secrets by default. Client redaction defaults every key closed and opens
231
+ * only names that are safe under *any* provider's vocabulary.
232
+ */
233
+ export interface Surface {
234
+ kind: SurfaceKind;
235
+ provider: SurfaceProvider;
236
+ params: Record<string, JsonValue>;
237
+ /**
238
+ * When the provider stops honouring the session, if it says so. Lets a client re-quote before
239
+ * the button goes dead; absent, the surface lives as long as the quote that carried it.
240
+ */
241
+ expiresAt?: string;
242
+ }
223
243
  export interface InstructionField {
224
244
  id: string;
225
245
  labelFallback: string;
@@ -228,8 +248,8 @@ export interface InstructionField {
228
248
  /**
229
249
  * Payload of `PAYMENT{INSTRUCT}` — push rails. Values are PII-adjacent: never logged.
230
250
  * 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
251
+ * v1 vocabulary while §The fiat step response's Instructions bullet enumerates it. Kept in the union
252
+ * per that section (and the conformance-package spec); handshake-gate it if OQ1 lands the
233
253
  * other way.
234
254
  * TODO(open-decision): contract OQ7 — expired instructions have no documented recovery and no
235
255
  * re-issue endpoint exists.
@@ -402,9 +422,16 @@ export type AuthChallenge = Record<string, JsonValue>;
402
422
  * 2. terminal outcomes are OrderStatus, never kinds
403
423
  * 3. every fallible state carries `error?: FailureReason`
404
424
  */
405
- export type FlowState = {
425
+ export type FlowState =
426
+ /**
427
+ * `surface` present means the provider's own component IS the primary CTA, inline on the entry
428
+ * screen — the placement Apple policy forces for a pay sheet, since no visible step may sit
429
+ * between the Apple-branded tap and the sheet. Absent, the quote's SUBMITs drive our button.
430
+ */
431
+ {
406
432
  kind: 'QUOTE';
407
433
  quote: Quote;
434
+ surface?: Surface;
408
435
  error?: FailureReason;
409
436
  } | {
410
437
  kind: 'FUN_AUTH';
@@ -437,7 +464,7 @@ export type FlowState = {
437
464
  * empty transitions, NOT terminal.
438
465
  * - `OrderStatus.ON_HOLD` = post-order compliance or amount mismatch, resolves to
439
466
  * `PROCESSING` or `REFUNDED`.
440
- * They cannot be confused in code — different types, different positions in the envelope
467
+ * They cannot be confused in code — different types, different positions in the response
441
468
  * but they can be confused in conversation, so say which one you mean.
442
469
  */
443
470
  reason: 'IN_REVIEW' | 'ON_HOLD' | 'REJECTED';
@@ -480,7 +507,7 @@ export type FlowState = {
480
507
  kind: 'PAYMENT';
481
508
  phase: 'INSTRUCT';
482
509
  /**
483
- * TODO(open-decision): `orderId` placement — envelope sibling (contract §Worked example,
510
+ * TODO(open-decision): `orderId` placement — beside `state` (contract §Worked example,
484
511
  * steps 4–5) vs inside state (FE doc v0, Screen 6). Both are accepted here; the FE doc's
485
512
  * in-state placement is the current shape, `FiatStepResponse.orderId` covers the other.
486
513
  */
@@ -495,7 +522,7 @@ export type FlowState = {
495
522
  } | {
496
523
  kind: 'ORDER';
497
524
  phase: 'CREATED';
498
- /** Absent in the contract's terse worked-example envelopes, present in every FE screen. */
525
+ /** Absent in the contract's terse worked-example responses, present in every FE screen. */
499
526
  order?: OrderSummary;
500
527
  status: OrderStatus;
501
528
  statusHistory?: StatusHistoryEntry[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fun-xyz/fiat-contract",
3
- "version": "0.5.0",
3
+ "version": "0.7.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
+ }