@proxy-checkout/server-js 0.0.1 → 0.0.2

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 CHANGED
@@ -12,10 +12,12 @@ This package should expose the merchant-authenticated backend routes that exist
12
12
  | `proxy.sessions.cart.set` | `PUT /proxy_sessions/:id/cart` | Replace the current cart snapshot before payment orchestration. |
13
13
  | `proxy.sessions.payerHandoff` | `POST /proxy_sessions/:id/payer_handoff` | Record merchant handoff issuance before the payer loads checkout. |
14
14
  | `proxy.sessions.payerOpened` | `POST /proxy_sessions/:id/payer_opened` | Record checkout open and read the current cart details. |
15
-
16
- The portal routes are intentionally excluded because they are Better Auth
17
- session-backed portal APIs. The PSP webhook route is also excluded because PSPs
18
- call it directly.
15
+ | `proxy.webhookEndpoints.list` | `GET /webhook_endpoints` | List outbound merchant webhook endpoints. |
16
+ | `proxy.webhookEndpoints.create` | `POST /webhook_endpoints` | Create an outbound endpoint and receive its one-time signing secret. |
17
+ | `proxy.webhookEndpoints.get` | `GET /webhook_endpoints/:id` | Read one outbound endpoint. |
18
+ | `proxy.webhookEndpoints.update` | `PATCH /webhook_endpoints/:id` | Update URL, event types, or active/inactive status. |
19
+ | `proxy.webhookEndpoints.archive` | `POST /webhook_endpoints/:id/archive` | Archive an outbound endpoint. |
20
+ | `proxy.webhookEndpoints.rotateSecret` | `POST /webhook_endpoints/:id/rotate_secret` | Rotate an outbound endpoint signing secret. |
19
21
 
20
22
  ## Usage
21
23
 
@@ -37,6 +39,25 @@ const session = await proxy.sessions.create({
37
39
  });
38
40
 
39
41
  await proxy.sessions.payerHandoff(session.id);
42
+
43
+ const endpoint = await proxy.webhookEndpoints.create({
44
+ url: "https://example.com/proxy-webhooks",
45
+ eventTypes: ["proxy_session.paid", "proxy_session.expired"],
46
+ });
47
+
48
+ console.log(endpoint.signingSecret);
49
+ ```
50
+
51
+ Verify outbound webhook signatures with the exact raw request body:
52
+
53
+ ```ts
54
+ import { verifyProxyWebhookSignature } from "@proxy-checkout/server-js";
55
+
56
+ const ok = verifyProxyWebhookSignature({
57
+ body: rawBodyBuffer,
58
+ header: request.headers["proxy-signature"],
59
+ secret: process.env.PROXY_WEBHOOK_SIGNING_SECRET!,
60
+ });
40
61
  ```
41
62
 
42
63
  Production calls default to `https://api.proxycheckout.com`. Tests, local
package/dist/client.d.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { ProxySessionsResource } from "./sessions.js";
2
2
  import type { ProxyCheckoutServerClientOptions } from "./types.js";
3
+ import { ProxyWebhookEndpointsResource } from "./webhooks.js";
3
4
  export declare class ProxyCheckoutServerClient {
4
5
  readonly apiHost: string;
5
6
  readonly sessions: ProxySessionsResource;
7
+ readonly webhookEndpoints: ProxyWebhookEndpointsResource;
6
8
  constructor(options: ProxyCheckoutServerClientOptions);
7
9
  }
8
10
  export declare function createProxyCheckoutServerClient(options: ProxyCheckoutServerClientOptions): ProxyCheckoutServerClient;
package/dist/client.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { ProxyCheckoutHttpClient } from "./http-client.js";
2
2
  import { ProxySessionsResource } from "./sessions.js";
3
+ import { ProxyWebhookEndpointsResource } from "./webhooks.js";
3
4
  export class ProxyCheckoutServerClient {
4
5
  apiHost;
5
6
  sessions;
7
+ webhookEndpoints;
6
8
  constructor(options) {
7
9
  const httpClient = new ProxyCheckoutHttpClient({
8
10
  apiHost: options.apiHost,
@@ -11,6 +13,7 @@ export class ProxyCheckoutServerClient {
11
13
  });
12
14
  this.apiHost = httpClient.apiHost;
13
15
  this.sessions = new ProxySessionsResource(httpClient);
16
+ this.webhookEndpoints = new ProxyWebhookEndpointsResource(httpClient);
14
17
  }
15
18
  }
16
19
  export function createProxyCheckoutServerClient(options) {
@@ -12,5 +12,5 @@ export declare class ProxyCheckoutHttpClient {
12
12
  apiKey: string;
13
13
  fetchImpl?: ProxyCheckoutFetch;
14
14
  });
15
- request(method: "POST" | "PUT", path: string, body: JsonValue, options?: ProxyCheckoutRequestOptions): Promise<unknown>;
15
+ request(method: "GET" | "PATCH" | "POST" | "PUT", path: string, body?: JsonValue, options?: ProxyCheckoutRequestOptions): Promise<unknown>;
16
16
  }
