payment-kit 1.29.0 → 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.
- 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 +36 -0
- package/api/src/crons/base.ts +3 -3
- package/api/src/crons/currency.ts +1 -1
- package/api/src/crons/index.ts +27 -24
- 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/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 +99 -38
- package/api/src/libs/context.ts +78 -1
- package/api/src/libs/currency.ts +2 -2
- package/api/src/libs/dayjs.ts +8 -2
- 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 +81 -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 +50 -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 +259 -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 +73 -0
- package/api/src/middlewares/hono/csrf.ts +72 -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 +214 -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 +17 -12
- 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 +12 -4
- 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 +41 -28
- package/api/src/queues/payout.ts +9 -5
- package/api/src/queues/refund.ts +18 -12
- 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} +193 -223
- 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} +96 -80
- 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 +667 -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 +66 -22
- 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/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/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/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 +236 -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/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/README.md +26 -6
- package/cloudflare/build.ts +28 -13
- package/cloudflare/did-connect-auth.ts +0 -217
- package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -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 +31 -56
- 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/util-csrf.ts +13 -0
- package/cloudflare/shims/blocklet-sdk/util-wallet.ts +8 -0
- 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/node-fetch.ts +35 -0
- package/cloudflare/shims/xss.ts +8 -0
- package/cloudflare/tenant-middleware.ts +36 -0
- package/cloudflare/tests/tenant-middleware.spec.ts +160 -0
- package/cloudflare/tests/worker-handler-gate.spec.ts +44 -0
- package/cloudflare/worker.ts +204 -433
- package/cloudflare/wrangler.local-e2e.jsonc +26 -0
- package/jest.config.js +3 -1
- package/package.json +33 -38
- 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/env.d.ts +13 -1
- package/tsconfig.json +1 -1
- 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/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,139 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// D2 (S3.0) E2E harness — really runs the cron driver + queue teardown and
|
|
3
|
+
// prints JSON-shaped results. Captured into planning/.../logs/sD2-e2e.log.
|
|
4
|
+
//
|
|
5
|
+
// Run: NODE_ENV=test npx tsx scripts/e2e-d2-cron-queue.ts
|
|
6
|
+
//
|
|
7
|
+
// Covers: (1) crons/index conservation — every declared job flows through the
|
|
8
|
+
// injected driver, no bare @abtnode/cron; (2) node-cron self-schedules a real
|
|
9
|
+
// 1s cron with NO external runDue tick; (3) teardown — stop() clears the cron
|
|
10
|
+
// timer (active-handle count drops); (4) cf-cron stays passive (negative).
|
|
11
|
+
process.env.NODE_ENV = 'test';
|
|
12
|
+
process.env.BLOCKLET_MODE = 'test';
|
|
13
|
+
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { createCronRegistry, setCronDriver } from '../api/src/libs/drivers/cron';
|
|
17
|
+
|
|
18
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
const cronTimers = () => (process as any).getActiveResourcesInfo().filter((r: string) => r === 'Timeout').length;
|
|
20
|
+
function emit(label: string, obj: unknown) {
|
|
21
|
+
console.log(`=== ${label} ===`);
|
|
22
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
// NOTE: crons/index conservation (every declared job flows through the
|
|
27
|
+
// injected driver) is verified in jest where the db/env is set up —
|
|
28
|
+
// tests/libs/crons-conservation-d2.spec.ts — because importing crons/index
|
|
29
|
+
// eagerly builds queues that need a real sequelize. Here we exercise the
|
|
30
|
+
// driver self-scheduling + teardown directly (no db needed).
|
|
31
|
+
setCronDriver(createCronRegistry('node-cron'));
|
|
32
|
+
|
|
33
|
+
// 1) job-names list — the full declared cron set crons/index registers through
|
|
34
|
+
// the driver (parsed from source; importing the graph needs a real DB/ESM).
|
|
35
|
+
const cronsSrc = fs.readFileSync(path.resolve(__dirname, '../api/src/crons/index.ts'), 'utf8');
|
|
36
|
+
const declaredNames = [...cronsSrc.matchAll(/name:\s*'([^']+)'/g)].map((m) => m[1]);
|
|
37
|
+
emit('C1 crons-conservation-job-names', {
|
|
38
|
+
registerCalls: (cronsSrc.match(/getCronDriver\(\)\.register\(/g) || []).length,
|
|
39
|
+
bareCronInit: /Cron\.init\s*\(/.test(cronsSrc),
|
|
40
|
+
count: declaredNames.length,
|
|
41
|
+
names: declaredNames,
|
|
42
|
+
success: declaredNames.length >= 18 && (cronsSrc.match(/getCronDriver\(\)\.register\(/g) || []).length === 1,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2) node-cron self-schedules a real 1s cron with no external runDue
|
|
46
|
+
const node = createCronRegistry('node-cron');
|
|
47
|
+
let ticks = 0;
|
|
48
|
+
const beforeReg = cronTimers();
|
|
49
|
+
node.register([
|
|
50
|
+
{
|
|
51
|
+
name: 'recon',
|
|
52
|
+
time: '*/1 * * * * *',
|
|
53
|
+
fn: () => {
|
|
54
|
+
ticks += 1;
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
const withTimer = cronTimers();
|
|
59
|
+
await wait(2200);
|
|
60
|
+
const ticksWhileRunning = ticks;
|
|
61
|
+
emit('C2 node-self-schedule', {
|
|
62
|
+
ticksWhileRunning,
|
|
63
|
+
timerAddedOnRegister: withTimer > beforeReg,
|
|
64
|
+
success: ticksWhileRunning >= 1 && withTimer > beforeReg,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 3) teardown — stop() clears the cron timer; no more ticks
|
|
68
|
+
node.stop();
|
|
69
|
+
const afterStop = cronTimers();
|
|
70
|
+
const ticksAtStop = ticks;
|
|
71
|
+
await wait(2200);
|
|
72
|
+
emit('C3 teardown-stop', {
|
|
73
|
+
timersWithCron: withTimer,
|
|
74
|
+
timersAfterStop: afterStop,
|
|
75
|
+
timerCleared: afterStop < withTimer,
|
|
76
|
+
ticksAfterStop: ticks - ticksAtStop,
|
|
77
|
+
success: afterStop < withTimer && ticks - ticksAtStop === 0,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 4) restart idempotent — re-register after stop, no double scheduler
|
|
81
|
+
let ran2 = 0;
|
|
82
|
+
node.register([
|
|
83
|
+
{
|
|
84
|
+
name: 'init2',
|
|
85
|
+
time: '0 0 0 1 1 *',
|
|
86
|
+
fn: () => {
|
|
87
|
+
ran2 += 1;
|
|
88
|
+
},
|
|
89
|
+
options: { runOnInit: true },
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
await wait(50);
|
|
93
|
+
const afterFirst = ran2;
|
|
94
|
+
node.stop();
|
|
95
|
+
node.register([
|
|
96
|
+
{
|
|
97
|
+
name: 'init2',
|
|
98
|
+
time: '0 0 0 1 1 *',
|
|
99
|
+
fn: () => {
|
|
100
|
+
ran2 += 1;
|
|
101
|
+
},
|
|
102
|
+
options: { runOnInit: true },
|
|
103
|
+
},
|
|
104
|
+
]);
|
|
105
|
+
await wait(50);
|
|
106
|
+
node.stop();
|
|
107
|
+
emit('C4 restart-idempotent', {
|
|
108
|
+
afterFirstStart: afterFirst,
|
|
109
|
+
afterRestart: ran2,
|
|
110
|
+
success: afterFirst === 1 && ran2 === 2, // exactly one run per start, no double
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 5) NEGATIVE — cf-cron stays passive: register does NOT self-fire
|
|
114
|
+
const cf = createCronRegistry('cf-cron');
|
|
115
|
+
let cfRan = 0;
|
|
116
|
+
cf.register([
|
|
117
|
+
{
|
|
118
|
+
name: 'cf',
|
|
119
|
+
time: '*/1 * * * * *',
|
|
120
|
+
fn: () => {
|
|
121
|
+
cfRan += 1;
|
|
122
|
+
},
|
|
123
|
+
options: { runOnInit: true },
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
await wait(1200);
|
|
127
|
+
const cfSelfFired = cfRan;
|
|
128
|
+
const due = await cf.runDue(new Date());
|
|
129
|
+
emit('C5 cf-passive-negative', {
|
|
130
|
+
selfFiredWithoutHostTick: cfSelfFired,
|
|
131
|
+
ranViaRunDue: due.ran,
|
|
132
|
+
success: cfSelfFired === 0 && due.ran.includes('cf'),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log('=== DONE ===');
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main();
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// D3 (S3.0) E2E harness — builds a REAL embedded multi-mode payment service over
|
|
3
|
+
// a file-backed sqlite (46-table schema) and exercises the background engine,
|
|
4
|
+
// emitting JSON. Captured into planning/.../logs/sD3-e2e.log.
|
|
5
|
+
//
|
|
6
|
+
// NODE_ENV=test npx tsx scripts/e2e-d3-embedded-multi.ts
|
|
7
|
+
process.env.NODE_ENV = 'test';
|
|
8
|
+
process.env.BLOCKLET_MODE = 'test';
|
|
9
|
+
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { Sequelize } from 'sequelize';
|
|
14
|
+
|
|
15
|
+
import { withTenant, getInstanceDid } from '../api/src/libs/context';
|
|
16
|
+
import { createNodeDbDriver } from '../api/src/libs/drivers/db';
|
|
17
|
+
import {
|
|
18
|
+
applyPaymentCoreMigrations,
|
|
19
|
+
createMemoryLocksDriver,
|
|
20
|
+
createCronRegistry,
|
|
21
|
+
createKeyringSecretsDriver,
|
|
22
|
+
nodeQueueHostHooks,
|
|
23
|
+
} from '../api/src/libs/drivers';
|
|
24
|
+
import { setQueueRuntimeMode } from '../api/src/libs/queue/runtime';
|
|
25
|
+
import { createEmbeddedPaymentService } from '../api/src/service';
|
|
26
|
+
|
|
27
|
+
const TENANT_A = 'did:abt:zD3A';
|
|
28
|
+
const TENANT_B = 'did:abt:zD3B';
|
|
29
|
+
const identity = {
|
|
30
|
+
resolveInstanceDidForHost: (h: string | undefined) => (h === 'a' ? TENANT_A : h === 'b' ? TENANT_B : null),
|
|
31
|
+
getAppEk: (id: string) => (id === TENANT_A ? 'a'.repeat(64) : 'b'.repeat(64)),
|
|
32
|
+
};
|
|
33
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
34
|
+
const settle = (e: any): Promise<string> =>
|
|
35
|
+
new Promise((res) => ['finished', 'failed', 'cancelled'].forEach((ev) => e.on(ev, () => res(ev))));
|
|
36
|
+
function emit(label: string, obj: unknown) {
|
|
37
|
+
console.log(`=== ${label} ===`);
|
|
38
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'd3-e2e-'));
|
|
43
|
+
const sequelize = new Sequelize({
|
|
44
|
+
dialect: 'sqlite',
|
|
45
|
+
storage: path.join(dir, 'p.db'),
|
|
46
|
+
logging: false,
|
|
47
|
+
pool: { max: 5 },
|
|
48
|
+
});
|
|
49
|
+
const driver = createNodeDbDriver(sequelize);
|
|
50
|
+
await applyPaymentCoreMigrations(driver);
|
|
51
|
+
const tables = await driver.all<{ name: string }>(
|
|
52
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const slots = () => ({
|
|
56
|
+
config: { BLOCKLET_APP_PID: TENANT_A, PAYMENT_LIVEMODE: 'true' },
|
|
57
|
+
db: { sequelize: sequelize as any },
|
|
58
|
+
tenancy: { mode: 'multi' as const },
|
|
59
|
+
identity,
|
|
60
|
+
secrets: createKeyringSecretsDriver(identity),
|
|
61
|
+
queue: nodeQueueHostHooks,
|
|
62
|
+
cron: createCronRegistry('node-cron'),
|
|
63
|
+
locks: createMemoryLocksDriver(),
|
|
64
|
+
});
|
|
65
|
+
const svc = createEmbeddedPaymentService(slots());
|
|
66
|
+
setQueueRuntimeMode('node');
|
|
67
|
+
const createQueue = require('../api/src/libs/queue').default;
|
|
68
|
+
const createQueueStore = require('../api/src/libs/queue/store').default;
|
|
69
|
+
|
|
70
|
+
// happy — assembly
|
|
71
|
+
emit('D3.1 assembly', {
|
|
72
|
+
tableCount: tables.length,
|
|
73
|
+
hasEntitlementsCheck: typeof svc.rpc.entitlements.check === 'function',
|
|
74
|
+
hasMeterReport: typeof svc.rpc.meterEvents.report === 'function',
|
|
75
|
+
success: tables.length === 46,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// bad input — slot misconfig (NEGATIVE)
|
|
79
|
+
const noSecrets: any = slots();
|
|
80
|
+
delete noSecrets.secrets;
|
|
81
|
+
const noIdentity: any = slots();
|
|
82
|
+
delete noIdentity.identity;
|
|
83
|
+
const tryBuild = (s: any) => {
|
|
84
|
+
try {
|
|
85
|
+
createEmbeddedPaymentService(s);
|
|
86
|
+
return { threw: false, slot: null };
|
|
87
|
+
} catch (e: any) {
|
|
88
|
+
return { threw: true, slot: e?.slot, code: e?.code };
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const s1 = tryBuild(noSecrets);
|
|
92
|
+
const s2 = tryBuild(noIdentity);
|
|
93
|
+
emit('D3.2 slot-misconfig-negative', {
|
|
94
|
+
missingSecrets: s1,
|
|
95
|
+
missingIdentity: s2,
|
|
96
|
+
success: s1.threw && s1.slot === 'secrets' && s2.threw && s2.slot === 'identity',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// retry trace
|
|
100
|
+
const trace: number[] = [];
|
|
101
|
+
let successes = 0;
|
|
102
|
+
const rq = createQueue({
|
|
103
|
+
name: 'd3-retry',
|
|
104
|
+
onJob: async () => {
|
|
105
|
+
trace.push(trace.length + 1);
|
|
106
|
+
if (trace.length < 3) throw new Error('transient');
|
|
107
|
+
successes += 1;
|
|
108
|
+
},
|
|
109
|
+
options: { maxRetries: 5, retryDelay: 1 },
|
|
110
|
+
});
|
|
111
|
+
const rEv = await withTenant(TENANT_A, async () => rq.push({ job: { instance_did: TENANT_A } }));
|
|
112
|
+
const rEnd = await settle(rEv);
|
|
113
|
+
emit('D3.3 retry-trace', {
|
|
114
|
+
attempts: trace.length,
|
|
115
|
+
successSideEffects: successes,
|
|
116
|
+
terminal: rEnd,
|
|
117
|
+
success: trace.length === 3 && successes === 1 && rEnd === 'finished',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// restart recovery — job-id set diff
|
|
121
|
+
const jobId = 'recover-1';
|
|
122
|
+
const store = createQueueStore('d3-recover');
|
|
123
|
+
await store.addJob(jobId, { instance_did: TENANT_A }, {});
|
|
124
|
+
const before = (await store.getJobs()).map((r: any) => r.id);
|
|
125
|
+
let ranId = '';
|
|
126
|
+
const recoverQ = createQueue({
|
|
127
|
+
name: 'd3-recover',
|
|
128
|
+
onJob: async () => {},
|
|
129
|
+
});
|
|
130
|
+
// the recovered row's id comes from the runtime finished event (the payload
|
|
131
|
+
// carries no id) — proving the recovered id is the original, not regenerated.
|
|
132
|
+
recoverQ.on('finished', (data: { id: string }) => {
|
|
133
|
+
ranId = data.id;
|
|
134
|
+
});
|
|
135
|
+
await wait(400);
|
|
136
|
+
const after = (await store.getJobs()).map((r: any) => r.id);
|
|
137
|
+
emit('D3.4 restart-recovery', {
|
|
138
|
+
beforeJobIds: before,
|
|
139
|
+
afterJobIds: after,
|
|
140
|
+
executedId: ranId,
|
|
141
|
+
success: before.includes(jobId) && !after.includes(jobId) && ranId === jobId,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// cross-tenant rejection (NEGATIVE)
|
|
145
|
+
const { assertJobObjectTenant } = require('../api/src/libs/queue');
|
|
146
|
+
let leaked = false;
|
|
147
|
+
let observed = '';
|
|
148
|
+
const xq = createQueue({
|
|
149
|
+
name: 'd3-xtenant',
|
|
150
|
+
onJob: async () => {
|
|
151
|
+
observed = getInstanceDid();
|
|
152
|
+
assertJobObjectTenant({ instance_did: TENANT_B });
|
|
153
|
+
leaked = true;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
const xEv = await withTenant(TENANT_A, async () => xq.push({ job: { instance_did: TENANT_A } }));
|
|
157
|
+
const xEnd = await settle(xEv);
|
|
158
|
+
emit('D3.5 cross-tenant-rejection-negative', {
|
|
159
|
+
observedTenant: observed,
|
|
160
|
+
leaked,
|
|
161
|
+
terminal: xEnd,
|
|
162
|
+
success: observed === TENANT_A && !leaked && xEnd === 'failed',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log('=== DONE ===');
|
|
166
|
+
await sequelize.close();
|
|
167
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
main();
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable no-console, global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
|
|
2
|
+
// Phase 2 (express→hono) E2E — SELF-VALIDATING harness. Serves the REAL
|
|
3
|
+
// buildConnectRoutesHono() (DID-Connect via did-connect-js native attachHono)
|
|
4
|
+
// through REAL @hono/node-server serve(), then drives the surface over a real TCP
|
|
5
|
+
// socket and prints PASS/FAIL for each spec-table category. Uses the spike's env
|
|
6
|
+
// bootstrap (testSetup → a valid appInfo so generateSession succeeds — jest's
|
|
7
|
+
// bare env cannot, which is why these session-dependent checks live here).
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const testSetup = require('@blocklet/sdk/lib/util/test-setup').default;
|
|
13
|
+
|
|
14
|
+
testSetup();
|
|
15
|
+
process.env.BLOCKLET_DATA_DIR = process.env.BLOCKLET_DATA_DIR || fs.mkdtempSync(path.join(os.tmpdir(), 'e2e-s2-'));
|
|
16
|
+
const { fromRandom } = require('@ocap/wallet');
|
|
17
|
+
const { types } = require('@ocap/mcrypto');
|
|
18
|
+
|
|
19
|
+
const appWallet = fromRandom({ role: types.RoleType.ROLE_APPLICATION });
|
|
20
|
+
process.env.BLOCKLET_APP_SK = appWallet.secretKey;
|
|
21
|
+
process.env.BLOCKLET_APP_PK = appWallet.publicKey;
|
|
22
|
+
process.env.BLOCKLET_APP_ID = appWallet.address;
|
|
23
|
+
process.env.BLOCKLET_APP_PID = appWallet.address;
|
|
24
|
+
|
|
25
|
+
const { serve } = require('@hono/node-server');
|
|
26
|
+
const { buildConnectRoutesHono } = require('../api/src/service');
|
|
27
|
+
|
|
28
|
+
const PORT = Number(process.env.E2E_PORT || 9272);
|
|
29
|
+
const connectApp = buildConnectRoutesHono();
|
|
30
|
+
|
|
31
|
+
let pass = 0;
|
|
32
|
+
let fail = 0;
|
|
33
|
+
const check = (name: string, cond: boolean, detail = '') => {
|
|
34
|
+
if (cond) {
|
|
35
|
+
pass += 1;
|
|
36
|
+
console.log(` PASS ${name}`);
|
|
37
|
+
} else {
|
|
38
|
+
fail += 1;
|
|
39
|
+
console.log(` FAIL ${name} ${detail}`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const server = serve({ fetch: connectApp.fetch, port: PORT }, async (info: { port: number }) => {
|
|
44
|
+
const base = `http://127.0.0.1:${info.port}`;
|
|
45
|
+
console.log(`E2E_READY ${info.port} (real @hono/node-server socket)`);
|
|
46
|
+
try {
|
|
47
|
+
// E1 happy path: generateSession returns the full session shape
|
|
48
|
+
const r1 = await fetch(`${base}/api/did/payment/token`);
|
|
49
|
+
const s1: any = await r1.json();
|
|
50
|
+
console.log(`E1 GET /api/did/payment/token → ${r1.status} ${JSON.stringify(s1).slice(0, 240)}`);
|
|
51
|
+
check('E1 happy: status 200', r1.status === 200);
|
|
52
|
+
check('E1 happy: status="created"', s1.status === 'created');
|
|
53
|
+
check('E1 happy: token present', !!s1.token);
|
|
54
|
+
|
|
55
|
+
// Data loss (deep-link): the url carries the session token as _t_
|
|
56
|
+
const decoded = decodeURIComponent(decodeURIComponent(s1.url || ''));
|
|
57
|
+
check('Data-loss: deep-link url carries _t_=<token>', decoded.includes(`_t_=${s1.token}`), decoded.slice(0, 120));
|
|
58
|
+
|
|
59
|
+
// Data damage: appInfo intact (name + publisher/nodeDid), not stripped/garbled
|
|
60
|
+
check(
|
|
61
|
+
'Data-damage: appInfo.name present',
|
|
62
|
+
!!(s1.appInfo && s1.appInfo.name),
|
|
63
|
+
JSON.stringify(s1.appInfo).slice(0, 120)
|
|
64
|
+
);
|
|
65
|
+
check('Data-damage: appInfo.publisher (did) present', !!(s1.appInfo && s1.appInfo.publisher));
|
|
66
|
+
|
|
67
|
+
// Data leak: two sessions get DISTINCT tokens (no reuse/bleed)
|
|
68
|
+
const r2 = await fetch(`${base}/api/did/collect/token`);
|
|
69
|
+
const s2: any = await r2.json();
|
|
70
|
+
check(
|
|
71
|
+
'Data-leak: a second session has a DISTINCT token',
|
|
72
|
+
!!s2.token && s2.token !== s1.token,
|
|
73
|
+
`${s1.token} vs ${s2.token}`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Security: with a VALID session token, an UNSIGNED/forged auth POST is not
|
|
77
|
+
// accepted — onAuthResponse refuses the unsigned payload, so the session never
|
|
78
|
+
// advances to succeed and no authInfo is returned (the auth verification path
|
|
79
|
+
// runs unchanged under the hono adapter).
|
|
80
|
+
const rAuth = await fetch(`${base}/api/did/payment/auth?_t_=${s1.token}`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ userDid: 'did:abt:zForgedUser', claims: [] }),
|
|
84
|
+
});
|
|
85
|
+
const authBody = await rAuth.text();
|
|
86
|
+
let authJson: any = {};
|
|
87
|
+
try {
|
|
88
|
+
authJson = JSON.parse(authBody);
|
|
89
|
+
} catch {
|
|
90
|
+
/* non-json error body */
|
|
91
|
+
}
|
|
92
|
+
console.log(`Security POST /auth (unsigned, valid _t_) → ${rAuth.status} ${authBody.slice(0, 160)}`);
|
|
93
|
+
check(
|
|
94
|
+
'Security: unsigned/forged auth POST does NOT succeed (onAuthResponse refuses it)',
|
|
95
|
+
authJson.status !== 'succeed' && !authJson.authInfo,
|
|
96
|
+
`status=${rAuth.status}`
|
|
97
|
+
);
|
|
98
|
+
// and the session must NOT have advanced to succeed
|
|
99
|
+
const rStatus = await fetch(`${base}/api/did/payment/status?_t_=${s1.token}`);
|
|
100
|
+
const st: any = await rStatus.json().catch(() => ({}));
|
|
101
|
+
check(
|
|
102
|
+
'Security: session stays unauthenticated after the unsigned POST',
|
|
103
|
+
st.status !== 'succeed',
|
|
104
|
+
`status=${st.status}`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// E3 negative: a forged/non-existent token never resolves to a session
|
|
108
|
+
const rForged = await fetch(`${base}/api/did/payment/auth?_t_=deadbeef-not-a-token`);
|
|
109
|
+
const forgedBody = await rForged.text();
|
|
110
|
+
console.log(`E3 negative GET /auth?_t_=forged → ${rForged.status} ${forgedBody.slice(0, 100)}`);
|
|
111
|
+
check(
|
|
112
|
+
'E3 negative: forged token does not return a created session/authInfo',
|
|
113
|
+
!forgedBody.includes('"status":"created"') && !forgedBody.includes('authInfo')
|
|
114
|
+
);
|
|
115
|
+
} catch (err: any) {
|
|
116
|
+
fail += 1;
|
|
117
|
+
console.log(` FAIL harness error: ${err.message}`);
|
|
118
|
+
} finally {
|
|
119
|
+
console.log(`\nphase2-e2e: ${pass} passed, ${fail} failed`);
|
|
120
|
+
server.close();
|
|
121
|
+
process.exit(fail ? 1 : 0);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export {}; // make this file a module so its top-level consts do not collide with sibling e2e scripts in tsc global scope
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/* eslint-disable no-console, global-require, import/no-extraneous-dependencies, @typescript-eslint/no-var-requires */
|
|
2
|
+
// Phase 3e (express→hono) E2E — SELF-VALIDATING. Proves the Stripe webhook RAW
|
|
3
|
+
// BODY survives the FULL native pipeline (cors→xss→csrf→ensureI18n→cdn→context→
|
|
4
|
+
// livemode, applied via mountResourceGroup) over a REAL @hono/node-server socket:
|
|
5
|
+
// xss SKIPS the webhook path, so the handler's c.req.arrayBuffer() gets the exact
|
|
6
|
+
// bytes and stripe.constructEvent verifies. Tampered bytes → reject (§3.1).
|
|
7
|
+
process.env.NODE_ENV = 'test';
|
|
8
|
+
process.env.BLOCKLET_APP_SK = process.env.BLOCKLET_APP_SK || 'e2e_s3e_secret';
|
|
9
|
+
process.env.BLOCKLET_APP_PID = process.env.BLOCKLET_APP_PID || 'did:abt:zE2ES3eTenant';
|
|
10
|
+
|
|
11
|
+
const Stripe = require('stripe');
|
|
12
|
+
const { serve } = require('@hono/node-server');
|
|
13
|
+
const { Hono } = require('hono');
|
|
14
|
+
const { buildHonoApp } = require('../api/src/service');
|
|
15
|
+
const { mountResourceGroup } = require('../api/src/middlewares/hono/pipeline');
|
|
16
|
+
|
|
17
|
+
const secret = 'whsec_e2e_s3e';
|
|
18
|
+
const stripe = new Stripe('sk_test_dummy', { apiVersion: '2023-08-16' });
|
|
19
|
+
const payload = JSON.stringify({
|
|
20
|
+
id: 'evt_s3e_1',
|
|
21
|
+
type: 'payment_intent.succeeded',
|
|
22
|
+
livemode: false,
|
|
23
|
+
data: { object: { metadata: {}, note: '欧元 €42 — naïve café 🚀 "x"' } },
|
|
24
|
+
});
|
|
25
|
+
const sigHeader = stripe.webhooks.generateTestHeaderString({ payload, secret });
|
|
26
|
+
|
|
27
|
+
// A representative stripe webhook mounted at /api/integrations/stripe — exercises
|
|
28
|
+
// the REAL native pipeline (xss skip) + raw-body read + constructEvent. (The real
|
|
29
|
+
// route's extra PaymentMethod lookup is orthogonal to raw-body fidelity.)
|
|
30
|
+
const stripeApp = new Hono();
|
|
31
|
+
stripeApp.post('/webhook', async (c: any) => {
|
|
32
|
+
const signature = c.req.header('stripe-signature');
|
|
33
|
+
if (!signature) return c.json({ error: 'no sig' }, 400);
|
|
34
|
+
const raw = Buffer.from(await c.req.arrayBuffer());
|
|
35
|
+
try {
|
|
36
|
+
const event = stripe.webhooks.constructEvent(raw, signature, secret);
|
|
37
|
+
return c.json({ received: true, id: event.id, bytesIn: raw.length });
|
|
38
|
+
} catch (err: any) {
|
|
39
|
+
return c.json({ error: err.message }, 400);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const bridge = (() => Promise.resolve(new Response('bridge', { status: 404 }))) as any;
|
|
44
|
+
bridge.close = () => Promise.resolve();
|
|
45
|
+
const app = buildHonoApp(
|
|
46
|
+
() => bridge,
|
|
47
|
+
(native: any) => mountResourceGroup(native, '/api/integrations/stripe', stripeApp)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let pass = 0;
|
|
51
|
+
let fail = 0;
|
|
52
|
+
const check = (name: string, cond: boolean, extra = '') => {
|
|
53
|
+
if (cond) {
|
|
54
|
+
pass += 1;
|
|
55
|
+
console.log(` PASS ${name}`);
|
|
56
|
+
} else {
|
|
57
|
+
fail += 1;
|
|
58
|
+
console.log(` FAIL ${name} ${extra}`);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const PORT = Number(process.env.E2E_PORT || 9278);
|
|
63
|
+
const server = serve({ fetch: app.fetch, port: PORT }, async (info: { port: number }) => {
|
|
64
|
+
const base = `http://127.0.0.1:${info.port}`;
|
|
65
|
+
console.log(`E2E_READY ${info.port} (real @hono/node-server socket)`);
|
|
66
|
+
try {
|
|
67
|
+
// 1) valid signature over a real socket → constructEvent verifies through the native pipeline
|
|
68
|
+
const r1 = await fetch(`${base}/api/integrations/stripe/webhook`, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
|
|
71
|
+
body: Buffer.from(payload, 'utf8'),
|
|
72
|
+
});
|
|
73
|
+
const b1: any = await r1.json();
|
|
74
|
+
console.log(`E1 signed webhook → ${r1.status} ${JSON.stringify(b1)}`);
|
|
75
|
+
check(
|
|
76
|
+
'E1 raw-body verified through native pipeline (xss skip)',
|
|
77
|
+
r1.status === 200 && b1.received === true && b1.id === 'evt_s3e_1'
|
|
78
|
+
);
|
|
79
|
+
check(
|
|
80
|
+
'E1 exact byte count delivered (no consumption/re-encode)',
|
|
81
|
+
b1.bytesIn === Buffer.byteLength(payload, 'utf8'),
|
|
82
|
+
`${b1.bytesIn} vs ${Buffer.byteLength(payload, 'utf8')}`
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// 2) tampered bytes over the wire → reject
|
|
86
|
+
const buf = Buffer.from(payload, 'utf8');
|
|
87
|
+
// eslint-disable-next-line no-bitwise -- intentional single-byte flip to tamper the payload
|
|
88
|
+
buf[10] = (buf[10] as number) ^ 0x01;
|
|
89
|
+
const r2 = await fetch(`${base}/api/integrations/stripe/webhook`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
|
|
92
|
+
body: buf,
|
|
93
|
+
});
|
|
94
|
+
console.log(`E2 tampered webhook → ${r2.status}`);
|
|
95
|
+
check('E2 tampered body → 400 (signature mismatch)', r2.status === 400);
|
|
96
|
+
|
|
97
|
+
// 3) chunked transfer-encoding (streamed body) → still verifies
|
|
98
|
+
const stream = new ReadableStream({
|
|
99
|
+
start(controller) {
|
|
100
|
+
const b = Buffer.from(payload, 'utf8');
|
|
101
|
+
let i = 0;
|
|
102
|
+
const push = () => {
|
|
103
|
+
if (i >= b.length) {
|
|
104
|
+
controller.close();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const end = Math.min(i + 5, b.length);
|
|
108
|
+
controller.enqueue(new Uint8Array(b.subarray(i, end)));
|
|
109
|
+
i = end;
|
|
110
|
+
setTimeout(push, 1);
|
|
111
|
+
};
|
|
112
|
+
push();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const r3 = await fetch(`${base}/api/integrations/stripe/webhook`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'stripe-signature': sigHeader, 'content-type': 'application/json' },
|
|
118
|
+
body: stream,
|
|
119
|
+
// @ts-ignore duplex is required for a streamed request body
|
|
120
|
+
duplex: 'half',
|
|
121
|
+
});
|
|
122
|
+
const b3: any = await r3.json();
|
|
123
|
+
console.log(`E3 chunked webhook → ${r3.status} ${JSON.stringify(b3)}`);
|
|
124
|
+
check('E3 chunked transfer-encoding raw-body verified', r3.status === 200 && b3.received === true);
|
|
125
|
+
} catch (err: any) {
|
|
126
|
+
fail += 1;
|
|
127
|
+
console.log(` FAIL harness error: ${err.message}`);
|
|
128
|
+
} finally {
|
|
129
|
+
console.log(`\nphase3e-e2e: ${pass} passed, ${fail} failed`);
|
|
130
|
+
server.close();
|
|
131
|
+
process.exit(fail ? 1 : 0);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
export {}; // make this file a module so its top-level consts do not collide with sibling e2e scripts in tsc global scope
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// express→hono Phase 4 E2E — adapter collapse evidence.
|
|
3
|
+
//
|
|
4
|
+
// Proves the two production paths the collapse must preserve, over a REAL
|
|
5
|
+
// @hono/node-server socket (not app.fetch mock) where it matters:
|
|
6
|
+
// 1. the full hono app (buildHonoApp) serves /api/healthz over a real socket —
|
|
7
|
+
// the same serve({ fetch }) path the blocklet server uses;
|
|
8
|
+
// 2. svc.http.fetch (createFetchHandler) strips the host mount prefix and
|
|
9
|
+
// forwards RAW body bytes byte-for-byte (Stripe webhook signature fidelity).
|
|
10
|
+
// Raw JSON evidence only — appended to logs/s4-e2e.log.
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import http from 'http';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { Hono } from 'hono';
|
|
16
|
+
import { serve } from '@hono/node-server';
|
|
17
|
+
import { buildHonoApp } from '../api/src/service';
|
|
18
|
+
import { createFetchHandler } from '../api/src/libs/http-fetch-adapter';
|
|
19
|
+
|
|
20
|
+
const LOG = path.resolve(__dirname, '../../../docs/arc-integration/planning/express-to-hono/logs/s4-e2e.log');
|
|
21
|
+
fs.mkdirSync(path.dirname(LOG), { recursive: true });
|
|
22
|
+
fs.writeFileSync(LOG, '');
|
|
23
|
+
const log = (header: string, payload: unknown) => {
|
|
24
|
+
fs.appendFileSync(LOG, `=== ${header} ===\n${JSON.stringify(payload)}\n`);
|
|
25
|
+
console.log(header, JSON.stringify(payload));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const socketGet = (port: number, p: string): Promise<{ status: number; body: string }> =>
|
|
29
|
+
new Promise((resolve, reject) => {
|
|
30
|
+
const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET' }, (res) => {
|
|
31
|
+
let data = '';
|
|
32
|
+
res.on('data', (c) => {
|
|
33
|
+
data += c;
|
|
34
|
+
});
|
|
35
|
+
res.on('end', () => resolve({ status: res.statusCode || 0, body: data }));
|
|
36
|
+
});
|
|
37
|
+
req.on('error', reject);
|
|
38
|
+
req.end();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
let failures = 0;
|
|
43
|
+
const expect = (name: string, cond: boolean) => {
|
|
44
|
+
if (!cond) {
|
|
45
|
+
failures += 1;
|
|
46
|
+
log(`FAIL ${name}`, { ok: false });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── E1: full hono app over a REAL @hono/node-server socket ───────────────────
|
|
51
|
+
const app = buildHonoApp();
|
|
52
|
+
const server = serve({ fetch: app.fetch, port: 0, hostname: '127.0.0.1' }) as any;
|
|
53
|
+
const port: number = await new Promise((resolve) => {
|
|
54
|
+
if (server.address()?.port) return resolve(server.address().port);
|
|
55
|
+
server.on('listening', () => resolve(server.address().port));
|
|
56
|
+
// @hono/node-server resolves synchronously in most versions; fall back to a tick
|
|
57
|
+
setImmediate(() => resolve(server.address().port));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const health = await socketGet(port, '/api/healthz');
|
|
61
|
+
log('E1 GET /api/healthz (real @hono/node-server socket)', { status: health.status, body: JSON.parse(health.body) });
|
|
62
|
+
expect('E1 healthz 200', health.status === 200);
|
|
63
|
+
expect('E1 healthz {ok:true}', JSON.parse(health.body).ok === true);
|
|
64
|
+
|
|
65
|
+
const notFound = await socketGet(port, '/api/__does_not_exist__');
|
|
66
|
+
log('E3(negative) GET /api/__does_not_exist__ (unmatched → 404)', { status: notFound.status });
|
|
67
|
+
expect('E3 unmatched 404', notFound.status === 404);
|
|
68
|
+
|
|
69
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
70
|
+
|
|
71
|
+
// ── E2: svc.http.fetch base-strip + RAW body fidelity ────────────────────────
|
|
72
|
+
const core = new Hono();
|
|
73
|
+
core.post('/api/raw', async (c) => {
|
|
74
|
+
const buf = Buffer.from(await c.req.arrayBuffer());
|
|
75
|
+
return c.json({ len: buf.length, sig: crypto.createHmac('sha256', 'whsec_s4').update(buf).digest('hex') });
|
|
76
|
+
});
|
|
77
|
+
const httpFetch = createFetchHandler(core);
|
|
78
|
+
|
|
79
|
+
const payload = JSON.stringify({ id: 'evt_1', data: { nested: ['<b>', 1, null] } });
|
|
80
|
+
const expectedSig = crypto.createHmac('sha256', 'whsec_s4').update(payload).digest('hex');
|
|
81
|
+
const stripped = await httpFetch(
|
|
82
|
+
new Request('http://app.local/.well-known/payment/api/raw', {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'content-type': 'application/json' },
|
|
85
|
+
body: payload,
|
|
86
|
+
}),
|
|
87
|
+
{ basePath: '/.well-known/payment' }
|
|
88
|
+
);
|
|
89
|
+
const strippedBody = await stripped.json();
|
|
90
|
+
log('E2 svc.http.fetch base-strip + raw-body HMAC', {
|
|
91
|
+
status: stripped.status,
|
|
92
|
+
forwardedBytes: strippedBody.len,
|
|
93
|
+
expectedBytes: Buffer.byteLength(payload),
|
|
94
|
+
sigMatch: strippedBody.sig === expectedSig,
|
|
95
|
+
});
|
|
96
|
+
expect('E2 status 200', stripped.status === 200);
|
|
97
|
+
expect('E2 byte-exact', strippedBody.len === Buffer.byteLength(payload));
|
|
98
|
+
expect('E2 sig match', strippedBody.sig === expectedSig);
|
|
99
|
+
|
|
100
|
+
// negative: segment boundary — "/mntbeta" not stripped by basePath "/mnt"
|
|
101
|
+
const boundary = await httpFetch(new Request('http://app.local/mntbeta/api/raw', { method: 'POST', body: 'x' }), {
|
|
102
|
+
basePath: '/mnt',
|
|
103
|
+
});
|
|
104
|
+
log('E2(negative) segment-boundary "/mntbeta" not stripped by "/mnt"', { status: boundary.status });
|
|
105
|
+
expect('E2 boundary 404', boundary.status === 404);
|
|
106
|
+
|
|
107
|
+
log('SUMMARY', { failures, success: failures === 0 });
|
|
108
|
+
if (failures > 0) process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
main().catch((err) => {
|
|
112
|
+
log('FATAL', { error: err?.message, stack: err?.stack?.split('\n').slice(0, 5) });
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|