payment-kit 1.29.1 → 1.29.2

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 (310) 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 +36 -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 +27 -24
  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/index.ts +22 -161
  12. package/api/src/integrations/app-store/client.ts +3 -4
  13. package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
  14. package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
  15. package/api/src/integrations/arcblock/token.ts +21 -7
  16. package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
  17. package/api/src/integrations/google-play/handlers/voided.ts +2 -2
  18. package/api/src/integrations/google-play/verify.ts +3 -2
  19. package/api/src/integrations/iap-reconcile.ts +3 -5
  20. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  21. package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
  22. package/api/src/libs/archive/query.ts +19 -0
  23. package/api/src/libs/audit.ts +61 -4
  24. package/api/src/libs/auth.ts +99 -38
  25. package/api/src/libs/context.ts +78 -1
  26. package/api/src/libs/currency.ts +2 -2
  27. package/api/src/libs/dayjs.ts +8 -2
  28. package/api/src/libs/drivers/auth-storage.ts +118 -0
  29. package/api/src/libs/drivers/cron.ts +264 -0
  30. package/api/src/libs/drivers/db.ts +170 -0
  31. package/api/src/libs/drivers/identity.ts +81 -0
  32. package/api/src/libs/drivers/index.ts +40 -0
  33. package/api/src/libs/drivers/locks.ts +226 -0
  34. package/api/src/libs/drivers/migrate-runner.ts +70 -0
  35. package/api/src/libs/drivers/queue.ts +104 -0
  36. package/api/src/libs/drivers/secrets.ts +194 -0
  37. package/api/src/libs/env.ts +170 -54
  38. package/api/src/libs/exchange-rate/service.ts +7 -6
  39. package/api/src/libs/http-fetch-adapter.ts +50 -0
  40. package/api/src/libs/invoice.ts +1 -1
  41. package/api/src/libs/lock.ts +51 -47
  42. package/api/src/libs/logger.ts +48 -8
  43. package/api/src/libs/notification/index.ts +1 -1
  44. package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
  45. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
  46. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
  47. package/api/src/libs/overdraft-protection.ts +1 -1
  48. package/api/src/libs/payout.ts +1 -1
  49. package/api/src/libs/queue/index.ts +259 -52
  50. package/api/src/libs/queue/runtime.ts +175 -0
  51. package/api/src/libs/resource.ts +3 -3
  52. package/api/src/libs/secrets.ts +38 -0
  53. package/api/src/libs/session.ts +3 -2
  54. package/api/src/libs/subscription.ts +5 -5
  55. package/api/src/libs/tenant.ts +92 -0
  56. package/api/src/libs/url.ts +3 -3
  57. package/api/src/libs/util.ts +21 -13
  58. package/api/src/middlewares/hono/cdn.ts +63 -0
  59. package/api/src/middlewares/hono/context.ts +73 -0
  60. package/api/src/middlewares/hono/csrf.ts +72 -0
  61. package/api/src/middlewares/hono/fallback.ts +194 -0
  62. package/api/src/middlewares/hono/pipeline.ts +73 -0
  63. package/api/src/middlewares/hono/resource-mount.ts +42 -0
  64. package/api/src/middlewares/hono/resource.ts +63 -0
  65. package/api/src/middlewares/hono/security.ts +214 -0
  66. package/api/src/middlewares/hono/session.ts +114 -0
  67. package/api/src/middlewares/hono/xss.ts +61 -0
  68. package/api/src/queues/auto-recharge.ts +12 -10
  69. package/api/src/queues/checkout-session.ts +17 -12
  70. package/api/src/queues/credit-consume.ts +40 -36
  71. package/api/src/queues/credit-grant.ts +25 -18
  72. package/api/src/queues/credit-reconciliation.ts +7 -5
  73. package/api/src/queues/discount-status.ts +9 -6
  74. package/api/src/queues/event.ts +12 -4
  75. package/api/src/queues/exchange-rate-health.ts +49 -30
  76. package/api/src/queues/invoice.ts +18 -15
  77. package/api/src/queues/notification.ts +14 -7
  78. package/api/src/queues/payment.ts +41 -28
  79. package/api/src/queues/payout.ts +9 -5
  80. package/api/src/queues/refund.ts +18 -12
  81. package/api/src/queues/subscription.ts +83 -53
  82. package/api/src/queues/token-transfer.ts +15 -10
  83. package/api/src/queues/usage-record.ts +8 -5
  84. package/api/src/queues/vendors/commission.ts +7 -5
  85. package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
  86. package/api/src/queues/vendors/fulfillment.ts +4 -2
  87. package/api/src/queues/vendors/return-processor.ts +5 -3
  88. package/api/src/queues/vendors/return-scanner.ts +5 -4
  89. package/api/src/queues/vendors/status-check.ts +10 -7
  90. package/api/src/queues/webhook.ts +60 -32
  91. package/api/src/routes/connect/shared.ts +1 -2
  92. package/api/src/routes/connect/subscribe.ts +3 -3
  93. package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
  94. package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
  95. package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
  96. package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
  97. package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
  98. package/api/src/routes/hono/credit-tokens.ts +43 -0
  99. package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
  100. package/api/src/routes/{customers.ts → hono/customers.ts} +193 -223
  101. package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
  102. package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
  103. package/api/src/routes/{events.ts → hono/events.ts} +107 -71
  104. package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
  105. package/api/src/routes/hono/exchange-rates.ts +77 -0
  106. package/api/src/routes/hono/index.ts +115 -0
  107. package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
  108. package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
  109. package/api/src/routes/hono/integrations/stripe.ts +74 -0
  110. package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
  111. package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
  112. package/api/src/routes/hono/meters.ts +288 -0
  113. package/api/src/routes/hono/passports.ts +73 -0
  114. package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
  115. package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
  116. package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
  117. package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
  118. package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
  119. package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
  120. package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
  121. package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
  122. package/api/src/routes/{products.ts → hono/products.ts} +172 -159
  123. package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
  124. package/api/src/routes/hono/redirect.ts +24 -0
  125. package/api/src/routes/{refunds.ts → hono/refunds.ts} +96 -80
  126. package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
  127. package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
  128. package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
  129. package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
  130. package/api/src/routes/hono/tool.ts +69 -0
  131. package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
  132. package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
  133. package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
  134. package/api/src/routes/hono/webhook-endpoints.ts +126 -0
  135. package/api/src/service.ts +667 -0
  136. package/api/src/store/migrations/20230911-seeding.ts +2 -1
  137. package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
  138. package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
  139. package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
  140. package/api/src/store/models/auto-recharge-config.ts +22 -10
  141. package/api/src/store/models/checkout-session.ts +15 -14
  142. package/api/src/store/models/coupon.ts +29 -20
  143. package/api/src/store/models/credit-grant.ts +38 -29
  144. package/api/src/store/models/credit-transaction.ts +32 -21
  145. package/api/src/store/models/customer.ts +19 -17
  146. package/api/src/store/models/discount.ts +11 -2
  147. package/api/src/store/models/entitlement-grant.ts +21 -9
  148. package/api/src/store/models/entitlement-product.ts +21 -9
  149. package/api/src/store/models/entitlement.ts +19 -10
  150. package/api/src/store/models/event.ts +18 -9
  151. package/api/src/store/models/exchange-rate-provider.ts +17 -4
  152. package/api/src/store/models/invoice-item.ts +18 -9
  153. package/api/src/store/models/invoice.ts +16 -8
  154. package/api/src/store/models/meter-event.ts +27 -9
  155. package/api/src/store/models/meter.ts +31 -22
  156. package/api/src/store/models/payment-currency.ts +25 -8
  157. package/api/src/store/models/payment-intent.ts +15 -6
  158. package/api/src/store/models/payment-link.ts +15 -6
  159. package/api/src/store/models/payment-method.ts +38 -22
  160. package/api/src/store/models/payment-stat.ts +18 -9
  161. package/api/src/store/models/payout.ts +15 -6
  162. package/api/src/store/models/price-quote.ts +17 -8
  163. package/api/src/store/models/price.ts +24 -12
  164. package/api/src/store/models/pricing-table.ts +29 -20
  165. package/api/src/store/models/product-vendor.ts +20 -10
  166. package/api/src/store/models/product.ts +15 -6
  167. package/api/src/store/models/promotion-code.ts +14 -6
  168. package/api/src/store/models/refund.ts +15 -6
  169. package/api/src/store/models/revenue-snapshot.ts +21 -9
  170. package/api/src/store/models/setting.ts +18 -9
  171. package/api/src/store/models/setup-intent.ts +36 -27
  172. package/api/src/store/models/subscription-item.ts +21 -9
  173. package/api/src/store/models/subscription-schedule.ts +21 -9
  174. package/api/src/store/models/subscription.ts +21 -10
  175. package/api/src/store/models/tax-rate.ts +29 -21
  176. package/api/src/store/models/usage-record.ts +11 -2
  177. package/api/src/store/models/webhook-attempt.ts +18 -9
  178. package/api/src/store/models/webhook-endpoint.ts +18 -9
  179. package/api/src/store/scoped-core.ts +55 -0
  180. package/api/src/store/scoped.ts +247 -0
  181. package/api/src/store/sequelize.ts +66 -22
  182. package/api/src/store/sql-migrations.ts +20 -0
  183. package/api/src/store/tenant-backfill.ts +260 -0
  184. package/api/src/store/tenant-model.ts +124 -0
  185. package/api/src/store/tenant-tables.ts +50 -0
  186. package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
  187. package/api/tests/fixtures/bare-query-violation.ts +13 -0
  188. package/api/tests/fixtures/core-env-violation.ts +10 -0
  189. package/api/tests/fixtures/host-read-violation.ts +19 -0
  190. package/api/tests/fixtures/tenants.ts +4 -0
  191. package/api/tests/integrations/iap-tenant.spec.ts +284 -0
  192. package/api/tests/libs/archive-query.spec.ts +26 -0
  193. package/api/tests/libs/audit-tenant.spec.ts +153 -0
  194. package/api/tests/libs/context.spec.ts +204 -0
  195. package/api/tests/libs/core-config.spec.ts +115 -0
  196. package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
  197. package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
  198. package/api/tests/libs/lock-tenant.spec.ts +66 -0
  199. package/api/tests/libs/scoped.spec.ts +222 -0
  200. package/api/tests/libs/secrets-facade.spec.ts +52 -0
  201. package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
  202. package/api/tests/libs/tenant-middleware.spec.ts +42 -0
  203. package/api/tests/libs/tenant-scanner.spec.ts +120 -0
  204. package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
  205. package/api/tests/middlewares/hono/context.spec.ts +113 -0
  206. package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
  207. package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
  208. package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
  209. package/api/tests/middlewares/hono/security.spec.ts +181 -0
  210. package/api/tests/middlewares/hono/session.spec.ts +42 -0
  211. package/api/tests/middlewares/hono/xss.spec.ts +81 -0
  212. package/api/tests/models/tenant-backfill.spec.ts +287 -0
  213. package/api/tests/models/tenant-columns-model.spec.ts +46 -0
  214. package/api/tests/models/tenant-columns.spec.ts +161 -0
  215. package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
  216. package/api/tests/queues/credit-consume.spec.ts +8 -1
  217. package/api/tests/queues/event-tenant.spec.ts +236 -0
  218. package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
  219. package/api/tests/queues/queue-parity.spec.ts +249 -0
  220. package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
  221. package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
  222. package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
  223. package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
  224. package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
  225. package/api/tests/service/collapse.spec.ts +96 -0
  226. package/api/tests/store/tenant-crosscut.spec.ts +202 -0
  227. package/api/tests/store/tenant-model-spike.spec.ts +177 -0
  228. package/api/tests/store/tenant-model.spec.ts +162 -0
  229. package/api/tests/store/tenant-residual.spec.ts +196 -0
  230. package/api/third.d.ts +4 -0
  231. package/blocklet.yml +1 -1
  232. package/cloudflare/README.md +26 -6
  233. package/cloudflare/build.ts +28 -13
  234. package/cloudflare/did-connect-auth.ts +0 -217
  235. package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
  236. package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
  237. package/cloudflare/migrations/0008_schema_parity.sql +16 -0
  238. package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
  239. package/cloudflare/queue-runtime-mode.ts +13 -0
  240. package/cloudflare/run-build.js +10 -56
  241. package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
  242. package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
  243. package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
  244. package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
  245. package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
  246. package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
  247. package/cloudflare/shims/blocklet-sdk/util-csrf.ts +13 -0
  248. package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
  249. package/cloudflare/shims/cron.ts +38 -158
  250. package/cloudflare/shims/events.ts +124 -0
  251. package/cloudflare/shims/fastq.ts +15 -1
  252. package/cloudflare/shims/nedb-storage.ts +16 -8
  253. package/cloudflare/shims/xss.ts +8 -0
  254. package/cloudflare/tenant-middleware.ts +36 -0
  255. package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
  256. package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
  257. package/cloudflare/worker.ts +204 -433
  258. package/cloudflare/wrangler.local-e2e.jsonc +26 -0
  259. package/jest.config.js +3 -1
  260. package/package.json +33 -38
  261. package/scripts/core-env-whitelist.json +1 -0
  262. package/scripts/e2e-12b-runtime.ts +149 -0
  263. package/scripts/e2e-core-config.ts +125 -0
  264. package/scripts/e2e-d1-tenancy.ts +116 -0
  265. package/scripts/e2e-d2-cron-queue.ts +139 -0
  266. package/scripts/e2e-d3-embedded-multi.ts +171 -0
  267. package/scripts/e2e-hono-s2.ts +125 -0
  268. package/scripts/e2e-hono-s3e.ts +135 -0
  269. package/scripts/e2e-hono-s4.ts +114 -0
  270. package/scripts/e2e-migration-contract.ts +100 -0
  271. package/scripts/e2e-s0.ts +61 -0
  272. package/scripts/e2e-s1.ts +107 -0
  273. package/scripts/e2e-s2.ts +178 -0
  274. package/scripts/e2e-s3.ts +110 -0
  275. package/scripts/e2e-s4.ts +191 -0
  276. package/scripts/e2e-s5.ts +139 -0
  277. package/scripts/e2e-s6.ts +127 -0
  278. package/scripts/e2e-tenant-model.ts +119 -0
  279. package/scripts/e2e-tenant-worker.ts +199 -0
  280. package/scripts/gen-sql-migrations.js +46 -0
  281. package/scripts/phase8-codemod.js +219 -0
  282. package/scripts/phase9a-env-getters-codemod.js +82 -0
  283. package/scripts/scan-core-env.js +109 -0
  284. package/scripts/scan-tenant-queries.js +235 -0
  285. package/scripts/schema-drift-guard.ts +210 -0
  286. package/scripts/tenant-scan-whitelist.json +1 -0
  287. package/src/env.d.ts +13 -1
  288. package/tsconfig.json +1 -1
  289. package/api/src/libs/did-space.ts +0 -235
  290. package/api/src/libs/middleware.ts +0 -50
  291. package/api/src/libs/security.ts +0 -192
  292. package/api/src/queues/space.ts +0 -662
  293. package/api/src/routes/credit-tokens.ts +0 -38
  294. package/api/src/routes/exchange-rates.ts +0 -87
  295. package/api/src/routes/index.ts +0 -142
  296. package/api/src/routes/integrations/stripe.ts +0 -61
  297. package/api/src/routes/meters.ts +0 -274
  298. package/api/src/routes/passports.ts +0 -68
  299. package/api/src/routes/redirect.ts +0 -20
  300. package/api/src/routes/tool.ts +0 -65
  301. package/api/src/routes/webhook-endpoints.ts +0 -126
  302. package/api/tests/routes/credit-grants.spec.ts +0 -1261
  303. package/cloudflare/shims/did-space-js.ts +0 -17
  304. package/cloudflare/shims/did-space.ts +0 -11
  305. package/cloudflare/shims/express-compat/index.ts +0 -80
  306. package/cloudflare/shims/express-compat/types.ts +0 -41
  307. package/cloudflare/shims/lock.ts +0 -115
  308. package/cloudflare/shims/queue.ts +0 -611
  309. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
  310. 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/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';
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. */