payment-kit 1.29.1 → 1.29.3

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 (343) hide show
  1. package/api/dev.ts +41 -2
  2. package/api/hono.d.ts +42 -0
  3. package/api/node-sqlite.d.ts +12 -0
  4. package/api/src/bootstrap.ts +47 -0
  5. package/api/src/crons/base.ts +3 -3
  6. package/api/src/crons/currency.ts +1 -1
  7. package/api/src/crons/index.ts +41 -37
  8. package/api/src/crons/metering-subscription-detection.ts +1 -1
  9. package/api/src/crons/overdue-detection.ts +2 -2
  10. package/api/src/crons/retry-pending-events.ts +6 -0
  11. package/api/src/crons/tenant-fanout.ts +82 -0
  12. package/api/src/host-node/did-connect-runtime-node.ts +33 -0
  13. package/api/src/host-node/serve-static-arc.ts +68 -0
  14. package/api/src/host-node/serve-static.ts +41 -0
  15. package/api/src/index.ts +22 -161
  16. package/api/src/integrations/app-store/client.ts +3 -4
  17. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  18. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  19. package/api/src/integrations/arcblock/token.ts +21 -7
  20. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  21. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  22. package/api/src/integrations/google-play/verify.ts +3 -2
  23. package/api/src/integrations/iap-reconcile.ts +3 -5
  24. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  25. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  26. package/api/src/libs/archive/query.ts +19 -0
  27. package/api/src/libs/audit.ts +61 -4
  28. package/api/src/libs/auth.ts +247 -47
  29. package/api/src/libs/context.ts +89 -1
  30. package/api/src/libs/currency.ts +2 -2
  31. package/api/src/libs/dayjs.ts +8 -2
  32. package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
  33. package/api/src/libs/did-connect/tenant-identity.ts +221 -0
  34. package/api/src/libs/drivers/auth-storage.ts +118 -0
  35. package/api/src/libs/drivers/cron.ts +264 -0
  36. package/api/src/libs/drivers/db.ts +170 -0
  37. package/api/src/libs/drivers/identity.ts +142 -0
  38. package/api/src/libs/drivers/index.ts +40 -0
  39. package/api/src/libs/drivers/locks.ts +226 -0
  40. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  41. package/api/src/libs/drivers/queue.ts +104 -0
  42. package/api/src/libs/drivers/secrets.ts +194 -0
  43. package/api/src/libs/env.ts +170 -54
  44. package/api/src/libs/exchange-rate/service.ts +7 -6
  45. package/api/src/libs/http-fetch-adapter.ts +60 -0
  46. package/api/src/libs/invoice.ts +1 -1
  47. package/api/src/libs/lock.ts +51 -47
  48. package/api/src/libs/logger.ts +48 -8
  49. package/api/src/libs/notification/index.ts +1 -1
  50. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  51. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  52. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  53. package/api/src/libs/overdraft-protection.ts +1 -1
  54. package/api/src/libs/payout.ts +1 -1
  55. package/api/src/libs/queue/index.ts +271 -52
  56. package/api/src/libs/queue/runtime.ts +175 -0
  57. package/api/src/libs/resource.ts +3 -3
  58. package/api/src/libs/secrets.ts +38 -0
  59. package/api/src/libs/session.ts +3 -2
  60. package/api/src/libs/subscription.ts +5 -5
  61. package/api/src/libs/tenant.ts +92 -0
  62. package/api/src/libs/url.ts +3 -3
  63. package/api/src/libs/util.ts +21 -13
  64. package/api/src/middlewares/hono/cdn.ts +63 -0
  65. package/api/src/middlewares/hono/context.ts +80 -0
  66. package/api/src/middlewares/hono/csrf.ts +83 -0
  67. package/api/src/middlewares/hono/fallback.ts +194 -0
  68. package/api/src/middlewares/hono/pipeline.ts +73 -0
  69. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  70. package/api/src/middlewares/hono/resource.ts +63 -0
  71. package/api/src/middlewares/hono/security.ts +209 -0
  72. package/api/src/middlewares/hono/session.ts +114 -0
  73. package/api/src/middlewares/hono/xss.ts +61 -0
  74. package/api/src/queues/auto-recharge.ts +12 -10
  75. package/api/src/queues/checkout-session.ts +38 -21
  76. package/api/src/queues/credit-consume.ts +40 -36
  77. package/api/src/queues/credit-grant.ts +25 -18
  78. package/api/src/queues/credit-reconciliation.ts +7 -5
  79. package/api/src/queues/discount-status.ts +9 -6
  80. package/api/src/queues/event.ts +41 -11
  81. package/api/src/queues/exchange-rate-health.ts +49 -30
  82. package/api/src/queues/invoice.ts +18 -15
  83. package/api/src/queues/notification.ts +14 -7
  84. package/api/src/queues/payment.ts +64 -37
  85. package/api/src/queues/payout.ts +37 -21
  86. package/api/src/queues/refund.ts +36 -18
  87. package/api/src/queues/subscription.ts +83 -53
  88. package/api/src/queues/token-transfer.ts +15 -10
  89. package/api/src/queues/usage-record.ts +8 -5
  90. package/api/src/queues/vendors/commission.ts +7 -5
  91. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  92. package/api/src/queues/vendors/fulfillment.ts +4 -2
  93. package/api/src/queues/vendors/return-processor.ts +5 -3
  94. package/api/src/queues/vendors/return-scanner.ts +5 -4
  95. package/api/src/queues/vendors/status-check.ts +10 -7
  96. package/api/src/queues/webhook.ts +60 -32
  97. package/api/src/routes/connect/shared.ts +1 -2
  98. package/api/src/routes/connect/subscribe.ts +3 -3
  99. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  100. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  101. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  102. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  103. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  104. package/api/src/routes/hono/credit-tokens.ts +43 -0
  105. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  106. package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
  107. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  108. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  109. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  110. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  111. package/api/src/routes/hono/exchange-rates.ts +77 -0
  112. package/api/src/routes/hono/index.ts +115 -0
  113. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  114. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  115. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  116. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  117. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  118. package/api/src/routes/hono/meters.ts +288 -0
  119. package/api/src/routes/hono/passports.ts +73 -0
  120. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  121. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  122. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  123. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  124. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  125. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  126. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  127. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  128. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  129. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  130. package/api/src/routes/hono/redirect.ts +24 -0
  131. package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
  132. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  133. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  134. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  135. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  136. package/api/src/routes/hono/tool.ts +69 -0
  137. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  138. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  139. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  140. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  141. package/api/src/service.ts +814 -0
  142. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  143. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  144. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  145. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  146. package/api/src/store/models/auto-recharge-config.ts +22 -10
  147. package/api/src/store/models/checkout-session.ts +15 -14
  148. package/api/src/store/models/coupon.ts +29 -20
  149. package/api/src/store/models/credit-grant.ts +38 -29
  150. package/api/src/store/models/credit-transaction.ts +32 -21
  151. package/api/src/store/models/customer.ts +19 -17
  152. package/api/src/store/models/discount.ts +11 -2
  153. package/api/src/store/models/entitlement-grant.ts +21 -9
  154. package/api/src/store/models/entitlement-product.ts +21 -9
  155. package/api/src/store/models/entitlement.ts +19 -10
  156. package/api/src/store/models/event.ts +18 -9
  157. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  158. package/api/src/store/models/invoice-item.ts +18 -9
  159. package/api/src/store/models/invoice.ts +16 -8
  160. package/api/src/store/models/meter-event.ts +27 -9
  161. package/api/src/store/models/meter.ts +31 -22
  162. package/api/src/store/models/payment-currency.ts +25 -8
  163. package/api/src/store/models/payment-intent.ts +15 -6
  164. package/api/src/store/models/payment-link.ts +15 -6
  165. package/api/src/store/models/payment-method.ts +38 -22
  166. package/api/src/store/models/payment-stat.ts +18 -9
  167. package/api/src/store/models/payout.ts +15 -6
  168. package/api/src/store/models/price-quote.ts +17 -8
  169. package/api/src/store/models/price.ts +24 -12
  170. package/api/src/store/models/pricing-table.ts +29 -20
  171. package/api/src/store/models/product-vendor.ts +20 -10
  172. package/api/src/store/models/product.ts +15 -6
  173. package/api/src/store/models/promotion-code.ts +14 -6
  174. package/api/src/store/models/refund.ts +15 -6
  175. package/api/src/store/models/revenue-snapshot.ts +21 -9
  176. package/api/src/store/models/setting.ts +18 -9
  177. package/api/src/store/models/setup-intent.ts +36 -27
  178. package/api/src/store/models/subscription-item.ts +21 -9
  179. package/api/src/store/models/subscription-schedule.ts +21 -9
  180. package/api/src/store/models/subscription.ts +21 -10
  181. package/api/src/store/models/tax-rate.ts +29 -21
  182. package/api/src/store/models/usage-record.ts +11 -2
  183. package/api/src/store/models/webhook-attempt.ts +18 -9
  184. package/api/src/store/models/webhook-endpoint.ts +18 -9
  185. package/api/src/store/scoped-core.ts +55 -0
  186. package/api/src/store/scoped.ts +247 -0
  187. package/api/src/store/sequelize.ts +82 -23
  188. package/api/src/store/sql-migrations.ts +20 -0
  189. package/api/src/store/tenant-backfill.ts +260 -0
  190. package/api/src/store/tenant-model.ts +124 -0
  191. package/api/src/store/tenant-tables.ts +50 -0
  192. package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
  193. package/api/tests/crons/tenant-fanout.spec.ts +158 -0
  194. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  195. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  196. package/api/tests/fixtures/core-env-violation.ts +10 -0
  197. package/api/tests/fixtures/host-read-violation.ts +19 -0
  198. package/api/tests/fixtures/tenants.ts +4 -0
  199. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  200. package/api/tests/libs/archive-query.spec.ts +26 -0
  201. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  202. package/api/tests/libs/context.spec.ts +204 -0
  203. package/api/tests/libs/core-config.spec.ts +115 -0
  204. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  205. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  206. package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
  207. package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
  208. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  209. package/api/tests/libs/scoped.spec.ts +222 -0
  210. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  211. package/api/tests/libs/service-host.spec.ts +37 -0
  212. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  213. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  214. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  215. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  216. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  217. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  218. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  219. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  220. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  221. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  222. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  223. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  224. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  225. package/api/tests/models/tenant-columns.spec.ts +161 -0
  226. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  227. package/api/tests/queues/credit-consume.spec.ts +8 -1
  228. package/api/tests/queues/event-tenant.spec.ts +292 -0
  229. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  230. package/api/tests/queues/queue-parity.spec.ts +249 -0
  231. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  232. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  233. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  234. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  235. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  236. package/api/tests/service/collapse.spec.ts +96 -0
  237. package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
  238. package/api/tests/service/fail-closed-http.spec.ts +79 -0
  239. package/api/tests/service/static-arc-handler.spec.ts +101 -0
  240. package/api/tests/service/static-externalized.spec.ts +48 -0
  241. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  242. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  243. package/api/tests/store/tenant-model.spec.ts +162 -0
  244. package/api/tests/store/tenant-residual.spec.ts +196 -0
  245. package/api/third.d.ts +4 -0
  246. package/blocklet.yml +1 -1
  247. package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
  248. package/cloudflare/README.md +34 -27
  249. package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
  250. package/cloudflare/build.ts +33 -13
  251. package/cloudflare/cf-adapter.ts +419 -0
  252. package/cloudflare/did-connect-runtime.ts +96 -0
  253. package/cloudflare/did-connect-token-storage.ts +151 -0
  254. package/cloudflare/esbuild-cf-config.cjs +407 -0
  255. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  256. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  257. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  258. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  259. package/cloudflare/queue-runtime-mode.ts +13 -0
  260. package/cloudflare/run-build.js +33 -403
  261. package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
  262. package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
  263. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  264. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  265. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  266. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  267. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  268. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  269. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
  270. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
  271. package/cloudflare/shims/cron.ts +38 -158
  272. package/cloudflare/shims/events.ts +124 -0
  273. package/cloudflare/shims/fastq.ts +15 -1
  274. package/cloudflare/shims/nedb-storage.ts +16 -8
  275. package/cloudflare/shims/xss.ts +8 -0
  276. package/cloudflare/tenant-middleware.ts +36 -0
  277. package/cloudflare/tests/cf-adapter.spec.ts +244 -0
  278. package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
  279. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  280. package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
  281. package/cloudflare/vite.config.ts +53 -45
  282. package/cloudflare/worker.ts +261 -448
  283. package/cloudflare/wrangler.json +0 -6
  284. package/cloudflare/wrangler.jsonc +0 -6
  285. package/cloudflare/wrangler.local-e2e.jsonc +25 -0
  286. package/cloudflare/wrangler.staging.json +0 -6
  287. package/jest.config.js +3 -1
  288. package/package.json +33 -38
  289. package/scripts/bootstrap-inject.ts +166 -0
  290. package/scripts/core-env-whitelist.json +1 -0
  291. package/scripts/e2e-12b-runtime.ts +149 -0
  292. package/scripts/e2e-core-config.ts +125 -0
  293. package/scripts/e2e-d1-tenancy.ts +116 -0
  294. package/scripts/e2e-d2-cron-queue.ts +139 -0
  295. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  296. package/scripts/e2e-hono-s2.ts +125 -0
  297. package/scripts/e2e-hono-s3e.ts +135 -0
  298. package/scripts/e2e-hono-s4.ts +114 -0
  299. package/scripts/e2e-migration-contract.ts +100 -0
  300. package/scripts/e2e-s0.ts +61 -0
  301. package/scripts/e2e-s1.ts +107 -0
  302. package/scripts/e2e-s2.ts +178 -0
  303. package/scripts/e2e-s3.ts +110 -0
  304. package/scripts/e2e-s4.ts +191 -0
  305. package/scripts/e2e-s5.ts +139 -0
  306. package/scripts/e2e-s6.ts +127 -0
  307. package/scripts/e2e-tenant-model.ts +119 -0
  308. package/scripts/e2e-tenant-worker.ts +199 -0
  309. package/scripts/gen-sql-migrations.js +46 -0
  310. package/scripts/phase8-codemod.js +219 -0
  311. package/scripts/phase9a-env-getters-codemod.js +82 -0
  312. package/scripts/scan-core-env.js +109 -0
  313. package/scripts/scan-tenant-queries.js +235 -0
  314. package/scripts/schema-drift-guard.ts +210 -0
  315. package/scripts/tenant-scan-whitelist.json +1 -0
  316. package/src/app.tsx +2 -1
  317. package/src/env.d.ts +13 -1
  318. package/src/libs/service-host.ts +13 -0
  319. package/tsconfig.json +1 -1
  320. package/vite.arc.config.ts +159 -0
  321. package/api/src/libs/did-space.ts +0 -235
  322. package/api/src/libs/middleware.ts +0 -50
  323. package/api/src/libs/security.ts +0 -192
  324. package/api/src/queues/space.ts +0 -662
  325. package/api/src/routes/credit-tokens.ts +0 -38
  326. package/api/src/routes/exchange-rates.ts +0 -87
  327. package/api/src/routes/index.ts +0 -142
  328. package/api/src/routes/integrations/stripe.ts +0 -61
  329. package/api/src/routes/meters.ts +0 -274
  330. package/api/src/routes/passports.ts +0 -68
  331. package/api/src/routes/redirect.ts +0 -20
  332. package/api/src/routes/tool.ts +0 -65
  333. package/api/src/routes/webhook-endpoints.ts +0 -126
  334. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  335. package/cloudflare/did-connect-auth.ts +0 -527
  336. package/cloudflare/shims/did-space-js.ts +0 -17
  337. package/cloudflare/shims/did-space.ts +0 -11
  338. package/cloudflare/shims/express-compat/index.ts +0 -80
  339. package/cloudflare/shims/express-compat/types.ts +0 -41
  340. package/cloudflare/shims/lock.ts +0 -115
  341. package/cloudflare/shims/queue.ts +0 -611
  342. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  343. package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+ // W1′ Phase 5: structural tenant-isolation backstop scanner.
