@volter/twin-stripe 0.1.2 → 2.0.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.
Files changed (219) hide show
  1. package/README.md +64 -27
  2. package/client/dashboard-api.ts +286 -0
  3. package/client/stripe-mirror.css +272 -159
  4. package/client/stripe-mirror.tsx +1384 -541
  5. package/dist/client/dashboard-api.d.ts +107 -0
  6. package/dist/client/dashboard-api.js +238 -0
  7. package/dist/client/dashboard-api.ts +286 -0
  8. package/dist/client/stripe-mirror.bundle.js +236 -0
  9. package/dist/client/stripe-mirror.css +275 -0
  10. package/dist/client/stripe-mirror.d.ts +134 -0
  11. package/dist/client/stripe-mirror.js +823 -0
  12. package/dist/client/stripe-mirror.tsx +1534 -0
  13. package/dist/src/cli.d.ts +2 -0
  14. package/dist/src/cli.js +39 -0
  15. package/dist/src/generated/events.gen.json +1 -0
  16. package/dist/src/generated/surface.gen.json +1 -0
  17. package/dist/src/generated/ui.gen.json +1 -0
  18. package/dist/src/index.d.ts +14 -0
  19. package/dist/src/index.js +73 -0
  20. package/dist/src/manifest.d.ts +2 -0
  21. package/dist/src/manifest.js +1065 -0
  22. package/dist/src/screens/checkout.d.ts +31 -0
  23. package/dist/src/screens/checkout.js +241 -0
  24. package/dist/src/screens/consent-skin.d.ts +4 -0
  25. package/dist/src/screens/consent-skin.js +18 -0
  26. package/dist/src/screens/financial-connections.d.ts +5 -0
  27. package/dist/src/screens/financial-connections.js +90 -0
  28. package/dist/src/screens/identity.d.ts +5 -0
  29. package/dist/src/screens/identity.js +86 -0
  30. package/dist/src/screens/industries.d.ts +1 -0
  31. package/dist/src/screens/industries.js +267 -0
  32. package/dist/src/screens/onboarding.d.ts +13 -0
  33. package/dist/src/screens/onboarding.js +225 -0
  34. package/dist/src/screens/portal.d.ts +5 -0
  35. package/dist/src/screens/portal.js +214 -0
  36. package/dist/src/screens/public-details.d.ts +5 -0
  37. package/dist/src/screens/public-details.js +90 -0
  38. package/dist/src/semantics/after-payment.d.ts +22 -0
  39. package/dist/src/semantics/after-payment.js +93 -0
  40. package/dist/src/semantics/apps-secrets.d.ts +2 -0
  41. package/dist/src/semantics/apps-secrets.js +54 -0
  42. package/dist/src/semantics/balance.d.ts +11 -0
  43. package/dist/src/semantics/balance.js +195 -0
  44. package/dist/src/semantics/billing.d.ts +2 -0
  45. package/dist/src/semantics/billing.js +220 -0
  46. package/dist/src/semantics/charges.d.ts +28 -0
  47. package/dist/src/semantics/charges.js +201 -0
  48. package/dist/src/semantics/checkout.d.ts +15 -0
  49. package/dist/src/semantics/checkout.js +303 -0
  50. package/dist/src/semantics/connect.d.ts +5 -0
  51. package/dist/src/semantics/connect.js +476 -0
  52. package/dist/src/semantics/coupons.d.ts +6 -0
  53. package/dist/src/semantics/coupons.js +92 -0
  54. package/dist/src/semantics/credit-notes.d.ts +2 -0
  55. package/dist/src/semantics/credit-notes.js +172 -0
  56. package/dist/src/semantics/customers.d.ts +6 -0
  57. package/dist/src/semantics/customers.js +429 -0
  58. package/dist/src/semantics/disputes.d.ts +2 -0
  59. package/dist/src/semantics/disputes.js +51 -0
  60. package/dist/src/semantics/entitlements.d.ts +2 -0
  61. package/dist/src/semantics/entitlements.js +95 -0
  62. package/dist/src/semantics/ephemeral-keys.d.ts +2 -0
  63. package/dist/src/semantics/ephemeral-keys.js +34 -0
  64. package/dist/src/semantics/files.d.ts +2 -0
  65. package/dist/src/semantics/files.js +125 -0
  66. package/dist/src/semantics/invoices.d.ts +18 -0
  67. package/dist/src/semantics/invoices.js +541 -0
  68. package/dist/src/semantics/issuing.d.ts +13 -0
  69. package/dist/src/semantics/issuing.js +570 -0
  70. package/dist/src/semantics/ledger.d.ts +54 -0
  71. package/dist/src/semantics/ledger.js +181 -0
  72. package/dist/src/semantics/payment-intents.d.ts +18 -0
  73. package/dist/src/semantics/payment-intents.js +404 -0
  74. package/dist/src/semantics/payment-links.d.ts +2 -0
  75. package/dist/src/semantics/payment-links.js +133 -0
  76. package/dist/src/semantics/payment-methods.d.ts +20 -0
  77. package/dist/src/semantics/payment-methods.js +138 -0
  78. package/dist/src/semantics/plans.d.ts +5 -0
  79. package/dist/src/semantics/plans.js +121 -0
  80. package/dist/src/semantics/platform.d.ts +9 -0
  81. package/dist/src/semantics/platform.js +206 -0
  82. package/dist/src/semantics/products.d.ts +2 -0
  83. package/dist/src/semantics/products.js +140 -0
  84. package/dist/src/semantics/radar.d.ts +2 -0
  85. package/dist/src/semantics/radar.js +83 -0
  86. package/dist/src/semantics/refunds.d.ts +9 -0
  87. package/dist/src/semantics/refunds.js +195 -0
  88. package/dist/src/semantics/renewals.d.ts +47 -0
  89. package/dist/src/semantics/renewals.js +251 -0
  90. package/dist/src/semantics/setup-intents.d.ts +2 -0
  91. package/dist/src/semantics/setup-intents.js +84 -0
  92. package/dist/src/semantics/shared.d.ts +78 -0
  93. package/dist/src/semantics/shared.js +192 -0
  94. package/dist/src/semantics/subscription-schedules.d.ts +2 -0
  95. package/dist/src/semantics/subscription-schedules.js +119 -0
  96. package/dist/src/semantics/subscriptions.d.ts +11 -0
  97. package/dist/src/semantics/subscriptions.js +605 -0
  98. package/dist/src/semantics/tax.d.ts +2 -0
  99. package/dist/src/semantics/tax.js +197 -0
  100. package/dist/src/semantics/terminal.d.ts +5 -0
  101. package/dist/src/semantics/terminal.js +182 -0
  102. package/dist/src/semantics/test-clocks.d.ts +6 -0
  103. package/dist/src/semantics/test-clocks.js +73 -0
  104. package/dist/src/semantics/tokens.d.ts +4 -0
  105. package/dist/src/semantics/tokens.js +44 -0
  106. package/dist/src/semantics/transfers.d.ts +2 -0
  107. package/dist/src/semantics/transfers.js +154 -0
  108. package/dist/src/semantics/treasury.d.ts +2 -0
  109. package/dist/src/semantics/treasury.js +377 -0
  110. package/dist/src/semantics/webhook-endpoints.d.ts +3 -0
  111. package/dist/src/semantics/webhook-endpoints.js +85 -0
  112. package/dist/src/stripe-budget.d.ts +55 -0
  113. package/dist/src/stripe-budget.js +155 -0
  114. package/dist/src/stripe-capabilities.d.ts +3 -0
  115. package/dist/src/stripe-capabilities.js +5052 -0
  116. package/dist/src/stripe-conformance.d.ts +41 -0
  117. package/dist/src/stripe-conformance.js +96 -0
  118. package/dist/src/stripe-connector.d.ts +161 -0
  119. package/dist/src/stripe-connector.js +414 -0
  120. package/dist/src/stripe-emit.d.ts +2 -0
  121. package/dist/src/stripe-emit.js +145 -0
  122. package/dist/src/stripe-events.d.ts +93 -0
  123. package/dist/src/stripe-events.js +388 -0
  124. package/dist/src/stripe-js.d.ts +4 -0
  125. package/dist/src/stripe-js.js +70 -0
  126. package/dist/src/stripe-mirror-ui.d.ts +15 -0
  127. package/dist/src/stripe-mirror-ui.js +87 -0
  128. package/dist/src/stripe-params.d.ts +3 -0
  129. package/dist/src/stripe-params.js +43 -0
  130. package/dist/src/stripe-perform-harness.d.ts +9 -0
  131. package/dist/src/stripe-perform-harness.js +26 -0
  132. package/dist/src/stripe-server.d.ts +33 -0
  133. package/dist/src/stripe-server.js +326 -0
  134. package/dist/src/stripe-shared.d.ts +106 -0
  135. package/dist/src/stripe-shared.js +273 -0
  136. package/dist/src/stripe-twin.d.ts +155 -0
  137. package/dist/src/stripe-twin.js +1226 -0
  138. package/dist/src/stripe-ui-conformance.d.ts +5 -0
  139. package/dist/src/stripe-ui-conformance.js +79 -0
  140. package/dist/src/stripe-ui-structure.d.ts +3 -0
  141. package/dist/src/stripe-ui-structure.js +168 -0
  142. package/dist/src/stripe-version.d.ts +10 -0
  143. package/dist/src/stripe-version.js +285 -0
  144. package/dist/test-fixtures/stripe-known-deviations.json +105 -0
  145. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +14 -0
  146. package/dist/test-fixtures/stripe-openapi-operations.json +4717 -0
  147. package/dist/test-fixtures/stripe-schemas.SOURCE.md +35 -0
  148. package/dist/test-fixtures/stripe-schemas.json +3740 -0
  149. package/package.json +18 -10
  150. package/src/cli.ts +7 -7
  151. package/src/generated/events.gen.json +1 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/generated/ui.gen.json +1 -0
  154. package/src/index.ts +31 -9
  155. package/src/manifest.ts +1097 -0
  156. package/src/screens/checkout.tsx +252 -0
  157. package/src/screens/consent-skin.ts +20 -0
  158. package/src/screens/financial-connections.tsx +101 -0
  159. package/src/screens/identity.tsx +96 -0
  160. package/src/screens/industries.ts +267 -0
  161. package/src/screens/onboarding.tsx +243 -0
  162. package/src/screens/portal.tsx +218 -0
  163. package/src/screens/public-details.tsx +105 -0
  164. package/src/semantics/after-payment.ts +113 -0
  165. package/src/semantics/apps-secrets.ts +58 -0
  166. package/src/semantics/balance.ts +209 -0
  167. package/src/semantics/billing.ts +216 -0
  168. package/src/semantics/charges.ts +211 -0
  169. package/src/semantics/checkout.ts +297 -0
  170. package/src/semantics/connect.ts +471 -0
  171. package/src/semantics/coupons.ts +97 -0
  172. package/src/semantics/credit-notes.ts +168 -0
  173. package/src/semantics/customers.ts +432 -0
  174. package/src/semantics/disputes.ts +62 -0
  175. package/src/semantics/entitlements.ts +94 -0
  176. package/src/semantics/ephemeral-keys.ts +34 -0
  177. package/src/semantics/files.ts +143 -0
  178. package/src/semantics/invoices.ts +541 -0
  179. package/src/semantics/issuing.ts +585 -0
  180. package/src/semantics/ledger.ts +216 -0
  181. package/src/semantics/payment-intents.ts +420 -0
  182. package/src/semantics/payment-links.ts +148 -0
  183. package/src/semantics/payment-methods.ts +143 -0
  184. package/src/semantics/plans.ts +131 -0
  185. package/src/semantics/platform.ts +220 -0
  186. package/src/semantics/products.ts +154 -0
  187. package/src/semantics/radar.ts +85 -0
  188. package/src/semantics/refunds.ts +218 -0
  189. package/src/semantics/renewals.ts +274 -0
  190. package/src/semantics/setup-intents.ts +87 -0
  191. package/src/semantics/shared.ts +215 -0
  192. package/src/semantics/subscription-schedules.ts +129 -0
  193. package/src/semantics/subscriptions.ts +610 -0
  194. package/src/semantics/tax.ts +220 -0
  195. package/src/semantics/terminal.ts +195 -0
  196. package/src/semantics/test-clocks.ts +77 -0
  197. package/src/semantics/tokens.ts +52 -0
  198. package/src/semantics/transfers.ts +174 -0
  199. package/src/semantics/treasury.ts +383 -0
  200. package/src/semantics/webhook-endpoints.ts +87 -0
  201. package/src/stripe-budget.ts +4 -4
  202. package/src/stripe-capabilities.ts +1456 -222
  203. package/src/stripe-conformance.ts +6 -5
  204. package/src/stripe-connector.ts +68 -40
  205. package/src/stripe-emit.ts +14 -7
  206. package/src/stripe-events.ts +94 -36
  207. package/src/stripe-js.ts +70 -0
  208. package/src/stripe-mirror-ui.ts +28 -298
  209. package/src/stripe-params.ts +44 -0
  210. package/src/stripe-perform-harness.ts +29 -0
  211. package/src/stripe-server.ts +263 -38
  212. package/src/stripe-shared.ts +294 -0
  213. package/src/stripe-twin.ts +429 -5325
  214. package/src/stripe-ui-conformance.ts +70 -107
  215. package/src/stripe-ui-structure.ts +124 -348
  216. package/src/stripe-version.ts +278 -0
  217. package/test-fixtures/stripe-known-deviations.json +2 -7
  218. package/test-fixtures/stripe-openapi-operations.json +1188 -2855
  219. package/src/stripe-form.ts +0 -35
