@primitivedotdev/sdk 1.26.0 → 1.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -71,8 +71,8 @@
71
71
  "test:coverage": "vitest run --coverage",
72
72
  "test:watch": "vitest",
73
73
  "typecheck": "pnpm generate && tsc --noEmit -p tsconfig.typecheck.json",
74
- "lint": "biome check --error-on-warnings src/index.ts src/validation.ts src/types.ts src/webhook src/contract src/parser src/api/index.ts src/x402 src/openapi/index.ts src/payloads src/interactions tests/",
75
- "lint:fix": "biome check --write --error-on-warnings src/index.ts src/validation.ts src/types.ts src/webhook src/contract src/parser src/api/index.ts src/x402 src/openapi/index.ts src/payloads src/interactions tests/",
74
+ "lint": "biome check --error-on-warnings src/index.ts src/validation.ts src/types.ts src/webhook src/contract src/parser src/api/index.ts src/api/events.ts src/api/event-transport.ts src/x402 src/openapi/index.ts src/payloads src/interactions tests/",
75
+ "lint:fix": "biome check --write --error-on-warnings src/index.ts src/validation.ts src/types.ts src/webhook src/contract src/parser src/api/index.ts src/api/events.ts src/api/event-transport.ts src/x402 src/openapi/index.ts src/payloads src/interactions tests/",
76
76
  "prepublishOnly": "pnpm build"
77
77
  },
