@fun-xyz/fiat-contract 0.10.0 → 0.11.1

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
@@ -64,6 +64,46 @@ this package is [ENG-5268](https://linear.app/funxyz/issue/ENG-5268).
64
64
  infer capacity from provider metadata or floating point.
65
65
  8. **Provider auth is capability-shaped.** SMS uses `SMS_OTP`; provider-owned auth uses
66
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.
67
107
 
68
108
  ## Versioning — the package version IS the table version
69
109
 
@@ -450,7 +490,7 @@ The current documented shape is what ships; none of these are settled here.
450
490
  | `orderId` placement | Both: optional on `PAYMENT{INSTRUCT}` (FE doc v0) **and** optional beside `state` (contract worked example). One fixture of each. |
451
491
  | `PENDING_ORDER` removal | Kind ships, with the removal proposal flagged on the type, the table entry, and the fixture. Screen 12 stays frozen. |
452
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. |
453
- | 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`. |
454
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. |
455
495
  | Cancel placement | `cancel` is legal on `PAYMENT{INSTRUCT}` and `ORDER{CREATED}`, marked conditional on the placement decision. |
456
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.10.0";
2
+ var version = "0.11.1";
3
3
 
4
4
  // src/table.ts
5
5
  var TABLE_VERSION = version;
@@ -101,6 +101,7 @@ var TRANSITION_TABLE = {
101
101
  screens: [2],
102
102
  docStatus: "SPECIFIED",
103
103
  allowedTransitions: [
104
+ { id: "start", mode: "SUBMIT", endpoint: "POST /fiat/session" },
104
105
  { id: "verify", mode: "SUBMIT", endpoint: "POST /fiat/session/verify" }
105
106
  ],
106
107
  mayReturn: [
@@ -114,7 +115,8 @@ var TRANSITION_TABLE = {
114
115
  terminal: false,
115
116
  notes: [
116
117
  "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)."
118
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
119
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
118
120
  ]
119
121
  },
120
122
  "KYC/CAPTURE": {
@@ -124,6 +126,12 @@ var TRANSITION_TABLE = {
124
126
  screens: [8],
125
127
  docStatus: "SPECIFIED",
126
128
  allowedTransitions: [
129
+ {
130
+ id: "refresh_sdk_token",
131
+ mode: "SUBMIT",
132
+ endpoint: "POST /fiat/kyc/sdk-token",
133
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
134
+ },
127
135
  {
128
136
  id: "capture",
129
137
  mode: "CLIENT_SURFACE",
@@ -143,7 +151,10 @@ var TRANSITION_TABLE = {
143
151
  "ORDER/AWAITING_CONFIRMATION"
144
152
  ],
145
153
  terminal: false,
146
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
154
+ notes: [
155
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
156
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
157
+ ]
147
158
  },
148
159
  "KYC/INPUT_REQUIRED": {
149
160
  key: "KYC/INPUT_REQUIRED",
@@ -399,10 +410,6 @@ var DOCUMENTED_ENDPOINTS = [
399
410
  "POST /fiat/orders/:id/surface-result"
400
411
  ];
401
412
  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
413
  {
407
414
  endpoint: "GET /fiat/payment-methods",
408
415
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
@@ -500,4 +507,4 @@ export {
500
507
  tableEntry,
501
508
  isTerminal
502
509
  };
503
- //# sourceMappingURL=chunk-BAGIBS54.mjs.map
510
+ //# sourceMappingURL=chunk-TXI4KLBL.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,6 +69,7 @@ __export(index_exports, {
66
69
  PaymentInstructStateSchema: () => PaymentInstructStateSchema,
67
70
  PendingOrderStateSchema: () => PendingOrderStateSchema,
68
71
  PollSpecSchema: () => PollSpecSchema,
72
+ PreferredIdentityDocumentSchema: () => PreferredIdentityDocumentSchema,
69
73
  QuoteLimitRangeSchema: () => QuoteLimitRangeSchema,
70
74
  QuoteLimitsSchema: () => QuoteLimitsSchema,
71
75
  QuoteSchema: () => QuoteSchema,
@@ -73,6 +77,7 @@ __export(index_exports, {
73
77
  RecoverySchema: () => RecoverySchema,
74
78
  RefundSchema: () => RefundSchema,
75
79
  ReportSpecSchema: () => ReportSpecSchema,
80
+ STANDARD_KYC_FIELD_IDS: () => STANDARD_KYC_FIELD_IDS,
76
81
  STATE_KEYS: () => STATE_KEYS,
77
82
  SelectOptionSchema: () => SelectOptionSchema,
78
83
  SessionAuthStateSchema: () => SessionAuthStateSchema,
@@ -83,6 +88,7 @@ __export(index_exports, {
83
88
  TABLE_VERSION: () => TABLE_VERSION,
84
89
  TERMINAL_ORDER_STATUSES: () => TERMINAL_ORDER_STATUSES,
85
90
  TRANSITION_TABLE: () => TRANSITION_TABLE,
91
+ TermsAcceptanceSchema: () => TermsAcceptanceSchema,
86
92
  TransitionInputsSchema: () => TransitionInputsSchema,
87
93
  TransitionParamsSchema: () => TransitionParamsSchema,
88
94
  TransitionSchema: () => TransitionSchema,
@@ -512,8 +518,7 @@ var $surfaceKind = import_zod.z.enum([
512
518
  "AUTH_COMPONENT"
513
519
  ]);
514
520
  var SurfaceKindSchema = $surfaceKind;
515
- var $surface = obj({
516
- kind: $surfaceKind,
521
+ var $surfaceBase = {
517
522
  provider: $surfaceProvider,
518
523
  /**
519
524
  * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
@@ -525,7 +530,84 @@ var $surface = obj({
525
530
  */
526
531
  params: import_zod.z.record(import_zod.z.string(), JsonSchema),
527
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}$/)
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
+ }
528
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
+ ]);
529
611
  var SurfaceSchema = $surface;
530
612
  var $instructionField = obj({
531
613
  id: import_zod.z.string(),
@@ -548,15 +630,15 @@ var $selectOption = obj({ id: import_zod.z.string(), labelFallback: import_zod.z
548
630
  var SelectOptionSchema = $selectOption;
549
631
  var $formFieldType = import_zod.z.enum(["TEXT", "DATE", "SELECT", "COUNTRY", "FILE", "HOSTED_LINK"]);
550
632
  var FormFieldTypeSchema = $formFieldType;
551
- var $formField = obj({
552
- id: import_zod.z.string(),
553
- type: $formFieldType,
554
- labelFallback: import_zod.z.string(),
555
- required: import_zod.z.boolean(),
556
- regex: import_zod.z.string().optional(),
557
- options: import_zod.z.array($selectOption).optional(),
558
- url: import_zod.z.string().optional()
559
- });
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
+ ]);
560
642
  var FormFieldSchema = $formField;
561
643
  var $formDescriptor = obj({ formId: import_zod.z.string(), fields: import_zod.z.array($formField) });
562
644
  var FormDescriptorSchema = $formDescriptor;
@@ -567,7 +649,8 @@ var FIELD_SPEC_TYPES = [
567
649
  "FILE",
568
650
  "SELECT",
569
651
  "HOSTED_LINK",
570
- "FORM_VALUES"
652
+ "FORM_VALUES",
653
+ "TERMS_ACCEPTANCE"
571
654
  ];
572
655
  var $fieldSpec = import_zod.z.union([
573
656
  obj({
@@ -585,9 +668,19 @@ var $fieldSpec = import_zod.z.union([
585
668
  labelFallback: import_zod.z.string().optional()
586
669
  }),
587
670
  obj({ type: import_zod.z.literal("HOSTED_LINK"), url: import_zod.z.string(), labelFallback: import_zod.z.string().optional() }),
588
- 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
+ })
589
678
  ]);
590
679
  var FieldSpecSchema = $fieldSpec;
680
+ var TermsAcceptanceSchema = obj({
681
+ version: import_zod.z.string(),
682
+ acceptedAt: import_zod.z.string()
683
+ });
591
684
  var looksLikeFieldSpec = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string" && FIELD_SPEC_TYPES.includes(value.type);
592
685
  var $params = import_zod.z.record(import_zod.z.string(), JsonSchema).superRefine((params, ctx) => {
593
686
  for (const [key, value] of Object.entries(params)) {
@@ -634,9 +727,15 @@ var PollSpecSchema = $pollSpec;
634
727
  var $reportSpec = obj({
635
728
  endpoint: $endpoint,
636
729
  params: $params.optional(),
637
- 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()
638
732
  });
639
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;
640
739
  var $transition = import_zod.z.union([
641
740
  obj({
642
741
  id: import_zod.z.string(),
@@ -711,7 +810,8 @@ var $blockedReason = import_zod.z.enum([
711
810
  "REGION_UNSUPPORTED",
712
811
  "ASSET_UNSUPPORTED",
713
812
  "ALL_PROVIDERS_DECLINED",
714
- "AMOUNT_UNSERVABLE"
813
+ "AMOUNT_UNSERVABLE",
814
+ "CLIENT_UPGRADE_REQUIRED"
715
815
  ]);
716
816
  var BlockedReasonSchema = $blockedReason;
717
817
  var $blockedState = obj({
@@ -755,7 +855,7 @@ var $orderCreatedState = obj({
755
855
  error: $failureReason.optional()
756
856
  });
757
857
  var OrderCreatedStateSchema = $orderCreatedState;
758
- var $flowState = import_zod.z.union([
858
+ var $providerBoundFlowState = import_zod.z.union([
759
859
  $quoteState,
760
860
  $funAuthState,
761
861
  $sessionAuthState,
@@ -763,23 +863,30 @@ var $flowState = import_zod.z.union([
763
863
  $kycInputRequiredState,
764
864
  $kycNoActionRequiredState,
765
865
  $pendingOrderState,
766
- $blockedState,
767
866
  $paymentCaptureState,
768
867
  $paymentInstructState,
769
868
  $orderAwaitingConfirmationState,
770
869
  $orderCreatedState
771
870
  ]);
871
+ var $flowState = import_zod.z.union([$providerBoundFlowState, $blockedState]);
772
872
  var FlowStateSchema = $flowState;
773
- var $stepResponse = obj({
774
- state: $flowState,
873
+ var $providerBoundStepResponse = obj({
874
+ state: $providerBoundFlowState,
775
875
  provider: $provider,
776
876
  transitions: import_zod.z.array($transition),
777
877
  orderId: import_zod.z.string().optional()
778
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]);
779
886
  var FiatStepResponseSchema = $stepResponse;
780
887
 
781
888
  // package.json
782
- var version = "0.10.0";
889
+ var version = "0.11.1";
783
890
 
784
891
  // src/table.ts
785
892
  var TABLE_VERSION = version;
@@ -881,6 +988,7 @@ var TRANSITION_TABLE = {
881
988
  screens: [2],
882
989
  docStatus: "SPECIFIED",
883
990
  allowedTransitions: [
991
+ { id: "start", mode: "SUBMIT", endpoint: "POST /fiat/session" },
884
992
  { id: "verify", mode: "SUBMIT", endpoint: "POST /fiat/session/verify" }
885
993
  ],
886
994
  mayReturn: [
@@ -894,7 +1002,8 @@ var TRANSITION_TABLE = {
894
1002
  terminal: false,
895
1003
  notes: [
896
1004
  "A bad code re-enters this same state with error {category: USER_ERROR, code: BAD_CODE} \u2014 banner, not a screen change.",
897
- "No resend affordance is specified in either doc (known gap, FE doc Screen 2)."
1005
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
1006
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
898
1007
  ]
899
1008
  },
900
1009
  "KYC/CAPTURE": {
@@ -904,6 +1013,12 @@ var TRANSITION_TABLE = {
904
1013
  screens: [8],
905
1014
  docStatus: "SPECIFIED",
906
1015
  allowedTransitions: [
1016
+ {
1017
+ id: "refresh_sdk_token",
1018
+ mode: "SUBMIT",
1019
+ endpoint: "POST /fiat/kyc/sdk-token",
1020
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
1021
+ },
907
1022
  {
908
1023
  id: "capture",
909
1024
  mode: "CLIENT_SURFACE",
@@ -923,7 +1038,10 @@ var TRANSITION_TABLE = {
923
1038
  "ORDER/AWAITING_CONFIRMATION"
924
1039
  ],
925
1040
  terminal: false,
926
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
1041
+ notes: [
1042
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
1043
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
1044
+ ]
927
1045
  },
928
1046
  "KYC/INPUT_REQUIRED": {
929
1047
  key: "KYC/INPUT_REQUIRED",
@@ -1179,10 +1297,6 @@ var DOCUMENTED_ENDPOINTS = [
1179
1297
  "POST /fiat/orders/:id/surface-result"
1180
1298
  ];
1181
1299
  var UNOFFERED_ENDPOINTS = [
1182
- {
1183
- endpoint: "POST /fiat/kyc/sdk-token",
1184
- reason: "SDK token refresh is adapter-driven rather than a flow transition."
1185
- },
1186
1300
  {
1187
1301
  endpoint: "GET /fiat/payment-methods",
1188
1302
  reason: "Discovery, called before any flow state exists \u2014 there is no state to hang it off."
@@ -1448,13 +1562,13 @@ var FIXTURE_DATA = {
1448
1562
  "formId": "kyc_q_8f2_1",
1449
1563
  "fields": [
1450
1564
  {
1451
- "id": "tin",
1565
+ "id": "TAX_IDENTIFIER",
1452
1566
  "type": "TEXT",
1453
1567
  "labelFallback": "Tax ID",
1454
1568
  "required": true
1455
1569
  },
1456
1570
  {
1457
- "id": "address",
1571
+ "id": "RESIDENTIAL_ADDRESS_LINE_1",
1458
1572
  "type": "TEXT",
1459
1573
  "labelFallback": "Home address",
1460
1574
  "required": true
@@ -1588,11 +1702,26 @@ var FIXTURE_DATA = {
1588
1702
  "params": {
1589
1703
  "accessToken": "eyJ\u2026"
1590
1704
  },
1705
+ "preferredDocuments": {
1706
+ "IDENTITY": {
1707
+ "idDocType": "PASSPORT",
1708
+ "country": "GBR"
1709
+ }
1710
+ },
1591
1711
  "expiresAt": "\u2026"
1592
1712
  }
1593
1713
  },
1594
1714
  "provider": "TRANSAK",
1595
1715
  "transitions": [
1716
+ {
1717
+ "id": "refresh_sdk_token",
1718
+ "mode": "SUBMIT",
1719
+ "endpoint": "POST /fiat/kyc/sdk-token",
1720
+ "params": {
1721
+ "quoteRef": "q_8f2"
1722
+ },
1723
+ "labelFallback": "Retry verification"
1724
+ },
1596
1725
  {
1597
1726
  "id": "capture",
1598
1727
  "mode": "CLIENT_SURFACE",
@@ -1600,6 +1729,10 @@ var FIXTURE_DATA = {
1600
1729
  "endpoint": "POST /fiat/kyc/capture",
1601
1730
  "params": {
1602
1731
  "quoteRef": "q_8f2"
1732
+ },
1733
+ "result": {
1734
+ "type": "KYC_SDK_RESULT",
1735
+ "bodyKey": "result"
1603
1736
  }
1604
1737
  }
1605
1738
  }
@@ -1844,7 +1977,6 @@ var FIXTURE_DATA = {
1844
1977
  "reason": "REGION_UNSUPPORTED",
1845
1978
  "retryAfter": null
1846
1979
  },
1847
- "provider": "TRANSAK",
1848
1980
  "transitions": []
1849
1981
  },
1850
1982
  "worked-example-01-quote": {
@@ -2222,6 +2354,9 @@ function bodyKeyIssues(transition) {
2222
2354
  if (transition.mode === "CLIENT_SURFACE") {
2223
2355
  const params = Object.keys(transition.report.params ?? {});
2224
2356
  collide(params, transition.report.expects ?? [], "report.params", "report.expects");
2357
+ const resultKeys = transition.report.result ? [transition.report.result.bodyKey] : [];
2358
+ collide(params, resultKeys, "report.params", "report.result");
2359
+ collide(transition.report.expects ?? [], resultKeys, "report.expects", "report.result");
2225
2360
  }
2226
2361
  return issues;
2227
2362
  }
@@ -2250,6 +2385,11 @@ function checkLegalEmission(state, transitions) {
2250
2385
  `transition "${transition.id}": CLIENT_SURFACE requires the Surface in state (\u03942 single-carry) \u2014 ${entry.key} carries none`
2251
2386
  );
2252
2387
  }
2388
+ if (transition.mode === "CLIENT_SURFACE" && transition.report.result?.type === "KYC_SDK_RESULT" && (!("surface" in state) || state.surface?.kind !== "KYC_SDK")) {
2389
+ issues.push(
2390
+ `transition "${transition.id}": KYC_SDK_RESULT requires a KYC_SDK surface in state`
2391
+ );
2392
+ }
2253
2393
  if (transition.mode === "SUBMIT" && Object.values(transition.inputs ?? {}).some((spec) => spec.type === "FORM_VALUES") && !("form" in state)) {
2254
2394
  issues.push(
2255
2395
  `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-BAGIBS54.mjs";
15
+ } from "./chunk-TXI4KLBL.mjs";
16
16
 
