experimental-ash 0.13.0 → 0.14.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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Twilio inbound-webhook verification.
3
+ *
4
+ * Twilio signs webhook requests with `X-Twilio-Signature`. For form
5
+ * posts, the signed payload is the exact public URL Twilio called plus
6
+ * every POST parameter sorted by name and appended as `name + value`.
7
+ */
8
+ import { createHmac, timingSafeEqual } from "node:crypto";
9
+ import { createLogger } from "#internal/logging.js";
10
+ const log = createLogger("twilio.verify");
11
+ /** Resolves a Twilio auth token, falling back to `TWILIO_AUTH_TOKEN`. */
12
+ export async function resolveTwilioAuthToken(authToken) {
13
+ const source = authToken ?? process.env.TWILIO_AUTH_TOKEN;
14
+ if (!source)
15
+ throw new Error("TWILIO_AUTH_TOKEN is required.");
16
+ return typeof source === "function" ? await source() : source;
17
+ }
18
+ /**
19
+ * Verifies an inbound Twilio webhook and returns the raw body plus form params.
20
+ *
21
+ * Throws when the auth token is missing, the signature header is missing,
22
+ * or the computed signature does not match `X-Twilio-Signature`.
23
+ */
24
+ export async function verifyTwilioRequest(request, options) {
25
+ const body = await request.text();
26
+ const params = new URLSearchParams(body);
27
+ const authToken = await resolveTwilioAuthToken(options.authToken);
28
+ const signature = request.headers.get("x-twilio-signature") ?? "";
29
+ if (!signature) {
30
+ throw new Error("twilioChannel: inbound request missing X-Twilio-Signature.");
31
+ }
32
+ const url = await resolveWebhookUrl(request, options.webhookUrl);
33
+ const expected = signTwilioRequest({ authToken, params, url });
34
+ if (!constantTimeCompare(expected, signature)) {
35
+ throw new Error("twilioChannel: inbound request signature mismatch.");
36
+ }
37
+ return { body, params };
38
+ }
39
+ /** Computes Twilio's HMAC-SHA1 request signature. */
40
+ export function signTwilioRequest(input) {
41
+ const base = buildTwilioSignatureBase(input.url, input.params);
42
+ return createHmac("sha1", input.authToken).update(base).digest("base64");
43
+ }
44
+ /** Builds the string Twilio signs for a form POST webhook. */
45
+ export function buildTwilioSignatureBase(url, params) {
46
+ const entries = Array.from(params.entries()).sort(([aName, aValue], [bName, bValue]) => {
47
+ const nameOrder = aName < bName ? -1 : aName > bName ? 1 : 0;
48
+ if (nameOrder !== 0)
49
+ return nameOrder;
50
+ return aValue < bValue ? -1 : aValue > bValue ? 1 : 0;
51
+ });
52
+ let base = url;
53
+ for (const [name, value] of entries) {
54
+ base += `${name}${value}`;
55
+ }
56
+ return base;
57
+ }
58
+ async function resolveWebhookUrl(request, webhookUrl) {
59
+ if (typeof webhookUrl === "function")
60
+ return webhookUrl(request);
61
+ return webhookUrl ?? request.url;
62
+ }
63
+ function constantTimeCompare(a, b) {
64
+ if (a.length !== b.length)
65
+ return false;
66
+ try {
67
+ return timingSafeEqual(Buffer.from(a), Buffer.from(b));
68
+ }
69
+ catch (error) {
70
+ log.debug("timingSafeEqual threw", { error });
71
+ return false;
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"
@@ -136,6 +136,11 @@
136
136
  "import": "./dist/src/public/channels/slack/index.js",
137
137
  "default": "./dist/src/public/channels/slack/index.js"
138
138
  },
139
+ "./channels/twilio": {
140
+ "types": "./dist/src/public/channels/twilio/index.d.ts",
141
+ "import": "./dist/src/public/channels/twilio/index.js",
142
+ "default": "./dist/src/public/channels/twilio/index.js"
143
+ },
139
144
  "./package.json": "./package.json"
140
145
  },
141
146
  "publishConfig": {