@primitivedotdev/sdk 0.7.0 → 0.9.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.
@@ -1,6 +1,10 @@
1
+ import { i as normalizeReceivedEmail } from "./received-email-D6tKtWwW.js";
1
2
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
-
3
3
  //#region src/generated/email-received-event.validator.generated.ts
4
+ /**
5
+ * AUTO-GENERATED - DO NOT EDIT
6
+ * Run `pnpm generate:validator` to regenerate.
7
+ */
4
8
  var email_received_event_validator_generated_default = validate10;
5
9
  const schema12 = {
6
10
  "type": "object",
@@ -182,12 +186,12 @@ const schema12 = {
182
186
  ],
183
187
  "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."
184
188
  };
185
- const pattern0 = new RegExp("^evt_[a-f0-9]{64}$", "u");
186
- const pattern1 = new RegExp("^(?:(?:\\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))$", "u");
187
- const pattern4 = new RegExp("^https?://", "u");
189
+ const pattern0 = /* @__PURE__ */ new RegExp("^evt_[a-f0-9]{64}$", "u");
190
+ const pattern1 = /* @__PURE__ */ new RegExp("^(?:(?:\\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))$", "u");
191
+ const pattern4 = /* @__PURE__ */ new RegExp("^https?://", "u");
188
192
  const formats0 = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[T\t ](?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/;
189
193
  const formats4 = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
190
- const pattern2 = new RegExp("^[a-fA-F0-9]{64}$", "u");
194
+ const pattern2 = /* @__PURE__ */ new RegExp("^[a-fA-F0-9]{64}$", "u");
191
195
  function validate12(data, { instancePath = "", parentData, parentDataProperty, rootData = data } = {}) {
192
196
  let vErrors = null;
193
197
  let errors = 0;
@@ -5627,7 +5631,6 @@ function validate10(data, { instancePath = "", parentData, parentDataProperty, r
5627
5631
  validate10.errors = vErrors;
5628
5632
  return errors === 0;
5629
5633
  }
5630
-
5631
5634
  //#endregion
5632
5635
  //#region src/webhook/errors.ts
5633
5636
  /**
@@ -5846,7 +5849,6 @@ var RawEmailDecodeError = class extends PrimitiveWebhookError {
5846
5849
  this.suggestion = RAW_EMAIL_ERRORS[code].suggestion;
5847
5850
  }
5848
5851
  };
5849
-
5850
5852
  //#endregion
5851
5853
  //#region src/validation.ts
5852
5854
  const validateSchema = email_received_event_validator_generated_default;
@@ -5870,7 +5872,7 @@ function resolveValueAtPath(input, instancePath) {
5870
5872
  current = current[key];
5871
5873
  continue;
5872
5874
  }
5873
- return void 0;
5875
+ return;
5874
5876
  }
5875
5877
  return current;
5876
5878
  }
@@ -5941,10 +5943,9 @@ function formatValidationIssue(error, input) {
5941
5943
  }
5942
5944
  case "format": {
5943
5945
  const format = String(error.params.format ?? "unknown format");
5944
- const humanFormat = format === "uri" ? "valid URI" : `valid ${format}`;
5945
5946
  return {
5946
5947
  field,
5947
- message: `Invalid value for ${field}: must be a ${humanFormat}`,
5948
+ message: `Invalid value for ${field}: must be a ${format === "uri" ? "valid URI" : `valid ${format}`}`,
5948
5949
  suggestion: field.endsWith("url") ? `Check that ${fromFieldLabel(field)} is a complete URL including the scheme.` : `Check the format of ${fromFieldLabel(field)} in the webhook payload.`
5949
5950
  };
5950
5951
  }
@@ -5979,10 +5980,9 @@ function formatValidationIssue(error, input) {
5979
5980
  }
5980
5981
  case "minItems": {
5981
5982
  const limit = Number(error.params.limit ?? 0);
5982
- const itemLabel = limit === 1 ? "item" : "items";
5983
5983
  return {
5984
5984
  field,
5985
- message: `Invalid value for ${field}: must have at least ${limit} ${itemLabel}`,
5985
+ message: `Invalid value for ${field}: must have at least ${limit} ${limit === 1 ? "item" : "items"}`,
5986
5986
  suggestion: `Add more entries to ${fromFieldLabel(field)} in the webhook payload.`
5987
5987
  };
5988
5988
  }
@@ -5999,22 +5999,16 @@ function formatValidationIssue(error, input) {
5999
5999
  suggestion: `Check that ${fromFieldLabel(field)} meets the minimum length requirement.`
6000
6000
  };
6001
6001
  }
6002
- case "minimum": {
6003
- const limit = Number(error.params.limit ?? 0);
6004
- return {
6005
- field,
6006
- message: `Invalid value for ${field}: must be >= ${limit}`,
6007
- suggestion: `Check that ${fromFieldLabel(field)} meets the minimum allowed value.`
6008
- };
6009
- }
6010
- case "maximum": {
6011
- const limit = Number(error.params.limit ?? 0);
6012
- return {
6013
- field,
6014
- message: `Invalid value for ${field}: must be <= ${limit}`,
6015
- suggestion: `Check that ${fromFieldLabel(field)} stays within the maximum allowed value.`
6016
- };
6017
- }
6002
+ case "minimum": return {
6003
+ field,
6004
+ message: `Invalid value for ${field}: must be >= ${Number(error.params.limit ?? 0)}`,
6005
+ suggestion: `Check that ${fromFieldLabel(field)} meets the minimum allowed value.`
6006
+ };
6007
+ case "maximum": return {
6008
+ field,
6009
+ message: `Invalid value for ${field}: must be <= ${Number(error.params.limit ?? 0)}`,
6010
+ suggestion: `Check that ${fromFieldLabel(field)} stays within the maximum allowed value.`
6011
+ };
6018
6012
  default: return {
6019
6013
  field,
6020
6014
  message: `Validation failed for ${field}: ${error.message ?? error.keyword}`,
@@ -6042,9 +6036,31 @@ function safeValidateEmailReceivedEvent(input) {
6042
6036
  data: input
6043
6037
  };
6044
6038
  }
6045
-
6046
6039
  //#endregion
6047
6040
  //#region src/webhook/download-tokens.ts
6041
+ /**
6042
+ * Signed download tokens.
6043
+ *
6044
+ * A download token is a self-describing bearer credential for fetching a
6045
+ * specific email's raw bytes or attachment bundle from a per-deployment
6046
+ * download endpoint. It binds:
6047
+ *
6048
+ * - `email_id` — the specific email the token authorizes.
6049
+ * - `aud` — a caller-chosen audience label (e.g. the resource kind being
6050
+ * downloaded). Tokens minted for one audience will not verify under another.
6051
+ * - `exp` — an absolute expiration time (unix seconds).
6052
+ *
6053
+ * Format: `<base64url(payload)>.<base64url(signature)>` where `signature`
6054
+ * is HMAC-SHA256 over the base64url-encoded payload using the shared secret.
6055
+ *
6056
+ * The audience is an opaque caller-chosen string. Both the issuer and the
6057
+ * verifier must agree on the exact bytes; the SDK does not prescribe a
6058
+ * convention. New integrations are encouraged to namespace audiences
6059
+ * (e.g. `primitive:raw-download`).
6060
+ *
6061
+ * Tokens are stateless: verification needs only the shared secret. Keep
6062
+ * expirations as short as operationally tolerable.
6063
+ */
6048
6064
  const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
6049
6065
  /**
6050
6066
  * Issue a signed download token.
@@ -6059,15 +6075,13 @@ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
6059
6075
  */
6060
6076
  function generateDownloadToken(params) {
6061
6077
  const { emailId, expiresAt, audience, secret } = params;
6062
- const payload = {
6078
+ const payloadJson = JSON.stringify({
6063
6079
  email_id: emailId,
6064
6080
  exp: expiresAt,
6065
6081
  aud: audience
6066
- };
6067
- const payloadJson = JSON.stringify(payload);
6082
+ });
6068
6083
  const payloadStr = Buffer.from(payloadJson, "utf8").toString("base64url");
6069
- const signature = createHmac("sha256", secret).update(payloadStr).digest("base64url");
6070
- return `${payloadStr}.${signature}`;
6084
+ return `${payloadStr}.${createHmac("sha256", secret).update(payloadStr).digest("base64url")}`;
6071
6085
  }
6072
6086
  /**
6073
6087
  * Verify a signed download token.
@@ -6131,16 +6145,17 @@ function verifyDownloadToken(params) {
6131
6145
  valid: false,
6132
6146
  error: "Email ID mismatch"
6133
6147
  };
6134
- const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
6135
- if (exp <= now) return {
6148
+ if (exp <= (nowSeconds ?? Math.floor(Date.now() / 1e3))) return {
6136
6149
  valid: false,
6137
6150
  error: "Token is expired"
6138
6151
  };
6139
6152
  return { valid: true };
6140
6153
  }
6141
-
6142
6154
  //#endregion
6143
6155
  //#region src/webhook/encoding.ts
6156
+ /**
6157
+ * Buffer encoding utilities
6158
+ */
6144
6159
  const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
6145
6160
  /**
6146
6161
  * Convert a Buffer to string with strict UTF-8 validation.
@@ -6161,15 +6176,28 @@ function bufferToString(buffer, label) {
6161
6176
  throw new WebhookPayloadError("INVALID_ENCODING", `${label} contains invalid UTF-8 bytes`, `Ensure the ${label} is valid UTF-8 encoded text. If the data is binary, it should be base64 encoded first.`, err instanceof Error ? err : void 0);
6162
6177
  }
6163
6178
  }
6164
-
6165
6179
  //#endregion
6166
6180
  //#region src/webhook/signing.ts
6167
6181
  /**
6182
+ * Webhook HMAC Signing (Stripe-style format)
6183
+ *
6184
+ * Implements HMAC-SHA256 signature for webhook security with timestamp validation.
6185
+ * Prevents replay attacks by including timestamp in signature.
6186
+ *
6187
+ * Header format:
6188
+ * Primitive-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
6189
+ *
6190
+ * Signed payload format: "{timestamp}.{raw_body}"
6191
+ *
6192
+ * This format matches Stripe's webhook signature scheme, which is widely understood
6193
+ * and easy to implement in any language with ~15 lines of code.
6194
+ */
6195
+ /**
6168
6196
  * WeakMap cache for computed signatures.
6169
6197
  * Only works for Buffer bodies (strings cannot be WeakMap keys).
6170
6198
  * Automatically garbage collected when Buffer is no longer referenced.
6171
6199
  */
6172
- const signatureCache = new WeakMap();
6200
+ const signatureCache = /* @__PURE__ */ new WeakMap();
6173
6201
  /**
6174
6202
  * Hash a secret for cache key comparison.
6175
6203
  * @internal
@@ -6186,7 +6214,7 @@ const PRIMITIVE_CONFIRMED_HEADER = "X-Primitive-Confirmed";
6186
6214
  /** Legacy confirmed header name kept for backward compatibility */
6187
6215
  const LEGACY_CONFIRMED_HEADER = "X-MyMX-Confirmed";
6188
6216
  /** Default max age for webhook requests (5 minutes) */
6189
- const DEFAULT_TOLERANCE_SECONDS$1 = 5 * 60;
6217
+ const DEFAULT_TOLERANCE_SECONDS$1 = 300;
6190
6218
  /** Future clock skew tolerance (1 minute) */
6191
6219
  const FUTURE_TOLERANCE_SECONDS$1 = 60;
6192
6220
  /** Valid hex pattern for signature verification */
@@ -6207,8 +6235,7 @@ const UNIX_SECONDS_PATTERN = /^\d+$/;
6207
6235
  */
6208
6236
  function signWebhookPayload(rawBody, secret, timestamp) {
6209
6237
  const ts = timestamp ?? Math.floor(Date.now() / 1e3);
6210
- const body = typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "rawBody");
6211
- const signedPayloadString = `${ts}.${body}`;
6238
+ const signedPayloadString = `${ts}.${typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "rawBody")}`;
6212
6239
  const hmac = createHmac("sha256", secret);
6213
6240
  hmac.update(signedPayloadString);
6214
6241
  const v1 = hmac.digest("hex");
@@ -6287,8 +6314,7 @@ function verifyWebhookSignature(opts) {
6287
6314
  const parsed = parseSignatureHeader(signatureHeader);
6288
6315
  if (!parsed) throw new WebhookVerificationError("INVALID_SIGNATURE_HEADER", "Invalid Primitive-Signature header format. Expected: t={timestamp},v1={signature}");
6289
6316
  const { timestamp, signatures } = parsed;
6290
- const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
6291
- const age = now - timestamp;
6317
+ const age = (nowSeconds ?? Math.floor(Date.now() / 1e3)) - timestamp;
6292
6318
  if (age > toleranceSeconds) throw new WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", `Webhook timestamp too old (${age}s). Max age is ${toleranceSeconds}s.`);
6293
6319
  if (age < -FUTURE_TOLERANCE_SECONDS$1) throw new WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", "Webhook timestamp is too far in the future. Check server clock sync.");
6294
6320
  const body = typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "request body");
@@ -6317,13 +6343,10 @@ function verifyWebhookSignature(opts) {
6317
6343
  }
6318
6344
  for (const receivedHex of signatures) {
6319
6345
  if (!isValidHex(receivedHex, 64)) continue;
6320
- const receivedBytes = Buffer.from(receivedHex, "hex");
6321
- const expectedBytes = Buffer.from(expectedHex, "hex");
6322
- if (timingSafeEqual(receivedBytes, expectedBytes)) return true;
6346
+ if (timingSafeEqual(Buffer.from(receivedHex, "hex"), Buffer.from(expectedHex, "hex"))) return true;
6323
6347
  }
6324
6348
  const reserializationHint = detectReserializedBody(body);
6325
- const message = reserializationHint ? `No valid signature found. ${reserializationHint}` : "No valid signature found. Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).";
6326
- throw new WebhookVerificationError("SIGNATURE_MISMATCH", message);
6349
+ throw new WebhookVerificationError("SIGNATURE_MISMATCH", reserializationHint ? `No valid signature found. ${reserializationHint}` : "No valid signature found. Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).");
6327
6350
  }
6328
6351
  /**
6329
6352
  * Detect if a body looks like it was re-serialized by a framework.
@@ -6334,13 +6357,27 @@ function detectReserializedBody(body) {
6334
6357
  if (/^\s*\{[\s\S]*\n\s{2,}/.test(body)) return "Request body appears re-serialized (pretty-printed). Use the raw request body before any JSON.parse() or JSON.stringify() calls.";
6335
6358
  return null;
6336
6359
  }
6337
-
6338
6360
  //#endregion
6339
6361
  //#region src/webhook/standard-webhooks.ts
6362
+ /**
6363
+ * Standard Webhooks Verification & Signing
6364
+ *
6365
+ * Implements the Standard Webhooks spec (standardwebhooks.com) for webhook
6366
+ * signature verification and payload signing.
6367
+ *
6368
+ * Header format:
6369
+ * webhook-id: <msg_id>
6370
+ * webhook-timestamp: <unix_seconds>
6371
+ * webhook-signature: v1,<base64_hmac_sha256> [v1,<base64_2> ...]
6372
+ *
6373
+ * Signed payload format: "{msg_id}.{timestamp}.{raw_body}"
6374
+ *
6375
+ * Secrets are base64-encoded, optionally prefixed with "whsec_".
6376
+ */
6340
6377
  const WHSEC_PREFIX = "whsec_";
6341
6378
  const BASE64_PATTERN$1 = /^[A-Za-z0-9+/]*={0,2}$/;
6342
6379
  /** Default max age for webhook requests (5 minutes) */
6343
- const DEFAULT_TOLERANCE_SECONDS = 5 * 60;
6380
+ const DEFAULT_TOLERANCE_SECONDS = 300;
6344
6381
  /** Future clock skew tolerance (1 minute) */
6345
6382
  const FUTURE_TOLERANCE_SECONDS = 60;
6346
6383
  /** Standard Webhooks header names */
@@ -6361,7 +6398,7 @@ function prepareStandardWebhooksSecret(secret) {
6361
6398
  return secret;
6362
6399
  }
6363
6400
  let keyStr = secret;
6364
- if (keyStr.startsWith(WHSEC_PREFIX)) keyStr = keyStr.slice(WHSEC_PREFIX.length);
6401
+ if (keyStr.startsWith(WHSEC_PREFIX)) keyStr = keyStr.slice(6);
6365
6402
  if (!keyStr || !BASE64_PATTERN$1.test(keyStr)) throw new WebhookVerificationError("MISSING_SECRET", "Standard Webhooks secret must be base64-encoded (optionally with whsec_ prefix)");
6366
6403
  const decoded = Buffer.from(keyStr, "base64");
6367
6404
  if (decoded.length === 0) throw new WebhookVerificationError("MISSING_SECRET", "Webhook secret is required but was empty or not provided");
@@ -6403,9 +6440,8 @@ function signStandardWebhooksPayload(rawBody, secret, msgId, timestamp) {
6403
6440
  const key = prepareStandardWebhooksSecret(secret);
6404
6441
  if (key.length === 0) throw new WebhookVerificationError("MISSING_SECRET", "Webhook secret is required but was empty or not provided");
6405
6442
  const signedPayload = `${msgId}.${ts}.${body}`;
6406
- const sig = createHmac("sha256", key).update(signedPayload).digest("base64");
6407
6443
  return {
6408
- signature: `v1,${sig}`,
6444
+ signature: `v1,${createHmac("sha256", key).update(signedPayload).digest("base64")}`,
6409
6445
  msgId,
6410
6446
  timestamp: ts
6411
6447
  };
@@ -6422,12 +6458,10 @@ function verifyStandardWebhooksSignature(opts) {
6422
6458
  if (!timestampStr || !/^\d+$/.test(timestampStr)) throw new WebhookVerificationError("INVALID_SIGNATURE_HEADER", `Invalid webhook-timestamp header: "${timestampStr}". Expected a unix timestamp in seconds`);
6423
6459
  const timestamp = Number(timestampStr);
6424
6460
  if (!Number.isInteger(timestamp) || timestamp < 0) throw new WebhookVerificationError("INVALID_SIGNATURE_HEADER", `Invalid webhook-timestamp header: "${timestampStr}". Expected a unix timestamp in seconds`);
6425
- const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
6426
- const age = now - timestamp;
6461
+ const age = (nowSeconds ?? Math.floor(Date.now() / 1e3)) - timestamp;
6427
6462
  if (age > toleranceSeconds) throw new WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", `Webhook timestamp too old (${age}s). Max age is ${toleranceSeconds}s.`);
6428
6463
  if (age < -FUTURE_TOLERANCE_SECONDS) throw new WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", "Webhook timestamp is too far in the future. Check server clock sync.");
6429
- const body = typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "request body");
6430
- const signedPayload = `${msgId}.${timestamp}.${body}`;
6464
+ const signedPayload = `${msgId}.${timestamp}.${typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "request body")}`;
6431
6465
  const expectedSig = createHmac("sha256", key).update(signedPayload).digest("base64");
6432
6466
  const signatures = parseStandardWebhooksSignatures(signatureHeader);
6433
6467
  if (signatures.length === 0) throw new WebhookVerificationError("INVALID_SIGNATURE_HEADER", "Invalid webhook-signature header format. Expected: \"v1,<base64>\"");
@@ -6439,7 +6473,6 @@ function verifyStandardWebhooksSignature(opts) {
6439
6473
  }
6440
6474
  throw new WebhookVerificationError("SIGNATURE_MISMATCH", "No valid signature found. Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).");
6441
6475
  }
6442
-
6443
6476
  //#endregion
6444
6477
  //#region src/schema.generated.ts
6445
6478
  const emailReceivedEventJsonSchema = {
@@ -7331,7 +7364,6 @@ const emailReceivedEventJsonSchema = {
7331
7364
  }
7332
7365
  }
7333
7366
  };
7334
-
7335
7367
  //#endregion
7336
7368
  //#region src/types.ts
7337
7369
  const EventType = { EmailReceived: "email.received" };
@@ -7380,7 +7412,6 @@ const AuthVerdict = {
7380
7412
  Suspicious: "suspicious",
7381
7413
  Unknown: "unknown"
7382
7414
  };
7383
-
7384
7415
  //#endregion
7385
7416
  //#region src/webhook/auth.ts
7386
7417
  /**
@@ -7577,7 +7608,6 @@ function validateEmailAuth(auth) {
7577
7608
  reasons: ["Unable to determine email authenticity"]
7578
7609
  };
7579
7610
  }
7580
-
7581
7611
  //#endregion
7582
7612
  //#region src/webhook/version.ts
7583
7613
  /**
@@ -7593,10 +7623,13 @@ function validateEmailAuth(auth) {
7593
7623
  * need to handle version-specific behavior.
7594
7624
  */
7595
7625
  const WEBHOOK_VERSION = "2025-12-14";
7596
-
7597
7626
  //#endregion
7598
7627
  //#region src/webhook/parsing.ts
7599
7628
  /**
7629
+ * JSON parsing utilities with helpful error messages.
7630
+ * @internal
7631
+ */
7632
+ /**
7600
7633
  * Parse a raw body string/Buffer into JSON with helpful error messages.
7601
7634
  *
7602
7635
  * Handles:
@@ -7617,12 +7650,10 @@ function parseJsonBody(rawBody) {
7617
7650
  return JSON.parse(cleanBody);
7618
7651
  } catch (e) {
7619
7652
  const jsonError = e;
7620
- const positionMatch = jsonError.message.match(/position\s*(\d+)/i);
7621
- const position = positionMatch?.[1];
7653
+ const position = jsonError.message.match(/position\s*(\d+)/i)?.[1];
7622
7654
  throw new WebhookPayloadError("JSON_PARSE_FAILED", "Failed to parse webhook body as JSON", position ? `Invalid JSON at position ${position}. Check your web framework isn't truncating the request body.` : `Invalid JSON: ${jsonError.message}. Check the raw request body is valid JSON.`, jsonError);
7623
7655
  }
7624
7656
  }
7625
-
7626
7657
  //#endregion
7627
7658
  //#region src/webhook/index.ts
7628
7659
  const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
@@ -7817,17 +7848,26 @@ function handleWebhook(options) {
7817
7848
  secret,
7818
7849
  toleranceSeconds
7819
7850
  });
7820
- else {
7821
- const signature = getSignatureHeader(headers);
7822
- verifyWebhookSignature({
7823
- rawBody: body,
7824
- signatureHeader: signature,
7825
- secret,
7826
- toleranceSeconds
7827
- });
7828
- }
7829
- const parsed = parseJsonBody(body);
7830
- return validateEmailReceivedEvent(parsed);
7851
+ else verifyWebhookSignature({
7852
+ rawBody: body,
7853
+ signatureHeader: getSignatureHeader(headers),
7854
+ secret,
7855
+ toleranceSeconds
7856
+ });
7857
+ return validateEmailReceivedEvent(parseJsonBody(body));
7858
+ }
7859
+ function receive(input, options) {
7860
+ if (input instanceof Request) return receiveFromRequest(input, options);
7861
+ return normalizeReceivedEmail(handleWebhook(input));
7862
+ }
7863
+ async function receiveFromRequest(request, options) {
7864
+ if (!options?.secret) throw new WebhookVerificationError("MISSING_SECRET", "Webhook secret is required but was empty or not provided");
7865
+ return normalizeReceivedEmail(handleWebhook({
7866
+ body: Buffer.from(await request.arrayBuffer()),
7867
+ headers: request.headers,
7868
+ secret: options.secret,
7869
+ toleranceSeconds: options.toleranceSeconds
7870
+ }));
7831
7871
  }
7832
7872
  /**
7833
7873
  * Returns headers for the optional "content discard" feature.
@@ -7885,8 +7925,7 @@ function confirmedHeaders() {
7885
7925
  * ```
7886
7926
  */
7887
7927
  function isDownloadExpired(event, now = Date.now()) {
7888
- const expiresAt = new Date(event.email.content.download.expires_at).getTime();
7889
- return now >= expiresAt;
7928
+ return now >= new Date(event.email.content.download.expires_at).getTime();
7890
7929
  }
7891
7930
  /**
7892
7931
  * Get the time remaining (in milliseconds) before the download URL expires.
@@ -8001,6 +8040,5 @@ function verifyRawEmailDownload(downloaded, event) {
8001
8040
  if (hash !== expected.toLowerCase()) throw new RawEmailDecodeError("HASH_MISMATCH", `SHA-256 hash mismatch. Expected: ${expected}, got: ${hash}. The downloaded content may be corrupted.`);
8002
8041
  return buffer;
8003
8042
  }
8004
-
8005
8043
  //#endregion
8006
- export { AuthConfidence, AuthVerdict, DkimResult, DmarcPolicy, DmarcResult, EventType, ForwardVerdict, LEGACY_CONFIRMED_HEADER, LEGACY_SIGNATURE_HEADER, PAYLOAD_ERRORS, PRIMITIVE_CONFIRMED_HEADER, PRIMITIVE_SIGNATURE_HEADER, ParsedStatus, PrimitiveWebhookError, RAW_EMAIL_ERRORS, RawEmailDecodeError, STANDARD_WEBHOOK_ID_HEADER, STANDARD_WEBHOOK_SIGNATURE_HEADER, STANDARD_WEBHOOK_TIMESTAMP_HEADER, SpfResult, VERIFICATION_ERRORS, WEBHOOK_VERSION, WebhookPayloadError, WebhookValidationError, WebhookVerificationError, confirmedHeaders, decodeRawEmail, emailReceivedEventJsonSchema, generateDownloadToken, getDownloadTimeRemaining, handleWebhook, isDownloadExpired, isEmailReceivedEvent, isRawIncluded, parseWebhookEvent, safeValidateEmailReceivedEvent, signStandardWebhooksPayload, signWebhookPayload, validateEmailAuth, validateEmailReceivedEvent, verifyDownloadToken, verifyRawEmailDownload, verifyStandardWebhooksSignature, verifyWebhookSignature };
8044
+ export { PRIMITIVE_CONFIRMED_HEADER as A, RAW_EMAIL_ERRORS as B, STANDARD_WEBHOOK_ID_HEADER as C, verifyStandardWebhooksSignature as D, signStandardWebhooksPayload as E, verifyDownloadToken as F, WebhookVerificationError as G, VERIFICATION_ERRORS as H, safeValidateEmailReceivedEvent as I, validateEmailReceivedEvent as L, signWebhookPayload as M, verifyWebhookSignature as N, LEGACY_CONFIRMED_HEADER as O, generateDownloadToken as P, PAYLOAD_ERRORS as R, emailReceivedEventJsonSchema as S, STANDARD_WEBHOOK_TIMESTAMP_HEADER as T, WebhookPayloadError as U, RawEmailDecodeError as V, WebhookValidationError as W, DmarcResult as _, isDownloadExpired as a, ParsedStatus as b, parseWebhookEvent as c, WEBHOOK_VERSION as d, validateEmailAuth as f, DmarcPolicy as g, DkimResult as h, handleWebhook as i, PRIMITIVE_SIGNATURE_HEADER as j, LEGACY_SIGNATURE_HEADER as k, receive as l, AuthVerdict as m, decodeRawEmail as n, isEmailReceivedEvent as o, AuthConfidence as p, getDownloadTimeRemaining as r, isRawIncluded as s, confirmedHeaders as t, verifyRawEmailDownload as u, EventType as v, STANDARD_WEBHOOK_SIGNATURE_HEADER as w, SpfResult as x, ForwardVerdict as y, PrimitiveWebhookError as z };
@@ -173,7 +173,7 @@
173
173
  "account:update-account": {
174
174
  "aliases": [],
175
175
  "args": {},
176
- "description": "PATCH /account",
176
+ "description": "PATCH /account\n\nBody fields (JSON --body):\n discard_content_on_webhook_confirmed boolean Whether to discard email content after the webhook endpoint confirms receipt.\n spam_threshold number? Global spam score threshold (0-15). Emails scoring above this are rejected....\n(* = required)",
177
177
  "flags": {
178
178
  "api-key": {
179
179
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -219,7 +219,7 @@
219
219
  "domains:add-domain": {
220
220
  "aliases": [],
221
221
  "args": {},
222
- "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n",
222
+ "description": "Creates an unverified domain claim. You will receive a\n`verification_token` to add as a DNS TXT record before\ncalling the verify endpoint.\n\n\nBody fields (JSON --body):\n * domain string The domain name to claim (e.g. \"example.com\")\n(* = required)",
223
223
  "flags": {
224
224
  "api-key": {
225
225
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -337,7 +337,7 @@
337
337
  "domains:update-domain": {
338
338
  "aliases": [],
339
339
  "args": {},
340
- "description": "Update a verified domain's settings. Only verified domains can be\nupdated. Per-domain spam thresholds require a Pro plan.\n",
340
+ "description": "Update a verified domain's settings. Only verified domains can be\nupdated. Per-domain spam thresholds require a Pro plan.\n\n\nBody fields (JSON --body):\n is_active boolean Whether the domain accepts incoming emails\n spam_threshold number? Per-domain spam threshold override (Pro plan required)\n(* = required)",
341
341
  "flags": {
342
342
  "api-key": {
343
343
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -755,7 +755,7 @@
755
755
  "endpoints:create-endpoint": {
756
756
  "aliases": [],
757
757
  "args": {},
758
- "description": "Creates a new webhook endpoint. If a deactivated endpoint with the\nsame URL and domain exists, it is reactivated instead.\nSubject to plan limits on the number of active endpoints.\n",
758
+ "description": "Creates a new webhook endpoint. If a deactivated endpoint with the\nsame URL and domain exists, it is reactivated instead.\nSubject to plan limits on the number of active endpoints.\n\n\nBody fields (JSON --body):\n * url string The webhook URL to deliver events to\n domain_id string? Restrict to emails from a specific domain\n enabled boolean Whether the endpoint is active\n rules object Endpoint-specific filtering rules\n(* = required)",
759
759
  "flags": {
760
760
  "api-key": {
761
761
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -913,7 +913,7 @@
913
913
  "endpoints:update-endpoint": {
914
914
  "aliases": [],
915
915
  "args": {},
916
- "description": "Updates an active webhook endpoint. If the URL is changed, the old\nendpoint is deactivated and a new one is created (or an existing\ndeactivated endpoint with the new URL is reactivated).\n",
916
+ "description": "Updates an active webhook endpoint. If the URL is changed, the old\nendpoint is deactivated and a new one is created (or an existing\ndeactivated endpoint with the new URL is reactivated).\n\n\nBody fields (JSON --body):\n domain_id string?\n enabled boolean\n rules object\n url string New webhook URL (triggers endpoint rotation)\n(* = required)",
917
917
  "flags": {
918
918
  "api-key": {
919
919
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -967,7 +967,7 @@
967
967
  "filters:create-filter": {
968
968
  "aliases": [],
969
969
  "args": {},
970
- "description": "Creates a new whitelist or blocklist filter. Per-domain filters\nrequire a Pro plan. Patterns are stored as lowercase.\n",
970
+ "description": "Creates a new whitelist or blocklist filter. Per-domain filters\nrequire a Pro plan. Patterns are stored as lowercase.\n\n\nBody fields (JSON --body):\n * pattern string Email address or pattern to filter\n * type string\n domain_id string? Restrict filter to a specific domain (Pro plan required)\n(* = required)",
971
971
  "flags": {
972
972
  "api-key": {
973
973
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1085,7 +1085,7 @@
1085
1085
  "filters:update-filter": {
1086
1086
  "aliases": [],
1087
1087
  "args": {},
1088
- "description": "Toggle a filter's enabled state.",
1088
+ "description": "Toggle a filter's enabled state.\n\nBody fields (JSON --body):\n * enabled boolean\n(* = required)",
1089
1089
  "flags": {
1090
1090
  "api-key": {
1091
1091
  "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
@@ -1136,6 +1136,52 @@
1136
1136
  "summary": "Update a filter rule",
1137
1137
  "enableJsonFlag": false
1138
1138
  },
1139
+ "sending:send-email": {
1140
+ "aliases": [],
1141
+ "args": {},
1142
+ "description": "Sends an outbound email through Primitive's outbound relay. By default\nthe request returns once the relay accepts the message for delivery.\nSet `wait: true` to wait for the first downstream SMTP delivery outcome.\n\n\nBody fields (JSON --body):\n * from string RFC 5322 From header. The sender domain must be a verified outbound domain ...\n * subject string Subject line for the outbound message\n * to string Recipient address. Recipient eligibility depends on your account's outbound...\n body_html string HTML message body. At least one of body_text or body_html is required. The ...\n body_text string Plain-text message body. At least one of body_text or body_html is required...\n in_reply_to string Message-ID of the direct parent email when sending a threaded reply.\n references array<string> Full ordered message-id chain for the thread.\n wait boolean When true, wait for the first downstream SMTP delivery outcome before retur...\n wait_timeout_ms integer Maximum time to wait for a delivery outcome when wait is true. Defaults to ...\n(* = required)",
1143
+ "flags": {
1144
+ "api-key": {
1145
+ "description": "Primitive API key (defaults to PRIMITIVE_API_KEY)",
1146
+ "env": "PRIMITIVE_API_KEY",
1147
+ "name": "api-key",
1148
+ "hasDynamicHelp": false,
1149
+ "multiple": false,
1150
+ "type": "option"
1151
+ },
1152
+ "base-url": {
1153
+ "description": "API base URL (defaults to PRIMITIVE_API_URL or production)",
1154
+ "env": "PRIMITIVE_API_URL",
1155
+ "name": "base-url",
1156
+ "hasDynamicHelp": false,
1157
+ "multiple": false,
1158
+ "type": "option"
1159
+ },
1160
+ "body": {
1161
+ "description": "JSON request body",
1162
+ "name": "body",
1163
+ "hasDynamicHelp": false,
1164
+ "multiple": false,
1165
+ "type": "option"
1166
+ },
1167
+ "body-file": {
1168
+ "description": "Path to a JSON file used as the request body",
1169
+ "name": "body-file",
1170
+ "hasDynamicHelp": false,
1171
+ "multiple": false,
1172
+ "type": "option"
1173
+ }
1174
+ },
1175
+ "hasDynamicHelp": false,
1176
+ "hiddenAliases": [],
1177
+ "id": "sending:send-email",
1178
+ "pluginAlias": "@primitivedotdev/sdk",
1179
+ "pluginName": "@primitivedotdev/sdk",
1180
+ "pluginType": "core",
1181
+ "strict": true,
1182
+ "summary": "Send outbound email",
1183
+ "enableJsonFlag": false
1184
+ },
1139
1185
  "webhook-deliveries:list-deliveries": {
1140
1186
  "aliases": [],
1141
1187
  "args": {},
@@ -1263,5 +1309,5 @@
1263
1309
  "enableJsonFlag": false
1264
1310
  }
1265
1311
  },
1266
- "version": "0.7.0"
1312
+ "version": "0.9.0"
1267
1313
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitivedotdev/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Official Primitive Node.js SDK — webhook, api, openapi, contract, and parser modules",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -68,6 +68,9 @@
68
68
  "emails": {
69
69
  "description": "List, inspect, and manage received emails"
70
70
  },
71
+ "sending": {
72
+ "description": "Send outbound emails through the Primitive API"
73
+ },
71
74
  "endpoints": {
72
75
  "description": "Manage webhook endpoints that receive email events"
73
76
  },
@@ -149,7 +152,7 @@
149
152
  "json-schema-to-typescript": "^15.0.4",
150
153
  "oclif": "^4.23.0",
151
154
  "shx": "^0.3.4",
152
- "tsdown": "^0.9.1",
155
+ "tsdown": "^0.21.10",
153
156
  "tsx": "^4.21.0",
154
157
  "typescript": "^5.7.2",
155
158
  "vite": "^8.0.8",
@@ -1,11 +0,0 @@
1
- //#region rolldown:runtime
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all) __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true
7
- });
8
- };
9
-
10
- //#endregion
11
- export { __export };