@samva/webhooks 0.5.0 → 0.6.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/AGENTS.md CHANGED
@@ -5,7 +5,7 @@ webhook. Verification requires the untouched request body and the endpoint signi
5
5
  expose that secret to browser code.
6
6
 
7
7
  Effect applications can import the typed adapter from `@samva/webhooks/effect` and install the
8
- optional exact peer `effect@4.0.0-rc.112`.
8
+ optional peer `effect` at the exact version this package's `peerDependencies` names.
9
9
 
10
10
  Import only package subpaths declared in `package.json`. The delivery, queue, persistence, retry,
11
11
  and endpoint-management implementation is not part of this package.
package/README.md CHANGED
@@ -17,9 +17,13 @@ request body and keep the signing secret on the server.
17
17
  Effect applications can use the optional Effect entrypoint:
18
18
 
19
19
  ```ts
20
+ import { Effect } from "effect";
20
21
  import * as Webhooks from "@samva/webhooks/effect";
21
22
 
22
- const verified = yield * Webhooks.verifyRequest(request, secret);
23
+ const program = Effect.gen(function* () {
24
+ const verified = yield* Webhooks.verifyRequest(request, secret);
25
+ return verified.event;
26
+ });
23
27
  ```
24
28
 
25
- Install the exact optional peer `effect@4.0.0-rc.112` when using that entrypoint.
29
+ Install the `effect` version this package lists in `peerDependencies` when using that entrypoint.
package/dist/effect.d.ts CHANGED
@@ -1,10 +1,43 @@
1
1
  import { Effect } from "effect";
2
2
  import type { VerifyOptions } from "./promise.js";
3
- import { type AnyWebhookVerificationError } from "./protocol.js";
4
- export declare const verify: (options: VerifyOptions) => Effect.Effect<import("./protocol.js").VerifiedWebhook, AnyWebhookVerificationError, never>;
3
+ import { type AnyWebhookVerificationError, type VerifiedWebhook } from "./protocol.js";
4
+ /**
5
+ * Effect version of the Promise entrypoint's `verify`: verifies a delivery from its body and
6
+ * header values and succeeds with its ID and parsed event.
7
+ *
8
+ * Reads the current time from Effect's `Clock`, so `TestClock` controls the replay window in
9
+ * tests. Fails with {@link AnyWebhookVerificationError}; branch with `Effect.catchTag`. Input
10
+ * that violates the declared types, such as an undefined secret, dies instead.
11
+ */
12
+ export declare const verify: (options: VerifyOptions) => Effect.Effect<VerifiedWebhook, AnyWebhookVerificationError, never>;
13
+ /**
14
+ * Effect version of the Promise entrypoint's `verifyRequest`: verifies a delivery received as a
15
+ * Fetch API `Request` and succeeds with its ID and parsed event. Reads the body from a clone, so
16
+ * the original request stays readable.
17
+ *
18
+ * Reads the current time from Effect's `Clock`. Fails with {@link AnyWebhookVerificationError};
19
+ * a body that cannot be read fails with {@link WebhookCryptoError}. Input that violates the
20
+ * declared types dies instead.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { Effect } from "effect";
25
+ * import * as Webhooks from "@samva/webhooks/effect";
26
+ *
27
+ * const receive = (request: Request, secret: string) =>
28
+ * Webhooks.verifyRequest(request, secret).pipe(
29
+ * Effect.flatMap(({ id, event }) => Effect.promise(() => handleOnce(id, event))),
30
+ * Effect.as(new Response(null, { status: 204 })),
31
+ * Effect.catchTags({
32
+ * WebhookSignatureError: () => Effect.succeed(new Response(null, { status: 400 })),
33
+ * WebhookTimestampError: () => Effect.succeed(new Response(null, { status: 400 })),
34
+ * }),
35
+ * );
36
+ * ```
37
+ */
5
38
  export declare const verifyRequest: (request: Request, secret: string, options?: {
6
39
  readonly toleranceSeconds?: number | undefined;
7
- } | undefined) => Effect.Effect<import("./protocol.js").VerifiedWebhook, AnyWebhookVerificationError, never>;
40
+ } | undefined) => Effect.Effect<VerifiedWebhook, AnyWebhookVerificationError, never>;
8
41
  export { DEFAULT_TOLERANCE_SECONDS, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, generateWebhookSecret, signWebhook, } from "./protocol.js";
9
- export type { AnyWebhookVerificationError, SamvaWebhookEvent, VerifiedWebhook, } from "./protocol.js";
42
+ export type { AnyWebhookVerificationError, SamvaWebhookEvent, VerifiedWebhook, WebhookHeaderName, } from "./protocol.js";
10
43
  export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./protocol.js";
package/dist/effect.js CHANGED
@@ -1,38 +1,75 @@
1
- import { _ as WebhookTimestampError, c as signWebhook, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, s as readWebhookHeaders, t as DEFAULT_TOLERANCE_SECONDS, v as WebhookVerificationError } from "./protocol-M4mOvMtW.js";
1
+ import { _ as WebhookTimestampError, c as signWebhook, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, t as DEFAULT_TOLERANCE_SECONDS, v as WebhookVerificationError } from "./protocol-BGKUmvlI.js";
2
2
  import { Clock, Effect } from "effect";
3
3
  //#region src/effect.ts