4
+ //
5
+ // Phase 3 made the 38 tenant models `extends TenantModel`, so a bare
6
+ // `Coupon.findAll()` is now tenant-scoped BY CONSTRUCTION — the old denylist
7
+ // ("bare model query = violation, 151-file whitelist") is obsolete and
8
+ // fail-open. This scanner replaces it with two STRUCTURAL invariants that make
9
+ // the safety load-bearing, plus the single-Host-reader rule:
10
+ //
11
+ // Assertion ① every tenant-table model `extends TenantModel` (not `Model`).
12
+ // If a tenant model regresses to `extends Model`, every query on
13
+ // it silently stops being scoped — caught here.
14
+ // Assertion ② a raw `.query(...)` whose SQL touches a tenant TABLE (DML, not
15
+ // DDL) must bind a tenant via `$instance_did` / `:instance_did`.
16
+ // Host rule the raw Host header is read at exactly one place (middleware).
17
+ //
18
+ // The whitelist collapses from 151 files to a handful of genuine raw/system
19
+ // exceptions (see tenant-scan-whitelist.json, reasons in three classes).
20
+ //
21
+ // Usage: node scripts/scan-tenant-queries.js [--json] [extra files...]
22
+ // Exit 1 when violations outside the whitelist are found (or whitelist is stale).
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ const ROOT = path.join(__dirname, '..');
28
+ const SRC_DIR = path.join(ROOT, 'api/src');
29
+ const MODELS_DIR = path.join(ROOT, 'api/src/store/models');
30
+ // TENANT_SCAN_WHITELIST overrides the whitelist path (tests point it at a temp
31
+ // file to prove an entry can never wildcard-swallow a directory).
32
+ const WHITELIST_FILE = process.env.TENANT_SCAN_WHITELIST || path.join(__dirname, 'tenant-scan-whitelist.json');
33
+
34
+ // canonical single source (scoped.ts re-exports this same constant)
35
+ const { TENANT_TABLES } = require(path.join(ROOT, 'api/src/store/tenant-tables.ts'));
36
+ const TENANT_TABLE_SET = new Set(TENANT_TABLES);
37
+
38
+ // table name -> model class name (customers -> Customer, payment_currencies -> PaymentCurrency)
39
+ function modelNameFor(table) {
40
+ const singular = table.endsWith('ies') ? `${table.slice(0, -3)}y` : table.replace(/s$/, '');
41
+ return singular
42
+ .split('_')
43
+ .map((part) => part[0].toUpperCase() + part.slice(1))
44
+ .join('');
45
+ }
46
+ const TENANT_MODEL_NAMES = new Set(TENANT_TABLES.map(modelNameFor));
47
+
48
+ // The raw-query exemptions encode the THREE reason classes that used to live as
49
+ // 151 whole-file whitelist entries (Phase 1 declared them; Phase 5 makes them
50
+ // structural). The whitelist file itself is now empty — these are the only
51
+ // exceptions, and they are by-design, not "unscheduled".
52
+ //
53
+ // class ① design-permanent : migrations + tenant-backfill cross tenants by
54
+ // design (RAW_EXEMPT_PATH).
55
+ // class ② entry-validated / : the scopedQuery guard helper (scoped.ts), the
56
+ // contract D1/SQLite executor whose callers own scoping
57
+ // (drivers/db.ts), startup pragmas (sequelize.ts),
58
+ // and DDL/schema statements (DDL regex below).
59
+ // class ③ unscheduled route : ELIMINATED — the route read surface is now
60
+ // tenant-scoped by construction (TenantModel),
61
+ // so it needs no exception at all.
62
+ const RAW_EXEMPT_FILES = new Set([
63
+ path.join('api', 'src', 'store', 'scoped.ts'),
64
+ path.join('api', 'src', 'libs', 'drivers', 'db.ts'),
65
+ path.join('api', 'src', 'store', 'sequelize.ts'),
66
+ ]);
67
+ const RAW_EXEMPT_PATH = /(^|\/)(store\/migrations|store\/tenant-backfill)/;
68
+
69
+ const ANY_QUERY = /\.query\s*\(/g;
70
+ // The STATEMENT VERB — the first keyword inside the query's string literal —
71
+ // decides DDL vs DML, so a DML SELECT can't hide behind a later `/* CREATE */`.
72
+ const QUERY_VERB = /\.query\s*\(\s*[`'"]\s*(\w+)/;
73
+ const DDL_VERBS = new Set(['PRAGMA', 'CREATE', 'ALTER', 'DROP', 'ATTACH', 'DETACH', 'REINDEX', 'VACUUM']);
74
+ const TENANT_BIND = /[$:]instance_did\b/;
75
+ const TENANT_TABLE_IN_SQL = new RegExp(`\\b(${TENANT_TABLES.join('|')})\\b`);
76
+
77
+ const HOST_READ =
78
+ /\breq\s*\.\s*(?:headers\s*(?:\.\s*host\b|\[\s*['"]host['"]\s*\])|hostname\b|get\s*\(\s*['"]host['"])/gi;
79
+ const HOST_READ_EXEMPT = [path.join('api', 'src', 'libs', 'middleware.ts')];
80
+
81
+ function listFiles(dir) {
82
+ const out = [];
83
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
84
+ const full = path.join(dir, entry.name);
85
+ if (entry.isDirectory()) out.push(...listFiles(full));
86
+ else if (entry.isFile() && full.endsWith('.ts')) out.push(full);
87
+ }
88
+ return out;
89
+ }
90
+
91
+ function stripLineComments(source) {
92
+ return source.replace(/\/\/[^\n]*/g, (m) => ' '.repeat(m.length));
93
+ }
94
+
95
+ function lineOf(source, index) {
96
+ return source.slice(0, index).split('\n').length;
97
+ }
98
+
99
+ // Assertion ① — scan model files: a tenant-table model must extend TenantModel.
100
+ function scanModelExtends() {
101
+ const violations = [];
102
+ for (const file of listFiles(MODELS_DIR)) {
103
+ const source = stripLineComments(fs.readFileSync(file, 'utf8'));
104
+ const rel = path.relative(ROOT, file);
105
+ const re = /export\s+class\s+(\w+)\s+extends\s+(\w+)\s*</g;
106
+ let m;
107
+ // eslint-disable-next-line no-cond-assign
108
+ while ((m = re.exec(source))) {
109
+ const [, className, base] = m;
110
+ if (TENANT_MODEL_NAMES.has(className) && base !== 'TenantModel') {
111
+ violations.push({
112
+ file: rel,
113
+ line: lineOf(source, m.index),
114
+ call: `${className} extends ${base}`,
115
+ kind: 'model-not-tenant',
116
+ });
117
+ }
118
+ }
119
+ }
120
+ return violations;
121
+ }
122
+
123
+ // Assertion ② (+ Host rule) — per-file scan.
124
+ function scanFile(file) {
125
+ const rel = path.relative(ROOT, file);
126
+ // Bad input: a file we cannot read/decode must FAIL LOUDLY, never be silently
127
+ // skipped (a silent skip would let an unscannable file hide a violation).
128
+ let raw;
129
+ try {
130
+ raw = fs.readFileSync(file, 'utf8');
131
+ } catch (err) {
132
+ return [{ file: rel, line: 0, call: `unreadable file: ${err.message}`, kind: 'unreadable' }];
133
+ }
134
+ if (raw.includes('\x00') || raw.includes('\uFFFD')) {
135
+ return [{ file: rel, line: 0, call: 'unparseable (binary/non-utf8) file', kind: 'unreadable' }];
136
+ }
137
+ const source = stripLineComments(raw);
138
+ const violations = [];
139
+
140
+ const rawExempt = RAW_EXEMPT_FILES.has(rel) || RAW_EXEMPT_PATH.test(rel.replace(/\\/g, '/'));
141
+ if (!rawExempt) {
142
+ ANY_QUERY.lastIndex = 0;
143
+ let m;
144
+ // eslint-disable-next-line no-cond-assign
145
+ while ((m = ANY_QUERY.exec(source))) {
146
+ // Skip the HTTP query accessor `req.query(...)` / `c.req.query(...)` — it
147
+ // reads the request query string, not SQL. hono resource routes (Phase 3
148
+ // express→hono) call `c.req.query()` pervasively; without this guard the
149
+ // 600-char SQL window below catches a nearby tenant-model name and false-
150
+ // positives. A raw `sequelize.query(` is never preceded by `req`.
151
+ if (source.slice(m.index - 3, m.index) === 'req') continue;
152
+ // inspect the SQL window after the `.query(` opening
153
+ const sql = source.slice(m.index, m.index + 600);
154
+ const verb = (sql.match(QUERY_VERB)?.[1] || '').toUpperCase();
155
+ if (DDL_VERBS.has(verb)) continue; // schema op, not a tenant-row read
156
+ if (TENANT_TABLE_IN_SQL.test(sql) && !TENANT_BIND.test(sql)) {
157
+ violations.push({
158
+ file: rel,
159
+ line: lineOf(source, m.index),
160
+ call: 'raw .query on tenant table without $instance_did',
161
+ kind: 'raw-unguarded',
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ if (!HOST_READ_EXEMPT.includes(rel)) {
168
+ HOST_READ.lastIndex = 0;
169
+ let m;
170
+ // eslint-disable-next-line no-cond-assign
171
+ while ((m = HOST_READ.exec(source))) {
172
+ violations.push({ file: rel, line: lineOf(source, m.index), call: m[0].replace(/\s*\($/, ''), kind: 'host' });
173
+ }
174
+ }
175
+ return violations;
176
+ }
177
+
178
+ function main() {
179
+ const args = process.argv.slice(2);
180
+ const json = args.includes('--json');
181
+ const extraFiles = args.filter((a) => !a.startsWith('--'));
182
+
183
+ const whitelist = JSON.parse(fs.readFileSync(WHITELIST_FILE, 'utf8'));
184
+ const whitelisted = new Set(whitelist.map((entry) => entry.file));
185
+
186
+ const files = extraFiles.length ? extraFiles.map((f) => path.resolve(f)) : listFiles(SRC_DIR);
187
+
188
+ let violations = [];
189
+ let whitelistedCount = 0;
190
+ for (const file of files) {
191
+ const rel = path.relative(ROOT, file);
192
+ const found = scanFile(file);
193
+ if (!found.length) continue;
194
+ if (whitelisted.has(rel)) whitelistedCount += found.length;
195
+ else violations = violations.concat(found);
196
+ }
197
+
198
+ // Assertion ① runs over the canonical model dir (skip when scanning explicit
199
+ // extra files, e.g. the scanner self-test fixture).
200
+ if (!extraFiles.length) {
201
+ for (const v of scanModelExtends()) {
202
+ if (whitelisted.has(v.file)) whitelistedCount += 1;
203
+ else violations.push(v);
204
+ }
205
+ }
206
+
207
+ // a whitelist entry is stale when its file is clean (or gone) under the NEW rules
208
+ const stale = whitelist
209
+ .map((entry) => entry.file)
210
+ .filter((file) => {
211
+ const full = path.join(ROOT, file);
212
+ if (!fs.existsSync(full)) return true;
213
+ return scanFile(full).length === 0;
214
+ });
215
+
216
+ const result = { violations, whitelisted: whitelistedCount, staleWhitelistEntries: stale };
217
+ if (json) {
218
+ console.log(JSON.stringify(result));
219
+ } else {
220
+ for (const v of violations) {
221
+ const label =
222
+ v.kind === 'host'
223
+ ? 'host read outside tenant middleware'
224
+ : v.kind === 'model-not-tenant'
225
+ ? 'tenant model not extending TenantModel'
226
+ : 'raw query on tenant table missing $instance_did';
227
+ console.error(`${label}: ${v.file}:${v.line} ${v.call}`);
228
+ }
229
+ if (stale.length) console.error(`stale whitelist entries (remove them): ${stale.join(', ')}`);
230
+ console.log(`tenant-scan: ${violations.length} violations, ${whitelistedCount} whitelisted`);
231
+ }
232
+ process.exit(violations.length > 0 || stale.length > 0 ? 1 : 0);
233
+ }
234
+
235
+ main();
@@ -0,0 +1,210 @@
1
+ /* eslint-disable no-console */
2
+ // Phase 11 (W2′): schema-drift guard — the "done" gate for the D1 SQL migration
3
+ // lineage (Path A). It builds two schemas from scratch and diffs them:
4
+ // - CANONICAL: the 75 Umzug TS migrations (api/src/store/migrations) run via
5
+ // api/src/store/migrate.ts — the authoritative blocklet-server SQLite schema.
6
+ // - D1 LINEAGE: the 7 cloudflare/migrations/*.sql applied to a fresh SQLite.
7
+ // If the D1 lineage drifts (missing/extra tables, columns, or indexes), the SQL
8
+ // lineage must be fixed — NOT switched to Umzug-on-D1.
9
+ //
10
+ // npx tsx scripts/schema-drift-guard.ts
11
+ // Exits non-zero on table/column drift (index drift is reported, warn-only).
12
+
13
+ import { DatabaseSync } from 'node:sqlite';
14
+ import fs from 'fs';
15
+ import os from 'os';
16
+ import path from 'path';
17
+ import { fromRandom } from '@ocap/wallet';
18
+ import { types } from '@ocap/mcrypto';
19
+
20
+ // Self-build a throwaway test blocklet env BEFORE importing the core: the
21
+ // canonical (Umzug) import graph pulls auth/wallet (libs/auth.ts initializes a
22
+ // wallet at import). This gate must NOT depend on the developer's machine env —
23
+ // it provisions its own, like the e2e scripts.
24
+ function setupTestEnv(): string {
25
+ const w = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
26
+ const a = w.address;
27
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'drift-env-'));
28
+ Object.assign(process.env, {
29
+ ABT_NODE_DID: a,
30
+ ABT_NODE_PK: w.publicKey,
31
+ ABT_NODE_PORT: '8089',
32
+ ABT_NODE_SERVICE_PORT: '40406',
33
+ BLOCKLET_MODE: 'test',
34
+ BLOCKLET_DID: a,
35
+ BLOCKLET_COMPONENT_DID: a,
36
+ BLOCKLET_APP_SK: w.secretKey,
37
+ BLOCKLET_APP_PSK: w.secretKey,
38
+ BLOCKLET_APP_PK: w.publicKey,
39
+ BLOCKLET_APP_EK: w.secretKey,
40
+ BLOCKLET_APP_ID: a,
41
+ BLOCKLET_APP_PID: a,
42
+ BLOCKLET_APP_IDS: a,
43
+ BLOCKLET_APP_NAME: 'payment-kit',
44
+ BLOCKLET_APP_DESCRIPTION: 'payment-kit',
45
+ BLOCKLET_APP_URL: 'http://127.0.0.1:3030',
46
+ BLOCKLET_DATA_DIR: dir,
47
+ BLOCKLET_LOG_DIR: dir,
48
+ BLOCKLET_APP_DIR: dir,
49
+ BLOCKLET_MOUNT_POINTS: '[]',
50
+ });
51
+ return dir;
52
+ }
53
+
54
+ type Column = { name: string; type: string; notnull: number; pk: number };
55
+ type TableSchema = { columns: Column[]; indexes: string[] };
56
+ type Schema = Record<string, TableSchema>;
57
+
58
+ const META_TABLES = new Set(['SequelizeMeta', 'd1_migrations', 'sqlite_sequence']);
59
+ // D1-only infrastructure tables: the standalone worker owns its locks + DID-Connect
60
+ // token storage as real tables; the blocklet server gets those from the blocklet
61
+ // runtime, so they never appear in the Umzug canonical. Not business-schema drift.
62
+ const D1_INFRA_TABLES = new Set(['_locks', '_did_connect_tokens']);
63
+ // `*_tmp` tables are transient SQLite rebuild scaffolding (ALTER-via-recreate);
64
+ // if one survives in a final schema it's a migration leftover, not real schema.
65
+ const isArtifact = (n: string) => n.endsWith('_tmp');
66
+ const isUserTable = (n: string) =>
67
+ !n.startsWith('sqlite_') && !META_TABLES.has(n) && !D1_INFRA_TABLES.has(n) && !isArtifact(n);
68
+
69
+ // Normalize index columns into a stable signature so the two engines compare
70
+ // equal regardless of index NAME (the SQL lineage and Umzug may name them
71
+ // differently); we compare the set of indexed-column-tuples per table.
72
+ function indexSignature(cols: string[]): string {
73
+ return cols.join(',');
74
+ }
75
+
76
+ function dump(query: (sql: string) => any[]): Schema {
77
+ const tables = query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
78
+ .map((r: any) => r.name)
79
+ .filter(isUserTable);
80
+ const schema: Schema = {};
81
+ for (const t of tables) {
82
+ const columns: Column[] = query(`PRAGMA table_info("${t}")`)
83
+ .map((c: any) => ({ name: c.name, type: String(c.type || '').toUpperCase(), notnull: c.notnull, pk: c.pk }))
84
+ .sort((a: Column, b: Column) => a.name.localeCompare(b.name));
85
+ const idxList = query(`PRAGMA index_list("${t}")`);
86
+ const indexes: string[] = [];
87
+ for (const idx of idxList) {
88
+ const info = query(`PRAGMA index_info("${idx.name}")`);
89
+ indexes.push(indexSignature(info.map((i: any) => i.name)));
90
+ }
91
+ schema[t] = { columns, indexes: [...new Set(indexes)].sort() };
92
+ }
93
+ return schema;
94
+ }
95
+
96
+ async function canonicalSchema(): Promise<Schema> {
97
+ // run the 75 Umzug TS migrations against a throwaway temp SQLite
98
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'drift-canon-'));
99
+ process.env.BLOCKLET_DATA_DIR = tmp;
100
+ process.env.BLOCKLET_LOG_DIR = tmp;
101
+ // eslint-disable-next-line global-require, import/no-dynamic-require
102
+ const { sequelize } = require('../api/src/store/sequelize');
103
+ // eslint-disable-next-line global-require
104
+ const migrate = require('../api/src/store/migrate').default;
105
+ await migrate();
106
+ // dump via a direct sync handle to the same file the sequelize migrations wrote
107
+ const dbFile = path.join(tmp, 'payment-kit.db');
108
+ await sequelize.close();
109
+ const db = new DatabaseSync(dbFile);
110
+ const schema = dump((sql) => db.prepare(sql).all() as any[]);
111
+ db.close();
112
+ fs.rmSync(tmp, { recursive: true, force: true });
113
+ return schema;
114
+ }
115
+
116
+ function d1Schema(): Schema {
117
+ const db = new DatabaseSync(':memory:');
118
+ const dir = path.resolve(__dirname, '../cloudflare/migrations');
119
+ const files = fs
120
+ .readdirSync(dir)
121
+ .filter((f) => f.endsWith('.sql'))
122
+ .sort();
123
+ for (const f of files) {
124
+ const sql = fs.readFileSync(path.join(dir, f), 'utf8');
125
+ db.exec(sql); // node:sqlite exec runs a multi-statement script
126
+ }
127
+ const schema = dump((sql) => db.prepare(sql).all() as any[]);
128
+ db.close();
129
+ return schema;
130
+ }
131
+
132
+ function diff(canon: Schema, d1: Schema) {
133
+ const drift: { tables: any; columns: any; indexes: any } = { tables: {}, columns: {}, indexes: {} };
134
+ const canonTables = new Set(Object.keys(canon));
135
+ const d1Tables = new Set(Object.keys(d1));
136
+ const missingInD1 = [...canonTables].filter((t) => !d1Tables.has(t));
137
+ const extraInD1 = [...d1Tables].filter((t) => !canonTables.has(t));
138
+ if (missingInD1.length) drift.tables.missing_in_d1 = missingInD1;
139
+ if (extraInD1.length) drift.tables.extra_in_d1 = extraInD1;
140
+
141
+ for (const t of [...canonTables].filter((x) => d1Tables.has(x))) {
142
+ const cCols = new Map(canon[t]!.columns.map((c) => [c.name, c]));
143
+ const dCols = new Map(d1[t]!.columns.map((c) => [c.name, c]));
144
+ const missingCols = [...cCols.keys()].filter((c) => !dCols.has(c));
145
+ const extraCols = [...dCols.keys()].filter((c) => !cCols.has(c));
146
+ if (missingCols.length || extraCols.length) {
147
+ drift.columns[t] = {
148
+ ...(missingCols.length ? { missing_in_d1: missingCols } : {}),
149
+ ...(extraCols.length ? { extra_in_d1: extraCols } : {}),
150
+ };
151
+ }
152
+ // index drift (warn-only): compare the set of indexed-column signatures
153
+ const cIdx = new Set(canon[t]!.indexes);
154
+ const dIdx = new Set(d1[t]!.indexes);
155
+ const missingIdx = [...cIdx].filter((i) => !dIdx.has(i) && i !== '');
156
+ if (missingIdx.length) drift.indexes[t] = { missing_in_d1: missingIdx };
157
+ }
158
+ return drift;
159
+ }
160
+
161
+ async function main() {
162
+ setupTestEnv();
163
+ const canon = await canonicalSchema();
164
+ const d1 = d1Schema();
165
+ console.log(JSON.stringify({ canonical_tables: Object.keys(canon).length, d1_tables: Object.keys(d1).length }));
166
+ const drift = diff(canon, d1);
167
+
168
+ // HARD GATE = anything the canonical has that D1 is MISSING (the worker would
169
+ // fail to write it). Extra-in-D1 (rebuild leftovers) and index diffs are
170
+ // WARN-only: inert for model reads/writes, and dropping columns on a deployed
171
+ // D1 is destructive — cleaned up separately if ever needed.
172
+ const missingTables = drift.tables.missing_in_d1 || [];
173
+ const missingCols = Object.entries(drift.columns).filter(([, v]: any) => v.missing_in_d1);
174
+ const extraTables = drift.tables.extra_in_d1 || [];
175
+ const extraCols = Object.entries(drift.columns).filter(([, v]: any) => v.extra_in_d1);
176
+
177
+ const hardFail = missingTables.length > 0 || missingCols.length > 0;
178
+
179
+ if (missingTables.length) console.log(JSON.stringify({ HARD_missing_tables_in_d1: missingTables }, null, 2));
180
+ if (missingCols.length)
181
+ console.log(
182
+ JSON.stringify(
183
+ { HARD_missing_columns_in_d1: Object.fromEntries(missingCols.map(([t, v]: any) => [t, v.missing_in_d1])) },
184
+ null,
185
+ 2
186
+ )
187
+ );
188
+ if (extraTables.length) console.log(JSON.stringify({ WARN_extra_tables_in_d1: extraTables }));
189
+ if (extraCols.length)
190
+ console.log(
191
+ JSON.stringify({
192
+ WARN_extra_columns_in_d1: Object.fromEntries(extraCols.map(([t, v]: any) => [t, v.extra_in_d1])),
193
+ })
194
+ );
195
+ if (Object.keys(drift.indexes).length) console.log(JSON.stringify({ WARN_missing_indexes_in_d1: drift.indexes }));
196
+
197
+ console.log(
198
+ JSON.stringify({
199
+ success: !hardFail,
200
+ hard_gate: 'no canonical table/column missing in D1',
201
+ warns: { extra_in_d1: 'inert leftovers', indexes: 'perf-only' },
202
+ })
203
+ );
204
+ if (hardFail) process.exit(1);
205
+ }
206
+
207
+ main().catch((err) => {
208
+ console.error(JSON.stringify({ success: false, error: String(err?.stack || err) }));
209
+ process.exit(1);
210
+ });
@@ -0,0 +1 @@
1
+ []
package/src/app.tsx CHANGED
@@ -13,6 +13,7 @@ import { Navigate, Route, BrowserRouter as Router, Routes, useNavigate } from 'r
13
13
  import { joinURL } from 'ufo';
14
14
 
15
15
  import ErrorFallback from './components/error-fallback';
16
+ import { resolveServiceHost } from './libs/service-host';
16
17
  import UserLayoutDefault from './components/layout/user';
17
18
  import UserLayoutCF from './components/layout/user-cf';
18
19
  import { TransitionProvider } from './components/progress-bar';
@@ -250,7 +251,7 @@ export default function WrappedApp() {
250
251
  <ToastProvider>
251
252
  <PaymentThemeProvider>
252
253
  <SessionProvider
253
- serviceHost={prefix}
254
+ serviceHost={resolveServiceHost(prefix)}
254
255
  useSocket={!(window as any).blocklet?.cloudflareWorker}
255
256
  protectedRoutes={['/admin/*', '/customer/*', '/integrations/*'].map((item) => joinURL(prefix, item))}>
256
257
  <Router basename={prefix}>
package/src/env.d.ts CHANGED
@@ -1,10 +1,22 @@
1
1
  declare var blocklet: {
2
+ appId: string;
3
+ appPk: string;
2
4
  appName: string;
3
5
  appLogo: string;
4
6
  appUrl: string;
5
7
  prefix: string;
6
8
  languages: { code: string; name: string }[];
7
- preferences: { overdraftProtectionMaxCount: number };
9
+ preferences: { overdraftProtectionMaxCount: number; [key: string]: any };
10
+ componentMountPoints: Array<{
11
+ appId?: string;
12
+ did?: string;
13
+ appName?: string;
14
+ appLogo?: string;
15
+ appUrl?: string;
16
+ [key: string]: any;
17
+ }>;
18
+ PAYMENT_CHANGE_LOCKED_PRICE?: string;
19
+ [key: string]: any;
8
20
  };
9
21
 
10
22
  declare module '*.svg';
@@ -0,0 +1,13 @@
1
+ // P5 T5.1 (README D4 / F5) — resolve the SessionProvider serviceHost.
2
+ //
3
+ // arc injects window.blocklet.serviceHost (the arc /.well-known/service endpoint)
4
+ // at build time via the P2 bootstrap. The blocklet-server form does NOT inject it,
5
+ // so we fall back to the app prefix — TM-8 regression: zero behavior change for the
6
+ // blocklet-server deploy (serviceHost stays = prefix exactly as before).
7
+ //
8
+ // Zero imports on purpose so the rule is unit-testable in a node jest env without
9
+ // pulling the frontend React graph.
10
+ export function resolveServiceHost(prefix: string): string {
11
+ const injected = (typeof window !== 'undefined' && (window as any)?.blocklet?.serviceHost) || '';
12
+ return injected || prefix;
13
+ }
package/tsconfig.json CHANGED
@@ -33,7 +33,7 @@
33
33
  // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
34
34
  // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
35
35
  // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
36
- "types": ["node", "react", "react-dom", "express", "cookie-parser", "cors", "debug", "dotenv-flow", "jest"], /* Only include types explicitly used by this package */
36
+ "types": ["node", "react", "react-dom", "debug", "dotenv-flow", "jest"], /* Only include types explicitly used by this package */
37
37
  // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
38
38
  // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
39
39
  // "resolveJsonModule": true, /* Enable importing .json files. */
@@ -0,0 +1,159 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ // P1 (README D1 / F1) — arc-targeted SPA build.
3
+ //
4
+ // The standalone `vite.config.ts` injects its base via createBlockletPlugin
5
+ // (vite.config.ts:56), whose base is CLI-immutable — so a `--base` override
6
+ // cannot retarget it. arc needs a DETERMINISTIC base of '/.well-known/payment/',
7
+ // so this config (like cloudflare/vite.config.ts) drops createBlockletPlugin and
8
+ // sets `base` + `outDir` explicitly. Output lands directly in
9
+ // packages/payment-core/web/ so the @arcblock/payment-service tarball ships the
10
+ // frontend (T1.3 appends web/** to files). There is no blocklet-server doing
11
+ // runtime window.blocklet injection here, so the P2 bootstrap helper injects it
12
+ // at build time (single source — same helper the CF build uses).
13
+ import path from 'path';
14
+
15
+ import react from '@vitejs/plugin-react';
16
+ import { defineConfig } from 'vite';
17
+ import svgr from 'vite-plugin-svgr';
18
+ import tsconfigPaths from 'vite-tsconfig-paths';
19
+
20
+ import { PAYMENT_KIT_DID, buildBootstrapScript } from './scripts/bootstrap-inject';
21
+
22
+ const coreDir = __dirname; // blocklets/core — where index.html and src/ live
23
+ const UI_PREFIX = '/.well-known/payment';
24
+ // Physical isolation (README D1 / data-leak): arc artifacts live in payment-core's
25
+ // web/, never mixed with the standalone did-pay worker output (cloudflare/public).
26
+ const webOutDir = path.resolve(coreDir, '../../packages/payment-core/web');
27
+
28
+ export default defineConfig({
29
+ root: coreDir,
30
+ base: `${UI_PREFIX}/`,
31
+ plugins: [
32
+ tsconfigPaths({ root: coreDir }),
33
+ react(),
34
+ // Build-time window.blocklet bootstrap (P2 helper, single source). The
35
+ // remoteBlockletUrl is root-exact (the helper throws otherwise, T2.3), prefix
36
+ // is local-only (G3), componentId = PAYMENT_KIT_DID so getPrefix resolves to
37
+ // the arc UI prefix (packages/react/src/libs/util.ts:87).
38
+ {
39
+ name: 'arc-inject-blocklet',
40
+ transformIndexHtml(html: string) {
41
+ const injection = buildBootstrapScript({
42
+ uiPrefix: UI_PREFIX,
43
+ componentId: PAYMENT_KIT_DID,
44
+ remoteBlockletUrl: '/__blocklet__.js?type=json',
45
+ // serviceHost = ORIGIN ROOT, not '/.well-known/service'. The DID-Connect
46
+ // SessionProvider treats serviceHost as the API base and appends its OWN
47
+ // auth-service prefix (groupPrefix + servicePrefix + '/api/did' =
48
+ // '/.well-known/service/api/did') — so the session/csrf URLs resolve to
49
+ // <origin>/.well-known/service/api/did/*. Passing '/.well-known/service'
50
+ // double-counts it (=> /.well-known/service/.well-known/service/api/did/session).
51
+ // Root '/' lets the lib's appended prefix land at origin root, where arc
52
+ // serves the auth service (a sibling of /.well-known/payment, not under it).
53
+ serviceHost: '/',
54
+ // protect serviceHost too: the remote __blocklet__.js carries arc's own
55
+ // serviceHost/servicePrefix — must not clobber this build-time root base.
56
+ localOnly: ['prefix', 'serviceHost', 'navigation', 'componentMountPoints'],
57
+ // the payment blocklet's own nav + mount point — AUTH_SERVICE's
58
+ // __blocklet__.js doesn't carry them, so @blocklet/ui-react's dashboard
59
+ // would crash on `navigation.forEach` without these (localOnly-protected).
60
+ extra: {
61
+ componentMountPoints: [
62
+ {
63
+ did: PAYMENT_KIT_DID,
64
+ name: 'Payment Kit',
65
+ mountPoint: UI_PREFIX,
66
+ appId: PAYMENT_KIT_DID,
67
+ status: 'running',
68
+ capabilities: { component: true },
69
+ },
70
+ ],
71
+ navigation: [
72
+ {
73
+ id: 'payments',
74
+ title: { en: 'Payments', zh: '支付管理' },
75
+ icon: 'ion:card-outline',
76
+ link: '/admin',
77
+ section: ['dashboard', 'sessionManager'],
78
+ role: ['admin', 'owner'],
79
+ },
80
+ {
81
+ id: 'integrations',
82
+ title: { en: 'Integrations', zh: '快速集成' },
83
+ icon: 'ion:flash-outline',
84
+ link: '/integrations',
85
+ section: ['dashboard', 'sessionManager'],
86
+ role: ['admin', 'owner'],
87
+ },
88
+ {
89
+ id: 'billing',
90
+ title: { en: 'Billing', zh: '我的账单' },
91
+ icon: 'ion:receipt-outline',
92
+ link: '/customer',
93
+ private: true,
94
+ section: ['userCenter', 'sessionManager'],
95
+ role: ['owner', 'admin', 'member', 'guest'],
96
+ },
97
+ ],
98
+ },
99
+ });
100
+ return html.replace('<head>', `<head>${injection}`);
101
+ },
102
+ },
103
+ svgr(),
104
+ ],
105
+ resolve: {
106
+ alias: {
107
+ // Point workspace packages at source (same as the standalone dev/CF builds).
108
+ '@blocklet/payment-react': path.resolve(coreDir, '../../packages/react/src'),
109
+ '@blocklet/payment-react-headless': path.resolve(coreDir, '../../packages/payment-react-headless/src'),
110
+ '@blocklet/payment-js': path.resolve(coreDir, '../../packages/client/src'),
111
+ 'lodash.assign': 'lodash/assign',
112
+ 'lodash.clonedeep': 'lodash/cloneDeep',
113
+ 'lodash.isequal': 'lodash/isEqual',
114
+ 'lodash.merge': 'lodash/merge',
115
+ 'lodash.find': 'lodash/find',
116
+ },
117
+ dedupe: [
118
+ '@blocklet/ui-react',
119
+ '@arcblock/ux',
120
+ '@arcblock/did-connect-react',
121
+ '@mui/material',
122
+ '@mui/icons-material',
123
+ 'react',
124
+ 'react-dom',
125
+ 'lodash',
126
+ 'bn.js',
127
+ ],
128
+ },
129
+ build: {
130
+ outDir: webOutDir,
131
+ emptyOutDir: true,
132
+ cssCodeSplit: false,
133
+ modulePreload: false,
134
+ // Align with build.mjs sourcemap:false — the shipped web/ carries no source
135
+ // maps (no source-layout leak, Security spec).
136
+ sourcemap: false,
137
+ reportCompressedSize: false,
138
+ chunkSizeWarningLimit: 2000,
139
+ commonjsOptions: { include: [/node_modules/], transformMixedEsModules: true },
140
+ rollupOptions: {
141
+ output: {
142
+ manualChunks: {
143
+ 'vendor-react': ['react', 'react-dom', 'react-router-dom'],
144
+ 'vendor-mui': ['@mui/material', '@mui/icons-material', '@mui/system'],
145
+ 'vendor-arcblock': ['@arcblock/did-connect-react', '@arcblock/ux'],
146
+ 'vendor-blocklet': ['@blocklet/ui-react'],
147
+ },
148
+ },
149
+ },
150
+ },
151
+ define: {
152
+ global: 'globalThis',
153
+ 'process.env': {},
154
+ },
155
+ optimizeDeps: {
156
+ include: ['react', 'react-dom', 'react/jsx-runtime'],
157
+ esbuildOptions: { mainFields: ['module', 'main'], resolveExtensions: ['.ts', '.tsx', '.js', '.jsx'] },
158
+ },
159
+ });