@supa-media/convex 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +517 -0
- package/package.json +36 -0
- package/src/auth/helpers.ts +99 -0
- package/src/auth/index.ts +13 -0
- package/src/auth/setup.ts +497 -0
- package/src/index.ts +73 -0
- package/src/lib/index.ts +8 -0
- package/src/lib/rateLimit.ts +92 -0
- package/src/lib/scheduling.ts +41 -0
- package/src/lib/validation.ts +63 -0
- package/src/notifications/index.ts +242 -0
- package/src/payments/index.ts +404 -0
- package/src/schema/authTables.ts +38 -0
- package/src/schema/chatTables.ts +58 -0
- package/src/schema/index.ts +8 -0
- package/src/schema/notificationTables.ts +58 -0
- package/src/schema/paymentTables.ts +47 -0
- package/src/schema/tenantScoping.ts +243 -0
- package/src/schema/tenantTables.ts +84 -0
- package/src/webhooks/hmac.ts +125 -0
- package/src/webhooks/index.ts +41 -0
- package/src/webhooks/sharedSecret.ts +48 -0
- package/src/webhooks/stripe.ts +97 -0
- package/src/webhooks/twilio.ts +80 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twilio webhook signature verification.
|
|
3
|
+
*
|
|
4
|
+
* Twilio signs incoming webhook requests with HMAC-SHA1 using the account's
|
|
5
|
+
* auth token. The signing input is built deterministically:
|
|
6
|
+
*
|
|
7
|
+
* 1. Start with the full request URL exactly as Twilio called it
|
|
8
|
+
* (scheme + host + path + query string).
|
|
9
|
+
* 2. Sort the POST form parameter keys alphabetically.
|
|
10
|
+
* 3. Append each key followed immediately by its value: `url + k1v1 + k2v2 + ...`.
|
|
11
|
+
* 4. HMAC-SHA1 with the auth token as the key.
|
|
12
|
+
* 5. Base64-encode the digest.
|
|
13
|
+
*
|
|
14
|
+
* Compare against the `X-Twilio-Signature` header using a timing-safe
|
|
15
|
+
* equality check. Ported as-is from Fount Studios'
|
|
16
|
+
* `apps/convex/lib/webhooks/twilio.ts` (issue #143) — the signing-input
|
|
17
|
+
* construction is Twilio-specific and isn't expressible via the generic
|
|
18
|
+
* `verifyHmacSignature` (which only covers "HMAC over the raw body" schemes).
|
|
19
|
+
*
|
|
20
|
+
* Reference: https://www.twilio.com/docs/usage/webhooks/webhooks-security
|
|
21
|
+
*/
|
|
22
|
+
import { timingSafeEqual, computeHmac } from "./hmac";
|
|
23
|
+
|
|
24
|
+
export interface VerifyTwilioSignatureArgs {
|
|
25
|
+
/** The full URL Twilio POSTed to (must include scheme, host, and any query string). Must NOT be normalized — Twilio signs the exact bytes it sent. */
|
|
26
|
+
url: string;
|
|
27
|
+
/** The application/x-www-form-urlencoded body parsed into a flat `Record<string, string>`. */
|
|
28
|
+
params: Record<string, string>;
|
|
29
|
+
/** The `X-Twilio-Signature` header value sent by Twilio. Null/missing returns `false`. */
|
|
30
|
+
signatureHeader: string | null | undefined;
|
|
31
|
+
/** The Twilio account auth token used to sign the request. Must be the *auth token*, not an API key secret. */
|
|
32
|
+
authToken: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Verify a Twilio webhook signature.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ```ts
|
|
40
|
+
* // convex/http.ts
|
|
41
|
+
* import { verifyTwilioSignature } from "@supa-media/convex/webhooks";
|
|
42
|
+
*
|
|
43
|
+
* http.route({
|
|
44
|
+
* path: "/twilio/sms",
|
|
45
|
+
* method: "POST",
|
|
46
|
+
* handler: httpAction(async (ctx, request) => {
|
|
47
|
+
* const bodyText = await request.text();
|
|
48
|
+
* const params = Object.fromEntries(new URLSearchParams(bodyText));
|
|
49
|
+
* const ok = await verifyTwilioSignature({
|
|
50
|
+
* url: process.env.CONVEX_SITE_URL + "/twilio/sms",
|
|
51
|
+
* params,
|
|
52
|
+
* signatureHeader: request.headers.get("x-twilio-signature"),
|
|
53
|
+
* authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
54
|
+
* });
|
|
55
|
+
* if (!ok) return new Response("Invalid signature", { status: 401 });
|
|
56
|
+
* // ...
|
|
57
|
+
* }),
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export async function verifyTwilioSignature(
|
|
62
|
+
args: VerifyTwilioSignatureArgs,
|
|
63
|
+
): Promise<boolean> {
|
|
64
|
+
if (!args.signatureHeader || !args.authToken) return false;
|
|
65
|
+
|
|
66
|
+
const sortedKeys = Object.keys(args.params).sort();
|
|
67
|
+
let signingInput = args.url;
|
|
68
|
+
for (const key of sortedKeys) {
|
|
69
|
+
// Key always exists (we just derived sortedKeys from this object's own
|
|
70
|
+
// keys) — the `?? ""` only satisfies noUncheckedIndexedAccess.
|
|
71
|
+
signingInput += key + (args.params[key] ?? "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const expected = await computeHmac(args.authToken, signingInput, {
|
|
75
|
+
hash: "SHA-1",
|
|
76
|
+
encoding: "base64",
|
|
77
|
+
});
|
|
78
|
+
// Base64 digests are case-sensitive — compare exactly, no case-folding.
|
|
79
|
+
return timingSafeEqual(args.signatureHeader, expected);
|
|
80
|
+
}
|