@stableops/api-sdk 0.1.0

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/dist/mock.js ADDED
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/mock-entry.ts
21
+ var mock_entry_exports = {};
22
+ __export(mock_entry_exports, {
23
+ MockServer: () => MockServer
24
+ });
25
+ module.exports = __toCommonJS(mock_entry_exports);
26
+
27
+ // src/mock-server.ts
28
+ var import_node_http = require("http");
29
+ var import_node_crypto2 = require("crypto");
30
+
31
+ // src/signature.ts
32
+ var import_node_crypto = require("crypto");
33
+ var DEFAULT_TOLERANCE_SECONDS = 5 * 60;
34
+ function buildSignatureHeader({
35
+ secret,
36
+ timestamp,
37
+ rawBody
38
+ }) {
39
+ return buildSignatureHeaderForSecrets({
40
+ secrets: [secret],
41
+ timestamp,
42
+ rawBody
43
+ });
44
+ }
45
+ function buildSignatureHeaderForSecrets({
46
+ secrets,
47
+ timestamp,
48
+ rawBody
49
+ }) {
50
+ const payload = `${timestamp}.${rawBody}`;
51
+ const signatures = secrets.filter((secret) => secret.length > 0).map((secret) => (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("hex"));
52
+ return `t=${timestamp},${signatures.map((signature) => `v1=${signature}`).join(",")}`;
53
+ }
54
+
55
+ // src/mock-server.ts
56
+ var MockServer = class {
57
+ constructor(options = {}) {
58
+ this.options = options;
59
+ this.idFactory = options.idFactory ?? (() => (0, import_node_crypto2.randomUUID)());
60
+ this.secretFactory = options.secretFactory ?? (() => (0, import_node_crypto2.randomUUID)());
61
+ this.server = (0, import_node_http.createServer)((req, res) => this.handle(req, res));
62
+ }
63
+ options;
64
+ server;
65
+ orders = /* @__PURE__ */ new Map();
66
+ endpoints = /* @__PURE__ */ new Map();
67
+ idempotency = /* @__PURE__ */ new Map();
68
+ idFactory;
69
+ secretFactory;
70
+ listen() {
71
+ return new Promise((resolve) => {
72
+ this.server.listen(this.options.port ?? 0, this.options.host ?? "127.0.0.1", () => {
73
+ const address = this.server.address();
74
+ if (typeof address === "string" || address === null) {
75
+ resolve({ url: `http://${this.options.host ?? "127.0.0.1"}:${this.options.port ?? 0}` });
76
+ return;
77
+ }
78
+ resolve({ url: `http://${address.address}:${address.port}` });
79
+ });
80
+ });
81
+ }
82
+ async close() {
83
+ await new Promise((resolve, reject) => {
84
+ this.server.close((err) => err ? reject(err) : resolve());
85
+ });
86
+ }
87
+ // 暴露用于断言的内部状态。
88
+ snapshot() {
89
+ return {
90
+ orders: Array.from(this.orders.values()),
91
+ endpoints: Array.from(this.endpoints.values())
92
+ };
93
+ }
94
+ async handle(req, res) {
95
+ const url = new URL(req.url ?? "/", "http://localhost");
96
+ const body = await readBody(req);
97
+ res.setHeader("content-type", "application/json");
98
+ try {
99
+ if (req.method === "POST" && url.pathname === "/v1/payment-orders") {
100
+ return this.createOrder(req, body, res);
101
+ }
102
+ if (req.method === "GET" && url.pathname === "/v1/payment-orders") {
103
+ return json(res, 200, { items: Array.from(this.orders.values()) });
104
+ }
105
+ const orderMatch = url.pathname.match(/^\/v1\/payment-orders\/([^/]+)$/u);
106
+ if (req.method === "GET" && orderMatch) {
107
+ const order = this.orders.get(orderMatch[1]);
108
+ return order ? json(res, 200, order) : json(res, 404, { code: "not_found" });
109
+ }
110
+ const cancelMatch = url.pathname.match(/^\/v1\/payment-orders\/([^/]+)\/cancel$/u);
111
+ if (req.method === "POST" && cancelMatch) {
112
+ return this.cancelOrder(cancelMatch[1], res);
113
+ }
114
+ if (req.method === "GET" && url.pathname === "/v1/events") {
115
+ return json(res, 200, { items: [] });
116
+ }
117
+ if (req.method === "POST" && url.pathname === "/v1/webhook-endpoints") {
118
+ return this.createEndpoint(body, res);
119
+ }
120
+ if (req.method === "GET" && url.pathname === "/v1/webhook-endpoints") {
121
+ return json(res, 200, {
122
+ items: Array.from(
123
+ this.endpoints.values(),
124
+ (endpoint) => webhookEndpointResponse(endpoint)
125
+ )
126
+ });
127
+ }
128
+ const endpointMatch = url.pathname.match(/^\/v1\/webhook-endpoints\/([^/]+)$/u);
129
+ if (req.method === "PATCH" && endpointMatch) {
130
+ return this.updateEndpoint(endpointMatch[1], body, res);
131
+ }
132
+ const rotateMatch = url.pathname.match(
133
+ /^\/v1\/webhook-endpoints\/([^/]+)\/rotate-secret$/u
134
+ );
135
+ if (req.method === "POST" && rotateMatch) {
136
+ return this.rotateEndpointSecret(rotateMatch[1], res);
137
+ }
138
+ json(res, 404, { code: "not_found" });
139
+ } catch (err) {
140
+ json(res, 500, { code: "internal", message: String(err) });
141
+ }
142
+ }
143
+ createOrder(req, body, res) {
144
+ const idem = headerValue(req, "idempotency-key");
145
+ if (idem && this.idempotency.has(idem)) {
146
+ const previous = this.idempotency.get(idem);
147
+ return json(res, 201, previous);
148
+ }
149
+ const input = body;
150
+ const id = this.idFactory();
151
+ const accepted = input.accepted_assets ?? [];
152
+ const order = {
153
+ id,
154
+ merchant_order_id: String(input.merchant_order_id ?? ""),
155
+ scenario: String(input.scenario ?? "generic"),
156
+ amount: String(input.amount ?? "0"),
157
+ requested_amount: String(input.amount ?? "0"),
158
+ settlement_asset: String(input.settlement_asset ?? "USDC"),
159
+ status: "created",
160
+ expires_at: input.expires_at ?? null,
161
+ metadata: input.metadata ?? null,
162
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
163
+ accepted_assets: accepted,
164
+ payment_instructions: accepted.length > 0 ? accepted.map((entry, index) => ({
165
+ chain: entry.chain,
166
+ asset: entry.asset,
167
+ address: `0xMOCK${id.slice(0, 8)}${index}`
168
+ })) : [{ chain: "base", asset: "USDC", address: `0xMOCK${id.slice(0, 8)}0` }],
169
+ timeline: [
170
+ { from: null, to: "created", reason: "order_created", at: (/* @__PURE__ */ new Date()).toISOString() }
171
+ ]
172
+ };
173
+ this.orders.set(id, order);
174
+ if (idem) this.idempotency.set(idem, order);
175
+ json(res, 201, order);
176
+ }
177
+ cancelOrder(id, res) {
178
+ const order = this.orders.get(id);
179
+ if (!order) return json(res, 404, { code: "not_found" });
180
+ order.status = "canceled";
181
+ order.timeline.push({
182
+ from: order.timeline[order.timeline.length - 1]?.to ?? "created",
183
+ to: "canceled",
184
+ reason: "manual_cancel",
185
+ at: (/* @__PURE__ */ new Date()).toISOString()
186
+ });
187
+ json(res, 200, order);
188
+ }
189
+ createEndpoint(body, res) {
190
+ const input = body;
191
+ const id = this.idFactory();
192
+ const secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`;
193
+ const endpoint = {
194
+ id,
195
+ url: String(input.url ?? ""),
196
+ description: input.description ?? null,
197
+ enabled_events: input.enabled_events ?? [],
198
+ redact_metadata: input.redact_metadata ?? false,
199
+ disabled_at: null,
200
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
201
+ secret
202
+ };
203
+ this.endpoints.set(id, endpoint);
204
+ json(res, 201, webhookEndpointResponse(endpoint, true));
205
+ }
206
+ updateEndpoint(id, body, res) {
207
+ const endpoint = this.endpoints.get(id);
208
+ if (!endpoint) return json(res, 404, { code: "not_found" });
209
+ const input = body;
210
+ if ("description" in input) {
211
+ endpoint.description = input.description ?? null;
212
+ }
213
+ if ("enabled_events" in input) {
214
+ endpoint.enabled_events = input.enabled_events;
215
+ }
216
+ if ("redact_metadata" in input) {
217
+ endpoint.redact_metadata = input.redact_metadata;
218
+ }
219
+ json(res, 200, webhookEndpointResponse(endpoint));
220
+ }
221
+ rotateEndpointSecret(id, res) {
222
+ const endpoint = this.endpoints.get(id);
223
+ if (!endpoint) return json(res, 404, { code: "not_found" });
224
+ endpoint.secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`;
225
+ json(res, 200, webhookEndpointResponse(endpoint, true));
226
+ }
227
+ // 暴露给测试:触发一次签名后的“delivery”,便于 webhook verifier 端到端测试。
228
+ buildSignedFixture(endpointId, eventType, payload) {
229
+ const endpoint = this.endpoints.get(endpointId);
230
+ if (!endpoint) throw new Error("endpoint not found");
231
+ const rawBody = JSON.stringify({ type: eventType, data: payload });
232
+ const timestamp = Math.floor(Date.now() / 1e3);
233
+ return {
234
+ header: buildSignatureHeader({ secret: endpoint.secret, timestamp, rawBody }),
235
+ rawBody,
236
+ secret: endpoint.secret
237
+ };
238
+ }
239
+ };
240
+ async function readBody(req) {
241
+ const chunks = [];
242
+ for await (const chunk of req) chunks.push(chunk);
243
+ if (chunks.length === 0) return null;
244
+ const text = Buffer.concat(chunks).toString("utf8");
245
+ try {
246
+ return JSON.parse(text);
247
+ } catch {
248
+ return text;
249
+ }
250
+ }
251
+ function json(res, status, body) {
252
+ res.statusCode = status;
253
+ res.end(JSON.stringify(body));
254
+ }
255
+ function headerValue(req, name) {
256
+ const raw = req.headers[name];
257
+ if (Array.isArray(raw)) return raw[0];
258
+ return raw;
259
+ }
260
+ function webhookEndpointResponse(endpoint, includeSecret = false) {
261
+ const { secret, ...response } = endpoint;
262
+ return includeSecret ? { ...response, secret } : response;
263
+ }
264
+ // Annotate the CommonJS export names for ESM import in node:
265
+ 0 && (module.exports = {
266
+ MockServer
267
+ });
268
+ //# sourceMappingURL=mock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mock-entry.ts","../src/mock-server.ts","../src/signature.ts"],"sourcesContent":["export { MockServer, type MockServerOptions } from './mock-server'\n","import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\n\nimport { buildSignatureHeader } from './signature'\n\n// 极简内存版 mock,用于 SDK 集成测试与本地 docs 示例。\n// 仅模拟 v1/payment-orders 创建/查询/取消、v1/events 列表、v1/webhook-endpoints CRUD,\n// 以及签名 fixture 构造。生产环境的 NestJS 服务才是参考实现,这里只做开发体感。\n\nexport type MockServerOptions = {\n port?: number\n host?: string\n // 测试时常希望注入固定 id 生成器,便于断言。\n idFactory?: () => string\n // 与资源 id 分离,避免固定 id 时所有轮换都得到同一个 secret。\n secretFactory?: () => string\n}\n\ntype PaymentOrder = {\n id: string\n merchant_order_id: string\n scenario: string\n amount: string\n requested_amount: string\n settlement_asset: string\n status:\n | 'created'\n | 'detected'\n | 'confirmed'\n | 'finalized'\n | 'reverted'\n | 'expired'\n | 'canceled'\n expires_at: string | null\n metadata: unknown\n created_at: string\n accepted_assets: { chain: string; asset: string }[]\n payment_instructions: { chain: string; asset: string; address: string }[]\n timeline: { from: string | null; to: string; reason: string | null; at: string }[]\n}\n\ntype WebhookEndpoint = {\n id: string\n url: string\n description: string | null\n enabled_events: string[]\n redact_metadata: boolean\n disabled_at: string | null\n created_at: string\n secret: string\n}\n\nexport class MockServer {\n private readonly server: Server\n private readonly orders = new Map<string, PaymentOrder>()\n private readonly endpoints = new Map<string, WebhookEndpoint>()\n private readonly idempotency = new Map<string, PaymentOrder>()\n private readonly idFactory: () => string\n private readonly secretFactory: () => string\n\n constructor(private readonly options: MockServerOptions = {}) {\n this.idFactory = options.idFactory ?? (() => randomUUID())\n this.secretFactory = options.secretFactory ?? (() => randomUUID())\n this.server = createServer((req, res) => this.handle(req, res))\n }\n\n listen(): Promise<{ url: string }> {\n return new Promise((resolve) => {\n this.server.listen(this.options.port ?? 0, this.options.host ?? '127.0.0.1', () => {\n const address = this.server.address()\n if (typeof address === 'string' || address === null) {\n resolve({ url: `http://${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 0}` })\n return\n }\n resolve({ url: `http://${address.address}:${address.port}` })\n })\n })\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.server.close((err) => (err ? reject(err) : resolve()))\n })\n }\n\n // 暴露用于断言的内部状态。\n snapshot() {\n return {\n orders: Array.from(this.orders.values()),\n endpoints: Array.from(this.endpoints.values()),\n }\n }\n\n private async handle(req: IncomingMessage, res: ServerResponse) {\n const url = new URL(req.url ?? '/', 'http://localhost')\n const body = await readBody(req)\n res.setHeader('content-type', 'application/json')\n\n try {\n if (req.method === 'POST' && url.pathname === '/v1/payment-orders') {\n return this.createOrder(req, body, res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/payment-orders') {\n return json(res, 200, { items: Array.from(this.orders.values()) })\n }\n const orderMatch = url.pathname.match(/^\\/v1\\/payment-orders\\/([^/]+)$/u)\n if (req.method === 'GET' && orderMatch) {\n const order = this.orders.get(orderMatch[1])\n return order ? json(res, 200, order) : json(res, 404, { code: 'not_found' })\n }\n const cancelMatch = url.pathname.match(/^\\/v1\\/payment-orders\\/([^/]+)\\/cancel$/u)\n if (req.method === 'POST' && cancelMatch) {\n return this.cancelOrder(cancelMatch[1], res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/events') {\n return json(res, 200, { items: [] })\n }\n\n if (req.method === 'POST' && url.pathname === '/v1/webhook-endpoints') {\n return this.createEndpoint(body, res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/webhook-endpoints') {\n return json(res, 200, {\n items: Array.from(this.endpoints.values(), (endpoint) =>\n webhookEndpointResponse(endpoint),\n ),\n })\n }\n const endpointMatch = url.pathname.match(/^\\/v1\\/webhook-endpoints\\/([^/]+)$/u)\n if (req.method === 'PATCH' && endpointMatch) {\n return this.updateEndpoint(endpointMatch[1], body, res)\n }\n const rotateMatch = url.pathname.match(\n /^\\/v1\\/webhook-endpoints\\/([^/]+)\\/rotate-secret$/u,\n )\n if (req.method === 'POST' && rotateMatch) {\n return this.rotateEndpointSecret(rotateMatch[1], res)\n }\n\n json(res, 404, { code: 'not_found' })\n } catch (err) {\n json(res, 500, { code: 'internal', message: String(err) })\n }\n }\n\n private createOrder(req: IncomingMessage, body: unknown, res: ServerResponse) {\n const idem = headerValue(req, 'idempotency-key')\n if (idem && this.idempotency.has(idem)) {\n const previous = this.idempotency.get(idem)!\n return json(res, 201, previous)\n }\n const input = body as Record<string, unknown>\n const id = this.idFactory()\n const accepted = (input.accepted_assets as { chain: string; asset: string }[]) ?? []\n const order: PaymentOrder = {\n id,\n merchant_order_id: String(input.merchant_order_id ?? ''),\n scenario: String(input.scenario ?? 'generic'),\n amount: String(input.amount ?? '0'),\n requested_amount: String(input.amount ?? '0'),\n settlement_asset: String(input.settlement_asset ?? 'USDC'),\n status: 'created',\n expires_at: (input.expires_at as string | undefined) ?? null,\n metadata: input.metadata ?? null,\n created_at: new Date().toISOString(),\n accepted_assets: accepted,\n payment_instructions:\n accepted.length > 0\n ? accepted.map((entry, index) => ({\n chain: entry.chain,\n asset: entry.asset,\n address: `0xMOCK${id.slice(0, 8)}${index}`,\n }))\n : [{ chain: 'base', asset: 'USDC', address: `0xMOCK${id.slice(0, 8)}0` }],\n timeline: [\n { from: null, to: 'created', reason: 'order_created', at: new Date().toISOString() },\n ],\n }\n this.orders.set(id, order)\n if (idem) this.idempotency.set(idem, order)\n json(res, 201, order)\n }\n\n private cancelOrder(id: string, res: ServerResponse) {\n const order = this.orders.get(id)\n if (!order) return json(res, 404, { code: 'not_found' })\n order.status = 'canceled'\n order.timeline.push({\n from: order.timeline[order.timeline.length - 1]?.to ?? 'created',\n to: 'canceled',\n reason: 'manual_cancel',\n at: new Date().toISOString(),\n })\n json(res, 200, order)\n }\n\n private createEndpoint(body: unknown, res: ServerResponse) {\n const input = body as Record<string, unknown>\n const id = this.idFactory()\n const secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`\n const endpoint: WebhookEndpoint = {\n id,\n url: String(input.url ?? ''),\n description: (input.description as string | null) ?? null,\n enabled_events: (input.enabled_events as string[]) ?? [],\n redact_metadata: (input.redact_metadata as boolean | undefined) ?? false,\n disabled_at: null,\n created_at: new Date().toISOString(),\n secret,\n }\n this.endpoints.set(id, endpoint)\n json(res, 201, webhookEndpointResponse(endpoint, true))\n }\n\n private updateEndpoint(id: string, body: unknown, res: ServerResponse) {\n const endpoint = this.endpoints.get(id)\n if (!endpoint) return json(res, 404, { code: 'not_found' })\n const input = body as Record<string, unknown>\n if ('description' in input) {\n endpoint.description = (input.description as string | null) ?? null\n }\n if ('enabled_events' in input) {\n endpoint.enabled_events = input.enabled_events as string[]\n }\n if ('redact_metadata' in input) {\n endpoint.redact_metadata = input.redact_metadata as boolean\n }\n json(res, 200, webhookEndpointResponse(endpoint))\n }\n\n private rotateEndpointSecret(id: string, res: ServerResponse) {\n const endpoint = this.endpoints.get(id)\n if (!endpoint) return json(res, 404, { code: 'not_found' })\n endpoint.secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`\n json(res, 200, webhookEndpointResponse(endpoint, true))\n }\n\n // 暴露给测试:触发一次签名后的“delivery”,便于 webhook verifier 端到端测试。\n buildSignedFixture(endpointId: string, eventType: string, payload: unknown) {\n const endpoint = this.endpoints.get(endpointId)\n if (!endpoint) throw new Error('endpoint not found')\n const rawBody = JSON.stringify({ type: eventType, data: payload })\n const timestamp = Math.floor(Date.now() / 1000)\n return {\n header: buildSignatureHeader({ secret: endpoint.secret, timestamp, rawBody }),\n rawBody,\n secret: endpoint.secret,\n }\n }\n}\n\nasync function readBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n if (chunks.length === 0) return null\n const text = Buffer.concat(chunks).toString('utf8')\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status\n res.end(JSON.stringify(body))\n}\n\nfunction headerValue(req: IncomingMessage, name: string): string | undefined {\n const raw = req.headers[name]\n if (Array.isArray(raw)) return raw[0]\n return raw\n}\n\nfunction webhookEndpointResponse(\n endpoint: WebhookEndpoint,\n includeSecret = false,\n): Omit<WebhookEndpoint, 'secret'> & { secret?: string } {\n const { secret, ...response } = endpoint\n return includeSecret ? { ...response, secret } : response\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto'\n\n// 与服务端 webhook 签名格式保持一致:\n// X-Product-Signature: t=<unix_ts>,v1=<hmac_sha256(t.rawBody)>\n// SDK 内联实现,避免发布包依赖内部 workspace 包。\n\nexport const SIGNATURE_HEADER = 'X-Product-Signature'\nexport const EVENT_ID_HEADER = 'X-Event-Id'\nexport const DELIVERY_ID_HEADER = 'X-Delivery-Id'\nexport const DEFAULT_TOLERANCE_SECONDS = 5 * 60\n\nexport type SignatureBuildInput = {\n secret: string\n timestamp: number\n rawBody: string\n}\n\nexport type MultiSignatureBuildInput = {\n secrets: readonly string[]\n timestamp: number\n rawBody: string\n}\n\nexport type VerifyInput = {\n secrets: readonly string[]\n header: string | undefined\n rawBody: string\n now?: number\n toleranceSeconds?: number\n}\n\nexport type VerifyResult =\n | { ok: true; timestamp: number }\n | {\n ok: false\n reason:\n | 'missing_header'\n | 'invalid_format'\n | 'timestamp_expired'\n | 'bad_signature'\n }\n\nexport function buildSignatureHeader({\n secret,\n timestamp,\n rawBody,\n}: SignatureBuildInput): string {\n return buildSignatureHeaderForSecrets({\n secrets: [secret],\n timestamp,\n rawBody,\n })\n}\n\nexport function buildSignatureHeaderForSecrets({\n secrets,\n timestamp,\n rawBody,\n}: MultiSignatureBuildInput): string {\n const payload = `${timestamp}.${rawBody}`\n const signatures = secrets\n .filter((secret) => secret.length > 0)\n .map((secret) => createHmac('sha256', secret).update(payload).digest('hex'))\n return `t=${timestamp},${signatures.map((signature) => `v1=${signature}`).join(',')}`\n}\n\nexport function verifySignature(input: VerifyInput): VerifyResult {\n if (!input.header) return { ok: false, reason: 'missing_header' }\n const parsed = parseHeader(input.header)\n if (!parsed) return { ok: false, reason: 'invalid_format' }\n\n const now = input.now ?? Math.floor(Date.now() / 1000)\n const tolerance = input.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS\n if (Math.abs(now - parsed.timestamp) > tolerance) {\n return { ok: false, reason: 'timestamp_expired' }\n }\n\n const payload = `${parsed.timestamp}.${input.rawBody}`\n for (const secret of input.secrets) {\n if (!secret) continue\n const expected = createHmac('sha256', secret).update(payload).digest('hex')\n for (const signature of parsed.signatures) {\n if (safeEqualHex(expected, signature)) {\n return { ok: true, timestamp: parsed.timestamp }\n }\n }\n }\n return { ok: false, reason: 'bad_signature' }\n}\n\nfunction parseHeader(\n header: string,\n): { timestamp: number; signatures: string[] } | null {\n let timestamp: number | null = null\n const signatures: string[] = []\n for (const segment of header.split(',')) {\n const [key, value] = segment.trim().split('=')\n if (!key || !value) continue\n if (key === 't') {\n const ts = Number(value)\n if (Number.isFinite(ts)) timestamp = ts\n } else if (key === 'v1') {\n signatures.push(value)\n }\n }\n if (timestamp === null || signatures.length === 0) return null\n return { timestamp, signatures }\n}\n\nfunction safeEqualHex(a: string, b: string): boolean {\n if (!isSha256Hex(a) || !isSha256Hex(b)) return false\n const aBuffer = Buffer.from(a, 'hex')\n const bBuffer = Buffer.from(b, 'hex')\n return timingSafeEqual(aBuffer, bBuffer)\n}\n\nfunction isSha256Hex(value: string): boolean {\n return /^[a-f0-9]{64}$/i.test(value)\n}\n"],"mappings":"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","names":["import_node_crypto"]}
package/dist/mock.mjs ADDED
@@ -0,0 +1,219 @@
1
+ import {
2
+ buildSignatureHeader
3
+ } from "./chunk-B2JLHYXK.mjs";
4
+
5
+ // src/mock-server.ts
6
+ import { createServer } from "http";
7
+ import { randomUUID } from "crypto";
8
+ var MockServer = class {
9
+ constructor(options = {}) {
10
+ this.options = options;
11
+ this.idFactory = options.idFactory ?? (() => randomUUID());
12
+ this.secretFactory = options.secretFactory ?? (() => randomUUID());
13
+ this.server = createServer((req, res) => this.handle(req, res));
14
+ }
15
+ options;
16
+ server;
17
+ orders = /* @__PURE__ */ new Map();
18
+ endpoints = /* @__PURE__ */ new Map();
19
+ idempotency = /* @__PURE__ */ new Map();
20
+ idFactory;
21
+ secretFactory;
22
+ listen() {
23
+ return new Promise((resolve) => {
24
+ this.server.listen(this.options.port ?? 0, this.options.host ?? "127.0.0.1", () => {
25
+ const address = this.server.address();
26
+ if (typeof address === "string" || address === null) {
27
+ resolve({ url: `http://${this.options.host ?? "127.0.0.1"}:${this.options.port ?? 0}` });
28
+ return;
29
+ }
30
+ resolve({ url: `http://${address.address}:${address.port}` });
31
+ });
32
+ });
33
+ }
34
+ async close() {
35
+ await new Promise((resolve, reject) => {
36
+ this.server.close((err) => err ? reject(err) : resolve());
37
+ });
38
+ }
39
+ // 暴露用于断言的内部状态。
40
+ snapshot() {
41
+ return {
42
+ orders: Array.from(this.orders.values()),
43
+ endpoints: Array.from(this.endpoints.values())
44
+ };
45
+ }
46
+ async handle(req, res) {
47
+ const url = new URL(req.url ?? "/", "http://localhost");
48
+ const body = await readBody(req);
49
+ res.setHeader("content-type", "application/json");
50
+ try {
51
+ if (req.method === "POST" && url.pathname === "/v1/payment-orders") {
52
+ return this.createOrder(req, body, res);
53
+ }
54
+ if (req.method === "GET" && url.pathname === "/v1/payment-orders") {
55
+ return json(res, 200, { items: Array.from(this.orders.values()) });
56
+ }
57
+ const orderMatch = url.pathname.match(/^\/v1\/payment-orders\/([^/]+)$/u);
58
+ if (req.method === "GET" && orderMatch) {
59
+ const order = this.orders.get(orderMatch[1]);
60
+ return order ? json(res, 200, order) : json(res, 404, { code: "not_found" });
61
+ }
62
+ const cancelMatch = url.pathname.match(/^\/v1\/payment-orders\/([^/]+)\/cancel$/u);
63
+ if (req.method === "POST" && cancelMatch) {
64
+ return this.cancelOrder(cancelMatch[1], res);
65
+ }
66
+ if (req.method === "GET" && url.pathname === "/v1/events") {
67
+ return json(res, 200, { items: [] });
68
+ }
69
+ if (req.method === "POST" && url.pathname === "/v1/webhook-endpoints") {
70
+ return this.createEndpoint(body, res);
71
+ }
72
+ if (req.method === "GET" && url.pathname === "/v1/webhook-endpoints") {
73
+ return json(res, 200, {
74
+ items: Array.from(
75
+ this.endpoints.values(),
76
+ (endpoint) => webhookEndpointResponse(endpoint)
77
+ )
78
+ });
79
+ }
80
+ const endpointMatch = url.pathname.match(/^\/v1\/webhook-endpoints\/([^/]+)$/u);
81
+ if (req.method === "PATCH" && endpointMatch) {
82
+ return this.updateEndpoint(endpointMatch[1], body, res);
83
+ }
84
+ const rotateMatch = url.pathname.match(
85
+ /^\/v1\/webhook-endpoints\/([^/]+)\/rotate-secret$/u
86
+ );
87
+ if (req.method === "POST" && rotateMatch) {
88
+ return this.rotateEndpointSecret(rotateMatch[1], res);
89
+ }
90
+ json(res, 404, { code: "not_found" });
91
+ } catch (err) {
92
+ json(res, 500, { code: "internal", message: String(err) });
93
+ }
94
+ }
95
+ createOrder(req, body, res) {
96
+ const idem = headerValue(req, "idempotency-key");
97
+ if (idem && this.idempotency.has(idem)) {
98
+ const previous = this.idempotency.get(idem);
99
+ return json(res, 201, previous);
100
+ }
101
+ const input = body;
102
+ const id = this.idFactory();
103
+ const accepted = input.accepted_assets ?? [];
104
+ const order = {
105
+ id,
106
+ merchant_order_id: String(input.merchant_order_id ?? ""),
107
+ scenario: String(input.scenario ?? "generic"),
108
+ amount: String(input.amount ?? "0"),
109
+ requested_amount: String(input.amount ?? "0"),
110
+ settlement_asset: String(input.settlement_asset ?? "USDC"),
111
+ status: "created",
112
+ expires_at: input.expires_at ?? null,
113
+ metadata: input.metadata ?? null,
114
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
115
+ accepted_assets: accepted,
116
+ payment_instructions: accepted.length > 0 ? accepted.map((entry, index) => ({
117
+ chain: entry.chain,
118
+ asset: entry.asset,
119
+ address: `0xMOCK${id.slice(0, 8)}${index}`
120
+ })) : [{ chain: "base", asset: "USDC", address: `0xMOCK${id.slice(0, 8)}0` }],
121
+ timeline: [
122
+ { from: null, to: "created", reason: "order_created", at: (/* @__PURE__ */ new Date()).toISOString() }
123
+ ]
124
+ };
125
+ this.orders.set(id, order);
126
+ if (idem) this.idempotency.set(idem, order);
127
+ json(res, 201, order);
128
+ }
129
+ cancelOrder(id, res) {
130
+ const order = this.orders.get(id);
131
+ if (!order) return json(res, 404, { code: "not_found" });
132
+ order.status = "canceled";
133
+ order.timeline.push({
134
+ from: order.timeline[order.timeline.length - 1]?.to ?? "created",
135
+ to: "canceled",
136
+ reason: "manual_cancel",
137
+ at: (/* @__PURE__ */ new Date()).toISOString()
138
+ });
139
+ json(res, 200, order);
140
+ }
141
+ createEndpoint(body, res) {
142
+ const input = body;
143
+ const id = this.idFactory();
144
+ const secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`;
145
+ const endpoint = {
146
+ id,
147
+ url: String(input.url ?? ""),
148
+ description: input.description ?? null,
149
+ enabled_events: input.enabled_events ?? [],
150
+ redact_metadata: input.redact_metadata ?? false,
151
+ disabled_at: null,
152
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
153
+ secret
154
+ };
155
+ this.endpoints.set(id, endpoint);
156
+ json(res, 201, webhookEndpointResponse(endpoint, true));
157
+ }
158
+ updateEndpoint(id, body, res) {
159
+ const endpoint = this.endpoints.get(id);
160
+ if (!endpoint) return json(res, 404, { code: "not_found" });
161
+ const input = body;
162
+ if ("description" in input) {
163
+ endpoint.description = input.description ?? null;
164
+ }
165
+ if ("enabled_events" in input) {
166
+ endpoint.enabled_events = input.enabled_events;
167
+ }
168
+ if ("redact_metadata" in input) {
169
+ endpoint.redact_metadata = input.redact_metadata;
170
+ }
171
+ json(res, 200, webhookEndpointResponse(endpoint));
172
+ }
173
+ rotateEndpointSecret(id, res) {
174
+ const endpoint = this.endpoints.get(id);
175
+ if (!endpoint) return json(res, 404, { code: "not_found" });
176
+ endpoint.secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`;
177
+ json(res, 200, webhookEndpointResponse(endpoint, true));
178
+ }
179
+ // 暴露给测试:触发一次签名后的“delivery”,便于 webhook verifier 端到端测试。
180
+ buildSignedFixture(endpointId, eventType, payload) {
181
+ const endpoint = this.endpoints.get(endpointId);
182
+ if (!endpoint) throw new Error("endpoint not found");
183
+ const rawBody = JSON.stringify({ type: eventType, data: payload });
184
+ const timestamp = Math.floor(Date.now() / 1e3);
185
+ return {
186
+ header: buildSignatureHeader({ secret: endpoint.secret, timestamp, rawBody }),
187
+ rawBody,
188
+ secret: endpoint.secret
189
+ };
190
+ }
191
+ };
192
+ async function readBody(req) {
193
+ const chunks = [];
194
+ for await (const chunk of req) chunks.push(chunk);
195
+ if (chunks.length === 0) return null;
196
+ const text = Buffer.concat(chunks).toString("utf8");
197
+ try {
198
+ return JSON.parse(text);
199
+ } catch {
200
+ return text;
201
+ }
202
+ }
203
+ function json(res, status, body) {
204
+ res.statusCode = status;
205
+ res.end(JSON.stringify(body));
206
+ }
207
+ function headerValue(req, name) {
208
+ const raw = req.headers[name];
209
+ if (Array.isArray(raw)) return raw[0];
210
+ return raw;
211
+ }
212
+ function webhookEndpointResponse(endpoint, includeSecret = false) {
213
+ const { secret, ...response } = endpoint;
214
+ return includeSecret ? { ...response, secret } : response;
215
+ }
216
+ export {
217
+ MockServer
218
+ };
219
+ //# sourceMappingURL=mock.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mock-server.ts"],"sourcesContent":["import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'\nimport { randomUUID } from 'node:crypto'\n\nimport { buildSignatureHeader } from './signature'\n\n// 极简内存版 mock,用于 SDK 集成测试与本地 docs 示例。\n// 仅模拟 v1/payment-orders 创建/查询/取消、v1/events 列表、v1/webhook-endpoints CRUD,\n// 以及签名 fixture 构造。生产环境的 NestJS 服务才是参考实现,这里只做开发体感。\n\nexport type MockServerOptions = {\n port?: number\n host?: string\n // 测试时常希望注入固定 id 生成器,便于断言。\n idFactory?: () => string\n // 与资源 id 分离,避免固定 id 时所有轮换都得到同一个 secret。\n secretFactory?: () => string\n}\n\ntype PaymentOrder = {\n id: string\n merchant_order_id: string\n scenario: string\n amount: string\n requested_amount: string\n settlement_asset: string\n status:\n | 'created'\n | 'detected'\n | 'confirmed'\n | 'finalized'\n | 'reverted'\n | 'expired'\n | 'canceled'\n expires_at: string | null\n metadata: unknown\n created_at: string\n accepted_assets: { chain: string; asset: string }[]\n payment_instructions: { chain: string; asset: string; address: string }[]\n timeline: { from: string | null; to: string; reason: string | null; at: string }[]\n}\n\ntype WebhookEndpoint = {\n id: string\n url: string\n description: string | null\n enabled_events: string[]\n redact_metadata: boolean\n disabled_at: string | null\n created_at: string\n secret: string\n}\n\nexport class MockServer {\n private readonly server: Server\n private readonly orders = new Map<string, PaymentOrder>()\n private readonly endpoints = new Map<string, WebhookEndpoint>()\n private readonly idempotency = new Map<string, PaymentOrder>()\n private readonly idFactory: () => string\n private readonly secretFactory: () => string\n\n constructor(private readonly options: MockServerOptions = {}) {\n this.idFactory = options.idFactory ?? (() => randomUUID())\n this.secretFactory = options.secretFactory ?? (() => randomUUID())\n this.server = createServer((req, res) => this.handle(req, res))\n }\n\n listen(): Promise<{ url: string }> {\n return new Promise((resolve) => {\n this.server.listen(this.options.port ?? 0, this.options.host ?? '127.0.0.1', () => {\n const address = this.server.address()\n if (typeof address === 'string' || address === null) {\n resolve({ url: `http://${this.options.host ?? '127.0.0.1'}:${this.options.port ?? 0}` })\n return\n }\n resolve({ url: `http://${address.address}:${address.port}` })\n })\n })\n }\n\n async close(): Promise<void> {\n await new Promise<void>((resolve, reject) => {\n this.server.close((err) => (err ? reject(err) : resolve()))\n })\n }\n\n // 暴露用于断言的内部状态。\n snapshot() {\n return {\n orders: Array.from(this.orders.values()),\n endpoints: Array.from(this.endpoints.values()),\n }\n }\n\n private async handle(req: IncomingMessage, res: ServerResponse) {\n const url = new URL(req.url ?? '/', 'http://localhost')\n const body = await readBody(req)\n res.setHeader('content-type', 'application/json')\n\n try {\n if (req.method === 'POST' && url.pathname === '/v1/payment-orders') {\n return this.createOrder(req, body, res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/payment-orders') {\n return json(res, 200, { items: Array.from(this.orders.values()) })\n }\n const orderMatch = url.pathname.match(/^\\/v1\\/payment-orders\\/([^/]+)$/u)\n if (req.method === 'GET' && orderMatch) {\n const order = this.orders.get(orderMatch[1])\n return order ? json(res, 200, order) : json(res, 404, { code: 'not_found' })\n }\n const cancelMatch = url.pathname.match(/^\\/v1\\/payment-orders\\/([^/]+)\\/cancel$/u)\n if (req.method === 'POST' && cancelMatch) {\n return this.cancelOrder(cancelMatch[1], res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/events') {\n return json(res, 200, { items: [] })\n }\n\n if (req.method === 'POST' && url.pathname === '/v1/webhook-endpoints') {\n return this.createEndpoint(body, res)\n }\n if (req.method === 'GET' && url.pathname === '/v1/webhook-endpoints') {\n return json(res, 200, {\n items: Array.from(this.endpoints.values(), (endpoint) =>\n webhookEndpointResponse(endpoint),\n ),\n })\n }\n const endpointMatch = url.pathname.match(/^\\/v1\\/webhook-endpoints\\/([^/]+)$/u)\n if (req.method === 'PATCH' && endpointMatch) {\n return this.updateEndpoint(endpointMatch[1], body, res)\n }\n const rotateMatch = url.pathname.match(\n /^\\/v1\\/webhook-endpoints\\/([^/]+)\\/rotate-secret$/u,\n )\n if (req.method === 'POST' && rotateMatch) {\n return this.rotateEndpointSecret(rotateMatch[1], res)\n }\n\n json(res, 404, { code: 'not_found' })\n } catch (err) {\n json(res, 500, { code: 'internal', message: String(err) })\n }\n }\n\n private createOrder(req: IncomingMessage, body: unknown, res: ServerResponse) {\n const idem = headerValue(req, 'idempotency-key')\n if (idem && this.idempotency.has(idem)) {\n const previous = this.idempotency.get(idem)!\n return json(res, 201, previous)\n }\n const input = body as Record<string, unknown>\n const id = this.idFactory()\n const accepted = (input.accepted_assets as { chain: string; asset: string }[]) ?? []\n const order: PaymentOrder = {\n id,\n merchant_order_id: String(input.merchant_order_id ?? ''),\n scenario: String(input.scenario ?? 'generic'),\n amount: String(input.amount ?? '0'),\n requested_amount: String(input.amount ?? '0'),\n settlement_asset: String(input.settlement_asset ?? 'USDC'),\n status: 'created',\n expires_at: (input.expires_at as string | undefined) ?? null,\n metadata: input.metadata ?? null,\n created_at: new Date().toISOString(),\n accepted_assets: accepted,\n payment_instructions:\n accepted.length > 0\n ? accepted.map((entry, index) => ({\n chain: entry.chain,\n asset: entry.asset,\n address: `0xMOCK${id.slice(0, 8)}${index}`,\n }))\n : [{ chain: 'base', asset: 'USDC', address: `0xMOCK${id.slice(0, 8)}0` }],\n timeline: [\n { from: null, to: 'created', reason: 'order_created', at: new Date().toISOString() },\n ],\n }\n this.orders.set(id, order)\n if (idem) this.idempotency.set(idem, order)\n json(res, 201, order)\n }\n\n private cancelOrder(id: string, res: ServerResponse) {\n const order = this.orders.get(id)\n if (!order) return json(res, 404, { code: 'not_found' })\n order.status = 'canceled'\n order.timeline.push({\n from: order.timeline[order.timeline.length - 1]?.to ?? 'created',\n to: 'canceled',\n reason: 'manual_cancel',\n at: new Date().toISOString(),\n })\n json(res, 200, order)\n }\n\n private createEndpoint(body: unknown, res: ServerResponse) {\n const input = body as Record<string, unknown>\n const id = this.idFactory()\n const secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`\n const endpoint: WebhookEndpoint = {\n id,\n url: String(input.url ?? ''),\n description: (input.description as string | null) ?? null,\n enabled_events: (input.enabled_events as string[]) ?? [],\n redact_metadata: (input.redact_metadata as boolean | undefined) ?? false,\n disabled_at: null,\n created_at: new Date().toISOString(),\n secret,\n }\n this.endpoints.set(id, endpoint)\n json(res, 201, webhookEndpointResponse(endpoint, true))\n }\n\n private updateEndpoint(id: string, body: unknown, res: ServerResponse) {\n const endpoint = this.endpoints.get(id)\n if (!endpoint) return json(res, 404, { code: 'not_found' })\n const input = body as Record<string, unknown>\n if ('description' in input) {\n endpoint.description = (input.description as string | null) ?? null\n }\n if ('enabled_events' in input) {\n endpoint.enabled_events = input.enabled_events as string[]\n }\n if ('redact_metadata' in input) {\n endpoint.redact_metadata = input.redact_metadata as boolean\n }\n json(res, 200, webhookEndpointResponse(endpoint))\n }\n\n private rotateEndpointSecret(id: string, res: ServerResponse) {\n const endpoint = this.endpoints.get(id)\n if (!endpoint) return json(res, 404, { code: 'not_found' })\n endpoint.secret = `whsec_mock_${this.secretFactory().slice(0, 12)}`\n json(res, 200, webhookEndpointResponse(endpoint, true))\n }\n\n // 暴露给测试:触发一次签名后的“delivery”,便于 webhook verifier 端到端测试。\n buildSignedFixture(endpointId: string, eventType: string, payload: unknown) {\n const endpoint = this.endpoints.get(endpointId)\n if (!endpoint) throw new Error('endpoint not found')\n const rawBody = JSON.stringify({ type: eventType, data: payload })\n const timestamp = Math.floor(Date.now() / 1000)\n return {\n header: buildSignatureHeader({ secret: endpoint.secret, timestamp, rawBody }),\n rawBody,\n secret: endpoint.secret,\n }\n }\n}\n\nasync function readBody(req: IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = []\n for await (const chunk of req) chunks.push(chunk as Buffer)\n if (chunks.length === 0) return null\n const text = Buffer.concat(chunks).toString('utf8')\n try {\n return JSON.parse(text)\n } catch {\n return text\n }\n}\n\nfunction json(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status\n res.end(JSON.stringify(body))\n}\n\nfunction headerValue(req: IncomingMessage, name: string): string | undefined {\n const raw = req.headers[name]\n if (Array.isArray(raw)) return raw[0]\n return raw\n}\n\nfunction webhookEndpointResponse(\n endpoint: WebhookEndpoint,\n includeSecret = false,\n): Omit<WebhookEndpoint, 'secret'> & { secret?: string } {\n const { secret, ...response } = endpoint\n return includeSecret ? { ...response, secret } : response\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,44 @@
1
+ declare const SIGNATURE_HEADER = "X-Product-Signature";
2
+ declare const EVENT_ID_HEADER = "X-Event-Id";
3
+ declare const DELIVERY_ID_HEADER = "X-Delivery-Id";
4
+ declare const DEFAULT_TOLERANCE_SECONDS: number;
5
+ type SignatureBuildInput = {
6
+ secret: string;
7
+ timestamp: number;
8
+ rawBody: string;
9
+ };
10
+ type MultiSignatureBuildInput = {
11
+ secrets: readonly string[];
12
+ timestamp: number;
13
+ rawBody: string;
14
+ };
15
+ type VerifyInput = {
16
+ secrets: readonly string[];
17
+ header: string | undefined;
18
+ rawBody: string;
19
+ now?: number;
20
+ toleranceSeconds?: number;
21
+ };
22
+ type VerifyResult = {
23
+ ok: true;
24
+ timestamp: number;
25
+ } | {
26
+ ok: false;
27
+ reason: 'missing_header' | 'invalid_format' | 'timestamp_expired' | 'bad_signature';
28
+ };
29
+ declare function buildSignatureHeader({ secret, timestamp, rawBody, }: SignatureBuildInput): string;
30
+ declare function buildSignatureHeaderForSecrets({ secrets, timestamp, rawBody, }: MultiSignatureBuildInput): string;
31
+
32
+ type WebhookVerifyInput = Omit<VerifyInput, 'secrets'> & ({
33
+ secret: string | readonly string[];
34
+ secrets?: never;
35
+ } | {
36
+ secrets: readonly string[];
37
+ secret?: never;
38
+ });
39
+ declare function verifySignature(input: WebhookVerifyInput): VerifyResult;
40
+ declare class WebhooksApi {
41
+ verify(input: WebhookVerifyInput): VerifyResult;
42
+ }
43
+
44
+ export { DEFAULT_TOLERANCE_SECONDS, DELIVERY_ID_HEADER, EVENT_ID_HEADER, type MultiSignatureBuildInput, SIGNATURE_HEADER, type SignatureBuildInput, type VerifyResult, type WebhookVerifyInput, WebhooksApi, buildSignatureHeader, buildSignatureHeaderForSecrets, verifySignature };
@@ -0,0 +1,44 @@
1
+ declare const SIGNATURE_HEADER = "X-Product-Signature";
2
+ declare const EVENT_ID_HEADER = "X-Event-Id";
3
+ declare const DELIVERY_ID_HEADER = "X-Delivery-Id";
4
+ declare const DEFAULT_TOLERANCE_SECONDS: number;
5
+ type SignatureBuildInput = {
6
+ secret: string;
7
+ timestamp: number;
8
+ rawBody: string;
9
+ };
10
+ type MultiSignatureBuildInput = {
11
+ secrets: readonly string[];
12
+ timestamp: number;
13
+ rawBody: string;
14
+ };
15
+ type VerifyInput = {
16
+ secrets: readonly string[];
17
+ header: string | undefined;
18
+ rawBody: string;
19
+ now?: number;
20
+ toleranceSeconds?: number;
21
+ };
22
+ type VerifyResult = {
23
+ ok: true;
24
+ timestamp: number;
25
+ } | {
26
+ ok: false;
27
+ reason: 'missing_header' | 'invalid_format' | 'timestamp_expired' | 'bad_signature';
28
+ };
29
+ declare function buildSignatureHeader({ secret, timestamp, rawBody, }: SignatureBuildInput): string;
30
+ declare function buildSignatureHeaderForSecrets({ secrets, timestamp, rawBody, }: MultiSignatureBuildInput): string;
31
+
32
+ type WebhookVerifyInput = Omit<VerifyInput, 'secrets'> & ({
33
+ secret: string | readonly string[];
34
+ secrets?: never;
35
+ } | {
36
+ secrets: readonly string[];
37
+ secret?: never;
38
+ });
39
+ declare function verifySignature(input: WebhookVerifyInput): VerifyResult;
40
+ declare class WebhooksApi {
41
+ verify(input: WebhookVerifyInput): VerifyResult;
42
+ }
43
+
44
+ export { DEFAULT_TOLERANCE_SECONDS, DELIVERY_ID_HEADER, EVENT_ID_HEADER, type MultiSignatureBuildInput, SIGNATURE_HEADER, type SignatureBuildInput, type VerifyResult, type WebhookVerifyInput, WebhooksApi, buildSignatureHeader, buildSignatureHeaderForSecrets, verifySignature };