@theholocron/holocron-plugin-clerk 2.0.0-alpha.59 → 2.0.0-alpha.60
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/index.mjs +40 -28
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthError, ProviderApiError, WebhookVerificationError, createResolveToken } from "@theholocron/cli";
|
|
2
2
|
import { createClerkClient } from "@theholocron/clerk-client";
|
|
3
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
4
|
//#region src/auth.ts
|
|
4
5
|
const resolveToken = createResolveToken({
|
|
5
6
|
envName: "HOLOCRON_CLERK_SECRET_KEY",
|
|
@@ -98,27 +99,23 @@ function isAlreadyExistsError(err) {
|
|
|
98
99
|
//#region src/parse-webhook.ts
|
|
99
100
|
/**
|
|
100
101
|
* Translates a Clerk webhook delivery into the normalized `AuthEvent`
|
|
101
|
-
* shape defined in `@theholocron/cli
|
|
102
|
-
*
|
|
102
|
+
* shape defined in `@theholocron/cli`, with full Svix HMAC-SHA256
|
|
103
|
+
* signature verification (closes #80).
|
|
103
104
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* deployments until the verification body lands.
|
|
112
|
-
*
|
|
113
|
-
* Reference event shapes:
|
|
114
|
-
* https://clerk.com/docs/integrations/webhooks/overview
|
|
115
|
-
* https://clerk.com/docs/reference/backend-api/tag/Webhooks
|
|
105
|
+
* Verification algorithm (https://docs.svix.com/receiving/verifying-payloads/how-manual):
|
|
106
|
+
* 1. Require svix-id, svix-timestamp, svix-signature headers
|
|
107
|
+
* 2. Reject if svix-timestamp is outside the ±5-minute replay window
|
|
108
|
+
* 3. Decode the whsec_<base64> signing secret
|
|
109
|
+
* 4. Compute HMAC-SHA256(secretBytes, "${id}.${timestamp}.${body}")
|
|
110
|
+
* 5. Constant-time compare against each v1,<base64> signature in the
|
|
111
|
+
* space-separated svix-signature header (supports key rotation)
|
|
116
112
|
*/
|
|
117
113
|
const CLERK_TO_NORMALIZED = {
|
|
118
114
|
"user.created": "user.created",
|
|
119
115
|
"user.updated": "user.updated",
|
|
120
116
|
"user.deleted": "user.deleted"
|
|
121
117
|
};
|
|
118
|
+
const REPLAY_WINDOW_SECONDS = 300;
|
|
122
119
|
async function parseWebhook(input) {
|
|
123
120
|
await verifySignature(input);
|
|
124
121
|
const bodyStr = typeof input.body === "string" ? input.body : input.body.toString("utf8");
|
|
@@ -144,22 +141,37 @@ async function parseWebhook(input) {
|
|
|
144
141
|
occurredAt
|
|
145
142
|
};
|
|
146
143
|
}
|
|
147
|
-
/**
|
|
148
|
-
* Stub for Svix signature verification. The real implementation
|
|
149
|
-
* lands at #80 and will:
|
|
150
|
-
*
|
|
151
|
-
* 1. Read svix-id, svix-timestamp, svix-signature headers
|
|
152
|
-
* 2. Verify svix-timestamp is within the replay window (±5 min)
|
|
153
|
-
* 3. Compute HMAC-SHA256(<signing_secret>, `${id}.${timestamp}.${body}`)
|
|
154
|
-
* 4. Compare (constant-time) against the base64 sig in svix-signature
|
|
155
|
-
*
|
|
156
|
-
* For now: throw WebhookVerificationError when the signing secret is
|
|
157
|
-
* missing, so consumers see the contract; let the call through
|
|
158
|
-
* otherwise (signature-validation TODO).
|
|
159
|
-
*/
|
|
160
144
|
async function verifySignature(input) {
|
|
161
145
|
if (!input.signingSecret) throw new WebhookVerificationError("Clerk webhook signingSecret is required (use the Svix whsec_… value from the Clerk dashboard)");
|
|
162
|
-
|
|
146
|
+
const lower = (s) => s.toLowerCase();
|
|
147
|
+
const h = (name) => {
|
|
148
|
+
const target = lower(name);
|
|
149
|
+
for (const [k, v] of Object.entries(input.headers)) if (lower(k) === target) return Array.isArray(v) ? v[0] : v;
|
|
150
|
+
};
|
|
151
|
+
const svixId = h("svix-id");
|
|
152
|
+
const svixTimestamp = h("svix-timestamp");
|
|
153
|
+
const svixSignature = h("svix-signature");
|
|
154
|
+
if (!svixId || !svixTimestamp || !svixSignature) throw new WebhookVerificationError("Missing required Svix headers: svix-id, svix-timestamp, svix-signature");
|
|
155
|
+
const ts = parseInt(svixTimestamp, 10);
|
|
156
|
+
if (isNaN(ts)) throw new WebhookVerificationError(`Invalid svix-timestamp: "${svixTimestamp}"`);
|
|
157
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
158
|
+
if (Math.abs(nowSeconds - ts) > REPLAY_WINDOW_SECONDS) throw new WebhookVerificationError(`Message timestamp is outside the ${REPLAY_WINDOW_SECONDS / 60}-minute replay window`);
|
|
159
|
+
const secretBase64 = input.signingSecret.startsWith("whsec_") ? input.signingSecret.slice(6) : input.signingSecret;
|
|
160
|
+
const secretBytes = Buffer.from(secretBase64, "base64");
|
|
161
|
+
const toSign = `${svixId}.${svixTimestamp}.${typeof input.body === "string" ? input.body : input.body.toString("utf8")}`;
|
|
162
|
+
const computed = createHmac("sha256", secretBytes).update(toSign).digest();
|
|
163
|
+
if (!svixSignature.split(" ").some((sig) => {
|
|
164
|
+
const comma = sig.indexOf(",");
|
|
165
|
+
if (comma === -1 || sig.slice(0, comma) !== "v1") return false;
|
|
166
|
+
let sigBytes;
|
|
167
|
+
try {
|
|
168
|
+
sigBytes = Buffer.from(sig.slice(comma + 1), "base64");
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (sigBytes.length !== computed.length) return false;
|
|
173
|
+
return timingSafeEqual(sigBytes, computed);
|
|
174
|
+
})) throw new WebhookVerificationError("Svix signature verification failed");
|
|
163
175
|
}
|
|
164
176
|
//#endregion
|
|
165
177
|
//#region src/verify-token.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-clerk",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.60",
|
|
4
4
|
"description": "Holocron plugin for Clerk. Implements the auth capability against Clerk's Backend REST API.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-clerk#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@theholocron/clerk-client": "^0.11.3",
|
|
25
|
-
"@theholocron/cli": "2.0.0-alpha.
|
|
25
|
+
"@theholocron/cli": "2.0.0-alpha.60"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@theholocron/clerk-client": "^0.11.3",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"tsx": "^4.22.4",
|
|
41
41
|
"typescript": "^5.9.3",
|
|
42
42
|
"vitest": "^4.1.10",
|
|
43
|
-
"@theholocron/cli": "2.0.0-alpha.
|
|
43
|
+
"@theholocron/cli": "2.0.0-alpha.60"
|
|
44
44
|
},
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|