@faable/auth-sdk 2.7.108 → 2.8.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/README.md CHANGED
@@ -20,12 +20,12 @@ Programmatically manage FaableAuth users, teams, connections and clients through
20
20
  > the whole tenant. A user's own bearer token or the browser's session cookie
21
21
  > only grant what that user can do, and those are fine client-side.
22
22
 
23
- | Strategy | Credential | Grants | Where |
24
- | ----------------------- | --------------------------------- | ----------------------------------------------------------------- | --------------------------------------- |
23
+ | Strategy | Credential | Grants | Where |
24
+ | ----------------------- | --------------------------------- | ------------------------------------------------------------------ | --------------------------------------- |
25
25
  | `authClientCredentials` | `client_id` + **`client_secret`** | The management scopes of the client (full catalog unless narrowed) | **Server only** |
26
- | `authApikey` | **API key** (`fak_…`) | The management scopes of the key | **Server only** |
27
- | `authBearer` | A token the caller already holds | Whatever that token's scopes and subject allow | Anywhere the token itself may live |
28
- | `authCookie` | The browser's session cookie | The signed-in user's own session (`/me`, MFA, passkeys…) | Browser, same-origin with the auth host |
26
+ | `authApikey` | **API key** (`fak_…`) | The management scopes of the key | **Server only** |
27
+ | `authBearer` | A token the caller already holds | Whatever that token's scopes and subject allow | Anywhere the token itself may live |
28
+ | `authCookie` | The browser's session cookie | The signed-in user's own session (`/me`, MFA, passkeys…) | Browser, same-origin with the auth host |
29
29
  | _(none)_ | — | Public endpoints only (login, passwordless start, device flow…) | Anywhere |
30
30
 
31
31
  ---
@@ -136,7 +136,9 @@ discovery). Those are not on the generated client; call them through the
136
136
  fetcher:
137
137
 
138
138
  ```ts
139
- const api = FaableAuthApi.create({ domain: "https://<your-account>.auth.faable.link" });
139
+ const api = FaableAuthApi.create({
140
+ domain: "https://<your-account>.auth.faable.link",
141
+ });
140
142
  await api.fetcher.post("/passwordless/start", { email, client_id });
141
143
  ```
142
144
 
@@ -148,7 +150,10 @@ itself instead — the value you pass replaces the default, on data calls and on
148
150
  the token request alike:
149
151
 
150
152
  ```ts
151
- FaableAuthApi.create({ domain, clientInfo: { client: "my-app/1.4.0", name: "my-app" } });
153
+ FaableAuthApi.create({
154
+ domain,
155
+ clientInfo: { client: "my-app/1.4.0", name: "my-app" },
156
+ });
152
157
  ```
153
158
 
154
159
  ### Per-call options
@@ -164,14 +169,14 @@ await api.fetcher.get(`/user/${id}`, { timeout: 800, retry: false });
164
169
 
165
170
  ### Constructor options
166
171
 