17
17
  // src/codes.ts
18
18
  var COUNTRY_CURRENCY_LIST = [
@@ -417,8 +417,7 @@ var $surfaceKind = z.enum([
417
417
  "AUTH_COMPONENT"
418
418
  ]);
419
419
  var SurfaceKindSchema = $surfaceKind;
420
- var $surface = obj({
421
- kind: $surfaceKind,
420
+ var $surfaceBase = {
422
421
  provider: $surfaceProvider,
423
422
  /**
424
423
  * The one deliberate non-strict leaf in the contract. Provider inputs are the adapter's
@@ -430,7 +429,84 @@ var $surface = obj({
430
429
  */
431
430
  params: z.record(z.string(), JsonSchema),
432
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}$/)
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
+ }
433
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
+ ]);
434
510
  var SurfaceSchema = $surface;
435
511
  var $instructionField = obj({
436
512
  id: z.string(),
@@ -453,15 +529,15 @@ var $selectOption = obj({ id: z.string(), labelFallback: z.string() });
453
529
  var SelectOptionSchema = $selectOption;
454
530
  var $formFieldType = z.enum(["TEXT", "DATE", "SELECT", "COUNTRY", "FILE", "HOSTED_LINK"]);
455
531
  var FormFieldTypeSchema = $formFieldType;
456
- var $formField = obj({
457
- id: z.string(),
458
- type: $formFieldType,
459
- labelFallback: z.string(),
460
- required: z.boolean(),
461
- regex: z.string().optional(),
462
- options: z.array($selectOption).optional(),
463
- url: z.string().optional()
464
- });
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
+ ]);
465
541
  var FormFieldSchema = $formField;
