sadasend 0.0.0-stage → 0.1.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.
@@ -0,0 +1,42 @@
1
+ import { type SadaSendError } from './errors.js';
2
+ /**
3
+ * When to try again, and how long to wait.
4
+ *
5
+ * Two rules, and the second is the one nobody implements:
6
+ *
7
+ * 1. **Exponential backoff with full jitter.** Fixed backoff synchronises every
8
+ * client that was refused in the same second, so they all return together and
9
+ * refuse together — the thundering herd that turns a brief limit into a long
10
+ * one. Full jitter (a uniform draw from `[0, backoff]`) spreads them.
11
+ *
12
+ * 2. **`Retry-After` wins, always.** The API already sends it — it is the
13
+ * server saying exactly when the window rolls over — and until this SDK
14
+ * existed nothing read it. A client that guesses shorter makes the limit
15
+ * worse; a client that guesses longer makes the customer wait for nothing.
16
+ * The header is not advice.
17
+ */
18
+ export type RetryPolicy = {
19
+ /** Attempts *after* the first. 0 disables retrying entirely. */
20
+ maxRetries: number;
21
+ /** First backoff, doubling each attempt. */
22
+ baseDelayMs: number;
23
+ /** Ceiling on the computed backoff. `Retry-After` is not capped by this. */
24
+ maxDelayMs: number;
25
+ /**
26
+ * How long a `Retry-After` may ask us to sleep before the client gives up
27
+ * and returns the error instead. An account throttled for reputation can be
28
+ * told to come back in an hour, and blocking a request handler for an hour is
29
+ * worse than returning an error the caller can schedule around.
30
+ */
31
+ maxRetryAfterMs: number;
32
+ };
33
+ export declare const DEFAULT_RETRY: RetryPolicy;
34
+ /**
35
+ * The wait before attempt `n` (1-based), or `null` for "stop, return the error".
36
+ *
37
+ * Split out and pure so the policy can be tested without sleeping — a retry
38
+ * test that actually waits is a test nobody runs often enough to trust.
39
+ */
40
+ export declare function delayFor(attempt: number, error: SadaSendError, policy: RetryPolicy, random?: () => number): number | null;
41
+ export declare const sleep: (ms: number) => Promise<void>;
42
+ //# sourceMappingURL=retry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,WAK3B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,WAAW,EACnB,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,GAAG,IAAI,CAcf;AAED,eAAO,MAAM,KAAK,GAAI,IAAI,MAAM,kBAAgD,CAAC"}
package/dist/retry.js ADDED
@@ -0,0 +1,30 @@
1
+ import { RateLimitError } from './errors.js';
2
+ export const DEFAULT_RETRY = {
3
+ maxRetries: 3,
4
+ baseDelayMs: 500,
5
+ maxDelayMs: 8_000,
6
+ maxRetryAfterMs: 60_000,
7
+ };
8
+ /**
9
+ * The wait before attempt `n` (1-based), or `null` for "stop, return the error".
10
+ *
11
+ * Split out and pure so the policy can be tested without sleeping — a retry
12
+ * test that actually waits is a test nobody runs often enough to trust.
13
+ */
14
+ export function delayFor(attempt, error, policy, random = Math.random) {
15
+ if (attempt > policy.maxRetries)
16
+ return null;
17
+ if (error instanceof RateLimitError && error.retryAfterSeconds !== undefined) {
18
+ const asked = error.retryAfterSeconds * 1000;
19
+ // Longer than we are willing to block: surface the error with the header
20
+ // intact so the caller can decide, rather than silently sleeping on it.
21
+ if (asked > policy.maxRetryAfterMs)
22
+ return null;
23
+ // Not jittered, and not backed off. The server named a time.
24
+ return asked;
25
+ }
26
+ const backoff = Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
27
+ return Math.floor(random() * backoff);
28
+ }
29
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
30
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAsB,MAAM,aAAa,CAAC;AAmCjE,MAAM,CAAC,MAAM,aAAa,GAAgB;IACxC,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,GAAG;IAChB,UAAU,EAAE,KAAK;IACjB,eAAe,EAAE,MAAM;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CACtB,OAAe,EACf,KAAoB,EACpB,MAAmB,EACnB,SAAuB,IAAI,CAAC,MAAM;IAElC,IAAI,OAAO,GAAG,MAAM,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IAE7C,IAAI,KAAK,YAAY,cAAc,IAAI,KAAK,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;QAC7E,MAAM,KAAK,GAAG,KAAK,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC7C,yEAAyE;QACzE,wEAAwE;QACxE,IAAI,KAAK,GAAG,MAAM,CAAC,eAAe;YAAE,OAAO,IAAI,CAAC;QAChD,6DAA6D;QAC7D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACrF,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The webhook signature verifier.
3
+ *
4
+ * This is the one piece of our security every customer has to reimplement, and
5
+ * the one most likely to be got subtly wrong — the usual mistakes are comparing
6
+ * with `===`, forgetting the timestamp is inside the signed material, and
7
+ * verifying the parsed object instead of the raw body. Shipping it means nobody
8
+ * has to make them.
9
+ *
10
+ * **WebCrypto, not `node:crypto`.** Webhook handlers live on Vercel Edge and
11
+ * Cloudflare Workers as often as they live on a Node server, and an
12
+ * unimportable verifier is a verifier those customers write themselves. The
13
+ * cost is that this is async; the handler already is.
14
+ */
15
+ /** Matches the server: `t=<unix-seconds>,v1=<hex>`. */
16
+ export declare const SIGNATURE_HEADER = "sadasend-signature";
17
+ /**
18
+ * Five minutes, matching the server. Without a timestamp in the signed material
19
+ * a captured request is a permanent credential; with one, it expires.
20
+ */
21
+ export declare const DEFAULT_TOLERANCE_MS: number;
22
+ export type VerifyOptions = {
23
+ /** For tests, and for a handler that has its own clock. */
24
+ atMs?: number;
25
+ toleranceMs?: number;
26
+ };
27
+ /**
28
+ * Verify a webhook.
29
+ *
30
+ * `body` must be the **raw request body as a string**, byte for byte as it
31
+ * arrived. `JSON.stringify(await req.json())` is not the same string — key
32
+ * order and whitespace survive the wire and do not survive a round trip — and
33
+ * every signature will fail in a way that looks like a wrong secret.
34
+ *
35
+ * ```ts
36
+ * const raw = await req.text();
37
+ * if (!await verifyWebhook(process.env.SADASEND_WEBHOOK_SECRET!, raw, req.headers.get('sadasend-signature'))) {
38
+ * return new Response('bad signature', { status: 400 });
39
+ * }
40
+ * const event = JSON.parse(raw);
41
+ * ```
42
+ */
43
+ export declare function verifyWebhook(secret: string, body: string, header: string | null | undefined, opts?: VerifyOptions): Promise<boolean>;
44
+ /**
45
+ * The events an endpoint can subscribe to — all of them, and only them.
46
+ *
47
+ * `email.opened` and `email.clicked` were listed here and are not subscribable:
48
+ * open and click tracking is not shipped, and `POST /webhooks` refuses both by
49
+ * name. A type that offers an event the API rejects sends the customer to write
50
+ * a handler for something they cannot receive.
51
+ */
52
+ export type WebhookEventType = 'email.sent' | 'email.delivered' | 'email.bounced' | 'email.complained' | 'email.refused';
53
+ export type WebhookEvent = {
54
+ id: string;
55
+ type: WebhookEventType;
56
+ createdAt: string;
57
+ data: {
58
+ messageId: string;
59
+ accountId: string;
60
+ to?: string[];
61
+ detail?: string;
62
+ };
63
+ };
64
+ /**
65
+ * Verify and parse in one call, returning the event or `null`.
66
+ *
67
+ * The shape most handlers want, and it makes the dangerous mistake harder: the
68
+ * raw body goes in, and there is no version of this that takes an object.
69
+ */
70
+ export declare function parseWebhook(secret: string, body: string, header: string | null | undefined, opts?: VerifyOptions): Promise<WebhookEvent | null>;
71
+ //# sourceMappingURL=webhooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,uDAAuD;AACvD,eAAO,MAAM,gBAAgB,uBAAuB,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAAa,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG;IAC1B,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA0BF;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,GACxB,YAAY,GAAG,iBAAiB,GAAG,eAAe,GAClD,kBAAkB,GAAG,eAAe,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,gBAAgB,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACjC,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAO9B"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The webhook signature verifier.
3
+ *
4
+ * This is the one piece of our security every customer has to reimplement, and
5
+ * the one most likely to be got subtly wrong — the usual mistakes are comparing
6
+ * with `===`, forgetting the timestamp is inside the signed material, and
7
+ * verifying the parsed object instead of the raw body. Shipping it means nobody
8
+ * has to make them.
9
+ *
10
+ * **WebCrypto, not `node:crypto`.** Webhook handlers live on Vercel Edge and
11
+ * Cloudflare Workers as often as they live on a Node server, and an
12
+ * unimportable verifier is a verifier those customers write themselves. The
13
+ * cost is that this is async; the handler already is.
14
+ */
15
+ const encoder = new TextEncoder();
16
+ /** Matches the server: `t=<unix-seconds>,v1=<hex>`. */
17
+ export const SIGNATURE_HEADER = 'sadasend-signature';
18
+ /**
19
+ * Five minutes, matching the server. Without a timestamp in the signed material
20
+ * a captured request is a permanent credential; with one, it expires.
21
+ */
22
+ export const DEFAULT_TOLERANCE_MS = 5 * 60_000;
23
+ async function hmacHex(secret, data) {
24
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
25
+ const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
26
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, '0')).join('');
27
+ }
28
+ /**
29
+ * Constant-time over the *content*.
30
+ *
31
+ * `===` on strings exits at the first differing byte, and how long that takes
32
+ * is enough to build a valid signature a byte at a time. The length is compared
33
+ * first and separately: both sides here are fixed-length SHA-256 hex, so a
34
+ * length mismatch is a malformed header rather than a guess in progress, and
35
+ * leaking that fact costs nothing.
36
+ */
37
+ function timingSafeEqualHex(a, b) {
38
+ if (a.length !== b.length)
39
+ return false;
40
+ let diff = 0;
41
+ for (let i = 0; i < a.length; i++)
42
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
43
+ return diff === 0;
44
+ }
45
+ /**
46
+ * Verify a webhook.
47
+ *
48
+ * `body` must be the **raw request body as a string**, byte for byte as it
49
+ * arrived. `JSON.stringify(await req.json())` is not the same string — key
50
+ * order and whitespace survive the wire and do not survive a round trip — and
51
+ * every signature will fail in a way that looks like a wrong secret.
52
+ *
53
+ * ```ts
54
+ * const raw = await req.text();
55
+ * if (!await verifyWebhook(process.env.SADASEND_WEBHOOK_SECRET!, raw, req.headers.get('sadasend-signature'))) {
56
+ * return new Response('bad signature', { status: 400 });
57
+ * }
58
+ * const event = JSON.parse(raw);
59
+ * ```
60
+ */
61
+ export async function verifyWebhook(secret, body, header, opts = {}) {
62
+ if (!header)
63
+ return false;
64
+ const { atMs = Date.now(), toleranceMs = DEFAULT_TOLERANCE_MS } = opts;
65
+ const parts = new Map();
66
+ for (const part of header.split(',')) {
67
+ const i = part.indexOf('=');
68
+ if (i > 0)
69
+ parts.set(part.slice(0, i).trim(), part.slice(i + 1).trim());
70
+ }
71
+ const t = Number(parts.get('t'));
72
+ const given = parts.get('v1');
73
+ if (!Number.isFinite(t) || !given)
74
+ return false;
75
+ // Checked before the HMAC, so an expired signature costs no crypto at all.
76
+ if (Math.abs(atMs - t * 1000) > toleranceMs)
77
+ return false;
78
+ return timingSafeEqualHex(await hmacHex(secret, `${t}.${body}`), given);
79
+ }
80
+ /**
81
+ * Verify and parse in one call, returning the event or `null`.
82
+ *
83
+ * The shape most handlers want, and it makes the dangerous mistake harder: the
84
+ * raw body goes in, and there is no version of this that takes an object.
85
+ */
86
+ export async function parseWebhook(secret, body, header, opts = {}) {
87
+ if (!await verifyWebhook(secret, body, header, opts))
88
+ return null;
89
+ try {
90
+ return JSON.parse(body);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ //# sourceMappingURL=webhooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "sadasend",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Official TypeScript/JavaScript SDK for SadaSend. Transactional email with unbypassable server-enforced guardrails for human engineers and autonomous AI agents (Claude, Cursor, MCP). Zero dependencies, native fetch & WebCrypto, automatic idempotency, and constant-time webhook verification.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./webhooks": {
14
+ "types": "./dist/webhooks.d.ts",
15
+ "default": "./dist/webhooks.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "LICENSE",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "bun test",
27
+ "test:e2e": "bun test test/e2e.test.ts",
28
+ "prepublishOnly": "bun run build"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/SadaSend/sadasend-sdk-ts.git"
33
+ },
34
+ "homepage": "https://sadasend.com",
35
+ "bugs": {
36
+ "url": "https://github.com/SadaSend/sadasend-sdk-ts/issues"
37
+ },
38
+ "author": "SadaSend <support@sadasend.com>",
39
+ "keywords": [
40
+ "sadasend",
41
+ "email",
42
+ "transactional-email",
43
+ "email-api",
44
+ "ai-agents",
45
+ "mcp",
46
+ "model-context-protocol",
47
+ "claude",
48
+ "cursor",
49
+ "langchain",
50
+ "crewai",
51
+ "agentic-ai",
52
+ "smtp",
53
+ "guardrails",
54
+ "idempotency",
55
+ "zero-dependencies",
56
+ "resend",
57
+ "sendgrid",
58
+ "postmark",
59
+ "webhooks",
60
+ "typescript"
61
+ ],
62
+ "devDependencies": {
63
+ "@types/bun": "^1.2.0",
64
+ "typescript": "^5.7.0"
65
+ },
66
+ "engines": {
67
+ "node": ">=18"
68
+ },
69
+ "sideEffects": false,
70
+ "license": "MIT",
71
+ "funding": {
72
+ "type": "individual",
73
+ "url": "https://sadasend.com/pricing"
74
+ }
75
+ }