167
- | Option | Type | Description |
168
- | -------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
169
- | `domain` | string | **Required.** The account host to target, e.g. `https://<your-account>.auth.faable.link`. No default. The protocol is optional — `<your-account>.auth.faable.link` and `https://<your-account>.auth.faable.link` are equivalent. |
170
- | `authStrategy` | strategy builder | Auth strategy (`authClientCredentials` \| `authApikey` \| `authBearer` \| `authCookie`), re-exported from `@faable/auth-sdk`. Defaults to `authClientCredentials` when `auth` is given; anonymous when neither is. |
171
- | `auth` | config | Config for the chosen strategy. Its shape is inferred from `authStrategy` (e.g. `{ client_id, client_secret }`). |
172
- | `debug` | boolean | Enables verbose logging in the underlying fetcher. |
173
- | `headers` | `{ project_id?; account_id? }` | Advanced / testing escape hatch — target `domain` but have the server resolve a different project/account (sent as `x-faable-project` / `x-faableauth-account`). `team_id` is a deprecated alias of `project_id`: a `team_<hex>` value is sent as `project_<hex>`, and passing both with different ids throws. The SDK no longer sends `x-faable-team`. |
174
- | `clientInfo` | `{ client?; name?; instance? }` | How this integration identifies itself (`x-faable-client` / `x-faable-instance`). `client` replaces the SDK's own `auth-sdk/<version>+<sha>`. |
172
+ | Option | Type | Description |
173
+ | -------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174
+ | `domain` | string | **Required.** The account host to target, e.g. `https://<your-account>.auth.faable.link`. No default. The protocol is optional — `<your-account>.auth.faable.link` and `https://<your-account>.auth.faable.link` are equivalent. |
175
+ | `authStrategy` | strategy builder | Auth strategy (`authClientCredentials` \| `authApikey` \| `authBearer` \| `authCookie`), re-exported from `@faable/auth-sdk`. Defaults to `authClientCredentials` when `auth` is given; anonymous when neither is. |
176
+ | `auth` | config | Config for the chosen strategy. Its shape is inferred from `authStrategy` (e.g. `{ client_id, client_secret }`). |
177
+ | `debug` | boolean | Enables verbose logging in the underlying fetcher. |
178
+ | `headers` | `{ project_id?; account_id? }` | Advanced / testing escape hatch — target `domain` but have the server resolve a different project/account (sent as `x-faable-project` / `x-faableauth-account`). `team_id` is a deprecated alias of `project_id`: a `team_<hex>` value is sent as `project_<hex>`, and passing both with different ids throws. The SDK no longer sends `x-faable-team`. |
179
+ | `clientInfo` | `{ client?; name?; instance? }` | How this integration identifies itself (`x-faable-client` / `x-faable-instance`). `client` replaces the SDK's own `auth-sdk/<version>+<sha>`. |
175
180
 
176
181
  ---
177
182
 
@@ -273,12 +278,65 @@ for await (const page of paginator) {
273
278
  }
274
279
  ```
275
280
 
281
+ ## Webhooks
282
+
283
+ Faable Auth sends signed `POST`s to your server when something happens in the tenant (`user.created`, `team.member.added`…). Create the subscription with `faable auth webhooks create --url https://… -e user.created` (it prints the signing secret once), then verify every delivery before trusting it:
284
+
285
+ ```ts
286
+ import {
287
+ constructWebhookEvent,
288
+ readRawBody,
289
+ WebhookVerificationError,
290
+ } from "@faable/auth-sdk";
291
+
292
+ // Next.js pages router: the signature is over the RAW body.
293
+ export const config = { api: { bodyParser: false } };
294
+
295
+ export default async function handler(req, res) {
296
+ let event;
297
+ try {
298
+ event = await constructWebhookEvent(
299
+ await readRawBody(req), // fetch-style handlers: await request.text()
300
+ req.headers,
301
+ process.env.FAABLEAUTH_WEBHOOK_SECRET!,
302
+ );
303
+ } catch (e) {
304
+ if (e instanceof WebhookVerificationError) return res.status(401).end();
305
+ throw e;
306
+ }
307
+
308
+ switch (event.type) {
309
+ case "user.created":
310
+ await welcome(event.payload.email); // payload is the User
311
+ break;
312
+ case "team.member.added":
313
+ await sync(event.payload.team_id, event.payload.user_id);
314
+ break;
315
+ }
316
+ res.status(200).end();
317
+ }
318
+ ```
319
+
320
+ - **Answer 2xx only when the work is done.** Timeouts, network errors, 5xx and 429 are retried at 1 min, 5 min, 30 min, 2 h and 12 h.
321
+ - **The same event can arrive twice** (a retry after a response that was lost). `event.delivery_id` (`x-faable-delivery`) is the same on every retry: deduplicate by it, or make the work idempotent.
322
+ - **Rotation:** `faable auth webhooks rotate-secret` keeps the old secret valid for 24 h and deliveries carry both signatures. To switch on your side without downtime, pass both: `await constructWebhookEvent(body, headers, [oldSecret, newSecret])`.
323
+ - Uses Web Crypto, so it runs on Node ≥ 19, Deno, Bun and edge runtimes; that is why it is `async`.
324
+ - Deliveries older than 5 minutes are refused as replays (`toleranceMs` to change it).
325
+
326
+ `WebhookEvent`, `WebhookEventType`, `WebhookEventOf<"user.created">` and `WebhookPayload<"team.member.added">` are generated from the auth OpenAPI, like every other type.
327
+
276
328
  ## Types
277
329
 
278
330
  All response shapes are generated from the OpenAPI spec and re-exported from the package root:
279
331
 
