From d6a61d5d518654e5e1579a3d92513ad3a3112e09 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 19 May 2026 11:06:25 -0400 Subject: [PATCH 1/5] feat(webhooks)!: Add webhook endpoint CRUD The Webhooks client previously only handled signature verification. Add list/create/update/delete operations for webhook endpoints, generated via oagen against the WorkOS API spec. The constructor now takes the WorkOS client instead of a CryptoProvider so generated methods can issue HTTP calls; the SignatureProvider is lazily constructed from workos.getCryptoProvider() to preserve existing verifyHeader/constructEvent behavior. BREAKING CHANGE: Webhooks constructor now takes a WorkOS instance instead of a CryptoProvider. Callers using workos.webhooks or workos.createWebhookClient() are unaffected; only code that instantiated `new Webhooks(...)` directly needs updating. --- .oagen-manifest.json | 26 ++++ src/index.ts | 2 +- src/index.worker.ts | 4 +- .../fixtures/create-webhook-endpoint.json | 4 + .../fixtures/list-webhook-endpoint.json | 18 +++ .../fixtures/update-webhook-endpoint.json | 5 + src/webhooks/fixtures/webhook-endpoint.json | 10 ++ ...reate-webhook-endpoint-events.interface.ts | 87 +++++++++++++ .../create-webhook-endpoint.interface.ts | 15 +++ src/webhooks/interfaces/index.ts | 9 ++ ...pdate-webhook-endpoint-events.interface.ts | 87 +++++++++++++ ...pdate-webhook-endpoint-status.interface.ts | 9 ++ .../update-webhook-endpoint.interface.ts | 19 +++ .../webhook-endpoint-status.interface.ts | 9 ++ .../interfaces/webhook-endpoint.interface.ts | 33 +++++ .../create-webhook-endpoint.serializer.ts | 20 +++ src/webhooks/serializers/index.ts | 5 + .../update-webhook-endpoint.serializer.ts | 22 ++++ .../webhook-endpoint.serializer.ts | 32 +++++ src/webhooks/webhooks.spec.ts | 101 +++++++++++++- src/webhooks/webhooks.ts | 123 +++++++++++++++++- src/workos.ts | 2 +- 22 files changed, 630 insertions(+), 12 deletions(-) create mode 100644 .oagen-manifest.json create mode 100644 src/webhooks/fixtures/create-webhook-endpoint.json create mode 100644 src/webhooks/fixtures/list-webhook-endpoint.json create mode 100644 src/webhooks/fixtures/update-webhook-endpoint.json create mode 100644 src/webhooks/fixtures/webhook-endpoint.json create mode 100644 src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts create mode 100644 src/webhooks/interfaces/create-webhook-endpoint.interface.ts create mode 100644 src/webhooks/interfaces/index.ts create mode 100644 src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts create mode 100644 src/webhooks/interfaces/update-webhook-endpoint-status.interface.ts create mode 100644 src/webhooks/interfaces/update-webhook-endpoint.interface.ts create mode 100644 src/webhooks/interfaces/webhook-endpoint-status.interface.ts create mode 100644 src/webhooks/interfaces/webhook-endpoint.interface.ts create mode 100644 src/webhooks/serializers/create-webhook-endpoint.serializer.ts create mode 100644 src/webhooks/serializers/index.ts create mode 100644 src/webhooks/serializers/update-webhook-endpoint.serializer.ts create mode 100644 src/webhooks/serializers/webhook-endpoint.serializer.ts diff --git a/.oagen-manifest.json b/.oagen-manifest.json new file mode 100644 index 000000000..5c0cc2583 --- /dev/null +++ b/.oagen-manifest.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "language": "node", + "generatedAt": "2026-05-19T13:10:49.990Z", + "files": [ + "src/webhooks/fixtures/create-webhook-endpoint.json", + "src/webhooks/fixtures/list-webhook-endpoint.json", + "src/webhooks/fixtures/update-webhook-endpoint.json", + "src/webhooks/fixtures/webhook-endpoint.json", + "src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts", + "src/webhooks/interfaces/create-webhook-endpoint.interface.ts", + "src/webhooks/interfaces/index.ts", + "src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts", + "src/webhooks/interfaces/update-webhook-endpoint-status.interface.ts", + "src/webhooks/interfaces/update-webhook-endpoint.interface.ts", + "src/webhooks/interfaces/webhook-endpoint-status.interface.ts", + "src/webhooks/interfaces/webhook-endpoint.interface.ts", + "src/webhooks/serializers/create-webhook-endpoint.serializer.ts", + "src/webhooks/serializers/index.ts", + "src/webhooks/serializers/update-webhook-endpoint.serializer.ts", + "src/webhooks/serializers/webhook-endpoint.serializer.ts", + "src/webhooks/webhooks.spec.ts", + "src/webhooks/webhooks.ts" + ], + "operations": {} +} diff --git a/src/index.ts b/src/index.ts index 8733e8a0d..459f54c4b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -74,7 +74,7 @@ class WorkOSNode extends WorkOS { /** @override */ createWebhookClient(): Webhooks { - return new Webhooks(this.getCryptoProvider()); + return new Webhooks(this); } override getCryptoProvider(): CryptoProvider { diff --git a/src/index.worker.ts b/src/index.worker.ts index 5ac0d6094..08f482cf5 100644 --- a/src/index.worker.ts +++ b/src/index.worker.ts @@ -62,9 +62,7 @@ class WorkOSWorker extends WorkOS { /** @override */ createWebhookClient(): Webhooks { - const cryptoProvider = new SubtleCryptoProvider(); - - return new Webhooks(cryptoProvider); + return new Webhooks(this); } override getCryptoProvider(): CryptoProvider { diff --git a/src/webhooks/fixtures/create-webhook-endpoint.json b/src/webhooks/fixtures/create-webhook-endpoint.json new file mode 100644 index 000000000..81a07af09 --- /dev/null +++ b/src/webhooks/fixtures/create-webhook-endpoint.json @@ -0,0 +1,4 @@ +{ + "endpoint_url": "https://example.com/webhooks", + "events": ["user.created", "dsync.user.created"] +} diff --git a/src/webhooks/fixtures/list-webhook-endpoint.json b/src/webhooks/fixtures/list-webhook-endpoint.json new file mode 100644 index 000000000..e4587c5c1 --- /dev/null +++ b/src/webhooks/fixtures/list-webhook-endpoint.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "enabled", + "events": ["user.created", "dsync.user.created"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/src/webhooks/fixtures/update-webhook-endpoint.json b/src/webhooks/fixtures/update-webhook-endpoint.json new file mode 100644 index 000000000..e73642781 --- /dev/null +++ b/src/webhooks/fixtures/update-webhook-endpoint.json @@ -0,0 +1,5 @@ +{ + "endpoint_url": "https://example.com/webhooks", + "status": "enabled", + "events": ["user.created", "dsync.user.created"] +} diff --git a/src/webhooks/fixtures/webhook-endpoint.json b/src/webhooks/fixtures/webhook-endpoint.json new file mode 100644 index 000000000..592c7cd23 --- /dev/null +++ b/src/webhooks/fixtures/webhook-endpoint.json @@ -0,0 +1,10 @@ +{ + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "enabled", + "events": ["user.created", "dsync.user.created"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts b/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts new file mode 100644 index 000000000..1127d7544 --- /dev/null +++ b/src/webhooks/interfaces/create-webhook-endpoint-events.interface.ts @@ -0,0 +1,87 @@ +// This file is auto-generated by oagen. Do not edit. + +export const CreateWebhookEndpointEvents = { + AuthenticationEmailVerificationSucceeded: + 'authentication.email_verification_succeeded', + AuthenticationMagicAuthFailed: 'authentication.magic_auth_failed', + AuthenticationMagicAuthSucceeded: 'authentication.magic_auth_succeeded', + AuthenticationMfaSucceeded: 'authentication.mfa_succeeded', + AuthenticationOAuthFailed: 'authentication.oauth_failed', + AuthenticationOAuthSucceeded: 'authentication.oauth_succeeded', + AuthenticationPasswordFailed: 'authentication.password_failed', + AuthenticationPasswordSucceeded: 'authentication.password_succeeded', + AuthenticationPasskeyFailed: 'authentication.passkey_failed', + AuthenticationPasskeySucceeded: 'authentication.passkey_succeeded', + AuthenticationSSOFailed: 'authentication.sso_failed', + AuthenticationSSOStarted: 'authentication.sso_started', + AuthenticationSSOSucceeded: 'authentication.sso_succeeded', + AuthenticationSSOTimedOut: 'authentication.sso_timed_out', + AuthenticationRadarRiskDetected: 'authentication.radar_risk_detected', + ApiKeyCreated: 'api_key.created', + ApiKeyRevoked: 'api_key.revoked', + ConnectionActivated: 'connection.activated', + ConnectionDeactivated: 'connection.deactivated', + ConnectionSAMLCertificateRenewalRequired: + 'connection.saml_certificate_renewal_required', + ConnectionSAMLCertificateRenewed: 'connection.saml_certificate_renewed', + ConnectionDeleted: 'connection.deleted', + DsyncActivated: 'dsync.activated', + DsyncDeleted: 'dsync.deleted', + DsyncGroupCreated: 'dsync.group.created', + DsyncGroupDeleted: 'dsync.group.deleted', + DsyncGroupUpdated: 'dsync.group.updated', + DsyncGroupUserAdded: 'dsync.group.user_added', + DsyncGroupUserRemoved: 'dsync.group.user_removed', + DsyncUserCreated: 'dsync.user.created', + DsyncUserDeleted: 'dsync.user.deleted', + DsyncUserUpdated: 'dsync.user.updated', + EmailVerificationCreated: 'email_verification.created', + GroupCreated: 'group.created', + GroupDeleted: 'group.deleted', + GroupMemberAdded: 'group.member_added', + GroupMemberRemoved: 'group.member_removed', + GroupUpdated: 'group.updated', + FlagCreated: 'flag.created', + FlagDeleted: 'flag.deleted', + FlagUpdated: 'flag.updated', + FlagRuleUpdated: 'flag.rule_updated', + InvitationAccepted: 'invitation.accepted', + InvitationCreated: 'invitation.created', + InvitationResent: 'invitation.resent', + InvitationRevoked: 'invitation.revoked', + MagicAuthCreated: 'magic_auth.created', + OrganizationCreated: 'organization.created', + OrganizationDeleted: 'organization.deleted', + OrganizationUpdated: 'organization.updated', + OrganizationDomainCreated: 'organization_domain.created', + OrganizationDomainDeleted: 'organization_domain.deleted', + OrganizationDomainUpdated: 'organization_domain.updated', + OrganizationDomainVerified: 'organization_domain.verified', + OrganizationDomainVerificationFailed: + 'organization_domain.verification_failed', + PasswordResetCreated: 'password_reset.created', + PasswordResetSucceeded: 'password_reset.succeeded', + UserCreated: 'user.created', + UserUpdated: 'user.updated', + UserDeleted: 'user.deleted', + OrganizationMembershipCreated: 'organization_membership.created', + OrganizationMembershipDeleted: 'organization_membership.deleted', + OrganizationMembershipUpdated: 'organization_membership.updated', + RoleCreated: 'role.created', + RoleDeleted: 'role.deleted', + RoleUpdated: 'role.updated', + OrganizationRoleCreated: 'organization_role.created', + OrganizationRoleDeleted: 'organization_role.deleted', + OrganizationRoleUpdated: 'organization_role.updated', + PermissionCreated: 'permission.created', + PermissionDeleted: 'permission.deleted', + PermissionUpdated: 'permission.updated', + SessionCreated: 'session.created', + SessionRevoked: 'session.revoked', + WaitlistUserApproved: 'waitlist_user.approved', + WaitlistUserCreated: 'waitlist_user.created', + WaitlistUserDenied: 'waitlist_user.denied', +} as const; + +export type CreateWebhookEndpointEvents = + (typeof CreateWebhookEndpointEvents)[keyof typeof CreateWebhookEndpointEvents]; diff --git a/src/webhooks/interfaces/create-webhook-endpoint.interface.ts b/src/webhooks/interfaces/create-webhook-endpoint.interface.ts new file mode 100644 index 000000000..0f8cb0742 --- /dev/null +++ b/src/webhooks/interfaces/create-webhook-endpoint.interface.ts @@ -0,0 +1,15 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { CreateWebhookEndpointEvents } from './create-webhook-endpoint-events.interface'; + +export interface CreateWebhookEndpoint { + /** The HTTPS URL where webhooks will be sent. */ + endpointUrl: string; + /** The events that the Webhook Endpoint is subscribed to. */ + events: CreateWebhookEndpointEvents[]; +} + +export interface CreateWebhookEndpointResponse { + endpoint_url: string; + events: CreateWebhookEndpointEvents[]; +} diff --git a/src/webhooks/interfaces/index.ts b/src/webhooks/interfaces/index.ts new file mode 100644 index 000000000..34216c628 --- /dev/null +++ b/src/webhooks/interfaces/index.ts @@ -0,0 +1,9 @@ +// This file is auto-generated by oagen. Do not edit. + +export * from './create-webhook-endpoint-events.interface'; +export * from './create-webhook-endpoint.interface'; +export * from './update-webhook-endpoint-events.interface'; +export * from './update-webhook-endpoint-status.interface'; +export * from './update-webhook-endpoint.interface'; +export * from './webhook-endpoint-status.interface'; +export * from './webhook-endpoint.interface'; diff --git a/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts b/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts new file mode 100644 index 000000000..743bd0c69 --- /dev/null +++ b/src/webhooks/interfaces/update-webhook-endpoint-events.interface.ts @@ -0,0 +1,87 @@ +// This file is auto-generated by oagen. Do not edit. + +export const UpdateWebhookEndpointEvents = { + AuthenticationEmailVerificationSucceeded: + 'authentication.email_verification_succeeded', + AuthenticationMagicAuthFailed: 'authentication.magic_auth_failed', + AuthenticationMagicAuthSucceeded: 'authentication.magic_auth_succeeded', + AuthenticationMfaSucceeded: 'authentication.mfa_succeeded', + AuthenticationOAuthFailed: 'authentication.oauth_failed', + AuthenticationOAuthSucceeded: 'authentication.oauth_succeeded', + AuthenticationPasswordFailed: 'authentication.password_failed', + AuthenticationPasswordSucceeded: 'authentication.password_succeeded', + AuthenticationPasskeyFailed: 'authentication.passkey_failed', + AuthenticationPasskeySucceeded: 'authentication.passkey_succeeded', + AuthenticationSSOFailed: 'authentication.sso_failed', + AuthenticationSSOStarted: 'authentication.sso_started', + AuthenticationSSOSucceeded: 'authentication.sso_succeeded', + AuthenticationSSOTimedOut: 'authentication.sso_timed_out', + AuthenticationRadarRiskDetected: 'authentication.radar_risk_detected', + ApiKeyCreated: 'api_key.created', + ApiKeyRevoked: 'api_key.revoked', + ConnectionActivated: 'connection.activated', + ConnectionDeactivated: 'connection.deactivated', + ConnectionSAMLCertificateRenewalRequired: + 'connection.saml_certificate_renewal_required', + ConnectionSAMLCertificateRenewed: 'connection.saml_certificate_renewed', + ConnectionDeleted: 'connection.deleted', + DsyncActivated: 'dsync.activated', + DsyncDeleted: 'dsync.deleted', + DsyncGroupCreated: 'dsync.group.created', + DsyncGroupDeleted: 'dsync.group.deleted', + DsyncGroupUpdated: 'dsync.group.updated', + DsyncGroupUserAdded: 'dsync.group.user_added', + DsyncGroupUserRemoved: 'dsync.group.user_removed', + DsyncUserCreated: 'dsync.user.created', + DsyncUserDeleted: 'dsync.user.deleted', + DsyncUserUpdated: 'dsync.user.updated', + EmailVerificationCreated: 'email_verification.created', + GroupCreated: 'group.created', + GroupDeleted: 'group.deleted', + GroupMemberAdded: 'group.member_added', + GroupMemberRemoved: 'group.member_removed', + GroupUpdated: 'group.updated', + FlagCreated: 'flag.created', + FlagDeleted: 'flag.deleted', + FlagUpdated: 'flag.updated', + FlagRuleUpdated: 'flag.rule_updated', + InvitationAccepted: 'invitation.accepted', + InvitationCreated: 'invitation.created', + InvitationResent: 'invitation.resent', + InvitationRevoked: 'invitation.revoked', + MagicAuthCreated: 'magic_auth.created', + OrganizationCreated: 'organization.created', + OrganizationDeleted: 'organization.deleted', + OrganizationUpdated: 'organization.updated', + OrganizationDomainCreated: 'organization_domain.created', + OrganizationDomainDeleted: 'organization_domain.deleted', + OrganizationDomainUpdated: 'organization_domain.updated', + OrganizationDomainVerified: 'organization_domain.verified', + OrganizationDomainVerificationFailed: + 'organization_domain.verification_failed', + PasswordResetCreated: 'password_reset.created', + PasswordResetSucceeded: 'password_reset.succeeded', + UserCreated: 'user.created', + UserUpdated: 'user.updated', + UserDeleted: 'user.deleted', + OrganizationMembershipCreated: 'organization_membership.created', + OrganizationMembershipDeleted: 'organization_membership.deleted', + OrganizationMembershipUpdated: 'organization_membership.updated', + RoleCreated: 'role.created', + RoleDeleted: 'role.deleted', + RoleUpdated: 'role.updated', + OrganizationRoleCreated: 'organization_role.created', + OrganizationRoleDeleted: 'organization_role.deleted', + OrganizationRoleUpdated: 'organization_role.updated', + PermissionCreated: 'permission.created', + PermissionDeleted: 'permission.deleted', + PermissionUpdated: 'permission.updated', + SessionCreated: 'session.created', + SessionRevoked: 'session.revoked', + WaitlistUserApproved: 'waitlist_user.approved', + WaitlistUserCreated: 'waitlist_user.created', + WaitlistUserDenied: 'waitlist_user.denied', +} as const; + +export type UpdateWebhookEndpointEvents = + (typeof UpdateWebhookEndpointEvents)[keyof typeof UpdateWebhookEndpointEvents]; diff --git a/src/webhooks/interfaces/update-webhook-endpoint-status.interface.ts b/src/webhooks/interfaces/update-webhook-endpoint-status.interface.ts new file mode 100644 index 000000000..0b9a26e7e --- /dev/null +++ b/src/webhooks/interfaces/update-webhook-endpoint-status.interface.ts @@ -0,0 +1,9 @@ +// This file is auto-generated by oagen. Do not edit. + +export const UpdateWebhookEndpointStatus = { + Enabled: 'enabled', + Disabled: 'disabled', +} as const; + +export type UpdateWebhookEndpointStatus = + (typeof UpdateWebhookEndpointStatus)[keyof typeof UpdateWebhookEndpointStatus]; diff --git a/src/webhooks/interfaces/update-webhook-endpoint.interface.ts b/src/webhooks/interfaces/update-webhook-endpoint.interface.ts new file mode 100644 index 000000000..7702709cb --- /dev/null +++ b/src/webhooks/interfaces/update-webhook-endpoint.interface.ts @@ -0,0 +1,19 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { UpdateWebhookEndpointStatus } from './update-webhook-endpoint-status.interface'; +import type { UpdateWebhookEndpointEvents } from './update-webhook-endpoint-events.interface'; + +export interface UpdateWebhookEndpoint { + /** The HTTPS URL where webhooks will be sent. */ + endpointUrl?: string; + /** Whether the Webhook Endpoint is enabled or disabled. */ + status?: UpdateWebhookEndpointStatus; + /** The events that the Webhook Endpoint is subscribed to. */ + events?: UpdateWebhookEndpointEvents[]; +} + +export interface UpdateWebhookEndpointResponse { + endpoint_url?: string; + status?: UpdateWebhookEndpointStatus; + events?: UpdateWebhookEndpointEvents[]; +} diff --git a/src/webhooks/interfaces/webhook-endpoint-status.interface.ts b/src/webhooks/interfaces/webhook-endpoint-status.interface.ts new file mode 100644 index 000000000..4f974bc15 --- /dev/null +++ b/src/webhooks/interfaces/webhook-endpoint-status.interface.ts @@ -0,0 +1,9 @@ +// This file is auto-generated by oagen. Do not edit. + +export const WebhookEndpointStatus = { + Enabled: 'enabled', + Disabled: 'disabled', +} as const; + +export type WebhookEndpointStatus = + (typeof WebhookEndpointStatus)[keyof typeof WebhookEndpointStatus]; diff --git a/src/webhooks/interfaces/webhook-endpoint.interface.ts b/src/webhooks/interfaces/webhook-endpoint.interface.ts new file mode 100644 index 000000000..d5e661028 --- /dev/null +++ b/src/webhooks/interfaces/webhook-endpoint.interface.ts @@ -0,0 +1,33 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { WebhookEndpointStatus } from './webhook-endpoint-status.interface'; + +export interface WebhookEndpoint { + /** Distinguishes the Webhook Endpoint object. */ + object: 'webhook_endpoint'; + /** Unique identifier of the Webhook Endpoint. */ + id: string; + /** The URL to which webhooks are sent. */ + endpointUrl: string; + /** The secret used to sign webhook payloads. */ + secret: string; + /** Whether the Webhook Endpoint is enabled or disabled. */ + status: WebhookEndpointStatus; + /** The events that the Webhook Endpoint is subscribed to. */ + events: string[]; + /** An ISO 8601 timestamp. */ + createdAt: Date; + /** An ISO 8601 timestamp. */ + updatedAt: Date; +} + +export interface WebhookEndpointResponse { + object: 'webhook_endpoint'; + id: string; + endpoint_url: string; + secret: string; + status: WebhookEndpointStatus; + events: string[]; + created_at: string; + updated_at: string; +} diff --git a/src/webhooks/serializers/create-webhook-endpoint.serializer.ts b/src/webhooks/serializers/create-webhook-endpoint.serializer.ts new file mode 100644 index 000000000..d7fed4fe4 --- /dev/null +++ b/src/webhooks/serializers/create-webhook-endpoint.serializer.ts @@ -0,0 +1,20 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + CreateWebhookEndpoint, + CreateWebhookEndpointResponse, +} from '../interfaces/create-webhook-endpoint.interface'; + +export const deserializeCreateWebhookEndpoint = ( + response: CreateWebhookEndpointResponse, +): CreateWebhookEndpoint => ({ + endpointUrl: response.endpoint_url, + events: response.events, +}); + +export const serializeCreateWebhookEndpoint = ( + model: CreateWebhookEndpoint, +): CreateWebhookEndpointResponse => ({ + endpoint_url: model.endpointUrl, + events: model.events, +}); diff --git a/src/webhooks/serializers/index.ts b/src/webhooks/serializers/index.ts new file mode 100644 index 000000000..a9c11ea91 --- /dev/null +++ b/src/webhooks/serializers/index.ts @@ -0,0 +1,5 @@ +// This file is auto-generated by oagen. Do not edit. + +export * from './create-webhook-endpoint.serializer'; +export * from './update-webhook-endpoint.serializer'; +export * from './webhook-endpoint.serializer'; diff --git a/src/webhooks/serializers/update-webhook-endpoint.serializer.ts b/src/webhooks/serializers/update-webhook-endpoint.serializer.ts new file mode 100644 index 000000000..981692ad8 --- /dev/null +++ b/src/webhooks/serializers/update-webhook-endpoint.serializer.ts @@ -0,0 +1,22 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + UpdateWebhookEndpoint, + UpdateWebhookEndpointResponse, +} from '../interfaces/update-webhook-endpoint.interface'; + +export const deserializeUpdateWebhookEndpoint = ( + response: UpdateWebhookEndpointResponse, +): UpdateWebhookEndpoint => ({ + endpointUrl: response.endpoint_url, + status: response.status, + events: response.events, +}); + +export const serializeUpdateWebhookEndpoint = ( + model: UpdateWebhookEndpoint, +): UpdateWebhookEndpointResponse => ({ + endpoint_url: model.endpointUrl, + status: model.status, + events: model.events, +}); diff --git a/src/webhooks/serializers/webhook-endpoint.serializer.ts b/src/webhooks/serializers/webhook-endpoint.serializer.ts new file mode 100644 index 000000000..d3d834b9b --- /dev/null +++ b/src/webhooks/serializers/webhook-endpoint.serializer.ts @@ -0,0 +1,32 @@ +// This file is auto-generated by oagen. Do not edit. + +import type { + WebhookEndpoint, + WebhookEndpointResponse, +} from '../interfaces/webhook-endpoint.interface'; + +export const deserializeWebhookEndpoint = ( + response: WebhookEndpointResponse, +): WebhookEndpoint => ({ + object: response.object, + id: response.id, + endpointUrl: response.endpoint_url, + secret: response.secret, + status: response.status, + events: response.events, + createdAt: new Date(response.created_at), + updatedAt: new Date(response.updated_at), +}); + +export const serializeWebhookEndpoint = ( + model: WebhookEndpoint, +): WebhookEndpointResponse => ({ + object: model.object, + id: model.id, + endpoint_url: model.endpointUrl, + secret: model.secret, + status: model.status, + events: model.events, + created_at: model.createdAt.toISOString(), + updated_at: model.updatedAt.toISOString(), +}); diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index 2ce5b9efa..9e2de2d53 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -1,10 +1,106 @@ -import crypto from 'crypto'; +// This file is auto-generated by oagen. Do not edit. + +import fetch from 'jest-fetch-mock'; +import { + fetchOnce, + fetchURL, + fetchMethod, + fetchSearchParams, + fetchBody, +} from '../common/utils/test-utils'; import { WorkOS } from '../workos'; + +import listWebhookEndpointFixture from './fixtures/list-webhook-endpoint.json'; +import webhookEndpointFixture from './fixtures/webhook-endpoint.json'; +import crypto from 'crypto'; import mockWebhook from './fixtures/webhook.json'; -const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); import { SignatureVerificationException } from '../common/exceptions'; +const workos = new WorkOS('sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU'); + +function expectWebhookEndpoint(result: any) { + expect(result.object).toBe('webhook_endpoint'); + expect(result.id).toBe('we_0123456789'); + expect(result.endpointUrl).toBe('https://example.com/webhooks'); + expect(result.secret).toBe('whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4'); + expect(result.status).toBe('enabled'); + expect(result.events).toEqual(['user.created', 'dsync.user.created']); + expect(result.createdAt.toISOString()).toBe('2026-01-15T12:00:00.000Z'); + expect(result.updatedAt.toISOString()).toBe('2026-01-15T12:00:00.000Z'); +} + describe('Webhooks', () => { + beforeEach(() => fetch.resetMocks()); + + describe('listWebhookEndpoints', () => { + it('returns paginated results', async () => { + fetchOnce(listWebhookEndpointFixture); + + const { data, listMetadata } = + await workos.webhooks.listWebhookEndpoints(); + + expect(fetchMethod()).toBe('GET'); + expect(new URL(String(fetchURL())).pathname).toBe('/webhook_endpoints'); + expect(fetchSearchParams()).toHaveProperty('order'); + expect(Array.isArray(data)).toBe(true); + expect(listMetadata).toBeDefined(); + expect(data.length).toBeGreaterThan(0); + expectWebhookEndpoint(data[0]); + }); + }); + + describe('createWebhookEndpoint', () => { + it('sends the correct request and returns result', async () => { + fetchOnce(webhookEndpointFixture); + + const result = await workos.webhooks.createWebhookEndpoint({ + endpointUrl: 'https://example.com', + events: ['authentication.email_verification_succeeded'], + }); + + expect(fetchMethod()).toBe('POST'); + expect(new URL(String(fetchURL())).pathname).toBe('/webhook_endpoints'); + expect(fetchBody()).toEqual( + expect.objectContaining({ + endpoint_url: 'https://example.com', + events: ['authentication.email_verification_succeeded'], + }), + ); + expectWebhookEndpoint(result); + }); + }); + + describe('updateWebhookEndpoint', () => { + it('sends the correct request and returns result', async () => { + fetchOnce(webhookEndpointFixture); + + const result = await workos.webhooks.updateWebhookEndpoint('test_id', {}); + + expect(fetchMethod()).toBe('PATCH'); + expect(new URL(String(fetchURL())).pathname).toBe( + '/webhook_endpoints/test_id', + ); + expect(fetchBody()).toBeDefined(); + expectWebhookEndpoint(result); + }); + }); + + describe('deleteWebhookEndpoint', () => { + it('sends a DELETE request', async () => { + fetchOnce({}, { status: 204 }); + + await workos.webhooks.deleteWebhookEndpoint('test_id'); + + expect(fetchMethod()).toBe('DELETE'); + expect(new URL(String(fetchURL())).pathname).toBe( + '/webhook_endpoints/test_id', + ); + }); + }); +}); + +// @oagen-ignore-start +describe('Webhook signatures', () => { let payload: any; let secret: string; let timestamp: number; @@ -211,3 +307,4 @@ describe('Webhooks', () => { }); }); }); +// @oagen-ignore-end diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 3fcff8277..7231256fa 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -1,14 +1,126 @@ -// @oagen-ignore-file +// This file is auto-generated by oagen. Do not edit. + +import type { WorkOS } from '../workos'; +import type { PaginationOptions } from '../common/interfaces/pagination-options.interface'; +import { AutoPaginatable } from '../common/utils/pagination'; +import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize'; +import type { + WebhookEndpoint, + WebhookEndpointResponse, +} from './interfaces/webhook-endpoint.interface'; +import type { + CreateWebhookEndpoint, + CreateWebhookEndpointResponse, +} from './interfaces/create-webhook-endpoint.interface'; +import type { + UpdateWebhookEndpoint, + UpdateWebhookEndpointResponse, +} from './interfaces/update-webhook-endpoint.interface'; +import { deserializeWebhookEndpoint } from './serializers/webhook-endpoint.serializer'; +import { serializeCreateWebhookEndpoint } from './serializers/create-webhook-endpoint.serializer'; +import { serializeUpdateWebhookEndpoint } from './serializers/update-webhook-endpoint.serializer'; import { deserializeEvent } from '../common/serializers'; import { Event, EventResponse } from '../common/interfaces'; import { SignatureProvider } from '../common/crypto/signature-provider'; -import { CryptoProvider } from '../common/crypto/crypto-provider'; export class Webhooks { - private signatureProvider: SignatureProvider; + constructor(private readonly workos: WorkOS) {} + + /** + * List Webhook Endpoints + * + * Get a list of all of your existing webhook endpoints. + * @param options - Pagination and filter options. + * @returns {Promise>} + */ + async listWebhookEndpoints( + options?: PaginationOptions, + ): Promise> { + return new AutoPaginatable( + await fetchAndDeserialize( + this.workos, + '/webhook_endpoints', + deserializeWebhookEndpoint, + options, + ), + (params) => + fetchAndDeserialize( + this.workos, + '/webhook_endpoints', + deserializeWebhookEndpoint, + params, + ), + options, + ); + } + + /** + * Create a Webhook Endpoint + * + * Create a new webhook endpoint to receive event notifications. + * @param payload - Object containing endpointUrl, events. + * @returns {Promise} + * @throws {ConflictException} 409 + * @throws {UnprocessableEntityException} 422 + */ + async createWebhookEndpoint( + payload: CreateWebhookEndpoint, + ): Promise { + const { data } = await this.workos.post< + WebhookEndpointResponse, + CreateWebhookEndpointResponse + >('/webhook_endpoints', serializeCreateWebhookEndpoint(payload)); + return deserializeWebhookEndpoint(data); + } + + /** + * Update a Webhook Endpoint + * + * Update the properties of an existing webhook endpoint. + * @param id - Unique identifier of the Webhook Endpoint. + * @example "we_0123456789" + * @param payload - The request body. + * @returns {Promise} + * @throws {NotFoundException} 404 + * @throws {ConflictException} 409 + * @throws {UnprocessableEntityException} 422 + */ + async updateWebhookEndpoint( + id: string, + payload: UpdateWebhookEndpoint, + ): Promise { + const { data } = await this.workos.patch< + WebhookEndpointResponse, + UpdateWebhookEndpointResponse + >( + `/webhook_endpoints/${encodeURIComponent(id)}`, + serializeUpdateWebhookEndpoint(payload), + ); + return deserializeWebhookEndpoint(data); + } + + /** + * Delete a Webhook Endpoint + * + * Delete an existing webhook endpoint. + * @param id - Unique identifier of the Webhook Endpoint. + * @example "we_0123456789" + * @returns {Promise} + * @throws {NotFoundException} 404 + */ + async deleteWebhookEndpoint(id: string): Promise { + await this.workos.delete(`/webhook_endpoints/${encodeURIComponent(id)}`); + } - constructor(cryptoProvider: CryptoProvider) { - this.signatureProvider = new SignatureProvider(cryptoProvider); + // @oagen-ignore-start + private _signatureProvider?: SignatureProvider; + private get signatureProvider(): SignatureProvider { + if (!this._signatureProvider) { + this._signatureProvider = new SignatureProvider( + this.workos.getCryptoProvider(), + ); + } + return this._signatureProvider; } get verifyHeader() { @@ -43,4 +155,5 @@ export class Webhooks { return deserializeEvent(webhookPayload); } + // @oagen-ignore-end } diff --git a/src/workos.ts b/src/workos.ts index d2ab92f17..77b932c32 100644 --- a/src/workos.ts +++ b/src/workos.ts @@ -178,7 +178,7 @@ export class WorkOS { } createWebhookClient() { - return new Webhooks(this.getCryptoProvider()); + return new Webhooks(this); } createActionsClient() { From 446c5290485ab81c48428d4f201b1b8d3512c298 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 19 May 2026 12:27:41 -0400 Subject: [PATCH 2/5] chore(webhooks): Regenerate and tighten update test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The create/update deserialize helpers were unused — only the request serializers and the response deserializer for the endpoint itself are needed. Removing them aligns the surface area with what oagen now emits. The update endpoint test only checked that a body was sent, which would have missed regressions in field casing or payload shape. Asserting the exact request body catches those. --- .oagen-manifest.json | 3 +- src/webhooks/serializers.spec.ts | 37 +++++++++++++++++++ .../create-webhook-endpoint.serializer.ts | 7 ---- .../update-webhook-endpoint.serializer.ts | 8 ---- src/webhooks/webhooks.spec.ts | 12 +++++- src/webhooks/webhooks.ts | 2 +- 6 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 src/webhooks/serializers.spec.ts diff --git a/.oagen-manifest.json b/.oagen-manifest.json index 5c0cc2583..f8a5578ef 100644 --- a/.oagen-manifest.json +++ b/.oagen-manifest.json @@ -1,7 +1,7 @@ { "version": 2, "language": "node", - "generatedAt": "2026-05-19T13:10:49.990Z", + "generatedAt": "2026-05-19T16:22:01.987Z", "files": [ "src/webhooks/fixtures/create-webhook-endpoint.json", "src/webhooks/fixtures/list-webhook-endpoint.json", @@ -15,6 +15,7 @@ "src/webhooks/interfaces/update-webhook-endpoint.interface.ts", "src/webhooks/interfaces/webhook-endpoint-status.interface.ts", "src/webhooks/interfaces/webhook-endpoint.interface.ts", + "src/webhooks/serializers.spec.ts", "src/webhooks/serializers/create-webhook-endpoint.serializer.ts", "src/webhooks/serializers/index.ts", "src/webhooks/serializers/update-webhook-endpoint.serializer.ts", diff --git a/src/webhooks/serializers.spec.ts b/src/webhooks/serializers.spec.ts new file mode 100644 index 000000000..2daca5f9f --- /dev/null +++ b/src/webhooks/serializers.spec.ts @@ -0,0 +1,37 @@ +// This file is auto-generated by oagen. Do not edit. + +import { serializeCreateWebhookEndpoint } from './serializers/create-webhook-endpoint.serializer'; +import { serializeUpdateWebhookEndpoint } from './serializers/update-webhook-endpoint.serializer'; +import { deserializeWebhookEndpoint } from './serializers/webhook-endpoint.serializer'; +import type { CreateWebhookEndpointResponse } from './interfaces/create-webhook-endpoint.interface'; +import type { UpdateWebhookEndpointResponse } from './interfaces/update-webhook-endpoint.interface'; +import type { WebhookEndpointResponse } from './interfaces/webhook-endpoint.interface'; +import createWebhookEndpointFixture from './fixtures/create-webhook-endpoint.json'; +import updateWebhookEndpointFixture from './fixtures/update-webhook-endpoint.json'; +import webhookEndpointFixture from './fixtures/webhook-endpoint.json'; + +describe('CreateWebhookEndpointSerializer', () => { + it('serializes correctly', () => { + const fixture = + createWebhookEndpointFixture as CreateWebhookEndpointResponse; + const serialized = serializeCreateWebhookEndpoint(fixture as any); + expect(serialized).toBeDefined(); + }); +}); + +describe('UpdateWebhookEndpointSerializer', () => { + it('serializes correctly', () => { + const fixture = + updateWebhookEndpointFixture as UpdateWebhookEndpointResponse; + const serialized = serializeUpdateWebhookEndpoint(fixture as any); + expect(serialized).toBeDefined(); + }); +}); + +describe('WebhookEndpointSerializer', () => { + it('deserializes correctly', () => { + const fixture = webhookEndpointFixture as WebhookEndpointResponse; + const deserialized = deserializeWebhookEndpoint(fixture); + expect(deserialized).toBeDefined(); + }); +}); diff --git a/src/webhooks/serializers/create-webhook-endpoint.serializer.ts b/src/webhooks/serializers/create-webhook-endpoint.serializer.ts index d7fed4fe4..791d5af91 100644 --- a/src/webhooks/serializers/create-webhook-endpoint.serializer.ts +++ b/src/webhooks/serializers/create-webhook-endpoint.serializer.ts @@ -5,13 +5,6 @@ import type { CreateWebhookEndpointResponse, } from '../interfaces/create-webhook-endpoint.interface'; -export const deserializeCreateWebhookEndpoint = ( - response: CreateWebhookEndpointResponse, -): CreateWebhookEndpoint => ({ - endpointUrl: response.endpoint_url, - events: response.events, -}); - export const serializeCreateWebhookEndpoint = ( model: CreateWebhookEndpoint, ): CreateWebhookEndpointResponse => ({ diff --git a/src/webhooks/serializers/update-webhook-endpoint.serializer.ts b/src/webhooks/serializers/update-webhook-endpoint.serializer.ts index 981692ad8..1feaa9448 100644 --- a/src/webhooks/serializers/update-webhook-endpoint.serializer.ts +++ b/src/webhooks/serializers/update-webhook-endpoint.serializer.ts @@ -5,14 +5,6 @@ import type { UpdateWebhookEndpointResponse, } from '../interfaces/update-webhook-endpoint.interface'; -export const deserializeUpdateWebhookEndpoint = ( - response: UpdateWebhookEndpointResponse, -): UpdateWebhookEndpoint => ({ - endpointUrl: response.endpoint_url, - status: response.status, - events: response.events, -}); - export const serializeUpdateWebhookEndpoint = ( model: UpdateWebhookEndpoint, ): UpdateWebhookEndpointResponse => ({ diff --git a/src/webhooks/webhooks.spec.ts b/src/webhooks/webhooks.spec.ts index 9e2de2d53..e4ccb62cd 100644 --- a/src/webhooks/webhooks.spec.ts +++ b/src/webhooks/webhooks.spec.ts @@ -74,13 +74,21 @@ describe('Webhooks', () => { it('sends the correct request and returns result', async () => { fetchOnce(webhookEndpointFixture); - const result = await workos.webhooks.updateWebhookEndpoint('test_id', {}); + const result = await workos.webhooks.updateWebhookEndpoint('test_id', { + endpointUrl: 'https://example.com', + status: 'enabled', + }); expect(fetchMethod()).toBe('PATCH'); expect(new URL(String(fetchURL())).pathname).toBe( '/webhook_endpoints/test_id', ); - expect(fetchBody()).toBeDefined(); + expect(fetchBody()).toEqual( + expect.objectContaining({ + endpoint_url: 'https://example.com', + status: 'enabled', + }), + ); expectWebhookEndpoint(result); }); }); diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 7231256fa..26ed58f64 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -31,7 +31,7 @@ export class Webhooks { * * Get a list of all of your existing webhook endpoints. * @param options - Pagination and filter options. - * @returns {Promise>} + * @returns {Promise>} */ async listWebhookEndpoints( options?: PaginationOptions, From a009edd2d35c1d655b8e3cc2034a6b183fcb51f9 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 20 May 2026 12:42:08 -0400 Subject: [PATCH 3/5] pretty --- src/webhooks/webhooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 072298614..0f5415cbb 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -39,7 +39,7 @@ function parseVerifiedPayload(payload: WebhookPayload): EventResponse { } export class Webhooks { - constructor(private readonly workos: WorkOS) { } + constructor(private readonly workos: WorkOS) {} /** * List Webhook Endpoints From bf9305fabe9ef5f0cf0f488852083a5bf14278c6 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 20 May 2026 12:53:19 -0400 Subject: [PATCH 4/5] remove unused import --- src/webhooks/webhooks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 0f5415cbb..3a1b45778 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -22,7 +22,6 @@ import { serializeUpdateWebhookEndpoint } from './serializers/update-webhook-end import { deserializeEvent } from '../common/serializers'; import { Event, EventResponse } from '../common/interfaces'; import { SignatureProvider } from '../common/crypto/signature-provider'; -import { CryptoProvider } from '../common/crypto/crypto-provider'; import { type WebhookPayload, decodePayloadToString, From 8325c3105a637a08432df31ea6f55a8a41c5f2bd Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 20 May 2026 13:03:49 -0400 Subject: [PATCH 5/5] protect this --- src/webhooks/webhooks.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/webhooks/webhooks.ts b/src/webhooks/webhooks.ts index 3a1b45778..e530ccce1 100644 --- a/src/webhooks/webhooks.ts +++ b/src/webhooks/webhooks.ts @@ -28,15 +28,6 @@ import { isBinaryPayload, } from '../common/crypto/decode-payload'; -// Parse only after verification succeeds — a malformed body never reaches -// JSON.parse on an unauthenticated request. -function parseVerifiedPayload(payload: WebhookPayload): EventResponse { - if (typeof payload === 'object' && !isBinaryPayload(payload)) { - return payload as unknown as EventResponse; - } - return JSON.parse(decodePayloadToString(payload)) as EventResponse; -} - export class Webhooks { constructor(private readonly workos: WorkOS) {} @@ -165,9 +156,17 @@ export class Webhooks { const options = { payload, sigHeader, secret, tolerance }; await this.verifyHeader(options); - const webhookPayload = parseVerifiedPayload(payload); + const webhookPayload = this.parseVerifiedPayload(payload); return deserializeEvent(webhookPayload); } + // Parse only after verification succeeds — a malformed body never reaches + // JSON.parse on an unauthenticated request. + private parseVerifiedPayload(payload: WebhookPayload): EventResponse { + if (typeof payload === 'object' && !isBinaryPayload(payload)) { + return payload as unknown as EventResponse; + } + return JSON.parse(decodePayloadToString(payload)) as EventResponse; + } // @oagen-ignore-end }