Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions examples/snippets/event_notification_handler_endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ handler.on('v1.billing.meter.error_report_triggered', async (event) => {
console.log(`Billing Meter ${meter.display_name} had a problem`);
});

// Handles events delivered through a channel that has already authenticated them, such as
// AWS EventBridge or Azure Event Grid. Those payloads carry no Stripe-Signature header, so
// this handler skips verification. Callbacks are registered separately from the one above.
const unverifiedHandler = client.notificationHandlerWithoutVerification(
async (unhandledEvent, client, details) => {
console.log(`Received unhandled event type: ${unhandledEvent.type}`);
}
);

unverifiedHandler.on(
'v1.billing.meter.error_report_triggered',
async (event) => {
const meter = await event.fetchRelatedObject();
console.log(`Billing Meter ${meter.display_name} had a problem`);
}
);

app.post(
'/webhook',
express.raw({type: 'application/json'}),
Expand All @@ -40,4 +57,13 @@ app.post(
}
);

app.post(
'/webhook-from-cloud-provider',
express.raw({type: 'application/json'}),
async (req, res) => {
// handle() takes only the body here; there's no signature to check
await unverifiedHandler.handle(req.body);
}
);

app.listen(4242, () => console.log('Running on port 4242'));
78 changes: 64 additions & 14 deletions src/StripeEventNotificationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,23 @@ const KNOWN_EVENT_TYPES = new Set([
// event-types: The end of the section generated from our OpenAPI spec
]);

