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.
- package/api/dev.ts +41 -2
- package/api/hono.d.ts +42 -0
- package/api/node-sqlite.d.ts +12 -0
- package/api/src/bootstrap.ts +47 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +41 -37
- package/api/src/crons/metering-subscription-detection.ts +1 -1
- package/api/src/crons/overdue-detection.ts +2 -2
- package/api/src/crons/retry-pending-events.ts +6 -0
- package/api/src/crons/tenant-fanout.ts +82 -0
- package/api/src/host-node/did-connect-runtime-node.ts +33 -0
- package/api/src/host-node/serve-static-arc.ts +68 -0
- package/api/src/host-node/serve-static.ts +41 -0
- package/api/src/index.ts +22 -161
- package/api/src/integrations/app-store/client.ts +3 -4
- package/api/src/integrations/app-store/handlers/subscription.ts +7 -7
- package/api/src/integrations/app-store/signed-data-verifier.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +21 -7
- package/api/src/integrations/google-play/handlers/subscription.ts +6 -6
- package/api/src/integrations/google-play/handlers/voided.ts +2 -2
- package/api/src/integrations/google-play/verify.ts +3 -2
- package/api/src/integrations/iap-reconcile.ts +3 -5
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/subscription.ts +3 -3
- package/api/src/libs/archive/query.ts +19 -0
- package/api/src/libs/audit.ts +61 -4
- package/api/src/libs/auth.ts +247 -47
- package/api/src/libs/context.ts +89 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- package/api/src/libs/did-connect/runtime-did-connect-js.ts +88 -0
- package/api/src/libs/did-connect/tenant-identity.ts +221 -0
- package/api/src/libs/drivers/auth-storage.ts +118 -0
- package/api/src/libs/drivers/cron.ts +264 -0
- package/api/src/libs/drivers/db.ts +170 -0
- package/api/src/libs/drivers/identity.ts +142 -0
- package/api/src/libs/drivers/index.ts +40 -0
- package/api/src/libs/drivers/locks.ts +226 -0
- package/api/src/libs/drivers/migrate-runner.ts +70 -0
- package/api/src/libs/drivers/queue.ts +104 -0
- package/api/src/libs/drivers/secrets.ts +194 -0
- package/api/src/libs/env.ts +170 -54
- package/api/src/libs/exchange-rate/service.ts +7 -6
- package/api/src/libs/http-fetch-adapter.ts +60 -0
- package/api/src/libs/invoice.ts +1 -1
- package/api/src/libs/lock.ts +51 -47
- package/api/src/libs/logger.ts +48 -8
- package/api/src/libs/notification/index.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +2 -1
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -1
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -1
- package/api/src/libs/overdraft-protection.ts +1 -1
- package/api/src/libs/payout.ts +1 -1
- package/api/src/libs/queue/index.ts +271 -52
- package/api/src/libs/queue/runtime.ts +175 -0
- package/api/src/libs/resource.ts +3 -3
- package/api/src/libs/secrets.ts +38 -0
- package/api/src/libs/session.ts +3 -2
- package/api/src/libs/subscription.ts +5 -5
- package/api/src/libs/tenant.ts +92 -0
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/util.ts +21 -13
- package/api/src/middlewares/hono/cdn.ts +63 -0
- package/api/src/middlewares/hono/context.ts +80 -0
- package/api/src/middlewares/hono/csrf.ts +83 -0
- package/api/src/middlewares/hono/fallback.ts +194 -0
- package/api/src/middlewares/hono/pipeline.ts +73 -0
- package/api/src/middlewares/hono/resource-mount.ts +42 -0
- package/api/src/middlewares/hono/resource.ts +63 -0
- package/api/src/middlewares/hono/security.ts +209 -0
- package/api/src/middlewares/hono/session.ts +114 -0
- package/api/src/middlewares/hono/xss.ts +61 -0
- package/api/src/queues/auto-recharge.ts +12 -10
- package/api/src/queues/checkout-session.ts +38 -21
- package/api/src/queues/credit-consume.ts +40 -36
- package/api/src/queues/credit-grant.ts +25 -18
- package/api/src/queues/credit-reconciliation.ts +7 -5
- package/api/src/queues/discount-status.ts +9 -6
- package/api/src/queues/event.ts +41 -11
- package/api/src/queues/exchange-rate-health.ts +49 -30
- package/api/src/queues/invoice.ts +18 -15
- package/api/src/queues/notification.ts +14 -7
- package/api/src/queues/payment.ts +64 -37
- package/api/src/queues/payout.ts +37 -21
- package/api/src/queues/refund.ts +36 -18
- package/api/src/queues/subscription.ts +83 -53
- package/api/src/queues/token-transfer.ts +15 -10
- package/api/src/queues/usage-record.ts +8 -5
- package/api/src/queues/vendors/commission.ts +7 -5
- package/api/src/queues/vendors/fulfillment-coordinator.ts +17 -13
- package/api/src/queues/vendors/fulfillment.ts +4 -2
- package/api/src/queues/vendors/return-processor.ts +5 -3
- package/api/src/queues/vendors/return-scanner.ts +5 -4
- package/api/src/queues/vendors/status-check.ts +10 -7
- package/api/src/queues/webhook.ts +60 -32
- package/api/src/routes/connect/shared.ts +1 -2
- package/api/src/routes/connect/subscribe.ts +3 -3
- package/api/src/routes/{archive.ts → hono/archive.ts} +69 -64
- package/api/src/routes/{auto-recharge-configs.ts → hono/auto-recharge-configs.ts} +39 -28
- package/api/src/routes/{checkout-sessions.ts → hono/checkout-sessions.ts} +790 -923
- package/api/src/routes/{coupons.ts → hono/coupons.ts} +93 -76
- package/api/src/routes/{credit-grants.ts → hono/credit-grants.ts} +140 -126
- package/api/src/routes/hono/credit-tokens.ts +43 -0
- package/api/src/routes/{credit-transactions.ts → hono/credit-transactions.ts} +37 -29
- package/api/src/routes/{customers.ts → hono/customers.ts} +199 -224
- package/api/src/routes/{donations.ts → hono/donations.ts} +41 -32
- package/api/src/routes/{entitlements.ts → hono/entitlements.ts} +28 -25
- package/api/src/routes/{events.ts → hono/events.ts} +107 -71
- package/api/src/routes/{exchange-rate-providers.ts → hono/exchange-rate-providers.ts} +138 -126
- package/api/src/routes/hono/exchange-rates.ts +77 -0
- package/api/src/routes/hono/index.ts +115 -0
- package/api/src/routes/{integrations → hono/integrations}/app-store.ts +68 -48
- package/api/src/routes/{integrations → hono/integrations}/google-play.ts +78 -58
- package/api/src/routes/hono/integrations/stripe.ts +74 -0
- package/api/src/routes/{invoices.ts → hono/invoices.ts} +253 -244
- package/api/src/routes/{meter-events.ts → hono/meter-events.ts} +120 -110
- package/api/src/routes/hono/meters.ts +288 -0
- package/api/src/routes/hono/passports.ts +73 -0
- package/api/src/routes/{payment-currencies.ts → hono/payment-currencies.ts} +219 -197
- package/api/src/routes/{payment-intents.ts → hono/payment-intents.ts} +136 -132
- package/api/src/routes/{payment-links.ts → hono/payment-links.ts} +145 -128
- package/api/src/routes/{payment-methods.ts → hono/payment-methods.ts} +125 -93
- package/api/src/routes/{payment-stats.ts → hono/payment-stats.ts} +30 -25
- package/api/src/routes/{payouts.ts → hono/payouts.ts} +55 -47
- package/api/src/routes/{prices.ts → hono/prices.ts} +265 -242
- package/api/src/routes/{pricing-table.ts → hono/pricing-table.ts} +94 -87
- package/api/src/routes/{products.ts → hono/products.ts} +172 -159
- package/api/src/routes/{promotion-codes.ts → hono/promotion-codes.ts} +207 -185
- package/api/src/routes/hono/redirect.ts +24 -0
- package/api/src/routes/{refunds.ts → hono/refunds.ts} +98 -83
- package/api/src/routes/{settings.ts → hono/settings.ts} +64 -55
- package/api/src/routes/{subscription-items.ts → hono/subscription-items.ts} +64 -57
- package/api/src/routes/{subscriptions.ts → hono/subscriptions.ts} +475 -528
- package/api/src/routes/{tax-rates.ts → hono/tax-rates.ts} +71 -70
- package/api/src/routes/hono/tool.ts +69 -0
- package/api/src/routes/{usage-records.ts → hono/usage-records.ts} +47 -42
- package/api/src/routes/{vendor.ts → hono/vendor.ts} +315 -167
- package/api/src/routes/{webhook-attempts.ts → hono/webhook-attempts.ts} +17 -13
- package/api/src/routes/hono/webhook-endpoints.ts +126 -0
- package/api/src/service.ts +814 -0
- package/api/src/store/migrations/20230911-seeding.ts +2 -1
- package/api/src/store/migrations/20260609-remove-did-space-jobs.ts +23 -0
- package/api/src/store/migrations/20260610-tenant-columns.ts +40 -0
- package/api/src/store/migrations/20260611-tenant-backfill.ts +33 -0
- package/api/src/store/models/auto-recharge-config.ts +22 -10
- package/api/src/store/models/checkout-session.ts +15 -14
- package/api/src/store/models/coupon.ts +29 -20
- package/api/src/store/models/credit-grant.ts +38 -29
- package/api/src/store/models/credit-transaction.ts +32 -21
- package/api/src/store/models/customer.ts +19 -17
- package/api/src/store/models/discount.ts +11 -2
- package/api/src/store/models/entitlement-grant.ts +21 -9
- package/api/src/store/models/entitlement-product.ts +21 -9
- package/api/src/store/models/entitlement.ts +19 -10
- package/api/src/store/models/event.ts +18 -9
- package/api/src/store/models/exchange-rate-provider.ts +17 -4
- package/api/src/store/models/invoice-item.ts +18 -9
- package/api/src/store/models/invoice.ts +16 -8
- package/api/src/store/models/meter-event.ts +27 -9
- package/api/src/store/models/meter.ts +31 -22
- package/api/src/store/models/payment-currency.ts +25 -8
- package/api/src/store/models/payment-intent.ts +15 -6
- package/api/src/store/models/payment-link.ts +15 -6
- package/api/src/store/models/payment-method.ts +38 -22
- package/api/src/store/models/payment-stat.ts +18 -9
- package/api/src/store/models/payout.ts +15 -6
- package/api/src/store/models/price-quote.ts +17 -8
- package/api/src/store/models/price.ts +24 -12
- package/api/src/store/models/pricing-table.ts +29 -20
- package/api/src/store/models/product-vendor.ts +20 -10
- package/api/src/store/models/product.ts +15 -6
- package/api/src/store/models/promotion-code.ts +14 -6
- package/api/src/store/models/refund.ts +15 -6
- package/api/src/store/models/revenue-snapshot.ts +21 -9
- package/api/src/store/models/setting.ts +18 -9
- package/api/src/store/models/setup-intent.ts +36 -27
- package/api/src/store/models/subscription-item.ts +21 -9
- package/api/src/store/models/subscription-schedule.ts +21 -9
- package/api/src/store/models/subscription.ts +21 -10
- package/api/src/store/models/tax-rate.ts +29 -21
- package/api/src/store/models/usage-record.ts +11 -2
- package/api/src/store/models/webhook-attempt.ts +18 -9
- package/api/src/store/models/webhook-endpoint.ts +18 -9
- package/api/src/store/scoped-core.ts +55 -0
- package/api/src/store/scoped.ts +247 -0
- package/api/src/store/sequelize.ts +82 -23
- package/api/src/store/sql-migrations.ts +20 -0
- package/api/src/store/tenant-backfill.ts +260 -0
- package/api/src/store/tenant-model.ts +124 -0
- package/api/src/store/tenant-tables.ts +50 -0
- package/api/tests/bootstrap/bootstrap.spec.ts +162 -0
- package/api/tests/crons/tenant-fanout.spec.ts +158 -0
- package/api/tests/embedded/embedded-multi-mode-d3.spec.ts +257 -0
- package/api/tests/fixtures/bare-query-violation.ts +13 -0
- package/api/tests/fixtures/core-env-violation.ts +10 -0
- package/api/tests/fixtures/host-read-violation.ts +19 -0
- package/api/tests/fixtures/tenants.ts +4 -0
- package/api/tests/integrations/iap-tenant.spec.ts +284 -0
- package/api/tests/libs/archive-query.spec.ts +26 -0
- package/api/tests/libs/audit-tenant.spec.ts +153 -0
- package/api/tests/libs/context.spec.ts +204 -0
- package/api/tests/libs/core-config.spec.ts +115 -0
- package/api/tests/libs/cron-driver-d2.spec.ts +237 -0
- package/api/tests/libs/crons-conservation-d2.spec.ts +52 -0
- package/api/tests/libs/did-connect-runtime-js.spec.ts +98 -0
- package/api/tests/libs/did-connect-tenant-identity.spec.ts +159 -0
- package/api/tests/libs/lock-tenant.spec.ts +66 -0
- package/api/tests/libs/scoped.spec.ts +222 -0
- package/api/tests/libs/secrets-facade.spec.ts +52 -0
- package/api/tests/libs/service-host.spec.ts +37 -0
- package/api/tests/libs/tenancy-slot-authority.spec.ts +209 -0
- package/api/tests/libs/tenant-middleware.spec.ts +42 -0
- package/api/tests/libs/tenant-scanner.spec.ts +120 -0
- package/api/tests/middlewares/hono/cdn.spec.ts +70 -0
- package/api/tests/middlewares/hono/context.spec.ts +113 -0
- package/api/tests/middlewares/hono/csrf.spec.ts +136 -0
- package/api/tests/middlewares/hono/fallback.spec.ts +67 -0
- package/api/tests/middlewares/hono/pipeline.spec.ts +47 -0
- package/api/tests/middlewares/hono/security.spec.ts +181 -0
- package/api/tests/middlewares/hono/session.spec.ts +42 -0
- package/api/tests/middlewares/hono/xss.spec.ts +81 -0
- package/api/tests/models/tenant-backfill.spec.ts +287 -0
- package/api/tests/models/tenant-columns-model.spec.ts +46 -0
- package/api/tests/models/tenant-columns.spec.ts +161 -0
- package/api/tests/queues/credit-consume-batch.spec.ts +8 -1
- package/api/tests/queues/credit-consume.spec.ts +8 -1
- package/api/tests/queues/event-tenant.spec.ts +292 -0
- package/api/tests/queues/exchange-rate-health-tenant-d6.spec.ts +62 -0
- package/api/tests/queues/queue-parity.spec.ts +249 -0
- package/api/tests/queues/queue-runtime-surface.spec.ts +277 -0
- package/api/tests/queues/queue-teardown-d2.spec.ts +127 -0
- package/api/tests/queues/tenant-matrix-a.spec.ts +245 -0
- package/api/tests/queues/tenant-matrix-b.spec.ts +168 -0
- package/api/tests/routes/connect/hono-attach.spec.ts +107 -0
- package/api/tests/service/collapse.spec.ts +96 -0
- package/api/tests/service/didconnect-storage-slot.spec.ts +60 -0
- package/api/tests/service/fail-closed-http.spec.ts +79 -0
- package/api/tests/service/static-arc-handler.spec.ts +101 -0
- package/api/tests/service/static-externalized.spec.ts +48 -0
- package/api/tests/store/tenant-crosscut.spec.ts +202 -0
- package/api/tests/store/tenant-model-spike.spec.ts +177 -0
- package/api/tests/store/tenant-model.spec.ts +162 -0
- package/api/tests/store/tenant-residual.spec.ts +196 -0
- package/api/third.d.ts +4 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-RUNBOOK.md +3 -8
- package/cloudflare/README.md +34 -27
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +3 -15
- package/cloudflare/build.ts +33 -13
- package/cloudflare/cf-adapter.ts +419 -0
- package/cloudflare/did-connect-runtime.ts +96 -0
- package/cloudflare/did-connect-token-storage.ts +151 -0
- package/cloudflare/esbuild-cf-config.cjs +407 -0
- package/cloudflare/migrations/0006_tenant_columns.sql +46 -0
- package/cloudflare/migrations/0007_tenant_backfill_indexes.sql +65 -0
- package/cloudflare/migrations/0008_schema_parity.sql +16 -0
- package/cloudflare/migrations/0009_remove_did_space_jobs.sql +5 -0
- package/cloudflare/queue-runtime-mode.ts +13 -0
- package/cloudflare/run-build.js +33 -403
- package/cloudflare/scripts/cf-package-import-probe.mjs +90 -0
- package/cloudflare/scripts/didconnect-mock-smoke.mjs +140 -0
- package/cloudflare/shims/blocklet-sdk/asset-host-transformer.ts +20 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -1
- package/cloudflare/shims/blocklet-sdk/login.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/service-api.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +4 -2
- package/cloudflare/shims/blocklet-sdk/util-constants.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +16 -1
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +18 -3
- package/cloudflare/shims/cron.ts +38 -158
- package/cloudflare/shims/events.ts +124 -0
- package/cloudflare/shims/fastq.ts +15 -1
- package/cloudflare/shims/nedb-storage.ts +16 -8
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/cf-adapter.spec.ts +244 -0
- package/cloudflare/tests/did-connect-token-storage.spec.ts +105 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +69 -0
- package/cloudflare/vite.config.ts +53 -45
- package/cloudflare/worker.ts +261 -448
- package/cloudflare/wrangler.json +0 -6
- package/cloudflare/wrangler.jsonc +0 -6
- package/cloudflare/wrangler.local-e2e.jsonc +25 -0
- package/cloudflare/wrangler.staging.json +0 -6
- package/jest.config.js +3 -1
- package/package.json +33 -38
- package/scripts/bootstrap-inject.ts +166 -0
- package/scripts/core-env-whitelist.json +1 -0
- package/scripts/e2e-12b-runtime.ts +149 -0
- package/scripts/e2e-core-config.ts +125 -0
- package/scripts/e2e-d1-tenancy.ts +116 -0
- package/scripts/e2e-d2-cron-queue.ts +139 -0
- package/scripts/e2e-d3-embedded-multi.ts +171 -0
- package/scripts/e2e-hono-s2.ts +125 -0
- package/scripts/e2e-hono-s3e.ts +135 -0
- package/scripts/e2e-hono-s4.ts +114 -0
- package/scripts/e2e-migration-contract.ts +100 -0
- package/scripts/e2e-s0.ts +61 -0
- package/scripts/e2e-s1.ts +107 -0
- package/scripts/e2e-s2.ts +178 -0
- package/scripts/e2e-s3.ts +110 -0
- package/scripts/e2e-s4.ts +191 -0
- package/scripts/e2e-s5.ts +139 -0
- package/scripts/e2e-s6.ts +127 -0
- package/scripts/e2e-tenant-model.ts +119 -0
- package/scripts/e2e-tenant-worker.ts +199 -0
- package/scripts/gen-sql-migrations.js +46 -0
- package/scripts/phase8-codemod.js +219 -0
- package/scripts/phase9a-env-getters-codemod.js +82 -0
- package/scripts/scan-core-env.js +109 -0
- package/scripts/scan-tenant-queries.js +235 -0
- package/scripts/schema-drift-guard.ts +210 -0
- package/scripts/tenant-scan-whitelist.json +1 -0
- package/src/app.tsx +2 -1
- package/src/env.d.ts +13 -1
- package/src/libs/service-host.ts +13 -0
- package/tsconfig.json +1 -1
- package/vite.arc.config.ts +159 -0
- package/api/src/libs/did-space.ts +0 -235
- package/api/src/libs/middleware.ts +0 -50
- package/api/src/libs/security.ts +0 -192
- package/api/src/queues/space.ts +0 -662
- package/api/src/routes/credit-tokens.ts +0 -38
- package/api/src/routes/exchange-rates.ts +0 -87
- package/api/src/routes/index.ts +0 -142
- package/api/src/routes/integrations/stripe.ts +0 -61
- package/api/src/routes/meters.ts +0 -274
- package/api/src/routes/passports.ts +0 -68
- package/api/src/routes/redirect.ts +0 -20
- package/api/src/routes/tool.ts +0 -65
- package/api/src/routes/webhook-endpoints.ts +0 -126
- package/api/tests/routes/credit-grants.spec.ts +0 -1261
- package/cloudflare/did-connect-auth.ts +0 -527
- package/cloudflare/shims/did-space-js.ts +0 -17
- package/cloudflare/shims/did-space.ts +0 -11
- package/cloudflare/shims/express-compat/index.ts +0 -80
- package/cloudflare/shims/express-compat/types.ts +0 -41
- package/cloudflare/shims/lock.ts +0 -115
- package/cloudflare/shims/queue.ts +0 -611
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +0 -87
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +0 -186
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Phase 8 (W2-1a): locks slot driver contract.
|
|
2
|
+
//
|
|
3
|
+
// W1 §2.2 exempts the `locks` table from an instance_did column: tenant
|
|
4
|
+
// isolation for locks is achieved by prefixing the lock NAME, not by a column.
|
|
5
|
+
// `scopedLockName` is the single place that prefix is applied, so both drivers
|
|
6
|
+
// (and the libs/lock facade) stay consistent.
|
|
7
|
+
//
|
|
8
|
+
// Two implementations conform to the contract:
|
|
9
|
+
// - memory driver: the original in-process EventEmitter lock (Blocklet Server
|
|
10
|
+
// single-process Node.js). Semantics unchanged — Phase 8 wraps, never
|
|
11
|
+
// rewrites.
|
|
12
|
+
// - d1 driver: the Cloudflare D1-backed lock (atomic INSERT OR IGNORE + TTL
|
|
13
|
+
// expiry + owner token), relocated here from the previously-orphaned
|
|
14
|
+
// cloudflare/shims/lock.ts so the worker wires it through the locks slot.
|
|
15
|
+
|
|
16
|
+
// the two lock classes are the two driver implementations — cohesive in one module
|
|
17
|
+
/* eslint-disable max-classes-per-file */
|
|
18
|
+
|
|
19
|
+
import type { D1Binding } from './db';
|
|
20
|
+
|
|
21
|
+
export interface LockHandle {
|
|
22
|
+
name: string;
|
|
23
|
+
/** whether this handle currently holds the lock — used by singleton start guards */
|
|
24
|
+
locked: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Acquire the lock. `maxWaitMs` is a bounded-wait hint: the d1 driver enforces
|
|
27
|
+
* it as a hard timeout (and rejects on expiry) because a holder lives in a
|
|
28
|
+
* different isolate and may crash; the in-process memory driver waits until
|
|
29
|
+
* release and does not time out — single process means there is no
|
|
30
|
+
* crashed-holder / cross-isolate-wait scenario. The shared parity cases
|
|
31
|
+
* (acquire-when-free, block-until-release) hold on both; TTL-expiry and
|
|
32
|
+
* timeout are d1-only capabilities (see drivers/locks.spec.ts).
|
|
33
|
+
*/
|
|
34
|
+
acquire(maxWaitMs?: number): Promise<true>;
|
|
35
|
+
release(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LocksDriverKind = 'memory' | 'd1';
|
|
39
|
+
|
|
40
|
+
export interface LocksDriver {
|
|
41
|
+
kind: LocksDriverKind;
|
|
42
|
+
getLock(name: string, options?: { ttl?: number }): LockHandle;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type LockScope = 'tenant' | 'global';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Apply the tenant prefix to a lock name. Tenant-scoped locks (the default for
|
|
49
|
+
* per-resource locks) are isolated per deployment/tenant; global-scoped locks
|
|
50
|
+
* (process-level singleton guards like queue start guards) are intentionally
|
|
51
|
+
* shared and keep their bare name. In single-tenant mode the prefix is the
|
|
52
|
+
* constant deployment app DID, so lock identity is unchanged in practice.
|
|
53
|
+
*/
|
|
54
|
+
export function scopedLockName(name: string, instanceDid: string | null, scope: LockScope): string {
|
|
55
|
+
if (scope === 'global') return name;
|
|
56
|
+
if (!instanceDid) {
|
|
57
|
+
// tenant scope requires a tenant — callers resolve it fail-closed before
|
|
58
|
+
// reaching here (see libs/lock.ts). Guard anyway so a programming error is
|
|
59
|
+
// loud rather than silently producing a cross-tenant-shared lock.
|
|
60
|
+
throw new Error('scopedLockName: tenant-scoped lock requires a non-empty instanceDid');
|
|
61
|
+
}
|
|
62
|
+
return `tenant:${instanceDid}::${name}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// memory driver — original in-process lock semantics
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const { EventEmitter } = require('events');
|
|
70
|
+
|
|
71
|
+
export class MemoryLock implements LockHandle {
|
|
72
|
+
name: string;
|
|
73
|
+
locked: boolean;
|
|
74
|
+
private events: any;
|
|
75
|
+
|
|
76
|
+
constructor(name: string) {
|
|
77
|
+
this.name = name;
|
|
78
|
+
this.locked = false;
|
|
79
|
+
this.events = new EventEmitter();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
acquire(): Promise<true> {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
if (this.locked) {
|
|
85
|
+
const tryAcquire = () => {
|
|
86
|
+
if (!this.locked) {
|
|
87
|
+
this.locked = true;
|
|
88
|
+
this.events.removeListener('release', tryAcquire);
|
|
89
|
+
resolve(true);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
this.events.on('release', tryAcquire);
|
|
93
|
+
} else {
|
|
94
|
+
this.locked = true;
|
|
95
|
+
resolve(true);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
release(): void {
|
|
101
|
+
this.locked = false;
|
|
102
|
+
setImmediate(() => this.events.emit('release'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createMemoryLocksDriver(): LocksDriver {
|
|
107
|
+
const locks = new Map<string, MemoryLock>();
|
|
108
|
+
return {
|
|
109
|
+
kind: 'memory',
|
|
110
|
+
getLock(name: string): LockHandle {
|
|
111
|
+
const exist = locks.get(name);
|
|
112
|
+
if (exist instanceof MemoryLock) return exist;
|
|
113
|
+
const lock = new MemoryLock(name);
|
|
114
|
+
locks.set(name, lock);
|
|
115
|
+
return lock;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// d1 driver — D1-backed cross-isolate lock (relocated from shims/lock.ts)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
let nanoidCounter = 0;
|
|
125
|
+
function simpleId(): string {
|
|
126
|
+
nanoidCounter += 1;
|
|
127
|
+
return `${Date.now().toString(36)}-${nanoidCounter.toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class D1Lock implements LockHandle {
|
|
131
|
+
name: string;
|
|
132
|
+
owner: string;
|
|
133
|
+
locked: boolean;
|
|
134
|
+
ttl: number;
|
|
135
|
+
private getBinding: () => D1Binding;
|
|
136
|
+
|
|
137
|
+
constructor(getBinding: () => D1Binding, name: string, options?: { ttl?: number }) {
|
|
138
|
+
this.getBinding = getBinding;
|
|
139
|
+
this.name = name;
|
|
140
|
+
this.owner = simpleId();
|
|
141
|
+
this.locked = false;
|
|
142
|
+
this.ttl = options?.ttl || 5000;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async acquire(maxWaitMs = 10000): Promise<true> {
|
|
146
|
+
const db: any = this.getBinding();
|
|
147
|
+
const deadline = Date.now() + maxWaitMs;
|
|
148
|
+
let delay = 30;
|
|
149
|
+
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
const now = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
// eslint-disable-next-line no-await-in-loop -- polling retry until acquired or timed out
|
|
154
|
+
const batchResult = await db.batch([
|
|
155
|
+
db.prepare('DELETE FROM _locks WHERE name = ? AND expires_at < ?').bind(this.name, now),
|
|
156
|
+
db
|
|
157
|
+
.prepare('INSERT OR IGNORE INTO _locks (name, owner, expires_at) VALUES (?, ?, ?)')
|
|
158
|
+
.bind(this.name, this.owner, now + this.ttl),
|
|
159
|
+
db.prepare('SELECT owner FROM _locks WHERE name = ?').bind(this.name),
|
|
160
|
+
]);
|
|
161
|
+
const row = batchResult[2]?.results?.[0] as { owner: string } | undefined;
|
|
162
|
+
if (row?.owner === this.owner) {
|
|
163
|
+
this.locked = true;
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
} catch (err: any) {
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
168
|
+
console.error(`[D1Lock] acquire error for "${this.name}":`, err?.message || err);
|
|
169
|
+
}
|
|
170
|
+
// backoff with jitter, capped — captured into a const so the closure does
|
|
171
|
+
// not reference the mutated `delay`
|
|
172
|
+
const waitMs = delay + Math.random() * delay * 0.3;
|
|
173
|
+
// eslint-disable-next-line no-await-in-loop, no-promise-executor-return
|
|
174
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
175
|
+
delay = Math.min(delay * 2, 500);
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`[D1Lock] Failed to acquire lock "${this.name}" within ${maxWaitMs}ms`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
release(): void {
|
|
181
|
+
if (!this.locked) return;
|
|
182
|
+
this.locked = false;
|
|
183
|
+
try {
|
|
184
|
+
const db: any = this.getBinding();
|
|
185
|
+
const promise = db
|
|
186
|
+
.prepare('DELETE FROM _locks WHERE name = ? AND owner = ?')
|
|
187
|
+
.bind(this.name, this.owner)
|
|
188
|
+
.run()
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
.catch((err: any) => console.error(`[D1Lock] release error for "${this.name}":`, err?.message || err));
|
|
191
|
+
|
|
192
|
+
// release is fire-and-forget; the worker shell flushes the pending delete
|
|
193
|
+
// before responding. This depends on ambient worker globals
|
|
194
|
+
// (__cfHttpContext__ / __cfWaitUntil__ / __cfPendingJobs__) set by the CF
|
|
195
|
+
// request handler — a host injecting the d1 locks driver must provide the
|
|
196
|
+
// same flush hooks (Phase 9 formalizes the flush contract). Outside a
|
|
197
|
+
// worker (e.g. the consistency suite) the delete still runs; it is simply
|
|
198
|
+
// not registered for flushing.
|
|
199
|
+
const isHttp = (globalThis as any).__cfHttpContext__;
|
|
200
|
+
if (isHttp) {
|
|
201
|
+
const waitUntil = (globalThis as any).__cfWaitUntil__;
|
|
202
|
+
if (typeof waitUntil === 'function') waitUntil(promise);
|
|
203
|
+
} else {
|
|
204
|
+
const pending = (globalThis as any).__cfPendingJobs__;
|
|
205
|
+
if (Array.isArray(pending)) pending.push(promise);
|
|
206
|
+
}
|
|
207
|
+
} catch (err: any) {
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.error(`[D1Lock] release setup error for "${this.name}":`, err?.message || err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* D1-backed locks driver. `getBinding` is a getter (the binding is resolved
|
|
216
|
+
* lazily per request in the worker). Each call returns a fresh lock — isolates
|
|
217
|
+
* never share instances, matching the original shim's behavior.
|
|
218
|
+
*/
|
|
219
|
+
export function createD1LocksDriver(getBinding: () => D1Binding): LocksDriver {
|
|
220
|
+
return {
|
|
221
|
+
kind: 'd1',
|
|
222
|
+
getLock(name: string, options?: { ttl?: number }): LockHandle {
|
|
223
|
+
return new D1Lock(getBinding, name, options);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Phase 11 (W2′): runtime-neutral SQL migration runner — the migration-driver
|
|
2
|
+
// boundary. The library provisions the embedded D1 schema by applying the D1 SQL
|
|
3
|
+
// migrations through the host's db driver (its `exec`), NEVER by shelling out to
|
|
4
|
+
// the wrangler CLI. `wrangler d1 migrations apply` stays the CF-native local-dev
|
|
5
|
+
// / deploy provisioning mechanism for the SAME .sql files; this runner is the
|
|
6
|
+
// in-process path arc-node (and any non-CF host) uses via lifecycle/host wiring.
|
|
7
|
+
//
|
|
8
|
+
// Idempotent: applied migrations are tracked in `_sql_migrations`, so re-running
|
|
9
|
+
// is a no-op (the .sql contain non-IF-NOT-EXISTS DDL like ALTER ... ADD COLUMN).
|
|
10
|
+
//
|
|
11
|
+
// NOTE: this tracker (`_sql_migrations`) is independent of wrangler's own
|
|
12
|
+
// `d1_migrations` table. That is safe because the two provisioning paths target
|
|
13
|
+
// DIFFERENT databases — arc-node's embedded SQLite (this runner) vs the CF
|
|
14
|
+
// worker's bound D1 (wrangler) — and never share one. Do NOT run both paths
|
|
15
|
+
// against the SAME database: the runner is blind to `d1_migrations`, so it would
|
|
16
|
+
// re-apply everything and the non-idempotent ALTER ... ADD COLUMN would fail.
|
|
17
|
+
|
|
18
|
+
import type { DbDriver, DbBatchOp } from './db';
|
|
19
|
+
|
|
20
|
+
export interface SqlMigration {
|
|
21
|
+
/** stable id (the migration filename) — the idempotency key */
|
|
22
|
+
name: string;
|
|
23
|
+
/** the migration body; may contain multiple `;`-separated statements */
|
|
24
|
+
sql: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Strip line comments and split a migration body into individual statements. */
|
|
28
|
+
export function splitStatements(sql: string): string[] {
|
|
29
|
+
return sql
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter((line) => !line.trim().startsWith('--'))
|
|
32
|
+
.join('\n')
|
|
33
|
+
.split(';')
|
|
34
|
+
.map((s) => s.trim())
|
|
35
|
+
.filter((s) => s.length > 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Apply pending SQL migrations through the db driver, in order. Returns the
|
|
40
|
+
* names actually applied this run (empty on a fully-migrated db). No wrangler.
|
|
41
|
+
*
|
|
42
|
+
* Each migration is applied ATOMICALLY: all of its statements PLUS the tracker
|
|
43
|
+
* insert run in a single `driver.batch(...)` (the contract's all-or-nothing
|
|
44
|
+
* transactional primitive). So a mid-migration failure rolls the whole migration
|
|
45
|
+
* back AND leaves the tracker unwritten — a re-run retries that migration from a
|
|
46
|
+
* clean slate, never replaying half-applied (non-IF-NOT-EXISTS) DDL like
|
|
47
|
+
* `ALTER ... ADD COLUMN`. This satisfies "中断后重跑不丢已迁移状态(幂等 + 事务边界)".
|
|
48
|
+
*/
|
|
49
|
+
export async function applySqlMigrations(driver: DbDriver, migrations: SqlMigration[]): Promise<string[]> {
|
|
50
|
+
await driver.exec('CREATE TABLE IF NOT EXISTS _sql_migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL)');
|
|
51
|
+
const rows = await driver.all<{ name: string }>('SELECT name FROM _sql_migrations');
|
|
52
|
+
const applied = new Set(rows.map((r) => r.name));
|
|
53
|
+
|
|
54
|
+
const ran: string[] = [];
|
|
55
|
+
const pending = migrations.filter((m) => !applied.has(m.name));
|
|
56
|
+
/* eslint-disable no-await-in-loop -- migrations are ordered + must apply sequentially */
|
|
57
|
+
for (const m of pending) {
|
|
58
|
+
const ops: DbBatchOp[] = splitStatements(m.sql).map((sql) => ({ sql }));
|
|
59
|
+
// tracker insert is the LAST op in the same batch — committed iff every
|
|
60
|
+
// schema statement of this migration committed (atomic boundary).
|
|
61
|
+
ops.push({
|
|
62
|
+
sql: 'INSERT INTO _sql_migrations (name, applied_at) VALUES (?, ?)',
|
|
63
|
+
params: [m.name, new Date().toISOString()],
|
|
64
|
+
});
|
|
65
|
+
await driver.batch(ops);
|
|
66
|
+
ran.push(m.name);
|
|
67
|
+
}
|
|
68
|
+
/* eslint-enable no-await-in-loop */
|
|
69
|
+
return ran;
|
|
70
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Phase 9 (W2-1b): queue slot driver contract.
|
|
2
|
+
//
|
|
3
|
+
// The queue contract is the WHOLE semantics the two engines share, not just
|
|
4
|
+
// enqueue:
|
|
5
|
+
// - jobs table (createQueueStore) = persistent scheduler / source of truth
|
|
6
|
+
// - immediate vs delayed dispatch (delayed rows wait for the due-poll)
|
|
7
|
+
// - consumer ack + failure re-delivery (retry_count up to maxRetries, then
|
|
8
|
+
// failed; nonRetryable errors fail immediately)
|
|
9
|
+
// - pushAndWait inline execution (caller awaits the result)
|
|
10
|
+
// - host flush hook (CF flushes pending push/timer work before responding;
|
|
11
|
+
// the Node long-lived process is a no-op)
|
|
12
|
+
// - tenant is carried in the payload (Phase 5/6); the contract layer passes
|
|
13
|
+
// it through and NEVER resolves Host on the background path
|
|
14
|
+
//
|
|
15
|
+
// ONE engine (api/src/libs/queue) serves every runtime (Phase 12b, option A):
|
|
16
|
+
// the host swaps only the EXECUTOR (real fastq on node, cloudflare/shims/fastq
|
|
17
|
+
// in the worker) and the TRIGGER (in-process poll loop on node, CF scheduled()
|
|
18
|
+
// calling the engine's dispatchDueJobs() in the worker — see
|
|
19
|
+
// api/src/libs/queue/runtime.ts). Persistence, retry policy and tenant handling
|
|
20
|
+
// are shared because there is no second engine. The old cloudflare/shims/queue.ts
|
|
21
|
+
// duplicate engine was removed in Phase 12c.
|
|
22
|
+
|
|
23
|
+
import type EventEmitter from 'events';
|
|
24
|
+
|
|
25
|
+
export interface QueueOptions<T> {
|
|
26
|
+
id?: (job: T) => string;
|
|
27
|
+
concurrency?: number;
|
|
28
|
+
maxRetries?: number;
|
|
29
|
+
maxTimeout?: number;
|
|
30
|
+
retryDelay?: number;
|
|
31
|
+
enableScheduledJob?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PushParams<T> {
|
|
35
|
+
job: T;
|
|
36
|
+
id?: string;
|
|
37
|
+
persist?: boolean;
|
|
38
|
+
/** seconds */
|
|
39
|
+
delay?: number;
|
|
40
|
+
/** unix timestamp in seconds */
|
|
41
|
+
runAt?: number;
|
|
42
|
+
skipDuplicateCheck?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Internal re-delivery flag for rows already persisted (startup / scheduled
|
|
45
|
+
* recovery). Skips the enqueue tenant gate so the execution-side legacy
|
|
46
|
+
* strategy applies. NEVER set from application code.
|
|
47
|
+
*/
|
|
48
|
+
fromStore?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** the per-job event channel returned by push (emits queued/finished/failed/retry/cancelled) */
|
|
52
|
+
export type JobEvents = EventEmitter & { id?: string };
|
|
53
|
+
|
|
54
|
+
export interface QueueHandle<T = any> {
|
|
55
|
+
push(params: PushParams<T>): JobEvents;
|
|
56
|
+
pushAndWait(params: PushParams<T>): Promise<{ id: string; job: T; result: any }>;
|
|
57
|
+
get(id: string): Promise<T | null>;
|
|
58
|
+
delete(id: string, knownExists?: boolean): Promise<boolean>;
|
|
59
|
+
cancel(id: string): Promise<T | null>;
|
|
60
|
+
update(id: string, updates: any): Promise<any>;
|
|
61
|
+
/**
|
|
62
|
+
* D2 teardown: stop this queue's node poll loop (clears its sleep timer).
|
|
63
|
+
* No-op when the queue is not scheduled or on a workerd host (no loop runs).
|
|
64
|
+
* The Node host tears every queue down via lifecycle.stop() → stopAllQueues().
|
|
65
|
+
*/
|
|
66
|
+
stop?(): void;
|
|
67
|
+
options: Required<Omit<QueueOptions<T>, 'id'>>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** factory shape both engines export as default */
|
|
71
|
+
export type QueueFactory = <T = any>(params: {
|
|
72
|
+
name: string;
|
|
73
|
+
onJob: (job: T) => Promise<any>;
|
|
74
|
+
options?: QueueOptions<T>;
|
|
75
|
+
}) => QueueHandle<T> & EventEmitter;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Host flush hook. CF must flush pending push/timer work before returning the
|
|
79
|
+
* response (the isolate is torn down after); a long-lived Node process never
|
|
80
|
+
* needs to and uses the no-op below.
|
|
81
|
+
*/
|
|
82
|
+
export interface QueueHostHooks {
|
|
83
|
+
flush(): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Node host: long-lived process, nothing to flush before a response. */
|
|
87
|
+
export const nodeQueueHostHooks: QueueHostHooks = {
|
|
88
|
+
async flush() {
|
|
89
|
+
/* no-op — the process stays alive, in-flight jobs continue */
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Active host hooks — injectable by the factory's `queue` slot; defaults to the
|
|
94
|
+
// Node no-op. The worker shell injects hooks that flush pending push/timer work
|
|
95
|
+
// before responding.
|
|
96
|
+
let activeQueueHostHooks: QueueHostHooks = nodeQueueHostHooks;
|
|
97
|
+
|
|
98
|
+
export function setQueueHostHooks(hooks: QueueHostHooks): void {
|
|
99
|
+
activeQueueHostHooks = hooks;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getQueueHostHooks(): QueueHostHooks {
|
|
103
|
+
return activeQueueHostHooks;
|
|
104
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Phase 11 (W2-3): secrets slot driver contract — per-tenant keyring.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the process-level single key (cloudflare/shims/blocklet-sdk/security
|
|
4
|
+
// `_password`, initialized once from APP_PID's EK). Each tenant gets its own
|
|
5
|
+
// encryption key derived from its own EK, so one tenant's key can never decrypt
|
|
6
|
+
// another's ciphertext.
|
|
7
|
+
//
|
|
8
|
+
// Two surfaces, because the payment hot path (PaymentMethod.encrypt/decrypt
|
|
9
|
+
// Settings, getStripeClient, ~50 sync call sites) cannot become async without a
|
|
10
|
+
// large, risky ripple:
|
|
11
|
+
// - encrypt/decrypt (async): the full contract — lazy EK fetch + cache + TTL,
|
|
12
|
+
// and decrypt-failure forces one EK re-fetch (tolerates identity-side EK
|
|
13
|
+
// rotation). Hosts use this.
|
|
14
|
+
// - encryptSync/decryptSync: the hot path — uses an already-resolved key
|
|
15
|
+
// (default driver: the process key; keyring driver: the cached password,
|
|
16
|
+
// warmed via warmup()). Throws if the keyring key is cold (fail-closed).
|
|
17
|
+
// - warmup(instanceDid): async pre-resolve so the sync path is a cache hit
|
|
18
|
+
// (no-op for the single-tenant default driver).
|
|
19
|
+
//
|
|
20
|
+
// single mode (Blocklet Server) uses the default driver = the existing process
|
|
21
|
+
// `@blocklet/sdk/lib/security`, so existing ciphertext stays decryptable and
|
|
22
|
+
// behavior is unchanged.
|
|
23
|
+
|
|
24
|
+
/* eslint-disable max-classes-per-file */
|
|
25
|
+
import crypto from 'crypto';
|
|
26
|
+
|
|
27
|
+
import type { IdentityDriver } from './identity';
|
|
28
|
+
|
|
29
|
+
// crypto-js ships no type declarations; required untyped (same AES chain as the
|
|
30
|
+
// prior process security so ciphertext is interchangeable)
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-extraneous-dependencies
|
|
32
|
+
const CryptoJS: any = require('crypto-js');
|
|
33
|
+
|
|
34
|
+
export interface SecretsDriver {
|
|
35
|
+
/** async per-tenant encrypt — lazy-resolves the tenant key on first use */
|
|
36
|
+
encrypt(instanceDid: string, value: string): Promise<string>;
|
|
37
|
+
/** async per-tenant decrypt — re-fetches the EK once on failure (EK rotation tolerance) */
|
|
38
|
+
decrypt(instanceDid: string, value: string): Promise<string>;
|
|
39
|
+
/** sync hot path — requires the tenant key already resolved (warmup first for the keyring) */
|
|
40
|
+
encryptSync(instanceDid: string, value: string): string;
|
|
41
|
+
decryptSync(instanceDid: string, value: string): string;
|
|
42
|
+
/** pre-resolve the tenant key so the sync path is a cache hit (no-op for the default driver) */
|
|
43
|
+
warmup(instanceDid: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// default driver — single-tenant, delegates to the process @blocklet/sdk security
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
class DefaultSecretsDriver implements SecretsDriver {
|
|
51
|
+
// lazily required so importing this module stays side-effect-free
|
|
52
|
+
private security() {
|
|
53
|
+
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
54
|
+
return require('@blocklet/sdk/lib/security').default ?? require('@blocklet/sdk/lib/security');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
encryptSync(_instanceDid: string, value: string): string {
|
|
58
|
+
return this.security().encrypt(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
decryptSync(_instanceDid: string, value: string): string {
|
|
62
|
+
return this.security().decrypt(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line require-await -- async contract; the single key is sync
|
|
66
|
+
async encrypt(instanceDid: string, value: string): Promise<string> {
|
|
67
|
+
return this.encryptSync(instanceDid, value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// eslint-disable-next-line require-await -- async contract; the single key is sync
|
|
71
|
+
async decrypt(instanceDid: string, value: string): Promise<string> {
|
|
72
|
+
return this.decryptSync(instanceDid, value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async warmup(): Promise<void> {
|
|
76
|
+
/* single key is always available — nothing to warm */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createDefaultSecretsDriver(): SecretsDriver {
|
|
81
|
+
return new DefaultSecretsDriver();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// keyring driver — per-tenant key derived from each tenant's EK
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // 10 min
|
|
89
|
+
|
|
90
|
+
type CacheEntry = { password: string; expiresAt: number };
|
|
91
|
+
|
|
92
|
+
// AES decrypt with a wrong key can throw "Malformed UTF-8" or return empty,
|
|
93
|
+
// depending on the ciphertext bytes. Normalize both to '' so a wrong key is a
|
|
94
|
+
// deterministic failure (never the plaintext, never a crash) — this is what
|
|
95
|
+
// drives the decrypt-retry and what keeps cross-tenant decryption safe.
|
|
96
|
+
function safeUtf8(decrypted: any): string {
|
|
97
|
+
try {
|
|
98
|
+
return decrypted.toString(CryptoJS.enc.Utf8);
|
|
99
|
+
} catch {
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function derivePassword(appEk: string, instanceDid: string): string {
|
|
105
|
+
// identical chain to the prior process security: PBKDF2(EK, salt) -> AES key.
|
|
106
|
+
// salt = the tenant DID (== blockletDid in single mode), so single-mode
|
|
107
|
+
// ciphertext written by the old path stays decryptable when the same EK is
|
|
108
|
+
// returned by identity.getAppEk(instanceDid).
|
|
109
|
+
return crypto.pbkdf2Sync(appEk, instanceDid, 256, 32, 'sha512').toString('hex');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class KeyringSecretsDriver implements SecretsDriver {
|
|
113
|
+
private identity: IdentityDriver;
|
|
114
|
+
private ttlMs: number;
|
|
115
|
+
private cache = new Map<string, CacheEntry>();
|
|
116
|
+
|
|
117
|
+
constructor(identity: IdentityDriver, opts?: { ttlMs?: number }) {
|
|
118
|
+
if (typeof identity.getAppEk !== 'function') {
|
|
119
|
+
throw new Error('createKeyringSecretsDriver: identity driver must provide getAppEk(instanceDid)');
|
|
120
|
+
}
|
|
121
|
+
this.identity = identity;
|
|
122
|
+
this.ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private fresh(instanceDid: string): string | null {
|
|
126
|
+
const entry = this.cache.get(instanceDid);
|
|
127
|
+
if (entry && entry.expiresAt > Date.now()) return entry.password;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async resolvePassword(instanceDid: string, force = false): Promise<string> {
|
|
132
|
+
if (!force) {
|
|
133
|
+
const cached = this.fresh(instanceDid);
|
|
134
|
+
if (cached) return cached;
|
|
135
|
+
}
|
|
136
|
+
const appEk = await this.identity.getAppEk!(instanceDid);
|
|
137
|
+
if (!appEk) {
|
|
138
|
+
throw new Error(`secrets: no EK for tenant ${instanceDid}`);
|
|
139
|
+
}
|
|
140
|
+
const password = derivePassword(appEk, instanceDid);
|
|
141
|
+
this.cache.set(instanceDid, { password, expiresAt: Date.now() + this.ttlMs });
|
|
142
|
+
return password;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async warmup(instanceDid: string): Promise<void> {
|
|
146
|
+
await this.resolvePassword(instanceDid);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
encryptSync(instanceDid: string, value: string): string {
|
|
150
|
+
const password = this.fresh(instanceDid);
|
|
151
|
+
if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
|
|
152
|
+
return CryptoJS.AES.encrypt(value, password).toString();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
decryptSync(instanceDid: string, value: string): string {
|
|
156
|
+
const password = this.fresh(instanceDid);
|
|
157
|
+
if (!password) throw new Error(`secrets: key for tenant ${instanceDid} is not warmed (call warmup first)`);
|
|
158
|
+
return safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async encrypt(instanceDid: string, value: string): Promise<string> {
|
|
162
|
+
const password = await this.resolvePassword(instanceDid);
|
|
163
|
+
return CryptoJS.AES.encrypt(value, password).toString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async decrypt(instanceDid: string, value: string): Promise<string> {
|
|
167
|
+
let password = await this.resolvePassword(instanceDid);
|
|
168
|
+
let plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
169
|
+
if (!plain) {
|
|
170
|
+
// empty result == wrong key (likely rotated EK) -> force one re-fetch + retry
|
|
171
|
+
password = await this.resolvePassword(instanceDid, true);
|
|
172
|
+
plain = safeUtf8(CryptoJS.AES.decrypt(value, password));
|
|
173
|
+
}
|
|
174
|
+
return plain;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createKeyringSecretsDriver(identity: IdentityDriver, opts?: { ttlMs?: number }): SecretsDriver {
|
|
179
|
+
return new KeyringSecretsDriver(identity, opts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// active driver + tenant-aware facade (resolves tenant from TenantContext)
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
let activeSecretsDriver: SecretsDriver = createDefaultSecretsDriver();
|
|
187
|
+
|
|
188
|
+
export function setSecretsDriver(driver: SecretsDriver): void {
|
|
189
|
+
activeSecretsDriver = driver;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getSecretsDriver(): SecretsDriver {
|
|
193
|
+
return activeSecretsDriver;
|
|
194
|
+
}
|