From 7cfe929b06d7e50c9bfe6bcb107fed6358edb749 Mon Sep 17 00:00:00 2001 From: saltbo Date: Sat, 9 May 2026 18:33:04 -0400 Subject: [PATCH] fix(store): use unified delivery webhook route --- server/app.ts | 2 +- server/licensing/cloud-event-token.ts | 2 +- server/routes/cloud-store.integration.test.ts | 44 +++++++++++++------ server/routes/cloud-store/storefront.ts | 1 + server/routes/cloud-store/webhooks.ts | 2 +- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/server/app.ts b/server/app.ts index 54a58298..815c794c 100644 --- a/server/app.ts +++ b/server/app.ts @@ -66,7 +66,7 @@ export function createApp(platform: Platform, auth: Auth) { app.route('/api/licensing', licensing) app.route('/api/branding', publicBranding) app.route('/api/site-invitations', publicSiteInvitations) - app.route('/api/store/webhooks', cloudStoreWebhooks) + app.route('/api/store', cloudStoreWebhooks) app.use('/api/*', authMiddleware) diff --git a/server/licensing/cloud-event-token.ts b/server/licensing/cloud-event-token.ts index 85aaf440..009f62a4 100644 --- a/server/licensing/cloud-event-token.ts +++ b/server/licensing/cloud-event-token.ts @@ -7,7 +7,7 @@ const CLOUD_EVENT_TOKEN_MAX_TTL_SECONDS = 5 * 60 const cloudEventTokenSchema = z.object({ type: z.literal('zpan.cloud.event'), - purpose: z.enum(['quota_store.delivery', 'store.delivery']), + purpose: z.literal('store.delivery'), issuer: z.string().min(1), audience: z.string().min(1), boundLicenseId: z.string().min(1), diff --git a/server/routes/cloud-store.integration.test.ts b/server/routes/cloud-store.integration.test.ts index fe96883c..f4c028fd 100644 --- a/server/routes/cloud-store.integration.test.ts +++ b/server/routes/cloud-store.integration.test.ts @@ -1007,6 +1007,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'http://localhost/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'http://localhost/storage', cancelUrl: 'http://localhost/storage', @@ -1060,6 +1063,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'http://localhost/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'http://localhost/storage', cancelUrl: 'http://localhost/storage', @@ -1080,6 +1086,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'https://files.example.com/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'https://files.example.com/storage', cancelUrl: 'https://files.example.com/storage', @@ -1100,6 +1109,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'https://auth.example.com/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'https://auth.example.com/storage', cancelUrl: 'https://auth.example.com/storage', @@ -1120,6 +1132,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'http://localhost/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'http://localhost/storage', cancelUrl: 'http://localhost/storage', @@ -1140,6 +1155,9 @@ describe('Quota Store API', () => { }) expect(checkout.status).toBe(200) + expect(orderPayload()).toMatchObject({ + deliveryCallbackUrl: 'http://localhost/api/store/webhook', + }) expect(paymentPayload()).toMatchObject({ successUrl: 'http://localhost/storage', cancelUrl: 'http://localhost/storage', @@ -2044,7 +2062,7 @@ describe('Quota Store API', () => { expiresAt: '2026-06-01T00:00:00.000Z', }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2885,7 +2903,7 @@ describe('Quota Store API', () => { const headers = await adminHeaders(app) await seedSettings(app, headers) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2903,7 +2921,7 @@ describe('Quota Store API', () => { await seedSettings(app, headers) const payload = JSON.stringify({ eventId: 'evt-bad-token' }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2922,7 +2940,7 @@ describe('Quota Store API', () => { await seedSettings(app, headers) const payload = JSON.stringify({ eventId: 'evt-wrong-purpose' }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2942,7 +2960,7 @@ describe('Quota Store API', () => { const payload = JSON.stringify({ eventId: 'evt-expired-token' }) const now = Math.floor(Date.now() / 1000) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2961,7 +2979,7 @@ describe('Quota Store API', () => { await seedSettings(app, headers) const payload = JSON.stringify({ eventId: 'evt-missing-issued-at' }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -2981,7 +2999,7 @@ describe('Quota Store API', () => { const payload = JSON.stringify({ eventId: 'evt-future-issued-at' }) const issuedAt = Math.floor(Date.now() / 1000) + 60 - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3001,7 +3019,7 @@ describe('Quota Store API', () => { const payload = JSON.stringify({ eventId: 'evt-overlong-token' }) const issuedAt = Math.floor(Date.now() / 1000) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3020,7 +3038,7 @@ describe('Quota Store API', () => { await seedSettings(app, headers) const payload = JSON.stringify({ eventId: 'evt-wrong-hash' }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3048,7 +3066,7 @@ describe('Quota Store API', () => { source: 'stripe', }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3067,7 +3085,7 @@ describe('Quota Store API', () => { await seedSettings(app, headers) const payload = JSON.stringify({ eventId: 'evt-wrong-audience' }) - const res = await app.request('/api/store/webhooks/cloud', { + const res = await app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3141,7 +3159,7 @@ async function seedSettingsRow(db: Awaited>['db } async function postWebhook(app: Awaited>['app'], payload: string) { - return app.request('/api/store/webhooks/cloud', { + return app.request('/api/store/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -3156,7 +3174,7 @@ async function signedWebhookHeaders(payload: string, overrides: Record() { items: [{ productId: body.packageId, priceId: price.id }], currency, + deliveryCallbackUrl: `${getInstanceOrigin(c)}/api/store/webhook`, target: { orgId: targetOrgId, endUserId: targetOrgId, diff --git a/server/routes/cloud-store/webhooks.ts b/server/routes/cloud-store/webhooks.ts index 702398c0..49c62f10 100644 --- a/server/routes/cloud-store/webhooks.ts +++ b/server/routes/cloud-store/webhooks.ts @@ -6,7 +6,7 @@ import { requireFeature } from '../../middleware/require-feature' import { getCloudStoreBinding, getRequiredSettings, processCloudOrderQuotaChange } from '../../services/cloud-store' import { getCloudBaseUrl, parseJson, sha256Hex } from '../cloud-store-helpers' -export const cloudStoreWebhooks = new Hono().use(requireFeature('quota_store')).post('/cloud', async (c) => { +export const cloudStoreWebhooks = new Hono().use(requireFeature('quota_store')).post('/webhook', async (c) => { const db = c.get('platform').db await getRequiredSettings(db) const binding = await getCloudStoreBinding(db)