@@ -0,0 +1,273 @@
1
+ // The stripe mirror's SHARED vocabulary — formatters, tones, reference resolution — with no
2
+ // import from the server side: the mirror client bundles this into the browser (a server import
3
+ // would drag core, and with it node:async_hooks, into the bundle and the page would never mount).
4
+ // The server module re-exports it, so nothing else moves.
5
+ /** The name an account shows its customers (Checkout, the customer portal): its public business name,
6
+ * `business_profile.name` ("The customer-facing business name", docs.stripe.com/api/accounts/object), which the
7
+ * operator sets on the Dashboard ("You can change a Checkout page's name by modifying the Business name field",
8
+ * docs.stripe.com/payments/checkout/customization/appearance). Where the documentation stops and the twin decides:
9
+ * with none set it falls back to the Dashboard's account name, `settings.dashboard.display_name` ("used on the Stripe
10
+ * Dashboard to differentiate between accounts", the same object page), and with neither to "Twin Inc.", the name the
11
+ * Dashboard mirror has always given the World's own account. */
12
+ export const PLATFORM_DEFAULT_NAME = 'Twin Inc.';
13
+ export function publicBusinessName(account) {
14
+ const named = (v) => (typeof v === 'string' && v.trim() ? v : undefined);
15
+ return named(account?.business_profile?.name) ?? named(account?.settings?.dashboard?.display_name) ?? PLATFORM_DEFAULT_NAME;
16
+ }
17
+ /** Zero-decimal currencies (Stripe stores these in whole units, not cents). */
18
+ const ZERO_DECIMAL = new Set(['bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg', 'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf']);
19
+ /**
20
+ * Format a Stripe minor-unit amount (cents) as a localized currency string.
21
+ * Honors zero-decimal currencies (¥4200 not ¥42.00). Non-numbers → an em dash.
22
+ */
23
+ export function formatStripeAmount(value, currency) {
24
+ if (typeof value !== 'number' || !Number.isFinite(value))
25
+ return '—';
26
+ const ccy = String(currency ?? 'usd').toLowerCase();
27
+ const zero = ZERO_DECIMAL.has(ccy);
28
+ const major = zero ? value : value / 100;
29
+ try {
30
+ return new Intl.NumberFormat('en-US', {
31
+ style: 'currency', currency: ccy.toUpperCase(),
32
+ minimumFractionDigits: zero ? 0 : 2, maximumFractionDigits: zero ? 0 : 2,
33
+ }).format(major);
34
+ }
35
+ catch {
36
+ // Unknown/invalid currency code → a stable, readable fallback.
37
+ return `${zero ? major : major.toFixed(2)} ${ccy.toUpperCase()}`;
38
+ }
39
+ }
40
+ /**
41
+ * Render a Stripe price's `recurring` block as a short interval label, e.g.
42
+ * "every month", "every 3 months". Returns '' when it isn't a recurring price.
43
+ */
44
+ export function formatRecurring(recurring) {
45
+ if (!recurring || typeof recurring !== 'object')
46
+ return '';
47
+ const r = recurring;
48
+ const interval = typeof r.interval === 'string' ? r.interval : '';
49
+ if (!interval)
50
+ return '';
51
+ const count = Number(r.interval_count);
52
+ return count > 1 ? `every ${count} ${interval}s` : `every ${interval}`;
53
+ }
54
+ /**
55
+ * Render a saved payment method as a short human label, e.g. "Visa •••• 4242"
56
+ * for a card, or the bare `type` ("us_bank_account") for non-card methods.
57
+ */
58
+ export function formatPaymentMethod(pm) {
59
+ if (!pm || typeof pm !== 'object')
60
+ return '—';
61
+ const m = pm;
62
+ const card = m.card && typeof m.card === 'object' ? m.card : undefined;
63
+ if (card && (card.brand || card.last4)) {
64
+ const brand = card.brand ? String(card.brand).replace(/\b\w/g, (c) => c.toUpperCase()) : 'Card';
65
+ return card.last4 ? `${brand} •••• ${card.last4}` : brand;
66
+ }
67
+ return typeof m.type === 'string' && m.type ? m.type : (m.id ?? '—');
68
+ }
69
+ /**
70
+ * Extract the HTTP(S) image URLs from a product's `images` field (Stripe stores an
71
+ * array of URL strings) so the mirror can render them as <img> thumbnails instead
72
+ * of plain text. Non-arrays / non-URL entries are dropped. Order is preserved.
73
+ */
74
+ export function productImageUrls(images) {
75
+ if (!Array.isArray(images))
76
+ return [];
77
+ return images.filter((u) => typeof u === 'string' && /^https?:\/\//i.test(u));
78
+ }
79
+ /**
80
+ * Render a payment_intent / charge `last_payment_error` (the test-card decline state)
81
+ * as a single human-readable line, e.g. "card_declined (insufficient_funds): Your card
82
+ * has insufficient funds." Returns '' when there is no error object. This is the
83
+ * vendor-faithful decline reason the twin populates on a declined confirm.
84
+ */
85
+ export function formatPaymentError(error) {
86
+ if (!error || typeof error !== 'object')
87
+ return '';
88
+ const e = error;
89
+ const code = typeof e.code === 'string' ? e.code : '';
90
+ const declineCode = typeof e.decline_code === 'string' ? e.decline_code : '';
91
+ const message = typeof e.message === 'string' ? e.message : '';
92
+ const head = code ? (declineCode ? `${code} (${declineCode})` : code) : declineCode;
93
+ if (head && message)
94
+ return `${head}: ${message}`;
95
+ return head || message;
96
+ }
97
+ /**
98
+ * Summarize a synthesized Stripe `balance` object (available/pending arrays, one entry
99
+ * per currency) into short per-bucket lines, e.g. ["available: $42.00", "pending: $0.00"].
100
+ * The balance is not a list collection, so the mirror renders this summary directly.
101
+ */
102
+ export function formatBalanceSummary(balance) {
103
+ if (!balance || typeof balance !== 'object')
104
+ return [];
105
+ const b = balance;
106
+ const sum = (arr) => {
107
+ if (!Array.isArray(arr) || arr.length === 0)
108
+ return formatStripeAmount(0, 'usd');
109
+ return arr
110
+ .map((e) => formatStripeAmount(e?.amount, e?.currency))
111
+ .join(', ');
112
+ };
113
+ const out = [];
114
+ if (Array.isArray(b.available))
115
+ out.push({ bucket: 'available', text: sum(b.available) });
116
+ if (Array.isArray(b.pending))
117
+ out.push({ bucket: 'pending', text: sum(b.pending) });
118
+ return out;
119
+ }
120
+ export function formatAccountFlags(account) {
121
+ if (!account || typeof account !== 'object')
122
+ return [];
123
+ const a = account;
124
+ return [
125
+ { key: 'charges_enabled', label: 'Charges', enabled: a.charges_enabled === true },
126
+ { key: 'payouts_enabled', label: 'Payouts', enabled: a.payouts_enabled === true },
127
+ { key: 'details_submitted', label: 'Details submitted', enabled: a.details_submitted === true },
128
+ ];
129
+ }
130
+ /**
131
+ * Extract a Connect account's outstanding onboarding requirements (the
132
+ * `requirements.currently_due` list real Stripe shows as "needs attention"). Returns
133
+ * an ordered list of the still-due field paths; empty when nothing is due.
134
+ */
135
+ export function accountCurrentlyDue(account) {
136
+ if (!account || typeof account !== 'object')
137
+ return [];
138
+ const req = account.requirements;
139
+ if (!req || typeof req !== 'object')
140
+ return [];
141
+ const due = req.currently_due;
142
+ return Array.isArray(due) ? due.filter((d) => typeof d === 'string') : [];
143
+ }
144
+ const PILL_OK = new Set(['active', 'succeeded', 'paid', 'true', 'enabled', 'available', 'won']);
145
+ const PILL_WARN = new Set(['open', 'draft', 'pending', 'processing', 'incomplete', 'trialing', 'requires_confirmation', 'requires_action', 'requires_capture', 'requires_payment_method', 'past_due', 'unpaid', 'in_transit', 'warning_needs_response', 'needs_response', 'under_review', 'warning_under_review']);
146
+ const PILL_BAD = new Set(['canceled', 'cancelled', 'void', 'uncollectible', 'failed', 'incomplete_expired', 'false', 'disabled', 'lost', 'charge_refunded']);
147
+ export function statusTone(value) {
148
+ const v = String(value ?? '').toLowerCase();
149
+ if (PILL_OK.has(v))
150
+ return 'ok';
151
+ if (PILL_BAD.has(v))
152
+ return 'bad';
153
+ if (PILL_WARN.has(v))
154
+ return 'warn';
155
+ return '';
156
+ }
157
+ /** True when a key names a field whose value is a Stripe object id we can link. */
158
+ export function isReferenceKey(key) {
159
+ return REFERENCE_FIELDS.has(key);
160
+ }
161
+ // field name → the COLLECTION it points at (so the UI can jump sections + select).
162
+ const REFERENCE_FIELD_MAP = {
163
+ customer: 'customers', product: 'products', price: 'prices',
164
+ subscription: 'subscriptions', latest_invoice: 'invoices', invoice: 'invoices',
165
+ payment_intent: 'payment_intents', latest_charge: 'charges', charge: 'charges',
166
+ payment_method: 'payment_methods', default_payment_method: 'payment_methods',
167
+ dispute: 'disputes', payout: 'payouts', balance_transaction: 'balance_transactions',
168
+ source_transaction: 'charges', setup_intent: 'setup_intents',
169
+ // Connect: a transfer's `destination` points at a connected account.
170
+ account: 'accounts', destination: 'accounts',
171
+ // Billing: a customer_balance_transaction may reference the credit_note that created it.
172
+ credit_note: 'credit_notes',
173
+ };
174
+ const REFERENCE_FIELDS = new Set(Object.keys(REFERENCE_FIELD_MAP));
175
+ /** The collection a reference field points at, or undefined if not a reference. */
176
+ export function referenceCollection(key) {
177
+ return REFERENCE_FIELD_MAP[key];
178
+ }
179
+ /**
180
+ * Flatten an arbitrary nested Stripe value (object / array / scalar) into ordered,
181
+ * indented label/value lines suitable for a readable detail view — instead of the
182
+ * old "[object Object]". Stripe "list" wrappers ({object:'list',data:[...]}) are
183
+ * unwrapped to their `data`. Amount-ish fields are currency-formatted.
184
+ */
185
+ export function flattenStripeValue(value, opts = {}) {
186
+ const depth = opts.depth ?? 0;
187
+ const label = opts.label ?? '';
188
+ const currency = opts.currency;
189
+ if (value === null || value === undefined)
190
+ return [{ depth, label, value: '—' }];
191
+ if (Array.isArray(value)) {
192
+ if (value.length === 0)
193
+ return [{ depth, label, value: '(none)' }];
194
+ const out = label ? [{ depth, label, value: '' }] : [];
195
+ value.forEach((item, i) => {
196
+ const childDepth = label ? depth + 1 : depth;
197
+ out.push(...flattenStripeValue(item, { label: `#${i + 1}`, depth: childDepth, currency }));
198
+ });
199
+ return out;
200
+ }
201
+ if (typeof value === 'object') {
202
+ const obj = value;
203
+ // Unwrap Stripe list objects to their data array.
204
+ if (obj.object === 'list' && Array.isArray(obj.data)) {
205
+ return flattenStripeValue(obj.data, { label, depth, currency });
206
+ }
207
+ const childCurrency = typeof obj.currency === 'string' ? obj.currency : currency;
208
+ const keys = Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== undefined && k !== 'object');
209
+ if (keys.length === 0)
210
+ return [{ depth, label, value: '(empty)' }];
211
+ const out = label ? [{ depth, label, value: '' }] : [];
212
+ const childDepth = label ? depth + 1 : depth;
213
+ for (const k of keys)
214
+ out.push(...flattenStripeValue(obj[k], { label: k, depth: childDepth, currency: childCurrency }));
215
+ return out;
216
+ }
217
+ // scalar
218
+ const ref = typeof value === 'string' && isReferenceKey(label) ? value : undefined;
219
+ const isMoney = typeof value === 'number' && (label === 'unit_amount' || /amount|total|subtotal|balance/.test(label));
220
+ const rendered = isMoney ? formatStripeAmount(value, currency) : String(value);
221
+ return [{ depth, label, value: rendered, ...(ref ? { ref } : {}) }];
222
+ }
223
+ const LABELERS = {
224
+ customers: (r) => r.name || r.email || r.id,
225
+ products: (r) => r.name || r.id,
226
+ prices: (r) => `${formatStripeAmount(r.unit_amount, r.currency)}${formatRecurring(r.recurring) ? ` ${formatRecurring(r.recurring)}` : ''}`,
227
+ subscriptions: (r) => `${r.status ?? 'subscription'} · ${r.id}`,
228
+ invoices: (r) => `${r.status ?? 'invoice'} · ${formatStripeAmount(r.total ?? r.amount_due, r.currency)}`,
229
+ payment_intents: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? ''}`,
230
+ charges: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? ''}`,
231
+ refunds: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? 'refund'}`,
232
+ payment_methods: (r) => `${formatPaymentMethod(r)} · ${r.id}`,
233
+ disputes: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? 'dispute'}`,
234
+ payouts: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? 'payout'}`,
235
+ balance_transactions: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.type ?? 'txn'}`,
236
+ events: (r) => `${r.type ?? 'event'} · ${r.id}`,
237
+ setup_intents: (r) => `${r.status ?? 'setup_intent'} · ${r.id}`,
238
+ invoiceitems: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.id}`,
239
+ accounts: (r) => `${r.email || r.id} · ${r.type ?? 'account'}`,
240
+ transfers: (r) => `${formatStripeAmount(r.amount, r.currency)} → ${r.destination ?? '?'}`,
241
+ credit_notes: (r) => `${formatStripeAmount(r.amount, r.currency)} · ${r.status ?? 'credit_note'}`,
242
+ tax_ids: (r) => `${r.value ?? r.id} · ${r.type ?? ''}`,
243
+ };
244
+ function labelFor(collection, row) {
245
+ return (LABELERS[collection] ?? ((r) => r.id))(row);
246
+ }
247
+ export function resolveCrossRefs(collection, row, data) {
248
+ const outgoing = [];
249
+ const incoming = [];
250
+ if (!row)
251
+ return { outgoing, incoming };
252
+ // Outgoing: every reference field on this row that we can resolve to a row.
253
+ for (const [key, value] of Object.entries(row)) {
254
+ if (typeof value !== 'string' || !isReferenceKey(key))
255
+ continue;
256
+ const target = referenceCollection(key);
257
+ const found = (data[target] ?? []).find((r) => r.id === value);
258
+ outgoing.push({ collection: target, id: value, label: `${key}: ${found ? labelFor(target, found) : value}` });
259
+ }
260
+ // Incoming: rows in other collections whose reference field points at this row.
261
+ for (const [other, rows] of Object.entries(data)) {
262
+ if (other === collection)
263
+ continue;
264
+ for (const r of rows) {
265
+ for (const [key, value] of Object.entries(r)) {
266
+ if (value !== row.id || !isReferenceKey(key))
267
+ continue;
268
+ incoming.push({ collection: other, id: r.id, label: `${labelFor(other, r)}` });
269
+ }
270
+ }
271
+ }
272
+ return { outgoing, incoming };
273
+ }
@@ -0,0 +1,155 @@
1
+ export type StripeRequest = {
2
+ method: string;
3
+ path: string;
4
+ body?: string;
5
+ occurredAt?: string;
6
+ root?: string;
7
+ readOnly?: boolean;
8
+ idempotencyKey?: string;
9
+ apiVersion?: string;
10
+ stripeAccount?: string;
11
+ };
12
+ export type StripeResponse = {
13
+ status: number;
14
+ body: unknown;
15
+ };
16
+ export declare const TWIN_API_VERSION: string;
17
+ export declare function isValidApiVersion(v: string): boolean;
18
+ export declare function validateMoney(params: Record<string, unknown>): StripeResponse | null;
19
+ type DeclineOutcome = {
20
+ code: string;
21
+ decline_code?: string;
22
+ message: string;
23
+ };
24
+ export declare const TEST_TOKEN_DECLINES: Record<string, DeclineOutcome | null>;
25
+ /** A stored subject by stored type and id, in the pack's view (tombstones included): how the
26
+ * semantics hand these helpers the tree they read (semantics/shared.ts `finder`). */
27
+ export type FindStored = (storedType: string, id: string) => Record<string, unknown> | undefined;
28
+ export declare function declineFor(find: FindStored, ...paramSets: Array<Record<string, unknown>>): DeclineOutcome | null | undefined;
29
+ export declare function requiresAuthentication(...paramSets: Array<Record<string, unknown>>): boolean;
30
+ export declare function threeDsNextAction(): Record<string, unknown>;
31
+ export declare function requiresMicrodeposits(...paramSets: Array<Record<string, unknown>>): boolean;
32
+ export declare function microdepositsNextAction(): Record<string, unknown>;
33
+ export declare function verifyMicrodeposits(params: Record<string, unknown>, _existing: Record<string, unknown>): StripeResponse | null;
34
+ /** A card error's body: `charge` "For card errors, the ID of the failed charge", and `payment_intent` "The PaymentIntent
35
+ * object for errors returned on a request involving a PaymentIntent" (docs.stripe.com/api/errors). */
36
+ export declare function cardError(outcome: DeclineOutcome, attach?: {
37
+ charge?: string;
38
+ payment_intent?: Record<string, unknown>;
39
+ }): StripeResponse;
40
+ export declare function nowUnix(occurredAt?: string): number;
41
+ export declare function mintClientSecret(id: string): string;
42
+ export declare const OBJECT_NAME: Record<string, string>;
43
+ export declare function view(type: string, r: Record<string, unknown>): Record<string, unknown>;
44
+ export declare function paginate(items: Array<Record<string, unknown>>, params: Record<string, unknown>): {
45
+ page: Array<Record<string, unknown>>;
46
+ hasMore: boolean;
47
+ };
48
+ /** What follows every stored Stripe write: the webhook for the state change and the stored Event
49
+ * the Events API lists. The derived pack's write hook (manifest.ts) is this same function. */
50
+ export declare function afterStripeWrite(type: string, op: string, out: Record<string, unknown>, root?: string, occurredAt?: string, apiVersion?: string, stripeAccount?: string): Promise<void>;
51
+ /** The id the World's next event takes: its place in the World's events, so it is deterministic and never repeats
52
+ * within a World (a Balance has no id of its own, so nothing about the object can tell two events apart). The number
53
+ * is taken synchronously — the count of stored events, or the last number taken, whichever is higher. */
54
+ export declare function nextStripeEventId(root?: string): string;
55
+ export declare function persistStripeEvent(eventType: string, resource: Record<string, unknown>, root?: string, occurredAt?: string, apiVersion?: string, account?: string, id?: string): Promise<void>;
56
+ export declare function asBool(v: unknown): boolean;
57
+ export declare function searchOver(items: Array<Record<string, unknown>>, params: Record<string, unknown>, path: string): StripeResponse;
58
+ export declare function subscriptionHasPrice(sub: Record<string, unknown>, priceId: string): boolean;
59
+ export declare function subscriptionCouponParam(params: Record<string, unknown>): string | undefined;
60
+ /** A coupon applied: the Discount object, naming what it applies to ("customer: The ID of the customer associated
61
+ * with this discount"; "subscription: The subscription that this coupon is applied to, if it is applied to a
62
+ * particular subscription", docs.stripe.com/api/discounts/object). */
63
+ export declare function buildDiscount(coupon: Record<string, unknown>, customer: string, at: number, on?: {
64
+ subscription?: string;
65
+ }): Record<string, unknown>;
66
+ export declare function subscriptionItemPairs(sub: Record<string, unknown>): Array<{
67
+ price: string;
68
+ quantity: number;
69
+ }>;
70
+ export declare function subscriptionItemEntries(value: unknown): Array<Record<string, unknown>>;
71
+ type BillingInterval = 'day' | 'week' | 'month' | 'year';
72
+ export declare function resolveSubscriptionBillingInterval(entries: Array<Record<string, unknown>>, find: FindStored): {
73
+ interval: BillingInterval;
74
+ interval_count: number;
75
+ resolved: boolean;
76
+ };
77
+ export declare function addBillingInterval(atUnix: number, interval: BillingInterval, count: number): number;
78
+ /** What a subscription item carries of its price: the Price itself and its plan view (a subscription item answers both,
79
+ * docs.stripe.com/api/subscription_items/object), by the price's id or an already-resolved Price. */
80
+ export declare function subscriptionItemPrice(p: unknown, find: FindStored): {
81
+ price: Record<string, unknown> | string | null;
82
+ plan: Record<string, unknown> | null;
83
+ };
84
+ export declare function buildSubscriptionItemsList(entries: Array<Record<string, unknown>>, subId: string, at: number, find: FindStored): Record<string, unknown>;
85
+ /** What a per-unit price bills for a quantity: its `unit_amount` times the quantity, or, for a price given only as a
86
+ * decimal (`unit_amount_decimal`, "represented as a decimal string with at most 12 decimal places", spec/openapi.json.gz),
87
+ * the decimal times the quantity rounded to a whole amount: "rounding occurs after multiplying the quantity by the
88
+ * decimal amount ... `0.05 * 30 = 1.5`, which rounds up to 2 cents" (docs.stripe.com/products-prices/manage-prices).
89
+ * Where the documentation stops and the twin decides: the rounding is to the nearest whole amount, halves up. */
90
+ export declare function priceAmount(price: Record<string, unknown> | undefined | null, quantity: number): number;
91
+ export declare function applyCouponDiscount(subtotal: number, coupon: Record<string, unknown> | undefined): number;
92
+ export declare function resolveTrial(params: Record<string, unknown>, at: number): {
93
+ start: number;
94
+ end: number;
95
+ } | undefined;
96
+ export declare function stashVendorType(_type: string, fields: Record<string, unknown>): Record<string, unknown>;
97
+ export declare function embedIssuingAuthorizationCard(body: Record<string, unknown>, root?: string): Record<string, unknown>;
98
+ export declare function emptySpendingControls(): Record<string, unknown>;
99
+ export declare function normalizeSpendingControls(raw: unknown, currency: string | null): {
100
+ controls?: Record<string, unknown>;
101
+ error?: StripeResponse;
102
+ };
103
+ type PriorAuthorization = {
104
+ amount: number;
105
+ created: number;
106
+ category: string;
107
+ };
108
+ export declare function spendingControlsViolation(controls: unknown, opts: {
109
+ amount: number;
110
+ category: string;
111
+ country: string | null;
112
+ nowSec: number;
113
+ priorApproved: PriorAuthorization[];
114
+ }): string | null;
115
+ export declare function disputeEvidence(): Record<string, unknown>;
116
+ export declare function paymentMethodSubObject(pmType: string, params: Record<string, unknown>): Record<string, unknown>;
117
+ export declare const ACCOUNT_TYPES: Set<string>;
118
+ export declare const PLATFORM_ACCOUNT_ID = "acct_twin_self";
119
+ export declare function accountRequirements(): Record<string, unknown>;
120
+ export declare function accountCapabilities(params: Record<string, unknown>): Record<string, unknown>;
121
+ export declare function accountSettings(input: unknown, existing?: Record<string, unknown>): Record<string, unknown>;
122
+ export type CheckoutItem = {
123
+ object: 'item';
124
+ id: string;
125
+ amount_discount: number;
126
+ amount_subtotal: number;
127
+ amount_tax: number;
128
+ amount_total: number;
129
+ currency: string;
130
+ description: string | null;
131
+ price: Record<string, unknown> | string | null;
132
+ quantity: number;
133
+ };
134
+ export declare function resolveLineItem(entry: Record<string, unknown>, index: number, fallbackCurrency: string, find: FindStored): {
135
+ item: CheckoutItem;
136
+ priceMissing?: string;
137
+ };
138
+ export declare function lineItemEntries(params: Record<string, unknown>): Array<Record<string, unknown>>;
139
+ export declare function chargeDefaults(chargeId: string, amount: number, captured: boolean, occurredAt?: string): Record<string, unknown>;
140
+ /**
141
+ * Public entry: honors the Idempotency-Key (replay → stored response, no re-write)
142
+ * then delegates to the router. Only POSTs are idempotent (matches Stripe; GET/DELETE
143
+ * pass straight through). A read-only twin never stores (it cannot write).
144
+ */
145
+ /** Stripe's API as one call: the request goes through the pack's own dispatch (the derived dispatch over
146
+ * its semantics, its derived core and the hand-written routes below), so a caller that holds no
147
+ * HTTP server reaches exactly what an SDK does. `occurredAt` pins the World instant. */
148
+ export declare function handleStripeTwinRequest(req: StripeRequest): Promise<StripeResponse>;
149
+ /**
150
+ * The twin's doors: what stands in for an act Stripe's API does not have, each under `/_twin/`. The
151
+ * platform's own account settings (its payout schedule) change on Stripe's Dashboard, which has no API:
152
+ * POST /_twin/account stands in for that page. Answers nothing for any other path.
153
+ */
154
+ export declare function handleStripeDoor(req: StripeRequest, params: Record<string, unknown>): Promise<StripeResponse | undefined>;
155
+ export {};