@primitivedotdev/sdk 0.2.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/README.md +227 -0
- package/dist/contract/index.d.ts +150 -0
- package/dist/contract/index.js +199 -0
- package/dist/index-C9pON-wY.d.ts +1474 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/parser/index.d.ts +185 -0
- package/dist/parser/index.js +438 -0
- package/dist/types-C6M6oCRS.d.ts +817 -0
- package/dist/webhook/index.d.ts +3 -0
- package/dist/webhook/index.js +3 -0
- package/dist/webhook-h24dbQEE.js +7661 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1474 @@
|
|
|
1
|
+
import { EmailAuth, EmailReceivedEvent, ValidateEmailAuthResult, WebhookEvent } from "./types-C6M6oCRS.js";
|
|
2
|
+
import { ErrorObject } from "ajv";
|
|
3
|
+
|
|
4
|
+
//#region src/webhook/errors.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Verification error definitions.
|
|
7
|
+
* Use these for documentation, dashboards, and i18n.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Verification error definitions.
|
|
11
|
+
* Use these for documentation, dashboards, and i18n.
|
|
12
|
+
*/
|
|
13
|
+
declare const VERIFICATION_ERRORS: {
|
|
14
|
+
readonly INVALID_SIGNATURE_HEADER: {
|
|
15
|
+
readonly message: "Missing or malformed Primitive-Signature header";
|
|
16
|
+
readonly suggestion: "Check that you're reading the correct header (Primitive-Signature) and it's being passed correctly from your web framework.";
|
|
17
|
+
};
|
|
18
|
+
readonly TIMESTAMP_OUT_OF_RANGE: {
|
|
19
|
+
readonly message: "Timestamp is too old (possible replay attack)";
|
|
20
|
+
readonly suggestion: "This could indicate a replay attack, network delay, or server clock drift. Check your server's time is synced.";
|
|
21
|
+
};
|
|
22
|
+
readonly SIGNATURE_MISMATCH: {
|
|
23
|
+
readonly message: "Signature doesn't match expected value";
|
|
24
|
+
readonly suggestion: "Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).";
|
|
25
|
+
};
|
|
26
|
+
readonly MISSING_SECRET: {
|
|
27
|
+
readonly message: "No webhook secret was provided";
|
|
28
|
+
readonly suggestion: "Pass your webhook secret from the Primitive dashboard. Check that the environment variable is set.";
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Payload parsing error definitions.
|
|
33
|
+
* Use these for documentation, dashboards, and i18n.
|
|
34
|
+
*/
|
|
35
|
+
declare const PAYLOAD_ERRORS: {
|
|
36
|
+
readonly PAYLOAD_NULL: {
|
|
37
|
+
readonly message: "Webhook payload is null";
|
|
38
|
+
readonly suggestion: "Ensure you're passing the parsed JSON body, not null. Check your framework's body parsing middleware.";
|
|
39
|
+
};
|
|
40
|
+
readonly PAYLOAD_UNDEFINED: {
|
|
41
|
+
readonly message: "Webhook payload is undefined";
|
|
42
|
+
readonly suggestion: "The payload was not provided. Make sure you're passing the request body to the handler.";
|
|
43
|
+
};
|
|
44
|
+
readonly PAYLOAD_WRONG_TYPE: {
|
|
45
|
+
readonly message: "Webhook payload must be an object";
|
|
46
|
+
readonly suggestion: "The payload should be a parsed JSON object. Check that you're not passing a string or other primitive.";
|
|
47
|
+
};
|
|
48
|
+
readonly PAYLOAD_IS_ARRAY: {
|
|
49
|
+
readonly message: "Webhook payload is an array, expected object";
|
|
50
|
+
readonly suggestion: "Primitive webhooks are single event objects, not arrays. Check the payload structure.";
|
|
51
|
+
};
|
|
52
|
+
readonly PAYLOAD_MISSING_EVENT: {
|
|
53
|
+
readonly message: "Webhook payload missing 'event' field";
|
|
54
|
+
readonly suggestion: "All webhook payloads must have an 'event' field. This may not be a valid Primitive webhook.";
|
|
55
|
+
};
|
|
56
|
+
readonly PAYLOAD_UNKNOWN_EVENT: {
|
|
57
|
+
readonly message: "Unknown webhook event type";
|
|
58
|
+
readonly suggestion: "This event type is not recognized. You may need to update your SDK or handle unknown events gracefully.";
|
|
59
|
+
};
|
|
60
|
+
readonly PAYLOAD_EMPTY_BODY: {
|
|
61
|
+
readonly message: "Request body is empty";
|
|
62
|
+
readonly suggestion: "The request body was empty. Ensure the webhook is sending data and your framework is parsing it correctly.";
|
|
63
|
+
};
|
|
64
|
+
readonly JSON_PARSE_FAILED: {
|
|
65
|
+
readonly message: "Failed to parse JSON body";
|
|
66
|
+
readonly suggestion: "The request body is not valid JSON. Check the raw body content and Content-Type header.";
|
|
67
|
+
};
|
|
68
|
+
readonly INVALID_ENCODING: {
|
|
69
|
+
readonly message: "Invalid body encoding";
|
|
70
|
+
readonly suggestion: "The request body encoding is not supported. Primitive webhooks use UTF-8 encoded JSON.";
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Raw email decode error definitions.
|
|
75
|
+
* Use these for documentation, dashboards, and i18n.
|
|
76
|
+
*/
|
|
77
|
+
declare const RAW_EMAIL_ERRORS: {
|
|
78
|
+
readonly NOT_INCLUDED: {
|
|
79
|
+
readonly message: "Raw email content not included inline";
|
|
80
|
+
readonly suggestion: "Use the download URL at event.email.content.download.url to fetch the raw email.";
|
|
81
|
+
};
|
|
82
|
+
readonly INVALID_BASE64: {
|
|
83
|
+
readonly message: "Raw email content is not valid base64";
|
|
84
|
+
readonly suggestion: "The raw email data is malformed. Fetch the raw email from the download URL or regenerate the webhook payload.";
|
|
85
|
+
};
|
|
86
|
+
readonly HASH_MISMATCH: {
|
|
87
|
+
readonly message: "SHA-256 hash verification failed";
|
|
88
|
+
readonly suggestion: "The raw email data may be corrupted. Try downloading from the URL instead.";
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* All error codes that can be thrown by the SDK.
|
|
93
|
+
*/
|
|
94
|
+
type WebhookErrorCode = WebhookVerificationErrorCode | WebhookPayloadErrorCode | WebhookValidationErrorCode | RawEmailDecodeErrorCode;
|
|
95
|
+
type RawEmailDecodeErrorCode = keyof typeof RAW_EMAIL_ERRORS;
|
|
96
|
+
/**
|
|
97
|
+
* Base class for all Primitive webhook errors.
|
|
98
|
+
*
|
|
99
|
+
* Catch this to handle any error from the SDK in a single catch block.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { handleWebhook, PrimitiveWebhookError } from '@primitivedotdev/sdk';
|
|
104
|
+
*
|
|
105
|
+
* try {
|
|
106
|
+
* const event = handleWebhook({ body, headers, secret });
|
|
107
|
+
* } catch (err) {
|
|
108
|
+
* if (err instanceof PrimitiveWebhookError) {
|
|
109
|
+
* console.error(`[${err.code}] ${err.message}`);
|
|
110
|
+
* return res.status(400).json({ error: err.code });
|
|
111
|
+
* }
|
|
112
|
+
* throw err;
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare abstract class PrimitiveWebhookError extends Error {
|
|
117
|
+
/** Programmatic error code for monitoring and handling */
|
|
118
|
+
abstract readonly code: WebhookErrorCode;
|
|
119
|
+
/** Actionable guidance for fixing the issue */
|
|
120
|
+
abstract readonly suggestion: string;
|
|
121
|
+
/**
|
|
122
|
+
* Formats the error for logging/display.
|
|
123
|
+
*/
|
|
124
|
+
toString(): string;
|
|
125
|
+
/**
|
|
126
|
+
* Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
|
|
127
|
+
*/
|
|
128
|
+
toJSON(): {
|
|
129
|
+
name: string;
|
|
130
|
+
code: WebhookErrorCode;
|
|
131
|
+
message: string;
|
|
132
|
+
suggestion: string;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Error codes for webhook verification failures.
|
|
137
|
+
* Derived from VERIFICATION_ERRORS keys.
|
|
138
|
+
*/
|
|
139
|
+
type WebhookVerificationErrorCode = keyof typeof VERIFICATION_ERRORS;
|
|
140
|
+
/**
|
|
141
|
+
* Error thrown when webhook signature verification fails.
|
|
142
|
+
*
|
|
143
|
+
* Use the `code` property to programmatically handle specific error cases.
|
|
144
|
+
*/
|
|
145
|
+
declare class WebhookVerificationError extends PrimitiveWebhookError {
|
|
146
|
+
readonly code: WebhookVerificationErrorCode;
|
|
147
|
+
readonly suggestion: string;
|
|
148
|
+
constructor(code: WebhookVerificationErrorCode, message?: string, suggestion?: string);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Error codes for webhook payload parsing failures.
|
|
152
|
+
* Derived from PAYLOAD_ERRORS keys.
|
|
153
|
+
*/
|
|
154
|
+
type WebhookPayloadErrorCode = keyof typeof PAYLOAD_ERRORS;
|
|
155
|
+
/**
|
|
156
|
+
* Error thrown when webhook payload parsing fails (lightweight parser).
|
|
157
|
+
*
|
|
158
|
+
* Use the `code` property for programmatic handling and monitoring.
|
|
159
|
+
* The `suggestion` property contains actionable guidance for fixing the issue.
|
|
160
|
+
*/
|
|
161
|
+
declare class WebhookPayloadError extends PrimitiveWebhookError {
|
|
162
|
+
readonly code: WebhookPayloadErrorCode;
|
|
163
|
+
readonly suggestion: string;
|
|
164
|
+
/** Original error if this wraps another error (e.g., JSON.parse failure) */
|
|
165
|
+
readonly cause?: Error;
|
|
166
|
+
constructor(code: WebhookPayloadErrorCode, message?: string, suggestion?: string, cause?: Error);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Error code for schema validation failures.
|
|
170
|
+
*/
|
|
171
|
+
type WebhookValidationErrorCode = "SCHEMA_VALIDATION_FAILED";
|
|
172
|
+
/**
|
|
173
|
+
* Error thrown when schema validation fails.
|
|
174
|
+
*/
|
|
175
|
+
declare class WebhookValidationError extends PrimitiveWebhookError {
|
|
176
|
+
readonly code: WebhookValidationErrorCode;
|
|
177
|
+
readonly suggestion: string;
|
|
178
|
+
/** The specific field path that failed (e.g., "email.headers.from") */
|
|
179
|
+
readonly field: string;
|
|
180
|
+
/** Original schema validation errors for advanced debugging */
|
|
181
|
+
readonly validationErrors: readonly ErrorObject[];
|
|
182
|
+
/** Number of additional validation errors beyond the first */
|
|
183
|
+
readonly additionalErrorCount: number;
|
|
184
|
+
constructor(field: string, message: string, suggestion: string, validationErrors: readonly ErrorObject[]);
|
|
185
|
+
/**
|
|
186
|
+
* Formats the error for logging/display.
|
|
187
|
+
* Includes error count and suggestion.
|
|
188
|
+
*/
|
|
189
|
+
toString(): string;
|
|
190
|
+
/**
|
|
191
|
+
* Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
|
|
192
|
+
*/
|
|
193
|
+
toJSON(): {
|
|
194
|
+
name: string;
|
|
195
|
+
code: "SCHEMA_VALIDATION_FAILED";
|
|
196
|
+
field: string;
|
|
197
|
+
message: string;
|
|
198
|
+
suggestion: string;
|
|
199
|
+
additionalErrorCount: number;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Error thrown when raw email decoding or verification fails.
|
|
204
|
+
*
|
|
205
|
+
* Use the `code` property to determine the failure reason:
|
|
206
|
+
* - `NOT_INCLUDED`: Raw email not inline, must download from URL
|
|
207
|
+
* - `HASH_MISMATCH`: SHA-256 verification failed, content may be corrupted
|
|
208
|
+
*/
|
|
209
|
+
declare class RawEmailDecodeError extends PrimitiveWebhookError {
|
|
210
|
+
readonly code: RawEmailDecodeErrorCode;
|
|
211
|
+
readonly suggestion: string;
|
|
212
|
+
constructor(code: RawEmailDecodeErrorCode, message?: string);
|
|
213
|
+
} //#endregion
|
|
214
|
+
//#region src/validation.d.ts
|
|
215
|
+
interface ValidationSuccess<T> {
|
|
216
|
+
success: true;
|
|
217
|
+
data: T;
|
|
218
|
+
}
|
|
219
|
+
interface ValidationFailure {
|
|
220
|
+
success: false;
|
|
221
|
+
error: WebhookValidationError;
|
|
222
|
+
}
|
|
223
|
+
type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
|
|
224
|
+
declare function validateEmailReceivedEvent(input: unknown): EmailReceivedEvent;
|
|
225
|
+
declare function safeValidateEmailReceivedEvent(input: unknown): ValidationResult<EmailReceivedEvent>;
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/webhook/signing.d.ts
|
|
229
|
+
/**
|
|
230
|
+
* Webhook HMAC Signing (Stripe-style format)
|
|
231
|
+
*
|
|
232
|
+
* Implements HMAC-SHA256 signature for webhook security with timestamp validation.
|
|
233
|
+
* Prevents replay attacks by including timestamp in signature.
|
|
234
|
+
*
|
|
235
|
+
* Header format:
|
|
236
|
+
* Primitive-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
|
|
237
|
+
*
|
|
238
|
+
* Signed payload format: "{timestamp}.{raw_body}"
|
|
239
|
+
*
|
|
240
|
+
* This format matches Stripe's webhook signature scheme, which is widely understood
|
|
241
|
+
* and easy to implement in any language with ~15 lines of code.
|
|
242
|
+
*/
|
|
243
|
+
/** Header name for incoming webhook signature */
|
|
244
|
+
declare const PRIMITIVE_SIGNATURE_HEADER = "Primitive-Signature";
|
|
245
|
+
/** Header name to confirm webhook was processed (prevents retries) */
|
|
246
|
+
declare const PRIMITIVE_CONFIRMED_HEADER = "X-Primitive-Confirmed";
|
|
247
|
+
/**
|
|
248
|
+
* Result from signing a webhook payload
|
|
249
|
+
*/
|
|
250
|
+
interface SignResult {
|
|
251
|
+
/** Full header value ready to set on Primitive-Signature */
|
|
252
|
+
header: string;
|
|
253
|
+
/** Unix timestamp used for signing */
|
|
254
|
+
timestamp: number;
|
|
255
|
+
/** Raw hex signature (useful for debugging/tests) */
|
|
256
|
+
v1: string;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Options for verifying a webhook signature
|
|
260
|
+
*/
|
|
261
|
+
interface VerifyOptions {
|
|
262
|
+
/** The raw HTTP request body (must be the exact bytes, not re-serialized JSON) */
|
|
263
|
+
rawBody: string | Buffer;
|
|
264
|
+
/** The full Primitive-Signature header value */
|
|
265
|
+
signatureHeader: string;
|
|
266
|
+
/** Your webhook secret */
|
|
267
|
+
secret: string | Buffer;
|
|
268
|
+
/** Max age in seconds (default: 300) */
|
|
269
|
+
toleranceSeconds?: number;
|
|
270
|
+
/** Override current time for testing (unix seconds) */
|
|
271
|
+
nowSeconds?: number;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Sign a webhook payload using HMAC-SHA256.
|
|
275
|
+
*
|
|
276
|
+
* Useful for:
|
|
277
|
+
* - Internal testing and dogfooding
|
|
278
|
+
* - Generating test vectors
|
|
279
|
+
* - Integration tests
|
|
280
|
+
*
|
|
281
|
+
* @param rawBody - The raw JSON body string to sign
|
|
282
|
+
* @param secret - The webhook secret key
|
|
283
|
+
* @param timestamp - Unix timestamp in seconds (defaults to current time)
|
|
284
|
+
*/
|
|
285
|
+
declare function signWebhookPayload(rawBody: string | Buffer, secret: string | Buffer, timestamp?: number): SignResult;
|
|
286
|
+
/**
|
|
287
|
+
* Verify a webhook signature.
|
|
288
|
+
*
|
|
289
|
+
* Throws `WebhookVerificationError` on failure with a specific error code.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* import { verifyWebhookSignature, WebhookVerificationError } from '@primitivedotdev/sdk';
|
|
294
|
+
*
|
|
295
|
+
* try {
|
|
296
|
+
* const signatureHeader = req.headers['primitive-signature'];
|
|
297
|
+
* if (typeof signatureHeader !== 'string') {
|
|
298
|
+
* throw new Error('Missing Primitive-Signature header');
|
|
299
|
+
* }
|
|
300
|
+
* verifyWebhookSignature({
|
|
301
|
+
* rawBody: req.body, // raw string, NOT parsed JSON
|
|
302
|
+
* signatureHeader,
|
|
303
|
+
* secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
|
|
304
|
+
* });
|
|
305
|
+
* // Signature is valid, process the webhook
|
|
306
|
+
* } catch (err) {
|
|
307
|
+
* if (err instanceof WebhookVerificationError) {
|
|
308
|
+
* console.error('Invalid webhook:', err.code, err.message);
|
|
309
|
+
* }
|
|
310
|
+
* return res.status(400).send('Invalid signature');
|
|
311
|
+
* }
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
declare function verifyWebhookSignature(opts: VerifyOptions): true;
|
|
315
|
+
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/schema.generated.d.ts
|
|
318
|
+
/**
|
|
319
|
+
* JSON Schema for EmailReceivedEvent.
|
|
320
|
+
*
|
|
321
|
+
* AUTO-GENERATED - DO NOT EDIT
|
|
322
|
+
* Run `pnpm generate:schema` to regenerate.
|
|
323
|
+
*/
|
|
324
|
+
declare const emailReceivedEventJsonSchema: {
|
|
325
|
+
readonly $schema: "http://json-schema.org/draft-07/schema#";
|
|
326
|
+
readonly $ref: "#/definitions/EmailReceivedEvent";
|
|
327
|
+
readonly definitions: {
|
|
328
|
+
readonly EmailReceivedEvent: {
|
|
329
|
+
readonly type: "object";
|
|
330
|
+
readonly properties: {
|
|
331
|
+
readonly id: {
|
|
332
|
+
readonly type: "string";
|
|
333
|
+
readonly pattern: "^evt_[a-f0-9]{64}$";
|
|
334
|
+
readonly description: "Unique delivery event ID.\n\nThis ID is stable across retries to the same endpoint - use it as your idempotency/dedupe key. Note that the same email delivered to different endpoints will have different event IDs.\n\nFormat: `evt_` prefix followed by a SHA-256 hash (64 hex characters). Example: `evt_a1b2c3d4e5f6...` (68 characters total)";
|
|
335
|
+
};
|
|
336
|
+
readonly event: {
|
|
337
|
+
readonly type: "string";
|
|
338
|
+
readonly const: "email.received";
|
|
339
|
+
readonly description: "Event type identifier. Always `\"email.received\"` for this event type.";
|
|
340
|
+
};
|
|
341
|
+
readonly version: {
|
|
342
|
+
readonly $ref: "#/definitions/WebhookVersion";
|
|
343
|
+
readonly description: "API version in date format (YYYY-MM-DD). Use this to detect version mismatches between webhook and SDK.";
|
|
344
|
+
};
|
|
345
|
+
readonly delivery: {
|
|
346
|
+
readonly type: "object";
|
|
347
|
+
readonly properties: {
|
|
348
|
+
readonly endpoint_id: {
|
|
349
|
+
readonly type: "string";
|
|
350
|
+
readonly description: "ID of the webhook endpoint receiving this event. Matches the endpoint ID from your Primitive dashboard.";
|
|
351
|
+
};
|
|
352
|
+
readonly attempt: {
|
|
353
|
+
readonly type: "integer";
|
|
354
|
+
readonly minimum: 1;
|
|
355
|
+
readonly description: "Delivery attempt number, starting at 1. Increments with each retry if previous attempts failed.";
|
|
356
|
+
};
|
|
357
|
+
readonly attempted_at: {
|
|
358
|
+
readonly type: "string";
|
|
359
|
+
readonly format: "date-time";
|
|
360
|
+
readonly description: "ISO 8601 timestamp (UTC) when this delivery was attempted.";
|
|
361
|
+
readonly examples: ["2025-01-15T10:30:00.000Z"];
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
readonly required: ["endpoint_id", "attempt", "attempted_at"];
|
|
365
|
+
readonly description: "Metadata about this webhook delivery.";
|
|
366
|
+
};
|
|
367
|
+
readonly email: {
|
|
368
|
+
readonly type: "object";
|
|
369
|
+
readonly properties: {
|
|
370
|
+
readonly id: {
|
|
371
|
+
readonly type: "string";
|
|
372
|
+
readonly description: "Unique email ID in Primitive. Use this ID when calling Primitive APIs to reference this email.";
|
|
373
|
+
};
|
|
374
|
+
readonly received_at: {
|
|
375
|
+
readonly type: "string";
|
|
376
|
+
readonly format: "date-time";
|
|
377
|
+
readonly description: "ISO 8601 timestamp (UTC) when Primitive received the email.";
|
|
378
|
+
readonly examples: ["2025-01-15T10:29:55.123Z"];
|
|
379
|
+
};
|
|
380
|
+
readonly smtp: {
|
|
381
|
+
readonly type: "object";
|
|
382
|
+
readonly properties: {
|
|
383
|
+
readonly helo: {
|
|
384
|
+
readonly type: ["string", "null"];
|
|
385
|
+
readonly description: "HELO/EHLO hostname from the sending server. Null if not provided during SMTP transaction.";
|
|
386
|
+
};
|
|
387
|
+
readonly mail_from: {
|
|
388
|
+
readonly type: "string";
|
|
389
|
+
readonly description: "SMTP envelope sender (MAIL FROM command). This is the bounce address, which may differ from the From header.";
|
|
390
|
+
};
|
|
391
|
+
readonly rcpt_to: {
|
|
392
|
+
readonly type: "array";
|
|
393
|
+
readonly items: {
|
|
394
|
+
readonly type: "string";
|
|
395
|
+
};
|
|
396
|
+
readonly minItems: 1;
|
|
397
|
+
readonly description: "SMTP envelope recipients (RCPT TO commands). All addresses that received this email in a single delivery.";
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
readonly required: ["helo", "mail_from", "rcpt_to"];
|
|
401
|
+
readonly description: "SMTP envelope information. This is the \"real\" sender/recipient info from the SMTP transaction, which may differ from the headers (e.g., BCC recipients).";
|
|
402
|
+
};
|
|
403
|
+
readonly headers: {
|
|
404
|
+
readonly type: "object";
|
|
405
|
+
readonly properties: {
|
|
406
|
+
readonly message_id: {
|
|
407
|
+
readonly type: ["string", "null"];
|
|
408
|
+
readonly description: "Message-ID header value. Null if the email had no Message-ID header.";
|
|
409
|
+
};
|
|
410
|
+
readonly subject: {
|
|
411
|
+
readonly type: ["string", "null"];
|
|
412
|
+
readonly description: "Subject header value. Null if the email had no Subject header.";
|
|
413
|
+
};
|
|
414
|
+
readonly from: {
|
|
415
|
+
readonly type: "string";
|
|
416
|
+
readonly minLength: 1;
|
|
417
|
+
readonly description: "From header value. May include display name: `\"John Doe\" <john@example.com>`";
|
|
418
|
+
};
|
|
419
|
+
readonly to: {
|
|
420
|
+
readonly type: "string";
|
|
421
|
+
readonly minLength: 1;
|
|
422
|
+
readonly description: "To header value. May include multiple addresses or display names.";
|
|
423
|
+
};
|
|
424
|
+
readonly date: {
|
|
425
|
+
readonly type: ["string", "null"];
|
|
426
|
+
readonly description: "Date header value as it appeared in the email. Null if the email had no Date header.";
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
readonly required: ["message_id", "subject", "from", "to", "date"];
|
|
430
|
+
readonly description: "Parsed email headers. These are extracted from the email content, not the SMTP envelope.";
|
|
431
|
+
};
|
|
432
|
+
readonly content: {
|
|
433
|
+
readonly type: "object";
|
|
434
|
+
readonly properties: {
|
|
435
|
+
readonly raw: {
|
|
436
|
+
readonly $ref: "#/definitions/RawContent";
|
|
437
|
+
readonly description: "Raw email in RFC 5322 format. May be inline (base64) or download-only depending on size.";
|
|
438
|
+
};
|
|
439
|
+
readonly download: {
|
|
440
|
+
readonly type: "object";
|
|
441
|
+
readonly properties: {
|
|
442
|
+
readonly url: {
|
|
443
|
+
readonly type: "string";
|
|
444
|
+
readonly format: "uri";
|
|
445
|
+
readonly pattern: "^https://";
|
|
446
|
+
readonly description: "HTTPS URL to download the raw email. Returns the email as-is in RFC 5322 format.";
|
|
447
|
+
};
|
|
448
|
+
readonly expires_at: {
|
|
449
|
+
readonly type: "string";
|
|
450
|
+
readonly format: "date-time";
|
|
451
|
+
readonly description: "ISO 8601 timestamp (UTC) when this URL expires. Download before this time or the URL will return 403.";
|
|
452
|
+
};
|
|
453
|
+
};
|
|
454
|
+
readonly required: ["url", "expires_at"];
|
|
455
|
+
readonly description: "Download information for the raw email. Always present, even if raw content is inline.";
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
readonly required: ["raw", "download"];
|
|
459
|
+
readonly description: "Raw email content and download information.";
|
|
460
|
+
};
|
|
461
|
+
readonly parsed: {
|
|
462
|
+
readonly $ref: "#/definitions/ParsedData";
|
|
463
|
+
readonly description: "Parsed email content (body text, HTML, attachments). Check `status` to determine if parsing succeeded.";
|
|
464
|
+
};
|
|
465
|
+
readonly analysis: {
|
|
466
|
+
readonly $ref: "#/definitions/EmailAnalysis";
|
|
467
|
+
readonly description: "Email analysis and classification results.";
|
|
468
|
+
};
|
|
469
|
+
readonly auth: {
|
|
470
|
+
readonly $ref: "#/definitions/EmailAuth";
|
|
471
|
+
readonly description: "Email authentication results (SPF, DKIM, DMARC).";
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
readonly required: ["id", "received_at", "smtp", "headers", "content", "parsed", "analysis", "auth"];
|
|
475
|
+
readonly description: "The email that triggered this event.";
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
readonly required: ["id", "event", "version", "delivery", "email"];
|
|
479
|
+
readonly description: "Webhook payload for the `email.received` event.\n\nThis is delivered to your webhook endpoint when Primitive receives an email matching your domain configuration.";
|
|
480
|
+
};
|
|
481
|
+
readonly WebhookVersion: {
|
|
482
|
+
readonly type: "string";
|
|
483
|
+
readonly pattern: "^(?:(?:\\d{4}-(?:(?:01|03|05|07|08|10|12)-(?:0[1-9]|[12]\\d|3[01])|(?:04|06|09|11)-(?:0[1-9]|[12]\\d|30)|02-(?:0[1-9]|1\\d|2[0-8])))|(?:(?:[02468][048]00|[13579][26]00|\\d{2}(?:0[48]|[2468][048]|[13579][26]))-02-29))$";
|
|
484
|
+
readonly description: "Valid webhook version format (YYYY-MM-DD date string). The SDK accepts any valid date-formatted version, not just the current one, for forward and backward compatibility.";
|
|
485
|
+
};
|
|
486
|
+
readonly RawContent: {
|
|
487
|
+
readonly anyOf: [{
|
|
488
|
+
readonly $ref: "#/definitions/RawContentInline";
|
|
489
|
+
}, {
|
|
490
|
+
readonly $ref: "#/definitions/RawContentDownloadOnly";
|
|
491
|
+
}];
|
|
492
|
+
readonly description: "Raw email content - a discriminated union on `included`.";
|
|
493
|
+
};
|
|
494
|
+
readonly RawContentInline: {
|
|
495
|
+
readonly type: "object";
|
|
496
|
+
readonly properties: {
|
|
497
|
+
readonly included: {
|
|
498
|
+
readonly type: "boolean";
|
|
499
|
+
readonly const: true;
|
|
500
|
+
readonly description: "Discriminant indicating raw content is included inline.";
|
|
501
|
+
};
|
|
502
|
+
readonly encoding: {
|
|
503
|
+
readonly type: "string";
|
|
504
|
+
readonly const: "base64";
|
|
505
|
+
readonly description: "Encoding used for the data field. Always \"base64\".";
|
|
506
|
+
};
|
|
507
|
+
readonly max_inline_bytes: {
|
|
508
|
+
readonly type: "integer";
|
|
509
|
+
readonly minimum: 1;
|
|
510
|
+
readonly description: "Maximum size in bytes for inline inclusion. Emails larger than this threshold require download.";
|
|
511
|
+
};
|
|
512
|
+
readonly size_bytes: {
|
|
513
|
+
readonly type: "integer";
|
|
514
|
+
readonly minimum: 0;
|
|
515
|
+
readonly description: "Actual size of the raw email in bytes.";
|
|
516
|
+
};
|
|
517
|
+
readonly sha256: {
|
|
518
|
+
readonly type: "string";
|
|
519
|
+
readonly pattern: "^[a-fA-F0-9]{64}$";
|
|
520
|
+
readonly description: "SHA-256 hash of the raw email content (hex-encoded). Use this to verify integrity after base64 decoding.";
|
|
521
|
+
};
|
|
522
|
+
readonly data: {
|
|
523
|
+
readonly type: "string";
|
|
524
|
+
readonly description: "Base64-encoded raw email (RFC 5322 format). Decode with `Buffer.from(data, 'base64')` in Node.js.";
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
readonly required: ["included", "encoding", "max_inline_bytes", "size_bytes", "sha256", "data"];
|
|
528
|
+
readonly description: "Raw email content included inline (base64 encoded).\n\nWhen the raw email is small enough (under {@link max_inline_bytes } ), it's included directly in the webhook payload for convenience.";
|
|
529
|
+
};
|
|
530
|
+
readonly RawContentDownloadOnly: {
|
|
531
|
+
readonly type: "object";
|
|
532
|
+
readonly properties: {
|
|
533
|
+
readonly included: {
|
|
534
|
+
readonly type: "boolean";
|
|
535
|
+
readonly const: false;
|
|
536
|
+
readonly description: "Discriminant indicating raw content must be downloaded.";
|
|
537
|
+
};
|
|
538
|
+
readonly reason_code: {
|
|
539
|
+
readonly type: "string";
|
|
540
|
+
readonly const: "size_exceeded";
|
|
541
|
+
readonly description: "Reason the content wasn't included inline.";
|
|
542
|
+
};
|
|
543
|
+
readonly max_inline_bytes: {
|
|
544
|
+
readonly type: "integer";
|
|
545
|
+
readonly minimum: 1;
|
|
546
|
+
readonly description: "Maximum size in bytes for inline inclusion. The email exceeded this threshold.";
|
|
547
|
+
};
|
|
548
|
+
readonly size_bytes: {
|
|
549
|
+
readonly type: "integer";
|
|
550
|
+
readonly minimum: 0;
|
|
551
|
+
readonly description: "Actual size of the raw email in bytes.";
|
|
552
|
+
};
|
|
553
|
+
readonly sha256: {
|
|
554
|
+
readonly type: "string";
|
|
555
|
+
readonly pattern: "^[a-fA-F0-9]{64}$";
|
|
556
|
+
readonly description: "SHA-256 hash of the raw email content (hex-encoded). Use this to verify integrity after download.";
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
readonly required: ["included", "reason_code", "max_inline_bytes", "size_bytes", "sha256"];
|
|
560
|
+
readonly description: "Raw email content not included (must be downloaded).\n\nWhen the raw email exceeds {@link max_inline_bytes } , it's not included in the webhook payload. Use the download URL from {@link EmailReceivedEvent.email.content.download } to fetch it.";
|
|
561
|
+
};
|
|
562
|
+
readonly ParsedData: {
|
|
563
|
+
readonly anyOf: [{
|
|
564
|
+
readonly $ref: "#/definitions/ParsedDataComplete";
|
|
565
|
+
}, {
|
|
566
|
+
readonly $ref: "#/definitions/ParsedDataFailed";
|
|
567
|
+
}];
|
|
568
|
+
readonly description: "Parsed email content - a discriminated union on `status`.";
|
|
569
|
+
};
|
|
570
|
+
readonly ParsedDataComplete: {
|
|
571
|
+
readonly type: "object";
|
|
572
|
+
readonly properties: {
|
|
573
|
+
readonly status: {
|
|
574
|
+
readonly type: "string";
|
|
575
|
+
readonly const: "complete";
|
|
576
|
+
readonly description: "Discriminant indicating successful parsing.";
|
|
577
|
+
};
|
|
578
|
+
readonly error: {
|
|
579
|
+
readonly type: "null";
|
|
580
|
+
readonly description: "Always null when parsing succeeds.";
|
|
581
|
+
};
|
|
582
|
+
readonly body_text: {
|
|
583
|
+
readonly type: ["string", "null"];
|
|
584
|
+
readonly description: "Plain text body of the email. Null if the email had no text/plain part.";
|
|
585
|
+
};
|
|
586
|
+
readonly body_html: {
|
|
587
|
+
readonly type: ["string", "null"];
|
|
588
|
+
readonly description: "HTML body of the email. Null if the email had no text/html part.";
|
|
589
|
+
};
|
|
590
|
+
readonly reply_to: {
|
|
591
|
+
readonly anyOf: [{
|
|
592
|
+
readonly type: "array";
|
|
593
|
+
readonly items: {
|
|
594
|
+
readonly $ref: "#/definitions/EmailAddress";
|
|
595
|
+
};
|
|
596
|
+
}, {
|
|
597
|
+
readonly type: "null";
|
|
598
|
+
}];
|
|
599
|
+
readonly description: "Parsed Reply-To header addresses. Null if the email had no Reply-To header.";
|
|
600
|
+
};
|
|
601
|
+
readonly cc: {
|
|
602
|
+
readonly anyOf: [{
|
|
603
|
+
readonly type: "array";
|
|
604
|
+
readonly items: {
|
|
605
|
+
readonly $ref: "#/definitions/EmailAddress";
|
|
606
|
+
};
|
|
607
|
+
}, {
|
|
608
|
+
readonly type: "null";
|
|
609
|
+
}];
|
|
610
|
+
readonly description: "Parsed CC header addresses. Null if the email had no CC header.";
|
|
611
|
+
};
|
|
612
|
+
readonly bcc: {
|
|
613
|
+
readonly anyOf: [{
|
|
614
|
+
readonly type: "array";
|
|
615
|
+
readonly items: {
|
|
616
|
+
readonly $ref: "#/definitions/EmailAddress";
|
|
617
|
+
};
|
|
618
|
+
}, {
|
|
619
|
+
readonly type: "null";
|
|
620
|
+
}];
|
|
621
|
+
readonly description: "Parsed BCC header addresses. Null if the email had no BCC header. Note: BCC is only available for outgoing emails or when explicitly provided.";
|
|
622
|
+
};
|
|
623
|
+
readonly in_reply_to: {
|
|
624
|
+
readonly anyOf: [{
|
|
625
|
+
readonly type: "array";
|
|
626
|
+
readonly items: {
|
|
627
|
+
readonly type: "string";
|
|
628
|
+
};
|
|
629
|
+
}, {
|
|
630
|
+
readonly type: "null";
|
|
631
|
+
}];
|
|
632
|
+
readonly description: "In-Reply-To header values (Message-IDs of the email(s) being replied to). Null if the email had no In-Reply-To header. Per RFC 5322, this can contain multiple Message-IDs, though typically just one.";
|
|
633
|
+
readonly examples: [["<original-message-id@example.com>"]];
|
|
634
|
+
};
|
|
635
|
+
readonly references: {
|
|
636
|
+
readonly anyOf: [{
|
|
637
|
+
readonly type: "array";
|
|
638
|
+
readonly items: {
|
|
639
|
+
readonly type: "string";
|
|
640
|
+
};
|
|
641
|
+
}, {
|
|
642
|
+
readonly type: "null";
|
|
643
|
+
}];
|
|
644
|
+
readonly description: "References header values (Message-IDs of the email thread). Null if the email had no References header.";
|
|
645
|
+
readonly examples: [["<msg1@example.com>", "<msg2@example.com>"]];
|
|
646
|
+
};
|
|
647
|
+
readonly attachments: {
|
|
648
|
+
readonly type: "array";
|
|
649
|
+
readonly items: {
|
|
650
|
+
readonly $ref: "#/definitions/WebhookAttachment";
|
|
651
|
+
};
|
|
652
|
+
readonly description: "List of attachments with metadata. Use {@link attachments_download_url } to download the actual files.";
|
|
653
|
+
};
|
|
654
|
+
readonly attachments_download_url: {
|
|
655
|
+
readonly type: ["string", "null"];
|
|
656
|
+
readonly format: "uri";
|
|
657
|
+
readonly pattern: "^https://";
|
|
658
|
+
readonly description: "HTTPS URL to download all attachments as a tar.gz archive. Null if the email had no attachments. URL expires - check the expiration before downloading.";
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
readonly required: ["status", "error", "body_text", "body_html", "reply_to", "cc", "bcc", "in_reply_to", "references", "attachments", "attachments_download_url"];
|
|
662
|
+
readonly description: "Parsed email content when parsing succeeded.\n\nUse the discriminant `status: \"complete\"` to narrow from {@link ParsedData } .";
|
|
663
|
+
};
|
|
664
|
+
readonly EmailAddress: {
|
|
665
|
+
readonly type: "object";
|
|
666
|
+
readonly properties: {
|
|
667
|
+
readonly address: {
|
|
668
|
+
readonly type: "string";
|
|
669
|
+
readonly description: "The email address portion (e.g., \"john@example.com\").\n\nThis is the raw value from the email header with no validation applied. May contain unusual but valid formats like quoted local parts.";
|
|
670
|
+
};
|
|
671
|
+
readonly name: {
|
|
672
|
+
readonly type: ["string", "null"];
|
|
673
|
+
readonly description: "The display name portion, if present. Null if the address had no display name.\n\nMay contain any characters including unicode, emoji, or special characters as they appeared in the original email header.";
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
readonly required: ["address", "name"];
|
|
677
|
+
readonly description: "A parsed email address with optional display name.\n\nThis structure is used in the `parsed` section of the webhook payload (e.g., `reply_to`, `cc`, `bcc`). For unparsed header strings, see the `headers` section (e.g., `event.email.headers.from`).";
|
|
678
|
+
};
|
|
679
|
+
readonly WebhookAttachment: {
|
|
680
|
+
readonly type: "object";
|
|
681
|
+
readonly properties: {
|
|
682
|
+
readonly filename: {
|
|
683
|
+
readonly type: ["string", "null"];
|
|
684
|
+
readonly description: "Original filename from the email. May be null if the attachment had no filename specified.";
|
|
685
|
+
};
|
|
686
|
+
readonly content_type: {
|
|
687
|
+
readonly type: "string";
|
|
688
|
+
readonly description: "MIME content type (e.g., \"application/pdf\", \"image/png\").";
|
|
689
|
+
};
|
|
690
|
+
readonly size_bytes: {
|
|
691
|
+
readonly type: "integer";
|
|
692
|
+
readonly minimum: 0;
|
|
693
|
+
readonly description: "Size of the attachment in bytes.";
|
|
694
|
+
};
|
|
695
|
+
readonly sha256: {
|
|
696
|
+
readonly type: "string";
|
|
697
|
+
readonly pattern: "^[a-fA-F0-9]{64}$";
|
|
698
|
+
readonly description: "SHA-256 hash of the attachment content (hex-encoded). Use this to verify attachment integrity after download.";
|
|
699
|
+
};
|
|
700
|
+
readonly part_index: {
|
|
701
|
+
readonly type: "integer";
|
|
702
|
+
readonly minimum: 0;
|
|
703
|
+
readonly description: "Zero-based index of this part in the MIME structure.";
|
|
704
|
+
};
|
|
705
|
+
readonly tar_path: {
|
|
706
|
+
readonly type: "string";
|
|
707
|
+
readonly description: "Path to this attachment within the downloaded tar.gz archive.";
|
|
708
|
+
};
|
|
709
|
+
};
|
|
710
|
+
readonly required: ["filename", "content_type", "size_bytes", "sha256", "part_index", "tar_path"];
|
|
711
|
+
readonly description: "Metadata for an email attachment.\n\nAttachment content is not included directly in the webhook payload. Use the `attachments_download_url` from {@link ParsedDataComplete } to download all attachments as a tar.gz archive.";
|
|
712
|
+
};
|
|
713
|
+
readonly ParsedDataFailed: {
|
|
714
|
+
readonly type: "object";
|
|
715
|
+
readonly properties: {
|
|
716
|
+
readonly status: {
|
|
717
|
+
readonly type: "string";
|
|
718
|
+
readonly const: "failed";
|
|
719
|
+
readonly description: "Discriminant indicating parsing failed.";
|
|
720
|
+
};
|
|
721
|
+
readonly error: {
|
|
722
|
+
readonly $ref: "#/definitions/ParsedError";
|
|
723
|
+
readonly description: "Details about why parsing failed.";
|
|
724
|
+
};
|
|
725
|
+
readonly body_text: {
|
|
726
|
+
readonly type: "null";
|
|
727
|
+
readonly description: "Always null when parsing fails.";
|
|
728
|
+
};
|
|
729
|
+
readonly body_html: {
|
|
730
|
+
readonly type: "null";
|
|
731
|
+
readonly description: "Always null when parsing fails.";
|
|
732
|
+
};
|
|
733
|
+
readonly reply_to: {
|
|
734
|
+
readonly type: "null";
|
|
735
|
+
readonly description: "Always null when parsing fails.";
|
|
736
|
+
};
|
|
737
|
+
readonly cc: {
|
|
738
|
+
readonly type: "null";
|
|
739
|
+
readonly description: "Always null when parsing fails.";
|
|
740
|
+
};
|
|
741
|
+
readonly bcc: {
|
|
742
|
+
readonly type: "null";
|
|
743
|
+
readonly description: "Always null when parsing fails.";
|
|
744
|
+
};
|
|
745
|
+
readonly in_reply_to: {
|
|
746
|
+
readonly type: "null";
|
|
747
|
+
readonly description: "Always null when parsing fails.";
|
|
748
|
+
};
|
|
749
|
+
readonly references: {
|
|
750
|
+
readonly type: "null";
|
|
751
|
+
readonly description: "Always null when parsing fails.";
|
|
752
|
+
};
|
|
753
|
+
readonly attachments: {
|
|
754
|
+
readonly type: "array";
|
|
755
|
+
readonly items: {
|
|
756
|
+
readonly $ref: "#/definitions/WebhookAttachment";
|
|
757
|
+
};
|
|
758
|
+
readonly description: "May contain partial attachment metadata even when parsing failed. Useful for debugging or recovering partial data.";
|
|
759
|
+
};
|
|
760
|
+
readonly attachments_download_url: {
|
|
761
|
+
readonly type: "null";
|
|
762
|
+
readonly description: "Always null when parsing fails.";
|
|
763
|
+
};
|
|
764
|
+
};
|
|
765
|
+
readonly required: ["status", "error", "body_text", "body_html", "reply_to", "cc", "bcc", "in_reply_to", "references", "attachments", "attachments_download_url"];
|
|
766
|
+
readonly description: "Parsed email content when parsing failed.\n\nUse the discriminant `status: \"failed\"` to narrow from {@link ParsedData } .";
|
|
767
|
+
};
|
|
768
|
+
readonly ParsedError: {
|
|
769
|
+
readonly type: "object";
|
|
770
|
+
readonly properties: {
|
|
771
|
+
readonly code: {
|
|
772
|
+
readonly type: "string";
|
|
773
|
+
readonly enum: ["PARSE_FAILED", "ATTACHMENT_EXTRACTION_FAILED"];
|
|
774
|
+
readonly description: "Error code indicating the type of failure.\n- `PARSE_FAILED`: The email could not be parsed (e.g., malformed MIME)\n- `ATTACHMENT_EXTRACTION_FAILED`: Email parsed but attachments couldn't be extracted";
|
|
775
|
+
};
|
|
776
|
+
readonly message: {
|
|
777
|
+
readonly type: "string";
|
|
778
|
+
readonly description: "Human-readable error message describing what went wrong.";
|
|
779
|
+
};
|
|
780
|
+
readonly retryable: {
|
|
781
|
+
readonly type: "boolean";
|
|
782
|
+
readonly description: "Whether retrying might succeed. If true, the error was transient (e.g., timeout). If false, the email itself is problematic.";
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
readonly required: ["code", "message", "retryable"];
|
|
786
|
+
readonly description: "Error details when email parsing fails.";
|
|
787
|
+
};
|
|
788
|
+
readonly EmailAnalysis: {
|
|
789
|
+
readonly type: "object";
|
|
790
|
+
readonly properties: {
|
|
791
|
+
readonly spamassassin: {
|
|
792
|
+
readonly type: "object";
|
|
793
|
+
readonly properties: {
|
|
794
|
+
readonly score: {
|
|
795
|
+
readonly type: "number";
|
|
796
|
+
readonly description: "Overall spam score (sum of all rule scores). Higher scores indicate higher likelihood of spam. Unbounded - can be negative (ham) or very high (spam).";
|
|
797
|
+
};
|
|
798
|
+
};
|
|
799
|
+
readonly required: ["score"];
|
|
800
|
+
readonly description: "SpamAssassin analysis results.";
|
|
801
|
+
};
|
|
802
|
+
readonly forward: {
|
|
803
|
+
readonly $ref: "#/definitions/ForwardAnalysis";
|
|
804
|
+
readonly description: "Forward detection and analysis results.";
|
|
805
|
+
};
|
|
806
|
+
};
|
|
807
|
+
readonly description: "Email analysis and classification results.";
|
|
808
|
+
};
|
|
809
|
+
readonly ForwardAnalysis: {
|
|
810
|
+
readonly type: "object";
|
|
811
|
+
readonly properties: {
|
|
812
|
+
readonly detected: {
|
|
813
|
+
readonly type: "boolean";
|
|
814
|
+
readonly description: "Whether any forwards were detected in the email.";
|
|
815
|
+
};
|
|
816
|
+
readonly results: {
|
|
817
|
+
readonly type: "array";
|
|
818
|
+
readonly items: {
|
|
819
|
+
readonly $ref: "#/definitions/ForwardResult";
|
|
820
|
+
};
|
|
821
|
+
readonly description: "Analysis results for each detected forward.";
|
|
822
|
+
};
|
|
823
|
+
readonly attachments_found: {
|
|
824
|
+
readonly type: "integer";
|
|
825
|
+
readonly minimum: 0;
|
|
826
|
+
readonly description: "Total number of .eml attachments found.";
|
|
827
|
+
};
|
|
828
|
+
readonly attachments_analyzed: {
|
|
829
|
+
readonly type: "integer";
|
|
830
|
+
readonly minimum: 0;
|
|
831
|
+
readonly description: "Number of .eml attachments that were analyzed.";
|
|
832
|
+
};
|
|
833
|
+
readonly attachments_limit: {
|
|
834
|
+
readonly type: ["integer", "null"];
|
|
835
|
+
readonly minimum: 1;
|
|
836
|
+
readonly description: "Maximum number of attachments that will be analyzed, or null if unlimited.";
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
readonly required: ["detected", "results", "attachments_found", "attachments_analyzed", "attachments_limit"];
|
|
840
|
+
readonly description: "Forward detection and analysis results.";
|
|
841
|
+
};
|
|
842
|
+
readonly ForwardResult: {
|
|
843
|
+
readonly anyOf: [{
|
|
844
|
+
readonly $ref: "#/definitions/ForwardResultInline";
|
|
845
|
+
}, {
|
|
846
|
+
readonly $ref: "#/definitions/ForwardResultAttachmentAnalyzed";
|
|
847
|
+
}, {
|
|
848
|
+
readonly $ref: "#/definitions/ForwardResultAttachmentSkipped";
|
|
849
|
+
}];
|
|
850
|
+
readonly description: "Result for a single forwarded email detected in the message.\n\nUse the `type` and `analyzed` fields to narrow the type:\n- `type: 'inline'` - Inline forward, always analyzed\n- `type: 'attachment'` + `analyzed: true` - Analyzed attachment\n- `type: 'attachment'` + `analyzed: false` - Skipped attachment";
|
|
851
|
+
};
|
|
852
|
+
readonly ForwardResultInline: {
|
|
853
|
+
readonly type: "object";
|
|
854
|
+
readonly properties: {
|
|
855
|
+
readonly type: {
|
|
856
|
+
readonly type: "string";
|
|
857
|
+
readonly const: "inline";
|
|
858
|
+
};
|
|
859
|
+
readonly original_sender: {
|
|
860
|
+
readonly anyOf: [{
|
|
861
|
+
readonly $ref: "#/definitions/ForwardOriginalSender";
|
|
862
|
+
}, {
|
|
863
|
+
readonly type: "null";
|
|
864
|
+
}];
|
|
865
|
+
readonly description: "Original sender of the forwarded email, if extractable.";
|
|
866
|
+
};
|
|
867
|
+
readonly verification: {
|
|
868
|
+
readonly $ref: "#/definitions/ForwardVerification";
|
|
869
|
+
readonly description: "Verification result for the forwarded email.";
|
|
870
|
+
};
|
|
871
|
+
readonly summary: {
|
|
872
|
+
readonly type: "string";
|
|
873
|
+
readonly description: "Human-readable summary of the forward analysis.";
|
|
874
|
+
};
|
|
875
|
+
};
|
|
876
|
+
readonly required: ["type", "original_sender", "verification", "summary"];
|
|
877
|
+
readonly description: "Result for an inline forward that was detected and analyzed. Inline forwards are always analyzed when forward detection is enabled.";
|
|
878
|
+
};
|
|
879
|
+
readonly ForwardOriginalSender: {
|
|
880
|
+
readonly type: "object";
|
|
881
|
+
readonly properties: {
|
|
882
|
+
readonly email: {
|
|
883
|
+
readonly type: "string";
|
|
884
|
+
readonly description: "Email address of the original sender.";
|
|
885
|
+
};
|
|
886
|
+
readonly domain: {
|
|
887
|
+
readonly type: "string";
|
|
888
|
+
readonly description: "Domain of the original sender.";
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
readonly required: ["email", "domain"];
|
|
892
|
+
readonly description: "Original sender information extracted from the forwarded email.";
|
|
893
|
+
};
|
|
894
|
+
readonly ForwardVerification: {
|
|
895
|
+
readonly type: "object";
|
|
896
|
+
readonly properties: {
|
|
897
|
+
readonly verdict: {
|
|
898
|
+
readonly $ref: "#/definitions/ForwardVerdict";
|
|
899
|
+
readonly description: "Overall verdict on whether the forward is authentic.";
|
|
900
|
+
};
|
|
901
|
+
readonly confidence: {
|
|
902
|
+
readonly $ref: "#/definitions/AuthConfidence";
|
|
903
|
+
readonly description: "Confidence level for this verdict.";
|
|
904
|
+
};
|
|
905
|
+
readonly dkim_verified: {
|
|
906
|
+
readonly type: "boolean";
|
|
907
|
+
readonly description: "Whether a valid DKIM signature was found that verifies the original sender.";
|
|
908
|
+
};
|
|
909
|
+
readonly dkim_domain: {
|
|
910
|
+
readonly type: ["string", "null"];
|
|
911
|
+
readonly description: "Domain of the DKIM signature that verified the forward, if any.";
|
|
912
|
+
};
|
|
913
|
+
readonly dmarc_policy: {
|
|
914
|
+
readonly $ref: "#/definitions/DmarcPolicy";
|
|
915
|
+
readonly description: "DMARC policy of the original sender's domain.";
|
|
916
|
+
};
|
|
917
|
+
};
|
|
918
|
+
readonly required: ["verdict", "confidence", "dkim_verified", "dkim_domain", "dmarc_policy"];
|
|
919
|
+
readonly description: "Verification result for a forwarded email.";
|
|
920
|
+
};
|
|
921
|
+
readonly ForwardVerdict: {
|
|
922
|
+
readonly type: "string";
|
|
923
|
+
readonly enum: ["legit", "unknown"];
|
|
924
|
+
readonly description: "Verdict for forwarded email verification.\n\n- `legit`: DKIM signature verified the original sender\n- `unknown`: Could not verify the forwarded email's authenticity";
|
|
925
|
+
};
|
|
926
|
+
readonly AuthConfidence: {
|
|
927
|
+
readonly type: "string";
|
|
928
|
+
readonly enum: ["high", "medium", "low"];
|
|
929
|
+
readonly description: "Confidence level for the authentication verdict.\n\n- `high`: Strong cryptographic evidence (DKIM aligned + DMARC pass)\n- `medium`: Good evidence but with caveats (SPF-only alignment)\n- `low`: Weak evidence (missing authentication or unclear results)";
|
|
930
|
+
};
|
|
931
|
+
readonly DmarcPolicy: {
|
|
932
|
+
readonly type: ["string", "null"];
|
|
933
|
+
readonly enum: ["reject", "quarantine", "none", null];
|
|
934
|
+
readonly description: "DMARC policy action specified in the domain's DMARC record.\n\n- `reject`: The domain owner requests that receivers reject failing emails\n- `quarantine`: The domain owner requests that failing emails be treated as suspicious\n- `none`: The domain owner is only monitoring (no action requested)\n- `null`: No DMARC policy was found for the domain";
|
|
935
|
+
};
|
|
936
|
+
readonly ForwardResultAttachmentAnalyzed: {
|
|
937
|
+
readonly type: "object";
|
|
938
|
+
readonly properties: {
|
|
939
|
+
readonly type: {
|
|
940
|
+
readonly type: "string";
|
|
941
|
+
readonly const: "attachment";
|
|
942
|
+
};
|
|
943
|
+
readonly attachment_tar_path: {
|
|
944
|
+
readonly type: "string";
|
|
945
|
+
readonly description: "Path to the attachment in the attachments tar archive.";
|
|
946
|
+
};
|
|
947
|
+
readonly attachment_filename: {
|
|
948
|
+
readonly type: ["string", "null"];
|
|
949
|
+
readonly description: "Original filename of the attachment, if available.";
|
|
950
|
+
};
|
|
951
|
+
readonly analyzed: {
|
|
952
|
+
readonly type: "boolean";
|
|
953
|
+
readonly const: true;
|
|
954
|
+
readonly description: "Whether this attachment was analyzed.";
|
|
955
|
+
};
|
|
956
|
+
readonly original_sender: {
|
|
957
|
+
readonly anyOf: [{
|
|
958
|
+
readonly $ref: "#/definitions/ForwardOriginalSender";
|
|
959
|
+
}, {
|
|
960
|
+
readonly type: "null";
|
|
961
|
+
}];
|
|
962
|
+
readonly description: "Original sender of the forwarded email, if extractable.";
|
|
963
|
+
};
|
|
964
|
+
readonly verification: {
|
|
965
|
+
readonly $ref: "#/definitions/ForwardVerification";
|
|
966
|
+
readonly description: "Verification result for the forwarded email.";
|
|
967
|
+
};
|
|
968
|
+
readonly summary: {
|
|
969
|
+
readonly type: "string";
|
|
970
|
+
readonly description: "Human-readable summary of the forward analysis.";
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
readonly required: ["type", "attachment_tar_path", "attachment_filename", "analyzed", "original_sender", "verification", "summary"];
|
|
974
|
+
readonly description: "Result for an attachment forward that was analyzed.";
|
|
975
|
+
};
|
|
976
|
+
readonly ForwardResultAttachmentSkipped: {
|
|
977
|
+
readonly type: "object";
|
|
978
|
+
readonly properties: {
|
|
979
|
+
readonly type: {
|
|
980
|
+
readonly type: "string";
|
|
981
|
+
readonly const: "attachment";
|
|
982
|
+
};
|
|
983
|
+
readonly attachment_tar_path: {
|
|
984
|
+
readonly type: "string";
|
|
985
|
+
readonly description: "Path to the attachment in the attachments tar archive.";
|
|
986
|
+
};
|
|
987
|
+
readonly attachment_filename: {
|
|
988
|
+
readonly type: ["string", "null"];
|
|
989
|
+
readonly description: "Original filename of the attachment, if available.";
|
|
990
|
+
};
|
|
991
|
+
readonly analyzed: {
|
|
992
|
+
readonly type: "boolean";
|
|
993
|
+
readonly const: false;
|
|
994
|
+
readonly description: "Whether this attachment was analyzed.";
|
|
995
|
+
};
|
|
996
|
+
readonly original_sender: {
|
|
997
|
+
readonly type: "null";
|
|
998
|
+
readonly description: "Always null when not analyzed.";
|
|
999
|
+
};
|
|
1000
|
+
readonly verification: {
|
|
1001
|
+
readonly type: "null";
|
|
1002
|
+
readonly description: "Always null when not analyzed.";
|
|
1003
|
+
};
|
|
1004
|
+
readonly summary: {
|
|
1005
|
+
readonly type: "string";
|
|
1006
|
+
readonly description: "Human-readable summary explaining why analysis was skipped.";
|
|
1007
|
+
};
|
|
1008
|
+
};
|
|
1009
|
+
readonly required: ["type", "attachment_tar_path", "attachment_filename", "analyzed", "original_sender", "verification", "summary"];
|
|
1010
|
+
readonly description: "Result for an attachment forward that was detected but not analyzed. This occurs when attachment analysis is disabled or the limit was reached.";
|
|
1011
|
+
};
|
|
1012
|
+
readonly EmailAuth: {
|
|
1013
|
+
readonly type: "object";
|
|
1014
|
+
readonly properties: {
|
|
1015
|
+
readonly spf: {
|
|
1016
|
+
readonly $ref: "#/definitions/SpfResult";
|
|
1017
|
+
readonly description: "SPF verification result.\n\nSPF checks if the sending IP is authorized by the envelope sender's domain. \"pass\" means the IP is authorized; \"fail\" means it's explicitly not allowed.";
|
|
1018
|
+
};
|
|
1019
|
+
readonly dmarc: {
|
|
1020
|
+
readonly $ref: "#/definitions/DmarcResult";
|
|
1021
|
+
readonly description: "DMARC verification result.\n\nDMARC passes if either SPF or DKIM passes AND aligns with the From: domain. \"pass\" means the email is authenticated according to the sender's policy.";
|
|
1022
|
+
};
|
|
1023
|
+
readonly dmarcPolicy: {
|
|
1024
|
+
readonly $ref: "#/definitions/DmarcPolicy";
|
|
1025
|
+
readonly description: "DMARC policy from the sender's DNS record.\n\n- `reject`: Domain wants receivers to reject failing emails\n- `quarantine`: Domain wants failing emails marked as suspicious\n- `none`: Domain is monitoring only (no action requested)\n- `null`: No DMARC record found for this domain";
|
|
1026
|
+
};
|
|
1027
|
+
readonly dmarcFromDomain: {
|
|
1028
|
+
readonly type: ["string", "null"];
|
|
1029
|
+
readonly description: "The organizational domain used for DMARC lookups.\n\nFor example, if the From: address is `user@mail.example.com`, the DMARC lookup checks `_dmarc.mail.example.com`, then falls back to `_dmarc.example.com`. This field shows which domain's policy was used.";
|
|
1030
|
+
};
|
|
1031
|
+
readonly dmarcSpfAligned: {
|
|
1032
|
+
readonly type: "boolean";
|
|
1033
|
+
readonly description: "Whether SPF aligned with the From: domain for DMARC purposes.\n\nTrue if the envelope sender domain matches the From: domain (per alignment mode). Optional in self-hosted environments.";
|
|
1034
|
+
};
|
|
1035
|
+
readonly dmarcDkimAligned: {
|
|
1036
|
+
readonly type: "boolean";
|
|
1037
|
+
readonly description: "Whether DKIM aligned with the From: domain for DMARC purposes.\n\nTrue if at least one DKIM signature's domain matches the From: domain. Optional in self-hosted environments.";
|
|
1038
|
+
};
|
|
1039
|
+
readonly dmarcSpfStrict: {
|
|
1040
|
+
readonly type: ["boolean", "null"];
|
|
1041
|
+
readonly description: "Whether DMARC SPF alignment mode is strict.\n\n- `true`: Strict alignment required (exact domain match)\n- `false`: Relaxed alignment allowed (organizational domain match)\n- `null`: No DMARC record found";
|
|
1042
|
+
};
|
|
1043
|
+
readonly dmarcDkimStrict: {
|
|
1044
|
+
readonly type: ["boolean", "null"];
|
|
1045
|
+
readonly description: "Whether DMARC DKIM alignment mode is strict.\n\n- `true`: Strict alignment required (exact domain match)\n- `false`: Relaxed alignment allowed (organizational domain match)\n- `null`: No DMARC record found";
|
|
1046
|
+
};
|
|
1047
|
+
readonly dkimSignatures: {
|
|
1048
|
+
readonly type: "array";
|
|
1049
|
+
readonly items: {
|
|
1050
|
+
readonly $ref: "#/definitions/DkimSignature";
|
|
1051
|
+
};
|
|
1052
|
+
readonly description: "All DKIM signatures found in the email with their verification results.\n\nMay be empty if no DKIM signatures were present.";
|
|
1053
|
+
};
|
|
1054
|
+
};
|
|
1055
|
+
readonly required: ["spf", "dmarc", "dmarcPolicy", "dmarcFromDomain", "dmarcSpfStrict", "dmarcDkimStrict", "dkimSignatures"];
|
|
1056
|
+
readonly description: "Email authentication results for SPF, DKIM, and DMARC.\n\nUse `validateEmailAuth()` to compute a verdict based on these results.";
|
|
1057
|
+
};
|
|
1058
|
+
readonly SpfResult: {
|
|
1059
|
+
readonly type: "string";
|
|
1060
|
+
readonly enum: ["pass", "fail", "softfail", "neutral", "none", "temperror", "permerror"];
|
|
1061
|
+
readonly description: "SPF verification result.";
|
|
1062
|
+
};
|
|
1063
|
+
readonly DmarcResult: {
|
|
1064
|
+
readonly type: "string";
|
|
1065
|
+
readonly enum: ["pass", "fail", "none", "temperror", "permerror"];
|
|
1066
|
+
readonly description: "DMARC verification result.";
|
|
1067
|
+
};
|
|
1068
|
+
readonly DkimSignature: {
|
|
1069
|
+
readonly type: "object";
|
|
1070
|
+
readonly properties: {
|
|
1071
|
+
readonly domain: {
|
|
1072
|
+
readonly type: "string";
|
|
1073
|
+
readonly description: "The domain that signed this DKIM signature (d= tag). This may differ from the From: domain (that's what alignment checks).";
|
|
1074
|
+
};
|
|
1075
|
+
readonly selector: {
|
|
1076
|
+
readonly type: ["string", "null"];
|
|
1077
|
+
readonly description: "The DKIM selector used to locate the public key (s= tag). Combined with the domain to form the DNS lookup: `selector._domainkey.domain`\n\nOptional in self-hosted environments where the milter may not provide selector info.";
|
|
1078
|
+
};
|
|
1079
|
+
readonly result: {
|
|
1080
|
+
readonly $ref: "#/definitions/DkimResult";
|
|
1081
|
+
readonly description: "Verification result for this specific signature.";
|
|
1082
|
+
};
|
|
1083
|
+
readonly aligned: {
|
|
1084
|
+
readonly type: "boolean";
|
|
1085
|
+
readonly description: "Whether this signature's domain aligns with the From: domain (for DMARC).\n\nAlignment can be \"strict\" (exact match) or \"relaxed\" (organizational domain match). For example, if From: is `user@sub.example.com` and DKIM is signed by `example.com`:\n- Relaxed alignment: true (same organizational domain)\n- Strict alignment: false (not exact match)";
|
|
1086
|
+
};
|
|
1087
|
+
readonly keyBits: {
|
|
1088
|
+
readonly type: ["integer", "null"];
|
|
1089
|
+
readonly minimum: 1;
|
|
1090
|
+
readonly maximum: 16384;
|
|
1091
|
+
readonly description: "Key size in bits (e.g., 1024, 2048). Null if the key size couldn't be determined.\n\nOptional in self-hosted environments.";
|
|
1092
|
+
};
|
|
1093
|
+
readonly algo: {
|
|
1094
|
+
readonly type: ["string", "null"];
|
|
1095
|
+
readonly description: "Signing algorithm (e.g., \"rsa-sha256\", \"ed25519-sha256\").\n\nOptional in self-hosted environments.";
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
readonly required: ["domain", "result", "aligned"];
|
|
1099
|
+
readonly description: "Details about a single DKIM signature found in the email.\n\nAn email may have multiple DKIM signatures (e.g., one from the sending domain and one from the ESP). Each signature is verified independently.\n\nFields marked optional (`selector`, `keyBits`, `algo`) may be unavailable in self-hosted environments where the milter provides limited DKIM detail.";
|
|
1100
|
+
};
|
|
1101
|
+
readonly DkimResult: {
|
|
1102
|
+
readonly type: "string";
|
|
1103
|
+
readonly enum: ["pass", "fail", "temperror", "permerror"];
|
|
1104
|
+
readonly description: "DKIM signature verification result for a single signature.";
|
|
1105
|
+
};
|
|
1106
|
+
};
|
|
1107
|
+
}; //#endregion
|
|
1108
|
+
//#region src/webhook/auth.d.ts
|
|
1109
|
+
/**
|
|
1110
|
+
* Validate email authentication and compute a verdict.
|
|
1111
|
+
*
|
|
1112
|
+
* This function analyzes SPF, DKIM, and DMARC results to determine
|
|
1113
|
+
* whether an email is likely authentic ("legit"), potentially spoofed
|
|
1114
|
+
* ("suspicious"), or indeterminate ("unknown").
|
|
1115
|
+
*
|
|
1116
|
+
* ## Verdict Logic
|
|
1117
|
+
*
|
|
1118
|
+
* **Legit (high confidence):**
|
|
1119
|
+
* - DMARC pass with DKIM alignment (cryptographic proof of authenticity)
|
|
1120
|
+
*
|
|
1121
|
+
* **Legit (medium confidence):**
|
|
1122
|
+
* - DMARC pass with SPF alignment only (no DKIM)
|
|
1123
|
+
* - Note: SPF can break through forwarding, but DMARC pass is still meaningful
|
|
1124
|
+
*
|
|
1125
|
+
* **Suspicious (high confidence):**
|
|
1126
|
+
* - DMARC fail when domain has `reject` or `quarantine` policy
|
|
1127
|
+
* - The domain owner explicitly says to distrust failing emails
|
|
1128
|
+
* - SPF explicitly fails (IP not authorized by sender)
|
|
1129
|
+
*
|
|
1130
|
+
* **Suspicious (low confidence):**
|
|
1131
|
+
* - DMARC fail when domain has `none` policy (monitoring mode)
|
|
1132
|
+
* - No DMARC record but SPF/DKIM fail
|
|
1133
|
+
*
|
|
1134
|
+
* **Unknown:**
|
|
1135
|
+
* - No DMARC record and no clear pass/fail
|
|
1136
|
+
* - Temporary errors during authentication
|
|
1137
|
+
* - No authentication data available
|
|
1138
|
+
*
|
|
1139
|
+
* @param auth - Email authentication results from the webhook
|
|
1140
|
+
* @returns Verdict, confidence level, and explanatory reasons
|
|
1141
|
+
*
|
|
1142
|
+
* @example
|
|
1143
|
+
* ```typescript
|
|
1144
|
+
* const result = validateEmailAuth({
|
|
1145
|
+
* spf: 'pass',
|
|
1146
|
+
* dmarc: 'pass',
|
|
1147
|
+
* dmarcPolicy: 'reject',
|
|
1148
|
+
* dmarcFromDomain: 'example.com',
|
|
1149
|
+
* dmarcSpfAligned: true,
|
|
1150
|
+
* dmarcDkimAligned: true,
|
|
1151
|
+
* dmarcSpfStrict: false,
|
|
1152
|
+
* dmarcDkimStrict: false,
|
|
1153
|
+
* dkimSignatures: [{
|
|
1154
|
+
* domain: 'example.com',
|
|
1155
|
+
* selector: 'default',
|
|
1156
|
+
* result: 'pass',
|
|
1157
|
+
* aligned: true,
|
|
1158
|
+
* keyBits: 2048,
|
|
1159
|
+
* algo: 'rsa-sha256',
|
|
1160
|
+
* }],
|
|
1161
|
+
* });
|
|
1162
|
+
*
|
|
1163
|
+
* // result.verdict === 'legit'
|
|
1164
|
+
* // result.confidence === 'high'
|
|
1165
|
+
* // result.reasons === ['DMARC passed with DKIM alignment']
|
|
1166
|
+
* ```
|
|
1167
|
+
*/
|
|
1168
|
+
declare function validateEmailAuth(auth: EmailAuth): ValidateEmailAuthResult;
|
|
1169
|
+
|
|
1170
|
+
//#endregion
|
|
1171
|
+
//#region src/webhook/version.d.ts
|
|
1172
|
+
/**
|
|
1173
|
+
* Webhook API Version
|
|
1174
|
+
*
|
|
1175
|
+
* Single source of truth for the webhook version.
|
|
1176
|
+
* Update this file when bumping the API version.
|
|
1177
|
+
*/
|
|
1178
|
+
/**
|
|
1179
|
+
* The current webhook API version this SDK is built for.
|
|
1180
|
+
* Webhooks may be sent with different versions - the SDK accepts any valid
|
|
1181
|
+
* YYYY-MM-DD formatted version string. Compare against this constant if you
|
|
1182
|
+
* need to handle version-specific behavior.
|
|
1183
|
+
*/
|
|
1184
|
+
declare const WEBHOOK_VERSION = "2025-12-14";
|
|
1185
|
+
|
|
1186
|
+
//#endregion
|
|
1187
|
+
//#region src/webhook/index.d.ts
|
|
1188
|
+
/**
|
|
1189
|
+
* Valid webhook version format (YYYY-MM-DD date string).
|
|
1190
|
+
* The SDK accepts any valid date-formatted version, not just the current one,
|
|
1191
|
+
* for forward and backward compatibility.
|
|
1192
|
+
*/
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Parse a webhook payload, returning typed events for known types
|
|
1196
|
+
* and UnknownEvent for future event types.
|
|
1197
|
+
*
|
|
1198
|
+
* This provides forward-compatibility: when Primitive adds new event types,
|
|
1199
|
+
* your code won't break - you'll receive an UnknownEvent that you can
|
|
1200
|
+
* handle or ignore.
|
|
1201
|
+
*
|
|
1202
|
+
* Known event types are validated against the canonical schema. Unknown
|
|
1203
|
+
* event types are returned as-is for forward compatibility.
|
|
1204
|
+
*
|
|
1205
|
+
* For most use cases, prefer `handleWebhook()` which also verifies the
|
|
1206
|
+
* signature before parsing the payload.
|
|
1207
|
+
*
|
|
1208
|
+
* @param input - The parsed JSON payload
|
|
1209
|
+
* @returns Typed event for known types, UnknownEvent for unknown types
|
|
1210
|
+
* @throws WebhookPayloadError if the input is not a valid webhook structure
|
|
1211
|
+
* @throws WebhookValidationError if a known event fails schema validation
|
|
1212
|
+
*
|
|
1213
|
+
* @example
|
|
1214
|
+
* ```typescript
|
|
1215
|
+
* import { parseWebhookEvent } from '@primitivedotdev/sdk';
|
|
1216
|
+
*
|
|
1217
|
+
* const event = parseWebhookEvent(JSON.parse(rawBody));
|
|
1218
|
+
*
|
|
1219
|
+
* if (event.event === "email.received") {
|
|
1220
|
+
* // TypeScript knows this is EmailReceivedEvent
|
|
1221
|
+
* console.log(event.email.headers.subject);
|
|
1222
|
+
* } else {
|
|
1223
|
+
* // Handle or log unknown event types
|
|
1224
|
+
* console.log("Unknown event:", event.event);
|
|
1225
|
+
* }
|
|
1226
|
+
* ```
|
|
1227
|
+
*/
|
|
1228
|
+
declare function parseWebhookEvent(input: unknown): WebhookEvent;
|
|
1229
|
+
/**
|
|
1230
|
+
* Type guard to check if a webhook event is an EmailReceivedEvent.
|
|
1231
|
+
*
|
|
1232
|
+
* @example
|
|
1233
|
+
* ```typescript
|
|
1234
|
+
* const event = parseWebhookEvent(payload);
|
|
1235
|
+
* if (isEmailReceivedEvent(event)) {
|
|
1236
|
+
* // TypeScript knows event is EmailReceivedEvent
|
|
1237
|
+
* console.log(event.email.headers.subject);
|
|
1238
|
+
* }
|
|
1239
|
+
* ```
|
|
1240
|
+
*/
|
|
1241
|
+
declare function isEmailReceivedEvent(event: WebhookEvent | unknown): event is EmailReceivedEvent;
|
|
1242
|
+
/**
|
|
1243
|
+
* Request headers in any common format.
|
|
1244
|
+
*
|
|
1245
|
+
* Accepts:
|
|
1246
|
+
* - **Plain object** from Express/Node.js (`req.headers`)
|
|
1247
|
+
* - **Fetch API `Headers`** from Next.js App Router, Cloudflare Workers (`request.headers`)
|
|
1248
|
+
*
|
|
1249
|
+
* Header lookup is case-insensitive per RFC 7230.
|
|
1250
|
+
*/
|
|
1251
|
+
type WebhookHeaders = Record<string, string | string[] | undefined> | Headers;
|
|
1252
|
+
/**
|
|
1253
|
+
* Options for the handleWebhook function.
|
|
1254
|
+
*/
|
|
1255
|
+
interface HandleWebhookOptions {
|
|
1256
|
+
/**
|
|
1257
|
+
* The raw request body (before JSON parsing).
|
|
1258
|
+
* Must be the exact bytes received - do not re-serialize.
|
|
1259
|
+
*/
|
|
1260
|
+
body: string | Buffer;
|
|
1261
|
+
/**
|
|
1262
|
+
* The request headers object.
|
|
1263
|
+
* Works with Express (req.headers), Fetch API (Request.headers), or any
|
|
1264
|
+
* object with string keys. The SDK will find the Primitive-Signature header.
|
|
1265
|
+
*/
|
|
1266
|
+
headers: WebhookHeaders;
|
|
1267
|
+
/**
|
|
1268
|
+
* Your webhook secret from the Primitive dashboard.
|
|
1269
|
+
*/
|
|
1270
|
+
secret: string;
|
|
1271
|
+
/**
|
|
1272
|
+
* Maximum age of the webhook in seconds.
|
|
1273
|
+
* Webhooks older than this will be rejected as potential replay attacks.
|
|
1274
|
+
* @default 300 (5 minutes)
|
|
1275
|
+
*/
|
|
1276
|
+
toleranceSeconds?: number;
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* Verify, parse, and validate a webhook in one call.
|
|
1280
|
+
*
|
|
1281
|
+
* This is the recommended way to handle Primitive webhooks. It:
|
|
1282
|
+
* 1. Verifies the signature to ensure the webhook is authentic
|
|
1283
|
+
* 2. Parses the JSON body
|
|
1284
|
+
* 3. Validates the payload against the canonical JSON schema
|
|
1285
|
+
* 4. Returns a fully typed EmailReceivedEvent
|
|
1286
|
+
*
|
|
1287
|
+
* @param options - The webhook data and secret
|
|
1288
|
+
* @returns A validated EmailReceivedEvent
|
|
1289
|
+
* @throws {WebhookVerificationError} If signature verification fails
|
|
1290
|
+
* @throws {WebhookPayloadError} If JSON parsing fails
|
|
1291
|
+
* @throws {WebhookValidationError} If schema validation fails
|
|
1292
|
+
*
|
|
1293
|
+
* @example
|
|
1294
|
+
* ```typescript
|
|
1295
|
+
* import { handleWebhook, PrimitiveWebhookError } from '@primitivedotdev/sdk';
|
|
1296
|
+
*
|
|
1297
|
+
* app.post('/webhooks/email', express.raw({ type: 'application/json' }), (req, res) => {
|
|
1298
|
+
* try {
|
|
1299
|
+
* const event = handleWebhook({
|
|
1300
|
+
* body: req.body,
|
|
1301
|
+
* headers: req.headers,
|
|
1302
|
+
* secret: process.env.PRIMITIVE_WEBHOOK_SECRET,
|
|
1303
|
+
* });
|
|
1304
|
+
*
|
|
1305
|
+
* console.log('Email from:', event.email.headers.from);
|
|
1306
|
+
* res.json({ received: true });
|
|
1307
|
+
* } catch (err) {
|
|
1308
|
+
* if (err instanceof PrimitiveWebhookError) {
|
|
1309
|
+
* console.error(`[${err.code}] ${err.message}`);
|
|
1310
|
+
* return res.status(400).json({ error: err.code });
|
|
1311
|
+
* }
|
|
1312
|
+
* throw err;
|
|
1313
|
+
* }
|
|
1314
|
+
* });
|
|
1315
|
+
* ```
|
|
1316
|
+
*/
|
|
1317
|
+
declare function handleWebhook(options: HandleWebhookOptions): EmailReceivedEvent;
|
|
1318
|
+
/**
|
|
1319
|
+
* Returns headers for the optional "content discard" feature.
|
|
1320
|
+
*
|
|
1321
|
+
* If you have the "content discard" setting enabled in your Primitive dashboard,
|
|
1322
|
+
* returning this header tells Primitive to permanently delete the email content
|
|
1323
|
+
* after successful delivery. Requires BOTH the dashboard setting AND this header.
|
|
1324
|
+
*
|
|
1325
|
+
* **Warning:** Only use this if you can durably guarantee you've processed the email.
|
|
1326
|
+
* Once discarded, the email content is gone forever.
|
|
1327
|
+
*
|
|
1328
|
+
* @returns Headers object to spread into your response
|
|
1329
|
+
*
|
|
1330
|
+
* @example Express (only if using content discard)
|
|
1331
|
+
* ```typescript
|
|
1332
|
+
* app.post('/webhook', async (req, res) => {
|
|
1333
|
+
* const event = handleWebhook({ ... });
|
|
1334
|
+
* // Durably save the email first!
|
|
1335
|
+
* await db.saveEmail(event);
|
|
1336
|
+
* res.set(confirmedHeaders()).json({ received: true });
|
|
1337
|
+
* });
|
|
1338
|
+
* ```
|
|
1339
|
+
*
|
|
1340
|
+
* @example Fetch API / Next.js (only if using content discard)
|
|
1341
|
+
* ```typescript
|
|
1342
|
+
* return new Response(JSON.stringify({ received: true }), {
|
|
1343
|
+
* status: 200,
|
|
1344
|
+
* headers: {
|
|
1345
|
+
* 'Content-Type': 'application/json',
|
|
1346
|
+
* ...confirmedHeaders(),
|
|
1347
|
+
* },
|
|
1348
|
+
* });
|
|
1349
|
+
* ```
|
|
1350
|
+
*/
|
|
1351
|
+
declare function confirmedHeaders(): {
|
|
1352
|
+
"X-Primitive-Confirmed": "true";
|
|
1353
|
+
};
|
|
1354
|
+
/**
|
|
1355
|
+
* Check if the download URL for a webhook event has expired.
|
|
1356
|
+
*
|
|
1357
|
+
* @param event - The webhook event
|
|
1358
|
+
* @param now - Optional current time for testing (defaults to Date.now())
|
|
1359
|
+
* @returns true if the download URL has expired
|
|
1360
|
+
*
|
|
1361
|
+
* @example
|
|
1362
|
+
* ```typescript
|
|
1363
|
+
* if (isDownloadExpired(event)) {
|
|
1364
|
+
* console.log("Download URL has expired, cannot fetch raw email");
|
|
1365
|
+
* } else {
|
|
1366
|
+
* const response = await fetch(event.email.content.download.url);
|
|
1367
|
+
* }
|
|
1368
|
+
* ```
|
|
1369
|
+
*/
|
|
1370
|
+
declare function isDownloadExpired(event: EmailReceivedEvent, now?: number): boolean;
|
|
1371
|
+
/**
|
|
1372
|
+
* Get the time remaining (in milliseconds) before the download URL expires.
|
|
1373
|
+
* Returns 0 if already expired.
|
|
1374
|
+
*
|
|
1375
|
+
* @param event - The webhook event
|
|
1376
|
+
* @param now - Optional current time for testing (defaults to Date.now())
|
|
1377
|
+
* @returns Milliseconds until expiration, or 0 if expired
|
|
1378
|
+
*
|
|
1379
|
+
* @example
|
|
1380
|
+
* ```typescript
|
|
1381
|
+
* const remaining = getDownloadTimeRemaining(event);
|
|
1382
|
+
* if (remaining > 60000) {
|
|
1383
|
+
* // More than 1 minute left, safe to download
|
|
1384
|
+
* }
|
|
1385
|
+
* ```
|
|
1386
|
+
*/
|
|
1387
|
+
declare function getDownloadTimeRemaining(event: EmailReceivedEvent, now?: number): number;
|
|
1388
|
+
/**
|
|
1389
|
+
* Check if the raw email content is included inline in the event.
|
|
1390
|
+
*
|
|
1391
|
+
* Use this to check before calling `decodeRawEmail()` to avoid try/catch.
|
|
1392
|
+
*
|
|
1393
|
+
* @param event - The webhook event
|
|
1394
|
+
* @returns true if raw content is included inline, false if download required
|
|
1395
|
+
*
|
|
1396
|
+
* @example
|
|
1397
|
+
* ```typescript
|
|
1398
|
+
* if (isRawIncluded(event)) {
|
|
1399
|
+
* const rawEmail = decodeRawEmail(event);
|
|
1400
|
+
* } else {
|
|
1401
|
+
* const response = await fetch(event.email.content.download.url);
|
|
1402
|
+
* }
|
|
1403
|
+
* ```
|
|
1404
|
+
*/
|
|
1405
|
+
declare function isRawIncluded(event: EmailReceivedEvent): boolean;
|
|
1406
|
+
/**
|
|
1407
|
+
* Options for decoding raw email content.
|
|
1408
|
+
*/
|
|
1409
|
+
interface DecodeRawEmailOptions {
|
|
1410
|
+
/**
|
|
1411
|
+
* Whether to verify the SHA-256 hash after decoding.
|
|
1412
|
+
* @default true
|
|
1413
|
+
*/
|
|
1414
|
+
verify?: boolean;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Decode the raw email content from an EmailReceivedEvent.
|
|
1418
|
+
*
|
|
1419
|
+
* Throws if the raw content is not included inline (i.e., must be downloaded).
|
|
1420
|
+
* By default, verifies the SHA-256 hash matches after decoding.
|
|
1421
|
+
*
|
|
1422
|
+
* NOTE: This function assumes a well-formed event from `handleWebhook()`.
|
|
1423
|
+
* Passing a manually constructed event with missing fields (e.g., `raw.data`
|
|
1424
|
+
* undefined when `raw.included` is true) will result in undefined behavior.
|
|
1425
|
+
*
|
|
1426
|
+
* @param event - The webhook event containing the raw email
|
|
1427
|
+
* @param options - Decoding options
|
|
1428
|
+
* @returns The decoded raw email as a Buffer
|
|
1429
|
+
* @throws {RawEmailDecodeError} If content not included or hash mismatch
|
|
1430
|
+
*
|
|
1431
|
+
* @example
|
|
1432
|
+
* ```typescript
|
|
1433
|
+
* import { handleWebhook, decodeRawEmail, isRawIncluded } from '@primitivedotdev/sdk';
|
|
1434
|
+
*
|
|
1435
|
+
* const event = handleWebhook({ body, headers, secret });
|
|
1436
|
+
*
|
|
1437
|
+
* if (isRawIncluded(event)) {
|
|
1438
|
+
* const rawEmail = decodeRawEmail(event);
|
|
1439
|
+
* // rawEmail is a Buffer containing the RFC 5322 email
|
|
1440
|
+
* } else {
|
|
1441
|
+
* // Must download from event.email.content.download.url
|
|
1442
|
+
* }
|
|
1443
|
+
* ```
|
|
1444
|
+
*/
|
|
1445
|
+
declare function decodeRawEmail(event: EmailReceivedEvent, options?: DecodeRawEmailOptions): Buffer;
|
|
1446
|
+
/**
|
|
1447
|
+
* Verify downloaded raw email content against the SHA-256 hash in the event.
|
|
1448
|
+
*
|
|
1449
|
+
* Use this after fetching from `event.email.content.download.url` to ensure
|
|
1450
|
+
* the downloaded content matches what Primitive received.
|
|
1451
|
+
*
|
|
1452
|
+
* @param downloaded - The downloaded raw email content (Buffer, ArrayBuffer, or Uint8Array)
|
|
1453
|
+
* @param event - The webhook event containing the expected hash
|
|
1454
|
+
* @returns The verified content as a Buffer
|
|
1455
|
+
* @throws {RawEmailDecodeError} If hash doesn't match
|
|
1456
|
+
*
|
|
1457
|
+
* @example
|
|
1458
|
+
* ```typescript
|
|
1459
|
+
* import { handleWebhook, verifyRawEmailDownload, isRawIncluded } from '@primitivedotdev/sdk';
|
|
1460
|
+
*
|
|
1461
|
+
* const event = handleWebhook({ body, headers, secret });
|
|
1462
|
+
*
|
|
1463
|
+
* if (!isRawIncluded(event)) {
|
|
1464
|
+
* const response = await fetch(event.email.content.download.url);
|
|
1465
|
+
* const arrayBuffer = await response.arrayBuffer();
|
|
1466
|
+
* const verified = verifyRawEmailDownload(arrayBuffer, event);
|
|
1467
|
+
* // verified is a Buffer containing the RFC 5322 email
|
|
1468
|
+
* }
|
|
1469
|
+
* ```
|
|
1470
|
+
*/
|
|
1471
|
+
declare function verifyRawEmailDownload(downloaded: Buffer | ArrayBuffer | Uint8Array, event: EmailReceivedEvent): Buffer;
|
|
1472
|
+
|
|
1473
|
+
//#endregion
|
|
1474
|
+
export { DecodeRawEmailOptions, HandleWebhookOptions, PAYLOAD_ERRORS as PAYLOAD_ERRORS$1, PRIMITIVE_CONFIRMED_HEADER as PRIMITIVE_CONFIRMED_HEADER$1, PRIMITIVE_SIGNATURE_HEADER as PRIMITIVE_SIGNATURE_HEADER$1, PrimitiveWebhookError as PrimitiveWebhookError$1, RAW_EMAIL_ERRORS as RAW_EMAIL_ERRORS$1, RawEmailDecodeError as RawEmailDecodeError$1, RawEmailDecodeErrorCode, SignResult, VERIFICATION_ERRORS as VERIFICATION_ERRORS$1, VerifyOptions, WEBHOOK_VERSION as WEBHOOK_VERSION$1, WebhookErrorCode, WebhookHeaders, WebhookPayloadError as WebhookPayloadError$1, WebhookPayloadErrorCode, WebhookValidationError as WebhookValidationError$1, WebhookValidationErrorCode, WebhookVerificationError as WebhookVerificationError$1, WebhookVerificationErrorCode, confirmedHeaders as confirmedHeaders$1, decodeRawEmail as decodeRawEmail$1, emailReceivedEventJsonSchema as emailReceivedEventJsonSchema$1, getDownloadTimeRemaining as getDownloadTimeRemaining$1, handleWebhook as handleWebhook$1, isDownloadExpired as isDownloadExpired$1, isEmailReceivedEvent as isEmailReceivedEvent$1, isRawIncluded as isRawIncluded$1, parseWebhookEvent as parseWebhookEvent$1, safeValidateEmailReceivedEvent as safeValidateEmailReceivedEvent$1, signWebhookPayload as signWebhookPayload$1, validateEmailAuth as validateEmailAuth$1, validateEmailReceivedEvent as validateEmailReceivedEvent$1, verifyRawEmailDownload as verifyRawEmailDownload$1, verifyWebhookSignature as verifyWebhookSignature$1 };
|