Seeding a webhook endpoint always generates its signing secret and ignores a supplied one, while the HTTP route accepts one. src/workos/index.ts:
ws.webhookEndpoints.insert({
object: 'webhook_endpoint',
endpoint_url: endpointUrl,
secret: randomBytes(32).toString('hex'),
enabled: whConfig.enabled !== false,
events: whConfig.events ?? [],
description: null,
});
versus src/workos/routes/webhook-endpoints.ts:
const secret = (body.secret as string) ?? randomBytes(32).toString('hex');
So a consumer that verifies signatures cannot use the seed file for its endpoint at all: the secret it would need is generated after the emulator starts and is only discoverable by calling GET /webhook_endpoints. That leaves registering over the API as the only workable route, which is what we ended up doing, and it has a second cost with in-memory state: an endpoint registered after boot does not come back when the container restarts, so deliveries stop with nothing to explain why, whereas a seeded one would.
The fix looks like two lines:
- secret: randomBytes(32).toString('hex'),
+ secret: whConfig.secret ?? randomBytes(32).toString('hex'),
plus secret?: string on WorkOSSeedWebhookEndpoint, and optionally a "must be a string if provided" check in config-validator.ts next to the existing events one. Unknown keys are not rejected today, so nothing else needs touching.
Is there a reason the webhook secret should differ from the other seeded credentials? apiKeys[].value is keyConfig.value ?? generated in the same function, and connectApplications[].client_secret is documented as optional and generated if omitted, both on the stated grounds that a service needs known credentials before anything interacts with the emulator. A signing secret seems like the same case, and it is the one seeded value a caller cannot discover without a follow-up request.
Seeding a webhook endpoint always generates its signing secret and ignores a supplied one, while the HTTP route accepts one.
src/workos/index.ts:versus
src/workos/routes/webhook-endpoints.ts:So a consumer that verifies signatures cannot use the seed file for its endpoint at all: the secret it would need is generated after the emulator starts and is only discoverable by calling
GET /webhook_endpoints. That leaves registering over the API as the only workable route, which is what we ended up doing, and it has a second cost with in-memory state: an endpoint registered after boot does not come back when the container restarts, so deliveries stop with nothing to explain why, whereas a seeded one would.The fix looks like two lines:
plus
secret?: stringonWorkOSSeedWebhookEndpoint, and optionally a "must be a string if provided" check inconfig-validator.tsnext to the existingeventsone. Unknown keys are not rejected today, so nothing else needs touching.Is there a reason the webhook secret should differ from the other seeded credentials?
apiKeys[].valueiskeyConfig.value ?? generatedin the same function, andconnectApplications[].client_secretis documented as optional and generated if omitted, both on the stated grounds that a service needs known credentials before anything interacts with the emulator. A signing secret seems like the same case, and it is the one seeded value a caller cannot discover without a follow-up request.