Skip to content

Commit 3fa609c

Browse files
tonyxiaoclaude
andcommitted
fix(service): use mountWebhookRoutes to avoid double-slash in OpenAPI spec
app.route('', subApp) in OpenAPIHono prefixes all sub-app paths with an empty string, producing "//webhooks/{credential_id}" in the generated spec. Switch to mountWebhookRoutes(app, push_event) which registers the route directly on the parent app. Regenerate docs/openapi/service.json accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Committed-By-Agent: claude
1 parent b4e46e1 commit 3fa609c

3 files changed

Lines changed: 47 additions & 39 deletions

File tree

apps/service/src/api/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
fileStateStore,
2424
fileLogSink,
2525
} from '../lib/stores-fs.js'
26-
import { createWebhookApp } from './webhook-app.js'
26+
import { mountWebhookRoutes } from './webhook-app.js'
2727

2828
// MARK: - Helpers
2929

@@ -796,7 +796,7 @@ export function createApp(options?: AppOptions) {
796796

797797
// MARK: - Webhook ingress (mounted from webhook-app.ts)
798798

799-
app.route('', createWebhookApp({ push_event: (id, e) => service.push_event(id, e) }))
799+
mountWebhookRoutes(app, (id, e) => service.push_event(id, e))
800800

801801
// MARK: - OpenAPI spec + Swagger UI
802802

apps/service/src/api/webhook-app.ts

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,47 @@ export interface WebhookAppOptions {
55
push_event: (credentialId: string, event: unknown) => void
66
}
77

8+
const WebhookParam = z.object({
9+
credential_id: z.string().openapi({
10+
param: { name: 'credential_id', in: 'path' },
11+
example: 'cred_abc123',
12+
}),
13+
})
14+
15+
const webhookRoute = createRoute({
16+
operationId: 'pushWebhook',
17+
method: 'post',
18+
path: '/webhooks/{credential_id}',
19+
tags: ['Webhooks'],
20+
summary: 'Ingest a Stripe webhook event',
21+
description:
22+
"Receives a raw Stripe webhook event, verifies its signature using the credential's webhook secret, and enqueues it for processing by the active sync.",
23+
request: { params: WebhookParam },
24+
responses: {
25+
200: {
26+
content: { 'text/plain': { schema: z.literal('ok') } },
27+
description: 'Event accepted',
28+
},
29+
},
30+
})
31+
32+
/**
33+
* Register POST /webhooks/{credential_id} directly on any OpenAPIHono app.
34+
* Used by both `createApp` (single-process) and `createWebhookApp` (standalone).
35+
*/
36+
export function mountWebhookRoutes(
37+
app: OpenAPIHono,
38+
push_event: (credentialId: string, event: unknown) => void
39+
) {
40+
app.openapi(webhookRoute, async (c) => {
41+
const { credential_id } = c.req.valid('param')
42+
const body = await c.req.text()
43+
const headers = Object.fromEntries(c.req.raw.headers.entries())
44+
push_event(credential_id, { body, headers })
45+
return c.text('ok', 200)
46+
})
47+
}
48+
849
/**
950
* Standalone webhook ingress app — POST /webhooks/{credential_id}.
1051
*
@@ -13,7 +54,7 @@ export interface WebhookAppOptions {
1354
* signals the matching workflow(s) via TemporalBridge.
1455
*
1556
* Used in two ways:
16-
* 1. Mounted inside the main service app for single-process dev.
57+
* 1. Mounted inside the main service app via `mountWebhookRoutes` for single-process dev.
1758
* 2. As a standalone server via `sync-service webhook` for production
1859
* deployments where webhook ingress runs on its own port/host.
1960
*/
@@ -25,39 +66,6 @@ export function createWebhookApp({ push_event }: WebhookAppOptions) {
2566
}
2667
},
2768
})
28-
29-
const WebhookParam = z.object({
30-
credential_id: z.string().openapi({
31-
param: { name: 'credential_id', in: 'path' },
32-
example: 'cred_abc123',
33-
}),
34-
})
35-
36-
app.openapi(
37-
createRoute({
38-
operationId: 'pushWebhook',
39-
method: 'post',
40-
path: '/webhooks/{credential_id}',
41-
tags: ['Webhooks'],
42-
summary: 'Ingest a Stripe webhook event',
43-
description:
44-
"Receives a raw Stripe webhook event, verifies its signature using the credential's webhook secret, and enqueues it for processing by the active sync.",
45-
request: { params: WebhookParam },
46-
responses: {
47-
200: {
48-
content: { 'text/plain': { schema: z.literal('ok') } },
49-
description: 'Event accepted',
50-
},
51-
},
52-
}),
53-
async (c) => {
54-
const { credential_id } = c.req.valid('param')
55-
const body = await c.req.text()
56-
const headers = Object.fromEntries(c.req.raw.headers.entries())
57-
push_event(credential_id, { body, headers })
58-
return c.text('ok', 200)
59-
}
60-
)
61-
69+
mountWebhookRoutes(app, push_event)
6270
return app
6371
}

