@atribu/node 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/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/README.md +423 -0
- package/dist/admin/index.cjs +326 -0
- package/dist/admin/index.cjs.map +1 -0
- package/dist/admin/index.d.cts +46 -0
- package/dist/admin/index.d.ts +46 -0
- package/dist/admin/index.js +323 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/api.d-BXINTQo6.d.cts +3547 -0
- package/dist/api.d-BXINTQo6.d.ts +3547 -0
- package/dist/errors-D3ApBz8J.d.cts +86 -0
- package/dist/errors-D3ApBz8J.d.ts +86 -0
- package/dist/index.cjs +549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +198 -0
- package/dist/index.d.ts +198 -0
- package/dist/index.js +536 -0
- package/dist/index.js.map +1 -0
- package/dist/next/index.cjs +153 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +43 -0
- package/dist/next/index.d.ts +43 -0
- package/dist/next/index.js +151 -0
- package/dist/next/index.js.map +1 -0
- package/dist/oauth/index.cjs +299 -0
- package/dist/oauth/index.cjs.map +1 -0
- package/dist/oauth/index.d.cts +117 -0
- package/dist/oauth/index.d.ts +117 -0
- package/dist/oauth/index.js +291 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/test/index.cjs +443 -0
- package/dist/test/index.cjs.map +1 -0
- package/dist/test/index.d.cts +321 -0
- package/dist/test/index.d.ts +321 -0
- package/dist/test/index.js +437 -0
- package/dist/test/index.js.map +1 -0
- package/dist/types-Dc6tIN_V.d.cts +101 -0
- package/dist/types-Dc6tIN_V.d.ts +101 -0
- package/dist/webhooks/index.cjs +97 -0
- package/dist/webhooks/index.cjs.map +1 -0
- package/dist/webhooks/index.d.cts +35 -0
- package/dist/webhooks/index.d.ts +35 -0
- package/dist/webhooks/index.js +94 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +101 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var AtribuError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var AtribuWebhookError = class extends AtribuError {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/webhooks/verify.ts
|
|
19
|
+
async function verifyWebhook(opts) {
|
|
20
|
+
if (!opts.signature) {
|
|
21
|
+
throw new AtribuWebhookError("missing_signature", "Missing X-Atribu-Signature header");
|
|
22
|
+
}
|
|
23
|
+
const parsed = parseHeader(opts.signature);
|
|
24
|
+
const bodyString = typeof opts.rawBody === "string" ? opts.rawBody : decodeUtf8(opts.rawBody);
|
|
25
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
26
|
+
const tolerance = opts.tolerance ?? 300;
|
|
27
|
+
const ageSeconds = Math.abs(nowMs / 1e3 - parsed.t);
|
|
28
|
+
if (ageSeconds > tolerance) {
|
|
29
|
+
throw new AtribuWebhookError(
|
|
30
|
+
"expired_timestamp",
|
|
31
|
+
`Signed timestamp is ${Math.round(ageSeconds)}s old (tolerance ${tolerance}s)`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const signingInput = `${parsed.t}.${bodyString}`;
|
|
35
|
+
const matchesCurrent = await safeCompareHmac(opts.secret, signingInput, parsed.v1);
|
|
36
|
+
if (!matchesCurrent) {
|
|
37
|
+
const previous = opts.previousSecret;
|
|
38
|
+
if (!previous || !await safeCompareHmac(previous, signingInput, parsed.v1)) {
|
|
39
|
+
throw new AtribuWebhookError("invalid_signature", "Signature does not match");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(bodyString);
|
|
43
|
+
}
|
|
44
|
+
function parseHeader(header) {
|
|
45
|
+
let t = null;
|
|
46
|
+
let v1 = null;
|
|
47
|
+
for (const part of header.split(",")) {
|
|
48
|
+
const eq = part.indexOf("=");
|
|
49
|
+
if (eq < 0) continue;
|
|
50
|
+
const key = part.slice(0, eq).trim();
|
|
51
|
+
const value = part.slice(eq + 1).trim();
|
|
52
|
+
if (key === "t") t = Number(value);
|
|
53
|
+
else if (key === "v1") v1 = value;
|
|
54
|
+
}
|
|
55
|
+
if (t === null || !Number.isFinite(t) || !v1) {
|
|
56
|
+
throw new AtribuWebhookError("malformed_header", `Malformed signature header: ${header}`);
|
|
57
|
+
}
|
|
58
|
+
return { t, v1 };
|
|
59
|
+
}
|
|
60
|
+
async function safeCompareHmac(secret, input, expectedHex) {
|
|
61
|
+
const encoder = new TextEncoder();
|
|
62
|
+
const key = await crypto.subtle.importKey(
|
|
63
|
+
"raw",
|
|
64
|
+
encoder.encode(secret),
|
|
65
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
66
|
+
false,
|
|
67
|
+
["sign"]
|
|
68
|
+
);
|
|
69
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(input));
|
|
70
|
+
const computed = bufferToHex(sig);
|
|
71
|
+
return constantTimeEqualHex(computed, expectedHex);
|
|
72
|
+
}
|
|
73
|
+
function bufferToHex(buf) {
|
|
74
|
+
const bytes = new Uint8Array(buf);
|
|
75
|
+
let hex = "";
|
|
76
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
77
|
+
const b = bytes[i];
|
|
78
|
+
hex += (b < 16 ? "0" : "") + b.toString(16);
|
|
79
|
+
}
|
|
80
|
+
return hex;
|
|
81
|
+
}
|
|
82
|
+
function constantTimeEqualHex(a, b) {
|
|
83
|
+
if (a.length !== b.length) return false;
|
|
84
|
+
let diff = 0;
|
|
85
|
+
for (let i = 0; i < a.length; i++) {
|
|
86
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
87
|
+
}
|
|
88
|
+
return diff === 0;
|
|
89
|
+
}
|
|
90
|
+
function decodeUtf8(buf) {
|
|
91
|
+
return new TextDecoder("utf-8").decode(buf);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/next/webhook-handler.ts
|
|
95
|
+
function withAtribuWebhook(opts) {
|
|
96
|
+
if (!opts.secret) throw new Error("withAtribuWebhook: secret is required");
|
|
97
|
+
return async function handler(request) {
|
|
98
|
+
const rawBody = await request.text();
|
|
99
|
+
const signature = request.headers.get("x-atribu-signature");
|
|
100
|
+
const deliveryId = request.headers.get("x-atribu-delivery-id");
|
|
101
|
+
let event;
|
|
102
|
+
try {
|
|
103
|
+
event = await verifyWebhook({
|
|
104
|
+
rawBody,
|
|
105
|
+
signature,
|
|
106
|
+
secret: opts.secret,
|
|
107
|
+
previousSecret: opts.previousSecret ?? null,
|
|
108
|
+
tolerance: opts.tolerance
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof AtribuWebhookError) {
|
|
112
|
+
opts.onInvalidSignature?.(err, request);
|
|
113
|
+
return new Response(JSON.stringify({ error: err.code, message: err.message }), {
|
|
114
|
+
status: 401,
|
|
115
|
+
headers: { "Content-Type": "application/json" }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
const ctx = {
|
|
121
|
+
request,
|
|
122
|
+
deliveryId,
|
|
123
|
+
signatureTimestamp: parseTimestamp(signature)
|
|
124
|
+
};
|
|
125
|
+
try {
|
|
126
|
+
await opts.onEvent(event, ctx);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (opts.onError) opts.onError(err, event);
|
|
129
|
+
else console.error("[atribu webhook] onEvent threw:", err);
|
|
130
|
+
return new Response(JSON.stringify({ error: "handler_error" }), {
|
|
131
|
+
status: 500,
|
|
132
|
+
headers: { "Content-Type": "application/json" }
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return new Response(null, { status: 200 });
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function parseTimestamp(header) {
|
|
139
|
+
if (!header) return null;
|
|
140
|
+
for (const part of header.split(",")) {
|
|
141
|
+
const eq = part.indexOf("=");
|
|
142
|
+
if (eq < 0) continue;
|
|
143
|
+
const key = part.slice(0, eq).trim();
|
|
144
|
+
if (key !== "t") continue;
|
|
145
|
+
const n = Number(part.slice(eq + 1).trim());
|
|
146
|
+
return Number.isFinite(n) ? n : null;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports.withAtribuWebhook = withAtribuWebhook;
|
|
152
|
+
//# sourceMappingURL=index.cjs.map
|
|
153
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/webhooks/verify.ts","../../src/next/webhook-handler.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Atribu error class hierarchy.\n *\n * Why typed errors: consumers need to branch on auth-failure vs rate-limit vs\n * server-error vs validation-error to decide whether to retry, refresh\n * credentials, or surface to the user. A single `Error` with a status code\n * forces every consumer to write the same `if (err.status === 401)` ladder.\n *\n * Why no automatic retries: the SDK derives a `retry` hint from status +\n * Retry-After + error code, but consumers' queue/job systems decide whether\n * to act on it. Auto-retry inside the SDK hides backpressure signals and\n * makes error budgets opaque.\n */\n\nimport type { RetryHint } from \"./retry\";\n\nexport type ApiErrorCode =\n | \"unauthorized\"\n | \"forbidden\"\n | \"insufficient_scope\"\n | \"not_found\"\n | \"invalid_parameter\"\n | \"invalid_request\"\n | \"validation_error\"\n | \"invalid_content\"\n | \"invalid_date_range\"\n | \"rate_limit_exceeded\"\n | \"connection_not_ready\"\n | \"provider_error\"\n | \"service_unavailable\"\n | \"internal_error\"\n | string;\n\nexport class AtribuError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class AtribuConfigError extends AtribuError {}\n\nexport class AtribuTransportError extends AtribuError {\n readonly cause: unknown;\n constructor(message: string, cause: unknown) {\n super(message);\n this.cause = cause;\n }\n}\n\nexport interface ApiErrorBody {\n code: ApiErrorCode;\n message: string;\n status: number;\n request_id?: string;\n}\n\nexport class AtribuApiError extends AtribuError {\n readonly code: ApiErrorCode;\n readonly status: number;\n readonly requestId: string | null;\n readonly retry: RetryHint;\n readonly responseBody: unknown;\n\n constructor(args: {\n code: ApiErrorCode;\n message: string;\n status: number;\n requestId: string | null;\n retry: RetryHint;\n responseBody: unknown;\n }) {\n super(`[${args.code}] ${args.message}`);\n this.code = args.code;\n this.status = args.status;\n this.requestId = args.requestId;\n this.retry = args.retry;\n this.responseBody = args.responseBody;\n }\n\n isRetryable(): boolean {\n return this.retry.action === \"retry\" || this.retry.action === \"retry_after\";\n }\n isAuthFailure(): boolean {\n return this.status === 401 || this.code === \"unauthorized\";\n }\n isRateLimit(): boolean {\n return this.status === 429 || this.code === \"rate_limit_exceeded\";\n }\n}\n\nexport type OauthErrorCode =\n | \"invalid_request\"\n | \"invalid_client\"\n | \"invalid_grant\"\n | \"unauthorized_client\"\n | \"unsupported_grant_type\"\n | \"invalid_scope\"\n | \"server_error\"\n | \"unsupported_token_type\"\n | string;\n\nexport class AtribuOauthError extends AtribuError {\n readonly code: OauthErrorCode;\n readonly status: number;\n readonly description: string | null;\n\n constructor(args: { code: OauthErrorCode; description: string | null; status: number }) {\n super(`[oauth/${args.code}] ${args.description ?? args.code}`);\n this.code = args.code;\n this.status = args.status;\n this.description = args.description;\n }\n}\n\nexport type WebhookErrorCode =\n | \"missing_signature\"\n | \"malformed_header\"\n | \"expired_timestamp\"\n | \"invalid_signature\";\n\nexport class AtribuWebhookError extends AtribuError {\n readonly code: WebhookErrorCode;\n constructor(code: WebhookErrorCode, message: string) {\n super(message);\n this.code = code;\n }\n}\n","/**\n * Verify Atribu webhook signatures using Web Crypto.\n *\n * Why Web Crypto: works in Node 18+, Bun, Deno, Vercel Edge, Cloudflare\n * Workers. No `node:crypto` import keeps this subpath edge-safe.\n *\n * Header format (Stripe-style): `t=<unix_seconds>,v1=<hex_hmac_sha256>`\n * Signed string: `<t>.<rawBody>`\n *\n * Rotation: pass `previousSecret` during the grace window after rotating.\n * Atribu always signs with the current secret; the previous slot exists\n * only so subscribers can dual-verify during their own deploy.\n */\n\nimport { AtribuWebhookError } from \"../errors\";\nimport type { AtribuWebhookEvent } from \"./types\";\n\nexport interface VerifyWebhookOptions {\n /** Raw, unparsed request body (string or Uint8Array). */\n rawBody: string | Uint8Array;\n /** Value of the `X-Atribu-Signature` header. */\n signature: string | null | undefined;\n /** Current HMAC secret. */\n secret: string;\n /** Previous secret during rotation grace. */\n previousSecret?: string | null;\n /** Max age of the signed timestamp in seconds. Default 300 (5 minutes). */\n tolerance?: number;\n /** Inject for tests; defaults to Date.now(). */\n now?: () => number;\n}\n\ninterface ParsedHeader {\n t: number;\n v1: string;\n}\n\nexport async function verifyWebhook(opts: VerifyWebhookOptions): Promise<AtribuWebhookEvent> {\n if (!opts.signature) {\n throw new AtribuWebhookError(\"missing_signature\", \"Missing X-Atribu-Signature header\");\n }\n\n const parsed = parseHeader(opts.signature);\n const bodyString = typeof opts.rawBody === \"string\" ? opts.rawBody : decodeUtf8(opts.rawBody);\n\n const nowMs = (opts.now ?? Date.now)();\n const tolerance = opts.tolerance ?? 300;\n const ageSeconds = Math.abs(nowMs / 1000 - parsed.t);\n if (ageSeconds > tolerance) {\n throw new AtribuWebhookError(\n \"expired_timestamp\",\n `Signed timestamp is ${Math.round(ageSeconds)}s old (tolerance ${tolerance}s)`,\n );\n }\n\n const signingInput = `${parsed.t}.${bodyString}`;\n const matchesCurrent = await safeCompareHmac(opts.secret, signingInput, parsed.v1);\n if (!matchesCurrent) {\n const previous = opts.previousSecret;\n if (!previous || !(await safeCompareHmac(previous, signingInput, parsed.v1))) {\n throw new AtribuWebhookError(\"invalid_signature\", \"Signature does not match\");\n }\n }\n\n return JSON.parse(bodyString) as AtribuWebhookEvent;\n}\n\nfunction parseHeader(header: string): ParsedHeader {\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === \"t\") t = Number(value);\n else if (key === \"v1\") v1 = value;\n }\n if (t === null || !Number.isFinite(t) || !v1) {\n throw new AtribuWebhookError(\"malformed_header\", `Malformed signature header: ${header}`);\n }\n return { t, v1 };\n}\n\nasync function safeCompareHmac(secret: string, input: string, expectedHex: string): Promise<boolean> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(input));\n const computed = bufferToHex(sig);\n return constantTimeEqualHex(computed, expectedHex);\n}\n\nfunction bufferToHex(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n hex += (b < 16 ? \"0\" : \"\") + b.toString(16);\n }\n return hex;\n}\n\nfunction constantTimeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n\nfunction decodeUtf8(buf: Uint8Array): string {\n return new TextDecoder(\"utf-8\").decode(buf);\n}\n","/**\n * Next.js App Router webhook handler.\n *\n * Usage:\n *\n * // app/api/atribu-webhook/route.ts\n * import { withAtribuWebhook } from \"@atribu/sdk/next\";\n *\n * export const POST = withAtribuWebhook({\n * secret: process.env.ATRIBU_WEBHOOK_SECRET!,\n * previousSecret: process.env.ATRIBU_WEBHOOK_PREVIOUS_SECRET,\n * onEvent: async (event) => {\n * if (event.type === \"message.received\" && event.provider === \"whatsapp\") {\n * // event.data.wa_message_id typed\n * }\n * },\n * });\n *\n * Reads the raw body via `req.text()`, calls `verifyWebhook`, dispatches\n * the typed event to `onEvent`, and returns a 200/401 response.\n */\n\nimport { AtribuWebhookError } from \"../errors\";\nimport { verifyWebhook } from \"../webhooks/verify\";\nimport type { AtribuWebhookEvent } from \"../webhooks/types\";\n\nexport interface WithAtribuWebhookOptions {\n secret: string;\n previousSecret?: string | null;\n tolerance?: number;\n onEvent: (event: AtribuWebhookEvent, ctx: WebhookContext) => Promise<void> | void;\n /** Called when a request arrives but signature validation fails. */\n onInvalidSignature?: (error: AtribuWebhookError, request: Request) => void;\n /** Called for any uncaught error inside `onEvent`. Default: log to console. */\n onError?: (error: unknown, event: AtribuWebhookEvent | null) => void;\n}\n\nexport interface WebhookContext {\n request: Request;\n deliveryId: string | null;\n signatureTimestamp: number | null;\n}\n\nexport function withAtribuWebhook(\n opts: WithAtribuWebhookOptions,\n): (request: Request) => Promise<Response> {\n if (!opts.secret) throw new Error(\"withAtribuWebhook: secret is required\");\n\n return async function handler(request: Request): Promise<Response> {\n const rawBody = await request.text();\n const signature = request.headers.get(\"x-atribu-signature\");\n const deliveryId = request.headers.get(\"x-atribu-delivery-id\");\n\n let event: AtribuWebhookEvent;\n try {\n event = await verifyWebhook({\n rawBody,\n signature,\n secret: opts.secret,\n previousSecret: opts.previousSecret ?? null,\n tolerance: opts.tolerance,\n });\n } catch (err) {\n if (err instanceof AtribuWebhookError) {\n opts.onInvalidSignature?.(err, request);\n return new Response(JSON.stringify({ error: err.code, message: err.message }), {\n status: 401,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n throw err;\n }\n\n const ctx: WebhookContext = {\n request,\n deliveryId,\n signatureTimestamp: parseTimestamp(signature),\n };\n\n try {\n await opts.onEvent(event, ctx);\n } catch (err) {\n if (opts.onError) opts.onError(err, event);\n else console.error(\"[atribu webhook] onEvent threw:\", err);\n // Return 500 so Atribu retries — surface real failures via DLQ.\n return new Response(JSON.stringify({ error: \"handler_error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n return new Response(null, { status: 200 });\n };\n}\n\nfunction parseTimestamp(header: string | null): number | null {\n if (!header) return null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n if (key !== \"t\") continue;\n const n = Number(part.slice(eq + 1).trim());\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { g as AtribuWebhookError } from '../errors-D3ApBz8J.cjs';
|
|
2
|
+
import { A as AtribuWebhookEvent } from '../types-Dc6tIN_V.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Next.js App Router webhook handler.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* // app/api/atribu-webhook/route.ts
|
|
10
|
+
* import { withAtribuWebhook } from "@atribu/sdk/next";
|
|
11
|
+
*
|
|
12
|
+
* export const POST = withAtribuWebhook({
|
|
13
|
+
* secret: process.env.ATRIBU_WEBHOOK_SECRET!,
|
|
14
|
+
* previousSecret: process.env.ATRIBU_WEBHOOK_PREVIOUS_SECRET,
|
|
15
|
+
* onEvent: async (event) => {
|
|
16
|
+
* if (event.type === "message.received" && event.provider === "whatsapp") {
|
|
17
|
+
* // event.data.wa_message_id typed
|
|
18
|
+
* }
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* Reads the raw body via `req.text()`, calls `verifyWebhook`, dispatches
|
|
23
|
+
* the typed event to `onEvent`, and returns a 200/401 response.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface WithAtribuWebhookOptions {
|
|
27
|
+
secret: string;
|
|
28
|
+
previousSecret?: string | null;
|
|
29
|
+
tolerance?: number;
|
|
30
|
+
onEvent: (event: AtribuWebhookEvent, ctx: WebhookContext) => Promise<void> | void;
|
|
31
|
+
/** Called when a request arrives but signature validation fails. */
|
|
32
|
+
onInvalidSignature?: (error: AtribuWebhookError, request: Request) => void;
|
|
33
|
+
/** Called for any uncaught error inside `onEvent`. Default: log to console. */
|
|
34
|
+
onError?: (error: unknown, event: AtribuWebhookEvent | null) => void;
|
|
35
|
+
}
|
|
36
|
+
interface WebhookContext {
|
|
37
|
+
request: Request;
|
|
38
|
+
deliveryId: string | null;
|
|
39
|
+
signatureTimestamp: number | null;
|
|
40
|
+
}
|
|
41
|
+
declare function withAtribuWebhook(opts: WithAtribuWebhookOptions): (request: Request) => Promise<Response>;
|
|
42
|
+
|
|
43
|
+
export { AtribuWebhookEvent, type WebhookContext, type WithAtribuWebhookOptions, withAtribuWebhook };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { g as AtribuWebhookError } from '../errors-D3ApBz8J.js';
|
|
2
|
+
import { A as AtribuWebhookEvent } from '../types-Dc6tIN_V.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Next.js App Router webhook handler.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* // app/api/atribu-webhook/route.ts
|
|
10
|
+
* import { withAtribuWebhook } from "@atribu/sdk/next";
|
|
11
|
+
*
|
|
12
|
+
* export const POST = withAtribuWebhook({
|
|
13
|
+
* secret: process.env.ATRIBU_WEBHOOK_SECRET!,
|
|
14
|
+
* previousSecret: process.env.ATRIBU_WEBHOOK_PREVIOUS_SECRET,
|
|
15
|
+
* onEvent: async (event) => {
|
|
16
|
+
* if (event.type === "message.received" && event.provider === "whatsapp") {
|
|
17
|
+
* // event.data.wa_message_id typed
|
|
18
|
+
* }
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* Reads the raw body via `req.text()`, calls `verifyWebhook`, dispatches
|
|
23
|
+
* the typed event to `onEvent`, and returns a 200/401 response.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface WithAtribuWebhookOptions {
|
|
27
|
+
secret: string;
|
|
28
|
+
previousSecret?: string | null;
|
|
29
|
+
tolerance?: number;
|
|
30
|
+
onEvent: (event: AtribuWebhookEvent, ctx: WebhookContext) => Promise<void> | void;
|
|
31
|
+
/** Called when a request arrives but signature validation fails. */
|
|
32
|
+
onInvalidSignature?: (error: AtribuWebhookError, request: Request) => void;
|
|
33
|
+
/** Called for any uncaught error inside `onEvent`. Default: log to console. */
|
|
34
|
+
onError?: (error: unknown, event: AtribuWebhookEvent | null) => void;
|
|
35
|
+
}
|
|
36
|
+
interface WebhookContext {
|
|
37
|
+
request: Request;
|
|
38
|
+
deliveryId: string | null;
|
|
39
|
+
signatureTimestamp: number | null;
|
|
40
|
+
}
|
|
41
|
+
declare function withAtribuWebhook(opts: WithAtribuWebhookOptions): (request: Request) => Promise<Response>;
|
|
42
|
+
|
|
43
|
+
export { AtribuWebhookEvent, type WebhookContext, type WithAtribuWebhookOptions, withAtribuWebhook };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var AtribuError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var AtribuWebhookError = class extends AtribuError {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/webhooks/verify.ts
|
|
17
|
+
async function verifyWebhook(opts) {
|
|
18
|
+
if (!opts.signature) {
|
|
19
|
+
throw new AtribuWebhookError("missing_signature", "Missing X-Atribu-Signature header");
|
|
20
|
+
}
|
|
21
|
+
const parsed = parseHeader(opts.signature);
|
|
22
|
+
const bodyString = typeof opts.rawBody === "string" ? opts.rawBody : decodeUtf8(opts.rawBody);
|
|
23
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
24
|
+
const tolerance = opts.tolerance ?? 300;
|
|
25
|
+
const ageSeconds = Math.abs(nowMs / 1e3 - parsed.t);
|
|
26
|
+
if (ageSeconds > tolerance) {
|
|
27
|
+
throw new AtribuWebhookError(
|
|
28
|
+
"expired_timestamp",
|
|
29
|
+
`Signed timestamp is ${Math.round(ageSeconds)}s old (tolerance ${tolerance}s)`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const signingInput = `${parsed.t}.${bodyString}`;
|
|
33
|
+
const matchesCurrent = await safeCompareHmac(opts.secret, signingInput, parsed.v1);
|
|
34
|
+
if (!matchesCurrent) {
|
|
35
|
+
const previous = opts.previousSecret;
|
|
36
|
+
if (!previous || !await safeCompareHmac(previous, signingInput, parsed.v1)) {
|
|
37
|
+
throw new AtribuWebhookError("invalid_signature", "Signature does not match");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return JSON.parse(bodyString);
|
|
41
|
+
}
|
|
42
|
+
function parseHeader(header) {
|
|
43
|
+
let t = null;
|
|
44
|
+
let v1 = null;
|
|
45
|
+
for (const part of header.split(",")) {
|
|
46
|
+
const eq = part.indexOf("=");
|
|
47
|
+
if (eq < 0) continue;
|
|
48
|
+
const key = part.slice(0, eq).trim();
|
|
49
|
+
const value = part.slice(eq + 1).trim();
|
|
50
|
+
if (key === "t") t = Number(value);
|
|
51
|
+
else if (key === "v1") v1 = value;
|
|
52
|
+
}
|
|
53
|
+
if (t === null || !Number.isFinite(t) || !v1) {
|
|
54
|
+
throw new AtribuWebhookError("malformed_header", `Malformed signature header: ${header}`);
|
|
55
|
+
}
|
|
56
|
+
return { t, v1 };
|
|
57
|
+
}
|
|
58
|
+
async function safeCompareHmac(secret, input, expectedHex) {
|
|
59
|
+
const encoder = new TextEncoder();
|
|
60
|
+
const key = await crypto.subtle.importKey(
|
|
61
|
+
"raw",
|
|
62
|
+
encoder.encode(secret),
|
|
63
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
64
|
+
false,
|
|
65
|
+
["sign"]
|
|
66
|
+
);
|
|
67
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(input));
|
|
68
|
+
const computed = bufferToHex(sig);
|
|
69
|
+
return constantTimeEqualHex(computed, expectedHex);
|
|
70
|
+
}
|
|
71
|
+
function bufferToHex(buf) {
|
|
72
|
+
const bytes = new Uint8Array(buf);
|
|
73
|
+
let hex = "";
|
|
74
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
75
|
+
const b = bytes[i];
|
|
76
|
+
hex += (b < 16 ? "0" : "") + b.toString(16);
|
|
77
|
+
}
|
|
78
|
+
return hex;
|
|
79
|
+
}
|
|
80
|
+
function constantTimeEqualHex(a, b) {
|
|
81
|
+
if (a.length !== b.length) return false;
|
|
82
|
+
let diff = 0;
|
|
83
|
+
for (let i = 0; i < a.length; i++) {
|
|
84
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
85
|
+
}
|
|
86
|
+
return diff === 0;
|
|
87
|
+
}
|
|
88
|
+
function decodeUtf8(buf) {
|
|
89
|
+
return new TextDecoder("utf-8").decode(buf);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/next/webhook-handler.ts
|
|
93
|
+
function withAtribuWebhook(opts) {
|
|
94
|
+
if (!opts.secret) throw new Error("withAtribuWebhook: secret is required");
|
|
95
|
+
return async function handler(request) {
|
|
96
|
+
const rawBody = await request.text();
|
|
97
|
+
const signature = request.headers.get("x-atribu-signature");
|
|
98
|
+
const deliveryId = request.headers.get("x-atribu-delivery-id");
|
|
99
|
+
let event;
|
|
100
|
+
try {
|
|
101
|
+
event = await verifyWebhook({
|
|
102
|
+
rawBody,
|
|
103
|
+
signature,
|
|
104
|
+
secret: opts.secret,
|
|
105
|
+
previousSecret: opts.previousSecret ?? null,
|
|
106
|
+
tolerance: opts.tolerance
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof AtribuWebhookError) {
|
|
110
|
+
opts.onInvalidSignature?.(err, request);
|
|
111
|
+
return new Response(JSON.stringify({ error: err.code, message: err.message }), {
|
|
112
|
+
status: 401,
|
|
113
|
+
headers: { "Content-Type": "application/json" }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
const ctx = {
|
|
119
|
+
request,
|
|
120
|
+
deliveryId,
|
|
121
|
+
signatureTimestamp: parseTimestamp(signature)
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
await opts.onEvent(event, ctx);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (opts.onError) opts.onError(err, event);
|
|
127
|
+
else console.error("[atribu webhook] onEvent threw:", err);
|
|
128
|
+
return new Response(JSON.stringify({ error: "handler_error" }), {
|
|
129
|
+
status: 500,
|
|
130
|
+
headers: { "Content-Type": "application/json" }
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return new Response(null, { status: 200 });
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function parseTimestamp(header) {
|
|
137
|
+
if (!header) return null;
|
|
138
|
+
for (const part of header.split(",")) {
|
|
139
|
+
const eq = part.indexOf("=");
|
|
140
|
+
if (eq < 0) continue;
|
|
141
|
+
const key = part.slice(0, eq).trim();
|
|
142
|
+
if (key !== "t") continue;
|
|
143
|
+
const n = Number(part.slice(eq + 1).trim());
|
|
144
|
+
return Number.isFinite(n) ? n : null;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { withAtribuWebhook };
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
151
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/webhooks/verify.ts","../../src/next/webhook-handler.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Atribu error class hierarchy.\n *\n * Why typed errors: consumers need to branch on auth-failure vs rate-limit vs\n * server-error vs validation-error to decide whether to retry, refresh\n * credentials, or surface to the user. A single `Error` with a status code\n * forces every consumer to write the same `if (err.status === 401)` ladder.\n *\n * Why no automatic retries: the SDK derives a `retry` hint from status +\n * Retry-After + error code, but consumers' queue/job systems decide whether\n * to act on it. Auto-retry inside the SDK hides backpressure signals and\n * makes error budgets opaque.\n */\n\nimport type { RetryHint } from \"./retry\";\n\nexport type ApiErrorCode =\n | \"unauthorized\"\n | \"forbidden\"\n | \"insufficient_scope\"\n | \"not_found\"\n | \"invalid_parameter\"\n | \"invalid_request\"\n | \"validation_error\"\n | \"invalid_content\"\n | \"invalid_date_range\"\n | \"rate_limit_exceeded\"\n | \"connection_not_ready\"\n | \"provider_error\"\n | \"service_unavailable\"\n | \"internal_error\"\n | string;\n\nexport class AtribuError extends Error {\n constructor(message: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class AtribuConfigError extends AtribuError {}\n\nexport class AtribuTransportError extends AtribuError {\n readonly cause: unknown;\n constructor(message: string, cause: unknown) {\n super(message);\n this.cause = cause;\n }\n}\n\nexport interface ApiErrorBody {\n code: ApiErrorCode;\n message: string;\n status: number;\n request_id?: string;\n}\n\nexport class AtribuApiError extends AtribuError {\n readonly code: ApiErrorCode;\n readonly status: number;\n readonly requestId: string | null;\n readonly retry: RetryHint;\n readonly responseBody: unknown;\n\n constructor(args: {\n code: ApiErrorCode;\n message: string;\n status: number;\n requestId: string | null;\n retry: RetryHint;\n responseBody: unknown;\n }) {\n super(`[${args.code}] ${args.message}`);\n this.code = args.code;\n this.status = args.status;\n this.requestId = args.requestId;\n this.retry = args.retry;\n this.responseBody = args.responseBody;\n }\n\n isRetryable(): boolean {\n return this.retry.action === \"retry\" || this.retry.action === \"retry_after\";\n }\n isAuthFailure(): boolean {\n return this.status === 401 || this.code === \"unauthorized\";\n }\n isRateLimit(): boolean {\n return this.status === 429 || this.code === \"rate_limit_exceeded\";\n }\n}\n\nexport type OauthErrorCode =\n | \"invalid_request\"\n | \"invalid_client\"\n | \"invalid_grant\"\n | \"unauthorized_client\"\n | \"unsupported_grant_type\"\n | \"invalid_scope\"\n | \"server_error\"\n | \"unsupported_token_type\"\n | string;\n\nexport class AtribuOauthError extends AtribuError {\n readonly code: OauthErrorCode;\n readonly status: number;\n readonly description: string | null;\n\n constructor(args: { code: OauthErrorCode; description: string | null; status: number }) {\n super(`[oauth/${args.code}] ${args.description ?? args.code}`);\n this.code = args.code;\n this.status = args.status;\n this.description = args.description;\n }\n}\n\nexport type WebhookErrorCode =\n | \"missing_signature\"\n | \"malformed_header\"\n | \"expired_timestamp\"\n | \"invalid_signature\";\n\nexport class AtribuWebhookError extends AtribuError {\n readonly code: WebhookErrorCode;\n constructor(code: WebhookErrorCode, message: string) {\n super(message);\n this.code = code;\n }\n}\n","/**\n * Verify Atribu webhook signatures using Web Crypto.\n *\n * Why Web Crypto: works in Node 18+, Bun, Deno, Vercel Edge, Cloudflare\n * Workers. No `node:crypto` import keeps this subpath edge-safe.\n *\n * Header format (Stripe-style): `t=<unix_seconds>,v1=<hex_hmac_sha256>`\n * Signed string: `<t>.<rawBody>`\n *\n * Rotation: pass `previousSecret` during the grace window after rotating.\n * Atribu always signs with the current secret; the previous slot exists\n * only so subscribers can dual-verify during their own deploy.\n */\n\nimport { AtribuWebhookError } from \"../errors\";\nimport type { AtribuWebhookEvent } from \"./types\";\n\nexport interface VerifyWebhookOptions {\n /** Raw, unparsed request body (string or Uint8Array). */\n rawBody: string | Uint8Array;\n /** Value of the `X-Atribu-Signature` header. */\n signature: string | null | undefined;\n /** Current HMAC secret. */\n secret: string;\n /** Previous secret during rotation grace. */\n previousSecret?: string | null;\n /** Max age of the signed timestamp in seconds. Default 300 (5 minutes). */\n tolerance?: number;\n /** Inject for tests; defaults to Date.now(). */\n now?: () => number;\n}\n\ninterface ParsedHeader {\n t: number;\n v1: string;\n}\n\nexport async function verifyWebhook(opts: VerifyWebhookOptions): Promise<AtribuWebhookEvent> {\n if (!opts.signature) {\n throw new AtribuWebhookError(\"missing_signature\", \"Missing X-Atribu-Signature header\");\n }\n\n const parsed = parseHeader(opts.signature);\n const bodyString = typeof opts.rawBody === \"string\" ? opts.rawBody : decodeUtf8(opts.rawBody);\n\n const nowMs = (opts.now ?? Date.now)();\n const tolerance = opts.tolerance ?? 300;\n const ageSeconds = Math.abs(nowMs / 1000 - parsed.t);\n if (ageSeconds > tolerance) {\n throw new AtribuWebhookError(\n \"expired_timestamp\",\n `Signed timestamp is ${Math.round(ageSeconds)}s old (tolerance ${tolerance}s)`,\n );\n }\n\n const signingInput = `${parsed.t}.${bodyString}`;\n const matchesCurrent = await safeCompareHmac(opts.secret, signingInput, parsed.v1);\n if (!matchesCurrent) {\n const previous = opts.previousSecret;\n if (!previous || !(await safeCompareHmac(previous, signingInput, parsed.v1))) {\n throw new AtribuWebhookError(\"invalid_signature\", \"Signature does not match\");\n }\n }\n\n return JSON.parse(bodyString) as AtribuWebhookEvent;\n}\n\nfunction parseHeader(header: string): ParsedHeader {\n let t: number | null = null;\n let v1: string | null = null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n const value = part.slice(eq + 1).trim();\n if (key === \"t\") t = Number(value);\n else if (key === \"v1\") v1 = value;\n }\n if (t === null || !Number.isFinite(t) || !v1) {\n throw new AtribuWebhookError(\"malformed_header\", `Malformed signature header: ${header}`);\n }\n return { t, v1 };\n}\n\nasync function safeCompareHmac(secret: string, input: string, expectedHex: string): Promise<boolean> {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey(\n \"raw\",\n encoder.encode(secret),\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n const sig = await crypto.subtle.sign(\"HMAC\", key, encoder.encode(input));\n const computed = bufferToHex(sig);\n return constantTimeEqualHex(computed, expectedHex);\n}\n\nfunction bufferToHex(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf);\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i]!;\n hex += (b < 16 ? \"0\" : \"\") + b.toString(16);\n }\n return hex;\n}\n\nfunction constantTimeEqualHex(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) {\n diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return diff === 0;\n}\n\nfunction decodeUtf8(buf: Uint8Array): string {\n return new TextDecoder(\"utf-8\").decode(buf);\n}\n","/**\n * Next.js App Router webhook handler.\n *\n * Usage:\n *\n * // app/api/atribu-webhook/route.ts\n * import { withAtribuWebhook } from \"@atribu/sdk/next\";\n *\n * export const POST = withAtribuWebhook({\n * secret: process.env.ATRIBU_WEBHOOK_SECRET!,\n * previousSecret: process.env.ATRIBU_WEBHOOK_PREVIOUS_SECRET,\n * onEvent: async (event) => {\n * if (event.type === \"message.received\" && event.provider === \"whatsapp\") {\n * // event.data.wa_message_id typed\n * }\n * },\n * });\n *\n * Reads the raw body via `req.text()`, calls `verifyWebhook`, dispatches\n * the typed event to `onEvent`, and returns a 200/401 response.\n */\n\nimport { AtribuWebhookError } from \"../errors\";\nimport { verifyWebhook } from \"../webhooks/verify\";\nimport type { AtribuWebhookEvent } from \"../webhooks/types\";\n\nexport interface WithAtribuWebhookOptions {\n secret: string;\n previousSecret?: string | null;\n tolerance?: number;\n onEvent: (event: AtribuWebhookEvent, ctx: WebhookContext) => Promise<void> | void;\n /** Called when a request arrives but signature validation fails. */\n onInvalidSignature?: (error: AtribuWebhookError, request: Request) => void;\n /** Called for any uncaught error inside `onEvent`. Default: log to console. */\n onError?: (error: unknown, event: AtribuWebhookEvent | null) => void;\n}\n\nexport interface WebhookContext {\n request: Request;\n deliveryId: string | null;\n signatureTimestamp: number | null;\n}\n\nexport function withAtribuWebhook(\n opts: WithAtribuWebhookOptions,\n): (request: Request) => Promise<Response> {\n if (!opts.secret) throw new Error(\"withAtribuWebhook: secret is required\");\n\n return async function handler(request: Request): Promise<Response> {\n const rawBody = await request.text();\n const signature = request.headers.get(\"x-atribu-signature\");\n const deliveryId = request.headers.get(\"x-atribu-delivery-id\");\n\n let event: AtribuWebhookEvent;\n try {\n event = await verifyWebhook({\n rawBody,\n signature,\n secret: opts.secret,\n previousSecret: opts.previousSecret ?? null,\n tolerance: opts.tolerance,\n });\n } catch (err) {\n if (err instanceof AtribuWebhookError) {\n opts.onInvalidSignature?.(err, request);\n return new Response(JSON.stringify({ error: err.code, message: err.message }), {\n status: 401,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n throw err;\n }\n\n const ctx: WebhookContext = {\n request,\n deliveryId,\n signatureTimestamp: parseTimestamp(signature),\n };\n\n try {\n await opts.onEvent(event, ctx);\n } catch (err) {\n if (opts.onError) opts.onError(err, event);\n else console.error(\"[atribu webhook] onEvent threw:\", err);\n // Return 500 so Atribu retries — surface real failures via DLQ.\n return new Response(JSON.stringify({ error: \"handler_error\" }), {\n status: 500,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n return new Response(null, { status: 200 });\n };\n}\n\nfunction parseTimestamp(header: string | null): number | null {\n if (!header) return null;\n for (const part of header.split(\",\")) {\n const eq = part.indexOf(\"=\");\n if (eq < 0) continue;\n const key = part.slice(0, eq).trim();\n if (key !== \"t\") continue;\n const n = Number(part.slice(eq + 1).trim());\n return Number.isFinite(n) ? n : null;\n }\n return null;\n}\n"]}
|