@@ -16,8 +16,8 @@ export class ProxyCheckoutHttpClient {
16
16
  }
17
17
  async request(method, path, body, options = {}) {
18
18
  const response = await this.fetchImpl(`${this.apiHost}${path}`, {
19
- body: JSON.stringify(body),
20
- headers: buildHeaders(this.apiKey, options),
19
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
20
+ headers: buildHeaders(this.apiKey, options, body !== undefined),
21
21
  method,
22
22
  });
23
23
  const responseBody = await readResponseBody(response);
@@ -28,13 +28,15 @@ export class ProxyCheckoutHttpClient {
28
28
  return responseBody;
29
29
  }
30
30
  }
31
- function buildHeaders(apiKey, options) {
31
+ function buildHeaders(apiKey, options, hasJsonBody) {
32
32
  const headers = {
33
33
  accept: "application/json",
34
34
  authorization: `Bearer ${apiKey}`,
35
- "content-type": "application/json",
36
35
  "user-agent": proxyCheckoutServerSdkUserAgent,
37
36
  };
37
+ if (hasJsonBody) {
38
+ headers["content-type"] = "application/json";
39
+ }
38
40
  if (options.idempotencyKey !== undefined) {
39
41
  headers["idempotency-key"] = options.idempotencyKey;
40
42
  }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export { ProxyCheckoutApiError, type ProxyCheckoutApiErrorDetails } from "./erro
3
3
  export { type CreateProxySessionInput, type CreateProxySessionOptions, type PayerHandoffResult, type PayerOpenedResult, type ProxySession, ProxySessionCartResource, type ProxySessionCartResult, ProxySessionsResource, proxyCheckoutServerEndpoints, type SetProxySessionCartInput, } from "./sessions.js";
4
4
  export type { JsonObject, JsonPrimitive, JsonValue, ProxyCheckoutFetch, ProxyCheckoutServerClientOptions, ProxyCheckoutServerRequestOptions, } from "./types.js";
5
5
  export { proxyCheckoutServerSdkName, proxyCheckoutServerSdkUserAgent, proxyCheckoutServerSdkVersion, } from "./version.js";
6
+ export { type CreateWebhookEndpointInput, ProxyWebhookEndpointsResource, proxyCheckoutWebhookEndpointEndpoints, type RotateWebhookEndpointSecretInput, type UpdateWebhookEndpointInput, type VerifyProxyWebhookSignatureInput, verifyProxyWebhookSignature, type WebhookEndpoint, type WebhookEndpointSecretResult, } from "./webhooks.js";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { createProxyCheckoutServerClient, ProxyCheckoutServerClient, } from "./c
2
2
  export { ProxyCheckoutApiError } from "./errors.js";
3
3
  export { ProxySessionCartResource, ProxySessionsResource, proxyCheckoutServerEndpoints, } from "./sessions.js";
4
4
  export { proxyCheckoutServerSdkName, proxyCheckoutServerSdkUserAgent, proxyCheckoutServerSdkVersion, } from "./version.js";
5
+ export { ProxyWebhookEndpointsResource, proxyCheckoutWebhookEndpointEndpoints, verifyProxyWebhookSignature, } from "./webhooks.js";
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare const proxyCheckoutServerSdkName = "@proxy-checkout/server-js";
2
- export declare const proxyCheckoutServerSdkVersion = "0.0.1";
3
- export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.1";
2
+ export declare const proxyCheckoutServerSdkVersion = "0.0.2";
3
+ export declare const proxyCheckoutServerSdkUserAgent = "@proxy-checkout/server-js/0.0.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export const proxyCheckoutServerSdkName = "@proxy-checkout/server-js";
2
- export const proxyCheckoutServerSdkVersion = "0.0.1";
2
+ export const proxyCheckoutServerSdkVersion = "0.0.2";
3
3
  export const proxyCheckoutServerSdkUserAgent = `${proxyCheckoutServerSdkName}/${proxyCheckoutServerSdkVersion}`;
@@ -0,0 +1,70 @@
1
+ import type { ProxyCheckoutHttpClient } from "./http-client.js";
2
+ import type { 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 declare const proxyCheckoutWebhookEndpointEndpoints: readonly [{
36
+ readonly method: "GET";
37
+ readonly operation: "webhookEndpoints.list";
38
+ readonly path: "/webhook_endpoints";
39
+ }, {
40
+ readonly method: "POST";
41
+ readonly operation: "webhookEndpoints.create";
42
+ readonly path: "/webhook_endpoints";
43
+ }, {
44
+ readonly method: "GET";
45
+ readonly operation: "webhookEndpoints.get";
46
+ readonly path: "/webhook_endpoints/:id";
47
+ }, {
48
+ readonly method: "PATCH";
49
+ readonly operation: "webhookEndpoints.update";
50
+ readonly path: "/webhook_endpoints/:id";
51
+ }, {
52
+ readonly method: "POST";
53
+ readonly operation: "webhookEndpoints.archive";
54
+ readonly path: "/webhook_endpoints/:id/archive";
55
+ }, {
56
+ readonly method: "POST";
57
+ readonly operation: "webhookEndpoints.rotateSecret";
58
+ readonly path: "/webhook_endpoints/:id/rotate_secret";
59
+ }];
60
+ export declare class ProxyWebhookEndpointsResource {
61
+ private readonly httpClient;
62
+ constructor(httpClient: ProxyCheckoutHttpClient);
63
+ list(options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint[]>;
64
+ create(input: CreateWebhookEndpointInput): Promise<WebhookEndpointSecretResult>;
65
+ get(webhookEndpointId: string, options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint>;
66
+ update(webhookEndpointId: string, input: UpdateWebhookEndpointInput): Promise<WebhookEndpoint>;
67
+ archive(webhookEndpointId: string, options?: ProxyCheckoutServerRequestOptions): Promise<WebhookEndpoint>;
68
+ rotateSecret(webhookEndpointId: string, input: RotateWebhookEndpointSecretInput): Promise<WebhookEndpointSecretResult>;
69
+ }
70
+ export declare function verifyProxyWebhookSignature(input: VerifyProxyWebhookSignatureInput): boolean;
@@ -0,0 +1,173 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export const proxyCheckoutWebhookEndpointEndpoints = [
3
+ {
4
+ method: "GET",
5
+ operation: "webhookEndpoints.list",
6
+ path: "/webhook_endpoints",
7
+ },
8
+ {
9
+ method: "POST",
10
+ operation: "webhookEndpoints.create",
11
+ path: "/webhook_endpoints",
12
+ },
13
+ {
14
+ method: "GET",
15
+ operation: "webhookEndpoints.get",
16
+ path: "/webhook_endpoints/:id",
17
+ },
18
+ {
19
+ method: "PATCH",
20
+ operation: "webhookEndpoints.update",
21
+ path: "/webhook_endpoints/:id",
22
+ },
23
+ {
24
+ method: "POST",
25
+ operation: "webhookEndpoints.archive",
26
+ path: "/webhook_endpoints/:id/archive",
27
+ },
28
+ {
29
+ method: "POST",
30
+ operation: "webhookEndpoints.rotateSecret",
31
+ path: "/webhook_endpoints/:id/rotate_secret",
32
+ },
33
+ ];
34
+ export class ProxyWebhookEndpointsResource {
35
+ httpClient;
36
+ constructor(httpClient) {
37
+ this.httpClient = httpClient;
38
+ }
39
+ async list(options = {}) {
40
+ const response = await this.httpClient.request("GET", "/webhook_endpoints", undefined, options);
41
+ const body = requireJsonObject(response, "webhookEndpoints.list");
42
+ const endpoints = body.webhook_endpoints;
43
+ if (!Array.isArray(endpoints)) {
44
+ throw new Error("Proxy API response field webhookEndpoints.list.webhookEndpoints must be an array.");
45
+ }
46
+ return endpoints.map(toWebhookEndpoint);
47
+ }
48
+ async create(input) {
49
+ const response = await this.httpClient.request("POST", "/webhook_endpoints", toWebhookEndpointBody(input), { requestId: input.requestId });
50
+ return toWebhookEndpointSecretResult(response, "webhookEndpoints.create");
51
+ }
52
+ async get(webhookEndpointId, options = {}) {
53
+ const response = await this.httpClient.request("GET", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}`, undefined, options);
54
+ return toWebhookEndpointEnvelope(response, "webhookEndpoints.get");
55
+ }
56
+ async update(webhookEndpointId, input) {
57
+ const response = await this.httpClient.request("PATCH", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}`, toWebhookEndpointBody(input), { requestId: input.requestId });
58
+ return toWebhookEndpointEnvelope(response, "webhookEndpoints.update");
59
+ }
60
+ async archive(webhookEndpointId, options = {}) {
61
+ const response = await this.httpClient.request("POST", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}/archive`, undefined, options);
62
+ return toWebhookEndpointEnvelope(response, "webhookEndpoints.archive");
63
+ }
64
+ async rotateSecret(webhookEndpointId, input) {
65
+ const response = await this.httpClient.request("POST", `/webhook_endpoints/${encodeURIComponent(webhookEndpointId)}/rotate_secret`, {
66
+ previous_signing_secret_expires_at: input.previousSigningSecretExpiresAt instanceof Date
67
+ ? input.previousSigningSecretExpiresAt.toISOString()
68
+ : input.previousSigningSecretExpiresAt,
69
+ }, { requestId: input.requestId });
70
+ return toWebhookEndpointSecretResult(response, "webhookEndpoints.rotateSecret");
71
+ }
72
+ }
73
+ export function verifyProxyWebhookSignature(input) {
74
+ const parsed = parseSignatureHeader(input.header);
75
+ if (!parsed) {
76
+ return false;
77
+ }
78
+ const toleranceSeconds = input.toleranceSeconds ?? 300;
79
+ const nowSeconds = Math.floor(Date.now() / 1000);
80
+ if (Math.abs(nowSeconds - parsed.timestamp) > toleranceSeconds) {
81
+ return false;
82
+ }
83
+ const body = typeof input.body === "string" ? input.body : input.body.toString("utf8");
84
+ const expected = createHmac("sha256", input.secret)
85
+ .update(`${parsed.timestamp}.${body}`)
86
+ .digest("hex");
87
+ return parsed.signatures.some((signature) => timingSafeEqualString(expected, signature));
88
+ }
89
+ function toWebhookEndpointBody(input) {
90
+ return {
91
+ ...(input.eventTypes === undefined ? {} : { event_types: input.eventTypes }),
92
+ ...(input.status === undefined ? {} : { status: input.status }),
93
+ ...(input.url === undefined ? {} : { url: input.url }),
94
+ };
95
+ }
96
+ function toWebhookEndpointSecretResult(response, operation) {
97
+ const body = requireJsonObject(response, operation);
98
+ return {
99
+ signingSecret: requireString(body.signing_secret, `${operation}.signingSecret`),
100
+ webhookEndpoint: toWebhookEndpoint(body.webhook_endpoint),
101
+ };
102
+ }
103
+ function toWebhookEndpointEnvelope(response, operation) {
104
+ const body = requireJsonObject(response, operation);
105
+ return toWebhookEndpoint(body.webhook_endpoint);
106
+ }
107
+ function toWebhookEndpoint(value) {
108
+ const body = requireJsonObject(value, "webhookEndpoint");
109
+ return {
110
+ createdAt: requireString(body.created_at, "webhookEndpoint.createdAt"),
111
+ eventSchemaVersion: requireString(body.event_schema_version, "webhookEndpoint.eventSchemaVersion"),
112
+ eventTypes: readStringArrayOrNull(body.event_types, "webhookEndpoint.eventTypes"),
113
+ id: requireString(body.id, "webhookEndpoint.id"),
114
+ previousSigningSecretExpiresAt: readStringOrNull(body.previous_signing_secret_expires_at, "webhookEndpoint.previousSigningSecretExpiresAt"),
115
+ status: requireString(body.status, "webhookEndpoint.status"),
116
+ updatedAt: requireString(body.updated_at, "webhookEndpoint.updatedAt"),
117
+ url: requireString(body.url, "webhookEndpoint.url"),
118
+ };
119
+ }
120
+ function parseSignatureHeader(header) {
121
+ if (!header) {
122
+ return undefined;
123
+ }
124
+ let timestamp;
125
+ const signatures = [];
126
+ for (const part of header.split(",")) {
127
+ const [rawKey, rawValue] = part.split("=", 2);
128
+ const key = rawKey?.trim();
129
+ const value = rawValue?.trim();
130
+ if (key === "t") {
131
+ timestamp = Number(value);
132
+ }
133
+ else if (key === "v1" && value) {
134
+ signatures.push(value);
135
+ }
136
+ }
137
+ if (timestamp === undefined || !Number.isSafeInteger(timestamp) || signatures.length === 0) {
138
+ return undefined;
139
+ }
140
+ return {
141
+ signatures,
142
+ timestamp,
143
+ };
144
+ }
145
+ function timingSafeEqualString(left, right) {
146
+ const leftBuffer = Buffer.from(left, "hex");
147
+ const rightBuffer = Buffer.from(right, "hex");
148
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
149
+ }
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.1",
10
+ "version": "0.0.2",
11
11
  "devDependencies": {
12
12
  "@types/node": "^24.12.4",
13
13
  "@vitest/coverage-v8": "4.1.7",