@proxy-checkout/server-js 0.0.1 → 0.0.3-pr-76.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -8
- package/dist/cart.d.ts +17 -0
- package/dist/cart.js +23 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +19 -1
- package/dist/consistency.d.ts +20 -0
- package/dist/consistency.js +32 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +83 -0
- package/dist/events.js +147 -0
- package/dist/http-client.d.ts +1 -1
- package/dist/http-client.js +6 -4
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -1
- package/dist/lifecycle.d.ts +41 -0
- package/dist/lifecycle.js +91 -0
- package/dist/response-validators.d.ts +8 -0
- package/dist/response-validators.js +51 -0
- package/dist/sessions.d.ts +80 -3
- package/dist/sessions.js +171 -20
- package/dist/subscriptions.d.ts +54 -0
- package/dist/subscriptions.js +55 -0
- package/dist/types.d.ts +2 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/dist/webhook-events.d.ts +29 -0
- package/dist/webhook-events.js +53 -0
- package/dist/webhook-handler.d.ts +84 -0
- package/dist/webhook-handler.js +154 -0
- package/dist/webhooks.d.ts +84 -0
- package/dist/webhooks.js +175 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical outbound Proxy webhook event types and predicates.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the event types the Proxy API can deliver to merchant webhook
|
|
5
|
+
* endpoints. Use the predicates instead of comparing raw strings so customer
|
|
6
|
+
* integration code does not encode the event taxonomy by hand.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROXY_WEBHOOK_EVENT_TYPES: {
|
|
9
|
+
readonly paymentAttemptCancelled: "payment_attempt.cancelled";
|
|
10
|
+
readonly paymentAttemptFailed: "payment_attempt.failed";
|
|
11
|
+
readonly paymentAttemptSucceeded: "payment_attempt.succeeded";
|
|
12
|
+
readonly sessionCancelled: "proxy_session.cancelled";
|
|
13
|
+
readonly sessionExpired: "proxy_session.expired";
|
|
14
|
+
readonly sessionFailed: "proxy_session.failed";
|
|
15
|
+
readonly sessionMerchantActionRequired: "proxy_session.merchant_action_required";
|
|
16
|
+
readonly sessionPaid: "proxy_session.paid";
|
|
17
|
+
readonly sessionProvisionable: "proxy_session.provisionable";
|
|
18
|
+
readonly subscriptionCancelled: "subscription.cancelled";
|
|
19
|
+
readonly subscriptionCancelScheduled: "subscription.cancel_scheduled";
|
|
20
|
+
readonly subscriptionPaymentFailed: "subscription.payment_failed";
|
|
21
|
+
readonly subscriptionRenewed: "subscription.renewed";
|
|
22
|
+
};
|
|
23
|
+
export type ProxyWebhookEventType = (typeof PROXY_WEBHOOK_EVENT_TYPES)[keyof typeof PROXY_WEBHOOK_EVENT_TYPES];
|
|
24
|
+
/** True for `proxy_session.*` events. */
|
|
25
|
+
export declare function isProxySessionEvent(eventType: string): boolean;
|
|
26
|
+
/** True for `subscription.*` events. */
|
|
27
|
+
export declare function isProxySubscriptionEvent(eventType: string): boolean;
|
|
28
|
+
/** True for `payment_attempt.*` events. */
|
|
29
|
+
export declare function isProxyPaymentAttemptEvent(eventType: string): boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical outbound Proxy webhook event types and predicates.
|
|
3
|
+
*
|
|
4
|
+
* These mirror the event types the Proxy API can deliver to merchant webhook
|
|
5
|
+
* endpoints. Use the predicates instead of comparing raw strings so customer
|
|
6
|
+
* integration code does not encode the event taxonomy by hand.
|
|
7
|
+
*/
|
|
8
|
+
export const PROXY_WEBHOOK_EVENT_TYPES = {
|
|
9
|
+
paymentAttemptCancelled: "payment_attempt.cancelled",
|
|
10
|
+
paymentAttemptFailed: "payment_attempt.failed",
|
|
11
|
+
paymentAttemptSucceeded: "payment_attempt.succeeded",
|
|
12
|
+
sessionCancelled: "proxy_session.cancelled",
|
|
13
|
+
sessionExpired: "proxy_session.expired",
|
|
14
|
+
sessionFailed: "proxy_session.failed",
|
|
15
|
+
sessionMerchantActionRequired: "proxy_session.merchant_action_required",
|
|
16
|
+
sessionPaid: "proxy_session.paid",
|
|
17
|
+
sessionProvisionable: "proxy_session.provisionable",
|
|
18
|
+
subscriptionCancelled: "subscription.cancelled",
|
|
19
|
+
subscriptionCancelScheduled: "subscription.cancel_scheduled",
|
|
20
|
+
subscriptionPaymentFailed: "subscription.payment_failed",
|
|
21
|
+
subscriptionRenewed: "subscription.renewed",
|
|
22
|
+
};
|
|
23
|
+
const SESSION_EVENT_TYPES = new Set([
|
|
24
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionCancelled,
|
|
25
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionExpired,
|
|
26
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionFailed,
|
|
27
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionMerchantActionRequired,
|
|
28
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionPaid,
|
|
29
|
+
PROXY_WEBHOOK_EVENT_TYPES.sessionProvisionable,
|
|
30
|
+
]);
|
|
31
|
+
const SUBSCRIPTION_EVENT_TYPES = new Set([
|
|
32
|
+
PROXY_WEBHOOK_EVENT_TYPES.subscriptionCancelScheduled,
|
|
33
|
+
PROXY_WEBHOOK_EVENT_TYPES.subscriptionCancelled,
|
|
34
|
+
PROXY_WEBHOOK_EVENT_TYPES.subscriptionPaymentFailed,
|
|
35
|
+
PROXY_WEBHOOK_EVENT_TYPES.subscriptionRenewed,
|
|
36
|
+
]);
|
|
37
|
+
const PAYMENT_ATTEMPT_EVENT_TYPES = new Set([
|
|
38
|
+
PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptCancelled,
|
|
39
|
+
PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptFailed,
|
|
40
|
+
PROXY_WEBHOOK_EVENT_TYPES.paymentAttemptSucceeded,
|
|
41
|
+
]);
|
|
42
|
+
/** True for `proxy_session.*` events. */
|
|
43
|
+
export function isProxySessionEvent(eventType) {
|
|
44
|
+
return SESSION_EVENT_TYPES.has(eventType);
|
|
45
|
+
}
|
|
46
|
+
/** True for `subscription.*` events. */
|
|
47
|
+
export function isProxySubscriptionEvent(eventType) {
|
|
48
|
+
return SUBSCRIPTION_EVENT_TYPES.has(eventType);
|
|
49
|
+
}
|
|
50
|
+
/** True for `payment_attempt.*` events. */
|
|
51
|
+
export function isProxyPaymentAttemptEvent(eventType) {
|
|
52
|
+
return PAYMENT_ATTEMPT_EVENT_TYPES.has(eventType);
|
|
53
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server SDK webhook delivery handler.
|
|
3
|
+
*
|
|
4
|
+
* `handle` turns a verified Proxy webhook into a current-state lifecycle
|
|
5
|
+
* decision and runs the merchant's entitlement callback, with optional
|
|
6
|
+
* idempotency via a pluggable {@link ProxyWebhookStore}. It owns signature
|
|
7
|
+
* verification, duplicate/in-flight handling, retryable responses, and
|
|
8
|
+
* current-state retrieval so customer webhook routes stay tiny.
|
|
9
|
+
*/
|
|
10
|
+
import type { ProxyEventsResource, ResolvedProxyEvent, ResolvedProxyEventKind } from "./events.js";
|
|
11
|
+
import { type ProxyWebhookEvent } from "./webhooks.js";
|
|
12
|
+
/** Seconds advertised in `Retry-After` when an event is already in flight. */
|
|
13
|
+
export declare const PROXY_WEBHOOK_RETRY_AFTER_SECONDS = 30;
|
|
14
|
+
export type ProxyWebhookClaimResult = "claimed" | "duplicate" | "processing";
|
|
15
|
+
export type ProxyWebhookOutcome = "duplicate" | "ignored" | "processed" | "processing";
|
|
16
|
+
/** Compact, lifecycle-free audit record handed to a {@link ProxyWebhookStore}. */
|
|
17
|
+
export interface ProxyWebhookProcessRecord {
|
|
18
|
+
readonly kind: ResolvedProxyEventKind;
|
|
19
|
+
readonly sessionId: string | null;
|
|
20
|
+
readonly subscriptionId: string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Optional merchant-owned idempotency/audit store. The handler treats already
|
|
24
|
+
* processed events as success and concurrently in-flight events as retryable;
|
|
25
|
+
* it never classifies lifecycle itself.
|
|
26
|
+
*/
|
|
27
|
+
export interface ProxyWebhookStore {
|
|
28
|
+
/** Atomically claim an event id. Return "duplicate" if already processed, "processing" if in flight. */
|
|
29
|
+
claim(event: ProxyWebhookEvent): Promise<ProxyWebhookClaimResult> | ProxyWebhookClaimResult;
|
|
30
|
+
/** Mark a claimed event failed so it can be retried/reclaimed. */
|
|
31
|
+
markFailed(eventId: string, error: unknown): Promise<void> | void;
|
|
32
|
+
/** Mark a claimed event processed (idempotency commit). */
|
|
33
|
+
markProcessed(eventId: string, record: ProxyWebhookProcessRecord): Promise<void> | void;
|
|
34
|
+
}
|
|
35
|
+
export interface ProxyWebhookPayloadInput {
|
|
36
|
+
readonly body: Buffer | string;
|
|
37
|
+
readonly signature?: string | null;
|
|
38
|
+
}
|
|
39
|
+
/** A web `Request` (Next route handler, Hono, Cloudflare Worker) or an explicit body/signature pair (Express/Node). */
|
|
40
|
+
export type ProxyWebhookRequestInput = ProxyWebhookPayloadInput | Request;
|
|
41
|
+
export interface ProxyWebhookHandlerOptions {
|
|
42
|
+
/** Merchant entitlement callback, invoked once per claimed event after current-state resolution. */
|
|
43
|
+
readonly onResolved: (resolved: ResolvedProxyEvent) => Promise<void> | void;
|
|
44
|
+
/** Optional request id forwarded to the current-state retrieval calls. */
|
|
45
|
+
readonly requestId?: string;
|
|
46
|
+
/** Endpoint signing secret. */
|
|
47
|
+
readonly secret: string;
|
|
48
|
+
/** Optional idempotency/audit store. Omit for at-least-once delivery with idempotent fulfillment. */
|
|
49
|
+
readonly store?: ProxyWebhookStore;
|
|
50
|
+
/** Signature timestamp tolerance in seconds (default 300). */
|
|
51
|
+
readonly toleranceSeconds?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface ProxyWebhookProcessResult {
|
|
54
|
+
readonly event: ProxyWebhookEvent;
|
|
55
|
+
readonly outcome: ProxyWebhookOutcome;
|
|
56
|
+
readonly resolved: ResolvedProxyEvent | null;
|
|
57
|
+
readonly retryable: boolean;
|
|
58
|
+
readonly statusCode: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class ProxyWebhooksResource {
|
|
61
|
+
private readonly events;
|
|
62
|
+
constructor(events: ProxyEventsResource);
|
|
63
|
+
/** Low-level: verify + construct the event without resolving lifecycle. */
|
|
64
|
+
constructEvent(input: {
|
|
65
|
+
body: Buffer | string;
|
|
66
|
+
header: string | null | undefined;
|
|
67
|
+
secret: string;
|
|
68
|
+
toleranceSeconds?: number;
|
|
69
|
+
}): ProxyWebhookEvent;
|
|
70
|
+
/**
|
|
71
|
+
* Framework-agnostic processing. Verifies the signature, applies the store,
|
|
72
|
+
* resolves current state, runs `onResolved`, and returns response metadata.
|
|
73
|
+
* Throws {@link ProxyWebhookSignatureVerificationError} on a bad signature so
|
|
74
|
+
* non-Response frameworks (Express) can map it to 400.
|
|
75
|
+
*/
|
|
76
|
+
process(input: ProxyWebhookRequestInput, options: ProxyWebhookHandlerOptions): Promise<ProxyWebhookProcessResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Turnkey handler for web `Request` frameworks. Returns a `Response` with the
|
|
79
|
+
* right status (200 success/duplicate, 409 + `Retry-After` in flight, 400 on
|
|
80
|
+
* bad signature). Re-throws merchant `onResolved` errors so the platform can
|
|
81
|
+
* surface a 500 and Proxy retries.
|
|
82
|
+
*/
|
|
83
|
+
handle(input: ProxyWebhookRequestInput, options: ProxyWebhookHandlerOptions): Promise<Response>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server SDK webhook delivery handler.
|
|
3
|
+
*
|
|
4
|
+
* `handle` turns a verified Proxy webhook into a current-state lifecycle
|
|
5
|
+
* decision and runs the merchant's entitlement callback, with optional
|
|
6
|
+
* idempotency via a pluggable {@link ProxyWebhookStore}. It owns signature
|
|
7
|
+
* verification, duplicate/in-flight handling, retryable responses, and
|
|
8
|
+
* current-state retrieval so customer webhook routes stay tiny.
|
|
9
|
+
*/
|
|
10
|
+
import { constructProxyWebhookEvent, PROXY_SIGNATURE_HEADER, ProxyWebhookSignatureVerificationError, } from "./webhooks.js";
|
|
11
|
+
/** Seconds advertised in `Retry-After` when an event is already in flight. */
|
|
12
|
+
export const PROXY_WEBHOOK_RETRY_AFTER_SECONDS = 30;
|
|
13
|
+
export class ProxyWebhooksResource {
|
|
14
|
+
events;
|
|
15
|
+
constructor(events) {
|
|
16
|
+
this.events = events;
|
|
17
|
+
}
|
|
18
|
+
/** Low-level: verify + construct the event without resolving lifecycle. */
|
|
19
|
+
constructEvent(input) {
|
|
20
|
+
return constructProxyWebhookEvent(input);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Framework-agnostic processing. Verifies the signature, applies the store,
|
|
24
|
+
* resolves current state, runs `onResolved`, and returns response metadata.
|
|
25
|
+
* Throws {@link ProxyWebhookSignatureVerificationError} on a bad signature so
|
|
26
|
+
* non-Response frameworks (Express) can map it to 400.
|
|
27
|
+
*/
|
|
28
|
+
async process(input, options) {
|
|
29
|
+
const { body, signature } = await readWebhookPayload(input);
|
|
30
|
+
const event = constructProxyWebhookEvent({
|
|
31
|
+
body,
|
|
32
|
+
header: signature,
|
|
33
|
+
secret: options.secret,
|
|
34
|
+
toleranceSeconds: options.toleranceSeconds,
|
|
35
|
+
});
|
|
36
|
+
const { store } = options;
|
|
37
|
+
if (store !== undefined) {
|
|
38
|
+
const claim = await store.claim(event);
|
|
39
|
+
if (claim === "duplicate") {
|
|
40
|
+
return {
|
|
41
|
+
event,
|
|
42
|
+
outcome: "duplicate",
|
|
43
|
+
resolved: null,
|
|
44
|
+
retryable: false,
|
|
45
|
+
statusCode: 200,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (claim === "processing") {
|
|
49
|
+
return {
|
|
50
|
+
event,
|
|
51
|
+
outcome: "processing",
|
|
52
|
+
resolved: null,
|
|
53
|
+
retryable: true,
|
|
54
|
+
statusCode: 409,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
let resolved;
|
|
59
|
+
try {
|
|
60
|
+
resolved = await this.events.resolveCurrentState(event, { requestId: options.requestId });
|
|
61
|
+
await options.onResolved(resolved);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (store !== undefined) {
|
|
65
|
+
await store.markFailed(event.id, error);
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (store !== undefined) {
|
|
70
|
+
await store.markProcessed(event.id, toProcessRecord(resolved));
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
event,
|
|
74
|
+
outcome: resolved.kind === "ignored" ? "ignored" : "processed",
|
|
75
|
+
resolved,
|
|
76
|
+
retryable: false,
|
|
77
|
+
statusCode: 200,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Turnkey handler for web `Request` frameworks. Returns a `Response` with the
|
|
82
|
+
* right status (200 success/duplicate, 409 + `Retry-After` in flight, 400 on
|
|
83
|
+
* bad signature). Re-throws merchant `onResolved` errors so the platform can
|
|
84
|
+
* surface a 500 and Proxy retries.
|
|
85
|
+
*/
|
|
86
|
+
async handle(input, options) {
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await this.process(input, options);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error instanceof ProxyWebhookSignatureVerificationError) {
|
|
93
|
+
return jsonResponse(400, {
|
|
94
|
+
error: { code: "invalid_signature", message: error.message },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
return toWebhookResponse(result);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function toWebhookResponse(result) {
|
|
103
|
+
const headers = { "content-type": "application/json" };
|
|
104
|
+
if (result.retryable) {
|
|
105
|
+
headers["retry-after"] = String(PROXY_WEBHOOK_RETRY_AFTER_SECONDS);
|
|
106
|
+
}
|
|
107
|
+
return new Response(JSON.stringify({ ok: !result.retryable, outcome: result.outcome }), {
|
|
108
|
+
headers,
|
|
109
|
+
status: result.statusCode,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function jsonResponse(status, body) {
|
|
113
|
+
return new Response(JSON.stringify(body), {
|
|
114
|
+
headers: { "content-type": "application/json" },
|
|
115
|
+
status,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function toProcessRecord(resolved) {
|
|
119
|
+
switch (resolved.kind) {
|
|
120
|
+
case "ignored":
|
|
121
|
+
return {
|
|
122
|
+
kind: resolved.kind,
|
|
123
|
+
sessionId: resolved.sessionId,
|
|
124
|
+
subscriptionId: resolved.subscriptionId,
|
|
125
|
+
};
|
|
126
|
+
case "terminal_session":
|
|
127
|
+
return { kind: resolved.kind, sessionId: resolved.session.id, subscriptionId: null };
|
|
128
|
+
case "initial_provision":
|
|
129
|
+
return {
|
|
130
|
+
kind: resolved.kind,
|
|
131
|
+
sessionId: resolved.session.id,
|
|
132
|
+
subscriptionId: resolved.subscription?.id ?? null,
|
|
133
|
+
};
|
|
134
|
+
default:
|
|
135
|
+
return {
|
|
136
|
+
kind: resolved.kind,
|
|
137
|
+
sessionId: resolved.session.id,
|
|
138
|
+
subscriptionId: resolved.subscription.id,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function readWebhookPayload(input) {
|
|
143
|
+
if (isFetchRequest(input)) {
|
|
144
|
+
return {
|
|
145
|
+
body: await input.text(),
|
|
146
|
+
signature: input.headers.get(PROXY_SIGNATURE_HEADER),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
150
|
+
return { body, signature: input.signature ?? null };
|
|
151
|
+
}
|
|
152
|
+
function isFetchRequest(input) {
|
|
153
|
+
return typeof input.text === "function";
|
|
154
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ProxyCheckoutHttpClient } from "./http-client.js";
|
|
2
|
+
import type { JsonObject, ProxyCheckoutServerRequestOptions } from "./types.js";
|
|
3
|
+
export interface WebhookEndpoint {
|
|
4
|
+
readonly createdAt: string;
|
|
5
|
+
readonly eventSchemaVersion: string;
|
|
6
|
+
readonly eventTypes: string[] | null;
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly previousSigningSecretExpiresAt: string | null;
|
|
9
|
+
readonly status: string;
|
|
10
|
+
readonly updatedAt: string;
|
|
11
|
+
readonly url: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateWebhookEndpointInput extends ProxyCheckoutServerRequestOptions {
|
|
14
|
+
readonly eventTypes?: string[];
|
|
15
|
+
readonly url: string;
|
|
16
|
+
}
|
|
17
|
+
export interface UpdateWebhookEndpointInput extends ProxyCheckoutServerRequestOptions {
|
|
18
|
+
readonly eventTypes?: string[];
|
|
19
|
+
readonly status?: "active" | "inactive";
|
|
20
|
+
readonly url?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RotateWebhookEndpointSecretInput extends ProxyCheckoutServerRequestOptions {
|
|
23
|
+
readonly previousSigningSecretExpiresAt: Date | string;
|
|
24
|
+
}
|
|
25
|
+
export interface WebhookEndpointSecretResult {
|
|
26
|
+
readonly signingSecret: string;
|
|
27
|
+
readonly webhookEndpoint: WebhookEndpoint;
|
|
28
|
+
}
|
|
29
|
+
export interface VerifyProxyWebhookSignatureInput {
|
|
30
|
+
readonly body: Buffer | string;
|
|
31
|
+
readonly header: string | null | undefined;
|
|
32
|
+
readonly secret: string;
|
|
33
|
+
readonly toleranceSeconds?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface ConstructProxyWebhookEventInput extends VerifyProxyWebhookSignatureInput {
|
|
36
|
+
}
|
|
37
|
+
export interface ProxyWebhookEvent {
|
|
38
|
+
readonly data: JsonObject;
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly schemaVersion: string;
|
|
41
|
+
readonly type: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const PROXY_SIGNATURE_HEADER = "proxy-signature";
|
|
44
|
+
/** Raised when a webhook payload fails signature verification. */
|
|
45
|
+
export declare class ProxyWebhookSignatureVerificationError extends Error {
|
|
46
|
+
readonly name = "ProxyWebhookSignatureVerificationError";
|
|
47
|
+
}
|
|
48
|
+
export declare const proxyCheckoutWebhookEndpointEndpoints: readonly [{
|
|
49
|
+
readonly method: "GET";
|
|
50
|
+
readonly operation: "webhookEndpoints.list";
|
|
51
|
+
readonly path: "/webhook_endpoints";
|
|
52
|
+
}, {
|
|
53
|
+
readonly method: "POST";
|
|
54
|
+
readonly operation: "webhookEndpoints.create";
|
|
55
|
+
readonly path: "/webhook_endpoints";
|
|
56
|
+
}, {
|
|
57
|
+
readonly method: "GET";
|
|
58
|
+
readonly operation: "webhookEndpoints.get";
|
|
59
|
+
readonly path: "/webhook_endpoints/:id";
|
|
60
|
+
}, {
|
|
61
|
+
readonly method: "PATCH";
|
|
62
|
+
readonly operation: "webhookEndpoints.update";
|
|
63
|
+
readonly path: "/webhook_endpoints/:id";
|
|
64
|
+
}, {
|
|
65
|
+
readonly method: "POST";
|
|
66
|
+
readonly operation: "webhookEndpoints.archive";
|
|
67
|
+
readonly path: "/webhook_endpoints/:id/archive";
|
|
68
|
+
}, {
|
|
69
|
+
readonly method: "POST";
|
|
70
|
+
readonly operation: "webhookEndpoints.rotateSecret";
|
|
71
|
+
readonly path: "/webhook_endpoints/:id/rotate_secret";
|
|
72
|
+
}];
|
|
73
|
+
export declare class ProxyWebhookEndpointsResource {
|
|
74
|
+
private readonly httpClient;
|
|
75
|
+
constructor(httpClient: ProxyCheckoutHttpClient);
|
|
76
|
+
list(options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint[]>;
|
|
77
|
+
create(input: CreateWebhookEndpointInput): Promise<WebhookEndpointSecretResult>;
|
|
78
|
+
get(webhookEndpointId: string, options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint>;
|
|
79
|
+
update(webhookEndpointId: string, input: UpdateWebhookEndpointInput): Promise<WebhookEndpoint>;
|
|
80
|
+
archive(webhookEndpointId: string, options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint>;
|
|
81
|
+
rotateSecret(webhookEndpointId: string, input: RotateWebhookEndpointSecretInput): Promise<WebhookEndpointSecretResult>;
|
|
82
|
+
}
|
|
83
|
+
export declare function verifyProxyWebhookSignature(input: VerifyProxyWebhookSignatureInput): boolean;
|
|
84
|
+
export declare function constructProxyWebhookEvent(input: ConstructProxyWebhookEventInput): ProxyWebhookEvent;
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { requireStringArrayOrNull as readStringArrayOrNull, requireNullableString as readStringOrNull, requireJsonObject, requireString, } from "./response-validators.js";
|
|
3
|
+
export const PROXY_SIGNATURE_HEADER = "proxy-signature";
|
|
4
|
+
/** Raised when a webhook payload fails signature verification. */
|
|
5
|
+
export class ProxyWebhookSignatureVerificationError extends Error {
|
|
6
|
+
name = "ProxyWebhookSignatureVerificationError";
|
|
7
|
+
}
|
|
8
|
+
export const proxyCheckoutWebhookEndpointEndpoints = [
|
|
9
|
+
{
|
|
10
|
+
method: "GET",
|
|
11
|
+
operation: "webhookEndpoints.list",
|
|
12
|
+
path: "/webhook_endpoints",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
method: "POST",
|
|
16
|
+
operation: "webhookEndpoints.create",
|
|
17
|
+
path: "/webhook_endpoints",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
method: "GET",
|
|
21
|
+
operation: "webhookEndpoints.get",
|
|
22
|
+
path: "/webhook_endpoints/:id",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
method: "PATCH",
|
|
26
|
+
operation: "webhookEndpoints.update",
|
|
27
|
+
path: "/webhook_endpoints/:id",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
method: "POST",
|
|
31
|
+
operation: "webhookEndpoints.archive",
|
|
32
|
+
path: "/webhook_endpoints/:id/archive",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
method: "POST",
|
|
36
|
+
operation: "webhookEndpoints.rotateSecret",
|
|
37
|
+
path: "/webhook_endpoints/:id/rotate_secret",
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
export class ProxyWebhookEndpointsResource {
|
|
41
|
+
httpClient;
|
|
42
|
+
constructor(httpClient) {
|
|
43
|
+
this.httpClient = httpClient;
|
|
44
|
+
}
|
|
45
|
+
async list(options = {}) {
|
|
46
|
+
const response = await this.httpClient.request("GET", "/webhook_endpoints", undefined, options);
|
|
47
|
+
const body = requireJsonObject(response, "webhookEndpoints.list");
|
|
48
|
+
const endpoints = body.webhook_endpoints;
|
|
49
|
+
if (!Array.isArray(endpoints)) {
|
|
50
|
+
throw new Error("Proxy API response field webhookEndpoints.list.webhookEndpoints must be an array.");
|
|
51
|
+
}
|
|
52
|
+
return endpoints.map(toWebhookEndpoint);
|
|
53
|
+
}
|
|
54
|
+
async create(input) {
|
|
55
|
+
const response = await this.httpClient.request("POST", "/webhook_endpoints", toWebhookEndpointBody(input), { requestId: input.requestId });
|
|
56
|
+
return toWebhookEndpointSecretResult(response, "webhookEndpoints.create");
|
|
57
|
+
}
|
|
58
|
+
async get(webhookEndpointId, options = {}) {
|
|
59
|
+
const response = await this.httpClient.request("GET", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}`, undefined, options);
|
|
60
|
+
return toWebhookEndpointEnvelope(response, "webhookEndpoints.get");
|
|
61
|
+
}
|
|
62
|
+
async update(webhookEndpointId, input) {
|
|
63
|
+
const response = await this.httpClient.request("PATCH", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}`, toWebhookEndpointBody(input), { requestId: input.requestId });
|
|
64
|
+
return toWebhookEndpointEnvelope(response, "webhookEndpoints.update");
|
|
65
|
+
}
|
|
66
|
+
async archive(webhookEndpointId, options = {}) {
|
|
67
|
+
const response = await this.httpClient.request("POST", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}/archive`, undefined, options);
|
|
68
|
+
return toWebhookEndpointEnvelope(response, "webhookEndpoints.archive");
|
|
69
|
+
}
|
|
70
|
+
async rotateSecret(webhookEndpointId, input) {
|
|
71
|
+
const response = await this.httpClient.request("POST", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}/rotate_secret`, {
|
|
72
|
+
previous_signing_secret_expires_at: input.previousSigningSecretExpiresAt instanceof Date
|
|
73
|
+
? input.previousSigningSecretExpiresAt.toISOString()
|
|
74
|
+
: input.previousSigningSecretExpiresAt,
|
|
75
|
+
}, { requestId: input.requestId });
|
|
76
|
+
return toWebhookEndpointSecretResult(response, "webhookEndpoints.rotateSecret");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function verifyProxyWebhookSignature(input) {
|
|
80
|
+
const parsed = parseSignatureHeader(input.header);
|
|
81
|
+
if (!parsed) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const toleranceSeconds = input.toleranceSeconds ?? 300;
|
|
85
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
86
|
+
if (Math.abs(nowSeconds - parsed.timestamp) > toleranceSeconds) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
90
|
+
const expected = createHmac("sha256", input.secret)
|
|
91
|
+
.update(`${parsed.timestamp}.${body}`)
|
|
92
|
+
.digest("hex");
|
|
93
|
+
return parsed.signatures.some((signature) => timingSafeEqualString(expected, signature));
|
|
94
|
+
}
|
|
95
|
+
export function constructProxyWebhookEvent(input) {
|
|
96
|
+
if (!verifyProxyWebhookSignature(input)) {
|
|
97
|
+
throw new ProxyWebhookSignatureVerificationError("Proxy webhook signature verification failed.");
|
|
98
|
+
}
|
|
99
|
+
const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
100
|
+
let parsed;
|
|
101
|
+
try {
|
|
102
|
+
parsed = JSON.parse(body);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error("Proxy webhook body must be valid JSON.");
|
|
106
|
+
}
|
|
107
|
+
const event = requireJsonObject(parsed, "webhookEvent");
|
|
108
|
+
return {
|
|
109
|
+
data: requireJsonObject(event.data, "webhookEvent.data"),
|
|
110
|
+
id: requireString(event.id, "webhookEvent.id"),
|
|
111
|
+
schemaVersion: requireString(event.schema_version, "webhookEvent.schemaVersion"),
|
|
112
|
+
type: requireString(event.type, "webhookEvent.type"),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function toWebhookEndpointBody(input) {
|
|
116
|
+
return {
|
|
117
|
+
...(input.eventTypes === undefined ? {} : { event_types: input.eventTypes }),
|
|
118
|
+
...(input.status === undefined ? {} : { status: input.status }),
|
|
119
|
+
...(input.url === undefined ? {} : { url: input.url }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function toWebhookEndpointSecretResult(response, operation) {
|
|
123
|
+
const body = requireJsonObject(response, operation);
|
|
124
|
+
return {
|
|
125
|
+
signingSecret: requireString(body.signing_secret, `${operation}.signingSecret`),
|
|
126
|
+
webhookEndpoint: toWebhookEndpoint(body.webhook_endpoint),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function toWebhookEndpointEnvelope(response, operation) {
|
|
130
|
+
const body = requireJsonObject(response, operation);
|
|
131
|
+
return toWebhookEndpoint(body.webhook_endpoint);
|
|
132
|
+
}
|
|
133
|
+
function toWebhookEndpoint(value) {
|
|
134
|
+
const body = requireJsonObject(value, "webhookEndpoint");
|
|
135
|
+
return {
|
|
136
|
+
createdAt: requireString(body.created_at, "webhookEndpoint.createdAt"),
|
|
137
|
+
eventSchemaVersion: requireString(body.event_schema_version, "webhookEndpoint.eventSchemaVersion"),
|
|
138
|
+
eventTypes: readStringArrayOrNull(body.event_types, "webhookEndpoint.eventTypes"),
|
|
139
|
+
id: requireString(body.id, "webhookEndpoint.id"),
|
|
140
|
+
previousSigningSecretExpiresAt: readStringOrNull(body.previous_signing_secret_expires_at, "webhookEndpoint.previousSigningSecretExpiresAt"),
|
|
141
|
+
status: requireString(body.status, "webhookEndpoint.status"),
|
|
142
|
+
updatedAt: requireString(body.updated_at, "webhookEndpoint.updatedAt"),
|
|
143
|
+
url: requireString(body.url, "webhookEndpoint.url"),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function parseSignatureHeader(header) {
|
|
147
|
+
if (!header) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
let timestamp;
|
|
151
|
+
const signatures = [];
|
|
152
|
+
for (const part of header.split(",")) {
|
|
153
|
+
const [rawKey, rawValue] = part.split("=", 2);
|
|
154
|
+
const key = rawKey?.trim();
|
|
155
|
+
const value = rawValue?.trim();
|
|
156
|
+
if (key === "t") {
|
|
157
|
+
timestamp = Number(value);
|
|
158
|
+
}
|
|
159
|
+
else if (key === "v1" && value) {
|
|
160
|
+
signatures.push(value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (timestamp === undefined || !Number.isSafeInteger(timestamp) || signatures.length === 0) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
signatures,
|
|
168
|
+
timestamp,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function timingSafeEqualString(left, right) {
|
|
172
|
+
const leftBuffer = Buffer.from(left, "hex");
|
|
173
|
+
const rightBuffer = Buffer.from(right, "hex");
|
|
174
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
175
|
+
}
|