78
78
  "keywords": [
@@ -1,673 +0,0 @@
1
- import { n as parseFromHeaderLoose, t as parseFromHeader } from "./address-parser-C2JJzLdA.js";
2
- import isEmail from "validator/lib/isEmail.js";
3
- //#region src/webhook/received-email.ts
4
- const REPLY_PREFIX_RE = /^re\s*:/i;
5
- const FORWARD_PREFIX_RE = /^(fwd?|fw)\s*:/i;
6
- function normalizeReceivedEmail(event) {
7
- const receivedBy = event.email.smtp.rcpt_to[0];
8
- if (!receivedBy) throw new Error("email.smtp.rcpt_to must contain at least one recipient");
9
- const sender = parseHeaderAddress(event.email.headers.from) ?? {
10
- address: event.email.smtp.mail_from.trim().toLowerCase(),
11
- name: null
12
- };
13
- const replyTarget = firstStructuredAddress(event.email.parsed.reply_to) ?? sender;
14
- const subject = event.email.headers.subject ?? null;
15
- const references = event.email.parsed.references ?? [];
16
- const messageId = event.email.headers.message_id ?? null;
17
- return {
18
- id: event.email.id,
19
- eventId: event.id,
20
- receivedAt: event.email.received_at,
21
- sender,
22
- replyTarget,
23
- receivedBy,
24
- receivedByAll: [...event.email.smtp.rcpt_to],
25
- subject,
26
- replySubject: buildReplySubject(subject),
27
- forwardSubject: buildForwardSubject(subject),
28
- text: event.email.parsed.body_text ?? null,
29
- thread: {
30
- messageId,
31
- inReplyTo: event.email.parsed.in_reply_to ?? [],
32
- references
33
- },
34
- attachments: event.email.parsed.attachments ?? [],
35
- auth: event.email.auth,
36
- analysis: event.email.analysis,
37
- raw: event
38
- };
39
- }
40
- function buildReplySubject(subject) {
41
- const trimmed = subject?.trim() ?? "";
42
- if (trimmed.length === 0) return "Re:";
43
- return REPLY_PREFIX_RE.test(trimmed) ? trimmed : `Re: ${trimmed}`;
44
- }
45
- function buildForwardSubject(subject) {
46
- const trimmed = subject?.trim() ?? "";
47
- if (trimmed.length === 0) return "Fwd:";
48
- return FORWARD_PREFIX_RE.test(trimmed) ? trimmed : `Fwd: ${trimmed}`;
49
- }
50
- function formatAddress(address) {
51
- return address.name ? `${address.name} <${address.address}>` : address.address;
52
- }
53
- function firstStructuredAddress(addresses) {
54
- const address = addresses?.[0];
55
- if (!address) return null;
56
- return {
57
- address: address.address.trim().toLowerCase(),
58
- name: address.name ?? null
59
- };
60
- }
61
- function parseHeaderAddress(value) {
62
- const parsed = parseFromHeaderLoose(value);
63
- if (!parsed) return null;
64
- return {
65
- address: parsed.address,
66
- name: parsed.name?.trim() || null
67
- };
68
- }
69
- //#endregion
70
- //#region src/types.ts
71
- const EventType = {
72
- EmailReceived: "email.received",
73
- EmailBounced: "email.bounced",
74
- EmailTlsReport: "email.tls_report",
75
- EmailDmarcReport: "email.dmarc_report",
76
- EmailDmarcFailure: "email.dmarc_failure"
77
- };
78
- const ParsedStatus = {
79
- Complete: "complete",
80
- Failed: "failed"
81
- };
82
- const ForwardVerdict = {
83
- Legit: "legit",
84
- Unknown: "unknown"
85
- };
86
- const SpfResult = {
87
- Pass: "pass",
88
- Fail: "fail",
89
- Softfail: "softfail",
90
- Neutral: "neutral",
91
- None: "none",
92
- Temperror: "temperror",
93
- Permerror: "permerror"
94
- };
95
- const DmarcResult = {
96
- Pass: "pass",
97
- Fail: "fail",
98
- None: "none",
99
- Temperror: "temperror",
100
- Permerror: "permerror"
101
- };
102
- const DmarcPolicy = {
103
- Reject: "reject",
104
- Quarantine: "quarantine",
105
- None: "none"
106
- };
107
- const DkimResult = {
108
- Pass: "pass",
109
- Fail: "fail",
110
- Temperror: "temperror",
111
- Permerror: "permerror"
112
- };
113
- const AuthConfidence = {
114
- High: "high",
115
- Medium: "medium",
116
- Low: "low"
117
- };
118
- const AuthVerdict = {
119
- Legit: "legit",
120
- Suspicious: "suspicious",
121
- Unknown: "unknown"
122
- };
123
- //#endregion
124
- //#region src/webhook/auth.ts
125
- /**
126
- * Minimum DKIM key size considered acceptable.
127
- *
128
- * 1024-bit RSA keys are cryptographically weak by modern standards (NIST
129
- * deprecated them in 2013), but they remain extremely common in email due to:
130
- * - DNS TXT record size limits (255 bytes per string)
131
- * - Legacy infrastructure constraints
132
- * - Major ESPs like Amazon SES and Resend still use 1024-bit keys
133
- *
134
- * We flag keys <1024 bits as weak (these are truly dangerous), while accepting
135
- * >=1024 bits to avoid false positives against legitimate senders. For maximum
136
- * security, domain owners should use 2048+ bit keys where possible.
137
- */
138
- const MIN_SECURE_KEY_BITS = 1024;
139
- /**
140
- * Validate email authentication and compute a verdict.
141
- *
142
- * This function analyzes SPF, DKIM, and DMARC results to determine
143
- * whether an email is likely authentic ("legit"), potentially spoofed
144
- * ("suspicious"), or indeterminate ("unknown").
145
- *
146
- * ## Verdict Logic
147
- *
148
- * **Legit (high confidence):**
149
- * - DMARC pass with DKIM alignment (cryptographic proof of authenticity)
150
- *
151
- * **Legit (medium confidence):**
152
- * - DMARC pass with SPF alignment only (no DKIM)
153
- * - Note: SPF can break through forwarding, but DMARC pass is still meaningful
154
- *
155
- * **Suspicious (high confidence):**
156
- * - DMARC fail when domain has `reject` or `quarantine` policy
157
- * - The domain owner explicitly says to distrust failing emails
158
- * - SPF explicitly fails (IP not authorized by sender)
159
- *
160
- * **Suspicious (low confidence):**
161
- * - DMARC fail when domain has `none` policy (monitoring mode)
162
- * - No DMARC record but SPF/DKIM fail
163
- *
164
- * **Unknown:**
165
- * - No DMARC record and no clear pass/fail
166
- * - Temporary errors during authentication
167
- * - No authentication data available
168
- *
169
- * A `legit` verdict means the email authenticated as its own From
170
- * domain, not as any particular domain you trust. For authorization
171
- * decisions, pair the verdict with a domain anchor via
172
- * {@link isTrustedSender} instead of checking the verdict alone.
173
- *
174
- * @param auth - Email authentication results from the webhook
175
- * @returns Verdict, confidence level, and explanatory reasons
176
- *
177
- * @example
178
- * ```typescript
179
- * const result = validateEmailAuth({
180
- * spf: 'pass',
181
- * dmarc: 'pass',
182
- * dmarcPolicy: 'reject',
183
- * dmarcFromDomain: 'example.com',
184
- * dmarcSpfAligned: true,
185
- * dmarcDkimAligned: true,
186
- * dmarcSpfStrict: false,
187
- * dmarcDkimStrict: false,
188
- * dkimSignatures: [{
189
- * domain: 'example.com',
190
- * selector: 'default',
191
- * result: 'pass',
192
- * aligned: true,
193
- * keyBits: 2048,
194
- * algo: 'rsa-sha256',
195
- * }],
196
- * });
197
- *
198
- * // result.verdict === 'legit'
199
- * // result.confidence === 'high'
200
- * // result.reasons === ['DMARC passed with DKIM alignment']
201
- * ```
202
- */
203
- function validateEmailAuth(auth) {
204
- const reasons = [];
205
- let verdict;
206
- let confidence;
207
- if (auth.dmarc === "temperror" || auth.dmarc === "permerror") return {
208
- verdict: "unknown",
209
- confidence: "low",
210
- reasons: [`DMARC verification error (${auth.dmarc})`, "Cannot determine email authenticity due to DNS or policy errors"]
211
- };
212
- if (auth.spf === "temperror" || auth.spf === "permerror") reasons.push(`SPF verification error (${auth.spf})`);
213
- const weakKeySignatures = auth.dkimSignatures.filter((sig) => sig.keyBits != null && sig.keyBits < MIN_SECURE_KEY_BITS);
214
- if (weakKeySignatures.length > 0) for (const sig of weakKeySignatures) reasons.push(`Weak DKIM key (${sig.keyBits} bits) for ${sig.domain} - minimum ${MIN_SECURE_KEY_BITS} bits recommended`);
215
- if (auth.dmarc === "pass") {
216
- const alignedSigs = auth.dkimSignatures.filter((sig) => sig.result === "pass" && sig.aligned);
217
- if (auth.dmarcDkimAligned && alignedSigs.length > 0) {
218
- const domains = alignedSigs.map((sig) => sig.domain).join(", ");
219
- reasons.unshift(`DMARC passed with DKIM alignment (${domains})`);
220
- verdict = "legit";
221
- confidence = weakKeySignatures.length > 0 ? "medium" : "high";
222
- return {
223
- verdict,
224
- confidence,
225
- reasons
226
- };
227
- }
228
- if (auth.dmarcSpfAligned && auth.spf === "pass") {
229
- reasons.unshift("DMARC passed with SPF alignment");
230
- reasons.push("No aligned DKIM signature (SPF can break through forwarding)");
231
- return {
232
- verdict: "legit",
233
- confidence: "medium",
234
- reasons
235
- };
236
- }
237
- reasons.unshift("DMARC passed");
238
- return {
239
- verdict: "legit",
240
- confidence: "medium",
241
- reasons
242
- };
243
- }
244
- if (auth.dmarc === "fail") {
245
- if (auth.dmarcPolicy === "reject") {
246
- reasons.unshift("DMARC failed and domain has reject policy");
247
- reasons.push("The sender's domain explicitly rejects emails that fail authentication");
248
- return {
249
- verdict: "suspicious",
250
- confidence: "high",
251
- reasons
252
- };
253
- }
254
- if (auth.dmarcPolicy === "quarantine") {
255
- reasons.unshift("DMARC failed and domain has quarantine policy");
256
- reasons.push("The sender's domain marks failing emails as suspicious");
257
- return {
258
- verdict: "suspicious",
259
- confidence: "high",
260
- reasons
261
- };
262
- }
263
- reasons.unshift("DMARC failed (domain is in monitoring mode)");
264
- if (auth.spf === "fail") {
265
- reasons.push("SPF failed - sending IP not authorized");
266
- return {
267
- verdict: "suspicious",
268
- confidence: "medium",
269
- reasons
270
- };
271
- }
272
- return {
273
- verdict: "suspicious",
274
- confidence: "low",
275
- reasons
276
- };
277
- }
278
- if (auth.dmarc === "none") {
279
- if (auth.spf === "fail") {
280
- reasons.push("No DMARC record for sender domain");
281
- reasons.push("SPF failed - sending IP not authorized");
282
- return {
283
- verdict: "suspicious",
284
- confidence: "medium",
285
- reasons
286
- };
287
- }
288
- const passingDkim = auth.dkimSignatures.filter((sig) => sig.result === "pass");
289
- if (passingDkim.length > 0) {
290
- const domains = passingDkim.map((sig) => sig.domain).join(", ");
291
- reasons.push("No DMARC record for sender domain");
292
- reasons.push(`DKIM verified for: ${domains}`);
293
- if (auth.spf === "pass") reasons.push("SPF passed");
294
- return {
295
- verdict: "unknown",
296
- confidence: "low",
297
- reasons
298
- };
299
- }
300
- if (auth.spf === "pass") {
301
- reasons.push("No DMARC record for sender domain");
302
- reasons.push("No DKIM signatures present");
303
- reasons.push("SPF passed (but SPF alone is weak authentication)");
304
- return {
305
- verdict: "unknown",
306
- confidence: "low",
307
- reasons
308
- };
309
- }
310
- reasons.push("No DMARC record for sender domain");
311
- reasons.push("No valid authentication found");
312
- return {
313
- verdict: "unknown",
314
- confidence: "low",
315
- reasons
316
- };
317
- }
318
- return {
319
- verdict: "unknown",
320
- confidence: "low",
321
- reasons: ["Unable to determine email authenticity"]
322
- };
323
- }
324
- //#endregion
325
- //#region src/webhook/trust.ts
326
- /**
327
- * Domain-Anchored Sender Trust
328
- *
329
- * `validateEmailAuth()` answers "was this email authenticated?" but not
330
- * "authenticated AS WHOM?". A fully authenticated email from any domain
331
- * returns a `legit` verdict, so a verdict check alone cannot gate actions
332
- * on "this really came from our domain". `isTrustedSender()` closes that
333
- * gap by anchoring the verdict to an expected From domain (and optionally
334
- * an exact sender address).
335
- *
336
- * @example
337
- * ```typescript
338
- * import { isTrustedSender } from '@primitivedotdev/sdk/api';
339
- *
340
- * const trust = isTrustedSender(event, { domain: 'example.com' });
341
- * if (trust.trusted) {
342
- * // Authenticated mail whose From domain is example.com
343
- * } else if (trust.retryable) {
344
- * // Transient DNS failure during DMARC evaluation; return a 5xx so
345
- * // webhook redelivery retries with fresh DNS.
346
- * } else {
347
- * console.warn('Untrusted email:', trust.reason, trust.auth.reasons);
348
- * }
349
- * ```
350
- *
351
- * @packageDocumentation
352
- */
353
- const SENDER_OPTION_EMAIL_OPTIONS = {
354
- allow_ip_domain: true,
355
- require_tld: true,
356
- allow_display_name: false,
357
- allow_utf8_local_part: true
358
- };
359
- function untrusted(reason, auth, retryable = false) {
360
- return {
361
- trusted: false,
362
- retryable,
363
- reason,
364
- auth
365
- };
366
- }
367
- /**
368
- * Check whether an inbound email is authenticated as an expected domain
369
- * (and optionally an exact sender address).
370
- *
371
- * `trusted` is true only when ALL of the following hold:
372
- *
373
- * 1. `validateEmailAuth(event.email.auth)` returns a `legit` verdict.
374
- * 2. `event.email.auth.dmarcFromDomain` (the domain the server's DMARC
375
- * evaluation ran against) equals `options.domain`.
376
- * 3. The From header strict-parses to exactly one valid address whose
377
- * domain equals `options.domain`.
378
- * 4. When `options.sender` is given, the parsed From address equals it
379
- * exactly (case-insensitive).
380
- *
381
- * ## Why the extra checks beyond the verdict
382
- *
383
- * The verdict alone says an email was authenticated, not which domain
384
- * it was authenticated as: a fully authenticated email from an
385
- * attacker-controlled domain is `legit`. Anchoring `dmarcFromDomain`
386
- * closes that. The strict From parse defends the remaining gaps:
387
- *
388
- * - Naively regexing the raw From header is unsafe. A header like
389
- * `From: "trusted@example.com" <x@evil.com>` plants an allowlisted
390
- * address in the display name while DMARC evaluates (and passes for)
391
- * `evil.com`. The strict parser extracts only the real addr-spec and
392
- * rejects multi-address and group forms outright.
393
- * - `normalizeReceivedEmail().sender` is NOT a safe anchor for
394
- * authorization: it uses a lenient parser and falls back to the SMTP
395
- * envelope sender (`smtp.mail_from`), which the sender fully
396
- * controls. The same goes for Reply-To (`replyTarget`). This function
397
- * never consults either.
398
- * - An `unknown` verdict is not one thing: a DMARC temperror is
399
- * transient (surfaced as `retryable: true`, respond 5xx and let
400
- * webhook redelivery retry), while "no DMARC record" is permanent
401
- * for the email and surfaced as non-retryable.
402
- *
403
- * Never throws for malformed event content; malformed input yields an
404
- * untrusted result with a reason. Throws `TypeError` only for invalid
405
- * `options` (programmer error).
406
- *
407
- * @param event - The verified `email.received` webhook event
408
- * @param options - Expected domain and optional exact sender
409
- * @returns Trust decision with a stable reason code and the underlying
410
- * auth result
411
- */
412
- function isTrustedSender(event, options) {
413
- const { domain, sender } = normalizeOptions(options);
414
- const auth = event?.email?.auth;
415
- if (auth === null || typeof auth !== "object" || !Array.isArray(auth.dkimSignatures)) return untrusted("auth-missing", {
416
- verdict: "unknown",
417
- confidence: "low",
418
- reasons: ["Missing or malformed email.auth on event"]
419
- });
420
- const authResult = validateEmailAuth(auth);
421
- if (authResult.verdict === "suspicious") return untrusted("auth-suspicious", authResult);
422
- if (authResult.verdict === "unknown") {
423
- if (auth.dmarc === "temperror") return untrusted("dmarc-temperror", authResult, true);
424
- return untrusted("auth-unknown", authResult);
425
- }
426
- const dmarcFromDomain = typeof auth.dmarcFromDomain === "string" ? auth.dmarcFromDomain.trim().toLowerCase() : "";
427
- if (dmarcFromDomain === "" || dmarcFromDomain !== domain) return untrusted("dmarc-domain-mismatch", authResult);
428
- const parsed = parseFromHeader(event.email?.headers?.from);
429
- if (!parsed.ok) return untrusted(parsed.reason === "multiple_addresses" ? "from-header-multiple-addresses" : "from-header-invalid", authResult);
430
- const fromAddress = parsed.value.address;
431
- if (fromAddress.slice(fromAddress.lastIndexOf("@") + 1) !== domain) return untrusted("from-domain-mismatch", authResult);
432
- if (sender !== void 0 && fromAddress !== sender) return untrusted("sender-mismatch", authResult);
433
- return {
434
- trusted: true,
435
- retryable: false,
436
- reason: "trusted",
437
- auth: authResult
438
- };
439
- }
440
- function normalizeOptions(options) {
441
- if (typeof options?.domain !== "string") throw new TypeError("options.domain is required");
442
- const domain = options.domain.trim().toLowerCase();
443
- if (domain.length === 0) throw new TypeError("options.domain must be a non-empty domain name");
444
- if (domain.includes("@") || /\s/.test(domain)) throw new TypeError("options.domain must be a bare domain name without @ or whitespace");
445
- if (options.sender === void 0) return { domain };
446
- if (typeof options.sender !== "string") throw new TypeError("options.sender must be a string when provided");
447
- const sender = options.sender.trim().toLowerCase();
448
- if (!isEmail(sender, SENDER_OPTION_EMAIL_OPTIONS)) throw new TypeError("options.sender must be a single bare email address (user@example.com)");
449
- return {
450
- domain,
451
- sender
452
- };
453
- }
454
- //#endregion
455
- //#region src/webhook/errors.ts
456
- /**
457
- * Verification error definitions.
458
- * Use these for documentation, dashboards, and i18n.
459
- */
460
- const VERIFICATION_ERRORS = {
461
- INVALID_SIGNATURE_HEADER: {
462
- message: "Missing or malformed Primitive-Signature header",
463
- suggestion: "Check that you're reading the correct header (Primitive-Signature) and it's being passed correctly from your web framework."
464
- },
465
- TIMESTAMP_OUT_OF_RANGE: {
466
- message: "Timestamp is too old (possible replay attack)",
467
- suggestion: "This could indicate a replay attack, network delay, or server clock drift. Check your server's time is synced."
468
- },
469
- SIGNATURE_MISMATCH: {
470
- message: "Signature doesn't match expected value",
471
- suggestion: "Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON)."
472
- },
473
- MISSING_SECRET: {
474
- message: "No webhook secret was provided",
475
- suggestion: "Pass your webhook secret from the Primitive dashboard. Check that the environment variable is set."
476
- }
477
- };
478
- /**
479
- * Payload parsing error definitions.
480
- * Use these for documentation, dashboards, and i18n.
481
- */
482
- const PAYLOAD_ERRORS = {
483
- PAYLOAD_NULL: {
484
- message: "Webhook payload is null",
485
- suggestion: "Ensure you're passing the parsed JSON body, not null. Check your framework's body parsing middleware."
486
- },
487
- PAYLOAD_UNDEFINED: {
488
- message: "Webhook payload is undefined",
489
- suggestion: "The payload was not provided. Make sure you're passing the request body to the handler."
490
- },
491
- PAYLOAD_WRONG_TYPE: {
492
- message: "Webhook payload must be an object",
493
- suggestion: "The payload should be a parsed JSON object. Check that you're not passing a string or other primitive."
494
- },
495
- PAYLOAD_IS_ARRAY: {
496
- message: "Webhook payload is an array, expected object",
497
- suggestion: "Primitive webhooks are single event objects, not arrays. Check the payload structure."
498
- },
499
- PAYLOAD_MISSING_EVENT: {
500
- message: "Webhook payload missing 'event' field",
501
- suggestion: "All webhook payloads must have an 'event' field. This may not be a valid Primitive webhook."
502
- },
503
- PAYLOAD_UNKNOWN_EVENT: {
504
- message: "Unknown webhook event type",
505
- suggestion: "This event type is not recognized. You may need to update your SDK or handle unknown events gracefully."
506
- },
507
- PAYLOAD_EMPTY_BODY: {
508
- message: "Request body is empty",
509
- suggestion: "The request body was empty. Ensure the webhook is sending data and your framework is parsing it correctly."
510
- },
511
- JSON_PARSE_FAILED: {
512
- message: "Failed to parse JSON body",
513
- suggestion: "The request body is not valid JSON. Check the raw body content and Content-Type header."
514
- },
515
- INVALID_ENCODING: {
516
- message: "Invalid body encoding",
517
- suggestion: "The request body encoding is not supported. Primitive webhooks use UTF-8 encoded JSON."
518
- }
519
- };
520
- /**
521
- * Raw email decode error definitions.
522
- * Use these for documentation, dashboards, and i18n.
523
- */
524
- const RAW_EMAIL_ERRORS = {
525
- NOT_INCLUDED: {
526
- message: "Raw email content not included inline",
527
- suggestion: "Use the download URL at event.email.content.download.url to fetch the raw email."
528
- },
529
- INVALID_BASE64: {
530
- message: "Raw email content is not valid base64",
531
- suggestion: "The raw email data is malformed. Fetch the raw email from the download URL or regenerate the webhook payload."
532
- },
533
- HASH_MISMATCH: {
534
- message: "SHA-256 hash verification failed",
535
- suggestion: "The raw email data may be corrupted. Try downloading from the URL instead."
536
- }
537
- };
538
- /**
539
- * Base class for all Primitive webhook errors.
540
- *
541
- * Catch this to handle any error from the SDK in a single catch block.
542
- *
543
- * @example
544
- * ```typescript
545
- * import { handleWebhook, PrimitiveWebhookError } from '@primitivedotdev/sdk';
546
- *
547
- * try {
548
- * const event = handleWebhook({ body, headers, secret });
549
- * } catch (err) {
550
- * if (err instanceof PrimitiveWebhookError) {
551
- * console.error(`[${err.code}] ${err.message}`);
552
- * return res.status(400).json({ error: err.code });
553
- * }
554
- * throw err;
555
- * }
556
- * ```
557
- */
558
- var PrimitiveWebhookError = class extends Error {
559
- /**
560
- * Formats the error for logging/display.
561
- */
562
- toString() {
563
- return `${this.name} [${this.code}]: ${this.message}\n\nSuggestion: ${this.suggestion}`;
564
- }
565
- /**
566
- * Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
567
- */
568
- toJSON() {
569
- return {
570
- name: this.name,
571
- code: this.code,
572
- message: this.message,
573
- suggestion: this.suggestion
574
- };
575
- }
576
- };
577
- /**
578
- * Error thrown when webhook signature verification fails.
579
- *
580
- * Use the `code` property to programmatically handle specific error cases.
581
- */
582
- var WebhookVerificationError = class extends PrimitiveWebhookError {
583
- code;
584
- suggestion;
585
- constructor(code, message, suggestion) {
586
- super(message ?? VERIFICATION_ERRORS[code].message);
587
- this.name = "WebhookVerificationError";
588
- this.code = code;
589
- this.suggestion = suggestion ?? VERIFICATION_ERRORS[code].suggestion;
590
- }
591
- };
592
- /**
593
- * Error thrown when webhook payload parsing fails (lightweight parser).
594
- *
595
- * Use the `code` property for programmatic handling and monitoring.
596
- * The `suggestion` property contains actionable guidance for fixing the issue.
597
- */
598
- var WebhookPayloadError = class extends PrimitiveWebhookError {
599
- code;
600
- suggestion;
601
- /** Original error if this wraps another error (e.g., JSON.parse failure) */
602
- cause;
603
- constructor(code, message, suggestion, cause) {
604
- super(message ?? PAYLOAD_ERRORS[code].message);
605
- this.name = "WebhookPayloadError";
606
- this.code = code;
607
- this.suggestion = suggestion ?? PAYLOAD_ERRORS[code].suggestion;
608
- this.cause = cause;
609
- }
610
- };
611
- /**
612
- * Error thrown when schema validation fails.
613
- */
614
- var WebhookValidationError = class extends PrimitiveWebhookError {
615
- code = "SCHEMA_VALIDATION_FAILED";
616
- suggestion;
617
- /** The specific field path that failed (e.g., "email.headers.from") */
618
- field;
619
- /** Original schema validation errors for advanced debugging */
620
- validationErrors;
621
- /** Number of additional validation errors beyond the first */
622
- additionalErrorCount;
623
- constructor(field, message, suggestion, validationErrors) {
624
- super(message);
625
- this.name = "WebhookValidationError";
626
- this.field = field;
627
- this.suggestion = suggestion;
628
- this.validationErrors = validationErrors;
629
- this.additionalErrorCount = Math.max(0, validationErrors.length - 1);
630
- }
631
- /**
632
- * Formats the error for logging/display.
633
- * Includes error count and suggestion.
634
- */
635
- toString() {
636
- let output = `${this.name} [${this.code}]: ${this.message}`;
637
- if (this.additionalErrorCount > 0) output += ` (and ${this.additionalErrorCount} more validation error${this.additionalErrorCount > 1 ? "s" : ""})`;
638
- output += `\n\nSuggestion: ${this.suggestion}`;
639
- return output;
640
- }
641
- /**
642
- * Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
643
- */
644
- toJSON() {
645
- return {
646
- name: this.name,
647
- code: this.code,
648
- field: this.field,
649
- message: this.message,
650
- suggestion: this.suggestion,
651
- additionalErrorCount: this.additionalErrorCount
652
- };
653
- }
654
- };
655
- /**
656
- * Error thrown when raw email decoding or verification fails.
657
- *
658
- * Use the `code` property to determine the failure reason:
659
- * - `NOT_INCLUDED`: Raw email not inline, must download from URL
660
- * - `HASH_MISMATCH`: SHA-256 verification failed, content may be corrupted
661
- */
662
- var RawEmailDecodeError = class extends PrimitiveWebhookError {
663
- code;
664
- suggestion;
665
- constructor(code, message) {
666
- super(message ?? RAW_EMAIL_ERRORS[code].message);
667
- this.name = "RawEmailDecodeError";
668
- this.code = code;
669
- this.suggestion = RAW_EMAIL_ERRORS[code].suggestion;
670
- }
671
- };
672
- //#endregion
673
- export { normalizeReceivedEmail as C, formatAddress as S, ForwardVerdict as _, VERIFICATION_ERRORS as a, buildForwardSubject as b, WebhookVerificationError as c, AuthConfidence as d, AuthVerdict as f, EventType as g, DmarcResult as h, RawEmailDecodeError as i, isTrustedSender as l, DmarcPolicy as m, PrimitiveWebhookError as n, WebhookPayloadError as o, DkimResult as p, RAW_EMAIL_ERRORS as r, WebhookValidationError as s, PAYLOAD_ERRORS as t, validateEmailAuth as u, ParsedStatus as v, parseHeaderAddress as w, buildReplySubject as x, SpfResult as y };