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.
- package/LICENSE +21 -0
- package/README.md +527 -2
- package/dist/client.d.ts +452 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +266 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +123 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +176 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/retry.d.ts +42 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +30 -0
- package/dist/retry.js.map +1 -0
- package/dist/webhooks.d.ts +71 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +96 -0
- package/dist/webhooks.js.map +1 -0
- package/package.json +73 -4
package/dist/retry.d.ts
ADDED
|
@@ -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"}
|
package/dist/webhooks.js
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|