@fun-xyz/fiat-contract 0.9.0 → 0.11.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 CHANGED
@@ -59,10 +59,51 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
59
59
  6. **Surface kind names the renderer; provider selects the adapter.** Sumsub capture is
60
60
  `{ kind: 'KYC_SDK', provider: 'SUMSUB' }`. It is not `URL_EMBED`: the client mounts an SDK,
61
61
  not a URL or iframe.
62
- 7. **Quote-screen bounds are explicit exact-money strings.** `QUOTE.limits` carries the minimum and
63
- current routing maximum; clients do not infer either from provider metadata or floating point.
62
+ 7. **Quote-screen bounds are explicit exact-money strings.** `QUOTE.limits` carries the bounding
63
+ minimum/maximum plus the exact `serviceableRanges` when a routing set has gaps; clients do not
64
+ infer capacity from provider metadata or floating point.
64
65
  8. **Provider auth is capability-shaped.** SMS uses `SMS_OTP`; provider-owned auth uses
65
66
  `PROVIDER_RENDERED` with an optional `AUTH_COMPONENT` surface rather than provider-specific channels.
67
+ 9. **KYC SDK refresh is a transition.** `KYC{CAPTURE}` may offer `refresh_sdk_token` to
68
+ `POST /fiat/kyc/sdk-token`; a transition-following client does not invent an auxiliary endpoint call.
69
+ 10. **SDK launch is not eligibility authority.** A `KYC_SDK_RESULT` report injects either
70
+ `{outcome: 'COMPLETED'}` or `{outcome: 'FAILED', errorType}`. `errorType` is opaque, and raw
71
+ messages/errors are excluded. The backend must still read the provider's authoritative state.
72
+ 11. **Client-executed delivery is current-session-only.** A provider-owned surface may declare a
73
+ `CLIENT_EXECUTED` delivery with `source: 'CURRENT_SESSION_INPUTS'` and a value-free `allowedFieldIds`
74
+ allowlist. The delivery shape cannot request or contain backend-stored KYC values.
75
+ 12. **Capability skew is explicit.** `BLOCKED{reason: 'CLIENT_UPGRADE_REQUIRED'}` means every
76
+ otherwise-viable route needs a client capability absent from the handshake. It is not a provider
77
+ decline and should render an upgrade path rather than generic retry copy.
78
+ 13. **Providerless is terminal-routing-only.** `FiatStepResponse.provider` may be absent only for
79
+ `BLOCKED`, including `ALL_PROVIDERS_DECLINED` when routing selected no provider. Every other state
80
+ remains provider-bound and requires `provider`; provider-specific blocked responses may keep it.
81
+
82
+ ## KYC SDK hints, results, and client delivery
83
+
84
+ `KYC_SDK` is a dedicated `Surface` union member. Its optional
85
+ `preferredDocuments.IDENTITY` contains the exact adapter input needed to bias document capture:
86
+ the identity-vendor `idDocType` plus its ISO alpha-3 `country`. The hint changes presentation only;
87
+ it does not establish that the document is accepted or available.
88
+
89
+ When `report.result` is `{type: 'KYC_SDK_RESULT', bodyKey: 'result'}`, the harness injects a
90
+ `KycSdkResultReport` under that body key. Completion means the SDK launch completed, not that KYC passed.
91
+ Failure exposes only the provider's opaque `errorType` (for example `Unauthorized`), never a raw
92
+ error object or message.
93
+
94
+ `surface.delivery` describes a client-side forwarding boundary on a provider-owned surface, not
95
+ transport mechanics. The selected provider adapter may forward only the declared `allowedFieldIds` that the user entered during the
96
+ current mounted session. The schema fixes the source to `CURRENT_SESSION_INPUTS`, requires a unique
97
+ non-empty field allowlist, and contains no values. Provider-specific submission mechanics remain
98
+ adapter-owned and are intentionally unresolved by this provider-neutral contract.
99
+
100
+ “Declarative KYC collection” means the backend sends a `FormDescriptor` listing canonical field
101
+ IDs, input kinds, labels, validation, and select options. The frontend renders that description,
102
+ collects values, and submits them through the transition's single `FORM_VALUES` input; it does not
103
+ hard-code a separate screen or endpoint for source of funds, occupation, purpose, nationality,
104
+ country of birth, employment status, or phone. Field IDs and type-specific properties are closed
105
+ unions, so misspelled IDs and impossible combinations such as `options` on a `TEXT` field fail
106
+ validation.
66
107
 
67
108
  ## Versioning — the package version IS the table version
68
109
 
@@ -449,7 +490,7 @@ The current documented shape is what ships; none of these are settled here.
449
490
  | `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
450
491
  | `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
451
492
  | `[OQ7]` failure enumeration | The published `FailureReason` taxonomy only. Expired instructions, partial payment, and per-rail cancel eligibility remain unenumerated; KYC rejection is terminal only for an explicit non-retryable outcome. |
452
- | Pre-order KYC sequencing | KYC states carry their quote; SDK submission reports to `POST /fiat/kyc/capture`; pending review polls the quote resource. There is no `GET /fiat/kyc`. |
493
+ | Pre-order KYC sequencing | KYC states carry their quote; SDK submission reports to `POST /fiat/kyc/capture`; expired SDK credentials refresh through the explicit `POST /fiat/kyc/sdk-token` transition; pending review polls the quote resource. There is no `GET /fiat/kyc`. |
453
494
  | Card capture report target | Both topologies are legal in the table (`POST /fiat/orders` for capture-then-order; `POST /fiat/orders/:id/surface-result` otherwise) pending the Transak answer. |
454
495
  | Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
455
496
  | `FUN_AUTH` shape | `challenge: Record<string, JsonValue>`; the table entry is `docStatus: 'UNSPECIFIED'`, so `assertLegalEmission` reports it unjudgeable instead of guessing. |
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.9.0";
2
+ var version = "0.11.0";
3
3
 
4
4
  // src/table.ts
5
5
  var TABLE_VERSION = version;
@@ -114,7 +114,8 @@ var TRANSITION_TABLE = {
114
114
  terminal: false,
115
115
  notes: [
116
116
  "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
117
- "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
117
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
118
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
118
119
  ]
119
120
  },
