@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.
- package/README.md +114 -210
- package/dist/address-parser-BYn8oW5r.js +111 -0
- package/dist/api/generated/index.js +1 -1
- package/dist/api/generated/sdk.gen.js +17 -0
- package/dist/api/index.d.ts +2 -1877
- package/dist/api/index.js +255 -0
- package/dist/api-COSr-Fqm.js +1311 -0
- package/dist/chunk-pbuEa-1d.js +13 -0
- package/dist/contract/index.d.ts +6 -8
- package/dist/contract/index.js +28 -15
- package/dist/{index-DLmAI4UQ.d.ts → index-CbEivn3S.d.ts} +13 -30
- package/dist/index-DVow4Fjd.d.ts +2140 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +10 -3
- package/dist/oclif/api-command.js +89 -1
- package/dist/openapi/index.d.ts +8 -3
- package/dist/openapi/openapi.generated.js +412 -1
- package/dist/openapi/operations.generated.js +255 -0
- package/dist/parser/address-parser.js +129 -0
- package/dist/parser/index.d.ts +4 -19
- package/dist/parser/index.js +7 -122
- package/dist/received-email-D6tKtWwW.js +69 -0
- package/dist/received-email-DNjpq_Wt.d.ts +37 -0
- package/dist/{types-CKFmgitP.d.ts → types-9vXGZjPd.d.ts} +3 -19
- package/dist/types.generated.js +7 -0
- package/dist/types.js +53 -0
- package/dist/webhook/index.d.ts +4 -3
- package/dist/webhook/index.js +3 -3
- package/dist/webhook/received-email.js +82 -0
- package/dist/{webhook-COe5N_Uj.js → webhook-zkN4wUTs.js} +119 -81
- package/oclif.manifest.json +54 -8
- package/package.json +5 -2
- package/dist/chunk-Cl8Af3a2.js +0 -11
|
@@ -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
|
|
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 ${
|
|
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} ${
|
|
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
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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,${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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 };
|
package/oclif.manifest.json
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
155
|
+
"tsdown": "^0.21.10",
|
|
153
156
|
"tsx": "^4.21.0",
|
|
154
157
|
"typescript": "^5.7.2",
|
|
155
158
|
"vite": "^8.0.8",
|
package/dist/chunk-Cl8Af3a2.js
DELETED