@proxy-checkout/server-js 0.0.2 → 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.
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import type { ProxyCheckoutHttpClient } from "./http-client.js";
2
- import type { ProxyCheckoutServerRequestOptions } from "./types.js";
2
+ import type { JsonObject, ProxyCheckoutServerRequestOptions } from "./types.js";
3
3
  export interface WebhookEndpoint {
4
4
  readonly createdAt: string;
5
5
  readonly eventSchemaVersion: string;
@@ -32,6 +32,19 @@ export interface VerifyProxyWebhookSignatureInput {
32
32
  readonly secret: string;
33
33
  readonly toleranceSeconds?: number;
34
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
+ }
35
48
  export declare const proxyCheckoutWebhookEndpointEndpoints: readonly [{
36
49
  readonly method: "GET";
37
50
  readonly operation: "webhookEndpoints.list";
@@ -68,3 +81,4 @@ export declare class ProxyWebhookEndpointsResource {
68
81
  rotateSecret(webhookEndpointId: string, input: RotateWebhookEndpointSecretInput): Promise<WebhookEndpointSecretResult>;
69
82
  }
70
83
  export declare function verifyProxyWebhookSignature(input: VerifyProxyWebhookSignatureInput): boolean;
84
+ export declare function constructProxyWebhookEvent(input: ConstructProxyWebhookEventInput): ProxyWebhookEvent;
package/dist/webhooks.js CHANGED
@@ -1,4 +1,10 @@
1
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
+ }
2
8
  export const proxyCheckoutWebhookEndpointEndpoints = [
3
9
  {
4
10
  method: "GET",
@@ -86,6 +92,26 @@ export function verifyProxyWebhookSignature(input) {
86
92
  .digest("hex");
87
93
  return parsed.signatures.some((signature) => timingSafeEqualString(expected, signature));
88
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
+ }
89
115
  function toWebhookEndpointBody(input) {
90
116
  return {
91
117
  ...(input.eventTypes === undefined ? {} : { event_types: input.eventTypes }),
@@ -147,27 +173,3 @@ function timingSafeEqualString(left, right) {
147
173
  const rightBuffer = Buffer.from(right, "hex");
148
174
  return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
149
175
  }
150
- function requireJsonObject(value, operation) {
151
- if (!value || typeof value !== "object" || Array.isArray(value)) {
152
- throw new Error(`Proxy API response for ${operation} must be a JSON object.`);
153
- }
154
- return value;
155
- }
156
- function requireString(value, field) {
157
- if (typeof value !== "string") {
158
- throw new Error(`Proxy API response field ${field} must be a string.`);
159
- }
160
- return value;
161
- }
162
- function readStringOrNull(value, field) {
163
- return value === null ? null : requireString(value, field);
164
- }
165
- function readStringArrayOrNull(value, field) {
166
- if (value === null) {
167
- return null;
168
- }
169
- if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
170
- throw new Error(`Proxy API response field ${field} must be an array or null.`);
171
- }
172
- return value;
173
- }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "sideEffects": false,
8
8
  "type": "module",
9
9
  "types": "./dist/index.d.ts",
10
- "version": "0.0.2",
10
+ "version": "0.0.3-pr-76.12.1",
11
11
  "devDependencies": {
12
12
  "@types/node": "^24.12.4",
13
13
  "@vitest/coverage-v8": "4.1.7",