@primitivedotdev/sdk 0.26.1 → 0.27.1
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 +2 -2
- package/dist/api/index.d.ts +5 -3
- package/dist/api/index.js +4 -406
- package/dist/{api-CnUa6o4r.js → api-CZIBnM4Q.js} +118 -92
- package/dist/contract/index.d.ts +2 -2
- package/dist/contract/index.js +1 -1
- package/dist/{errors-x91I_yEt.js → errors-BPJGp9I6.js} +1 -1
- package/dist/{errors-C53fe686.d.ts → errors-T_0JE528.d.ts} +1 -1
- package/dist/{index-BbEVpN5e.d.ts → index-9Rqocr-c.d.ts} +87 -75
- package/dist/{index-Dbx9udpX.d.ts → index-EQZK4vWT.d.ts} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/openapi/index.d.ts +1 -52
- package/dist/openapi/index.js +2 -8
- package/dist/operations.generated-BJERV_56.d.ts +53 -0
- package/dist/operations.generated-T3exFpgJ.js +7632 -0
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/webhook/index.d.ts +3 -3
- package/dist/webhook/index.js +2 -2
- package/dist/{webhook-DJkfUnFZ.js → webhook-Bra-g1q8.js} +1 -1
- package/package.json +9 -69
- package/bin/run.js +0 -20
- package/dist/api/generated/client/client.gen.js +0 -235
- package/dist/api/generated/client/index.js +0 -6
- package/dist/api/generated/client/types.gen.js +0 -2
- package/dist/api/generated/client/utils.gen.js +0 -228
- package/dist/api/generated/client.gen.js +0 -3
- package/dist/api/generated/core/auth.gen.js +0 -14
- package/dist/api/generated/core/bodySerializer.gen.js +0 -57
- package/dist/api/generated/core/params.gen.js +0 -100
- package/dist/api/generated/core/pathSerializer.gen.js +0 -106
- package/dist/api/generated/core/queryKeySerializer.gen.js +0 -92
- package/dist/api/generated/core/serverSentEvents.gen.js +0 -132
- package/dist/api/generated/core/types.gen.js +0 -2
- package/dist/api/generated/core/utils.gen.js +0 -87
- package/dist/api/generated/index.js +0 -2
- package/dist/api/generated/sdk.gen.js +0 -878
- package/dist/api/generated/types.gen.js +0 -2
- package/dist/api/verify-signature.js +0 -198
- package/dist/oclif/api-command.js +0 -755
- package/dist/oclif/auth.js +0 -223
- package/dist/oclif/commands/emails-latest.js +0 -185
- package/dist/oclif/commands/emails-poll.js +0 -121
- package/dist/oclif/commands/emails-wait.js +0 -171
- package/dist/oclif/commands/emails-watch.js +0 -165
- package/dist/oclif/commands/functions-deploy.js +0 -124
- package/dist/oclif/commands/functions-init.js +0 -256
- package/dist/oclif/commands/functions-redeploy.js +0 -113
- package/dist/oclif/commands/functions-set-secret.js +0 -213
- package/dist/oclif/commands/login.js +0 -237
- package/dist/oclif/commands/logout.js +0 -88
- package/dist/oclif/commands/send.js +0 -222
- package/dist/oclif/commands/whoami.js +0 -95
- package/dist/oclif/fish-completion.js +0 -87
- package/dist/oclif/index.js +0 -167
- package/dist/oclif/lint/raw-send-mail-fetch.js +0 -98
- package/dist/openapi/openapi.generated.js +0 -5754
- package/dist/openapi/operations.generated.js +0 -4626
- package/dist/parser/address-parser.js +0 -129
- package/dist/types.generated.js +0 -7
- package/dist/types.js +0 -53
- package/dist/webhook/errors.js +0 -224
- package/dist/webhook/received-email.js +0 -82
- package/oclif.manifest.json +0 -4380
- /package/dist/{address-parser-BYn8oW5r.js → address-parser-CQbFjgRC.js} +0 -0
- /package/dist/{types-9vXGZjPd.d.ts → types-Nslo1CU0.d.ts} +0 -0
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import addressparser from "nodemailer/lib/addressparser/index.js";
|
|
2
|
-
import isEmail from "validator/lib/isEmail.js";
|
|
3
|
-
// Per RFC 5322 §2.1.1, header lines are bounded at 998 octets. We measure
|
|
4
|
-
// in UTF-8 bytes, not JS code units, so SMTPUTF8 (RFC 6531) headers with
|
|
5
|
-
// multi-byte characters cannot bypass the cap by being short on chars but
|
|
6
|
-
// long on bytes. Reject anything beyond as malformed without parsing: a
|
|
7
|
-
// longer From field is either a header-injection probe or a corrupt feed.
|
|
8
|
-
const MAX_HEADER_LENGTH = 998;
|
|
9
|
-
// Options for validator's isEmail. The per-part length limits (64-octet
|
|
10
|
-
// local-part, 255-octet domain), dot-atom rules, hostname-label rules,
|
|
11
|
-
// and TLD requirement are all enforced inside isEmail. We choose:
|
|
12
|
-
// allow_ip_domain: true -- accept user@[192.168.1.1] address-literals
|
|
13
|
-
// require_tld: true -- reject user@localhost
|
|
14
|
-
// allow_display_name: false -- we already extracted the address with
|
|
15
|
-
// addressparser, so isEmail only sees the
|
|
16
|
-
// bare addr-spec
|
|
17
|
-
// allow_utf8_local_part: true -- accept SMTPUTF8 / EAI local-parts
|
|
18
|
-
const IS_EMAIL_OPTIONS = {
|
|
19
|
-
allow_ip_domain: true,
|
|
20
|
-
require_tld: true,
|
|
21
|
-
allow_display_name: false,
|
|
22
|
-
allow_utf8_local_part: true,
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Strict parser for RFC 5322 From-style headers in security-bearing
|
|
26
|
-
* contexts (allowlist gates, permission grants).
|
|
27
|
-
*
|
|
28
|
-
* Rejects, without falling back to a "best guess":
|
|
29
|
-
* - empty / whitespace-only input
|
|
30
|
-
* - inputs longer than RFC 5322's 998-octet line limit
|
|
31
|
-
* - multi-address From (RFC 5322 allows it but it is vanishingly
|
|
32
|
-
* rare and ambiguous as an identity)
|
|
33
|
-
* - group syntax ("Friends: a@b.com, c@d.com;")
|
|
34
|
-
* - any address that fails validator's isEmail check with our chosen
|
|
35
|
-
* options. That covers per-part length limits, dot-atom rules,
|
|
36
|
-
* hostname-label rules, TLD requirement, and other RFC 5321/5322
|
|
37
|
-
* conformance checks.
|
|
38
|
-
*
|
|
39
|
-
* Returns ONLY the validated address, with no display name. Strict
|
|
40
|
-
* exists for gating decisions, where the address is the security-
|
|
41
|
-
* bearing field. Display names from addressparser are not trustworthy
|
|
42
|
-
* here: weird inputs like `Name <user@x.com> <attacker@y.com>` get
|
|
43
|
-
* parsed as a single entry whose `name` silently includes the second
|
|
44
|
-
* address. Surfacing that as a "parsed name" would invite downstream
|
|
45
|
-
* misuse, so we drop it. If you need the name, call
|
|
46
|
-
* {@link parseFromHeaderLoose} alongside (it returns null on failure
|
|
47
|
-
* anyway, so you can still gate on strict's Result).
|
|
48
|
-
*
|
|
49
|
-
* Returns a typed Result so callers can map the failure reason to
|
|
50
|
-
* stable error codes without inspecting message text.
|
|
51
|
-
*/
|
|
52
|
-
export function parseFromHeader(header) {
|
|
53
|
-
if (header === null || header === undefined) {
|
|
54
|
-
return { ok: false, reason: "empty" };
|
|
55
|
-
}
|
|
56
|
-
const trimmed = header.trim();
|
|
57
|
-
if (trimmed.length === 0) {
|
|
58
|
-
return { ok: false, reason: "empty" };
|
|
59
|
-
}
|
|
60
|
-
if (Buffer.byteLength(trimmed, "utf8") > MAX_HEADER_LENGTH) {
|
|
61
|
-
return { ok: false, reason: "too_long" };
|
|
62
|
-
}
|
|
63
|
-
// Default (no flatten) so group entries surface as { name, group: [] }
|
|
64
|
-
// rather than being silently merged into the address list.
|
|
65
|
-
const parsed = addressparser(trimmed);
|
|
66
|
-
if (parsed.length > 1) {
|
|
67
|
-
return { ok: false, reason: "multiple_addresses" };
|
|
68
|
-
}
|
|
69
|
-
const entry = parsed[0];
|
|
70
|
-
// addressparser returns a single entry with empty `address` for raw
|
|
71
|
-
// garbage rather than an empty array, so an empty result is only
|
|
72
|
-
// possible for inputs that already failed our trim/empty check above.
|
|
73
|
-
// The defensive fall-through maps any future regression to
|
|
74
|
-
// invalid_address rather than crashing on parsed[0].
|
|
75
|
-
if (entry === undefined) {
|
|
76
|
-
return { ok: false, reason: "invalid_address" };
|
|
77
|
-
}
|
|
78
|
-
if ("group" in entry) {
|
|
79
|
-
return { ok: false, reason: "group_syntax" };
|
|
80
|
-
}
|
|
81
|
-
const address = entry.address;
|
|
82
|
-
if (address === undefined || !isEmail(address, IS_EMAIL_OPTIONS)) {
|
|
83
|
-
return { ok: false, reason: "invalid_address" };
|
|
84
|
-
}
|
|
85
|
-
return {
|
|
86
|
-
ok: true,
|
|
87
|
-
value: { address: address.toLowerCase() },
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Lenient parser for display-only call sites (inbox card "from",
|
|
92
|
-
* log lines, debugging). Returns the first parseable address with its
|
|
93
|
-
* display name, or null.
|
|
94
|
-
*
|
|
95
|
-
* Differences from {@link parseFromHeader}:
|
|
96
|
-
* - Multi-address From returns the first address instead of rejecting
|
|
97
|
-
* - Group syntax is flattened into its member addresses
|
|
98
|
-
* - Returns null instead of a typed reason on failure
|
|
99
|
-
* - Includes the parsed display name in the result
|
|
100
|
-
*
|
|
101
|
-
* Do not use for permission gates or any decision that grants access.
|
|
102
|
-
* That is what {@link parseFromHeader} is for. Names returned here can
|
|
103
|
-
* include addressparser's recovery output (trailing tokens, garbage
|
|
104
|
-
* before the address); treat as opaque text for display.
|
|
105
|
-
*/
|
|
106
|
-
export function parseFromHeaderLoose(header) {
|
|
107
|
-
if (header === null || header === undefined) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
const trimmed = header.trim();
|
|
111
|
-
if (trimmed.length === 0 ||
|
|
112
|
-
Buffer.byteLength(trimmed, "utf8") > MAX_HEADER_LENGTH) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
const parsed = addressparser(trimmed);
|
|
116
|
-
for (const entry of parsed) {
|
|
117
|
-
const candidates = "group" in entry && Array.isArray(entry.group) ? entry.group : [entry];
|
|
118
|
-
for (const candidate of candidates) {
|
|
119
|
-
const address = candidate.address;
|
|
120
|
-
if (address !== undefined && isEmail(address, IS_EMAIL_OPTIONS)) {
|
|
121
|
-
return {
|
|
122
|
-
address: address.toLowerCase(),
|
|
123
|
-
name: candidate.name && candidate.name.length > 0 ? candidate.name : null,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
package/dist/types.generated.js
DELETED
package/dist/types.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Primitive webhook event types derived from the canonical JSON schema.
|
|
3
|
-
*
|
|
4
|
-
* @packageDocumentation
|
|
5
|
-
*/
|
|
6
|
-
export const EventType = {
|
|
7
|
-
EmailReceived: "email.received",
|
|
8
|
-
};
|
|
9
|
-
export const ParsedStatus = {
|
|
10
|
-
Complete: "complete",
|
|
11
|
-
Failed: "failed",
|
|
12
|
-
};
|
|
13
|
-
export const ForwardVerdict = {
|
|
14
|
-
Legit: "legit",
|
|
15
|
-
Unknown: "unknown",
|
|
16
|
-
};
|
|
17
|
-
export const SpfResult = {
|
|
18
|
-
Pass: "pass",
|
|
19
|
-
Fail: "fail",
|
|
20
|
-
Softfail: "softfail",
|
|
21
|
-
Neutral: "neutral",
|
|
22
|
-
None: "none",
|
|
23
|
-
Temperror: "temperror",
|
|
24
|
-
Permerror: "permerror",
|
|
25
|
-
};
|
|
26
|
-
export const DmarcResult = {
|
|
27
|
-
Pass: "pass",
|
|
28
|
-
Fail: "fail",
|
|
29
|
-
None: "none",
|
|
30
|
-
Temperror: "temperror",
|
|
31
|
-
Permerror: "permerror",
|
|
32
|
-
};
|
|
33
|
-
export const DmarcPolicy = {
|
|
34
|
-
Reject: "reject",
|
|
35
|
-
Quarantine: "quarantine",
|
|
36
|
-
None: "none",
|
|
37
|
-
};
|
|
38
|
-
export const DkimResult = {
|
|
39
|
-
Pass: "pass",
|
|
40
|
-
Fail: "fail",
|
|
41
|
-
Temperror: "temperror",
|
|
42
|
-
Permerror: "permerror",
|
|
43
|
-
};
|
|
44
|
-
export const AuthConfidence = {
|
|
45
|
-
High: "high",
|
|
46
|
-
Medium: "medium",
|
|
47
|
-
Low: "low",
|
|
48
|
-
};
|
|
49
|
-
export const AuthVerdict = {
|
|
50
|
-
Legit: "legit",
|
|
51
|
-
Suspicious: "suspicious",
|
|
52
|
-
Unknown: "unknown",
|
|
53
|
-
};
|
package/dist/webhook/errors.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// Error Definitions (Single Source of Truth)
|
|
3
|
-
// -----------------------------------------------------------------------------
|
|
4
|
-
/**
|
|
5
|
-
* Verification error definitions.
|
|
6
|
-
* Use these for documentation, dashboards, and i18n.
|
|
7
|
-
*/
|
|
8
|
-
export const VERIFICATION_ERRORS = {
|
|
9
|
-
INVALID_SIGNATURE_HEADER: {
|
|
10
|
-
message: "Missing or malformed Primitive-Signature header",
|
|
11
|
-
suggestion: "Check that you're reading the correct header (Primitive-Signature) and it's being passed correctly from your web framework.",
|
|
12
|
-
},
|
|
13
|
-
TIMESTAMP_OUT_OF_RANGE: {
|
|
14
|
-
message: "Timestamp is too old (possible replay attack)",
|
|
15
|
-
suggestion: "This could indicate a replay attack, network delay, or server clock drift. Check your server's time is synced.",
|
|
16
|
-
},
|
|
17
|
-
SIGNATURE_MISMATCH: {
|
|
18
|
-
message: "Signature doesn't match expected value",
|
|
19
|
-
suggestion: "Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).",
|
|
20
|
-
},
|
|
21
|
-
MISSING_SECRET: {
|
|
22
|
-
message: "No webhook secret was provided",
|
|
23
|
-
suggestion: "Pass your webhook secret from the Primitive dashboard. Check that the environment variable is set.",
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Payload parsing error definitions.
|
|
28
|
-
* Use these for documentation, dashboards, and i18n.
|
|
29
|
-
*/
|
|
30
|
-
export const PAYLOAD_ERRORS = {
|
|
31
|
-
PAYLOAD_NULL: {
|
|
32
|
-
message: "Webhook payload is null",
|
|
33
|
-
suggestion: "Ensure you're passing the parsed JSON body, not null. Check your framework's body parsing middleware.",
|
|
34
|
-
},
|
|
35
|
-
PAYLOAD_UNDEFINED: {
|
|
36
|
-
message: "Webhook payload is undefined",
|
|
37
|
-
suggestion: "The payload was not provided. Make sure you're passing the request body to the handler.",
|
|
38
|
-
},
|
|
39
|
-
PAYLOAD_WRONG_TYPE: {
|
|
40
|
-
message: "Webhook payload must be an object",
|
|
41
|
-
suggestion: "The payload should be a parsed JSON object. Check that you're not passing a string or other primitive.",
|
|
42
|
-
},
|
|
43
|
-
PAYLOAD_IS_ARRAY: {
|
|
44
|
-
message: "Webhook payload is an array, expected object",
|
|
45
|
-
suggestion: "Primitive webhooks are single event objects, not arrays. Check the payload structure.",
|
|
46
|
-
},
|
|
47
|
-
PAYLOAD_MISSING_EVENT: {
|
|
48
|
-
message: "Webhook payload missing 'event' field",
|
|
49
|
-
suggestion: "All webhook payloads must have an 'event' field. This may not be a valid Primitive webhook.",
|
|
50
|
-
},
|
|
51
|
-
PAYLOAD_UNKNOWN_EVENT: {
|
|
52
|
-
message: "Unknown webhook event type",
|
|
53
|
-
suggestion: "This event type is not recognized. You may need to update your SDK or handle unknown events gracefully.",
|
|
54
|
-
},
|
|
55
|
-
PAYLOAD_EMPTY_BODY: {
|
|
56
|
-
message: "Request body is empty",
|
|
57
|
-
suggestion: "The request body was empty. Ensure the webhook is sending data and your framework is parsing it correctly.",
|
|
58
|
-
},
|
|
59
|
-
JSON_PARSE_FAILED: {
|
|
60
|
-
message: "Failed to parse JSON body",
|
|
61
|
-
suggestion: "The request body is not valid JSON. Check the raw body content and Content-Type header.",
|
|
62
|
-
},
|
|
63
|
-
INVALID_ENCODING: {
|
|
64
|
-
message: "Invalid body encoding",
|
|
65
|
-
suggestion: "The request body encoding is not supported. Primitive webhooks use UTF-8 encoded JSON.",
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Raw email decode error definitions.
|
|
70
|
-
* Use these for documentation, dashboards, and i18n.
|
|
71
|
-
*/
|
|
72
|
-
export const RAW_EMAIL_ERRORS = {
|
|
73
|
-
NOT_INCLUDED: {
|
|
74
|
-
message: "Raw email content not included inline",
|
|
75
|
-
suggestion: "Use the download URL at event.email.content.download.url to fetch the raw email.",
|
|
76
|
-
},
|
|
77
|
-
INVALID_BASE64: {
|
|
78
|
-
message: "Raw email content is not valid base64",
|
|
79
|
-
suggestion: "The raw email data is malformed. Fetch the raw email from the download URL or regenerate the webhook payload.",
|
|
80
|
-
},
|
|
81
|
-
HASH_MISMATCH: {
|
|
82
|
-
message: "SHA-256 hash verification failed",
|
|
83
|
-
suggestion: "The raw email data may be corrupted. Try downloading from the URL instead.",
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
/**
|
|
87
|
-
* Base class for all Primitive webhook errors.
|
|
88
|
-
*
|
|
89
|
-
* Catch this to handle any error from the SDK in a single catch block.
|
|
90
|
-
*
|
|
91
|
-
* @example
|
|
92
|
-
* ```typescript
|
|
93
|
-
* import { handleWebhook, PrimitiveWebhookError } from '@primitivedotdev/sdk';
|
|
94
|
-
*
|
|
95
|
-
* try {
|
|
96
|
-
* const event = handleWebhook({ body, headers, secret });
|
|
97
|
-
* } catch (err) {
|
|
98
|
-
* if (err instanceof PrimitiveWebhookError) {
|
|
99
|
-
* console.error(`[${err.code}] ${err.message}`);
|
|
100
|
-
* return res.status(400).json({ error: err.code });
|
|
101
|
-
* }
|
|
102
|
-
* throw err;
|
|
103
|
-
* }
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
export class PrimitiveWebhookError extends Error {
|
|
107
|
-
/**
|
|
108
|
-
* Formats the error for logging/display.
|
|
109
|
-
*/
|
|
110
|
-
toString() {
|
|
111
|
-
return `${this.name} [${this.code}]: ${this.message}\n\nSuggestion: ${this.suggestion}`;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
|
|
115
|
-
*/
|
|
116
|
-
toJSON() {
|
|
117
|
-
return {
|
|
118
|
-
name: this.name,
|
|
119
|
-
code: this.code,
|
|
120
|
-
message: this.message,
|
|
121
|
-
suggestion: this.suggestion,
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Error thrown when webhook signature verification fails.
|
|
127
|
-
*
|
|
128
|
-
* Use the `code` property to programmatically handle specific error cases.
|
|
129
|
-
*/
|
|
130
|
-
export class WebhookVerificationError extends PrimitiveWebhookError {
|
|
131
|
-
code;
|
|
132
|
-
suggestion;
|
|
133
|
-
constructor(code, message, suggestion) {
|
|
134
|
-
super(message ?? VERIFICATION_ERRORS[code].message);
|
|
135
|
-
this.name = "WebhookVerificationError";
|
|
136
|
-
this.code = code;
|
|
137
|
-
this.suggestion = suggestion ?? VERIFICATION_ERRORS[code].suggestion;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Error thrown when webhook payload parsing fails (lightweight parser).
|
|
142
|
-
*
|
|
143
|
-
* Use the `code` property for programmatic handling and monitoring.
|
|
144
|
-
* The `suggestion` property contains actionable guidance for fixing the issue.
|
|
145
|
-
*/
|
|
146
|
-
export class WebhookPayloadError extends PrimitiveWebhookError {
|
|
147
|
-
code;
|
|
148
|
-
suggestion;
|
|
149
|
-
/** Original error if this wraps another error (e.g., JSON.parse failure) */
|
|
150
|
-
cause;
|
|
151
|
-
constructor(code, message, suggestion, cause) {
|
|
152
|
-
super(message ?? PAYLOAD_ERRORS[code].message);
|
|
153
|
-
this.name = "WebhookPayloadError";
|
|
154
|
-
this.code = code;
|
|
155
|
-
this.suggestion = suggestion ?? PAYLOAD_ERRORS[code].suggestion;
|
|
156
|
-
this.cause = cause;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Error thrown when schema validation fails.
|
|
161
|
-
*/
|
|
162
|
-
export class WebhookValidationError extends PrimitiveWebhookError {
|
|
163
|
-
code = "SCHEMA_VALIDATION_FAILED";
|
|
164
|
-
suggestion;
|
|
165
|
-
/** The specific field path that failed (e.g., "email.headers.from") */
|
|
166
|
-
field;
|
|
167
|
-
/** Original schema validation errors for advanced debugging */
|
|
168
|
-
validationErrors;
|
|
169
|
-
/** Number of additional validation errors beyond the first */
|
|
170
|
-
additionalErrorCount;
|
|
171
|
-
constructor(field, message, suggestion, validationErrors) {
|
|
172
|
-
super(message);
|
|
173
|
-
this.name = "WebhookValidationError";
|
|
174
|
-
this.field = field;
|
|
175
|
-
this.suggestion = suggestion;
|
|
176
|
-
this.validationErrors = validationErrors;
|
|
177
|
-
this.additionalErrorCount = Math.max(0, validationErrors.length - 1);
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Formats the error for logging/display.
|
|
181
|
-
* Includes error count and suggestion.
|
|
182
|
-
*/
|
|
183
|
-
toString() {
|
|
184
|
-
let output = `${this.name} [${this.code}]: ${this.message}`;
|
|
185
|
-
if (this.additionalErrorCount > 0) {
|
|
186
|
-
output += ` (and ${this.additionalErrorCount} more validation error${this.additionalErrorCount > 1 ? "s" : ""})`;
|
|
187
|
-
}
|
|
188
|
-
output += `\n\nSuggestion: ${this.suggestion}`;
|
|
189
|
-
return output;
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Serializes cleanly for structured logging (Datadog, CloudWatch, etc.)
|
|
193
|
-
*/
|
|
194
|
-
toJSON() {
|
|
195
|
-
return {
|
|
196
|
-
name: this.name,
|
|
197
|
-
code: this.code,
|
|
198
|
-
field: this.field,
|
|
199
|
-
message: this.message,
|
|
200
|
-
suggestion: this.suggestion,
|
|
201
|
-
additionalErrorCount: this.additionalErrorCount,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// -----------------------------------------------------------------------------
|
|
206
|
-
// Raw Email Decode Errors
|
|
207
|
-
// -----------------------------------------------------------------------------
|
|
208
|
-
/**
|
|
209
|
-
* Error thrown when raw email decoding or verification fails.
|
|
210
|
-
*
|
|
211
|
-
* Use the `code` property to determine the failure reason:
|
|
212
|
-
* - `NOT_INCLUDED`: Raw email not inline, must download from URL
|
|
213
|
-
* - `HASH_MISMATCH`: SHA-256 verification failed, content may be corrupted
|
|
214
|
-
*/
|
|
215
|
-
export class RawEmailDecodeError extends PrimitiveWebhookError {
|
|
216
|
-
code;
|
|
217
|
-
suggestion;
|
|
218
|
-
constructor(code, message) {
|
|
219
|
-
super(message ?? RAW_EMAIL_ERRORS[code].message);
|
|
220
|
-
this.name = "RawEmailDecodeError";
|
|
221
|
-
this.code = code;
|
|
222
|
-
this.suggestion = RAW_EMAIL_ERRORS[code].suggestion;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { parseFromHeaderLoose } from "../parser/address-parser.js";
|
|
2
|
-
const REPLY_PREFIX_RE = /^re\s*:/i;
|
|
3
|
-
const FORWARD_PREFIX_RE = /^(fwd?|fw)\s*:/i;
|
|
4
|
-
export function normalizeReceivedEmail(event) {
|
|
5
|
-
const receivedBy = event.email.smtp.rcpt_to[0];
|
|
6
|
-
if (!receivedBy) {
|
|
7
|
-
throw new Error("email.smtp.rcpt_to must contain at least one recipient");
|
|
8
|
-
}
|
|
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
|
-
export function buildReplySubject(subject) {
|
|
41
|
-
const trimmed = subject?.trim() ?? "";
|
|
42
|
-
if (trimmed.length === 0) {
|
|
43
|
-
return "Re:";
|
|
44
|
-
}
|
|
45
|
-
return REPLY_PREFIX_RE.test(trimmed) ? trimmed : `Re: ${trimmed}`;
|
|
46
|
-
}
|
|
47
|
-
export function buildForwardSubject(subject) {
|
|
48
|
-
const trimmed = subject?.trim() ?? "";
|
|
49
|
-
if (trimmed.length === 0) {
|
|
50
|
-
return "Fwd:";
|
|
51
|
-
}
|
|
52
|
-
return FORWARD_PREFIX_RE.test(trimmed) ? trimmed : `Fwd: ${trimmed}`;
|
|
53
|
-
}
|
|
54
|
-
export function formatAddress(address) {
|
|
55
|
-
return address.name
|
|
56
|
-
? `${address.name} <${address.address}>`
|
|
57
|
-
: address.address;
|
|
58
|
-
}
|
|
59
|
-
function firstStructuredAddress(addresses) {
|
|
60
|
-
const address = addresses?.[0];
|
|
61
|
-
if (!address) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
address: address.address.trim().toLowerCase(),
|
|
66
|
-
name: address.name ?? null,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
// Lenient about quirky headers (unquoted commas in display names, missing
|
|
70
|
-
// closing angle brackets) but strict about the resulting address: it must
|
|
71
|
-
// validate as an email, otherwise the normalizer falls back to the SMTP
|
|
72
|
-
// envelope sender. Exported so cross-SDK fixtures can exercise it directly.
|
|
73
|
-
export function parseHeaderAddress(value) {
|
|
74
|
-
const parsed = parseFromHeaderLoose(value);
|
|
75
|
-
if (!parsed) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
address: parsed.address,
|
|
80
|
-
name: parsed.name?.trim() || null,
|
|
81
|
-
};
|
|
82
|
-
}
|