export class StripeEventNotificationHandler {
/**
* Shared registration and dispatch machinery for the two handlers below.
*
* Deliberately does not declare `handle`, and is not exported. TypeScript won't
* let a subclass add a required parameter to an inherited method, so a verifying
* handler cannot extend a non-verifying one (or vice versa) without either
* loosening a signature or suppressing the error. Making them siblings lets each
* declare its own exact `handle` while sharing everything else.
*/
class BaseEventNotificationHandler {
private registeredHandlers: Record<string, HandlerCallback> = {};
private hasHandledEvent = false;
protected hasHandledEvent = false;

// the body is empty but the parameter properties are not, so the lint is ignorable
// eslint-disable-next-line no-useless-constructor
constructor(
private client: Stripe,
private webhookSecret: string,
protected client: Stripe,
private fallbackCallback: FallbackCallback
) {}

Expand Down Expand Up @@ -149,19 +158,11 @@ export class StripeEventNotificationHandler {
return keys;
}

public async handle(
// these types are duplicated in the manual types, so they're just here for internal use
rawBody: string | Uint8Array,
signature: string | Uint8Array
protected async dispatchEvent(
event: Stripe.V2.Core.EventNotification
): Promise<void> {
// we're not worried about thread safety here because we expect callbacks will be registered synchronously on app startup
this.hasHandledEvent = true;
const event = this.client.parseEventNotification(
rawBody,
signature,
this.webhookSecret
);

// Create a new client with the event's context instead of modifying the shared client
// This ensures thread-safety when processing webhooks in parallel
// We create a shallow copy and override _api with a new object containing the event context
Expand All @@ -187,3 +188,52 @@ export class StripeEventNotificationHandler {
}
}
}

export class StripeEventNotificationHandler extends BaseEventNotificationHandler {
constructor(
client: Stripe,
private webhookSecret: string,
fallbackCallback: FallbackCallback
) {
super(client, fallbackCallback);
if (!webhookSecret) {
throw new Error('webhookSecret must be a non-empty string');
}
}

static withoutVerification(
client: Stripe,
fallbackCallback: FallbackCallback
): StripeEventNotificationHandlerWithoutVerification {
return new StripeEventNotificationHandlerWithoutVerification(
client,
fallbackCallback
);
}

public async handle(
// these types are duplicated in the manual types, so they're just here for internal use
rawBody: string | Uint8Array,
signature: string | Uint8Array
): Promise<void> {
return await this.dispatchEvent(
this.client.parseEventNotification(rawBody, signature, this.webhookSecret)
);
}
}

/**
* A variant of StripeEventNotificationHandler that parses events without
* verifying webhook signatures. Intended for pre-authenticated channels
* like AWS EventBridge or Azure Event Grid.
*
* Prefer StripeEventNotificationHandler.withoutVerification() or
* client.notificationHandlerWithoutVerification() to construct one.
*/
export class StripeEventNotificationHandlerWithoutVerification extends BaseEventNotificationHandler {
public async handle(rawBody: string | Uint8Array): Promise<void> {
return await this.dispatchEvent(
this.client.parseEventNotificationWithoutVerification(rawBody)
);
}
}
3 changes: 3 additions & 0 deletions src/stripe.cjs.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20810,6 +20810,9 @@ declare namespace StripeConstructor {
export type Signature = Stripe_.Signature;
export type StripeContextType = Stripe_.StripeContextType;
export type StripeRawError = Stripe_.StripeRawError;
export type UnhandledNotificationDetails = Stripe_.UnhandledNotificationDetails;
export type StripeEventNotificationHandler = Stripe_.StripeEventNotificationHandler;
export type StripeEventNotificationHandlerWithoutVerification = Stripe_.StripeEventNotificationHandlerWithoutVerification;
export type Decimal = Stripe_.Decimal;
export namespace errors {
export type StripeError = InstanceType<typeof Stripe_.errors.StripeError>;
Expand Down
22 changes: 21 additions & 1 deletion src/stripe.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from './utils.js';
import {
StripeEventNotificationHandler,
StripeEventNotificationHandlerWithoutVerification,
UnhandledNotificationDetails,
} from './StripeEventNotificationHandler.js';
import {
Expand Down Expand Up @@ -1862,7 +1863,7 @@ export class Stripe {
* parse in a single call, use `parseEventNotification(...)` instead.
*/
parseEventNotificationWithoutVerification(
payload: string
payload: string | Uint8Array
): V2.Core.EventNotification {
return this._buildEventNotification(
maybeExtractFromCloudProviderEnvelope(payload)
Expand All @@ -1883,6 +1884,19 @@ export class Stripe {
fallbackCallback
);
}

notificationHandlerWithoutVerification(
fallbackCallback: (
event: UnknownEventNotification,
client: Stripe,
details: UnhandledNotificationDetails
) => Promise<void>
): StripeEventNotificationHandlerWithoutVerification {
return StripeEventNotificationHandler.withoutVerification(
this,
fallbackCallback
);
}
}

// For backward compatibility, export createStripe as a factory function
Expand Down Expand Up @@ -2875,6 +2889,12 @@ export declare namespace Stripe {

export {StripeContext as StripeContextType};
export {StripeRawError};
// Type-only: these classes are not attached as statics on the Stripe constructor,
// so they can be named in annotations but not used as values. Construct handlers
// through stripe.notificationHandler() / stripe.notificationHandlerWithoutVerification().
export type UnhandledNotificationDetails = import('./StripeEventNotificationHandler.js').UnhandledNotificationDetails;
export type StripeEventNotificationHandler = import('./StripeEventNotificationHandler.js').StripeEventNotificationHandler;
export type StripeEventNotificationHandlerWithoutVerification = import('./StripeEventNotificationHandler.js').StripeEventNotificationHandlerWithoutVerification;
// ErrorTypeNamespaces: The beginning of the section generated from our OpenAPI spec
export namespace ErrorType {
export type StripeError = InstanceType<typeof _Error.StripeError>;
Expand Down
21 changes: 20 additions & 1 deletion src/stripe.esm.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from './utils.js';
import {
StripeEventNotificationHandler,
StripeEventNotificationHandlerWithoutVerification,
UnhandledNotificationDetails,
} from './StripeEventNotificationHandler.js';
import {
Expand Down Expand Up @@ -1862,7 +1863,7 @@ export class Stripe {
* or [Azure Event Grid](https://docs.stripe.com/event-destinations/eventgrid) envelope.
*/
parseEventNotificationWithoutVerification(
payload: string
payload: string | Uint8Array
): V2.Core.EventNotification {
const inner = maybeExtractFromCloudProviderEnvelope(payload);
if (inner.object === 'event') {
Expand Down Expand Up @@ -1892,6 +1893,19 @@ export class Stripe {
fallbackCallback
);
}

notificationHandlerWithoutVerification(
fallbackCallback: (
event: UnknownEventNotification,
client: Stripe,
details: UnhandledNotificationDetails
) => Promise<void>
): StripeEventNotificationHandlerWithoutVerification {
return StripeEventNotificationHandler.withoutVerification(
this,
fallbackCallback
);
}
}

// For backward compatibility, export createStripe as a factory function
Expand Down Expand Up @@ -2885,6 +2899,11 @@ export declare namespace Stripe {
export {StripeContext as StripeContextType};
export {StripeRawError};
export {UnhandledNotificationDetails};
// Type-only: these classes are not attached as statics on the Stripe constructor,
// so they can be named in annotations but not used as values. Construct handlers
// through stripe.notificationHandler() / stripe.notificationHandlerWithoutVerification().
export type StripeEventNotificationHandler = import('./StripeEventNotificationHandler.js').StripeEventNotificationHandler;
export type StripeEventNotificationHandlerWithoutVerification = import('./StripeEventNotificationHandler.js').StripeEventNotificationHandlerWithoutVerification;
export import Events = V2.Core.Events;

// ErrorTypeNamespaces: The beginning of the section generated from our OpenAPI spec
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ export function parsePayload(
}

export function maybeExtractFromCloudProviderEnvelope(
payload: string
payload: string | Uint8Array
): Record<string, unknown> {
const parsed = parsePayload(payload);

Expand Down
Loading
Loading