@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/index.js CHANGED
@@ -51,6 +51,7 @@ __export(index_exports, {
51
51
  InstructionsSchema: () => InstructionsSchema,
52
52
  InstrumentSchema: () => InstrumentSchema,
53
53
  JsonSchema: () => JsonSchema,
54
+ KYCProviderSchema: () => KYCProviderSchema,
54
55
  KycCaptureStateSchema: () => KycCaptureStateSchema,
55
56
  KycInputRequiredStateSchema: () => KycInputRequiredStateSchema,
56
57
  KycNoActionRequiredStateSchema: () => KycNoActionRequiredStateSchema,
@@ -73,6 +74,8 @@ __export(index_exports, {
73
74
  SelectOptionSchema: () => SelectOptionSchema,
74
75
  SessionAuthStateSchema: () => SessionAuthStateSchema,
75
76
  StatusHistoryEntrySchema: () => StatusHistoryEntrySchema,
77
+ SurfaceKindSchema: () => SurfaceKindSchema,
78
+ SurfaceProviderSchema: () => SurfaceProviderSchema,
76
79
  SurfaceSchema: () => SurfaceSchema,
77
80
  TABLE_VERSION: () => TABLE_VERSION,
78
81
  TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
@@ -81,6 +84,8 @@ __export(index_exports, {
81
84
  TransitionParamsSchema: () => TransitionParamsSchema,
82
85
  TransitionSchema: () => TransitionSchema,
83
86
  TxSchema: () => TxSchema,
87
+ UNOFFERED_ENDPOINTS: () => UNOFFERED_ENDPOINTS,
88
+ UNREACHABLE_STATES: () => UNREACHABLE_STATES,
84
89
  assertAllFixtures: () => assertAllFixtures,
85
90
  assertFiatStepResponse: () => assertFiatStepResponse,
86
91
  assertFixture: () => assertFixture,
@@ -95,6 +100,8 @@ __export(index_exports, {
95
100
  looksLikeFieldSpec: () => looksLikeFieldSpec,
96
101
  stateKey: () => stateKey,
97
102
  tableEntry: () => tableEntry,
103
+ unofferedEndpoints: () => unofferedEndpoints,
104
+ unreachableStates: () => unreachableStates,
98
105
  walkTable: () => walkTable
99
106
  });
100
107
  module.exports = __toCommonJS(index_exports);
@@ -380,8 +387,12 @@ var HTTP_VERBS = ["GET", "POST"];
380
387
  var ENDPOINT_PATTERN = /^(GET|POST) \/fiat\/[^\s]*$/;
381
388
  var $endpoint = import_zod.z.string().regex(ENDPOINT_PATTERN, 'endpoint must read "<GET|POST> /fiat/\u2026"');
382
389
  var FiatEndpointSchema = $endpoint;
383
- var $provider = import_zod.z.enum(["TRANSAK", "SWAPPED"]);
390
+ var $provider = import_zod.z.enum(["TRANSAK", "SWAPPED", "BANXA"]);
384
391
  var FiatProviderSchema = $provider;
392
+ var $kycProvider = import_zod.z.enum(["SUMSUB"]);
393
+ var KYCProviderSchema = $kycProvider;
394
+ var $surfaceProvider = import_zod.z.enum([...$provider.options, ...$kycProvider.options]);
395
+ var SurfaceProviderSchema = $surfaceProvider;
385
396
  var $orderStatus = import_zod.z.enum([
386
397
  "CREATING",
387
398
  "AWAITING_PAYMENT",
@@ -473,13 +484,22 @@ var $refund = obj({
473
484
  expectedBy: import_zod.z.string()
474
485
  });
475
486
  var RefundSchema = $refund;
476
- var $surface = import_zod.z.union([
477
- obj({ kind: import_zod.z.literal("URL_REDIRECT"), url: import_zod.z.string() }),
478
- obj({ kind: import_zod.z.literal("URL_EMBED"), url: import_zod.z.string() }),
479
- obj({ kind: import_zod.z.literal("PCI_COMPONENT"), session: import_zod.z.string() }),
480
- obj({ kind: import_zod.z.literal("PAY_SHEET"), session: import_zod.z.string() }),
481
- obj({ kind: import_zod.z.literal("ACH_COMPONENT"), session: import_zod.z.string() })
482
- ]);
487
+ var $surfaceKind = import_zod.z.enum(["URL_REDIRECT", "URL_EMBED", "PCI_COMPONENT", "PAY_SHEET", "ACH_COMPONENT"]);
488
+ var SurfaceKindSchema = $surfaceKind;
489
+ var $surface = obj({
490
+ kind: $surfaceKind,
491
+ provider: $surfaceProvider,
492
+ /**
493
+ * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
494
+ * vocabulary, so typing them here would drag every provider field change through a contract
495
+ * release and both consumers — and a normalised `environment` could only be lossy across
496
+ * Transak's four values and Banxa's two. Both ends narrow instead: fun-backend emits from a
497
+ * typed provider session, the client adapter validates with its own schema, and a recorded
498
+ * fixture per `(provider, kind)` keeps the two honest.
499
+ */
500
+ params: import_zod.z.record(import_zod.z.string(), JsonSchema),
501
+ expiresAt: import_zod.z.string().optional()
502
+ });
483
503
  var SurfaceSchema = $surface;
484
504
  var $instructionField = obj({
485
505
  id: import_zod.z.string(),
@@ -609,6 +629,8 @@ var AuthChallengeSchema = $authChallenge;
609
629
  var $quoteState = obj({
610
630
  kind: import_zod.z.literal("QUOTE"),
611
631
  quote: $quote,
632
+ /** Present when the provider's component is the primary CTA, inline on the entry screen. */
633
+ surface: $surface.optional(),
612
634
  error: $failureReason.optional()
613
635
  });
614
636
  var QuoteStateSchema = $quoteState;
@@ -718,7 +740,7 @@ var $stepResponse = obj({
718
740
  var FiatStepResponseSchema = $stepResponse;
719
741
 
720
742
  // package.json
721
- var version = "0.5.0";
743
+ var version = "0.7.0";
722
744
 
723
745
  // src/table.ts
724
746
  var TABLE_VERSION = version;
@@ -749,22 +771,46 @@ var TRANSITION_TABLE = {
749
771
  endpoint: "POST /fiat/session",
750
772
  when: "no valid provider session token (own-tables check)"
751
773
  },
774
+ {
775
+ id: "open_session",
776
+ mode: "SUBMIT",
777
+ endpoint: "POST /fiat/payment-session",
778
+ when: "returning user, session + KYC valid, capture-first rail \u2014 the method authorises before any order exists",
779
+ 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."
780
+ },
752
781
  {
753
782
  id: "continue",
754
783
  mode: "SUBMIT",
755
784
  endpoint: "POST /fiat/orders",
756
- when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
785
+ when: "returning user, session + KYC valid, order-first rail \u2014 the quote screen doubles as review",
786
+ 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."
757
787
  },
758
788
  {
759
789
  id: "continue",
760
790
  mode: "SUBMIT",
761
791
  endpoint: "POST /fiat/auth",
762
792
  when: "no ambient Fun identity (non-fomo surfaces only)"
793
+ },
794
+ {
795
+ id: "capture",
796
+ mode: "CLIENT_SURFACE",
797
+ endpoint: "POST /fiat/orders",
798
+ when: "capture-then-order rail whose session is minted at quote time (inline pay sheet)",
799
+ 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)."
800
+ },
801
+ {
802
+ id: "capture",
803
+ mode: "CLIENT_SURFACE",
804
+ endpoint: "POST /fiat/orders/:id/surface-result",
805
+ when: "order-then-capture rail whose provider order was created with the quote-time session",
806
+ note: "Same quote-time placement, other topology: the order already exists, so the surface reports against it rather than creating one."
763
807
  }
764
808
  ],
765
809
  mayReturn: [
766
810
  "SESSION_AUTH",
767
811
  ...KYC_ANY,
812
+ "PAYMENT/CAPTURE",
813
+ "PAYMENT/INSTRUCT",
768
814
  "PENDING_ORDER",
769
815
  "ORDER/AWAITING_CONFIRMATION",
770
816
  "ORDER/CREATED",
@@ -772,8 +818,10 @@ var TRANSITION_TABLE = {
772
818
  ],
773
819
  terminal: false,
774
820
  notes: [
775
- "Re-quote on amount change is a client-local event, not a transition.",
776
- 'If the PENDING_ORDER removal lands, QUOTE additionally returns PAYMENT{INSTRUCT} / ORDER{CREATED} directly. TODO(open-decision): contract \xA7"Proposal: remove PENDING_ORDER".'
821
+ "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.",
822
+ "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.",
823
+ `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.`,
824
+ '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.'
777
825
  ]
778
826
  },
779
827
  FUN_AUTH: {
@@ -1071,6 +1119,7 @@ var DOCUMENTED_ENDPOINTS = [
1071
1119
  "GET /fiat/payment-methods",
1072
1120
  "POST /fiat/quote",
1073
1121
  "POST /fiat/auth",
1122
+ "POST /fiat/payment-session",
1074
1123
  "POST /fiat/session",
1075
1124
  "POST /fiat/session/verify",
1076
1125
  "GET /fiat/kyc",
@@ -1085,6 +1134,54 @@ var DOCUMENTED_ENDPOINTS = [
1085
1134
  "POST /fiat/orders/:id/cancel",
1086
1135
  "POST /fiat/orders/:id/surface-result"
1087
1136
  ];
1137
+ var UNOFFERED_ENDPOINTS = [
1138
+ {
1139
+ endpoint: "GET /fiat/payment-methods",
1140
+ reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
1141
+ },
1142
+ {
1143
+ endpoint: "POST /fiat/kyc/document",
1144
+ reason: "FILE fields stream to it directly; the harness owns that upload path and it is not a transition (see KYC/INPUT_REQUIRED notes)."
1145
+ },
1146
+ {
1147
+ endpoint: "POST /fiat/instruments",
1148
+ 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."
1149
+ },
1150
+ {
1151
+ endpoint: "GET /fiat/orders/:id/instructions",
1152
+ 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."
1153
+ },
1154
+ {
1155
+ endpoint: "POST /fiat/orders/:id/reference",
1156
+ reason: "The bank-reference submission is offered by no state. TODO(open-decision): PAYMENT{INSTRUCT} is the plausible home \u2014 confirm or wire it."
1157
+ }
1158
+ ];
1159
+ var UNREACHABLE_STATES = [
1160
+ {
1161
+ stateKey: "FUN_AUTH",
1162
+ 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."
1163
+ }
1164
+ ];
1165
+ function unofferedEndpoints() {
1166
+ const offered = /* @__PURE__ */ new Set();
1167
+ for (const key of STATE_KEYS) {
1168
+ for (const allowed of TRANSITION_TABLE[key].allowedTransitions) offered.add(allowed.endpoint);
1169
+ }
1170
+ return DOCUMENTED_ENDPOINTS.filter((endpoint) => !offered.has(endpoint));
1171
+ }
1172
+ function unreachableStates(from = "QUOTE") {
1173
+ const seen = /* @__PURE__ */ new Set([from]);
1174
+ const queue = [from];
1175
+ while (queue.length > 0) {
1176
+ for (const target of TRANSITION_TABLE[queue.pop()].mayReturn ?? []) {
1177
+ if (!seen.has(target)) {
1178
+ seen.add(target);
1179
+ queue.push(target);
1180
+ }
1181
+ }
1182
+ }
1183
+ return STATE_KEYS.filter((key) => !seen.has(key));
1184
+ }
1088
1185
  function stateKey(state) {
1089
1186
  switch (state.kind) {
1090
1187
  case "QUOTE":
@@ -1167,6 +1264,64 @@ var FIXTURE_DATA = {
1167
1264
  }
1168
1265
  ]
1169
1266
  },
1267
+ "screen-01b-quote-with-pay-sheet": {
1268
+ "state": {
1269
+ "kind": "QUOTE",
1270
+ "quote": {
1271
+ "quoteRef": "q_8f2",
1272
+ "fiat": {
1273
+ "currency": "GBP",
1274
+ "amount": "100.00"
1275
+ },
1276
+ "crypto": {
1277
+ "currency": "USDC",
1278
+ "network": "polygon",
1279
+ "amount": "98.41"
1280
+ },
1281
+ "rate": "0.9841",
1282
+ "totalFee": "1.59",
1283
+ "feeBreakdown": [
1284
+ {
1285
+ "id": "provider_fee",
1286
+ "labelFallback": "Processing fee",
1287
+ "amount": "1.09"
1288
+ },
1289
+ {
1290
+ "id": "network_fee",
1291
+ "labelFallback": "Network fee",
1292
+ "amount": "0.50"
1293
+ }
1294
+ ],
1295
+ "paymentMethod": "bank_transfer",
1296
+ "expiresAt": "\u2026"
1297
+ },
1298
+ "surface": {
1299
+ "kind": "PAY_SHEET",
1300
+ "provider": "TRANSAK",
1301
+ "params": {
1302
+ "sessionId": "txs_\u2026",
1303
+ "environment": "STAGING"
1304
+ }
1305
+ }
1306
+ },
1307
+ "provider": "TRANSAK",
1308
+ "transitions": [
1309
+ {
1310
+ "id": "capture",
1311
+ "mode": "CLIENT_SURFACE",
1312
+ "report": {
1313
+ "endpoint": "POST /fiat/orders",
1314
+ "params": {
1315
+ "quoteRef": "q_8f2",
1316
+ "depositAddress": "\u2026"
1317
+ },
1318
+ "expects": [
1319
+ "paymentAuthorisationId"
1320
+ ]
1321
+ }
1322
+ }
1323
+ ]
1324
+ },
1170
1325
  "screen-02-session-auth": {
1171
1326
  "state": {
1172
1327
  "kind": "SESSION_AUTH",
@@ -1305,7 +1460,11 @@ var FIXTURE_DATA = {
1305
1460
  "phase": "CAPTURE",
1306
1461
  "surface": {
1307
1462
  "kind": "PCI_COMPONENT",
1308
- "session": "eyJ\u2026"
1463
+ "provider": "TRANSAK",
1464
+ "params": {
1465
+ "sessionId": "eyJ\u2026",
1466
+ "environment": "STAGING"
1467
+ }
1309
1468
  }
1310
1469
  },
1311
1470
  "provider": "TRANSAK",
@@ -1630,6 +1789,18 @@ var FIXTURES = [
1630
1789
  screen: 1,
1631
1790
  stateKey: "QUOTE"
1632
1791
  },
1792
+ {
1793
+ id: "screen-01b-quote-with-pay-sheet",
1794
+ file: "screen-01b-quote-with-pay-sheet.json",
1795
+ source: "CONTRACT_DOC",
1796
+ docRef: "Fiat Contract 0.7 \xA7The contract diff \xB7 example envelopes",
1797
+ screen: 1,
1798
+ stateKey: "QUOTE",
1799
+ notes: [
1800
+ "The 0.7 single-screen shape: the quote carries its own PAY_SHEET, so the provider component is the primary CTA rather than something a later state reveals.",
1801
+ "Quote payload is screen-01-quote verbatim; only the surface and the transition differ."
1802
+ ]
1803
+ },
1633
1804
  {
1634
1805
  id: "screen-02-session-auth",
1635
1806
  file: "screen-02-session-auth.json",
@@ -1766,18 +1937,18 @@ var FIXTURES = [
1766
1937
  docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
1767
1938
  stateKey: "PAYMENT/INSTRUCT",
1768
1939
  notes: [
1769
- "The envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
1940
+ "The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
1770
1941
  ]
1771
1942
  }
1772
1943
  ];
1773
1944
  var FIXTURE_COVERAGE_GAPS = [
1774
1945
  {
1775
1946
  stateKey: "FUN_AUTH",
1776
- reason: "No envelope exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
1947
+ 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
1948
  },
1778
1949
  {
1779
1950
  stateKey: "KYC/CAPTURE",
1780
- reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no envelope was ever published. Kind retained as handshake-gated vocabulary."
1951
+ 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
1952
  }
1782
1953
  ];
1783
1954
  var fixtureMeta = (id) => {
@@ -1815,7 +1986,7 @@ var formatIssues = (error) => {
1815
1986
  };
1816
1987
  function assertFiatStepResponse(json) {
1817
1988
  const result = FiatStepResponseSchema.safeParse(json);
1818
- if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
1989
+ if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
1819
1990
  return result.data;
1820
1991
  }
1821
1992
  function endpointMatches(template, actual) {
@@ -1867,6 +2038,16 @@ function checkLegalEmission(state, transitions) {
1867
2038
  `${entry.key} is terminal (table) and must carry transitions: [] \u2014 got ${transitions.length}`
1868
2039
  );
1869
2040
  }
2041
+ const surfaceTargets = new Set(
2042
+ transitions.filter((t) => t.mode === "CLIENT_SURFACE").map((t) => t.report.endpoint)
2043
+ );
2044
+ for (const transition of transitions) {
2045
+ if (transition.mode === "SUBMIT" && surfaceTargets.has(transition.endpoint)) {
2046
+ issues.push(
2047
+ `transition "${transition.id}": SUBMIT ${transition.endpoint} coexists with a CLIENT_SURFACE reporting to it`
2048
+ );
2049
+ }
2050
+ }
1870
2051
  for (const transition of transitions) {
1871
2052
  issues.push(...bodyKeyIssues(transition));
1872
2053
  if (transition.mode === "CLIENT_SURFACE" && !("surface" in state)) {
@@ -1912,14 +2093,14 @@ function walkTable(fn) {
1912
2093
  function assertFixture(id) {
1913
2094
  const meta = fixtureMeta(id);
1914
2095
  const json = loadFixture(id);
1915
- const envelope = assertFiatStepResponse(json);
1916
- if (stateKey(envelope.state) !== meta.stateKey) {
2096
+ const stepResponse = assertFiatStepResponse(json);
2097
+ if (stateKey(stepResponse.state) !== meta.stateKey) {
1917
2098
  throw new ContractViolation(
1918
- `fixture ${id}: manifest says ${meta.stateKey}, envelope carries ${stateKey(envelope.state)}`
2099
+ `fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
1919
2100
  );
1920
2101
  }
1921
- assertLegalEmission(envelope.state, envelope.transitions);
1922
- return { meta, envelope };
2102
+ assertLegalEmission(stepResponse.state, stepResponse.transitions);
2103
+ return { meta, stepResponse };
1923
2104
  }
1924
2105
  function assertAllFixtures() {
1925
2106
  return FIXTURES.map((meta) => assertFixture(meta.id));
package/dist/index.mjs CHANGED
@@ -5,10 +5,14 @@ import {
5
5
  TABLE_VERSION,
6
6
  TERMINAL_ORDER_STATUSES,
7
7
  TRANSITION_TABLE,
8
+ UNOFFERED_ENDPOINTS,
9
+ UNREACHABLE_STATES,
8
10
  isTerminal,
9
11
  stateKey,
10
- tableEntry
11
- } from "./chunk-3R5GGUAK.mjs";
12
+ tableEntry,
13
+ unofferedEndpoints,
14
+ unreachableStates
15
+ } from "./chunk-OGNYHIOE.mjs";
12
16
 
13
17
  // src/codes.ts
14
18
  var COUNTRY_CURRENCY_LIST = [
@@ -291,8 +295,12 @@ var HTTP_VERBS = ["GET", "POST"];
291
295
  var ENDPOINT_PATTERN = /^(GET|POST) \/fiat\/[^\s]*$/;
292
296
  var $endpoint = z.string().regex(ENDPOINT_PATTERN, 'endpoint must read "<GET|POST> /fiat/\u2026"');
293
297
  var FiatEndpointSchema = $endpoint;
294
- var $provider = z.enum(["TRANSAK", "SWAPPED"]);
298
+ var $provider = z.enum(["TRANSAK", "SWAPPED", "BANXA"]);
295
299
  var FiatProviderSchema = $provider;
300
+ var $kycProvider = z.enum(["SUMSUB"]);
301
+ var KYCProviderSchema = $kycProvider;
302
+ var $surfaceProvider = z.enum([...$provider.options, ...$kycProvider.options]);
303
+ var SurfaceProviderSchema = $surfaceProvider;
296
304
  var $orderStatus = z.enum([
297
305
  "CREATING",
298
306
  "AWAITING_PAYMENT",
@@ -384,13 +392,22 @@ var $refund = obj({
384
392
  expectedBy: z.string()
385
393
  });
386
394
  var RefundSchema = $refund;
387
- var $surface = z.union([
388
- obj({ kind: z.literal("URL_REDIRECT"), url: z.string() }),
389
- obj({ kind: z.literal("URL_EMBED"), url: z.string() }),
390
- obj({ kind: z.literal("PCI_COMPONENT"), session: z.string() }),
391
- obj({ kind: z.literal("PAY_SHEET"), session: z.string() }),
392
- obj({ kind: z.literal("ACH_COMPONENT"), session: z.string() })
393
- ]);
395
+ var $surfaceKind = z.enum(["URL_REDIRECT", "URL_EMBED", "PCI_COMPONENT", "PAY_SHEET", "ACH_COMPONENT"]);
396
+ var SurfaceKindSchema = $surfaceKind;
397
+ var $surface = obj({
398
+ kind: $surfaceKind,
399
+ provider: $surfaceProvider,
400
+ /**
401
+ * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
402
+ * vocabulary, so typing them here would drag every provider field change through a contract
403
+ * release and both consumers — and a normalised `environment` could only be lossy across
404
+ * Transak's four values and Banxa's two. Both ends narrow instead: fun-backend emits from a
405
+ * typed provider session, the client adapter validates with its own schema, and a recorded
406
+ * fixture per `(provider, kind)` keeps the two honest.
407
+ */
408
+ params: z.record(z.string(), JsonSchema),
409
+ expiresAt: z.string().optional()
410
+ });
394
411
  var SurfaceSchema = $surface;
395
412
  var $instructionField = obj({
396
413
  id: z.string(),
@@ -520,6 +537,8 @@ var AuthChallengeSchema = $authChallenge;
520
537
  var $quoteState = obj({
521
538
  kind: z.literal("QUOTE"),
522
539
  quote: $quote,
540
+ /** Present when the provider's component is the primary CTA, inline on the entry screen. */
541
+ surface: $surface.optional(),
523
542
  error: $failureReason.optional()
524
543
  });
525
544
  var QuoteStateSchema = $quoteState;
@@ -674,6 +693,64 @@ var FIXTURE_DATA = {
674
693
  }
675
694
  ]
676
695
  },
696
+ "screen-01b-quote-with-pay-sheet": {
697
+ "state": {
698
+ "kind": "QUOTE",
699
+ "quote": {
700
+ "quoteRef": "q_8f2",
701
+ "fiat": {
702
+ "currency": "GBP",
703
+ "amount": "100.00"
704
+ },
705
+ "crypto": {
706
+ "currency": "USDC",
707
+ "network": "polygon",
708
+ "amount": "98.41"
709
+ },
710
+ "rate": "0.9841",
711
+ "totalFee": "1.59",
712
+ "feeBreakdown": [
713
+ {
714
+ "id": "provider_fee",
715
+ "labelFallback": "Processing fee",
716
+ "amount": "1.09"
717
+ },
718
+ {
719
+ "id": "network_fee",
720
+ "labelFallback": "Network fee",
721
+ "amount": "0.50"
722
+ }
723
+ ],
724
+ "paymentMethod": "bank_transfer",
725
+ "expiresAt": "\u2026"
726
+ },
727
+ "surface": {
728
+ "kind": "PAY_SHEET",
729
+ "provider": "TRANSAK",
730
+ "params": {
731
+ "sessionId": "txs_\u2026",
732
+ "environment": "STAGING"
733
+ }
734
+ }
735
+ },
736
+ "provider": "TRANSAK",
737
+ "transitions": [
738
+ {
739
+ "id": "capture",
740
+ "mode": "CLIENT_SURFACE",
741
+ "report": {
742
+ "endpoint": "POST /fiat/orders",
743
+ "params": {
744
+ "quoteRef": "q_8f2",
745
+ "depositAddress": "\u2026"
746
+ },
747
+ "expects": [
748
+ "paymentAuthorisationId"
749
+ ]
750
+ }
751
+ }
752
+ ]
753
+ },
677
754
  "screen-02-session-auth": {
678
755
  "state": {
679
756
  "kind": "SESSION_AUTH",
@@ -812,7 +889,11 @@ var FIXTURE_DATA = {
812
889
  "phase": "CAPTURE",
813
890
  "surface": {
814
891
  "kind": "PCI_COMPONENT",
815
- "session": "eyJ\u2026"
892
+ "provider": "TRANSAK",
893
+ "params": {
894
+ "sessionId": "eyJ\u2026",
895
+ "environment": "STAGING"
896
+ }
816
897
  }
817
898
  },
818
899
  "provider": "TRANSAK",
@@ -1137,6 +1218,18 @@ var FIXTURES = [
1137
1218
  screen: 1,
1138
1219
  stateKey: "QUOTE"
1139
1220
  },
1221
+ {
1222
+ id: "screen-01b-quote-with-pay-sheet",
1223
+ file: "screen-01b-quote-with-pay-sheet.json",
1224
+ source: "CONTRACT_DOC",
1225
+ docRef: "Fiat Contract 0.7 \xA7The contract diff \xB7 example envelopes",
1226
+ screen: 1,
1227
+ stateKey: "QUOTE",
1228
+ notes: [
1229
+ "The 0.7 single-screen shape: the quote carries its own PAY_SHEET, so the provider component is the primary CTA rather than something a later state reveals.",
1230
+ "Quote payload is screen-01-quote verbatim; only the surface and the transition differ."
1231
+ ]
1232
+ },
1140
1233
  {
1141
1234
  id: "screen-02-session-auth",
1142
1235
  file: "screen-02-session-auth.json",
@@ -1273,18 +1366,18 @@ var FIXTURES = [
1273
1366
  docRef: "\xA7Worked example, steps 4\u20135 (order + bank instructions)",
1274
1367
  stateKey: "PAYMENT/INSTRUCT",
1275
1368
  notes: [
1276
- "The envelope-sibling `orderId` case. TODO(open-decision): orderId placement (envelope sibling here vs inside state in the FE doc v0)."
1369
+ "The response-level `orderId` case. TODO(open-decision): orderId placement (beside `state` here vs inside it in the FE doc v0)."
1277
1370
  ]
1278
1371
  }
1279
1372
  ];
1280
1373
  var FIXTURE_COVERAGE_GAPS = [
1281
1374
  {
1282
1375
  stateKey: "FUN_AUTH",
1283
- reason: "No envelope exists in either doc \u2014 shape owned by the auth spike, and FUN_AUTH never renders in fomo. TODO(open-decision): contract \xA7Auth & ref binding, item 5."
1376
+ 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
1377
  },
1285
1378
  {
1286
1379
  stateKey: "KYC/CAPTURE",
1287
- reason: "Dropped from v1 (FE Screen 8 tombstone, no UK module coverage); no envelope was ever published. Kind retained as handshake-gated vocabulary."
1380
+ 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
1381
  }
1289
1382
  ];
1290
1383
  var fixtureMeta = (id) => {
@@ -1322,7 +1415,7 @@ var formatIssues = (error) => {
1322
1415
  };
1323
1416
  function assertFiatStepResponse(json) {
1324
1417
  const result = FiatStepResponseSchema.safeParse(json);
1325
- if (!result.success) throw new ContractViolation("invalid envelope", formatIssues(result.error));
1418
+ if (!result.success) throw new ContractViolation("invalid fiat step response", formatIssues(result.error));
1326
1419
  return result.data;
1327
1420
  }
1328
1421
  function endpointMatches(template, actual) {
@@ -1374,6 +1467,16 @@ function checkLegalEmission(state, transitions) {
1374
1467
  `${entry.key} is terminal (table) and must carry transitions: [] \u2014 got ${transitions.length}`
1375
1468
  );
1376
1469
  }
1470
+ const surfaceTargets = new Set(
1471
+ transitions.filter((t) => t.mode === "CLIENT_SURFACE").map((t) => t.report.endpoint)
1472
+ );
1473
+ for (const transition of transitions) {
1474
+ if (transition.mode === "SUBMIT" && surfaceTargets.has(transition.endpoint)) {
1475
+ issues.push(
1476
+ `transition "${transition.id}": SUBMIT ${transition.endpoint} coexists with a CLIENT_SURFACE reporting to it`
1477
+ );
1478
+ }
1479
+ }
1377
1480
  for (const transition of transitions) {
1378
1481
  issues.push(...bodyKeyIssues(transition));
1379
1482
  if (transition.mode === "CLIENT_SURFACE" && !("surface" in state)) {
@@ -1419,14 +1522,14 @@ function walkTable(fn) {
1419
1522
  function assertFixture(id) {
1420
1523
  const meta = fixtureMeta(id);
1421
1524
  const json = loadFixture(id);
1422
- const envelope = assertFiatStepResponse(json);
1423
- if (stateKey(envelope.state) !== meta.stateKey) {
1525
+ const stepResponse = assertFiatStepResponse(json);
1526
+ if (stateKey(stepResponse.state) !== meta.stateKey) {
1424
1527
  throw new ContractViolation(
1425
- `fixture ${id}: manifest says ${meta.stateKey}, envelope carries ${stateKey(envelope.state)}`
1528
+ `fixture ${id}: manifest says ${meta.stateKey}, step response carries ${stateKey(stepResponse.state)}`
1426
1529
  );
1427
1530
  }
1428
- assertLegalEmission(envelope.state, envelope.transitions);
1429
- return { meta, envelope };
1531
+ assertLegalEmission(stepResponse.state, stepResponse.transitions);
1532
+ return { meta, stepResponse };
1430
1533
  }
1431
1534
  function assertAllFixtures() {
1432
1535
  return FIXTURES.map((meta) => assertFixture(meta.id));
@@ -1463,6 +1566,7 @@ export {
1463
1566
  InstructionsSchema,
1464
1567
  InstrumentSchema,
1465
1568
  JsonSchema,
1569
+ KYCProviderSchema,
1466
1570
  KycCaptureStateSchema,
1467
1571
  KycInputRequiredStateSchema,
1468
1572
  KycNoActionRequiredStateSchema,
@@ -1485,6 +1589,8 @@ export {
1485
1589
  SelectOptionSchema,
1486
1590
  SessionAuthStateSchema,
1487
1591
  StatusHistoryEntrySchema,
1592
+ SurfaceKindSchema,
1593
+ SurfaceProviderSchema,
1488
1594
  SurfaceSchema,
1489
1595
  TABLE_VERSION,
1490
1596
  TERMINAL_ORDER_STATUSES,
@@ -1493,6 +1599,8 @@ export {
1493
1599
  TransitionParamsSchema,
1494
1600
  TransitionSchema,
1495
1601
  TxSchema,
1602
+ UNOFFERED_ENDPOINTS,
1603
+ UNREACHABLE_STATES,
1496
1604
  assertAllFixtures,
1497
1605
  assertFiatStepResponse,
1498
1606
  assertFixture,
@@ -1507,6 +1615,8 @@ export {
1507
1615
  looksLikeFieldSpec,
1508
1616
  stateKey,
1509
1617
  tableEntry,
1618
+ unofferedEndpoints,
1619
+ unreachableStates,
1510
1620
  walkTable
1511
1621
  };
1512
1622
  //# sourceMappingURL=index.mjs.map
package/dist/schemas.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * fiat-contract — zod schemas
3
3
  *
4
4
  * zod is the single runtime validator: TypeScript cannot validate at the boundary, and the
5
- * table (`src/table.ts`) cannot reject a *malformed* envelope — only an illegal *sequence*.
5
+ * table (`src/table.ts`) cannot reject a *malformed* step response — only an illegal *sequence*.
6
6
  *
7
7
  * Every object schema is `.strict()`: an undeclared key is a contract violation, not extra data.
8
8
  *
@@ -27,13 +27,15 @@
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, FiatStepResponse, Surface, Transition, TransitionInputs, TransitionParams, Tx } from './types';
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, SurfaceKind, SurfaceProvider, KYCProvider, Transition, TransitionInputs, TransitionParams, Tx } from './types';
31
31
  export declare const JsonSchema: z.ZodType<JsonValue>;
32
32
  export declare const HTTP_VERBS: readonly ["GET", "POST"];
33
33
  /** `"POST /fiat/session/verify"`, `"GET /fiat/orders/o_31c"` — the verb rides the string. */
34
34
  export declare const ENDPOINT_PATTERN: RegExp;
35
35
  export declare const FiatEndpointSchema: z.ZodType<FiatEndpoint>;
36
36
  export declare const FiatProviderSchema: z.ZodType<FiatProvider>;
37
+ export declare const KYCProviderSchema: z.ZodType<KYCProvider>;
38
+ export declare const SurfaceProviderSchema: z.ZodType<SurfaceProvider>;
37
39
  export declare const OrderStatusSchema: z.ZodType<OrderStatus>;
38
40
  export declare const RecoverySchema: z.ZodType<Recovery>;
39
41
  export declare const FailureReasonSchema: z.ZodType<FailureReason>;
@@ -47,6 +49,7 @@ export declare const OrderSummarySchema: z.ZodType<OrderSummary>;
47
49
  export declare const StatusHistoryEntrySchema: z.ZodType<StatusHistoryEntry>;
48
50
  export declare const TxSchema: z.ZodType<Tx>;
49
51
  export declare const RefundSchema: z.ZodType<Refund>;
52
+ export declare const SurfaceKindSchema: z.ZodType<SurfaceKind>;
50
53
  export declare const SurfaceSchema: z.ZodType<Surface>;
51
54
  export declare const InstructionFieldSchema: z.ZodType<InstructionField>;
52
55
  export declare const InstructionsSchema: z.ZodType<Instructions>;