280
332
  ```ts
281
- import type { User, Team, TeamMember, Connection, Client } from "@faable/auth-sdk";
333
+ import type {
334
+ User,
335
+ Team,
336
+ TeamMember,
337
+ Connection,
338
+ Client,
339
+ } from "@faable/auth-sdk";
282
340
  ```
283
341
 
284
342
  ## License
package/dist/index.d.ts CHANGED
@@ -3,5 +3,7 @@ export { SDK_CLIENT, commit, version } from "./version.js";
3
3
  export * from "./api/generated-client.js";
4
4
  export * from "./api/api-types.js";
5
5
  export * from "./api/types.js";
6
+ export { WEBHOOK_HEADERS, DEFAULT_WEBHOOK_TOLERANCE_MS, WebhookVerificationError, verifyWebhookSignature, constructWebhookEvent, readRawBody, } from "./webhooks.js";
7
+ export type { WebhookEventOf, WebhookPayload, WebhookHeaders, WebhookVerificationFailure, VerifyWebhookOptions, VerifiedWebhookEvent, } from "./webhooks.js";
6
8
  export { authClientCredentials, authApikey, authBearer, authCookie, FaableApiError, } from "@faable/sdk-base";
7
9
  export type { ApiParams, ClientCredentialsConfig, ApikeyConfig, BearerConfig, RetryConfig, Fetcher, FetcherConfig, FetcherRequestParams, AuthStrategy, AuthStrategyBuilder, AuthStrategyContext, } from "@faable/sdk-base";
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export { SDK_CLIENT, commit, version } from "./version.js";
3
3
  export * from "./api/generated-client.js";
4
4
  export * from "./api/api-types.js";
5
5
  export * from "./api/types.js";
6
+ // Receiving webhooks: verify the signature, get the event typed.
7
+ export { WEBHOOK_HEADERS, DEFAULT_WEBHOOK_TOLERANCE_MS, WebhookVerificationError, verifyWebhookSignature, constructWebhookEvent, readRawBody, } from "./webhooks.js";
6
8
  // Re-export the auth strategies (and their config/types) from
7
9
  // @faable/sdk-base so consumers depend on @faable/auth-sdk ONLY — sdk-base
8
10
  // stays a transitive dependency and no app needs to import it directly.
package/dist/version.js CHANGED
@@ -9,13 +9,13 @@
9
9
  // login pages could not import the SDK at all. Same pattern as auth-js.
10
10
  //
11
11
  // The sentinels MUST stay byte-identical to the `from` values in `.releaserc`.
12
- export const version = "2.7.108";
12
+ export const version = "2.8.0";
13
13
  // Short git SHA of the released commit. The version dates a build; this names
14
14
  // the exact tree, so a canonical log line or an audit entry leads straight to
15
15
  // `git show <sha>`. Deliberately NOT hex: an unreleased build (dev, a local
16
16
  // link) cannot be mistaken for a real commit — auth only records values that
17
17
  // look like a SHA, and this one never will.
18
- export const commit = "e70d451";
18
+ export const commit = "861ed06";
19
19
  // What this SDK writes in `x-faable-client`. Exported for a consumer that
20
20
  // builds a strategy on its own (outside `FaableAuthApi`) and still wants the
21
21
  // token request attributed — a bare `authClientCredentials` stamps only what
