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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Phase 3 (express→hono) — hono fork of routes/integrations/app-store.ts.
|
|
2
2
|
//
|
|
3
3
|
// /verify — client-initiated verify. Payload schema mirrors aistro
|
|
4
4
|
// (`{ receipt?, signedTransaction?, ... }`, `.or('receipt','signedTransaction')`)
|
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
// JWS path takes priority; falls back to legacy receipt verifyReceipt.
|
|
7
7
|
// /webhook — App Store Server Notifications V2. Stubbed; full state machine
|
|
8
8
|
// lands in A1-followup.
|
|
9
|
+
//
|
|
10
|
+
// NOT raw-body: all three POST routes read the body via c.get('sanitizedBody') ?? {}.
|
|
11
|
+
// (Contrast with stripe.ts which does c.req.arrayBuffer() — that's Stripe only.)
|
|
9
12
|
|
|
10
|
-
import {
|
|
13
|
+
import { Hono } from 'hono';
|
|
11
14
|
import Joi from 'joi';
|
|
12
15
|
|
|
13
|
-
import handleAppStoreNotification from '
|
|
14
|
-
import { ingestVerifiedAppStorePurchase } from '
|
|
15
|
-
import { peekNotificationRouting } from '
|
|
16
|
-
import logger from '
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
16
|
+
import handleAppStoreNotification from '../../../integrations/app-store/handlers';
|
|
17
|
+
import { ingestVerifiedAppStorePurchase } from '../../../integrations/app-store/handlers/subscription';
|
|
18
|
+
import { peekNotificationRouting } from '../../../integrations/app-store/notification-routing';
|
|
19
|
+
import logger from '../../../libs/logger';
|
|
20
|
+
import { withTenant } from '../../../libs/context';
|
|
21
|
+
import { TENANT_MISMATCH, resolveRowTenant } from '../../../libs/tenant';
|
|
22
|
+
import { authenticate } from '../../../middlewares/hono/security';
|
|
23
|
+
import { Customer, PaymentMethod } from '../../../store/models';
|
|
24
|
+
import { systemFindAll } from '../../../store/scoped';
|
|
19
25
|
|
|
20
|
-
const
|
|
26
|
+
const app = new Hono();
|
|
21
27
|
const userAuth = authenticate<Customer>({ component: false, ensureLogin: true });
|
|
22
28
|
|
|
23
29
|
const verifyBodySchema = Joi.object<{
|
|
@@ -35,21 +41,20 @@ const verifyBodySchema = Joi.object<{
|
|
|
35
41
|
language: Joi.string().empty(['', null]),
|
|
36
42
|
}).or('receipt', 'signedTransaction');
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
app.post('/verify', userAuth, async (c) => {
|
|
39
45
|
try {
|
|
40
|
-
const did = (
|
|
46
|
+
const did = c.get('user')?.did;
|
|
41
47
|
if (!did) {
|
|
42
|
-
|
|
43
|
-
return;
|
|
48
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
44
49
|
}
|
|
45
|
-
const
|
|
50
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
51
|
+
const input = await verifyBodySchema.validateAsync(body, { stripUnknown: true });
|
|
46
52
|
|
|
47
53
|
const method = await PaymentMethod.findOne({
|
|
48
|
-
where: { type: 'app_store', active: true, livemode: !!
|
|
54
|
+
where: { type: 'app_store', active: true, livemode: !!c.get('livemode') },
|
|
49
55
|
});
|
|
50
56
|
if (!method) {
|
|
51
|
-
|
|
52
|
-
return;
|
|
57
|
+
return c.json({ error: 'app_store PaymentMethod not configured' }, 503);
|
|
53
58
|
}
|
|
54
59
|
const client = method.getAppStoreClient();
|
|
55
60
|
|
|
@@ -61,7 +66,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
|
|
|
61
66
|
receipt: input.receipt,
|
|
62
67
|
});
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
return c.json({
|
|
65
70
|
success: true,
|
|
66
71
|
subscription_id: result.subscription.id,
|
|
67
72
|
isFirstSubscribe: result.isFirstSubscribe,
|
|
@@ -77,7 +82,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
|
|
|
77
82
|
} catch (err: any) {
|
|
78
83
|
const message = err?.message || (typeof err === 'string' ? err : null) || 'verify failed';
|
|
79
84
|
logger.error('app_store verify failed', { message, stack: err?.stack });
|
|
80
|
-
|
|
85
|
+
return c.json({ success: false, error: { message, code: err?.code } }, 400);
|
|
81
86
|
}
|
|
82
87
|
});
|
|
83
88
|
|
|
@@ -112,21 +117,20 @@ const restoreBodySchema = Joi.object<{
|
|
|
112
117
|
* or create one. Partial success is allowed — per-item failures land in the
|
|
113
118
|
* `errors` array; per-item successes land in `restored`.
|
|
114
119
|
*/
|
|
115
|
-
|
|
120
|
+
app.post('/restore', userAuth, async (c) => {
|
|
116
121
|
try {
|
|
117
|
-
const did = (
|
|
122
|
+
const did = c.get('user')?.did;
|
|
118
123
|
if (!did) {
|
|
119
|
-
|
|
120
|
-
return;
|
|
124
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
121
125
|
}
|
|
122
|
-
const
|
|
126
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
127
|
+
const input = await restoreBodySchema.validateAsync(body, { stripUnknown: true });
|
|
123
128
|
|
|
124
129
|
const method = await PaymentMethod.findOne({
|
|
125
|
-
where: { type: 'app_store', active: true, livemode: !!
|
|
130
|
+
where: { type: 'app_store', active: true, livemode: !!c.get('livemode') },
|
|
126
131
|
});
|
|
127
132
|
if (!method) {
|
|
128
|
-
|
|
129
|
-
return;
|
|
133
|
+
return c.json({ error: 'app_store PaymentMethod not configured' }, 503);
|
|
130
134
|
}
|
|
131
135
|
const client = method.getAppStoreClient();
|
|
132
136
|
|
|
@@ -136,8 +140,8 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
136
140
|
// through `ingestVerifiedAppStorePurchase`.
|
|
137
141
|
const seen = new Set<string>();
|
|
138
142
|
const items: Array<{ kind: 'jws' | 'receipt'; value: string }> = [
|
|
139
|
-
...(input.signedTransactions ?? []).map((v) => ({ kind: 'jws' as const, value: v })),
|
|
140
|
-
...(input.receipts ?? []).map((v) => ({ kind: 'receipt' as const, value: v })),
|
|
143
|
+
...(input.signedTransactions ?? []).map((v: string) => ({ kind: 'jws' as const, value: v })),
|
|
144
|
+
...(input.receipts ?? []).map((v: string) => ({ kind: 'receipt' as const, value: v })),
|
|
141
145
|
].filter((item) => {
|
|
142
146
|
if (seen.has(item.value)) return false;
|
|
143
147
|
seen.add(item.value);
|
|
@@ -188,13 +192,13 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
188
192
|
results.push(...batchResults);
|
|
189
193
|
}
|
|
190
194
|
|
|
191
|
-
|
|
195
|
+
return c.json({
|
|
192
196
|
restored: results.filter((r) => r.ok),
|
|
193
197
|
errors: results.filter((r) => !r.ok),
|
|
194
198
|
});
|
|
195
199
|
} catch (err: any) {
|
|
196
200
|
logger.error('app_store restore failed', { error: err?.message, stack: err?.stack });
|
|
197
|
-
|
|
201
|
+
return c.json({ error: err?.message ?? 'restore failed' }, 400);
|
|
198
202
|
}
|
|
199
203
|
});
|
|
200
204
|
|
|
@@ -206,23 +210,27 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
206
210
|
* Apple doesn't retry-storm; a failure while PROCESSING a verified notification
|
|
207
211
|
* is transient and returns 5xx so Apple retries.
|
|
208
212
|
*/
|
|
209
|
-
|
|
210
|
-
const
|
|
213
|
+
app.post('/webhook', async (c) => {
|
|
214
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
215
|
+
const signedPayload = (body?.signedPayload as string | undefined) ?? '';
|
|
211
216
|
if (!signedPayload) {
|
|
212
217
|
logger.warn('app_store webhook missing signedPayload');
|
|
213
|
-
|
|
214
|
-
return;
|
|
218
|
+
return c.json({ skipped: true, reason: 'no signedPayload' });
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
// --- Select the matching method + verify. Failures here are NOT retryable. ---
|
|
218
222
|
let notification: any;
|
|
219
223
|
let client: any;
|
|
224
|
+
let matched: PaymentMethod;
|
|
220
225
|
try {
|
|
221
|
-
|
|
226
|
+
// Reverse lookup: the App Store Server Notification arrives with NO tenant
|
|
227
|
+
// context — find which tenant registered this bundle_id. A genuine
|
|
228
|
+
// cross-tenant read (systemFindAll bypasses TenantModel scoping); the
|
|
229
|
+
// matched method's tenant is enforced via withTenant(resolveRowTenant).
|
|
230
|
+
const methods = await systemFindAll(PaymentMethod, { where: { type: 'app_store' } });
|
|
222
231
|
if (methods.length === 0) {
|
|
223
232
|
logger.warn('app_store webhook: no PaymentMethod configured');
|
|
224
|
-
|
|
225
|
-
return;
|
|
233
|
+
return c.json({ skipped: true, reason: 'no app_store PaymentMethod' });
|
|
226
234
|
}
|
|
227
235
|
|
|
228
236
|
// Read bundleId/environment from the UNVERIFIED payload to pick the method.
|
|
@@ -230,38 +238,50 @@ router.post('/webhook', async (req: Request, res: Response) => {
|
|
|
230
238
|
// before the correct method is ever tried, and the old catch then 200'd —
|
|
231
239
|
// silently discarding valid notifications (PR #1381 review P1).
|
|
232
240
|
const routing = peekNotificationRouting(signedPayload);
|
|
233
|
-
const
|
|
241
|
+
const candidates = methods.filter((m) => {
|
|
234
242
|
const settings = PaymentMethod.decryptSettings(m.settings);
|
|
235
243
|
if (settings.app_store?.bundle_id !== routing?.bundleId) return false;
|
|
236
244
|
if (!routing?.environment) return true;
|
|
237
245
|
return settings.app_store?.environment === routing.environment.toLowerCase();
|
|
238
246
|
});
|
|
239
|
-
if (
|
|
247
|
+
if (candidates.length === 0) {
|
|
240
248
|
logger.warn('app_store webhook: no matching PaymentMethod', {
|
|
241
249
|
bundleId: routing?.bundleId,
|
|
242
250
|
environment: routing?.environment,
|
|
243
251
|
});
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
return c.json({ skipped: true, reason: 'no matching PaymentMethod' });
|
|
253
|
+
}
|
|
254
|
+
// Phase 6 (W1-4b): a channel identifier must map to exactly ONE tenant in
|
|
255
|
+
// a deployment — ambiguity means the reverse lookup could land the
|
|
256
|
+
// notification in the wrong tenant, so it is refused loudly.
|
|
257
|
+
if (candidates.length > 1) {
|
|
258
|
+
logger.error('app_store webhook: bundle_id registered by multiple methods, refusing', {
|
|
259
|
+
code: TENANT_MISMATCH,
|
|
260
|
+
bundleId: routing?.bundleId,
|
|
261
|
+
methodIds: candidates.map((m) => m.id),
|
|
262
|
+
});
|
|
263
|
+
return c.json({ skipped: true, reason: 'ambiguous bundle_id registration' });
|
|
246
264
|
}
|
|
265
|
+
matched = candidates[0]!;
|
|
247
266
|
|
|
248
267
|
client = matched.getAppStoreClient();
|
|
249
268
|
notification = await client.verifyNotificationPayload(signedPayload);
|
|
250
269
|
} catch (err: any) {
|
|
251
270
|
// Malformed / forged / not-for-us → ack so Apple stops retrying.
|
|
252
271
|
logger.warn('app_store webhook: verification/selection failed — acking', { error: err?.message });
|
|
253
|
-
|
|
254
|
-
return;
|
|
272
|
+
return c.json({ skipped: true, reason: 'verification failed' });
|
|
255
273
|
}
|
|
256
274
|
|
|
257
275
|
// --- Process the verified notification. Failures here ARE transient. ---
|
|
258
276
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
277
|
+
// tenant = the method that registered this bundle_id (reverse lookup);
|
|
278
|
+
// the whole ingest chain (queries, events, jobs) runs under it
|
|
279
|
+
await withTenant(resolveRowTenant(matched!), () => handleAppStoreNotification(notification, client));
|
|
280
|
+
return c.json({ received: true });
|
|
261
281
|
} catch (err: any) {
|
|
262
282
|
logger.error('app_store webhook processing failed — will retry', { error: err?.message, stack: err?.stack });
|
|
263
|
-
|
|
283
|
+
return c.json({ error: err?.message ?? 'processing failed' }, 500);
|
|
264
284
|
}
|
|
265
285
|
});
|
|
266
286
|
|
|
267
|
-
export default
|
|
287
|
+
export default app;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// Phase 3e (express→hono) — hono fork of routes/integrations/google-play.ts.
|
|
1
2
|
// Google Play Real-Time Developer Notification webhook receiver.
|
|
2
3
|
//
|
|
3
4
|
// Pub/Sub Push body:
|
|
@@ -8,19 +9,26 @@
|
|
|
8
9
|
//
|
|
9
10
|
// Auth: Pub/Sub puts a Google-signed JWT in `Authorization: Bearer <jwt>`.
|
|
10
11
|
// We verify the JWT claims here (signature verification is TODO — see verify.ts).
|
|
12
|
+
//
|
|
13
|
+
// NOTE: /webhook, /verify, /restore are NORMAL JSON POSTs — body is read via
|
|
14
|
+
// c.get('sanitizedBody') ?? {} (NOT c.req.arrayBuffer).
|
|
11
15
|
|
|
12
|
-
import {
|
|
16
|
+
import { Hono } from 'hono';
|
|
13
17
|
import Joi from 'joi';
|
|
18
|
+
import { googlePubsubPushServiceAccount, googlePubsubAllowUnverifiedSender, isTestEnv } from '../../../libs/env';
|
|
14
19
|
|
|
15
|
-
import handleGooglePlayEvent, { GooglePlayRtdnPayload } from '
|
|
16
|
-
import { ingestVerifiedGooglePlayPurchase } from '
|
|
17
|
-
import { decodePubSubMessage, verifyPubSubJwt } from '
|
|
18
|
-
import logger from '
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import handleGooglePlayEvent, { GooglePlayRtdnPayload } from '../../../integrations/google-play/handlers';
|
|
21
|
+
import { ingestVerifiedGooglePlayPurchase } from '../../../integrations/google-play/handlers/subscription';
|
|
22
|
+
import { decodePubSubMessage, verifyPubSubJwt } from '../../../integrations/google-play/verify';
|
|
23
|
+
import logger from '../../../libs/logger';
|
|
24
|
+
import { withTenant } from '../../../libs/context';
|
|
25
|
+
import { TENANT_MISMATCH, resolveRowTenant } from '../../../libs/tenant';
|
|
26
|
+
import { authenticate } from '../../../middlewares/hono/security';
|
|
27
|
+
import { googlePlayEndpoint } from '../../../libs/util';
|
|
28
|
+
import { Customer, PaymentMethod } from '../../../store/models';
|
|
29
|
+
import { systemFindAll } from '../../../store/scoped';
|
|
22
30
|
|
|
23
|
-
const
|
|
31
|
+
const app = new Hono();
|
|
24
32
|
const userAuth = authenticate<Customer>({ component: false, ensureLogin: true });
|
|
25
33
|
|
|
26
34
|
const verifyBodySchema = Joi.object<{
|
|
@@ -35,25 +43,24 @@ const verifyBodySchema = Joi.object<{
|
|
|
35
43
|
* Client-initiated verify (aistro-shape).
|
|
36
44
|
* Mobile client POSTs after StoreKit / BillingClient finishes the purchase.
|
|
37
45
|
*/
|
|
38
|
-
|
|
46
|
+
app.post('/verify', userAuth, async (c) => {
|
|
39
47
|
try {
|
|
40
|
-
const did = (
|
|
48
|
+
const did = (c.get('user') as any)?.did;
|
|
41
49
|
if (!did) {
|
|
42
|
-
|
|
43
|
-
return;
|
|
50
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
44
51
|
}
|
|
45
|
-
const
|
|
52
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
53
|
+
const input = await verifyBodySchema.validateAsync(body, { stripUnknown: true });
|
|
46
54
|
|
|
47
55
|
// Resolve the Google Play PaymentMethod for THIS livemode. Without the
|
|
48
56
|
// livemode filter a testmode request would silently fall through to the
|
|
49
57
|
// production method (and vice versa), and its encrypted credentials may
|
|
50
58
|
// not even decrypt under the current process key.
|
|
51
59
|
const method = await PaymentMethod.findOne({
|
|
52
|
-
where: { type: 'google_play', active: true, livemode: !!
|
|
60
|
+
where: { type: 'google_play', active: true, livemode: !!c.get('livemode') },
|
|
53
61
|
});
|
|
54
62
|
if (!method) {
|
|
55
|
-
|
|
56
|
-
return;
|
|
63
|
+
return c.json({ error: 'google_play PaymentMethod not configured' }, 503);
|
|
57
64
|
}
|
|
58
65
|
const client = method.getGooglePlayClient();
|
|
59
66
|
|
|
@@ -65,7 +72,7 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
|
|
|
65
72
|
subscriptionId: input.subscriptionId,
|
|
66
73
|
});
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
return c.json({
|
|
69
76
|
success: true,
|
|
70
77
|
subscription_id: result.subscription.id,
|
|
71
78
|
isFirstSubscribe: result.isFirstSubscribe,
|
|
@@ -86,10 +93,13 @@ router.post('/verify', userAuth, async (req: Request, res: Response) => {
|
|
|
86
93
|
errKeys: err ? Object.keys(err) : [],
|
|
87
94
|
stack: err?.stack,
|
|
88
95
|
});
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
return c.json(
|
|
97
|
+
{
|
|
98
|
+
success: false,
|
|
99
|
+
error: { message, raw: err?.errorMessage ?? null },
|
|
100
|
+
},
|
|
101
|
+
400
|
|
102
|
+
);
|
|
93
103
|
}
|
|
94
104
|
});
|
|
95
105
|
|
|
@@ -130,21 +140,20 @@ const restoreBodySchema = Joi.object<{
|
|
|
130
140
|
* either return the existing local Subscription or create one. Partial
|
|
131
141
|
* success is reported per item.
|
|
132
142
|
*/
|
|
133
|
-
|
|
143
|
+
app.post('/restore', userAuth, async (c) => {
|
|
134
144
|
try {
|
|
135
|
-
const did = (
|
|
145
|
+
const did = (c.get('user') as any)?.did;
|
|
136
146
|
if (!did) {
|
|
137
|
-
|
|
138
|
-
return;
|
|
147
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
139
148
|
}
|
|
140
|
-
const
|
|
149
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
150
|
+
const input = await restoreBodySchema.validateAsync(body, { stripUnknown: true });
|
|
141
151
|
|
|
142
152
|
const method = await PaymentMethod.findOne({
|
|
143
|
-
where: { type: 'google_play', active: true, livemode: !!
|
|
153
|
+
where: { type: 'google_play', active: true, livemode: !!c.get('livemode') },
|
|
144
154
|
});
|
|
145
155
|
if (!method) {
|
|
146
|
-
|
|
147
|
-
return;
|
|
156
|
+
return c.json({ error: 'google_play PaymentMethod not configured' }, 503);
|
|
148
157
|
}
|
|
149
158
|
const client = method.getGooglePlayClient();
|
|
150
159
|
|
|
@@ -152,7 +161,7 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
152
161
|
// Play purchase, so duplicates in the request would otherwise double-
|
|
153
162
|
// call Google's verifier and re-upsert the same Subscription row.
|
|
154
163
|
const seen = new Set<string>();
|
|
155
|
-
const purchases = input.purchases.filter((p) => {
|
|
164
|
+
const purchases = input.purchases.filter((p: { purchaseToken: string; subscriptionId: string }) => {
|
|
156
165
|
if (seen.has(p.purchaseToken)) return false;
|
|
157
166
|
seen.add(p.purchaseToken);
|
|
158
167
|
return true;
|
|
@@ -175,7 +184,7 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
175
184
|
const batch = purchases.slice(i, i + RESTORE_CONCURRENCY);
|
|
176
185
|
// eslint-disable-next-line no-await-in-loop -- intentional: batches must complete sequentially to bound concurrency
|
|
177
186
|
const batchResults = await Promise.all(
|
|
178
|
-
batch.map(async (p): Promise<ItemResult> => {
|
|
187
|
+
batch.map(async (p: { purchaseToken: string; subscriptionId: string }): Promise<ItemResult> => {
|
|
179
188
|
try {
|
|
180
189
|
const r = await ingestVerifiedGooglePlayPurchase({
|
|
181
190
|
customerDid: did,
|
|
@@ -202,13 +211,13 @@ router.post('/restore', userAuth, async (req: Request, res: Response) => {
|
|
|
202
211
|
results.push(...batchResults);
|
|
203
212
|
}
|
|
204
213
|
|
|
205
|
-
|
|
214
|
+
return c.json({
|
|
206
215
|
restored: results.filter((r) => r.ok),
|
|
207
216
|
errors: results.filter((r) => !r.ok),
|
|
208
217
|
});
|
|
209
218
|
} catch (err: any) {
|
|
210
219
|
logger.error('google_play restore failed', { error: err?.message, stack: err?.stack });
|
|
211
|
-
|
|
220
|
+
return c.json({ error: err?.message ?? 'restore failed' }, 400);
|
|
212
221
|
}
|
|
213
222
|
});
|
|
214
223
|
|
|
@@ -246,17 +255,17 @@ function markHandled(messageId: string): void {
|
|
|
246
255
|
seenMessageIds.set(messageId, now + MESSAGE_DEDUP_TTL_MS);
|
|
247
256
|
}
|
|
248
257
|
|
|
249
|
-
|
|
250
|
-
const expectedEmail =
|
|
258
|
+
app.post('/webhook', async (c) => {
|
|
259
|
+
const expectedEmail = googlePubsubPushServiceAccount();
|
|
251
260
|
// Fail CLOSED: in production the push service account MUST be configured. A
|
|
252
261
|
// sandbox/test bypass has to be explicit (PR #1381 review P1).
|
|
253
|
-
const allowUnverifiedSender =
|
|
254
|
-
process.env.GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER === 'true' || process.env.NODE_ENV === 'test';
|
|
262
|
+
const allowUnverifiedSender = googlePubsubAllowUnverifiedSender() || isTestEnv();
|
|
255
263
|
|
|
256
264
|
// --- Phase 1: authenticate + select. Failures here are rejections / not-for-us,
|
|
257
265
|
// NOT processing failures. ---
|
|
258
266
|
let payload: GooglePlayRtdnPayload;
|
|
259
267
|
let client: ReturnType<PaymentMethod['getGooglePlayClient']>;
|
|
268
|
+
let method: PaymentMethod;
|
|
260
269
|
let messageId: string | undefined;
|
|
261
270
|
try {
|
|
262
271
|
if (!expectedEmail && !allowUnverifiedSender) {
|
|
@@ -264,61 +273,72 @@ router.post('/webhook', async (req: Request, res: Response) => {
|
|
|
264
273
|
'google_play webhook refusing: GOOGLE_PUBSUB_PUSH_SERVICE_ACCOUNT unset ' +
|
|
265
274
|
'(set GOOGLE_PUBSUB_ALLOW_UNVERIFIED_SENDER=true only for sandbox)'
|
|
266
275
|
);
|
|
267
|
-
|
|
268
|
-
return;
|
|
276
|
+
return c.json({ error: 'sender verification not configured' }, 403);
|
|
269
277
|
}
|
|
270
278
|
|
|
271
|
-
const authHeader = req.
|
|
279
|
+
const authHeader = c.req.header('authorization') || c.req.header('Authorization');
|
|
272
280
|
if (authHeader) {
|
|
273
281
|
const token = authHeader.replace(/^Bearer\s+/i, '');
|
|
274
282
|
await verifyPubSubJwt(token, { expectedAudience: googlePlayEndpoint(), expectedEmail });
|
|
275
283
|
} else if (!allowUnverifiedSender) {
|
|
276
284
|
logger.warn('google_play webhook missing Authorization header');
|
|
277
|
-
|
|
278
|
-
return;
|
|
285
|
+
return c.json({ error: 'missing authorization' }, 401);
|
|
279
286
|
}
|
|
280
287
|
|
|
281
|
-
|
|
288
|
+
const body = c.get('sanitizedBody') ?? {};
|
|
289
|
+
messageId = (body as any)?.message?.messageId;
|
|
282
290
|
// Skip only messages we already handled SUCCESSFULLY (mark happens post-success).
|
|
283
291
|
if (messageId && wasHandled(messageId)) {
|
|
284
292
|
logger.info('google_play webhook: duplicate Pub/Sub messageId, skipping', { messageId });
|
|
285
|
-
|
|
286
|
-
return;
|
|
293
|
+
return c.json({ deduped: true });
|
|
287
294
|
}
|
|
288
295
|
|
|
289
|
-
payload = decodePubSubMessage<GooglePlayRtdnPayload>(
|
|
296
|
+
payload = decodePubSubMessage<GooglePlayRtdnPayload>(body);
|
|
290
297
|
|
|
291
|
-
|
|
292
|
-
|
|
298
|
+
// Reverse lookup: the RTDN webhook arrives with NO tenant context — find
|
|
299
|
+
// which tenant registered this package_name. A genuine cross-tenant read,
|
|
300
|
+
// so it must use systemFindAll to bypass TenantModel scoping; the matched
|
|
301
|
+
// method's tenant is then enforced via withTenant(resolveRowTenant) below.
|
|
302
|
+
const methods = await systemFindAll(PaymentMethod, { where: { type: 'google_play' } });
|
|
303
|
+
const candidates = methods.filter((m) => {
|
|
293
304
|
const settings = PaymentMethod.decryptSettings(m.settings);
|
|
294
305
|
return settings.google_play?.package_name === payload.packageName;
|
|
295
306
|
});
|
|
296
|
-
if (
|
|
307
|
+
if (candidates.length === 0) {
|
|
297
308
|
logger.warn('google_play webhook: no matching PaymentMethod for packageName', {
|
|
298
309
|
packageName: payload.packageName,
|
|
299
310
|
});
|
|
300
311
|
// Not for us → ack so Pub/Sub doesn't retry a misconfigured topic forever.
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
return c.json({ skipped: true });
|
|
313
|
+
}
|
|
314
|
+
// Phase 6 (W1-4b): one channel identifier maps to exactly one tenant
|
|
315
|
+
if (candidates.length > 1) {
|
|
316
|
+
logger.error('google_play webhook: package_name registered by multiple methods, refusing', {
|
|
317
|
+
code: TENANT_MISMATCH,
|
|
318
|
+
packageName: payload.packageName,
|
|
319
|
+
methodIds: candidates.map((m) => m.id),
|
|
320
|
+
});
|
|
321
|
+
return c.json({ skipped: true, reason: 'ambiguous package_name registration' });
|
|
303
322
|
}
|
|
323
|
+
method = candidates[0]!;
|
|
304
324
|
client = method.getGooglePlayClient();
|
|
305
325
|
} catch (err: any) {
|
|
306
326
|
// Auth / decode / selection failure → forged or malformed; reject.
|
|
307
327
|
logger.warn('google_play webhook: auth/decode failed', { error: err?.message });
|
|
308
|
-
|
|
309
|
-
return;
|
|
328
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
310
329
|
}
|
|
311
330
|
|
|
312
331
|
// --- Phase 2: process the verified event. Failure here is transient → 5xx so
|
|
313
332
|
// Pub/Sub retries; mark the messageId handled ONLY after success. ---
|
|
314
333
|
try {
|
|
315
|
-
|
|
334
|
+
// tenant = the method that registered this package_name (reverse lookup)
|
|
335
|
+
await withTenant(resolveRowTenant(method!), () => handleGooglePlayEvent(payload, client));
|
|
316
336
|
if (messageId) markHandled(messageId);
|
|
317
|
-
|
|
337
|
+
return c.json({ received: true });
|
|
318
338
|
} catch (err: any) {
|
|
319
339
|
logger.error('google_play webhook processing failed — will retry', { error: err?.message, stack: err?.stack });
|
|
320
|
-
|
|
340
|
+
return c.json({ error: err?.message ?? 'processing failed' }, 500);
|
|
321
341
|
}
|
|
322
342
|
});
|
|
323
343
|
|
|
324
|
-
export default
|
|
344
|
+
export default app;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Phase 3e (express→hono) — hono fork of routes/integrations/stripe.ts. The
|
|
2
|
+
// Stripe webhook is RAW-BODY: the signature is an HMAC over the EXACT received
|
|
3
|
+
// bytes, so the body must NOT be parsed/sanitized before verification. xss skips
|
|
4
|
+
// this path (RAW_BODY_PREFIXES); the route reads c.req.arrayBuffer() directly
|
|
5
|
+
// (§3.1). stripeEvent/stripeClient are injected into context by verifyWebhookSig
|
|
6
|
+
// (the route-internal middleware, not a global one).
|
|
7
|
+
import { Hono } from 'hono';
|
|
8
|
+
import type { MiddlewareHandler } from 'hono';
|
|
9
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
10
|
+
import { env } from '@blocklet/sdk/lib/env';
|
|
11
|
+
import get from 'lodash/get';
|
|
12
|
+
import { stripeWebhookSecret } from '../../../libs/env';
|
|
13
|
+
|
|
14
|
+
import handleStripeEvent from '../../../integrations/stripe/handlers';
|
|
15
|
+
import logger from '../../../libs/logger';
|
|
16
|
+
import { STRIPE_EVENTS } from '../../../libs/util';
|
|
17
|
+
import { PaymentMethod } from '../../../store/models';
|
|
18
|
+
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
|
|
21
|
+
const verifyWebhookSig: MiddlewareHandler = async (c, next) => {
|
|
22
|
+
try {
|
|
23
|
+
const signature = c.req.header('stripe-signature');
|
|
24
|
+
if (!signature) {
|
|
25
|
+
return c.json({ error: 'No stripe webhook signature found' }, 400);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// RAW body — read the exact bytes once (xss did not consume this path).
|
|
29
|
+
const raw = Buffer.from(await c.req.arrayBuffer());
|
|
30
|
+
const json = JSON.parse(raw.toString('utf8'));
|
|
31
|
+
const method = await PaymentMethod.findOne({ where: { type: 'stripe', livemode: json.livemode } });
|
|
32
|
+
if (!method) {
|
|
33
|
+
return c.json({ error: 'No stripe payment method found' }, 400);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const stripe = method.getStripeClient();
|
|
37
|
+
const settings = PaymentMethod.decryptSettings(method.settings);
|
|
38
|
+
const secret = stripeWebhookSecret() || settings.stripe?.webhook_signing_secret;
|
|
39
|
+
c.set('stripeEvent', stripe.webhooks.constructEvent(raw, signature, secret as string));
|
|
40
|
+
c.set('stripeClient', stripe);
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/return-await -- do NOT await: downstream route errors must not be caught by this sig-verify catch
|
|
43
|
+
return next();
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
logger.error('verify signature error', { error: err });
|
|
46
|
+
return c.json({ error: err.message }, 400);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleEvent: MiddlewareHandler = async (c) => {
|
|
51
|
+
const stripeEvent = c.get('stripeEvent');
|
|
52
|
+
const stripeClient = c.get('stripeClient');
|
|
53
|
+
|
|
54
|
+
if (STRIPE_EVENTS.includes(stripeEvent.type) === false) {
|
|
55
|
+
logger.debug('webhook event not interested', { id: stripeEvent.id, type: stripeEvent.type });
|
|
56
|
+
return c.json({ skipped: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// only events from this app should be processed
|
|
60
|
+
const appPid = get(stripeEvent, 'data.object.metadata.appPid');
|
|
61
|
+
if (appPid && appPid !== (env as any).appPid) {
|
|
62
|
+
logger.debug('webhook event for other app', { id: stripeEvent.id, type: stripeEvent.type });
|
|
63
|
+
return c.json({ skipped: true });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
logger.debug('webhook received event', { id: stripeEvent.id, type: stripeEvent.type });
|
|
67
|
+
await handleStripeEvent(stripeEvent, stripeClient);
|
|
68
|
+
|
|
69
|
+
return c.json({ received: true });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
app.post('/webhook', verifyWebhookSig, handleEvent);
|
|
73
|
+
|
|
74
|
+
export default app;
|