docs/openapi/service.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"info": {
44
"title": "Stripe Sync Service",
55
"version": "1.0.0",
6-
"description": "Stripe Sync Service — manage credentials, syncs, and webhook ingress.\n\n## Endpoints\n\n| Method | Path | Summary |\n|--------|------|---------|\n| GET | /health | Health check |\n| GET | /credentials | List credentials |\n| POST | /credentials | Create credential |\n| GET | /credentials/{id} | Retrieve credential |\n| PATCH | /credentials/{id} | Update credential |\n| DELETE | /credentials/{id} | Delete credential |\n| GET | /syncs | List syncs |\n| POST | /syncs | Create sync |\n| GET | /syncs/{id} | Retrieve sync |\n| PATCH | /syncs/{id} | Update sync |\n| DELETE | /syncs/{id} | Delete sync |\n| POST | /syncs/{id}/setup | Set up destination schema for a sync |\n| POST | /syncs/{id}/teardown | Tear down destination schema for a sync |\n| GET | /syncs/{id}/check | Check connector connection for a sync |\n| POST | /syncs/{id}/read | Read records from the sync source |\n| POST | /syncs/{id}/write | Write records to the sync destination |\n| POST | /syncs/{id}/sync | Run sync pipeline (read → write) |\n| POST | /syncs/{id}/pause | Pause a running sync (Temporal mode only) |\n| POST | /syncs/{id}/resume | Resume a paused sync (Temporal mode only) |\n| POST | //webhooks/{credential_id} | Ingest a Stripe webhook event |"
6+
"description": "Stripe Sync Service — manage credentials, syncs, and webhook ingress.\n\n## Endpoints\n\n| Method | Path | Summary |\n|--------|------|---------|\n| GET | /health | Health check |\n| GET | /credentials | List credentials |\n| POST | /credentials | Create credential |\n| GET | /credentials/{id} | Retrieve credential |\n| PATCH | /credentials/{id} | Update credential |\n| DELETE | /credentials/{id} | Delete credential |\n| GET | /syncs | List syncs |\n| POST | /syncs | Create sync |\n| GET | /syncs/{id} | Retrieve sync |\n| PATCH | /syncs/{id} | Update sync |\n| DELETE | /syncs/{id} | Delete sync |\n| POST | /syncs/{id}/setup | Set up destination schema for a sync |\n| POST | /syncs/{id}/teardown | Tear down destination schema for a sync |\n| GET | /syncs/{id}/check | Check connector connection for a sync |\n| POST | /syncs/{id}/read | Read records from the sync source |\n| POST | /syncs/{id}/write | Write records to the sync destination |\n| POST | /syncs/{id}/sync | Run sync pipeline (read → write) |\n| POST | /syncs/{id}/pause | Pause a running sync (Temporal mode only) |\n| POST | /syncs/{id}/resume | Resume a paused sync (Temporal mode only) |\n| POST | /webhooks/{credential_id} | Ingest a Stripe webhook event |"
77
},
88
"components": {
99
"schemas": {},
@@ -1331,7 +1331,7 @@
13311331
}
13321332
}
13331333
},
1334-
"//webhooks/{credential_id}": {
1334+
"/webhooks/{credential_id}": {
13351335
"post": {
13361336
"operationId": "pushWebhook",
13371337
"tags": ["Webhooks"],

0 commit comments

Comments
 (0)