@fun-xyz/fiat-contract 0.6.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/README.md +1 -1
- package/dist/{chunk-EHYDQU4T.mjs → chunk-OGNYHIOE.mjs} +84 -5
- package/dist/index.js +194 -13
- package/dist/index.mjs +121 -11
- package/dist/schemas.d.ts +4 -1
- package/dist/table.d.ts +38 -3
- package/dist/table.js +84 -5
- package/dist/table.mjs +11 -3
- package/dist/types.d.ts +48 -21
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Four things, zero runtime logic beyond validation:
|
|
|
17
17
|
| Import | Weight | Contains | Used by |
|
|
18
18
|
| --- | --- | --- | --- |
|
|
19
19
|
| `@fun-xyz/fiat-contract/types` | **0.1 KB** (types erase) | every type; no runtime values | production, both repos |
|
|
20
|
-
| `@fun-xyz/fiat-contract/table` | **13.8 KB**, zero deps | `TRANSITION_TABLE`, `stateKey`, `tableEntry`, `isTerminal`, `walkTable`, `TABLE_VERSION`, `TERMINAL_ORDER_STATUSES`, `DOCUMENTED_ENDPOINTS` | **production frontend** + backend |
|
|
20
|
+
| `@fun-xyz/fiat-contract/table` | **13.8 KB**, zero deps | `TRANSITION_TABLE`, `stateKey`, `tableEntry`, `isTerminal`, `walkTable`, `TABLE_VERSION`, `TERMINAL_ORDER_STATUSES`, `DOCUMENTED_ENDPOINTS`, `UNOFFERED_ENDPOINTS`, `UNREACHABLE_STATES`, `unofferedEndpoints`, `unreachableStates` | **production frontend** + backend |
|
|
21
21
|
| `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 16 fixtures | tests, and backend dev/test guards |
|
|
22
22
|
|
|
23
23
|
`./table` is not a micro-optimisation. Terminality is table data a **shipped** client must read
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.7.0";
|
|
3
3
|
|
|
4
4
|
// src/table.ts
|
|
5
5
|
var TABLE_VERSION = version;
|
|
@@ -30,22 +30,46 @@ var TRANSITION_TABLE = {
|
|
|
30
30
|
endpoint: "POST /fiat/session",
|
|
31
31
|
when: "no valid provider session token (own-tables check)"
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
id: "open_session",
|
|
35
|
+
mode: "SUBMIT",
|
|
36
|
+
endpoint: "POST /fiat/payment-session",
|
|
37
|
+
when: "returning user, session + KYC valid, capture-first rail \u2014 the method authorises before any order exists",
|
|
38
|
+
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."
|
|
39
|
+
},
|
|
33
40
|
{
|
|
34
41
|
id: "continue",
|
|
35
42
|
mode: "SUBMIT",
|
|
36
43
|
endpoint: "POST /fiat/orders",
|
|
37
|
-
when: "returning user, session + KYC valid \u2014 the quote screen doubles as review"
|
|
44
|
+
when: "returning user, session + KYC valid, order-first rail \u2014 the quote screen doubles as review",
|
|
45
|
+
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."
|
|
38
46
|
},
|
|
39
47
|
{
|
|
40
48
|
id: "continue",
|
|
41
49
|
mode: "SUBMIT",
|
|
42
50
|
endpoint: "POST /fiat/auth",
|
|
43
51
|
when: "no ambient Fun identity (non-fomo surfaces only)"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "capture",
|
|
55
|
+
mode: "CLIENT_SURFACE",
|
|
56
|
+
endpoint: "POST /fiat/orders",
|
|
57
|
+
when: "capture-then-order rail whose session is minted at quote time (inline pay sheet)",
|
|
58
|
+
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)."
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "capture",
|
|
62
|
+
mode: "CLIENT_SURFACE",
|
|
63
|
+
endpoint: "POST /fiat/orders/:id/surface-result",
|
|
64
|
+
when: "order-then-capture rail whose provider order was created with the quote-time session",
|
|
65
|
+
note: "Same quote-time placement, other topology: the order already exists, so the surface reports against it rather than creating one."
|
|
44
66
|
}
|
|
45
67
|
],
|
|
46
68
|
mayReturn: [
|
|
47
69
|
"SESSION_AUTH",
|
|
48
70
|
...KYC_ANY,
|
|
71
|
+
"PAYMENT/CAPTURE",
|
|
72
|
+
"PAYMENT/INSTRUCT",
|
|
49
73
|
"PENDING_ORDER",
|
|
50
74
|
"ORDER/AWAITING_CONFIRMATION",
|
|
51
75
|
"ORDER/CREATED",
|
|
@@ -53,8 +77,10 @@ var TRANSITION_TABLE = {
|
|
|
53
77
|
],
|
|
54
78
|
terminal: false,
|
|
55
79
|
notes: [
|
|
56
|
-
"Re-quote on amount change is a client-local event, not a transition.",
|
|
57
|
-
|
|
80
|
+
"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.",
|
|
81
|
+
"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.",
|
|
82
|
+
`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.`,
|
|
83
|
+
'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.'
|
|
58
84
|
]
|
|
59
85
|
},
|
|
60
86
|
FUN_AUTH: {
|
|
@@ -352,6 +378,7 @@ var DOCUMENTED_ENDPOINTS = [
|
|
|
352
378
|
"GET /fiat/payment-methods",
|
|
353
379
|
"POST /fiat/quote",
|
|
354
380
|
"POST /fiat/auth",
|
|
381
|
+
"POST /fiat/payment-session",
|
|
355
382
|
"POST /fiat/session",
|
|
356
383
|
"POST /fiat/session/verify",
|
|
357
384
|
"GET /fiat/kyc",
|
|
@@ -366,6 +393,54 @@ var DOCUMENTED_ENDPOINTS = [
|
|
|
366
393
|
"POST /fiat/orders/:id/cancel",
|
|
367
394
|
"POST /fiat/orders/:id/surface-result"
|
|
368
395
|
];
|
|
396
|
+
var UNOFFERED_ENDPOINTS = [
|
|
397
|
+
{
|
|
398
|
+
endpoint: "GET /fiat/payment-methods",
|
|
399
|
+
reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
endpoint: "POST /fiat/kyc/document",
|
|
403
|
+
reason: "FILE fields stream to it directly; the harness owns that upload path and it is not a transition (see KYC/INPUT_REQUIRED notes)."
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
endpoint: "POST /fiat/instruments",
|
|
407
|
+
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."
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
endpoint: "GET /fiat/orders/:id/instructions",
|
|
411
|
+
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."
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
endpoint: "POST /fiat/orders/:id/reference",
|
|
415
|
+
reason: "The bank-reference submission is offered by no state. TODO(open-decision): PAYMENT{INSTRUCT} is the plausible home \u2014 confirm or wire it."
|
|
416
|
+
}
|
|
417
|
+
];
|
|
418
|
+
var UNREACHABLE_STATES = [
|
|
419
|
+
{
|
|
420
|
+
stateKey: "FUN_AUTH",
|
|
421
|
+
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."
|
|
422
|
+
}
|
|
423
|
+
];
|
|
424
|
+
function unofferedEndpoints() {
|
|
425
|
+
const offered = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const key of STATE_KEYS) {
|
|
427
|
+
for (const allowed of TRANSITION_TABLE[key].allowedTransitions) offered.add(allowed.endpoint);
|
|
428
|
+
}
|
|
429
|
+
return DOCUMENTED_ENDPOINTS.filter((endpoint) => !offered.has(endpoint));
|
|
430
|
+
}
|
|
431
|
+
function unreachableStates(from = "QUOTE") {
|
|
432
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
433
|
+
const queue = [from];
|
|
434
|
+
while (queue.length > 0) {
|
|
435
|
+
for (const target of TRANSITION_TABLE[queue.pop()].mayReturn ?? []) {
|
|
436
|
+
if (!seen.has(target)) {
|
|
437
|
+
seen.add(target);
|
|
438
|
+
queue.push(target);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return STATE_KEYS.filter((key) => !seen.has(key));
|
|
443
|
+
}
|
|
369
444
|
function stateKey(state) {
|
|
370
445
|
switch (state.kind) {
|
|
371
446
|
case "QUOTE":
|
|
@@ -408,8 +483,12 @@ export {
|
|
|
408
483
|
TRANSITION_TABLE,
|
|
409
484
|
STATE_KEYS,
|
|
410
485
|
DOCUMENTED_ENDPOINTS,
|
|
486
|
+
UNOFFERED_ENDPOINTS,
|
|
487
|
+
UNREACHABLE_STATES,
|
|
488
|
+
unofferedEndpoints,
|
|
489
|
+
unreachableStates,
|
|
411
490
|
stateKey,
|
|
412
491
|
tableEntry,
|
|
413
492
|
isTerminal
|
|
414
493
|
};
|
|
415
|
-
//# sourceMappingURL=chunk-
|
|
494
|
+
//# sourceMappingURL=chunk-OGNYHIOE.mjs.map
|
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 $
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
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",
|
|
@@ -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)) {
|
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
|
-
|
|
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 $
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
"
|
|
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",
|
|
@@ -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)) {
|
|
@@ -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
|
@@ -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>;
|
package/dist/table.d.ts
CHANGED
|
@@ -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
|
|
88
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -65,8 +65,10 @@ export type FiatEndpoint = 'GET /fiat/payment-methods' | 'POST /fiat/quote' | 'P
|
|
|
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 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.
|
|
@@ -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
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* `
|
|
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 tenant — an 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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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;
|
|
@@ -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';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fun-xyz/fiat-contract",
|
|
3
|
-
"version": "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
|
+
}
|