@@ -0,0 +1,117 @@
1
+ import type { WebhookEvent, WebhookEventType } from "./api/api-types.js";
2
+ /**
3
+ * Receiving Faable Auth webhooks.
4
+ *
5
+ * Every delivery is a `POST` whose JSON body is a {@link WebhookEvent}, signed
6
+ * with the subscription's secret:
7
+ *
8
+ * - `x-faable-timestamp`: when it was sent, in Unix milliseconds.
9
+ * - `x-faable-signature`: `sha256=<hex>` — HMAC-SHA256 of
10
+ * `${timestamp}.${raw body}`. During a secret rotation it carries two,
11
+ * comma-separated (new first); one matching is enough.
12
+ * - `x-faable-delivery`: the event id, the same on every retry.
13
+ *
14
+ * Failed deliveries (timeout, network, 5xx, 429) are retried at 1 min, 5 min,
15
+ * 30 min, 2 h and 12 h, so a receiver must answer 2xx only once it has done
16
+ * its work, and must tolerate the same event twice.
17
+ *
18
+ * The event types come from the auth OpenAPI of the release this SDK was
19
+ * generated from: `type` narrows `payload`.
20
+ */
21
+ export type { WebhookEvent, WebhookEventType };
22
+ /** The delivery of one event type, e.g. `WebhookEventOf<"user.created">`. */
23
+ export type WebhookEventOf<T extends WebhookEventType> = Extract<WebhookEvent, {
24
+ type: T;
25
+ }>;
26
+ /** The payload of one event type, e.g. the `User` for `"user.created"`. */
27
+ export type WebhookPayload<T extends WebhookEventType> = WebhookEventOf<T>["payload"];
28
+ export declare const WEBHOOK_HEADERS: {
29
+ readonly signature: "x-faable-signature";
30
+ readonly timestamp: "x-faable-timestamp";
31
+ readonly delivery: "x-faable-delivery";
32
+ readonly event: "x-faable-event";
33
+ };
34
+ /** How far a delivery's timestamp may be from now before it is a replay. */
35
+ export declare const DEFAULT_WEBHOOK_TOLERANCE_MS: number;
36
+ export type WebhookVerificationFailure = "missing_secret" | "missing_headers" | "timestamp_out_of_tolerance" | "signature_mismatch" | "invalid_body";
37
+ /**
38
+ * Thrown by {@link verifyWebhookSignature} and {@link constructWebhookEvent}.
39
+ * Answer the delivery with a 400/401 and do nothing else: it was not sent by
40
+ * Faable Auth with your secret, or it was tampered with or replayed.
41
+ */
42
+ export declare class WebhookVerificationError extends Error {
43
+ readonly reason: WebhookVerificationFailure;
44
+ constructor(reason: WebhookVerificationFailure, message: string);
45
+ }
46
+ /** Fetch `Headers`, Node's `IncomingHttpHeaders`, or a plain object. */
47
+ export type WebhookHeaders = {
48
+ get(name: string): string | null;
49
+ } | Record<string, string | string[] | undefined>;
50
+ type RawBody = string | Uint8Array;
51
+ export type VerifyWebhookOptions = {
52
+ /** The raw request body, exactly as received — not re-serialised JSON. */
53
+ body: RawBody;
54
+ /** `x-faable-signature`. */
55
+ signature: string | undefined;
56
+ /** `x-faable-timestamp`. */
57
+ timestamp: string | undefined;
58
+ /**
59
+ * The subscription's signing secret. Pass several while you rotate on your
60
+ * side (old and new): any of them may verify the delivery.
61
+ */
62
+ secret: string | string[];
63
+ /** Defaults to 5 minutes. */
64
+ toleranceMs?: number;
65
+ /** For tests. Defaults to `Date.now()`. */
66
+ now?: number;
67
+ };
68
+ /**
69
+ * Checks that a delivery was signed by Faable Auth with your secret, recently.
70
+ * Throws {@link WebhookVerificationError}; returns nothing when it is valid.
71
+ *
72
+ * Prefer {@link constructWebhookEvent}, which also parses and types the body.
73
+ */
74
+ export declare function verifyWebhookSignature(options: VerifyWebhookOptions): Promise<void>;
75
+ /** A verified delivery: the typed event plus the delivery id to deduplicate on. */
76
+ export type VerifiedWebhookEvent = WebhookEvent & {
77
+ /** `x-faable-delivery`: the same on every retry of this event. */
78
+ delivery_id: string;
79
+ };
80
+ /**
81
+ * Verifies a delivery and returns its typed event.
82
+ *
83
+ * @example Next.js (pages router)
84
+ * ```ts
85
+ * import { constructWebhookEvent, readRawBody } from "@faable/auth-sdk";
86
+ *
87
+ * export const config = { api: { bodyParser: false } };
88
+ *
89
+ * export default async function handler(req, res) {
90
+ * let event;
91
+ * try {
92
+ * event = await constructWebhookEvent(
93
+ * await readRawBody(req),
94
+ * req.headers,
95
+ * process.env.FAABLEAUTH_WEBHOOK_SECRET!,
96
+ * );
97
+ * } catch {
98
+ * return res.status(401).end();
99
+ * }
100
+ * if (event.type === "user.created") {
101
+ * event.payload.email; // typed: the User
102
+ * }
103
+ * res.status(200).end();
104
+ * }
105
+ * ```
106
+ */
107
+ export declare function constructWebhookEvent(body: RawBody, headers: WebhookHeaders, secret: string | string[], options?: {
108
+ toleranceMs?: number;
109
+ now?: number;
110
+ }): Promise<VerifiedWebhookEvent>;
111
+ /**
112
+ * The raw body of a Node request (`IncomingMessage`, Next.js pages API with
113
+ * `bodyParser: false`, Express without `express.json()` on the route): what
114
+ * the signature is computed over. With fetch-style handlers (Next.js route
115
+ * handlers, Hono…) use `await request.text()` instead.
116
+ */
117
+ export declare const readRawBody: (req: AsyncIterable<Uint8Array | string>) => Promise<string>;
@@ -0,0 +1,170 @@
1
+ export const WEBHOOK_HEADERS = {
2
+ signature: "x-faable-signature",
3
+ timestamp: "x-faable-timestamp",
4
+ delivery: "x-faable-delivery",
5
+ event: "x-faable-event",
6
+ };
7
+ /** How far a delivery's timestamp may be from now before it is a replay. */
8
+ export const DEFAULT_WEBHOOK_TOLERANCE_MS = 5 * 60 * 1000;
9
+ /**
10
+ * Thrown by {@link verifyWebhookSignature} and {@link constructWebhookEvent}.
11
+ * Answer the delivery with a 400/401 and do nothing else: it was not sent by
12
+ * Faable Auth with your secret, or it was tampered with or replayed.
13
+ */
14
+ export class WebhookVerificationError extends Error {
15
+ reason;
16
+ constructor(reason, message) {
17
+ super(message);
18
+ this.name = "WebhookVerificationError";
19
+ this.reason = reason;
20
+ }
21
+ }
22
+ const readHeader = (headers, name) => {
23
+ if (typeof headers.get === "function") {
24
+ return (headers.get(name) ?? undefined);
25
+ }
26
+ const record = headers;
27
+ const value = record[name] ??
28
+ Object.entries(record).find(([k]) => k.toLowerCase() === name)?.[1];
29
+ return Array.isArray(value) ? value[0] : value;
30
+ };
31
+ // Web Crypto and TextEncoder, not `node:crypto` / `Buffer`: this package also
32
+ // loads in browser bundles (the hosted login pages), and the same code then
33
+ // runs on Node ≥ 19, Deno, Bun and edge runtimes.
34
+ const encoder = new TextEncoder();
35
+ const decoder = new TextDecoder();
36
+ const bodyText = (body) => typeof body === "string" ? body : decoder.decode(body);
37
+ const fromHex = (hex) => {
38
+ if (hex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(hex))
39
+ return null;
40
+ const out = new Uint8Array(hex.length / 2);
41
+ for (let i = 0; i < out.length; i++) {
42
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
43
+ }
44
+ return out;
45
+ };
46
+ // Constant time for equal lengths: the comparison does not stop at the first
47
+ // differing byte, so its timing does not leak how much of a guess was right.
48
+ const sameBytes = (a, b) => {
49
+ if (a.length !== b.length)
50
+ return false;
51
+ let diff = 0;
52
+ for (let i = 0; i < a.length; i++)
53
+ diff |= a[i] ^ b[i];
54
+ return diff === 0;
55
+ };
56
+ const hmacSha256 = async (secret, data) => {
57
+ const subtle = globalThis.crypto?.subtle;
58
+ if (!subtle) {
59
+ throw new Error("Web Crypto (globalThis.crypto.subtle) is not available: verifying webhooks needs Node ≥ 19 or a runtime with Web Crypto.");
60
+ }
61
+ const key = await subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
62
+ return new Uint8Array(await subtle.sign("HMAC", key, encoder.encode(data)));
63
+ };
64
+ /**
65
+ * Checks that a delivery was signed by Faable Auth with your secret, recently.
66
+ * Throws {@link WebhookVerificationError}; returns nothing when it is valid.
67
+ *
68
+ * Prefer {@link constructWebhookEvent}, which also parses and types the body.
69
+ */
70
+ export async function verifyWebhookSignature(options) {
71
+ const secrets = (Array.isArray(options.secret) ? options.secret : [options.secret]).filter((s) => typeof s === "string" && s.length > 0);
72
+ if (secrets.length === 0) {
73
+ throw new WebhookVerificationError("missing_secret", "No webhook secret configured: refusing every delivery.");
74
+ }
75
+ const { signature, timestamp } = options;
76
+ if (!signature || !timestamp) {
77
+ throw new WebhookVerificationError("missing_headers", `Missing ${WEBHOOK_HEADERS.signature} or ${WEBHOOK_HEADERS.timestamp}.`);
78
+ }
79
+ const sent = Number(timestamp);
80
+ const tolerance = options.toleranceMs ?? DEFAULT_WEBHOOK_TOLERANCE_MS;
81
+ const now = options.now ?? Date.now();
82
+ if (!Number.isFinite(sent) || Math.abs(now - sent) > tolerance) {
83
+ throw new WebhookVerificationError("timestamp_out_of_tolerance", "The delivery timestamp is outside the tolerance: a replay, or a clock far off.");
84
+ }
85
+ const signed = `${timestamp}.${bodyText(options.body)}`;
86
+ const received = signature
87
+ .split(",")
88
+ .map((part) => part.trim())
89
+ .filter((part) => part.startsWith("sha256="))
90
+ .map((part) => fromHex(part.slice("sha256=".length)))
91
+ .filter((r) => r !== null);
92
+ let matches = false;
93
+ for (const secret of secrets) {
94
+ const expected = await hmacSha256(secret, signed);
95
+ if (received.some((r) => sameBytes(r, expected)))
96
+ matches = true;
97
+ }
98
+ if (!matches) {
99
+ throw new WebhookVerificationError("signature_mismatch", "The signature does not match: wrong secret, or the body was changed (verify the RAW body, not re-serialised JSON).");
100
+ }
101
+ }
102
+ /**
103
+ * Verifies a delivery and returns its typed event.
104
+ *
105
+ * @example Next.js (pages router)
106
+ * ```ts
107
+ * import { constructWebhookEvent, readRawBody } from "@faable/auth-sdk";
108
+ *
109
+ * export const config = { api: { bodyParser: false } };
110
+ *
111
+ * export default async function handler(req, res) {
112
+ * let event;
113
+ * try {
114
+ * event = await constructWebhookEvent(
115
+ * await readRawBody(req),
116
+ * req.headers,
117
+ * process.env.FAABLEAUTH_WEBHOOK_SECRET!,
118
+ * );
119
+ * } catch {
120
+ * return res.status(401).end();
121
+ * }
122
+ * if (event.type === "user.created") {
123
+ * event.payload.email; // typed: the User
124
+ * }
125
+ * res.status(200).end();
126
+ * }
127
+ * ```
128
+ */
129
+ export async function constructWebhookEvent(body, headers, secret, options = {}) {
130
+ await verifyWebhookSignature({
131
+ body,
132
+ signature: readHeader(headers, WEBHOOK_HEADERS.signature),
133
+ timestamp: readHeader(headers, WEBHOOK_HEADERS.timestamp),
134
+ secret,
135
+ ...options,
136
+ });
137
+ let event;
138
+ try {
139
+ event = JSON.parse(bodyText(body));
140
+ }
141
+ catch {
142
+ throw new WebhookVerificationError("invalid_body", "The body is not JSON.");
143
+ }
144
+ if (!event || typeof event !== "object" || typeof event.type !== "string") {
145
+ throw new WebhookVerificationError("invalid_body", "The body is not a webhook event.");
146
+ }
147
+ return {
148
+ ...event,
149
+ delivery_id: readHeader(headers, WEBHOOK_HEADERS.delivery) ?? event.id,
150
+ };
151
+ }
152
+ /**
153
+ * The raw body of a Node request (`IncomingMessage`, Next.js pages API with
154
+ * `bodyParser: false`, Express without `express.json()` on the route): what
155
+ * the signature is computed over. With fetch-style handlers (Next.js route
156
+ * handlers, Hono…) use `await request.text()` instead.
157
+ */
158
+ export const readRawBody = async (req) => {
159
+ const chunks = [];
160
+ for await (const chunk of req) {
161
+ chunks.push(typeof chunk === "string" ? encoder.encode(chunk) : chunk);
162
+ }
163
+ const all = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
164
+ let at = 0;
165
+ for (const c of chunks) {
166
+ all.set(c, at);
167
+ at += c.length;
168
+ }
169
+ return decoder.decode(all);
170
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/auth-sdk",
3
- "version": "2.7.108",
3
+ "version": "2.8.0",
4
4
  "author": "Marc Pomar <marc@faable.com>",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",