466
542
  var $formDescriptor = obj({ formId: z.string(), fields: z.array($formField) });
467
543
  var FormDescriptorSchema = $formDescriptor;
@@ -472,7 +548,8 @@ var FIELD_SPEC_TYPES = [
472
548
  "FILE",
473
549
  "SELECT",
474
550
  "HOSTED_LINK",
475
- "FORM_VALUES"
551
+ "FORM_VALUES",
552
+ "TERMS_ACCEPTANCE"
476
553
  ];
477
554
  var $fieldSpec = z.union([
478
555
  obj({
@@ -490,9 +567,19 @@ var $fieldSpec = z.union([
490
567
  labelFallback: z.string().optional()
491
568
  }),
492
569
  obj({ type: z.literal("HOSTED_LINK"), url: z.string(), labelFallback: z.string().optional() }),
493
- 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
+ })
494
577
  ]);
495
578
  var FieldSpecSchema = $fieldSpec;
579
+ var TermsAcceptanceSchema = obj({
580
+ version: z.string(),
581
+ acceptedAt: z.string()
582
+ });
496
583
  var looksLikeFieldSpec = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.type === "string" && FIELD_SPEC_TYPES.includes(value.type);
497
584
  var $params = z.record(z.string(), JsonSchema).superRefine((params, ctx) => {
498
585
  for (const [key, value] of Object.entries(params)) {
@@ -539,9 +626,15 @@ var PollSpecSchema = $pollSpec;
539
626
  var $reportSpec = obj({
540
627
  endpoint: $endpoint,
541
628
  params: $params.optional(),
542
- 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()
543
631
  });
544
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;
545
638
  var $transition = z.union([
546
639
  obj({
547
640
  id: z.string(),
@@ -616,7 +709,8 @@ var $blockedReason = z.enum([
616
709
  "REGION_UNSUPPORTED",
617
710
  "ASSET_UNSUPPORTED",
618
711
  "ALL_PROVIDERS_DECLINED",
619
- "AMOUNT_UNSERVABLE"
712
+ "AMOUNT_UNSERVABLE",
713
+ "CLIENT_UPGRADE_REQUIRED"
620
714
  ]);
621
715
  var BlockedReasonSchema = $blockedReason;
622
716
  var $blockedState = obj({
@@ -660,7 +754,7 @@ var $orderCreatedState = obj({
660
754
  error: $failureReason.optional()
661
755
  });
662
756
  var OrderCreatedStateSchema = $orderCreatedState;
663
- var $flowState = z.union([
757
+ var $providerBoundFlowState = z.union([
664
758
  $quoteState,
665
759
  $funAuthState,
666
760
  $sessionAuthState,
@@ -668,19 +762,26 @@ var $flowState = z.union([
668
762
  $kycInputRequiredState,
669
763
  $kycNoActionRequiredState,
670
764
  $pendingOrderState,
671
- $blockedState,
672
765
  $paymentCaptureState,
673
766
  $paymentInstructState,
674
767
  $orderAwaitingConfirmationState,
675
768
  $orderCreatedState
676
769
  ]);
770
+ var $flowState = z.union([$providerBoundFlowState, $blockedState]);
677
771
  var FlowStateSchema = $flowState;
678
- var $stepResponse = obj({
679
- state: $flowState,
772
+ var $providerBoundStepResponse = obj({
773
+ state: $providerBoundFlowState,
680
774
  provider: $provider,
681
775
  transitions: z.array($transition),
682
776
  orderId: z.string().optional()
683
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]);
684
785
  var FiatStepResponseSchema = $stepResponse;
685
786
 
686
787
  // src/fixtures/data.ts
@@ -865,13 +966,13 @@ var FIXTURE_DATA = {
865
966
  "formId": "kyc_q_8f2_1",
866
967
  "fields": [
867
968
  {
868
- "id": "tin",
969
+ "id": "TAX_IDENTIFIER",
869
970
  "type": "TEXT",
870
971
  "labelFallback": "Tax ID",
871
972
  "required": true
872
973
  },
873
974
  {
874
- "id": "address",
975
+ "id": "RESIDENTIAL_ADDRESS_LINE_1",
875
976
  "type": "TEXT",
876
977
  "labelFallback": "Home address",
877
978
  "required": true
@@ -1005,11 +1106,26 @@ var FIXTURE_DATA = {
1005
1106
  "params": {
1006
1107
  "accessToken": "eyJ\u2026"
1007
1108
  },
1109
+ "preferredDocuments": {
1110
+ "IDENTITY": {
1111
+ "idDocType": "PASSPORT",
1112
+ "country": "GBR"
1113
+ }
1114
+ },
1008
1115
  "expiresAt": "\u2026"
1009
1116
  }
1010
1117
  },
1011
1118
  "provider": "TRANSAK",
1012
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
+ },
1013
1129
  {
1014
1130
  "id": "capture",
1015
1131
  "mode": "CLIENT_SURFACE",
@@ -1017,6 +1133,10 @@ var FIXTURE_DATA = {
1017
1133
  "endpoint": "POST /fiat/kyc/capture",
1018
1134
  "params": {
1019
1135
  "quoteRef": "q_8f2"
1136
+ },
1137
+ "result": {
1138
+ "type": "KYC_SDK_RESULT",
1139
+ "bodyKey": "result"
1020
1140
  }
1021
1141
  }
1022
1142
  }
@@ -1261,7 +1381,6 @@ var FIXTURE_DATA = {
1261
1381
  "reason": "REGION_UNSUPPORTED",
1262
1382
  "retryAfter": null
1263
1383
  },
1264
- "provider": "TRANSAK",
1265
1384
  "transitions": []
1266
1385
  },
1267
1386
  "worked-example-01-quote": {
@@ -1639,6 +1758,9 @@ function bodyKeyIssues(transition) {
1639
1758
  if (transition.mode === "CLIENT_SURFACE") {
1640
1759
  const params = Object.keys(transition.report.params ?? {});
1641
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");
1642
1764
  }
1643
1765
  return issues;
1644
1766
  }
@@ -1667,6 +1789,11 @@ function checkLegalEmission(state, transitions) {
1667
1789
  `transition "${transition.id}": CLIENT_SURFACE requires the Surface in state (\u03942 single-carry) \u2014 ${entry.key} carries none`
1668
1790
  );
1669
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
+ }
1670
1797
  if (transition.mode === "SUBMIT" && Object.values(transition.inputs ?? {}).some((spec) => spec.type === "FORM_VALUES") && !("form" in state)) {
1671
1798
  issues.push(
1672
1799
  `transition "${transition.id}": inputs {type: FORM_VALUES} collects state.form \u2014 ${entry.key} carries no form`
@@ -1723,6 +1850,7 @@ export {
1723
1850
  BlockedStateSchema,
1724
1851
  COUNTRY_NAMES,
1725
1852
  COUNTRY_TO_CURRENCY,
1853
+ ClientExecutedDeliverySchema,
1726
1854
  ContractViolation,
1727
1855
  CountryCode,
1728
1856
  CryptoAmountSchema,
@@ -1751,9 +1879,11 @@ export {
1751
1879
  InstrumentSchema,
1752
1880
  JsonSchema,
1753
1881
  KYCProviderSchema,
1882
+ KYC_ACKNOWLEDGEMENT_FIELD_IDS,
1754
1883
  KycCaptureStateSchema,
1755
1884
  KycInputRequiredStateSchema,
1756
1885
  KycNoActionRequiredStateSchema,
1886
+ KycSdkResultReportSchema,
1757
1887
  OrderAwaitingConfirmationStateSchema,
1758
1888
  OrderCreatedStateSchema,
1759
1889
  OrderRefSchema,
@@ -1764,6 +1894,7 @@ export {
1764
1894
  PaymentInstructStateSchema,
1765
1895
  PendingOrderStateSchema,
1766
1896
  PollSpecSchema,
1897
+ PreferredIdentityDocumentSchema,
1767
1898
  QuoteLimitRangeSchema,
1768
1899
  QuoteLimitsSchema,
1769
1900
  QuoteSchema,
@@ -1771,6 +1902,7 @@ export {
1771
1902
  RecoverySchema,
1772
1903
  RefundSchema,
1773
1904
  ReportSpecSchema,
1905
+ STANDARD_KYC_FIELD_IDS,
1774
1906
  STATE_KEYS,
1775
1907
  SelectOptionSchema,
1776
1908
  SessionAuthStateSchema,
@@ -1781,6 +1913,7 @@ export {
1781
1913
  TABLE_VERSION,
1782
1914
  TERMINAL_ORDER_STATUSES,
1783
1915
  TRANSITION_TABLE,
1916
+ TermsAcceptanceSchema,
1784
1917
  TransitionInputsSchema,
1785
1918
  TransitionParamsSchema,
1786
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, QuoteLimitRange, 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. */
@@ -52,6 +52,10 @@ export declare const StatusHistoryEntrySchema: z.ZodType<StatusHistoryEntry>;
52
52
  export declare const TxSchema: z.ZodType<Tx>;
53
53
  export declare const RefundSchema: z.ZodType<Refund>;
54
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>;
55
59
  export declare const SurfaceSchema: z.ZodType<Surface>;
56
60
  export declare const InstructionFieldSchema: z.ZodType<InstructionField>;
57
61
  export declare const InstructionsSchema: z.ZodType<Instructions>;
@@ -59,8 +63,10 @@ export declare const SelectOptionSchema: z.ZodType<SelectOption>;
59
63
  export declare const FormFieldTypeSchema: z.ZodType<FormFieldType>;
60
64
  export declare const FormFieldSchema: z.ZodType<FormField>;
61
65
  export declare const FormDescriptorSchema: z.ZodType<FormDescriptor>;
62
- 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"];
63
67
  export declare const FieldSpecSchema: z.ZodType<FieldSpec>;
68
+ /** The value a `TERMS_ACCEPTANCE` input yields. */
69
+ export declare const TermsAcceptanceSchema: z.ZodType<TermsAcceptance>;
64
70
  /**
65
71
  * True when a `params` value is wearing a `FieldSpec`'s clothes — i.e. the exact defect the
66
72
  * accepted split exists to kill (a spec object spread into a request body as if it were a value).
@@ -70,6 +76,7 @@ export declare const TransitionParamsSchema: z.ZodType<TransitionParams>;
70
76
  export declare const TransitionInputsSchema: z.ZodType<TransitionInputs>;
71
77
  export declare const PollSpecSchema: z.ZodType<PollSpec>;
72
78
  export declare const ReportSpecSchema: z.ZodType<ReportSpec>;
79
+ export declare const KycSdkResultReportSchema: z.ZodType<KycSdkResultReport>;
73
80
  export declare const TransitionSchema: z.ZodType<Transition>;
74
81
  export declare const AuthChallengeSchema: z.ZodType<Record<string, JsonValue>>;
75
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.10.0";
39
+ var version = "0.11.1";
40
40
 
41
41
  // src/table.ts
42
42
  var TABLE_VERSION = version;
@@ -138,6 +138,7 @@ var TRANSITION_TABLE = {
138
138
  screens: [2],
139
139
  docStatus: "SPECIFIED",
140
140
  allowedTransitions: [
141
+ { id: "start", mode: "SUBMIT", endpoint: "POST /fiat/session" },
141
142
  { id: "verify", mode: "SUBMIT", endpoint: "POST /fiat/session/verify" }
142
143
  ],
143
144
  mayReturn: [
@@ -151,7 +152,8 @@ var TRANSITION_TABLE = {
151
152
  terminal: false,
152
153
  notes: [
153
154
  "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)."
155
+ "No resend affordance is specified in either doc (known gap, FE doc Screen 2).",
156
+ "A provider-owned surface may declare client-executed delivery from allowlisted CURRENT_SESSION_INPUTS only."
155
157
  ]
156
158
  },
157
159
  "KYC/CAPTURE": {
@@ -161,6 +163,12 @@ var TRANSITION_TABLE = {
161
163
  screens: [8],
162
164
  docStatus: "SPECIFIED",
163
165
  allowedTransitions: [
166
+ {
167
+ id: "refresh_sdk_token",
168
+ mode: "SUBMIT",
169
+ endpoint: "POST /fiat/kyc/sdk-token",
170
+ note: "Refreshes an expired SDK credential and returns a new KYC/CAPTURE state."
171
+ },
164
172
  {
165
173
  id: "capture",
166
174
  mode: "CLIENT_SURFACE",
@@ -180,7 +188,10 @@ var TRANSITION_TABLE = {
180
188
  "ORDER/AWAITING_CONFIRMATION"
181
189
  ],
182
190
  terminal: false,
183
- notes: ["Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter."]
191
+ notes: [
192
+ "Carries the handshake-gated KYC_SDK surface; provider selects the identity-vendor adapter.",
193
+ "SDK completion or failure is only a launch report; eligibility is re-read authoritatively by the backend."
194
+ ]
184
195
  },
185
196
  "KYC/INPUT_REQUIRED": {
186
197
  key: "KYC/INPUT_REQUIRED",
@@ -436,10 +447,6 @@ var DOCUMENTED_ENDPOINTS = [
436
447
  "POST /fiat/orders/:id/surface-result"
437
448
  ];
438
449
  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
450
  {
444
451
  endpoint: "GET /fiat/payment-methods",
445
452
  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-BAGIBS54.mjs";
14
+ } from "./chunk-TXI4KLBL.mjs";
15
15
  export {
16
16
  DOCUMENTED_ENDPOINTS,
17
17
  STATE_KEYS,
package/dist/types.d.ts CHANGED
@@ -241,8 +241,7 @@ export type SurfaceProvider = FiatProvider | KYCProvider;
241
241
  * `params` values are secrets by default. Client redaction defaults every key closed and opens
242
242
  * only names that are safe under *any* provider's vocabulary.
243
243
  */
244
- export interface Surface {
245
- kind: SurfaceKind;
244
+ interface SurfaceBase {
246
245
  provider: SurfaceProvider;
247
246
  params: Record<string, JsonValue>;
248
247
  /**
@@ -251,6 +250,49 @@ export interface Surface {
251
250
  */
252
251
  expiresAt?: string;
253
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
+ });
254
296
  export interface InstructionField {
255
297
  id: string;
256
298
  labelFallback: string;
@@ -288,17 +330,28 @@ export interface SelectOption {
288
330
  }
289
331
  /** Field types inside a KYC form round. A new field TYPE is a client release (handshake-gated). */
290
332
  export type FormFieldType = 'TEXT' | 'DATE' | 'SELECT' | 'COUNTRY' | 'FILE' | 'HOSTED_LINK';
291
- export interface FormField {
292
- id: string;
293
- type: FormFieldType;
333
+ interface FormFieldBase {
334
+ id: KycFieldId;
294
335
  labelFallback: string;
295
336
  required: boolean;
296
- regex?: string;
297
- /** SELECT only. */
298
- options?: SelectOption[];
299
- /** HOSTED_LINK only. */
300
- url?: string;
301
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
+ });
302
355
  /** Payload of `KYC{INPUT_REQUIRED}`. One uniform renderer walks `fields`. */
303
356
  export interface FormDescriptor {
304
357
  formId: string;
@@ -310,7 +363,7 @@ export interface FormDescriptor {
310
363
  * transition never re-carries the descriptor (the double-carry disease Δ2 killed).
311
364
  *
312
365
  * Note: the contract's illustrative list reads `'TEXT' | 'NUMBER' | …`; the conformance-package
313
- * 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.
314
367
  */
315
368
  export type FieldSpec = {
316
369
  type: 'TEXT';
@@ -336,8 +389,28 @@ export type FieldSpec = {
336
389
  labelFallback?: string;
337
390
  } | {
338
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;
339
402
  };
340
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
+ }
341
414
  /** Server literals — spread into the body verbatim, opaque to the client. */
342
415
  export type TransitionParams = Record<string, JsonValue>;
343
416
  /** Client-collected specs — the harness renders + locally validates, then merges values in. */
@@ -391,7 +464,22 @@ export interface ReportSpec {
391
464
  * Declared so conformance can assert the surface actually yields them.
392
465
  */
393
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
+ };
394
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
+ };
395
483
  export type TransitionMode = 'SUBMIT' | 'AWAIT' | 'CLIENT_SURFACE';
396
484
  /**
397
485
  * Body assembly, harness-owned, one rule:
@@ -544,16 +632,33 @@ export type FlowState =
544
632
  error?: FailureReason;
545
633
  };
546
634
  export type FlowStateKind = FlowState['kind'];
547
- 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';
548
638
  /**
549
639
  * Every `/fiat/*` response. `transitions: []` = nothing the wizard can do; whether the *flow*
550
640
  * is over is read from the table (`terminal`), never from array emptiness (ON_HOLD is the proof).
551
641
  */
552
- export interface FiatStepResponse {
553
- state: FlowState;
554
- provider: FiatProvider;
642
+ type FiatStepResponseBase = {
555
643
  transitions: Transition[];
556
644
  /** TODO(open-decision): `orderId` placement — see `PAYMENT{INSTRUCT}.orderId` above. */
557
645
  orderId?: string;
558
- }
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 {};
559
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.10.0",
3
+ "version": "0.11.1",
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": {