@samva/webhooks 0.1.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/AGENTS.md ADDED
@@ -0,0 +1,11 @@
1
+ # Samva webhooks consumer guide
2
+
3
+ Use `verifyRequest` from `@samva/webhooks` in the server or edge handler that receives a Samva
4
+ webhook. Verification requires the untouched request body and the endpoint signing secret; never
5
+ expose that secret to browser code.
6
+
7
+ Effect applications can import the typed adapter from `@samva/webhooks/effect` and install the
8
+ optional exact peer `effect@4.0.0-beta.102`.
9
+
10
+ Import only package subpaths declared in `package.json`. The delivery, queue, persistence, retry,
11
+ and endpoint-management implementation is not part of this package.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arya Labs, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # `@samva/webhooks`
2
+
3
+ Verify signed Samva webhook deliveries in Node.js, Bun, Cloudflare Workers, and other Web
4
+ Crypto-compatible runtimes.
5
+
6
+ ```ts
7
+ import { verifyRequest } from "@samva/webhooks";
8
+
9
+ const event = await verifyRequest(request, process.env.SAMVA_WEBHOOK_SECRET!);
10
+ ```
11
+
12
+ Verification reads the exact request body, validates the Standard Webhooks signature and replay
13
+ window, and returns the verified event envelope. Preserve the raw request body and keep the signing
14
+ secret on the server.
15
+
16
+ Effect applications can use the optional Effect entrypoint:
17
+
18
+ ```ts
19
+ import * as Webhooks from "@samva/webhooks/effect";
20
+
21
+ const event = yield * Webhooks.verifyRequest(request, secret);
22
+ ```
23
+
24
+ Install the exact optional peer `effect@4.0.0-beta.102` when using that entrypoint.
@@ -0,0 +1,10 @@
1
+ import { Effect } from "effect";
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>;
5
+ export declare const verifyRequest: (request: Request, secret: string, options?: {
6
+ readonly toleranceSeconds?: number | undefined;
7
+ } | undefined) => Effect.Effect<import("./protocol.js").VerifiedWebhook, AnyWebhookVerificationError, never>;
8
+ 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";
10
+ export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./protocol.js";
package/dist/effect.js ADDED
@@ -0,0 +1,41 @@
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";
2
+ import { Clock, Effect } from "effect";
3
+ //#region src/effect.ts
4
+ const verify = Effect.fn("Webhooks.verify")(function* (options) {
5
+ 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
19
+ });
20
+ });
21
+ const verifyRequest = Effect.fn("Webhooks.verifyRequest")(function* (request, secret, options) {
22
+ const nowMilliseconds = yield* Clock.currentTimeMillis;
23
+ const body = yield* Effect.tryPromise({
24
+ try: async () => new Uint8Array(await request.clone().arrayBuffer()),
25
+ catch: () => new WebhookCryptoError({ detail: "Unable to read webhook request body" })
26
+ });
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
36
+ });
37
+ });
38
+ //#endregion
39
+ export { DEFAULT_TOLERANCE_SECONDS, InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, generateWebhookSecret, signWebhook, verify, verifyRequest };
40
+
41
+ //# sourceMappingURL=effect.js.map
@@ -0,0 +1,28 @@
1
+ interface WebhookVerificationFailureOptions {
2
+ readonly detail: string;
3
+ }
4
+ export declare abstract class WebhookVerificationError<Tag extends string = string> extends Error {
5
+ readonly _tag: Tag;
6
+ readonly detail: string;
7
+ protected constructor(tag: Tag, options: WebhookVerificationFailureOptions);
8
+ }
9
+ export declare class InvalidWebhookSecretError extends WebhookVerificationError<"InvalidWebhookSecretError"> {
10
+ constructor(options: WebhookVerificationFailureOptions);
11
+ }
12
+ export declare class InvalidWebhookHeadersError extends WebhookVerificationError<"InvalidWebhookHeadersError"> {
13
+ constructor(options: WebhookVerificationFailureOptions);
14
+ }
15
+ export declare class WebhookTimestampError extends WebhookVerificationError<"WebhookTimestampError"> {
16
+ constructor(options: WebhookVerificationFailureOptions);
17
+ }
18
+ export declare class WebhookSignatureError extends WebhookVerificationError<"WebhookSignatureError"> {
19
+ constructor(options: WebhookVerificationFailureOptions);
20
+ }
21
+ export declare class MalformedWebhookPayloadError extends WebhookVerificationError<"MalformedWebhookPayloadError"> {
22
+ constructor(options: WebhookVerificationFailureOptions);
23
+ }
24
+ export declare class WebhookCryptoError extends WebhookVerificationError<"WebhookCryptoError"> {
25
+ constructor(options: WebhookVerificationFailureOptions);
26
+ }
27
+ export type AnyWebhookVerificationError = InvalidWebhookSecretError | InvalidWebhookHeadersError | WebhookTimestampError | WebhookSignatureError | MalformedWebhookPayloadError | WebhookCryptoError;
28
+ export {};
@@ -0,0 +1,23 @@
1
+ import { type AnyWebhookVerificationError, type VerifiedWebhook } from "./protocol.js";
2
+ export interface VerifyOptions {
3
+ readonly body: string | Uint8Array;
4
+ readonly id: string;
5
+ readonly timestamp: string;
6
+ readonly signature: string;
7
+ readonly secret: string;
8
+ readonly toleranceSeconds?: number | undefined;
9
+ }
10
+ export declare const verify: (options: VerifyOptions) => Promise<VerifiedWebhook>;
11
+ export declare const verifyRequest: (request: Request, secret: string, options?: {
12
+ readonly toleranceSeconds?: number | undefined;
13
+ }) => Promise<VerifiedWebhook>;
14
+ export declare const safeVerify: (options: VerifyOptions) => Promise<{
15
+ readonly verified: true;
16
+ readonly value: VerifiedWebhook;
17
+ } | {
18
+ readonly verified: false;
19
+ readonly error: AnyWebhookVerificationError;
20
+ }>;
21
+ 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";
23
+ export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./protocol.js";
@@ -0,0 +1,37 @@
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";
2
+ //#region src/promise.ts
3
+ const verify = (options) => verifyWebhookAt({
4
+ body: options.body,
5
+ headers: {
6
+ id: options.id,
7
+ timestamp: options.timestamp,
8
+ signature: options.signature
9
+ },
10
+ secret: options.secret,
11
+ toleranceSeconds: options.toleranceSeconds,
12
+ nowMilliseconds: Date.now()
13
+ });
14
+ const verifyRequest = async (request, secret, options) => verifyWebhookAt({
15
+ body: new Uint8Array(await request.clone().arrayBuffer()),
16
+ headers: readWebhookHeaders(request.headers),
17
+ secret,
18
+ toleranceSeconds: options?.toleranceSeconds,
19
+ nowMilliseconds: Date.now()
20
+ });
21
+ const safeVerify = async (options) => {
22
+ try {
23
+ return {
24
+ verified: true,
25
+ value: await verify(options)
26
+ };
27
+ } catch (error) {
28
+ return {
29
+ verified: false,
30
+ error
31
+ };
32
+ }
33
+ };
34
+ //#endregion
35
+ export { DEFAULT_TOLERANCE_SECONDS, InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, generateWebhookSecret, safeVerify, signWebhook, verify, verifyRequest };
36
+
37
+ //# sourceMappingURL=promise.js.map
@@ -0,0 +1,210 @@
1
+ //#region src/errors.ts
2
+ var WebhookVerificationError = class extends Error {
3
+ _tag;
4
+ detail;
5
+ constructor(tag, options) {
6
+ super(options.detail);
7
+ this.name = tag;
8
+ this._tag = tag;
9
+ this.detail = options.detail;
10
+ }
11
+ };
12
+ var InvalidWebhookSecretError = class extends WebhookVerificationError {
13
+ constructor(options) {
14
+ super("InvalidWebhookSecretError", options);
15
+ }
16
+ };
17
+ var InvalidWebhookHeadersError = class extends WebhookVerificationError {
18
+ constructor(options) {
19
+ super("InvalidWebhookHeadersError", options);
20
+ }
21
+ };
22
+ var WebhookTimestampError = class extends WebhookVerificationError {
23
+ constructor(options) {
24
+ super("WebhookTimestampError", options);
25
+ }
26
+ };
27
+ var WebhookSignatureError = class extends WebhookVerificationError {
28
+ constructor(options) {
29
+ super("WebhookSignatureError", options);
30
+ }
31
+ };
32
+ var MalformedWebhookPayloadError = class extends WebhookVerificationError {
33
+ constructor(options) {
34
+ super("MalformedWebhookPayloadError", options);
35
+ }
36
+ };
37
+ var WebhookCryptoError = class extends WebhookVerificationError {
38
+ constructor(options) {
39
+ super("WebhookCryptoError", options);
40
+ }
41
+ };
42
+ //#endregion
43
+ //#region src/protocol.ts
44
+ const WEBHOOK_ID_HEADER = "webhook-id";
45
+ const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
46
+ const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
47
+ const DEFAULT_TOLERANCE_SECONDS = 300;
48
+ const SECRET_PREFIX = "whsec_";
49
+ const ASYMMETRIC_ALGORITHM = {
50
+ name: "RSASSA-PKCS1-v1_5",
51
+ hash: "SHA-256"
52
+ };
53
+ const MINIMUM_SECRET_BYTES = 24;
54
+ const MAXIMUM_SECRET_BYTES = 64;
55
+ const GENERATED_SECRET_BYTES = 32;
56
+ const encoder = new TextEncoder();
57
+ const toBytes = (body) => typeof body === "string" ? encoder.encode(body) : body;
58
+ const toCryptoBytes = (bytes) => new Uint8Array(bytes);
59
+ const bytesToBase64 = (bytes) => {
60
+ let binary = "";
61
+ for (const byte of bytes) binary += String.fromCharCode(byte);
62
+ return btoa(binary);
63
+ };
64
+ const base64ToBytes = (encoded) => {
65
+ const binary = atob(encoded);
66
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
67
+ };
68
+ const decodeSecret = (secret) => {
69
+ if (!secret.startsWith(SECRET_PREFIX)) throw new InvalidWebhookSecretError({ detail: "Webhook secret must start with whsec_" });
70
+ const decoded = (() => {
71
+ try {
72
+ return base64ToBytes(secret.slice(6));
73
+ } catch {
74
+ throw new InvalidWebhookSecretError({ detail: "Webhook secret is not valid Base64" });
75
+ }
76
+ })();
77
+ if (decoded.byteLength < MINIMUM_SECRET_BYTES || decoded.byteLength > MAXIMUM_SECRET_BYTES) throw new InvalidWebhookSecretError({ detail: `Webhook secret must decode to ${MINIMUM_SECRET_BYTES}-${MAXIMUM_SECRET_BYTES} bytes` });
78
+ return decoded;
79
+ };
80
+ const importHmacKey = async (secret) => {
81
+ const secretBytes = decodeSecret(secret);
82
+ try {
83
+ return await crypto.subtle.importKey("raw", toCryptoBytes(secretBytes), {
84
+ name: "HMAC",
85
+ hash: "SHA-256"
86
+ }, false, ["sign"]);
87
+ } catch (cause) {
88
+ throw new WebhookCryptoError({ detail: `Unable to import webhook signing key: ${cause instanceof Error ? cause.message : String(cause)}` });
89
+ }
90
+ };
91
+ const importVerificationKey = async (publicKey) => {
92
+ try {
93
+ return await crypto.subtle.importKey("spki", toCryptoBytes(base64ToBytes(publicKey.trim())), ASYMMETRIC_ALGORITHM, false, ["verify"]);
94
+ } catch {
95
+ return;
96
+ }
97
+ };
98
+ const signedBytes = (id, timestamp, body) => {
99
+ const prefix = encoder.encode(`${id}.${timestamp}.`);
100
+ const material = new Uint8Array(prefix.byteLength + body.byteLength);
101
+ material.set(prefix);
102
+ material.set(body, prefix.byteLength);
103
+ return material;
104
+ };
105
+ const hmac = async (secret, id, timestamp, body) => {
106
+ const key = await importHmacKey(secret);
107
+ try {
108
+ return new Uint8Array(await crypto.subtle.sign("HMAC", key, toCryptoBytes(signedBytes(id, timestamp, body))));
109
+ } catch (cause) {
110
+ throw new WebhookCryptoError({ detail: `Unable to sign webhook: ${cause instanceof Error ? cause.message : String(cause)}` });
111
+ }
112
+ };
113
+ const asymmetricMatch = async (key, candidate, content) => {
114
+ try {
115
+ return await crypto.subtle.verify(ASYMMETRIC_ALGORITHM, key, toCryptoBytes(candidate), content);
116
+ } catch {
117
+ return false;
118
+ }
119
+ };
120
+ const timingSafeEqual = (left, right) => {
121
+ if (left.byteLength !== right.byteLength) return false;
122
+ let difference = 0;
123
+ for (let index = 0; index < left.byteLength; index += 1) difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
124
+ return difference === 0;
125
+ };
126
+ const signatureCandidates = (header, expected) => header.split(" ").map((candidate) => candidate.split(",", 2)).flatMap(([version, encoded]) => {
127
+ if (version !== expected || encoded === void 0) return [];
128
+ try {
129
+ return [base64ToBytes(encoded)];
130
+ } catch {
131
+ return [];
132
+ }
133
+ });
134
+ const parseEvent = (body) => {
135
+ try {
136
+ const parsed = JSON.parse(new TextDecoder("utf-8", {
137
+ fatal: true,
138
+ ignoreBOM: false
139
+ }).decode(body));
140
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new MalformedWebhookPayloadError({ detail: "Webhook event must be an object" });
141
+ const event = parsed;
142
+ const keys = Object.keys(event);
143
+ if (keys.length !== 3 || !keys.every((key) => key === "type" || key === "timestamp" || key === "data") || typeof event.type !== "string" || typeof event.timestamp !== "string" || !("data" in event)) throw new MalformedWebhookPayloadError({ detail: "Webhook event has an invalid envelope" });
144
+ return {
145
+ type: event.type,
146
+ timestamp: event.timestamp,
147
+ data: event.data
148
+ };
149
+ } catch {
150
+ throw new MalformedWebhookPayloadError({ detail: "Webhook body must be valid UTF-8 JSON containing type, timestamp, and data" });
151
+ }
152
+ };
153
+ const generateWebhookSecret = () => {
154
+ const bytes = crypto.getRandomValues(new Uint8Array(GENERATED_SECRET_BYTES));
155
+ return `${SECRET_PREFIX}${bytesToBase64(bytes)}`;
156
+ };
157
+ const webhookSignedContent = (input) => signedBytes(input.id, input.timestamp, toBytes(input.body));
158
+ const encodeWebhookSignature = (version, signature) => `${version},${bytesToBase64(signature)}`;
159
+ const signWebhook = async (input) => {
160
+ const timestamp = String(input.timestamp);
161
+ const signature = await hmac(input.secret, input.id, timestamp, toBytes(input.body));
162
+ return encodeWebhookSignature("v1", signature);
163
+ };
164
+ 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" });
167
+ const timestampSeconds = Number(timestamp);
168
+ if (!Number.isInteger(timestampSeconds)) throw new WebhookTimestampError({ detail: "webhook-timestamp must be Unix seconds" });
169
+ const toleranceSeconds = input.toleranceSeconds ?? 300;
170
+ if (!Number.isFinite(toleranceSeconds) || toleranceSeconds < 0) throw new WebhookTimestampError({ detail: "Webhook tolerance must be non-negative" });
171
+ if (Math.abs(input.nowMilliseconds / 1e3 - timestampSeconds) > toleranceSeconds) throw new WebhookTimestampError({ detail: "Webhook timestamp is outside the replay tolerance" });
172
+ return {
173
+ id,
174
+ timestamp,
175
+ signature
176
+ };
177
+ };
178
+ const verifyWebhookAt = async (input) => {
179
+ const { id, timestamp, signature } = freshHeaders(input);
180
+ const body = toBytes(input.body);
181
+ const expected = await hmac(input.secret, id, timestamp, body);
182
+ if (!signatureCandidates(signature, "v1").some((candidate) => timingSafeEqual(expected, candidate))) throw new WebhookSignatureError({ detail: "Webhook signature verification failed" });
183
+ return {
184
+ id,
185
+ event: parseEvent(body)
186
+ };
187
+ };
188
+ const verifyWebhookAtWithPublicKeys = async (input) => {
189
+ const { id, timestamp, signature } = freshHeaders(input);
190
+ if (input.publicKeys.length === 0) throw new WebhookSignatureError({ detail: "No webhook verification key is available" });
191
+ const keys = (await Promise.all(input.publicKeys.map(importVerificationKey))).filter((key) => key !== void 0);
192
+ if (keys.length === 0) throw new WebhookCryptoError({ detail: "Unable to import any webhook verification key: keys must be Base64 SPKI DER" });
193
+ const body = toBytes(input.body);
194
+ const content = toCryptoBytes(signedBytes(id, timestamp, body));
195
+ const candidates = signatureCandidates(signature, "v2");
196
+ if (!(await Promise.all(keys.flatMap((key) => candidates.map((candidate) => asymmetricMatch(key, candidate, content))))).includes(true)) throw new WebhookSignatureError({ detail: "Webhook signature verification failed" });
197
+ return {
198
+ id,
199
+ event: parseEvent(body)
200
+ };
201
+ };
202
+ const readWebhookHeaders = (headers) => ({
203
+ id: headers.get("webhook-id") ?? "",
204
+ timestamp: headers.get("webhook-timestamp") ?? "",
205
+ signature: headers.get("webhook-signature") ?? ""
206
+ });
207
+ //#endregion
208
+ 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
+
210
+ //# sourceMappingURL=protocol-M4mOvMtW.js.map
@@ -0,0 +1,46 @@
1
+ export declare const WEBHOOK_ID_HEADER = "webhook-id";
2
+ export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
3
+ export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
4
+ export declare const DEFAULT_TOLERANCE_SECONDS = 300;
5
+ export interface SamvaWebhookEvent {
6
+ readonly type: string;
7
+ readonly timestamp: string;
8
+ readonly data: unknown;
9
+ }
10
+ export interface VerifiedWebhook {
11
+ readonly id: string;
12
+ readonly event: SamvaWebhookEvent;
13
+ }
14
+ export interface WebhookHeaders {
15
+ readonly id: string;
16
+ readonly timestamp: string;
17
+ readonly signature: string;
18
+ }
19
+ export interface VerifyWebhookInput {
20
+ readonly body: string | Uint8Array;
21
+ readonly headers: WebhookHeaders;
22
+ readonly secret: string;
23
+ readonly toleranceSeconds?: number | undefined;
24
+ readonly nowMilliseconds: number;
25
+ }
26
+ export type WebhookSignatureVersion = "v1" | "v2";
27
+ export declare const generateWebhookSecret: () => string;
28
+ export declare const webhookSignedContent: (input: {
29
+ readonly id: string;
30
+ readonly timestamp: string;
31
+ readonly body: string | Uint8Array;
32
+ }) => Uint8Array;
33
+ export declare const encodeWebhookSignature: (version: WebhookSignatureVersion, signature: Uint8Array) => string;
34
+ export declare const signWebhook: (input: {
35
+ readonly body: string | Uint8Array;
36
+ readonly id: string;
37
+ readonly timestamp: string | number;
38
+ readonly secret: string;
39
+ }) => Promise<string>;
40
+ export declare const verifyWebhookAt: (input: VerifyWebhookInput) => Promise<VerifiedWebhook>;
41
+ export declare const verifyWebhookAtWithPublicKeys: (input: Omit<VerifyWebhookInput, "secret"> & {
42
+ readonly publicKeys: ReadonlyArray<string>;
43
+ }) => Promise<VerifiedWebhook>;
44
+ export declare const readWebhookHeaders: (headers: Headers) => WebhookHeaders;
45
+ export { InvalidWebhookHeadersError, InvalidWebhookSecretError, MalformedWebhookPayloadError, WebhookCryptoError, WebhookSignatureError, WebhookTimestampError, WebhookVerificationError, } from "./errors.js";
46
+ export type { AnyWebhookVerificationError } from "./errors.js";
@@ -0,0 +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";
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 ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@samva/webhooks",
3
+ "version": "0.1.1",
4
+ "description": "Runtime-neutral verification for Samva webhook deliveries",
5
+ "keywords": [
6
+ "samva",
7
+ "standard-webhooks",
8
+ "typescript",
9
+ "webcrypto",
10
+ "webhooks"
11
+ ],
12
+ "homepage": "https://samva.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/AryaLabsHQ/samva/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Arya Labs, Inc.",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/AryaLabsHQ/samva.git",
21
+ "directory": "packages/webhooks"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "!dist/**/*.map",
26
+ "package.json",
27
+ "README.md",
28
+ "AGENTS.md",
29
+ "LICENSE"
30
+ ],
31
+ "type": "module",
32
+ "sideEffects": false,
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/promise.d.ts",
36
+ "import": "./dist/promise.js"
37
+ },
38
+ "./promise": {
39
+ "types": "./dist/promise.d.ts",
40
+ "import": "./dist/promise.js"
41
+ },
42
+ "./effect": {
43
+ "types": "./dist/effect.d.ts",
44
+ "import": "./dist/effect.js"
45
+ },
46
+ "./protocol": {
47
+ "types": "./dist/protocol.d.ts",
48
+ "import": "./dist/protocol.js"
49
+ }
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "scripts": {
55
+ "build": "tsdown && tsc --project tsconfig.build.json",
56
+ "prepack": "bun run build",
57
+ "test": "vitest run --root ../.. --project packages packages/webhooks/tests",
58
+ "typecheck": "tsgo --noEmit"
59
+ },
60
+ "devDependencies": {
61
+ "@effect/vitest": "4.0.0-beta.102",
62
+ "effect": "4.0.0-beta.102",
63
+ "tsdown": "^0.22.13",
64
+ "typescript": "^6.0.3",
65
+ "vitest": "^4.1.10"
66
+ },
67
+ "peerDependencies": {
68
+ "effect": "4.0.0-beta.102"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "effect": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "engines": {
76
+ "node": ">=22"
77
+ }
78
+ }