@sendcraft/sdk 1.0.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,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Emails = void 0;
4
+ /**
5
+ * Render a React Email component to an HTML string.
6
+ * Throws if `@react-email/render` is not installed.
7
+ */
8
+ async function renderReact(element) {
9
+ let render;
10
+ try {
11
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
12
+ const mod = require('@react-email/render');
13
+ render = mod.render ?? mod.default?.render;
14
+ if (typeof render !== 'function') {
15
+ throw new Error('@react-email/render does not export a `render` function — check the installed version');
16
+ }
17
+ }
18
+ catch (err) {
19
+ // Re-throw a helpful install message, but chain the root cause so
20
+ // callers can inspect it (e.g. module-not-found vs wrong-version).
21
+ const cause = err instanceof Error ? err.message : String(err);
22
+ throw new Error('[sendcraft-sdk] The `react` option requires @react-email/render.\n' +
23
+ 'Install it: npm install @react-email/render react react-dom\n' +
24
+ `Root cause: ${cause}`);
25
+ }
26
+ return render(element);
27
+ }
28
+ class Emails {
29
+ constructor(http) {
30
+ this.http = http;
31
+ }
32
+ /**
33
+ * Send a single transactional email.
34
+ * @example — plain HTML
35
+ * await client.emails.send({ to: 'user@example.com', subject: 'Hi', html: '<p>Hello</p>' })
36
+ *
37
+ * @example — React Email component (requires @react-email/render)
38
+ * await client.emails.send({ to: 'user@example.com', subject: 'Hi', react: <WelcomeEmail /> })
39
+ */
40
+ async send(options) {
41
+ const html = options.react ? await renderReact(options.react) : options.html;
42
+ if (!html)
43
+ throw new Error('[sendcraft-sdk] Provide either `html` or `react` in send()');
44
+ return this.http.post('/emails/send', {
45
+ toEmail: options.to,
46
+ toName: options.toName,
47
+ subject: options.subject,
48
+ htmlContent: html,
49
+ plainTextContent: options.text,
50
+ fromEmail: options.from,
51
+ fromName: options.fromName,
52
+ replyTo: options.replyTo,
53
+ cc: options.cc,
54
+ bcc: options.bcc,
55
+ }, options.idempotencyKey ? { 'X-Idempotency-Key': options.idempotencyKey } : undefined);
56
+ }
57
+ /**
58
+ * Render a React Email component (or return a plain HTML string) without sending.
59
+ * Useful for previewing templates locally or in tests.
60
+ * @example
61
+ * const html = await client.emails.render({ react: <WelcomeEmail name="Alice" /> });
62
+ * console.log(html); // full HTML string
63
+ */
64
+ async render(options) {
65
+ if (options.react)
66
+ return renderReact(options.react);
67
+ if (options.html)
68
+ return options.html;
69
+ throw new Error('[sendcraft-sdk] Provide either `html` or `react` in render()');
70
+ }
71
+ /**
72
+ * Send the same email to multiple recipients.
73
+ * @example
74
+ * await client.emails.sendBulk({ emails: ['a@b.com', 'c@d.com'], subject: 'News', html: '...' })
75
+ */
76
+ async sendBulk(options) {
77
+ const html = options.react ? await renderReact(options.react) : options.html;
78
+ if (!html)
79
+ throw new Error('[sendcraft-sdk] Provide either `html` or `react` in sendBulk()');
80
+ return this.http.post('/emails/send-bulk', {
81
+ emails: options.emails,
82
+ subject: options.subject,
83
+ htmlContent: html,
84
+ plainTextContent: options.text,
85
+ fromEmail: options.from,
86
+ fromName: options.fromName,
87
+ });
88
+ }
89
+ /**
90
+ * Schedule an email for future delivery.
91
+ * @example
92
+ * await client.emails.schedule({ to: '...', subject: '...', html: '...', scheduledAt: '2026-04-01T09:00:00Z' })
93
+ */
94
+ async schedule(options) {
95
+ const html = options.react ? await renderReact(options.react) : options.html;
96
+ if (!html)
97
+ throw new Error('[sendcraft-sdk] Provide either `html` or `react` in schedule()');
98
+ return this.http.post('/emails/schedule', {
99
+ toEmail: options.to,
100
+ subject: options.subject,
101
+ htmlContent: html,
102
+ fromEmail: options.from,
103
+ scheduledTime: options.scheduledAt instanceof Date
104
+ ? options.scheduledAt.toISOString()
105
+ : options.scheduledAt,
106
+ });
107
+ }
108
+ /**
109
+ * Send up to 100 distinct emails in a single API call.
110
+ * Each item can have a different to, subject, and html.
111
+ * @example
112
+ * await client.emails.batch([
113
+ * { to: 'a@example.com', subject: 'Hello A', html: '<p>Hi A</p>' },
114
+ * { to: 'b@example.com', subject: 'Hello B', html: '<p>Hi B</p>' },
115
+ * ])
116
+ */
117
+ async batch(emails, idempotencyKey) {
118
+ const rendered = await Promise.all(emails.map(async (e) => {
119
+ const html = e.react ? await renderReact(e.react) : e.html;
120
+ if (!html)
121
+ throw new Error('[sendcraft-sdk] Each email in batch() must have `html` or `react`');
122
+ return {
123
+ toEmail: e.to,
124
+ toName: e.toName,
125
+ subject: e.subject,
126
+ htmlContent: html,
127
+ plainTextContent: e.text,
128
+ fromEmail: e.from,
129
+ fromName: e.fromName,
130
+ replyTo: e.replyTo,
131
+ };
132
+ }));
133
+ return this.http.post('/emails/batch', { emails: rendered }, idempotencyKey ? { 'X-Idempotency-Key': idempotencyKey } : undefined);
134
+ }
135
+ /** Get a single email by ID. */
136
+ get(id) {
137
+ return this.http.get(`/emails/${id}`);
138
+ }
139
+ /** Update the scheduled delivery time of a scheduled email. */
140
+ updateSchedule(id, scheduledAt) {
141
+ return this.http.patch(`/emails/${id}/schedule`, {
142
+ scheduledTime: scheduledAt instanceof Date ? scheduledAt.toISOString() : scheduledAt,
143
+ });
144
+ }
145
+ /** Cancel a scheduled email before it is sent. */
146
+ cancelSchedule(id) {
147
+ return this.http.delete(`/emails/${id}/schedule`);
148
+ }
149
+ /** List sent emails with optional filters. */
150
+ list(options = {}) {
151
+ return this.http.get('/emails', options);
152
+ }
153
+ /** Get account-level email stats (open rate, click rate, etc.). */
154
+ stats() {
155
+ return this.http.get('/emails/stats/summary');
156
+ }
157
+ }
158
+ exports.Emails = Emails;
@@ -0,0 +1,55 @@
1
+ import { HttpClient } from '../client';
2
+ export interface SegmentRule {
3
+ field: string;
4
+ operator: string;
5
+ value: unknown;
6
+ }
7
+ export interface Segment {
8
+ _id: string;
9
+ name: string;
10
+ description?: string;
11
+ rules: SegmentRule[];
12
+ matchType: 'all' | 'any';
13
+ subscriberCount?: number;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+ export interface CreateSegmentParams {
18
+ name: string;
19
+ description?: string;
20
+ rules: SegmentRule[];
21
+ matchType?: 'all' | 'any';
22
+ }
23
+ export declare class Segments {
24
+ private http;
25
+ constructor(http: HttpClient);
26
+ /** List all segments */
27
+ list(): Promise<{
28
+ success: boolean;
29
+ segments: Segment[];
30
+ }>;
31
+ /** Get a segment by ID */
32
+ get(id: string): Promise<{
33
+ success: boolean;
34
+ segment: Segment;
35
+ }>;
36
+ /** Create a new segment */
37
+ create(params: CreateSegmentParams): Promise<{
38
+ success: boolean;
39
+ segment: Segment;
40
+ }>;
41
+ /** Update a segment */
42
+ update(id: string, params: Partial<CreateSegmentParams>): Promise<{
43
+ success: boolean;
44
+ segment: Segment;
45
+ }>;
46
+ /** Delete a segment */
47
+ delete(id: string): Promise<{
48
+ success: boolean;
49
+ }>;
50
+ /** Preview subscriber count matching a segment's rules */
51
+ preview(id: string): Promise<{
52
+ success: boolean;
53
+ count: number;
54
+ }>;
55
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Segments = void 0;
4
+ class Segments {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /** List all segments */
9
+ list() {
10
+ return this.http.get('/segments');
11
+ }
12
+ /** Get a segment by ID */
13
+ get(id) {
14
+ return this.http.get(`/segments/${id}`);
15
+ }
16
+ /** Create a new segment */
17
+ create(params) {
18
+ return this.http.post('/segments', params);
19
+ }
20
+ /** Update a segment */
21
+ update(id, params) {
22
+ return this.http.put(`/segments/${id}`, params);
23
+ }
24
+ /** Delete a segment */
25
+ delete(id) {
26
+ return this.http.delete(`/segments/${id}`);
27
+ }
28
+ /** Preview subscriber count matching a segment's rules */
29
+ preview(id) {
30
+ return this.http.get(`/segments/${id}/preview`);
31
+ }
32
+ }
33
+ exports.Segments = Segments;
@@ -0,0 +1,44 @@
1
+ import { HttpClient } from '../client';
2
+ export interface SmtpCredentials {
3
+ host: string;
4
+ port: number;
5
+ encryption: string;
6
+ username: string;
7
+ passwordHint: string;
8
+ instructions: {
9
+ note: string;
10
+ apiKeyLocation: string;
11
+ };
12
+ }
13
+ export interface SmtpWarmupStatus {
14
+ warmupDay: number;
15
+ dailyLimit: number | null;
16
+ todayCount: number;
17
+ remainingToday: number | null;
18
+ isWarmedUp: boolean;
19
+ percentComplete: number;
20
+ warmupStartDate: string;
21
+ totalSent: number;
22
+ }
23
+ export declare class Smtp {
24
+ private http;
25
+ constructor(http: HttpClient);
26
+ /**
27
+ * Get SMTP relay credentials for your account.
28
+ * Use the returned host/port/username + your API key as the SMTP password
29
+ * in any nodemailer, smtplib, or PHPMailer integration.
30
+ */
31
+ credentials(): Promise<{
32
+ success: boolean;
33
+ smtp: SmtpCredentials;
34
+ examples: Record<string, string>;
35
+ }>;
36
+ /**
37
+ * Get current IP warmup status for the self-hosted SMTP server.
38
+ * Shows daily limit, emails sent today, and warmup progress.
39
+ */
40
+ warmupStatus(): Promise<{
41
+ success: boolean;
42
+ warmup: SmtpWarmupStatus;
43
+ }>;
44
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Smtp = void 0;
4
+ class Smtp {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * Get SMTP relay credentials for your account.
10
+ * Use the returned host/port/username + your API key as the SMTP password
11
+ * in any nodemailer, smtplib, or PHPMailer integration.
12
+ */
13
+ credentials() {
14
+ return this.http.get('/smtp/credentials');
15
+ }
16
+ /**
17
+ * Get current IP warmup status for the self-hosted SMTP server.
18
+ * Shows daily limit, emails sent today, and warmup progress.
19
+ */
20
+ warmupStatus() {
21
+ return this.http.get('/smtp/warmup');
22
+ }
23
+ }
24
+ exports.Smtp = Smtp;
@@ -0,0 +1,61 @@
1
+ import { HttpClient } from '../client';
2
+ export interface AddSubscriberOptions {
3
+ email: string;
4
+ listId: string;
5
+ firstName?: string;
6
+ lastName?: string;
7
+ customFields?: Record<string, unknown>;
8
+ }
9
+ export interface ImportSubscribersOptions {
10
+ listId: string;
11
+ subscribers: Array<{
12
+ email: string;
13
+ firstName?: string;
14
+ lastName?: string;
15
+ phone?: string;
16
+ customFields?: Record<string, unknown>;
17
+ }>;
18
+ }
19
+ export interface ListSubscribersOptions {
20
+ page?: number;
21
+ limit?: number;
22
+ status?: 'active' | 'unsubscribed' | 'bounced';
23
+ }
24
+ export interface UpdateSubscriberOptions {
25
+ firstName?: string;
26
+ lastName?: string;
27
+ customFields?: Record<string, unknown>;
28
+ emailPreferences?: Record<string, unknown>;
29
+ }
30
+ export declare class Subscribers {
31
+ private http;
32
+ constructor(http: HttpClient);
33
+ /**
34
+ * Add a single subscriber to a list.
35
+ * @example
36
+ * await client.subscribers.add({ email: 'user@example.com', listId: 'list_id' })
37
+ */
38
+ add(options: AddSubscriberOptions): Promise<unknown>;
39
+ /**
40
+ * Bulk import up to 10,000 subscribers into a list.
41
+ * @example
42
+ * await client.subscribers.import({ listId: '...', subscribers: [{ email: 'a@b.com' }, { email: 'c@d.com' }] })
43
+ */
44
+ import(options: ImportSubscribersOptions): Promise<unknown>;
45
+ /** List all subscribers across all your lists. */
46
+ list(options?: ListSubscribersOptions): Promise<unknown>;
47
+ /** Get subscribers for a specific list. */
48
+ listByList(listId: string, options?: {
49
+ status?: string;
50
+ limit?: number;
51
+ skip?: number;
52
+ }): Promise<unknown>;
53
+ /** Update subscriber details. */
54
+ update(subscriberId: string, options: UpdateSubscriberOptions): Promise<unknown>;
55
+ /** Soft-delete (archive) a subscriber. */
56
+ delete(subscriberId: string): Promise<unknown>;
57
+ /** Get a subscriber's email preferences. */
58
+ preferences(subscriberId: string): Promise<unknown>;
59
+ /** Update a subscriber's email preferences. */
60
+ updatePreferences(subscriberId: string, emailPreferences: Record<string, unknown>): Promise<unknown>;
61
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Subscribers = void 0;
4
+ class Subscribers {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * Add a single subscriber to a list.
10
+ * @example
11
+ * await client.subscribers.add({ email: 'user@example.com', listId: 'list_id' })
12
+ */
13
+ add(options) {
14
+ return this.http.post('/subscribers/add', {
15
+ email: options.email,
16
+ listId: options.listId,
17
+ firstName: options.firstName,
18
+ lastName: options.lastName,
19
+ customFields: options.customFields,
20
+ });
21
+ }
22
+ /**
23
+ * Bulk import up to 10,000 subscribers into a list.
24
+ * @example
25
+ * await client.subscribers.import({ listId: '...', subscribers: [{ email: 'a@b.com' }, { email: 'c@d.com' }] })
26
+ */
27
+ import(options) {
28
+ return this.http.post('/subscribers/import', {
29
+ listId: options.listId,
30
+ subscribers: options.subscribers,
31
+ });
32
+ }
33
+ /** List all subscribers across all your lists. */
34
+ list(options = {}) {
35
+ return this.http.get('/subscribers', options);
36
+ }
37
+ /** Get subscribers for a specific list. */
38
+ listByList(listId, options = {}) {
39
+ return this.http.get(`/subscribers/list/${listId}`, options);
40
+ }
41
+ /** Update subscriber details. */
42
+ update(subscriberId, options) {
43
+ return this.http.put(`/subscribers/${subscriberId}`, options);
44
+ }
45
+ /** Soft-delete (archive) a subscriber. */
46
+ delete(subscriberId) {
47
+ return this.http.delete(`/subscribers/${subscriberId}`);
48
+ }
49
+ /** Get a subscriber's email preferences. */
50
+ preferences(subscriberId) {
51
+ return this.http.get(`/subscribers/preferences/${subscriberId}`);
52
+ }
53
+ /** Update a subscriber's email preferences. */
54
+ updatePreferences(subscriberId, emailPreferences) {
55
+ return this.http.put(`/subscribers/${subscriberId}/preferences`, { emailPreferences });
56
+ }
57
+ }
58
+ exports.Subscribers = Subscribers;
@@ -0,0 +1,34 @@
1
+ import { HttpClient } from '../client';
2
+ export interface CreateTemplateOptions {
3
+ name: string;
4
+ subject: string;
5
+ html: string;
6
+ category?: string;
7
+ variables?: string[];
8
+ }
9
+ export interface UpdateTemplateOptions {
10
+ name?: string;
11
+ subject?: string;
12
+ html?: string;
13
+ text?: string;
14
+ previewText?: string;
15
+ category?: string;
16
+ }
17
+ export declare class Templates {
18
+ private http;
19
+ constructor(http: HttpClient);
20
+ /**
21
+ * Create a reusable email template.
22
+ * @example
23
+ * await client.templates.create({ name: 'Welcome', subject: 'Welcome to {{appName}}', html: '<p>Hi {{firstName}}</p>' })
24
+ */
25
+ create(options: CreateTemplateOptions): Promise<unknown>;
26
+ /** List all templates. */
27
+ list(): Promise<unknown>;
28
+ /** Get a template by ID. */
29
+ get(templateId: string): Promise<unknown>;
30
+ /** Update a template. */
31
+ update(templateId: string, options: UpdateTemplateOptions): Promise<unknown>;
32
+ /** Delete a template. */
33
+ delete(templateId: string): Promise<unknown>;
34
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Templates = void 0;
4
+ class Templates {
5
+ constructor(http) {
6
+ this.http = http;
7
+ }
8
+ /**
9
+ * Create a reusable email template.
10
+ * @example
11
+ * await client.templates.create({ name: 'Welcome', subject: 'Welcome to {{appName}}', html: '<p>Hi {{firstName}}</p>' })
12
+ */
13
+ create(options) {
14
+ return this.http.post('/templates', {
15
+ name: options.name,
16
+ subject: options.subject,
17
+ htmlContent: options.html,
18
+ category: options.category,
19
+ variables: options.variables,
20
+ });
21
+ }
22
+ /** List all templates. */
23
+ list() {
24
+ return this.http.get('/templates');
25
+ }
26
+ /** Get a template by ID. */
27
+ get(templateId) {
28
+ return this.http.get(`/templates/${templateId}`);
29
+ }
30
+ /** Update a template. */
31
+ update(templateId, options) {
32
+ return this.http.put(`/templates/${templateId}`, {
33
+ name: options.name,
34
+ subject: options.subject,
35
+ htmlContent: options.html,
36
+ plainTextContent: options.text,
37
+ previewText: options.previewText,
38
+ category: options.category,
39
+ });
40
+ }
41
+ /** Delete a template. */
42
+ delete(templateId) {
43
+ return this.http.delete(`/templates/${templateId}`);
44
+ }
45
+ }
46
+ exports.Templates = Templates;
@@ -0,0 +1,58 @@
1
+ import { HttpClient } from '../client';
2
+ export interface CreateWebhookOptions {
3
+ url: string;
4
+ events: string[];
5
+ name?: string;
6
+ }
7
+ export interface WebhookEvent {
8
+ type: string;
9
+ messageId: string;
10
+ timestamp: string;
11
+ [key: string]: unknown;
12
+ }
13
+ export declare class Webhooks {
14
+ private http;
15
+ constructor(http: HttpClient);
16
+ /**
17
+ * Register a webhook endpoint.
18
+ * @example
19
+ * await client.webhooks.create({ url: 'https://myapp.com/hooks/email', events: ['email.bounced', 'email.opened'] })
20
+ */
21
+ create(options: CreateWebhookOptions): Promise<unknown>;
22
+ /** List all registered webhooks. */
23
+ list(): Promise<unknown>;
24
+ /** Delete a webhook by ID. */
25
+ delete(webhookId: string): Promise<unknown>;
26
+ /**
27
+ * Verify the HMAC-SHA256 signature of an incoming webhook request.
28
+ * Use this in your webhook handler to confirm the request came from SendCraft.
29
+ *
30
+ * @param payload - Raw request body string (before JSON.parse)
31
+ * @param signature - Value of the `x-sendcraft-signature` header
32
+ * @param secret - Your webhook signing secret from the SendCraft dashboard
33
+ * @returns `true` if the signature is valid
34
+ *
35
+ * @example — Express
36
+ * app.post('/hooks/sendcraft', express.raw({ type: 'application/json' }), (req, res) => {
37
+ * const valid = client.webhooks.verify(
38
+ * req.body.toString(),
39
+ * req.headers['x-sendcraft-signature'] as string,
40
+ * process.env.SENDCRAFT_WEBHOOK_SECRET!,
41
+ * );
42
+ * if (!valid) return res.sendStatus(401);
43
+ * const event = JSON.parse(req.body.toString()) as WebhookEvent;
44
+ * // handle event...
45
+ * res.sendStatus(200);
46
+ * });
47
+ */
48
+ verify(payload: string, signature: string, secret: string): boolean;
49
+ /**
50
+ * Parse and type a raw webhook payload string.
51
+ * Throws a descriptive error on malformed JSON rather than an opaque
52
+ * SyntaxError from the runtime.
53
+ * @example
54
+ * const event = client.webhooks.parse(req.body.toString());
55
+ * if (event.type === 'email.bounced') { ... }
56
+ */
57
+ parse(payload: string): WebhookEvent;
58
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Webhooks = void 0;
4
+ const crypto_1 = require("crypto");
5
+ class Webhooks {
6
+ constructor(http) {
7
+ this.http = http;
8
+ }
9
+ /**
10
+ * Register a webhook endpoint.
11
+ * @example
12
+ * await client.webhooks.create({ url: 'https://myapp.com/hooks/email', events: ['email.bounced', 'email.opened'] })
13
+ */
14
+ create(options) {
15
+ return this.http.post('/webhooks', options);
16
+ }
17
+ /** List all registered webhooks. */
18
+ list() {
19
+ return this.http.get('/webhooks');
20
+ }
21
+ /** Delete a webhook by ID. */
22
+ delete(webhookId) {
23
+ return this.http.delete(`/webhooks/${webhookId}`);
24
+ }
25
+ /**
26
+ * Verify the HMAC-SHA256 signature of an incoming webhook request.
27
+ * Use this in your webhook handler to confirm the request came from SendCraft.
28
+ *
29
+ * @param payload - Raw request body string (before JSON.parse)
30
+ * @param signature - Value of the `x-sendcraft-signature` header
31
+ * @param secret - Your webhook signing secret from the SendCraft dashboard
32
+ * @returns `true` if the signature is valid
33
+ *
34
+ * @example — Express
35
+ * app.post('/hooks/sendcraft', express.raw({ type: 'application/json' }), (req, res) => {
36
+ * const valid = client.webhooks.verify(
37
+ * req.body.toString(),
38
+ * req.headers['x-sendcraft-signature'] as string,
39
+ * process.env.SENDCRAFT_WEBHOOK_SECRET!,
40
+ * );
41
+ * if (!valid) return res.sendStatus(401);
42
+ * const event = JSON.parse(req.body.toString()) as WebhookEvent;
43
+ * // handle event...
44
+ * res.sendStatus(200);
45
+ * });
46
+ */
47
+ verify(payload, signature, secret) {
48
+ if (!payload || !signature || !secret)
49
+ return false;
50
+ const expected = (0, crypto_1.createHmac)('sha256', secret).update(payload).digest('hex');
51
+ // Use Node's built-in constant-time comparison — avoids manual loop and
52
+ // the early-exit length check that leaks whether the provided signature
53
+ // is the right length (expected is always 64 hex chars).
54
+ try {
55
+ return (0, crypto_1.timingSafeEqual)(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
56
+ }
57
+ catch {
58
+ // Buffer.from() throws if signature is not valid hex; treat as invalid.
59
+ return false;
60
+ }
61
+ }
62
+ /**
63
+ * Parse and type a raw webhook payload string.
64
+ * Throws a descriptive error on malformed JSON rather than an opaque
65
+ * SyntaxError from the runtime.
66
+ * @example
67
+ * const event = client.webhooks.parse(req.body.toString());
68
+ * if (event.type === 'email.bounced') { ... }
69
+ */
70
+ parse(payload) {
71
+ if (typeof payload !== 'string' || payload.length === 0) {
72
+ throw new Error('[sendcraft-sdk] webhooks.parse() received an empty payload');
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(payload);
77
+ }
78
+ catch {
79
+ throw new Error('[sendcraft-sdk] webhooks.parse() received invalid JSON');
80
+ }
81
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
82
+ throw new Error('[sendcraft-sdk] webhooks.parse() expected a JSON object');
83
+ }
84
+ // Guard against prototype pollution: __proto__ / constructor overrides
85
+ const obj = parsed;
86
+ if (Object.prototype.hasOwnProperty.call(obj, '__proto__') ||
87
+ Object.prototype.hasOwnProperty.call(obj, 'constructor') ||
88
+ Object.prototype.hasOwnProperty.call(obj, 'prototype')) {
89
+ throw new Error('[sendcraft-sdk] webhooks.parse() rejected payload with forbidden keys');
90
+ }
91
+ return obj;
92
+ }
93
+ }
94
+ exports.Webhooks = Webhooks;