@primitivedotdev/sdk 0.6.0 → 0.8.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/dist/api/index.js CHANGED
@@ -4,9 +4,15 @@
4
4
  * Generated operations are exported directly, and `PrimitiveApiClient`
5
5
  * provides a configured fetch client for those operations.
6
6
  */
7
+ import { formatAddress } from "../webhook/received-email.js";
7
8
  import { createClient, createConfig, } from "./generated/client/index.js";
8
9
  import * as generatedOperations from "./generated/sdk.gen.js";
9
10
  export const DEFAULT_BASE_URL = "https://www.primitive.dev/api/v1";
11
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
+ const MAX_THREAD_REFERENCES = 100;
13
+ const MAX_THREAD_HEADER_BYTES = 8 * 1024;
14
+ const MAX_FROM_HEADER_LENGTH = 998;
15
+ const MAX_TO_HEADER_LENGTH = 320;
10
16
  function createDefaultAuth(apiKey) {
11
17
  return (security) => {
12
18
  if (security.type === "http" && security.scheme === "bearer") {
@@ -15,6 +21,134 @@ function createDefaultAuth(apiKey) {
15
21
  return undefined;
16
22
  };
17
23
  }
24
+ function validateAddressHeader(field, value) {
25
+ const trimmed = value.trim();
26
+ const maxLength = field === "from" ? MAX_FROM_HEADER_LENGTH : MAX_TO_HEADER_LENGTH;
27
+ if (trimmed.length < 3) {
28
+ throw new TypeError(`${field} must be at least 3 characters`);
29
+ }
30
+ if (trimmed.length > maxLength) {
31
+ throw new TypeError(`${field} must be at most ${maxLength} characters`);
32
+ }
33
+ }
34
+ function validateEmailAddress(field, value) {
35
+ if (!EMAIL_REGEX.test(value) &&
36
+ !/^.+<[^\s@]+@[^\s@]+\.[^\s@]+>$/.test(value)) {
37
+ throw new TypeError(`${field} must be a valid email address`);
38
+ }
39
+ }
40
+ function validateThreadHeaderValue(field, value) {
41
+ if (value.trim().length === 0) {
42
+ throw new TypeError(`${field} must be a non-empty string`);
43
+ }
44
+ if ([...value].some((char) => {
45
+ const code = char.charCodeAt(0);
46
+ return code <= 0x1f || code === 0x7f;
47
+ })) {
48
+ throw new TypeError(`${field} must not contain control characters`);
49
+ }
50
+ if (value.length > 998) {
51
+ throw new TypeError(`${field} must be at most 998 characters`);
52
+ }
53
+ }
54
+ function validateSendInput(input) {
55
+ validateAddressHeader("from", input.from);
56
+ validateAddressHeader("to", input.to);
57
+ validateEmailAddress("to", input.to);
58
+ if (input.subject.trim().length === 0) {
59
+ throw new TypeError("subject must be a non-empty string");
60
+ }
61
+ if (!input.bodyText && !input.bodyHtml) {
62
+ throw new TypeError("one of bodyText or bodyHtml is required");
63
+ }
64
+ if (input.thread?.inReplyTo) {
65
+ validateThreadHeaderValue("thread.inReplyTo", input.thread.inReplyTo);
66
+ }
67
+ if (input.thread?.references) {
68
+ if (input.thread.references.length > MAX_THREAD_REFERENCES) {
69
+ throw new TypeError(`thread.references must contain at most ${MAX_THREAD_REFERENCES} values`);
70
+ }
71
+ for (const [index, reference] of input.thread.references.entries()) {
72
+ validateThreadHeaderValue(`thread.references[${index}]`, reference);
73
+ }
74
+ if (input.thread.references.join(" ").length > MAX_THREAD_HEADER_BYTES) {
75
+ throw new TypeError(`thread.references header must be at most ${MAX_THREAD_HEADER_BYTES} characters`);
76
+ }
77
+ }
78
+ if (input.waitTimeoutMs !== undefined) {
79
+ if (!Number.isInteger(input.waitTimeoutMs)) {
80
+ throw new TypeError("waitTimeoutMs must be an integer");
81
+ }
82
+ if (input.waitTimeoutMs < 1000 || input.waitTimeoutMs > 30000) {
83
+ throw new TypeError("waitTimeoutMs must be between 1000 and 30000");
84
+ }
85
+ }
86
+ }
87
+ function validateForwardInput(input) {
88
+ validateEmailAddress("to", input.to);
89
+ if (input.subject !== undefined && input.subject.trim().length === 0) {
90
+ throw new TypeError("subject must be a non-empty string");
91
+ }
92
+ }
93
+ function parseApiErrorPayload(payload) {
94
+ const fallback = {
95
+ message: "Primitive API request failed",
96
+ code: undefined,
97
+ gates: undefined,
98
+ requestId: undefined,
99
+ details: undefined,
100
+ };
101
+ if (!payload || typeof payload !== "object") {
102
+ return fallback;
103
+ }
104
+ if ("error" in payload &&
105
+ payload.error &&
106
+ typeof payload.error === "object") {
107
+ const err = payload.error;
108
+ return {
109
+ message: typeof err.message === "string" ? err.message : fallback.message,
110
+ code: typeof err.code === "string" ? err.code : undefined,
111
+ gates: Array.isArray(err.gates) ? err.gates : undefined,
112
+ requestId: typeof err.request_id === "string" ? err.request_id : undefined,
113
+ details: err.details && typeof err.details === "object"
114
+ ? err.details
115
+ : undefined,
116
+ };
117
+ }
118
+ if ("message" in payload && typeof payload.message === "string") {
119
+ return { ...fallback, message: payload.message };
120
+ }
121
+ return fallback;
122
+ }
123
+ export class PrimitiveApiError extends Error {
124
+ status;
125
+ code;
126
+ gates;
127
+ requestId;
128
+ retryAfter;
129
+ details;
130
+ payload;
131
+ constructor(message, options) {
132
+ super(message);
133
+ this.name = "PrimitiveApiError";
134
+ this.payload = options.payload;
135
+ this.status = options.status;
136
+ this.code = options.code;
137
+ this.gates = options.gates;
138
+ this.requestId = options.requestId;
139
+ this.retryAfter = options.retryAfter;
140
+ this.details = options.details;
141
+ }
142
+ }
143
+ function parseRetryAfterHeader(response) {
144
+ if (!response)
145
+ return undefined;
146
+ const raw = response.headers.get("retry-after");
147
+ if (!raw)
148
+ return undefined;
149
+ const seconds = Number.parseInt(raw, 10);
150
+ return Number.isFinite(seconds) ? seconds : undefined;
151
+ }
18
152
  export class PrimitiveApiClient {
19
153
  client;
20
154
  constructor(options = {}) {
@@ -32,8 +166,129 @@ export class PrimitiveApiClient {
32
166
  return this.client.setConfig(config);
33
167
  }
34
168
  }
169
+ export class PrimitiveClient extends PrimitiveApiClient {
170
+ async send(input) {
171
+ validateSendInput(input);
172
+ const body = {
173
+ from: input.from,
174
+ to: input.to,
175
+ subject: input.subject,
176
+ ...(input.bodyText !== undefined ? { body_text: input.bodyText } : {}),
177
+ ...(input.bodyHtml !== undefined ? { body_html: input.bodyHtml } : {}),
178
+ ...(input.thread?.inReplyTo
179
+ ? { in_reply_to: input.thread.inReplyTo }
180
+ : {}),
181
+ ...(input.thread?.references?.length
182
+ ? { references: input.thread.references }
183
+ : {}),
184
+ ...(input.wait !== undefined ? { wait: input.wait } : {}),
185
+ ...(input.waitTimeoutMs !== undefined
186
+ ? { wait_timeout_ms: input.waitTimeoutMs }
187
+ : {}),
188
+ };
189
+ const result = await generatedOperations.sendEmail({
190
+ body,
191
+ ...(input.idempotencyKey
192
+ ? { headers: { "Idempotency-Key": input.idempotencyKey } }
193
+ : {}),
194
+ client: this.client,
195
+ responseStyle: "fields",
196
+ });
197
+ const response = result.response;
198
+ if (result.error) {
199
+ const parsed = parseApiErrorPayload(result.error);
200
+ throw new PrimitiveApiError(parsed.message, {
201
+ payload: result.error,
202
+ status: response?.status,
203
+ code: parsed.code,
204
+ gates: parsed.gates,
205
+ requestId: parsed.requestId,
206
+ retryAfter: parseRetryAfterHeader(response),
207
+ details: parsed.details,
208
+ });
209
+ }
210
+ if (!result.data?.data) {
211
+ throw new PrimitiveApiError("Primitive API returned no send result", {
212
+ payload: result,
213
+ status: response?.status,
214
+ });
215
+ }
216
+ return mapSendResult(result.data.data);
217
+ }
218
+ async reply(email, input) {
219
+ const resolved = typeof input === "string" ? { text: input } : input;
220
+ return this.send({
221
+ from: resolved.from ?? email.receivedBy,
222
+ to: email.replyTarget.address,
223
+ subject: resolved.subject ?? email.replySubject,
224
+ bodyText: resolved.text,
225
+ thread: {
226
+ ...(email.thread.messageId
227
+ ? { inReplyTo: email.thread.messageId }
228
+ : {}),
229
+ references: email.thread.messageId
230
+ ? [...email.thread.references, email.thread.messageId]
231
+ : email.thread.references,
232
+ },
233
+ });
234
+ }
235
+ async forward(email, input) {
236
+ validateForwardInput(input);
237
+ return this.send({
238
+ from: input.from ?? email.receivedBy,
239
+ to: input.to,
240
+ subject: input.subject ?? email.forwardSubject,
241
+ bodyText: buildForwardText(email, input.bodyText),
242
+ });
243
+ }
244
+ }
245
+ function buildForwardText(email, intro) {
246
+ const lines = [
247
+ ...(intro ? [intro.trim(), ""] : []),
248
+ "---------- Forwarded message ----------",
249
+ `From: ${formatAddress(email.sender)}`,
250
+ `To: ${email.raw.email.headers.to}`,
251
+ `Subject: ${email.subject ?? ""}`,
252
+ ...(email.raw.email.headers.date
253
+ ? [`Date: ${email.raw.email.headers.date}`]
254
+ : []),
255
+ ...(email.thread.messageId
256
+ ? [`Message-ID: ${email.thread.messageId}`]
257
+ : []),
258
+ "",
259
+ email.text ?? "",
260
+ ];
261
+ return lines.join("\n").trimEnd();
262
+ }
263
+ function mapSendResult(result) {
264
+ return {
265
+ id: result.id,
266
+ status: result.status,
267
+ queueId: result.queue_id,
268
+ accepted: result.accepted,
269
+ rejected: result.rejected,
270
+ clientIdempotencyKey: result.client_idempotency_key,
271
+ requestId: result.request_id,
272
+ contentHash: result.content_hash,
273
+ ...(result.delivery_status !== undefined
274
+ ? { deliveryStatus: result.delivery_status }
275
+ : {}),
276
+ ...(result.smtp_response_code !== undefined
277
+ ? { smtpResponseCode: result.smtp_response_code }
278
+ : {}),
279
+ ...(result.smtp_response_text !== undefined
280
+ ? { smtpResponseText: result.smtp_response_text }
281
+ : {}),
282
+ };
283
+ }
35
284
  export function createPrimitiveApiClient(options = {}) {
36
285
  return new PrimitiveApiClient(options);
37
286
  }
287
+ export function createPrimitiveClient(options = {}) {
288
+ return new PrimitiveClient(options);
289
+ }
290
+ export function client(options = {}) {
291
+ return new PrimitiveClient(options);
292
+ }
38
293
  export const operations = generatedOperations;
39
294
  export * from "./generated/index.js";