4
+ /**
5
+ * Fails with the verification errors `verifyWebhookAt` rejects with. Any other rejection, such as
6
+ * a `TypeError` from input that violates the declared types, is a defect rather than a failed
7
+ * delivery.
8
+ */
9
+ const verifyAt = (input) => Effect.flatten(Effect.promise(() => verifyWebhookAt(input).then((verified) => Effect.succeed(verified), (error) => error instanceof WebhookVerificationError ? Effect.fail(error) : Effect.die(error))));
10
+ /**
11
+ * Effect version of the Promise entrypoint's `verify`: verifies a delivery from its body and
12
+ * header values and succeeds with its ID and parsed event.
13
+ *
14
+ * Reads the current time from Effect's `Clock`, so `TestClock` controls the replay window in
15
+ * tests. Fails with {@link AnyWebhookVerificationError}; branch with `Effect.catchTag`. Input
16
+ * that violates the declared types, such as an undefined secret, dies instead.
17
+ */
4
18
  const verify = Effect.fn("Webhooks.verify")(function* (options) {
5
19
  const nowMilliseconds = yield* Clock.currentTimeMillis;
6
- return yield* Effect.tryPromise({
7
- try: () => verifyWebhookAt({
8
- body: options.body,
9
- headers: {
10
- id: options.id,
11
- timestamp: options.timestamp,
12
- signature: options.signature
13
- },
14
- secret: options.secret,
15
- toleranceSeconds: options.toleranceSeconds,
16
- nowMilliseconds
17
- }),
18
- catch: (error) => error
20
+ return yield* verifyAt({
21
+ body: options.body,
22
+ headers: {
23
+ id: options.id,
24
+ timestamp: options.timestamp,
25
+ signature: options.signature
26
+ },
27
+ secret: options.secret,
28
+ toleranceSeconds: options.toleranceSeconds,
29
+ nowMilliseconds
19
30
  });
20
31
  });
32
+ /**
33
+ * Effect version of the Promise entrypoint's `verifyRequest`: verifies a delivery received as a
34
+ * Fetch API `Request` and succeeds with its ID and parsed event. Reads the body from a clone, so
35
+ * the original request stays readable.
36
+ *
37
+ * Reads the current time from Effect's `Clock`. Fails with {@link AnyWebhookVerificationError};
38
+ * a body that cannot be read fails with {@link WebhookCryptoError}. Input that violates the
39
+ * declared types dies instead.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { Effect } from "effect";
44
+ * import * as Webhooks from "@samva/webhooks/effect";
45
+ *
46
+ * const receive = (request: Request, secret: string) =>
47
+ * Webhooks.verifyRequest(request, secret).pipe(
48
+ * Effect.flatMap(({ id, event }) => Effect.promise(() => handleOnce(id, event))),
49
+ * Effect.as(new Response(null, { status: 204 })),
50
+ * Effect.catchTags({
51
+ * WebhookSignatureError: () => Effect.succeed(new Response(null, { status: 400 })),
52
+ * WebhookTimestampError: () => Effect.succeed(new Response(null, { status: 400 })),
53
+ * }),
54
+ * );
55
+ * ```
56
+ */
21
57
  const verifyRequest = Effect.fn("Webhooks.verifyRequest")(function* (request, secret, options) {
22
58
  const nowMilliseconds = yield* Clock.currentTimeMillis;
23
59
  const body = yield* Effect.tryPromise({
24
60
  try: async () => new Uint8Array(await request.clone().arrayBuffer()),
25
61
  catch: () => new WebhookCryptoError({ detail: "Unable to read webhook request body" })
26
62
  });
27
- return yield* Effect.tryPromise({
28
- try: () => verifyWebhookAt({
29
- body,
30
- headers: readWebhookHeaders(request.headers),
31
- secret,
32
- toleranceSeconds: options?.toleranceSeconds,
33
- nowMilliseconds
34
- }),
35
- catch: (error) => error
63
+ return yield* verifyAt({
64
+ body,
65
+ headers: {
66
+ id: request.headers.get(WEBHOOK_ID_HEADER),
67
+ timestamp: request.headers.get(WEBHOOK_TIMESTAMP_HEADER),
68
+ signature: request.headers.get(WEBHOOK_SIGNATURE_HEADER)
69
+ },
70
+ secret,
71
+ toleranceSeconds: options?.toleranceSeconds,
72
+ nowMilliseconds
36
73
  });
37
74
  });
38
75
  //#endregion
package/dist/errors.d.ts CHANGED
@@ -1,28 +1,77 @@
1
1
  interface WebhookVerificationFailureOptions {
2
2
  readonly detail: string;
3
3
  }
4
+ /** Name of one of the three Standard Webhooks signing headers a delivery carries. */
5
+ export type WebhookHeaderName = "webhook-id" | "webhook-timestamp" | "webhook-signature";
6
+ /**
7
+ * Base class for every failure this package reports while verifying a webhook delivery.
8
+ *
9
+ * Each subclass carries a literal `_tag` (also used as `name`), so you can branch on the failure
10
+ * with a `switch` or Effect's `catchTag`. `detail` is a human-readable reason for your logs; do not
11
+ * echo it to the sender, and do not match on its wording.
12
+ */
4
13
  export declare abstract class WebhookVerificationError<Tag extends string = string> extends Error {
14
+ /** Discriminant naming the concrete failure; equal to the subclass name. */
5
15
  readonly _tag: Tag;
16
+ /** Human-readable reason for logs. The wording is not a stable contract. */
6
17
  readonly detail: string;
7
18
  protected constructor(tag: Tag, options: WebhookVerificationFailureOptions);
8
19
  }
20
+ /**
21
+ * The signing secret is unusable: it lacks the `whsec_` prefix, the remainder is not Base64, or
22
+ * the decoded key length is outside the accepted range (`detail` names the range). This is a
23
+ * configuration error on the receiving side, not a bad delivery; copy the endpoint's signing
24
+ * secret from Samva again.
25
+ */
9
26
  export declare class InvalidWebhookSecretError extends WebhookVerificationError<"InvalidWebhookSecretError"> {
10
27
  constructor(options: WebhookVerificationFailureOptions);
11
28
  }
29
+ /**
30
+ * One of the `webhook-id`, `webhook-timestamp`, or `webhook-signature` headers is missing or
31
+ * empty; `header` names it. The request is not a Samva delivery, or something between Samva and
32
+ * your handler stripped the headers.
33
+ */
12
34
  export declare class InvalidWebhookHeadersError extends WebhookVerificationError<"InvalidWebhookHeadersError"> {
13
- constructor(options: WebhookVerificationFailureOptions);
35
+ /** The first signing header, in id, timestamp, signature order, that is missing or empty. */
36
+ readonly header: WebhookHeaderName;
37
+ constructor(options: WebhookVerificationFailureOptions & {
38
+ readonly header: WebhookHeaderName;
39
+ });
14
40
  }
41
+ /**
42
+ * The `webhook-timestamp` header is not an integer count of Unix seconds, it lies further from the
43
+ * current time than the replay tolerance, or the tolerance you passed is negative or not finite.
44
+ * A delivery outside the tolerance is rejected even when its signature is valid; that bounds how
45
+ * long a captured request can be replayed. Repeated failures on genuine deliveries usually mean
46
+ * the receiving server's clock is wrong.
47
+ */
15
48
  export declare class WebhookTimestampError extends WebhookVerificationError<"WebhookTimestampError"> {
16
49
  constructor(options: WebhookVerificationFailureOptions);
17
50
  }
51
+ /**
52
+ * No signature in the `webhook-signature` header matches the delivery, or no verification key was
53
+ * supplied. Treat the request as forged and reject it. When genuine deliveries fail, the usual
54
+ * causes are the wrong endpoint secret or a body that was parsed and re-serialized before
55
+ * verification instead of passed through as the exact bytes received.
56
+ */
18
57
  export declare class WebhookSignatureError extends WebhookVerificationError<"WebhookSignatureError"> {
19
58
  constructor(options: WebhookVerificationFailureOptions);
20
59
  }
60
+ /**
61
+ * The signature is valid, but the body is not UTF-8 JSON holding exactly the `type`,
62
+ * `timestamp`, and `data` fields of a Samva event envelope.
63
+ */
21
64
  export declare class MalformedWebhookPayloadError extends WebhookVerificationError<"MalformedWebhookPayloadError"> {
22
65
  constructor(options: WebhookVerificationFailureOptions);
23
66
  }
67
+ /**
68
+ * The runtime's Web Crypto implementation could not import a key or compute a signature, none of
69
+ * the supplied public keys could be imported, or the Effect entrypoint could not read the request
70
+ * body. Points at the runtime or its configuration rather than at the delivery.
71
+ */
24
72
  export declare class WebhookCryptoError extends WebhookVerificationError<"WebhookCryptoError"> {
25
73
  constructor(options: WebhookVerificationFailureOptions);
26
74
  }
75
+ /** Every concrete failure a verification call can report, discriminated by `_tag`. */
27
76
  export type AnyWebhookVerificationError = InvalidWebhookSecretError | InvalidWebhookHeadersError | WebhookTimestampError | WebhookSignatureError | MalformedWebhookPayloadError | WebhookCryptoError;
28
77
  export {};
package/dist/promise.d.ts CHANGED
@@ -1,16 +1,108 @@
1
1
  import { type AnyWebhookVerificationError, type VerifiedWebhook } from "./protocol.js";
2
+ /** A delivery's parts, for frameworks that do not expose a Fetch API `Request`. */
2
3
  export interface VerifyOptions {
4
+ /**
5
+ * The request body exactly as received, as text or bytes. Read it before any JSON body parser
6
+ * runs: parsing and re-serializing changes the bytes and fails the signature check.
7
+ */
3
8
  readonly body: string | Uint8Array;
4
- readonly id: string;
5
- readonly timestamp: string;
6
- readonly signature: string;
9
+ /**
10
+ * Value of the `webhook-id` header (`WEBHOOK_ID_HEADER`). Pass an absent header through as
11
+ * `null` or `undefined`; verification rejects it with `InvalidWebhookHeadersError`.
12
+ */
13
+ readonly id: string | null | undefined;
14
+ /** Value of the `webhook-timestamp` header (`WEBHOOK_TIMESTAMP_HEADER`), or absent. */
15
+ readonly timestamp: string | null | undefined;
16
+ /** Value of the `webhook-signature` header (`WEBHOOK_SIGNATURE_HEADER`), unmodified, or absent. */
17
+ readonly signature: string | null | undefined;
18
+ /** The endpoint's signing secret, including its `whsec_` prefix. Keep it on the server. */
7
19
  readonly secret: string;
20
+ /** Replay tolerance in seconds; defaults to `DEFAULT_TOLERANCE_SECONDS`. */
8
21
  readonly toleranceSeconds?: number | undefined;
9
22
  }
23
+ /**
24
+ * Verifies a Samva webhook delivery from its body and header values, then returns its ID and
25
+ * parsed event.
26
+ *
27
+ * Verification follows Standard Webhooks: the `webhook-timestamp` must be within the replay
28
+ * tolerance of the current time, and an HMAC-SHA256 signature over `<id>.<timestamp>.<body>` with
29
+ * the endpoint secret must match one entry in `webhook-signature`. Only then is the body parsed.
30
+ * A valid delivery can arrive more than once, so handle each returned `id` once.
31
+ *
32
+ * Rejects with a subclass of `WebhookVerificationError`; {@link AnyWebhookVerificationError}
33
+ * lists them. A missing or empty header rejects with `InvalidWebhookHeadersError`, whose `header`
34
+ * names it. Use {@link safeVerify} for a result instead of a rejection.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { verify } from "@samva/webhooks";
39
+ *
40
+ * // Express: mount express.raw({ type: "application/json" }) on this route so body is a Buffer.
41
+ * app.post("/webhooks/samva", async (req, res) => {
42
+ * const verified = await verify({
43
+ * body: req.body,
44
+ * id: req.header("webhook-id"),
45
+ * timestamp: req.header("webhook-timestamp"),
46
+ * signature: req.header("webhook-signature"),
47
+ * secret: process.env.SAMVA_WEBHOOK_SECRET!,
48
+ * });
49
+ * await handleOnce(verified.id, verified.event);
50
+ * res.sendStatus(204);
51
+ * });
52
+ * ```
53
+ */
10
54
  export declare const verify: (options: VerifyOptions) => Promise<VerifiedWebhook>;
55
+ /**
56
+ * Verifies a Samva webhook delivery received as a Fetch API `Request` (Cloudflare Workers, Bun,
57
+ * Deno, Next.js route handlers, Hono, and similar), then returns its ID and parsed event.
58
+ *
59
+ * Reads the body from a clone, so the original request stays readable. Performs the same checks
60
+ * as {@link verify} and rejects with the same errors; a missing signing header rejects with
61
+ * `InvalidWebhookHeadersError`, whose `header` names it.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * import { verifyRequest, WebhookVerificationError } from "@samva/webhooks";
66
+ *
67
+ * export default {
68
+ * async fetch(request: Request, env: { SAMVA_WEBHOOK_SECRET: string }) {
69
+ * try {
70
+ * const { id, event } = await verifyRequest(request, env.SAMVA_WEBHOOK_SECRET);
71
+ * await handleOnce(id, event);
72
+ * return new Response(null, { status: 204 });
73
+ * } catch (error) {
74
+ * if (error instanceof WebhookVerificationError) {
75
+ * return new Response(null, { status: 400 });
76
+ * }
77
+ * throw error;
78
+ * }
79
+ * },
80
+ * };
81
+ * ```
82
+ */
11
83
  export declare const verifyRequest: (request: Request, secret: string, options?: {
12
84
  readonly toleranceSeconds?: number | undefined;
13
85
  }) => Promise<VerifiedWebhook>;
86
+ /**
87
+ * Runs {@link verify} and resolves to a result instead of rejecting: `{ verified: true, value }`
88
+ * on success, or `{ verified: false, error }` carrying the verification failure. Any other error,
89
+ * such as a `TypeError` from input that violates the declared types, rejects.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { safeVerify, type VerifyOptions } from "@samva/webhooks";
94
+ *
95
+ * async function receive(delivery: VerifyOptions): Promise<Response> {
96
+ * const result = await safeVerify(delivery);
97
+ * if (!result.verified) {
98
+ * console.warn("rejected webhook", result.error._tag);
99
+ * return new Response(null, { status: 400 });
100
+ * }
101
+ * await handleOnce(result.value.id, result.value.event);
102
+ * return new Response(null, { status: 204 });
103
+ * }
104
+ * ```
105
+ */
14
106
  export declare const safeVerify: (options: VerifyOptions) => Promise<{
15
107
  readonly verified: true;
16
108
  readonly value: VerifiedWebhook;
@@ -19,5 +111,5 @@ export declare const safeVerify: (options: VerifyOptions) => Promise<{
19
111
  readonly error: AnyWebhookVerificationError;
20
112
  }>;
21
113
  export { DEFAULT_TOLERANCE_SECONDS, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, generateWebhookSecret, signWebhook, } from "./protocol.js";
22
- export type { AnyWebhookVerificationError, SamvaWebhookEvent, VerifiedWebhook, } from "./protocol.js";
114
+ export type { AnyWebhookVerificationError, SamvaWebhookEvent, VerifiedWebhook, WebhookHeaderName, } from "./protocol.js";
23
115
  export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./protocol.js";
package/dist/promise.js CHANGED
@@ -1,5 +1,36 @@
1
- import { _ as WebhookTimestampError, c as signWebhook, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, s as readWebhookHeaders, t as DEFAULT_TOLERANCE_SECONDS, v as WebhookVerificationError } from "./protocol-M4mOvMtW.js";
1
+ import { _ as WebhookTimestampError, c as signWebhook, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, s as readWebhookHeaders, t as DEFAULT_TOLERANCE_SECONDS, v as WebhookVerificationError } from "./protocol-BGKUmvlI.js";
2
2
  //#region src/promise.ts
3
+ /**
4
+ * Verifies a Samva webhook delivery from its body and header values, then returns its ID and
5
+ * parsed event.
6
+ *
7
+ * Verification follows Standard Webhooks: the `webhook-timestamp` must be within the replay
8
+ * tolerance of the current time, and an HMAC-SHA256 signature over `<id>.<timestamp>.<body>` with
9
+ * the endpoint secret must match one entry in `webhook-signature`. Only then is the body parsed.
10
+ * A valid delivery can arrive more than once, so handle each returned `id` once.
11
+ *
12
+ * Rejects with a subclass of `WebhookVerificationError`; {@link AnyWebhookVerificationError}
13
+ * lists them. A missing or empty header rejects with `InvalidWebhookHeadersError`, whose `header`
14
+ * names it. Use {@link safeVerify} for a result instead of a rejection.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { verify } from "@samva/webhooks";
19
+ *
20
+ * // Express: mount express.raw({ type: "application/json" }) on this route so body is a Buffer.
21
+ * app.post("/webhooks/samva", async (req, res) => {
22
+ * const verified = await verify({
23
+ * body: req.body,
24
+ * id: req.header("webhook-id"),
25
+ * timestamp: req.header("webhook-timestamp"),
26
+ * signature: req.header("webhook-signature"),
27
+ * secret: process.env.SAMVA_WEBHOOK_SECRET!,
28
+ * });
29
+ * await handleOnce(verified.id, verified.event);
30
+ * res.sendStatus(204);
31
+ * });
32
+ * ```
33
+ */
3
34
  const verify = (options) => verifyWebhookAt({
4
35
  body: options.body,
5
36
  headers: {
@@ -11,6 +42,34 @@ const verify = (options) => verifyWebhookAt({
11
42
  toleranceSeconds: options.toleranceSeconds,
12
43
  nowMilliseconds: Date.now()
13
44
  });
45
+ /**
46
+ * Verifies a Samva webhook delivery received as a Fetch API `Request` (Cloudflare Workers, Bun,
47
+ * Deno, Next.js route handlers, Hono, and similar), then returns its ID and parsed event.
48
+ *
49
+ * Reads the body from a clone, so the original request stays readable. Performs the same checks
50
+ * as {@link verify} and rejects with the same errors; a missing signing header rejects with
51
+ * `InvalidWebhookHeadersError`, whose `header` names it.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import { verifyRequest, WebhookVerificationError } from "@samva/webhooks";
56
+ *
57
+ * export default {
58
+ * async fetch(request: Request, env: { SAMVA_WEBHOOK_SECRET: string }) {
59
+ * try {
60
+ * const { id, event } = await verifyRequest(request, env.SAMVA_WEBHOOK_SECRET);
61
+ * await handleOnce(id, event);
62
+ * return new Response(null, { status: 204 });
63
+ * } catch (error) {
64
+ * if (error instanceof WebhookVerificationError) {
65
+ * return new Response(null, { status: 400 });
66
+ * }
67
+ * throw error;
68
+ * }
69
+ * },
70
+ * };
71
+ * ```
72
+ */
14
73
  const verifyRequest = async (request, secret, options) => verifyWebhookAt({
15
74
  body: new Uint8Array(await request.clone().arrayBuffer()),
16
75
  headers: readWebhookHeaders(request.headers),
@@ -18,6 +77,26 @@ const verifyRequest = async (request, secret, options) => verifyWebhookAt({
18
77
  toleranceSeconds: options?.toleranceSeconds,
19
78
  nowMilliseconds: Date.now()
20
79
  });
80
+ /**
81
+ * Runs {@link verify} and resolves to a result instead of rejecting: `{ verified: true, value }`
82
+ * on success, or `{ verified: false, error }` carrying the verification failure. Any other error,
83
+ * such as a `TypeError` from input that violates the declared types, rejects.
84
+ *
85
+ * @example
86
+ * ```ts
87
+ * import { safeVerify, type VerifyOptions } from "@samva/webhooks";
88
+ *
89
+ * async function receive(delivery: VerifyOptions): Promise<Response> {
90
+ * const result = await safeVerify(delivery);
91
+ * if (!result.verified) {
92
+ * console.warn("rejected webhook", result.error._tag);
93
+ * return new Response(null, { status: 400 });
94
+ * }
95
+ * await handleOnce(result.value.id, result.value.event);
96
+ * return new Response(null, { status: 204 });
97
+ * }
98
+ * ```
99
+ */
21
100
  const safeVerify = async (options) => {
22
101
  try {
23
102
  return {
@@ -25,10 +104,11 @@ const safeVerify = async (options) => {
25
104
  value: await verify(options)
26
105
  };
27
106
  } catch (error) {
28
- return {
107
+ if (error instanceof WebhookVerificationError) return {
29
108
  verified: false,
30
109
  error
31
110
  };
111
+ throw error;
32
112
  }
33
113
  };
34
114
  //#endregion
@@ -1,6 +1,15 @@
1
1
  //#region src/errors.ts
2
+ /**
3
+ * Base class for every failure this package reports while verifying a webhook delivery.
4
+ *
5
+ * Each subclass carries a literal `_tag` (also used as `name`), so you can branch on the failure
6
+ * with a `switch` or Effect's `catchTag`. `detail` is a human-readable reason for your logs; do not
7
+ * echo it to the sender, and do not match on its wording.
8
+ */
2
9
  var WebhookVerificationError = class extends Error {
10
+ /** Discriminant naming the concrete failure; equal to the subclass name. */
3
11
  _tag;
12
+ /** Human-readable reason for logs. The wording is not a stable contract. */
4
13
  detail;
5
14
  constructor(tag, options) {
6
15
  super(options.detail);
@@ -9,31 +18,67 @@ var WebhookVerificationError = class extends Error {
9
18
  this.detail = options.detail;
10
19
  }
11
20
  };
21
+ /**
22
+ * The signing secret is unusable: it lacks the `whsec_` prefix, the remainder is not Base64, or
23
+ * the decoded key length is outside the accepted range (`detail` names the range). This is a
24
+ * configuration error on the receiving side, not a bad delivery; copy the endpoint's signing
25
+ * secret from Samva again.
26
+ */
12
27
  var InvalidWebhookSecretError = class extends WebhookVerificationError {
13
28
  constructor(options) {
14
29
  super("InvalidWebhookSecretError", options);
15
30
  }
16
31
  };
32
+ /**
33
+ * One of the `webhook-id`, `webhook-timestamp`, or `webhook-signature` headers is missing or
34
+ * empty; `header` names it. The request is not a Samva delivery, or something between Samva and
35
+ * your handler stripped the headers.
36
+ */
17
37
  var InvalidWebhookHeadersError = class extends WebhookVerificationError {
38
+ /** The first signing header, in id, timestamp, signature order, that is missing or empty. */
39
+ header;
18
40
  constructor(options) {
19
41
  super("InvalidWebhookHeadersError", options);
42
+ this.header = options.header;
20
43
  }
21
44
  };
45
+ /**
46
+ * The `webhook-timestamp` header is not an integer count of Unix seconds, it lies further from the
47
+ * current time than the replay tolerance, or the tolerance you passed is negative or not finite.
48
+ * A delivery outside the tolerance is rejected even when its signature is valid; that bounds how
49
+ * long a captured request can be replayed. Repeated failures on genuine deliveries usually mean
50
+ * the receiving server's clock is wrong.
51
+ */
22
52
  var WebhookTimestampError = class extends WebhookVerificationError {
23
53
  constructor(options) {
24
54
  super("WebhookTimestampError", options);
25
55
  }
26
56
  };
57
+ /**
58
+ * No signature in the `webhook-signature` header matches the delivery, or no verification key was
59
+ * supplied. Treat the request as forged and reject it. When genuine deliveries fail, the usual
60
+ * causes are the wrong endpoint secret or a body that was parsed and re-serialized before
61
+ * verification instead of passed through as the exact bytes received.
62
+ */
27
63
  var WebhookSignatureError = class extends WebhookVerificationError {
28
64
  constructor(options) {
29
65
  super("WebhookSignatureError", options);
30
66
  }
31
67
  };
68
+ /**
69
+ * The signature is valid, but the body is not UTF-8 JSON holding exactly the `type`,
70
+ * `timestamp`, and `data` fields of a Samva event envelope.
71
+ */
32
72
  var MalformedWebhookPayloadError = class extends WebhookVerificationError {
33
73
  constructor(options) {
34
74
  super("MalformedWebhookPayloadError", options);
35
75
  }
36
76
  };
77
+ /**
78
+ * The runtime's Web Crypto implementation could not import a key or compute a signature, none of
79
+ * the supplied public keys could be imported, or the Effect entrypoint could not read the request
80
+ * body. Points at the runtime or its configuration rather than at the delivery.
81
+ */
37
82
  var WebhookCryptoError = class extends WebhookVerificationError {
38
83
  constructor(options) {
39
84
  super("WebhookCryptoError", options);
@@ -41,9 +86,24 @@ var WebhookCryptoError = class extends WebhookVerificationError {
41
86
  };
42
87
  //#endregion
43
88
  //#region src/protocol.ts
89
+ /**
90
+ * Header carrying the delivery's unique message ID. Retries of the same delivery reuse it, so it
91
+ * is the key for deduplicating deliveries you have already handled.
92
+ */
44
93
  const WEBHOOK_ID_HEADER = "webhook-id";
94
+ /** Header carrying the time Samva signed the delivery, as integer Unix seconds. */
45
95
  const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
96
+ /**
97
+ * Header carrying one or more space-separated `<version>,<base64 signature>` entries. More than one
98
+ * entry appears while a signing secret is being rotated; a delivery verifies when any entry of the
99
+ * expected version matches.
100
+ */
46
101
  const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
102
+ /**
103
+ * Replay tolerance, in seconds, used when a caller passes no `toleranceSeconds`. A delivery whose
104
+ * `webhook-timestamp` differs from the current time by more than this, in either direction, is
105
+ * rejected with {@link WebhookTimestampError}.
106
+ */
47
107
  const DEFAULT_TOLERANCE_SECONDS = 300;
48
108
  const SECRET_PREFIX = "whsec_";
49
109
  const ASYMMETRIC_ALGORITHM = {
@@ -150,20 +210,54 @@ const parseEvent = (body) => {
150
210
  throw new MalformedWebhookPayloadError({ detail: "Webhook body must be valid UTF-8 JSON containing type, timestamp, and data" });
151
211
  }
152
212
  };
213
+ /**
214
+ * Creates a random `whsec_`-prefixed signing secret from the runtime's secure random source.
215
+ * Samva issues each endpoint's secret for you; use this to sign test deliveries with
216
+ * {@link signWebhook}.
217
+ */
153
218
  const generateWebhookSecret = () => {
154
219
  const bytes = crypto.getRandomValues(new Uint8Array(GENERATED_SECRET_BYTES));
155
220
  return `${SECRET_PREFIX}${bytesToBase64(bytes)}`;
156
221
  };
222
+ /**
223
+ * Builds the bytes every signature version covers: `<id>.<timestamp>.<body>`. Use it to verify a
224
+ * signature with your own crypto code; {@link verifyWebhookAt} and
225
+ * {@link verifyWebhookAtWithPublicKeys} build it for you.
226
+ */
157
227
  const webhookSignedContent = (input) => signedBytes(input.id, input.timestamp, toBytes(input.body));
228
+ /** Formats raw signature bytes as one `<version>,<base64>` entry of the `webhook-signature` header. */
158
229
  const encodeWebhookSignature = (version, signature) => `${version},${bytesToBase64(signature)}`;
230
+ /**
231
+ * Computes the `v1` HMAC-SHA256 signature Samva sends, formatted as one `webhook-signature`
232
+ * entry. Use it to produce signed test deliveries for your handler.
233
+ *
234
+ * `timestamp` is Unix seconds; a number is stringified, and the verifier requires an integer.
235
+ * Rejects with {@link InvalidWebhookSecretError} for an unusable secret and
236
+ * {@link WebhookCryptoError} when Web Crypto fails.
237
+ */
159
238
  const signWebhook = async (input) => {
160
239
  const timestamp = String(input.timestamp);
161
240
  const signature = await hmac(input.secret, input.id, timestamp, toBytes(input.body));
162
241
  return encodeWebhookSignature("v1", signature);
163
242
  };
243
+ const requireHeader = (header, value) => {
244
+ if (value === null || value === void 0) throw new InvalidWebhookHeadersError({
245
+ header,
246
+ detail: `${header} header is missing`
247
+ });
248
+ if (value.length === 0) throw new InvalidWebhookHeadersError({
249
+ header,
250
+ detail: `${header} header is empty`
251
+ });
252
+ return value;
253
+ };
254
+ const requireHeaders = (values) => ({
255
+ id: requireHeader(WEBHOOK_ID_HEADER, values.id),
256
+ timestamp: requireHeader(WEBHOOK_TIMESTAMP_HEADER, values.timestamp),
257
+ signature: requireHeader(WEBHOOK_SIGNATURE_HEADER, values.signature)
258
+ });
164
259
  const freshHeaders = (input) => {
165
- const { id, timestamp, signature } = input.headers;
166
- if (id.length === 0 || timestamp.length === 0 || signature.length === 0) throw new InvalidWebhookHeadersError({ detail: "webhook-id, webhook-timestamp, and webhook-signature are required" });
260
+ const { id, timestamp, signature } = requireHeaders(input.headers);
167
261
  const timestampSeconds = Number(timestamp);
168
262
  if (!Number.isInteger(timestampSeconds)) throw new WebhookTimestampError({ detail: "webhook-timestamp must be Unix seconds" });
169
263
  const toleranceSeconds = input.toleranceSeconds ?? 300;
@@ -175,6 +269,20 @@ const freshHeaders = (input) => {
175
269
  signature
176
270
  };
177
271
  };
272
+ /**
273
+ * Verifies a `v1` (HMAC-SHA256) delivery against an explicit current time. The `verify` and
274
+ * `verifyRequest` entry points call this with the current clock; call it directly when your
275
+ * framework owns the clock or a test needs a fixed time.
276
+ *
277
+ * Checks, in order: all three headers are present and non-empty; the timestamp is integer Unix seconds within
278
+ * `toleranceSeconds` of `nowMilliseconds`; some `v1` entry in the signature header matches, using
279
+ * a constant-time comparison; the body is a Samva event envelope. The replay window rejects stale
280
+ * deliveries but does not detect a repeat inside it; deduplicate on the returned `id`.
281
+ *
282
+ * Rejects with {@link InvalidWebhookHeadersError}, {@link WebhookTimestampError},
283
+ * {@link InvalidWebhookSecretError}, {@link WebhookSignatureError},
284
+ * {@link MalformedWebhookPayloadError}, or {@link WebhookCryptoError}.
285
+ */
178
286
  const verifyWebhookAt = async (input) => {
179
287
  const { id, timestamp, signature } = freshHeaders(input);
180
288
  const body = toBytes(input.body);
@@ -185,6 +293,15 @@ const verifyWebhookAt = async (input) => {
185
293
  event: parseEvent(body)
186
294
  };
187
295
  };
296
+ /**
297
+ * Verifies a `v2` (RSASSA-PKCS1-v1_5, SHA-256) delivery against one or more public keys, each a
298
+ * Base64-encoded SPKI DER key. Pass several keys during a key rotation; the delivery verifies when
299
+ * any `v2` signature entry matches any key.
300
+ *
301
+ * Header, timestamp, and envelope checks match {@link verifyWebhookAt}. Keys that fail to import
302
+ * are skipped. Rejects with {@link WebhookSignatureError} when `publicKeys` is empty or nothing
303
+ * matches, and {@link WebhookCryptoError} when none of the keys imports.
304
+ */
188
305
  const verifyWebhookAtWithPublicKeys = async (input) => {
189
306
  const { id, timestamp, signature } = freshHeaders(input);
190
307
  if (input.publicKeys.length === 0) throw new WebhookSignatureError({ detail: "No webhook verification key is available" });
@@ -199,12 +316,18 @@ const verifyWebhookAtWithPublicKeys = async (input) => {
199
316
  event: parseEvent(body)
200
317
  };
201
318
  };
202
- const readWebhookHeaders = (headers) => ({
203
- id: headers.get("webhook-id") ?? "",
204
- timestamp: headers.get("webhook-timestamp") ?? "",
205
- signature: headers.get("webhook-signature") ?? ""
319
+ /**
320
+ * Reads the three signing headers from a Fetch API `Headers` object, for code that needs a header
321
+ * value, such as the timestamp, before or beside verification. Throws
322
+ * {@link InvalidWebhookHeadersError}, naming the header, when one is missing or empty; call it
323
+ * inside the same error handling as the verification that follows.
324
+ */
325
+ const readWebhookHeaders = (headers) => requireHeaders({
326
+ id: headers.get(WEBHOOK_ID_HEADER),
327
+ timestamp: headers.get(WEBHOOK_TIMESTAMP_HEADER),
328
+ signature: headers.get(WEBHOOK_SIGNATURE_HEADER)
206
329
  });
207
330
  //#endregion
208
331
  export { WebhookTimestampError as _, encodeWebhookSignature as a, signWebhook as c, webhookSignedContent as d, InvalidWebhookHeadersError as f, WebhookSignatureError as g, WebhookCryptoError as h, WEBHOOK_TIMESTAMP_HEADER as i, verifyWebhookAt as l, MalformedWebhookPayloadError as m, WEBHOOK_ID_HEADER as n, generateWebhookSecret as o, InvalidWebhookSecretError as p, WEBHOOK_SIGNATURE_HEADER as r, readWebhookHeaders as s, DEFAULT_TOLERANCE_SECONDS as t, verifyWebhookAtWithPublicKeys as u, WebhookVerificationError as v };
209
332
 
210
- //# sourceMappingURL=protocol-M4mOvMtW.js.map
333
+ //# sourceMappingURL=protocol-BGKUmvlI.js.map
@@ -1,46 +1,148 @@
1
+ /**
2
+ * Header carrying the delivery's unique message ID. Retries of the same delivery reuse it, so it
3
+ * is the key for deduplicating deliveries you have already handled.
4
+ */
1
5
  export declare const WEBHOOK_ID_HEADER = "webhook-id";
6
+ /** Header carrying the time Samva signed the delivery, as integer Unix seconds. */
2
7
  export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
8
+ /**
9
+ * Header carrying one or more space-separated `<version>,<base64 signature>` entries. More than one
10
+ * entry appears while a signing secret is being rotated; a delivery verifies when any entry of the
11
+ * expected version matches.
12
+ */
3
13
  export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
14
+ /**
15
+ * Replay tolerance, in seconds, used when a caller passes no `toleranceSeconds`. A delivery whose
16
+ * `webhook-timestamp` differs from the current time by more than this, in either direction, is
17
+ * rejected with {@link WebhookTimestampError}.
18
+ */
4
19
  export declare const DEFAULT_TOLERANCE_SECONDS = 300;
20
+ /** The event envelope carried in a verified delivery's body. */
5
21
  export interface SamvaWebhookEvent {
22
+ /** Event type, such as `message.delivered`. Branch on it before reading `data`. */
6
23
  readonly type: string;
24
+ /** When the event occurred, as an ISO 8601 string. Distinct from the delivery's signing time. */
7
25
  readonly timestamp: string;
26
+ /** Event-specific payload. Its shape depends on `type`; validate it before use. */
8
27
  readonly data: unknown;
9
28
  }
29
+ /** A delivery whose signature, timestamp, and envelope all checked out. */
10
30
  export interface VerifiedWebhook {
31
+ /**
32
+ * The delivery's `webhook-id` header. Stable across retries of the same delivery; record it to
33
+ * handle each delivery once, because a valid delivery may arrive more than once within the
34
+ * replay tolerance.
35
+ */
11
36
  readonly id: string;
12
37
  readonly event: SamvaWebhookEvent;
13
38
  }
39
+ /** The three signing headers of a delivery, each present and non-empty, read verbatim. */
14
40
  export interface WebhookHeaders {
41
+ /** Value of {@link WEBHOOK_ID_HEADER}. */
15
42
  readonly id: string;
43
+ /** Value of {@link WEBHOOK_TIMESTAMP_HEADER}. */
16
44
  readonly timestamp: string;
45
+ /** Value of {@link WEBHOOK_SIGNATURE_HEADER}. */
17
46
  readonly signature: string;
18
47
  }
48
+ /**
49
+ * The three signing header values as your framework reports them. An absent header is `null` (as
50
+ * Fetch API `Headers.get` returns it) or `undefined` (as Node.js and Express return it); pass it
51
+ * through as-is and verification rejects it with {@link InvalidWebhookHeadersError}.
52
+ */
53
+ export interface WebhookHeaderValues {
54
+ /** Value of {@link WEBHOOK_ID_HEADER}. */
55
+ readonly id: string | null | undefined;
56
+ /** Value of {@link WEBHOOK_TIMESTAMP_HEADER}. */
57
+ readonly timestamp: string | null | undefined;
58
+ /** Value of {@link WEBHOOK_SIGNATURE_HEADER}. */
59
+ readonly signature: string | null | undefined;
60
+ }
61
+ /** Input to {@link verifyWebhookAt}, which takes the current time explicitly. */
19
62
  export interface VerifyWebhookInput {
63
+ /**
64
+ * The request body exactly as received. Pass the raw text or bytes; a body that was parsed and
65
+ * re-serialized no longer matches its signature.
66
+ */
20
67
  readonly body: string | Uint8Array;
21
- readonly headers: WebhookHeaders;
68
+ readonly headers: WebhookHeaderValues;
69
+ /** The endpoint's signing secret, including its `whsec_` prefix. Keep it on the server. */
22
70
  readonly secret: string;
71
+ /** Replay tolerance in seconds; defaults to {@link DEFAULT_TOLERANCE_SECONDS}. */
23
72
  readonly toleranceSeconds?: number | undefined;
73
+ /** The current time in Unix milliseconds, as `Date.now()` returns it. */
24
74
  readonly nowMilliseconds: number;
25
75
  }
76
+ /**
77
+ * Signature scheme version inside a `webhook-signature` entry: `v1` is HMAC-SHA256 with the
78
+ * endpoint's shared secret; `v2` is RSASSA-PKCS1-v1_5 with SHA-256, checked against a public key.
79
+ */
26
80
  export type WebhookSignatureVersion = "v1" | "v2";
81
+ /**
82
+ * Creates a random `whsec_`-prefixed signing secret from the runtime's secure random source.
83
+ * Samva issues each endpoint's secret for you; use this to sign test deliveries with
84
+ * {@link signWebhook}.
85
+ */
27
86
  export declare const generateWebhookSecret: () => string;
87
+ /**
88
+ * Builds the bytes every signature version covers: `<id>.<timestamp>.<body>`. Use it to verify a
89
+ * signature with your own crypto code; {@link verifyWebhookAt} and
90
+ * {@link verifyWebhookAtWithPublicKeys} build it for you.
91
+ */
28
92
  export declare const webhookSignedContent: (input: {
29
93
  readonly id: string;
30
94
  readonly timestamp: string;
31
95
  readonly body: string | Uint8Array;
32
96
  }) => Uint8Array;
97
+ /** Formats raw signature bytes as one `<version>,<base64>` entry of the `webhook-signature` header. */
33
98
  export declare const encodeWebhookSignature: (version: WebhookSignatureVersion, signature: Uint8Array) => string;
99
+ /**
100
+ * Computes the `v1` HMAC-SHA256 signature Samva sends, formatted as one `webhook-signature`
101
+ * entry. Use it to produce signed test deliveries for your handler.
102
+ *
103
+ * `timestamp` is Unix seconds; a number is stringified, and the verifier requires an integer.
104
+ * Rejects with {@link InvalidWebhookSecretError} for an unusable secret and
105
+ * {@link WebhookCryptoError} when Web Crypto fails.
106
+ */
34
107
  export declare const signWebhook: (input: {
35
108
  readonly body: string | Uint8Array;
36
109
  readonly id: string;
37
110
  readonly timestamp: string | number;
38
111
  readonly secret: string;
39
112
  }) => Promise<string>;
113
+ /**
114
+ * Verifies a `v1` (HMAC-SHA256) delivery against an explicit current time. The `verify` and
115
+ * `verifyRequest` entry points call this with the current clock; call it directly when your
116
+ * framework owns the clock or a test needs a fixed time.
117
+ *
118
+ * Checks, in order: all three headers are present and non-empty; the timestamp is integer Unix seconds within
119
+ * `toleranceSeconds` of `nowMilliseconds`; some `v1` entry in the signature header matches, using
120
+ * a constant-time comparison; the body is a Samva event envelope. The replay window rejects stale
121
+ * deliveries but does not detect a repeat inside it; deduplicate on the returned `id`.
122
+ *
123
+ * Rejects with {@link InvalidWebhookHeadersError}, {@link WebhookTimestampError},
124
+ * {@link InvalidWebhookSecretError}, {@link WebhookSignatureError},
125
+ * {@link MalformedWebhookPayloadError}, or {@link WebhookCryptoError}.
126
+ */
40
127
  export declare const verifyWebhookAt: (input: VerifyWebhookInput) => Promise<VerifiedWebhook>;
128
+ /**
129
+ * Verifies a `v2` (RSASSA-PKCS1-v1_5, SHA-256) delivery against one or more public keys, each a
130
+ * Base64-encoded SPKI DER key. Pass several keys during a key rotation; the delivery verifies when
131
+ * any `v2` signature entry matches any key.
132
+ *
133
+ * Header, timestamp, and envelope checks match {@link verifyWebhookAt}. Keys that fail to import
134
+ * are skipped. Rejects with {@link WebhookSignatureError} when `publicKeys` is empty or nothing
135
+ * matches, and {@link WebhookCryptoError} when none of the keys imports.
136
+ */
41
137
  export declare const verifyWebhookAtWithPublicKeys: (input: Omit<VerifyWebhookInput, "secret"> & {
42
138
  readonly publicKeys: ReadonlyArray<string>;
43
139
  }) => Promise<VerifiedWebhook>;
140
+ /**
141
+ * Reads the three signing headers from a Fetch API `Headers` object, for code that needs a header
142
+ * value, such as the timestamp, before or beside verification. Throws
143
+ * {@link InvalidWebhookHeadersError}, naming the header, when one is missing or empty; call it
144
+ * inside the same error handling as the verification that follows.
145
+ */
44
146
  export declare const readWebhookHeaders: (headers: Headers) => WebhookHeaders;
45
147
  export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./errors.js";
46
- export type { AnyWebhookVerificationError } from "./errors.js";
148
+ export type { AnyWebhookVerificationError, WebhookHeaderName } from "./errors.js";
package/dist/protocol.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as WebhookTimestampError, a as encodeWebhookSignature, c as signWebhook, d as webhookSignedContent, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, s as readWebhookHeaders, t as DEFAULT_TOLERANCE_SECONDS, u as verifyWebhookAtWithPublicKeys, v as WebhookVerificationError } from "./protocol-M4mOvMtW.js";
1
+ import { _ as WebhookTimestampError, a as encodeWebhookSignature, c as signWebhook, d as webhookSignedContent, f as InvalidWebhookHeadersError, g as WebhookSignatureError, h as WebhookCryptoError, i as WEBHOOK_TIMESTAMP_HEADER, l as verifyWebhookAt, m as MalformedWebhookPayloadError, n as WEBHOOK_ID_HEADER, o as generateWebhookSecret, p as InvalidWebhookSecretError, r as WEBHOOK_SIGNATURE_HEADER, s as readWebhookHeaders, t as DEFAULT_TOLERANCE_SECONDS, u as verifyWebhookAtWithPublicKeys, v as WebhookVerificationError } from "./protocol-BGKUmvlI.js";
2
2
  export { DEFAULT_TOLERANCE_SECONDS, InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, encodeWebhookSignature, generateWebhookSecret, readWebhookHeaders, signWebhook, verifyWebhookAt, verifyWebhookAtWithPublicKeys, webhookSignedContent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/webhooks",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Runtime-neutral verification for Samva webhook deliveries",
5
5
  "keywords": [
6
6
  "samva",
@@ -58,14 +58,14 @@
58
58
  "typecheck": "tsc --noEmit"
59
59
  },
60
60
  "devDependencies": {
61
- "@effect/vitest": "4.0.0-rc.112",
62
- "effect": "4.0.0-rc.112",
61
+ "@effect/vitest": "4.0.0-rc.117",
62
+ "effect": "4.0.0-rc.117",
63
63
  "tsdown": "^0.22.13",
64
64
  "typescript": "^7.0.2",
65
65
  "vitest": "^4.1.11"
66
66
  },
67
67
  "peerDependencies": {
68
- "effect": "4.0.0-rc.112"
68
+ "effect": "4.0.0-rc.117"
69
69
  },
70
70
  "peerDependenciesMeta": {
71
71
  "effect": {