120
121
  "KYC/CAPTURE": {
@@ -124,6 +125,12 @@ var TRANSITION_TABLE = {
124
125
  screens: [8],
125
126
  docStatus: "SPECIFIED",
126
127
  allowedTransitions: [
128
+ {
129
+ id: "refresh_sdk_token",
130
+ mode: "SUBMIT",
131
+ endpoint: "POST /fiat/kyc/sdk-token",
132
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
133
+ },
127
134
  {
128
135
  id: "capture",
129
136
  mode: "CLIENT_SURFACE",
@@ -143,7 +150,10 @@ var TRANSITION_TABLE = {
143
150
  "ORDER/AWAITING_CONFIRMATION"
144
151
  ],
145
152
  terminal: false,
146
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
153
+ notes: [
154
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
155
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
156
+ ]
147
157
  },
148
158
  "KYC/INPUT_REQUIRED": {
149
159
  key: "KYC/INPUT_REQUIRED",
@@ -399,10 +409,6 @@ var DOCUMENTED_ENDPOINTS = [
399
409
  "POST /fiat/orders/:id/surface-result"
400
410
  ];
401
411
  var UNOFFERED_ENDPOINTS = [
402
- {
403
- endpoint: "POST /fiat/kyc/sdk-token",
404
- reason: "SDK token refresh is adapter-driven rather than a flow transition."
405
- },
406
412
  {
407
413
  endpoint: "GET /fiat/payment-methods",
408
414
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
@@ -500,4 +506,4 @@ export {
500
506
  tableEntry,
501
507
  isTerminal
502
508
  };
503
- //# sourceMappingURL=chunk-5QGVY5Z4.mjs.map
509
+ //# sourceMappingURL=chunk-GKKBF7QB.mjs.map
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  BlockedStateSchema: () => BlockedStateSchema,
26
26
  COUNTRY_NAMES: () => COUNTRY_NAMES,
27
27
  COUNTRY_TO_CURRENCY: () => COUNTRY_TO_CURRENCY,
28
+ ClientExecutedDeliverySchema: () => ClientExecutedDeliverySchema,
28
29
  ContractViolation: () => ContractViolation,
29
30
  CountryCode: () => CountryCode,
30
31
  CryptoAmountSchema: () => CryptoAmountSchema,
@@ -53,9 +54,11 @@ __export(index_exports, {
53
54
  InstrumentSchema: () => InstrumentSchema,
54
55
  JsonSchema: () => JsonSchema,
55
56
  KYCProviderSchema: () => KYCProviderSchema,
57
+ KYC_ACKNOWLEDGEMENT_FIELD_IDS: () => KYC_ACKNOWLEDGEMENT_FIELD_IDS,
56
58
  KycCaptureStateSchema: () => KycCaptureStateSchema,
57
59
  KycInputRequiredStateSchema: () => KycInputRequiredStateSchema,
58
60
  KycNoActionRequiredStateSchema: () => KycNoActionRequiredStateSchema,
61
+ KycSdkResultReportSchema: () => KycSdkResultReportSchema,
59
62
  OrderAwaitingConfirmationStateSchema: () => OrderAwaitingConfirmationStateSchema,
60
63
  OrderCreatedStateSchema: () => OrderCreatedStateSchema,
61
64
  OrderRefSchema: () => OrderRefSchema,
@@ -66,12 +69,15 @@ __export(index_exports, {
66
69
  PaymentInstructStateSchema: () => PaymentInstructStateSchema,
67
70
  PendingOrderStateSchema: () => PendingOrderStateSchema,
68
71
  PollSpecSchema: () => PollSpecSchema,
72
+ PreferredIdentityDocumentSchema: () => PreferredIdentityDocumentSchema,
73
+ QuoteLimitRangeSchema: () => QuoteLimitRangeSchema,
69
74
  QuoteLimitsSchema: () => QuoteLimitsSchema,
70
75
  QuoteSchema: () => QuoteSchema,
71
76
  QuoteStateSchema: () => QuoteStateSchema,
72
77
  RecoverySchema: () => RecoverySchema,
73
78
  RefundSchema: () => RefundSchema,
74
79
  ReportSpecSchema: () => ReportSpecSchema,
80
+ STANDARD_KYC_FIELD_IDS: () => STANDARD_KYC_FIELD_IDS,
75
81
  STATE_KEYS: () => STATE_KEYS,
76
82
  SelectOptionSchema: () => SelectOptionSchema,
77
83
  SessionAuthStateSchema: () => SessionAuthStateSchema,
@@ -82,6 +88,7 @@ __export(index_exports, {
82
88
  TABLE_VERSION: () => TABLE_VERSION,
83
89
  TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
84
90
  TRANSITION_TABLE: () => TRANSITION_TABLE,
91
+ TermsAcceptanceSchema: () => TermsAcceptanceSchema,
85
92
  TransitionInputsSchema: () => TransitionInputsSchema,
86
93
  TransitionParamsSchema: () => TransitionParamsSchema,
87
94
  TransitionSchema: () => TransitionSchema,
@@ -464,7 +471,13 @@ var $quote = obj({
464
471
  expiresAt: import_zod.z.string()
465
472
  });
466
473
  var QuoteSchema = $quote;
467
- var $quoteLimits = obj({ min: import_zod.z.string(), max: import_zod.z.string() });
474
+ var $quoteLimitRange = obj({ min: import_zod.z.string(), max: import_zod.z.string() });
475
+ var QuoteLimitRangeSchema = $quoteLimitRange;
476
+ var $quoteLimits = obj({
477
+ min: import_zod.z.string(),
478
+ max: import_zod.z.string(),
479
+ serviceableRanges: import_zod.z.array($quoteLimitRange).min(1).optional()
480
+ });
468
481
  var QuoteLimitsSchema = $quoteLimits;
469
482
  var $instrument = obj({ brandLabel: import_zod.z.string(), last4: import_zod.z.string() });
470
483
  var InstrumentSchema = $instrument;
@@ -505,8 +518,7 @@ var $surfaceKind = import_zod.z.enum([
505
518
  "AUTH_COMPONENT"
506
519
  ]);
507
520
  var SurfaceKindSchema = $surfaceKind;
508
- var $surface = obj({
509
- kind: $surfaceKind,
521
+ var $surfaceBase = {
510
522
  provider: $surfaceProvider,
511
523
  /**
512
524
  * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
@@ -518,7 +530,84 @@ var $surface = obj({
518
530
  */
519
531
  params: import_zod.z.record(import_zod.z.string(), JsonSchema),
520
532
  expiresAt: import_zod.z.string().optional()
533
+ };
534
+ var $preferredIdentityDocument = obj({
535
+ idDocType: import_zod.z.enum(["PASSPORT", "ID_CARD", "DRIVERS", "RESIDENCE_PERMIT", "VISA"]),
536
+ country: import_zod.z.string().regex(/^[A-Z]{3}$/)
521
537
  });
538
+ var PreferredIdentityDocumentSchema = $preferredIdentityDocument;
539
+ var STANDARD_KYC_FIELD_IDS = [
540
+ "LEGAL_FIRST_NAME",
541
+ "LEGAL_MIDDLE_NAME",
542
+ "LEGAL_LAST_NAME",
543
+ "DATE_OF_BIRTH",
544
+ "COUNTRY_OF_BIRTH",
545
+ "CITY_OF_BIRTH",
546
+ "NATIONALITY",
547
+ "EMAIL_ADDRESS",
548
+ "EMAIL_OWNERSHIP_VERIFICATION",
549
+ "PHONE_NUMBER",
550
+ "PHONE_OWNERSHIP_VERIFICATION",
551
+ "RESIDENTIAL_ADDRESS_LINE_1",
552
+ "RESIDENTIAL_ADDRESS_LINE_2",
553
+ "RESIDENTIAL_ADDRESS_CITY",
554
+ "RESIDENTIAL_ADDRESS_REGION",
555
+ "RESIDENTIAL_ADDRESS_POSTAL_CODE",
556
+ "RESIDENTIAL_ADDRESS_COUNTRY",
557
+ "TAX_IDENTIFIER",
558
+ "TAX_RESIDENCE_COUNTRY",
559
+ "EMPLOYMENT_STATUS",
560
+ "OCCUPATION_INDUSTRY",
561
+ "OCCUPATION_TITLE",
562
+ "SOURCE_OF_FUNDS_DECLARATION",
563
+ "SOURCE_OF_INCOME_DECLARATION",
564
+ "SOURCE_OF_WEALTH_DECLARATION",
565
+ "PURPOSE_OF_SERVICE",
566
+ "PURPOSE_OF_TRANSACTION",
567
+ "CUSTOMER_CATEGORY"
568
+ ];
569
+ var KYC_ACKNOWLEDGEMENT_FIELD_IDS = [
570
+ "RISK_ACKNOWLEDGEMENT",
571
+ "COOLING_OFF_ACKNOWLEDGEMENT",
572
+ "TERMS_ACCEPTANCE"
573
+ ];
574
+ var $questionnaireKycFieldId = import_zod.z.custom(
575
+ (value) => typeof value === "string" && /^QUESTIONNAIRE_RESPONSE:[^:]+:[^:]+$/.test(value),
576
+ "Questionnaire field ids must identify exactly one policy and question"
577
+ );
578
+ var $kycFieldId = import_zod.z.union([
579
+ import_zod.z.enum(STANDARD_KYC_FIELD_IDS),
580
+ import_zod.z.enum(KYC_ACKNOWLEDGEMENT_FIELD_IDS),
581
+ $questionnaireKycFieldId
582
+ ]);
583
+ var $clientExecutedDelivery = obj({
584
+ channel: import_zod.z.literal("CLIENT_EXECUTED"),
585
+ source: import_zod.z.literal("CURRENT_SESSION_INPUTS"),
586
+ allowedFieldIds: import_zod.z.array($kycFieldId).min(1)
587
+ }).superRefine((delivery, context) => {
588
+ if (new Set(delivery.allowedFieldIds).size !== delivery.allowedFieldIds.length) {
589
+ context.addIssue({
590
+ code: "custom",
591
+ path: ["allowedFieldIds"],
592
+ message: "allowedFieldIds must be unique"
593
+ });
594
+ }
595
+ });
596
+ var ClientExecutedDeliverySchema = $clientExecutedDelivery;
597
+ var $surface = import_zod.z.union([
598
+ obj({
599
+ ...$surfaceBase,
600
+ kind: import_zod.z.literal("KYC_SDK"),
601
+ provider: $kycProvider,
602
+ preferredDocuments: obj({ IDENTITY: $preferredIdentityDocument }).optional()
603
+ }),
604
+ obj({
605
+ ...$surfaceBase,
606
+ kind: import_zod.z.enum(["URL_REDIRECT", "PCI_COMPONENT", "PAY_SHEET", "ACH_COMPONENT", "AUTH_COMPONENT"]),
607
+ provider: $provider,
608
+ delivery: $clientExecutedDelivery.optional()
609
+ })
610
+ ]);
522
611
  var SurfaceSchema = $surface;
523
612
  var $instructionField = obj({
524
613
  id: import_zod.z.string(),
@@ -541,15 +630,15 @@ var $selectOption = obj({ id: import_zod.z.string(), labelFallback: import_zod.z
541
630
  var SelectOptionSchema = $selectOption;
542
631
  var $formFieldType = import_zod.z.enum(["TEXT", "DATE", "SELECT", "COUNTRY", "FILE", "HOSTED_LINK"]);
543
632
  var FormFieldTypeSchema = $formFieldType;
544
- var $formField = obj({
545
- id: import_zod.z.string(),
546
- type: $formFieldType,
547
- labelFallback: import_zod.z.string(),
548
- required: import_zod.z.boolean(),
549
- regex: import_zod.z.string().optional(),
550
- options: import_zod.z.array($selectOption).optional(),
551
- url: import_zod.z.string().optional()
552
- });
633
+ var $formFieldBase = { id: $kycFieldId, labelFallback: import_zod.z.string(), required: import_zod.z.boolean() };
634
+ var $formField = import_zod.z.union([
635
+ obj({ ...$formFieldBase, type: import_zod.z.literal("TEXT"), regex: import_zod.z.string().optional() }),
636
+ obj({ ...$formFieldBase, type: import_zod.z.literal("DATE") }),
637
+ obj({ ...$formFieldBase, type: import_zod.z.literal("SELECT"), options: import_zod.z.array($selectOption) }),
638
+ obj({ ...$formFieldBase, type: import_zod.z.literal("COUNTRY") }),
639
+ obj({ ...$formFieldBase, type: import_zod.z.literal("FILE") }),
640
+ obj({ ...$formFieldBase, type: import_zod.z.literal("HOSTED_LINK"), url: import_zod.z.string() })
641
+ ]);
553
642
  var FormFieldSchema = $formField;
554
643
  var $formDescriptor = obj({ formId: import_zod.z.string(), fields: import_zod.z.array($formField) });
555
644
  var FormDescriptorSchema = $formDescriptor;
@@ -560,7 +649,8 @@ var FIELD_SPEC_TYPES = [
560
649
  "FILE",
561
650
  "SELECT",
562
651
  "HOSTED_LINK",
563
- "FORM_VALUES"
652
+ "FORM_VALUES",
653
+ "TERMS_ACCEPTANCE"
564
654
  ];
565
655
  var $fieldSpec = import_zod.z.union([
566
656
  obj({
@@ -578,9 +668,19 @@ var $fieldSpec = import_zod.z.union([
578
668
  labelFallback: import_zod.z.string().optional()
579
669
  }),
580
670
  obj({ type: import_zod.z.literal("HOSTED_LINK"), url: import_zod.z.string(), labelFallback: import_zod.z.string().optional() }),
581
- obj({ type: import_zod.z.literal("FORM_VALUES") })
671
+ obj({ type: import_zod.z.literal("FORM_VALUES") }),
672
+ obj({
673
+ type: import_zod.z.literal("TERMS_ACCEPTANCE"),
674
+ documentUrl: import_zod.z.string(),
675
+ version: import_zod.z.string(),
676
+ labelFallback: import_zod.z.string().optional()
677
+ })
582
678
  ]);
583
679
  var FieldSpecSchema = $fieldSpec;
680
+ var TermsAcceptanceSchema = obj({
681
+ version: import_zod.z.string(),
682
+ acceptedAt: import_zod.z.string()
683
+ });
584
684
  var looksLikeFieldSpec = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string" && FIELD_SPEC_TYPES.includes(value.type);
585
685
  var $params = import_zod.z.record(import_zod.z.string(), JsonSchema).superRefine((params, ctx) => {
586
686
  for (const [key, value] of Object.entries(params)) {
@@ -627,9 +727,15 @@ var PollSpecSchema = $pollSpec;
627
727
  var $reportSpec = obj({
628
728
  endpoint: $endpoint,
629
729
  params: $params.optional(),
630
- expects: import_zod.z.array(import_zod.z.string()).optional()
730
+ expects: import_zod.z.array(import_zod.z.string()).optional(),
731
+ result: obj({ type: import_zod.z.literal("KYC_SDK_RESULT"), bodyKey: import_zod.z.string().trim().min(1) }).optional()
631
732
  });
632
733
  var ReportSpecSchema = $reportSpec;
734
+ var $kycSdkResult = import_zod.z.union([
735
+ obj({ outcome: import_zod.z.literal("COMPLETED") }),
736
+ obj({ outcome: import_zod.z.literal("FAILED"), errorType: import_zod.z.string().trim().min(1) })
737
+ ]);
738
+ var KycSdkResultReportSchema = $kycSdkResult;
633
739
  var $transition = import_zod.z.union([
634
740
  obj({
635
741
  id: import_zod.z.string(),
@@ -704,7 +810,8 @@ var $blockedReason = import_zod.z.enum([
704
810
  "REGION_UNSUPPORTED",
705
811
  "ASSET_UNSUPPORTED",
706
812
  "ALL_PROVIDERS_DECLINED",
707
- "AMOUNT_UNSERVABLE"
813
+ "AMOUNT_UNSERVABLE",
814
+ "CLIENT_UPGRADE_REQUIRED"
708
815
  ]);
709
816
  var BlockedReasonSchema = $blockedReason;
710
817
  var $blockedState = obj({
@@ -748,7 +855,7 @@ var $orderCreatedState = obj({
748
855
  error: $failureReason.optional()
749
856
  });
750
857
  var OrderCreatedStateSchema = $orderCreatedState;
751
- var $flowState = import_zod.z.union([
858
+ var $providerBoundFlowState = import_zod.z.union([
752
859
  $quoteState,
753
860
  $funAuthState,
754
861
  $sessionAuthState,
@@ -756,23 +863,30 @@ var $flowState = import_zod.z.union([
756
863
  $kycInputRequiredState,
757
864
  $kycNoActionRequiredState,
758
865
  $pendingOrderState,
759
- $blockedState,
760
866
  $paymentCaptureState,
761
867
  $paymentInstructState,
762
868
  $orderAwaitingConfirmationState,
763
869
  $orderCreatedState
764
870
  ]);
871
+ var $flowState = import_zod.z.union([$providerBoundFlowState, $blockedState]);
765
872
  var FlowStateSchema = $flowState;
766
- var $stepResponse = obj({
767
- state: $flowState,
873
+ var $providerBoundStepResponse = obj({
874
+ state: $providerBoundFlowState,
768
875
  provider: $provider,
769
876
  transitions: import_zod.z.array($transition),
770
877
  orderId: import_zod.z.string().optional()
771
878
  });
879
+ var $blockedStepResponse = obj({
880
+ state: $blockedState,
881
+ provider: $provider.optional(),
882
+ transitions: import_zod.z.array($transition),
883
+ orderId: import_zod.z.string().optional()
884
+ });
885
+ var $stepResponse = import_zod.z.union([$providerBoundStepResponse, $blockedStepResponse]);
772
886
  var FiatStepResponseSchema = $stepResponse;
773
887
 
774
888
  // package.json
775
- var version = "0.9.0";
889
+ var version = "0.11.0";
776
890
 
777
891
  // src/table.ts
778
892
  var TABLE_VERSION = version;
@@ -887,7 +1001,8 @@ var TRANSITION_TABLE = {
887
1001
  terminal: false,
888
1002
  notes: [
889
1003
  "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
890
- "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
1004
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
1005
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
891
1006
  ]
892
1007
  },
893
1008
  "KYC/CAPTURE": {
@@ -897,6 +1012,12 @@ var TRANSITION_TABLE = {
897
1012
  screens: [8],
898
1013
  docStatus: "SPECIFIED",
899
1014
  allowedTransitions: [
1015
+ {
1016
+ id: "refresh_sdk_token",
1017
+ mode: "SUBMIT",
1018
+ endpoint: "POST /fiat/kyc/sdk-token",
1019
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
1020
+ },
900
1021
  {
901
1022
  id: "capture",
902
1023
  mode: "CLIENT_SURFACE",
@@ -916,7 +1037,10 @@ var TRANSITION_TABLE = {
916
1037
  "ORDER/AWAITING_CONFIRMATION"
917
1038
  ],
918
1039
  terminal: false,
919
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
1040
+ notes: [
1041
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
1042
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
1043
+ ]
920
1044
  },
921
1045
  "KYC/INPUT_REQUIRED": {
922
1046
  key: "KYC/INPUT_REQUIRED",
@@ -1172,10 +1296,6 @@ var DOCUMENTED_ENDPOINTS = [
1172
1296
  "POST /fiat/orders/:id/surface-result"
1173
1297
  ];
1174
1298
  var UNOFFERED_ENDPOINTS = [
1175
- {
1176
- endpoint: "POST /fiat/kyc/sdk-token",
1177
- reason: "SDK token refresh is adapter-driven rather than a flow transition."
1178
- },
1179
1299
  {
1180
1300
  endpoint: "GET /fiat/payment-methods",
1181
1301
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
@@ -1266,7 +1386,17 @@ var FIXTURE_DATA = {
1266
1386
  "kind": "QUOTE",
1267
1387
  "limits": {
1268
1388
  "min": "1.00",
1269
- "max": "3000.00"
1389
+ "max": "3000.00",
1390
+ "serviceableRanges": [
1391
+ {
1392
+ "min": "1.00",
1393
+ "max": "500.00"
1394
+ },
1395
+ {
1396
+ "min": "1000.00",
1397
+ "max": "3000.00"
1398
+ }
1399
+ ]
1270
1400
  },
1271
1401
  "quote": {
1272
1402
  "quoteRef": "q_8f2",
@@ -1314,7 +1444,13 @@ var FIXTURE_DATA = {
1314
1444
  "kind": "QUOTE",
1315
1445
  "limits": {
1316
1446
  "min": "1.00",
1317
- "max": "3000.00"
1447
+ "max": "3000.00",
1448
+ "serviceableRanges": [
1449
+ {
1450
+ "min": "1.00",
1451
+ "max": "3000.00"
1452
+ }
1453
+ ]
1318
1454
  },
1319
1455
  "quote": {
1320
1456
  "quoteRef": "q_8f2",
@@ -1425,13 +1561,13 @@ var FIXTURE_DATA = {
1425
1561
  "formId": "kyc_q_8f2_1",
1426
1562
  "fields": [
1427
1563
  {
1428
- "id": "tin",
1564
+ "id": "TAX_IDENTIFIER",
1429
1565
  "type": "TEXT",
1430
1566
  "labelFallback": "Tax ID",
1431
1567
  "required": true
1432
1568
  },
1433
1569
  {
1434
- "id": "address",
1570
+ "id": "RESIDENTIAL_ADDRESS_LINE_1",
1435
1571
  "type": "TEXT",
1436
1572
  "labelFallback": "Home address",
1437
1573
  "required": true
@@ -1565,11 +1701,26 @@ var FIXTURE_DATA = {
1565
1701
  "params": {
1566
1702
  "accessToken": "eyJ\u2026"
1567
1703
  },
1704
+ "preferredDocuments": {
1705
+ "IDENTITY": {
1706
+ "idDocType": "PASSPORT",
1707
+ "country": "GBR"
1708
+ }
1709
+ },
1568
1710
  "expiresAt": "\u2026"
1569
1711
  }
1570
1712
  },
1571
1713
  "provider": "TRANSAK",
1572
1714
  "transitions": [
1715
+ {
1716
+ "id": "refresh_sdk_token",
1717
+ "mode": "SUBMIT",
1718
+ "endpoint": "POST /fiat/kyc/sdk-token",
1719
+ "params": {
1720
+ "quoteRef": "q_8f2"
1721
+ },
1722
+ "labelFallback": "Retry verification"
1723
+ },
1573
1724
  {
1574
1725
  "id": "capture",
1575
1726
  "mode": "CLIENT_SURFACE",
@@ -1577,6 +1728,10 @@ var FIXTURE_DATA = {
1577
1728
  "endpoint": "POST /fiat/kyc/capture",
1578
1729
  "params": {
1579
1730
  "quoteRef": "q_8f2"
1731
+ },
1732
+ "result": {
1733
+ "type": "KYC_SDK_RESULT",
1734
+ "bodyKey": "result"
1580
1735
  }
1581
1736
  }
1582
1737
  }
@@ -1821,7 +1976,6 @@ var FIXTURE_DATA = {
1821
1976
  "reason": "REGION_UNSUPPORTED",
1822
1977
  "retryAfter": null
1823
1978
  },
1824
- "provider": "TRANSAK",
1825
1979
  "transitions": []
1826
1980
  },
1827
1981
  "worked-example-01-quote": {
@@ -1829,7 +1983,13 @@ var FIXTURE_DATA = {
1829
1983
  "kind": "QUOTE",
1830
1984
  "limits": {
1831
1985
  "min": "1.00",
1832
- "max": "3000.00"
1986
+ "max": "3000.00",
1987
+ "serviceableRanges": [
1988
+ {
1989
+ "min": "1.00",
1990
+ "max": "3000.00"
1991
+ }
1992
+ ]
1833
1993
  },
1834
1994
  "quote": {
1835
1995
  "quoteRef": "q_8f2",
@@ -2193,6 +2353,9 @@ function bodyKeyIssues(transition) {
2193
2353
  if (transition.mode === "CLIENT_SURFACE") {
2194
2354
  const params = Object.keys(transition.report.params ?? {});
2195
2355
  collide(params, transition.report.expects ?? [], "report.params", "report.expects");
2356
+ const resultKeys = transition.report.result ? [transition.report.result.bodyKey] : [];
2357
+ collide(params, resultKeys, "report.params", "report.result");
2358
+ collide(transition.report.expects ?? [], resultKeys, "report.expects", "report.result");
2196
2359
  }
2197
2360
  return issues;
2198
2361
  }
@@ -2221,6 +2384,11 @@ function checkLegalEmission(state, transitions) {
2221
2384
  `transition "${transition.id}": CLIENT_SURFACE requires the Surface in state (\u03942 single-carry) \u2014 ${entry.key} carries none`
2222
2385
  );
2223
2386
  }
2387
+ if (transition.mode === "CLIENT_SURFACE" && transition.report.result?.type === "KYC_SDK_RESULT" && (!("surface" in state) || state.surface?.kind !== "KYC_SDK")) {
2388
+ issues.push(
2389
+ `transition "${transition.id}": KYC_SDK_RESULT requires a KYC_SDK surface in state`
2390
+ );
2391
+ }
2224
2392
  if (transition.mode === "SUBMIT" && Object.values(transition.inputs ?? {}).some((spec) => spec.type === "FORM_VALUES") && !("form" in state)) {
2225
2393
  issues.push(
2226
2394
  `transition "${transition.id}": inputs {type: FORM_VALUES} collects state.form \u2014 ${entry.key} carries no form`
package/dist/index.mjs CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  tableEntry,
13
13
  unofferedEndpoints,
14
14
  unreachableStates
15
- } from "./chunk-5QGVY5Z4.mjs";
15
+ } from "./chunk-GKKBF7QB.mjs";
16
16
 
17
17
  // src/codes.ts
18
18
  var COUNTRY_CURRENCY_LIST = [
@@ -370,7 +370,13 @@ var $quote = obj({
370
370
  expiresAt: z.string()
371
371
  });
372
372
  var QuoteSchema = $quote;
373
- var $quoteLimits = obj({ min: z.string(), max: z.string() });
373
+ var $quoteLimitRange = obj({ min: z.string(), max: z.string() });
374
+ var QuoteLimitRangeSchema = $quoteLimitRange;
375
+ var $quoteLimits = obj({
376
+ min: z.string(),
377
+ max: z.string(),
378
+ serviceableRanges: z.array($quoteLimitRange).min(1).optional()
379
+ });
374
380
  var QuoteLimitsSchema = $quoteLimits;
375
381
  var $instrument = obj({ brandLabel: z.string(), last4: z.string() });
376
382
  var InstrumentSchema = $instrument;
@@ -411,8 +417,7 @@ var $surfaceKind = z.enum([
411
417
  "AUTH_COMPONENT"
412
418
  ]);
413
419
  var SurfaceKindSchema = $surfaceKind;
414
- var $surface = obj({
415
- kind: $surfaceKind,
420
+ var $surfaceBase = {
416
421
  provider: $surfaceProvider,
417
422
  /**
418
423
  * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
@@ -424,7 +429,84 @@ var $surface = obj({
424
429
  */
425
430
  params: z.record(z.string(), JsonSchema),
426
431
  expiresAt: z.string().optional()
432
+ };
433
+ var $preferredIdentityDocument = obj({
434
+ idDocType: z.enum(["PASSPORT", "ID_CARD", "DRIVERS", "RESIDENCE_PERMIT", "VISA"]),
435
+ country: z.string().regex(/^[A-Z]{3}$/)
427
436
  });
437
+ var PreferredIdentityDocumentSchema = $preferredIdentityDocument;
438
+ var STANDARD_KYC_FIELD_IDS = [
439
+ "LEGAL_FIRST_NAME",
440
+ "LEGAL_MIDDLE_NAME",
441
+ "LEGAL_LAST_NAME",
442
+ "DATE_OF_BIRTH",
443
+ "COUNTRY_OF_BIRTH",
444
+ "CITY_OF_BIRTH",
445
+ "NATIONALITY",
446
+ "EMAIL_ADDRESS",
447
+ "EMAIL_OWNERSHIP_VERIFICATION",
448
+ "PHONE_NUMBER",
449
+ "PHONE_OWNERSHIP_VERIFICATION",
450
+ "RESIDENTIAL_ADDRESS_LINE_1",
451
+ "RESIDENTIAL_ADDRESS_LINE_2",
452
+ "RESIDENTIAL_ADDRESS_CITY",
453
+ "RESIDENTIAL_ADDRESS_REGION",
454
+ "RESIDENTIAL_ADDRESS_POSTAL_CODE",
455
+ "RESIDENTIAL_ADDRESS_COUNTRY",
456
+ "TAX_IDENTIFIER",
457
+ "TAX_RESIDENCE_COUNTRY",
458
+ "EMPLOYMENT_STATUS",
459
+ "OCCUPATION_INDUSTRY",
460
+ "OCCUPATION_TITLE",
461
+ "SOURCE_OF_FUNDS_DECLARATION",
462
+ "SOURCE_OF_INCOME_DECLARATION",
463
+ "SOURCE_OF_WEALTH_DECLARATION",
464
+ "PURPOSE_OF_SERVICE",
465
+ "PURPOSE_OF_TRANSACTION",
466
+ "CUSTOMER_CATEGORY"
467
+ ];
468
+ var KYC_ACKNOWLEDGEMENT_FIELD_IDS = [
469
+ "RISK_ACKNOWLEDGEMENT",
470
+ "COOLING_OFF_ACKNOWLEDGEMENT",
471
+ "TERMS_ACCEPTANCE"
472
+ ];
473
+ var $questionnaireKycFieldId = z.custom(
474
+ (value) => typeof value === "string" && /^QUESTIONNAIRE_RESPONSE:[^:]+:[^:]+$/.test(value),
475
+ "Questionnaire field ids must identify exactly one policy and question"
476
+ );
477
+ var $kycFieldId = z.union([
478
+ z.enum(STANDARD_KYC_FIELD_IDS),
479
+ z.enum(KYC_ACKNOWLEDGEMENT_FIELD_IDS),
480
+ $questionnaireKycFieldId
481
+ ]);
482
+ var $clientExecutedDelivery = obj({
483
+ channel: z.literal("CLIENT_EXECUTED"),
484
+ source: z.literal("CURRENT_SESSION_INPUTS"),
485
+ allowedFieldIds: z.array($kycFieldId).min(1)
486
+ }).superRefine((delivery, context) => {
487
+ if (new Set(delivery.allowedFieldIds).size !== delivery.allowedFieldIds.length) {
488
+ context.addIssue({
489
+ code: "custom",
490
+ path: ["allowedFieldIds"],
491
+ message: "allowedFieldIds must be unique"
492
+ });
493
+ }
494
+ });
495
+ var ClientExecutedDeliverySchema = $clientExecutedDelivery;
496
+ var $surface = z.union([
497
+ obj({
498
+ ...$surfaceBase,
499
+ kind: z.literal("KYC_SDK"),
500
+ provider: $kycProvider,
501
+ preferredDocuments: obj({ IDENTITY: $preferredIdentityDocument }).optional()
502
+ }),
503
+ obj({
504
+ ...$surfaceBase,
505
+ kind: z.enum(["URL_REDIRECT", "PCI_COMPONENT", "PAY_SHEET", "ACH_COMPONENT", "AUTH_COMPONENT"]),
506
+ provider: $provider,
507
+ delivery: $clientExecutedDelivery.optional()
508
+ })
509
+ ]);
428
510
  var SurfaceSchema = $surface;
429
511
  var $instructionField = obj({
430
512
  id: z.string(),
@@ -447,15 +529,15 @@ var $selectOption = obj({ id: z.string(), labelFallback: z.string() });
447
529
  var SelectOptionSchema = $selectOption;
448
530
  var $formFieldType = z.enum(["TEXT", "DATE", "SELECT", "COUNTRY", "FILE", "HOSTED_LINK"]);
449
531
  var FormFieldTypeSchema = $formFieldType;
450
- var $formField = obj({
451
- id: z.string(),
452
- type: $formFieldType,
453
- labelFallback: z.string(),
454
- required: z.boolean(),
455
- regex: z.string().optional(),
456
- options: z.array($selectOption).optional(),
457
- url: z.string().optional()
458
- });
532
+ var $formFieldBase = { id: $kycFieldId, labelFallback: z.string(), required: z.boolean() };
533
+ var $formField = z.union([
534
+ obj({ ...$formFieldBase, type: z.literal("TEXT"), regex: z.string().optional() }),
535
+ obj({ ...$formFieldBase, type: z.literal("DATE") }),
536
+ obj({ ...$formFieldBase, type: z.literal("SELECT"), options: z.array($selectOption) }),
537
+ obj({ ...$formFieldBase, type: z.literal("COUNTRY") }),
538
+ obj({ ...$formFieldBase, type: z.literal("FILE") }),
539
+ obj({ ...$formFieldBase, type: z.literal("HOSTED_LINK"), url: z.string() })
540
+ ]);
459
541
  var FormFieldSchema = $formField;
460
542
  var $formDescriptor = obj({ formId: z.string(), fields: z.array($formField) });
461
543
  var FormDescriptorSchema = $formDescriptor;
@@ -466,7 +548,8 @@ var FIELD_SPEC_TYPES = [
466
548
  "FILE",
467
549
  "SELECT",
468
550
  "HOSTED_LINK",
469
- "FORM_VALUES"
551
+ "FORM_VALUES",
552
+ "TERMS_ACCEPTANCE"
470
553
  ];
471
554
  var $fieldSpec = z.union([
472
555
  obj({
@@ -484,9 +567,19 @@ var $fieldSpec = z.union([
484
567
  labelFallback: z.string().optional()
485
568
  }),
486
569
  obj({ type: z.literal("HOSTED_LINK"), url: z.string(), labelFallback: z.string().optional() }),
487
- obj({ type: z.literal("FORM_VALUES") })
570
+ obj({ type: z.literal("FORM_VALUES") }),
571
+ obj({
572
+ type: z.literal("TERMS_ACCEPTANCE"),
573
+ documentUrl: z.string(),
574
+ version: z.string(),
575
+ labelFallback: z.string().optional()
576
+ })
488
577
  ]);
489
578
  var FieldSpecSchema = $fieldSpec;
579
+ var TermsAcceptanceSchema = obj({
580
+ version: z.string(),
581
+ acceptedAt: z.string()
582
+ });
490
583
  var looksLikeFieldSpec = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string" && FIELD_SPEC_TYPES.includes(value.type);
491
584
  var $params = z.record(z.string(), JsonSchema).superRefine((params, ctx) => {
492
585
  for (const [key, value] of Object.entries(params)) {
@@ -533,9 +626,15 @@ var PollSpecSchema = $pollSpec;
533
626
  var $reportSpec = obj({
534
627
  endpoint: $endpoint,
535
628
  params: $params.optional(),
536
- expects: z.array(z.string()).optional()
629
+ expects: z.array(z.string()).optional(),
630
+ result: obj({ type: z.literal("KYC_SDK_RESULT"), bodyKey: z.string().trim().min(1) }).optional()
537
631
  });
538
632
  var ReportSpecSchema = $reportSpec;
633
+ var $kycSdkResult = z.union([
634
+ obj({ outcome: z.literal("COMPLETED") }),
635
+ obj({ outcome: z.literal("FAILED"), errorType: z.string().trim().min(1) })
636
+ ]);
637
+ var KycSdkResultReportSchema = $kycSdkResult;
539
638
  var $transition = z.union([
540
639
  obj({
541
640
  id: z.string(),
@@ -610,7 +709,8 @@ var $blockedReason = z.enum([
610
709
  "REGION_UNSUPPORTED",
611
710
  "ASSET_UNSUPPORTED",
612
711
  "ALL_PROVIDERS_DECLINED",
613
- "AMOUNT_UNSERVABLE"
712
+ "AMOUNT_UNSERVABLE",
713
+ "CLIENT_UPGRADE_REQUIRED"
614
714
  ]);
615
715
  var BlockedReasonSchema = $blockedReason;
616
716
  var $blockedState = obj({
@@ -654,7 +754,7 @@ var $orderCreatedState = obj({
654
754
  error: $failureReason.optional()
655
755
  });
656
756
  var OrderCreatedStateSchema = $orderCreatedState;
657
- var $flowState = z.union([
757
+ var $providerBoundFlowState = z.union([
658
758
  $quoteState,
659
759
  $funAuthState,
660
760
  $sessionAuthState,
@@ -662,19 +762,26 @@ var $flowState = z.union([
662
762
  $kycInputRequiredState,
663
763
  $kycNoActionRequiredState,
664
764
  $pendingOrderState,
665
- $blockedState,
666
765
  $paymentCaptureState,
667
766
  $paymentInstructState,
668
767
  $orderAwaitingConfirmationState,
669
768
  $orderCreatedState
670
769
  ]);
770
+ var $flowState = z.union([$providerBoundFlowState, $blockedState]);
671
771
  var FlowStateSchema = $flowState;
672
- var $stepResponse = obj({
673
- state: $flowState,
772
+ var $providerBoundStepResponse = obj({
773
+ state: $providerBoundFlowState,
674
774
  provider: $provider,
675
775
  transitions: z.array($transition),
676
776
  orderId: z.string().optional()
677
777
  });
778
+ var $blockedStepResponse = obj({
779
+ state: $blockedState,
780
+ provider: $provider.optional(),
781
+ transitions: z.array($transition),
782
+ orderId: z.string().optional()
783
+ });
784
+ var $stepResponse = z.union([$providerBoundStepResponse, $blockedStepResponse]);
678
785
  var FiatStepResponseSchema = $stepResponse;
679
786
 
680
787
  // src/fixtures/data.ts
@@ -684,7 +791,17 @@ var FIXTURE_DATA = {
684
791
  "kind": "QUOTE",
685
792
  "limits": {
686
793
  "min": "1.00",
687
- "max": "3000.00"
794
+ "max": "3000.00",
795
+ "serviceableRanges": [
796
+ {
797
+ "min": "1.00",
798
+ "max": "500.00"
799
+ },
800
+ {
801
+ "min": "1000.00",
802
+ "max": "3000.00"
803
+ }
804
+ ]
688
805
  },
689
806
  "quote": {
690
807
  "quoteRef": "q_8f2",
@@ -732,7 +849,13 @@ var FIXTURE_DATA = {
732
849
  "kind": "QUOTE",
733
850
  "limits": {
734
851
  "min": "1.00",
735
- "max": "3000.00"
852
+ "max": "3000.00",
853
+ "serviceableRanges": [
854
+ {
855
+ "min": "1.00",
856
+ "max": "3000.00"
857
+ }
858
+ ]
736
859
  },
737
860
  "quote": {
738
861
  "quoteRef": "q_8f2",
@@ -843,13 +966,13 @@ var FIXTURE_DATA = {
843
966
  "formId": "kyc_q_8f2_1",
844
967
  "fields": [
845
968
  {
846
- "id": "tin",
969
+ "id": "TAX_IDENTIFIER",
847
970
  "type": "TEXT",
848
971
  "labelFallback": "Tax ID",
849
972
  "required": true
850
973
  },
851
974
  {
852
- "id": "address",
975
+ "id": "RESIDENTIAL_ADDRESS_LINE_1",
853
976
  "type": "TEXT",
854
977
  "labelFallback": "Home address",
855
978
  "required": true
@@ -983,11 +1106,26 @@ var FIXTURE_DATA = {
983
1106
  "params": {
984
1107
  "accessToken": "eyJ\u2026"
985
1108
  },
1109
+ "preferredDocuments": {
1110
+ "IDENTITY": {
1111
+ "idDocType": "PASSPORT",
1112
+ "country": "GBR"
1113
+ }
1114
+ },
986
1115
  "expiresAt": "\u2026"
987
1116
  }
988
1117
  },
989
1118
  "provider": "TRANSAK",
990
1119
  "transitions": [
1120
+ {
1121
+ "id": "refresh_sdk_token",
1122
+ "mode": "SUBMIT",
1123
+ "endpoint": "POST /fiat/kyc/sdk-token",
1124
+ "params": {
1125
+ "quoteRef": "q_8f2"
1126
+ },
1127
+ "labelFallback": "Retry verification"
1128
+ },
991
1129
  {
992
1130
  "id": "capture",
993
1131
  "mode": "CLIENT_SURFACE",
@@ -995,6 +1133,10 @@ var FIXTURE_DATA = {
995
1133
  "endpoint": "POST /fiat/kyc/capture",
996
1134
  "params": {
997
1135
  "quoteRef": "q_8f2"
1136
+ },
1137
+ "result": {
1138
+ "type": "KYC_SDK_RESULT",
1139
+ "bodyKey": "result"
998
1140
  }
999
1141
  }
1000
1142
  }
@@ -1239,7 +1381,6 @@ var FIXTURE_DATA = {
1239
1381
  "reason": "REGION_UNSUPPORTED",
1240
1382
  "retryAfter": null
1241
1383
  },
1242
- "provider": "TRANSAK",
1243
1384
  "transitions": []
1244
1385
  },
1245
1386
  "worked-example-01-quote": {
@@ -1247,7 +1388,13 @@ var FIXTURE_DATA = {
1247
1388
  "kind": "QUOTE",
1248
1389
  "limits": {
1249
1390
  "min": "1.00",
1250
- "max": "3000.00"
1391
+ "max": "3000.00",
1392
+ "serviceableRanges": [
1393
+ {
1394
+ "min": "1.00",
1395
+ "max": "3000.00"
1396
+ }
1397
+ ]
1251
1398
  },
1252
1399
  "quote": {
1253
1400
  "quoteRef": "q_8f2",
@@ -1611,6 +1758,9 @@ function bodyKeyIssues(transition) {
1611
1758
  if (transition.mode === "CLIENT_SURFACE") {
1612
1759
  const params = Object.keys(transition.report.params ?? {});
1613
1760
  collide(params, transition.report.expects ?? [], "report.params", "report.expects");
1761
+ const resultKeys = transition.report.result ? [transition.report.result.bodyKey] : [];
1762
+ collide(params, resultKeys, "report.params", "report.result");
1763
+ collide(transition.report.expects ?? [], resultKeys, "report.expects", "report.result");
1614
1764
  }
1615
1765
  return issues;
1616
1766
  }
@@ -1639,6 +1789,11 @@ function checkLegalEmission(state, transitions) {
1639
1789
  `transition "${transition.id}": CLIENT_SURFACE requires the Surface in state (\u03942 single-carry) \u2014 ${entry.key} carries none`
1640
1790
  );
1641
1791
  }
1792
+ if (transition.mode === "CLIENT_SURFACE" && transition.report.result?.type === "KYC_SDK_RESULT" && (!("surface" in state) || state.surface?.kind !== "KYC_SDK")) {
1793
+ issues.push(
1794
+ `transition "${transition.id}": KYC_SDK_RESULT requires a KYC_SDK surface in state`
1795
+ );
1796
+ }
1642
1797
  if (transition.mode === "SUBMIT" && Object.values(transition.inputs ?? {}).some((spec) => spec.type === "FORM_VALUES") && !("form" in state)) {
1643
1798
  issues.push(
1644
1799
  `transition "${transition.id}": inputs {type: FORM_VALUES} collects state.form \u2014 ${entry.key} carries no form`
@@ -1695,6 +1850,7 @@ export {
1695
1850
  BlockedStateSchema,
1696
1851
  COUNTRY_NAMES,
1697
1852
  COUNTRY_TO_CURRENCY,
1853
+ ClientExecutedDeliverySchema,
1698
1854
  ContractViolation,
1699
1855
  CountryCode,
1700
1856
  CryptoAmountSchema,
@@ -1723,9 +1879,11 @@ export {
1723
1879
  InstrumentSchema,
1724
1880
  JsonSchema,
1725
1881
  KYCProviderSchema,
1882
+ KYC_ACKNOWLEDGEMENT_FIELD_IDS,
1726
1883
  KycCaptureStateSchema,
1727
1884
  KycInputRequiredStateSchema,
1728
1885
  KycNoActionRequiredStateSchema,
1886
+ KycSdkResultReportSchema,
1729
1887
  OrderAwaitingConfirmationStateSchema,
1730
1888
  OrderCreatedStateSchema,
1731
1889
  OrderRefSchema,
@@ -1736,12 +1894,15 @@ export {
1736
1894
  PaymentInstructStateSchema,
1737
1895
  PendingOrderStateSchema,
1738
1896
  PollSpecSchema,
1897
+ PreferredIdentityDocumentSchema,
1898
+ QuoteLimitRangeSchema,
1739
1899
  QuoteLimitsSchema,
1740
1900
  QuoteSchema,
1741
1901
  QuoteStateSchema,
1742
1902
  RecoverySchema,
1743
1903
  RefundSchema,
1744
1904
  ReportSpecSchema,
1905
+ STANDARD_KYC_FIELD_IDS,
1745
1906
  STATE_KEYS,
1746
1907
  SelectOptionSchema,
1747
1908
  SessionAuthStateSchema,
@@ -1752,6 +1913,7 @@ export {
1752
1913
  TABLE_VERSION,
1753
1914
  TERMINAL_ORDER_STATUSES,
1754
1915
  TRANSITION_TABLE,
1916
+ TermsAcceptanceSchema,
1755
1917
  TransitionInputsSchema,
1756
1918
  TransitionParamsSchema,
1757
1919
  TransitionSchema,
package/dist/schemas.d.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  * They are module-private, so none of it reaches the published declarations.
28
28
  */
29
29
  import { z } from 'zod';
30
- import type { BlockedReason, CryptoAmount, FailureReason, FeeLine, FiatEndpoint, FieldSpec, FlowState, FormDescriptor, FormField, FormFieldType, Instructions, InstructionField, Instrument, JsonValue, FiatAmount, OrderRef, OrderStatus, OrderSummary, PollSpec, FiatProvider, Quote, QuoteLimits, Recovery, Refund, ReportSpec, SelectOption, StatusHistoryEntry, FiatStepResponse, Surface, SurfaceKind, SurfaceProvider, KYCProvider, Transition, TransitionInputs, TransitionParams, Tx } from './types';
30
+ import type { BlockedReason, CryptoAmount, FailureReason, FeeLine, FiatAmount, FiatEndpoint, FiatProvider, FiatStepResponse, FieldSpec, FlowState, FormDescriptor, FormField, FormFieldType, InstructionField, Instructions, Instrument, JsonValue, KYCProvider, OrderRef, OrderStatus, OrderSummary, PollSpec, Quote, QuoteLimitRange, QuoteLimits, Recovery, Refund, ReportSpec, SelectOption, StatusHistoryEntry, Surface, PreferredIdentityDocument, ClientExecutedDelivery, KycSdkResultReport, SurfaceKind, SurfaceProvider, TermsAcceptance, 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. */
@@ -43,6 +43,7 @@ export declare const FiatAmountSchema: z.ZodType<FiatAmount>;
43
43
  export declare const CryptoAmountSchema: z.ZodType<CryptoAmount>;
44
44
  export declare const FeeLineSchema: z.ZodType<FeeLine>;
45
45
  export declare const QuoteSchema: z.ZodType<Quote>;
46
+ export declare const QuoteLimitRangeSchema: z.ZodType<QuoteLimitRange>;
46
47
  export declare const QuoteLimitsSchema: z.ZodType<QuoteLimits>;
47
48
  export declare const InstrumentSchema: z.ZodType<Instrument>;
48
49
  export declare const OrderRefSchema: z.ZodType<OrderRef>;
@@ -51,6 +52,10 @@ export declare const StatusHistoryEntrySchema: z.ZodType<StatusHistoryEntry>;
51
52
  export declare const TxSchema: z.ZodType<Tx>;
52
53
  export declare const RefundSchema: z.ZodType<Refund>;
53
54
  export declare const SurfaceKindSchema: z.ZodType<SurfaceKind>;
55
+ export declare const PreferredIdentityDocumentSchema: z.ZodType<PreferredIdentityDocument>;
56
+ export declare const STANDARD_KYC_FIELD_IDS: readonly ["LEGAL_FIRST_NAME", "LEGAL_MIDDLE_NAME", "LEGAL_LAST_NAME", "DATE_OF_BIRTH", "COUNTRY_OF_BIRTH", "CITY_OF_BIRTH", "NATIONALITY", "EMAIL_ADDRESS", "EMAIL_OWNERSHIP_VERIFICATION", "PHONE_NUMBER", "PHONE_OWNERSHIP_VERIFICATION", "RESIDENTIAL_ADDRESS_LINE_1", "RESIDENTIAL_ADDRESS_LINE_2", "RESIDENTIAL_ADDRESS_CITY", "RESIDENTIAL_ADDRESS_REGION", "RESIDENTIAL_ADDRESS_POSTAL_CODE", "RESIDENTIAL_ADDRESS_COUNTRY", "TAX_IDENTIFIER", "TAX_RESIDENCE_COUNTRY", "EMPLOYMENT_STATUS", "OCCUPATION_INDUSTRY", "OCCUPATION_TITLE", "SOURCE_OF_FUNDS_DECLARATION", "SOURCE_OF_INCOME_DECLARATION", "SOURCE_OF_WEALTH_DECLARATION", "PURPOSE_OF_SERVICE", "PURPOSE_OF_TRANSACTION", "CUSTOMER_CATEGORY"];
57
+ export declare const KYC_ACKNOWLEDGEMENT_FIELD_IDS: readonly ["RISK_ACKNOWLEDGEMENT", "COOLING_OFF_ACKNOWLEDGEMENT", "TERMS_ACCEPTANCE"];
58
+ export declare const ClientExecutedDeliverySchema: z.ZodType<ClientExecutedDelivery>;
54
59
  export declare const SurfaceSchema: z.ZodType<Surface>;
55
60
  export declare const InstructionFieldSchema: z.ZodType<InstructionField>;
56
61
  export declare const InstructionsSchema: z.ZodType<Instructions>;
@@ -58,8 +63,10 @@ export declare const SelectOptionSchema: z.ZodType<SelectOption>;
58
63
  export declare const FormFieldTypeSchema: z.ZodType<FormFieldType>;
59
64
  export declare const FormFieldSchema: z.ZodType<FormField>;
60
65
  export declare const FormDescriptorSchema: z.ZodType<FormDescriptor>;
61
- export declare const FIELD_SPEC_TYPES: readonly ["TEXT", "DATE", "COUNTRY", "FILE", "SELECT", "HOSTED_LINK", "FORM_VALUES"];
66
+ export declare const FIELD_SPEC_TYPES: readonly ["TEXT", "DATE", "COUNTRY", "FILE", "SELECT", "HOSTED_LINK", "FORM_VALUES", "TERMS_ACCEPTANCE"];
62
67
  export declare const FieldSpecSchema: z.ZodType<FieldSpec>;
68
+ /** The value a `TERMS_ACCEPTANCE` input yields. */
69
+ export declare const TermsAcceptanceSchema: z.ZodType<TermsAcceptance>;
63
70
  /**
64
71
  * True when a `params` value is wearing a `FieldSpec`'s clothes — i.e. the exact defect the
65
72
  * accepted split exists to kill (a spec object spread into a request body as if it were a value).
@@ -69,6 +76,7 @@ export declare const TransitionParamsSchema: z.ZodType<TransitionParams>;
69
76
  export declare const TransitionInputsSchema: z.ZodType<TransitionInputs>;
70
77
  export declare const PollSpecSchema: z.ZodType<PollSpec>;
71
78
  export declare const ReportSpecSchema: z.ZodType<ReportSpec>;
79
+ export declare const KycSdkResultReportSchema: z.ZodType<KycSdkResultReport>;
72
80
  export declare const TransitionSchema: z.ZodType<Transition>;
73
81
  export declare const AuthChallengeSchema: z.ZodType<Record<string, JsonValue>>;
74
82
  /** `Extract` keeps each per-state export pinned to the union member it validates. */
package/dist/table.js CHANGED
@@ -36,7 +36,7 @@ __export(table_exports, {
36
36
  module.exports = __toCommonJS(table_exports);
37
37
 
38
38
  // package.json
39
- var version = "0.9.0";
39
+ var version = "0.11.0";
40
40
 
41
41
  // src/table.ts
42
42
  var TABLE_VERSION = version;
@@ -151,7 +151,8 @@ var TRANSITION_TABLE = {
151
151
  terminal: false,
152
152
  notes: [
153
153
  "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
154
- "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
154
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
155
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
155
156
  ]
156
157
  },
157
158
  "KYC/CAPTURE": {
@@ -161,6 +162,12 @@ var TRANSITION_TABLE = {
161
162
  screens: [8],
162
163
  docStatus: "SPECIFIED",
163
164
  allowedTransitions: [
165
+ {
166
+ id: "refresh_sdk_token",
167
+ mode: "SUBMIT",
168
+ endpoint: "POST /fiat/kyc/sdk-token",
169
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
170
+ },
164
171
  {
165
172
  id: "capture",
166
173
  mode: "CLIENT_SURFACE",
@@ -180,7 +187,10 @@ var TRANSITION_TABLE = {
180
187
  "ORDER/AWAITING_CONFIRMATION"
181
188
  ],
182
189
  terminal: false,
183
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
190
+ notes: [
191
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
192
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
193
+ ]
184
194
  },
185
195
  "KYC/INPUT_REQUIRED": {
186
196
  key: "KYC/INPUT_REQUIRED",
@@ -436,10 +446,6 @@ var DOCUMENTED_ENDPOINTS = [
436
446
  "POST /fiat/orders/:id/surface-result"
437
447
  ];
438
448
  var UNOFFERED_ENDPOINTS = [
439
- {
440
- endpoint: "POST /fiat/kyc/sdk-token",
441
- reason: "SDK token refresh is adapter-driven rather than a flow transition."
442
- },
443
449
  {
444
450
  endpoint: "GET /fiat/payment-methods",
445
451
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
package/dist/table.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  tableEntry,
12
12
  unofferedEndpoints,
13
13
  unreachableStates
14
- } from "./chunk-5QGVY5Z4.mjs";
14
+ } from "./chunk-GKKBF7QB.mjs";
15
15
  export {
16
16
  DOCUMENTED_ENDPOINTS,
17
17
  STATE_KEYS,
package/dist/types.d.ts CHANGED
@@ -151,10 +151,23 @@ export interface Quote {
151
151
  paymentMethod: PaymentMethodCategory;
152
152
  expiresAt: string;
153
153
  }
154
- /** Input bounds for the quote screen. Decimal strings in the quote currency. */
154
+ /** One inclusive amount interval the current routing set can actually serve. */
155
+ export interface QuoteLimitRange {
156
+ min: string;
157
+ max: string;
158
+ }
159
+ /**
160
+ * Input bounds for the quote screen. Decimal strings in the quote currency.
161
+ *
162
+ * `min`/`max` remain the bounding envelope for clients on contract <=0.9. A routing set can have
163
+ * gaps (one path serves a low interval and another a higher interval), so new emitters also carry
164
+ * the exact sorted, non-overlapping intervals in `serviceableRanges`. The field is optional only so
165
+ * an upgraded client can cold-render an older recorded response during a rolling deployment.
166
+ */
155
167
  export interface QuoteLimits {
156
168
  min: string;
157
169
  max: string;
170
+ serviceableRanges?: QuoteLimitRange[];
158
171
  }
159
172
  /** Deferred to the credit-card iteration (FE doc port ledger) — optional wherever it appears. */
160
173
  export interface Instrument {
@@ -228,8 +241,7 @@ export type SurfaceProvider = FiatProvider | KYCProvider;
228
241
  * `params` values are secrets by default. Client redaction defaults every key closed and opens
229
242
  * only names that are safe under *any* provider's vocabulary.
230
243
  */
231
- export interface Surface {
232
- kind: SurfaceKind;
244
+ interface SurfaceBase {
233
245
  provider: SurfaceProvider;
234
246
  params: Record<string, JsonValue>;
235
247
  /**
@@ -238,6 +250,49 @@ export interface Surface {
238
250
  */
239
251
  expiresAt?: string;
240
252
  }
253
+ /**
254
+ * Identity-document kinds the Sumsub adapter may prefer for the `IDENTITY` step.
255
+ *
256
+ * These are the Sumsub identity spellings understood by the capture adapter. Adding a new kind is
257
+ * a contract change: the client adapter must know how to pass it to the identity SDK and every
258
+ * relying provider must either map or explicitly decline it.
259
+ */
260
+ export type KycIdentityDocumentType = 'PASSPORT' | 'ID_CARD' | 'DRIVERS' | 'RESIDENCE_PERMIT' | 'VISA';
261
+ /** Provider-native document definition understood by the selected KYC adapter. */
262
+ export interface PreferredIdentityDocument {
263
+ idDocType: KycIdentityDocumentType;
264
+ /** ISO 3166-1 alpha-3 country code expected by the identity-vendor SDK. */
265
+ country: string;
266
+ }
267
+ /** Canonical KYC values a declarative form or current-session delivery may name. */
268
+ export type StandardKycFieldId = 'LEGAL_FIRST_NAME' | 'LEGAL_MIDDLE_NAME' | 'LEGAL_LAST_NAME' | 'DATE_OF_BIRTH' | 'COUNTRY_OF_BIRTH' | 'CITY_OF_BIRTH' | 'NATIONALITY' | 'EMAIL_ADDRESS' | 'EMAIL_OWNERSHIP_VERIFICATION' | 'PHONE_NUMBER' | 'PHONE_OWNERSHIP_VERIFICATION' | 'RESIDENTIAL_ADDRESS_LINE_1' | 'RESIDENTIAL_ADDRESS_LINE_2' | 'RESIDENTIAL_ADDRESS_CITY' | 'RESIDENTIAL_ADDRESS_REGION' | 'RESIDENTIAL_ADDRESS_POSTAL_CODE' | 'RESIDENTIAL_ADDRESS_COUNTRY' | 'TAX_IDENTIFIER' | 'TAX_RESIDENCE_COUNTRY' | 'EMPLOYMENT_STATUS' | 'OCCUPATION_INDUSTRY' | 'OCCUPATION_TITLE' | 'SOURCE_OF_FUNDS_DECLARATION' | 'SOURCE_OF_INCOME_DECLARATION' | 'SOURCE_OF_WEALTH_DECLARATION' | 'PURPOSE_OF_SERVICE' | 'PURPOSE_OF_TRANSACTION' | 'CUSTOMER_CATEGORY';
269
+ export type KycAcknowledgementFieldId = 'RISK_ACKNOWLEDGEMENT' | 'COOLING_OFF_ACKNOWLEDGEMENT' | 'TERMS_ACCEPTANCE';
270
+ export type QuestionnaireKycFieldId = `QUESTIONNAIRE_RESPONSE:${string}:${string}`;
271
+ export type KycFieldId = StandardKycFieldId | KycAcknowledgementFieldId | QuestionnaireKycFieldId;
272
+ export interface ClientExecutedDelivery {
273
+ channel: 'CLIENT_EXECUTED';
274
+ /**
275
+ * A hard security boundary: only values entered during this mounted session may be selected.
276
+ * Backend-stored KYC values are never returned to the client through this contract.
277
+ */
278
+ source: 'CURRENT_SESSION_INPUTS';
279
+ /** Server-declared canonical field ids the adapter may select; this carries no field values. */
280
+ allowedFieldIds: KycFieldId[];
281
+ }
282
+ export type Surface = (SurfaceBase & {
283
+ kind: 'KYC_SDK';
284
+ provider: KYCProvider;
285
+ /** Credentials and adapter configuration only; never backend-stored KYC field values. */
286
+ params: Record<string, JsonValue>;
287
+ preferredDocuments?: {
288
+ IDENTITY: PreferredIdentityDocument;
289
+ };
290
+ }) | (SurfaceBase & {
291
+ kind: Exclude<SurfaceKind, 'KYC_SDK'>;
292
+ provider: FiatProvider;
293
+ /** Adapter-owned submission mechanics; the shared contract declares only the safe source. */
294
+ delivery?: ClientExecutedDelivery;
295
+ });
241
296
  export interface InstructionField {
242
297
  id: string;
243
298
  labelFallback: string;
@@ -275,17 +330,28 @@ export interface SelectOption {
275
330
  }
276
331
  /** Field types inside a KYC form round. A new field TYPE is a client release (handshake-gated). */
277
332
  export type FormFieldType = 'TEXT' | 'DATE' | 'SELECT' | 'COUNTRY' | 'FILE' | 'HOSTED_LINK';
278
- export interface FormField {
279
- id: string;
280
- type: FormFieldType;
333
+ interface FormFieldBase {
334
+ id: KycFieldId;
281
335
  labelFallback: string;
282
336
  required: boolean;
283
- regex?: string;
284
- /** SELECT only. */
285
- options?: SelectOption[];
286
- /** HOSTED_LINK only. */
287
- url?: string;
288
337
  }
338
+ /** Invalid property combinations (for example `options` on TEXT) are unrepresentable. */
339
+ export type FormField = (FormFieldBase & {
340
+ type: 'TEXT';
341
+ regex?: string;
342
+ }) | (FormFieldBase & {
343
+ type: 'DATE';
344
+ }) | (FormFieldBase & {
345
+ type: 'SELECT';
346
+ options: SelectOption[];
347
+ }) | (FormFieldBase & {
348
+ type: 'COUNTRY';
349
+ }) | (FormFieldBase & {
350
+ type: 'FILE';
351
+ }) | (FormFieldBase & {
352
+ type: 'HOSTED_LINK';
353
+ url: string;
354
+ });
289
355
  /** Payload of `KYC{INPUT_REQUIRED}`. One uniform renderer walks `fields`. */
290
356
  export interface FormDescriptor {
291
357
  formId: string;
@@ -297,7 +363,7 @@ export interface FormDescriptor {
297
363
  * transition never re-carries the descriptor (the double-carry disease Δ2 killed).
298
364
  *
299
365
  * Note: the contract's illustrative list reads `'TEXT' | 'NUMBER' | …`; the conformance-package
300
- * spec closes the set at the seven below, so `NUMBER` is not declared here.
366
+ * spec closes the set at the eight below, so `NUMBER` is not declared here.
301
367
  */
302
368
  export type FieldSpec = {
303
369
  type: 'TEXT';
@@ -323,8 +389,28 @@ export type FieldSpec = {
323
389
  labelFallback?: string;
324
390
  } | {
325
391
  type: 'FORM_VALUES';
392
+ } | {
393
+ type: 'TERMS_ACCEPTANCE';
394
+ /** The document being accepted. The client shows it; the user cannot accept what they cannot read. */
395
+ documentUrl: string;
396
+ /**
397
+ * Which revision of that document. Opaque to the client and echoed back verbatim, so a later
398
+ * dispute names the text the user saw rather than whatever is at `documentUrl` today.
399
+ */
400
+ version: string;
401
+ labelFallback?: string;
326
402
  };
327
403
  export type FieldSpecType = FieldSpec['type'];
404
+ /**
405
+ * What a `TERMS_ACCEPTANCE` input collects. A bare timestamp answers "when" and nothing else; the
406
+ * version is what ties the moment to the words, which is the only part a dispute turns on.
407
+ */
408
+ export interface TermsAcceptance {
409
+ /** Echoed from the `FieldSpec`, unmodified. */
410
+ version: string;
411
+ /** ISO-8601, stamped by the client when the user accepted. Never defaulted server-side. */
412
+ acceptedAt: string;
413
+ }
328
414
  /** Server literals — spread into the body verbatim, opaque to the client. */
329
415
  export type TransitionParams = Record<string, JsonValue>;
330
416
  /** Client-collected specs — the harness renders + locally validates, then merges values in. */
@@ -378,7 +464,22 @@ export interface ReportSpec {
378
464
  * Declared so conformance can assert the surface actually yields them.
379
465
  */
380
466
  expects?: string[];
467
+ /** Typed SDK result injected under `bodyKey`; mutually exclusive outcome shapes are below. */
468
+ result?: {
469
+ type: 'KYC_SDK_RESULT';
470
+ bodyKey: string;
471
+ };
381
472
  }
473
+ /**
474
+ * Client report for a KYC SDK launch. This reports SDK completion, not KYC eligibility: only the
475
+ * backend's authoritative provider read can advance eligibility.
476
+ */
477
+ export type KycSdkResultReport = {
478
+ outcome: 'COMPLETED';
479
+ } | {
480
+ outcome: 'FAILED';
481
+ errorType: string;
482
+ };
382
483
  export type TransitionMode = 'SUBMIT' | 'AWAIT' | 'CLIENT_SURFACE';
383
484
  /**
384
485
  * Body assembly, harness-owned, one rule:
@@ -531,16 +632,33 @@ export type FlowState =
531
632
  error?: FailureReason;
532
633
  };
533
634
  export type FlowStateKind = FlowState['kind'];
534
- export type BlockedReason = 'REGION_UNSUPPORTED' | 'ASSET_UNSUPPORTED' | 'ALL_PROVIDERS_DECLINED' | 'AMOUNT_UNSERVABLE';
635
+ export type BlockedReason = 'REGION_UNSUPPORTED' | 'ASSET_UNSUPPORTED' | 'ALL_PROVIDERS_DECLINED' | 'AMOUNT_UNSERVABLE'
636
+ /** The installed client did not declare a capability required by every otherwise-viable route. */
637
+ | 'CLIENT_UPGRADE_REQUIRED';
535
638
  /**
536
639
  * Every `/fiat/*` response. `transitions: []` = nothing the wizard can do; whether the *flow*
537
640
  * is over is read from the table (`terminal`), never from array emptiness (ON_HOLD is the proof).
538
641
  */
539
- export interface FiatStepResponse {
540
- state: FlowState;
541
- provider: FiatProvider;
642
+ type FiatStepResponseBase = {
542
643
  transitions: Transition[];
543
644
  /** TODO(open-decision): `orderId` placement — see `PAYMENT{INSTRUCT}.orderId` above. */
544
645
  orderId?: string;
545
- }
646
+ };
647
+ /**
648
+ * Provider is absent only when routing ended before a provider could be selected. A BLOCKED response
649
+ * may still name a provider for compatibility with provider-specific terminal decisions; every other
650
+ * state is provider-bound and therefore requires one.
651
+ */
652
+ export type FiatStepResponse = (FiatStepResponseBase & {
653
+ state: Extract<FlowState, {
654
+ kind: 'BLOCKED';
655
+ }>;
656
+ provider?: FiatProvider;
657
+ }) | (FiatStepResponseBase & {
658
+ state: Exclude<FlowState, {
659
+ kind: 'BLOCKED';
660
+ }>;
661
+ provider: FiatProvider;
662
+ });
663
+ export {};
546
664
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fun-xyz/fiat-contract",
3
- "version": "0.9.0",
3
+ "version": "0.11.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": {