@mailsai/sdk 0.1.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/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/index.d.ts +686 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +674 -0
- package/package.json +38 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
// @mailsai/sdk — TypeScript client for the Mails.ai API.
|
|
2
|
+
// 6 lines to give an AI agent a working email primitive:
|
|
3
|
+
//
|
|
4
|
+
// import { mails } from "@mailsai/sdk";
|
|
5
|
+
// const sarah = mails.agent("sarah");
|
|
6
|
+
// await sarah.send({ to: "lead@acme.com", subject: "Demo", body: "Hi…" });
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
const DEFAULT_BASE = "https://api.mails.ai";
|
|
9
|
+
// Bounded exponential backoff for transient failures (429 / retry-safe 5xx).
|
|
10
|
+
const MAX_RETRIES = 3;
|
|
11
|
+
const RETRY_BASE_MS = 500;
|
|
12
|
+
const RETRY_CAP_MS = 8000;
|
|
13
|
+
class MailsError extends Error {
|
|
14
|
+
type;
|
|
15
|
+
code;
|
|
16
|
+
status;
|
|
17
|
+
requestId;
|
|
18
|
+
param;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
super(opts.message);
|
|
21
|
+
this.name = "MailsError";
|
|
22
|
+
this.type = opts.type;
|
|
23
|
+
this.code = opts.code;
|
|
24
|
+
this.status = opts.status;
|
|
25
|
+
this.requestId = opts.requestId;
|
|
26
|
+
this.param = opts.param;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class MailsClient {
|
|
30
|
+
apiKey;
|
|
31
|
+
baseUrl;
|
|
32
|
+
fetchImpl;
|
|
33
|
+
messages;
|
|
34
|
+
received;
|
|
35
|
+
threads;
|
|
36
|
+
drafts;
|
|
37
|
+
events;
|
|
38
|
+
agents;
|
|
39
|
+
suppression;
|
|
40
|
+
api_keys;
|
|
41
|
+
webhooks;
|
|
42
|
+
reputation;
|
|
43
|
+
billing;
|
|
44
|
+
logs;
|
|
45
|
+
constructor(opts = {}) {
|
|
46
|
+
const envKey = typeof process !== "undefined" ? process.env?.MAILS_API_KEY : undefined;
|
|
47
|
+
const envBase = typeof process !== "undefined" ? process.env?.MAILS_BASE_URL : undefined;
|
|
48
|
+
this.apiKey = opts.apiKey ?? envKey ?? "";
|
|
49
|
+
this.baseUrl = (opts.baseUrl ?? envBase ?? DEFAULT_BASE).replace(/\/+$/, "");
|
|
50
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
51
|
+
if (!this.apiKey) {
|
|
52
|
+
throw new Error("Mails.ai SDK: apiKey not provided and MAILS_API_KEY env var not set.");
|
|
53
|
+
}
|
|
54
|
+
this.messages = new MessagesResource(this);
|
|
55
|
+
this.received = new ReceivedResource(this);
|
|
56
|
+
this.threads = new ThreadsResource(this);
|
|
57
|
+
this.drafts = new DraftsResource(this);
|
|
58
|
+
this.events = new EventsResource(this);
|
|
59
|
+
this.agents = new AgentsResource(this);
|
|
60
|
+
this.suppression = new SuppressionResource(this);
|
|
61
|
+
this.api_keys = new ApiKeysResource(this);
|
|
62
|
+
this.webhooks = new WebhooksResource(this);
|
|
63
|
+
this.reputation = new ReputationResource(this);
|
|
64
|
+
this.billing = new BillingResource(this);
|
|
65
|
+
this.logs = new LogsResource(this);
|
|
66
|
+
}
|
|
67
|
+
agent(name) {
|
|
68
|
+
return new Agent(this, name);
|
|
69
|
+
}
|
|
70
|
+
async send(agent, input) {
|
|
71
|
+
return this.messages.send(agent, input);
|
|
72
|
+
}
|
|
73
|
+
async createAgent(name, options) {
|
|
74
|
+
return this.agents.create(name, options);
|
|
75
|
+
}
|
|
76
|
+
async listAgents(opts) {
|
|
77
|
+
return this.agents.list(opts);
|
|
78
|
+
}
|
|
79
|
+
async me() {
|
|
80
|
+
return this.request("GET", "/api/v1/me");
|
|
81
|
+
}
|
|
82
|
+
// Raw helpers used by resources
|
|
83
|
+
request(method, path, body, headersIn) {
|
|
84
|
+
return this._request(method, path, body, headersIn);
|
|
85
|
+
}
|
|
86
|
+
async _request(method, path, body, headersIn) {
|
|
87
|
+
const headers = {
|
|
88
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
...(headersIn ?? {}),
|
|
91
|
+
};
|
|
92
|
+
const payload = body !== undefined ? JSON.stringify(body) : undefined;
|
|
93
|
+
// A retry is safe when the request can't have applied a side effect twice:
|
|
94
|
+
// a 429 was REJECTED (rate-limited, never processed), and 5xx is only retried
|
|
95
|
+
// for read methods or when an Idempotency-Key makes a resend a no-op server-side.
|
|
96
|
+
const idempotentMethod = method === "GET" || method === "HEAD";
|
|
97
|
+
const hasIdemKey = Object.keys(headers).some((k) => k.toLowerCase() === "idempotency-key");
|
|
98
|
+
for (let attempt = 0;; attempt++) {
|
|
99
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, { method, headers, body: payload });
|
|
100
|
+
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
101
|
+
if (res.ok) {
|
|
102
|
+
if (res.status === 204)
|
|
103
|
+
return undefined;
|
|
104
|
+
return (await res.json());
|
|
105
|
+
}
|
|
106
|
+
const retryable = attempt < MAX_RETRIES && (res.status === 429 || (res.status >= 500 && (idempotentMethod || hasIdemKey)));
|
|
107
|
+
if (retryable) {
|
|
108
|
+
const retryAfter = Number(res.headers.get("retry-after"));
|
|
109
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
|
|
110
|
+
? retryAfter * 1000
|
|
111
|
+
: Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** attempt);
|
|
112
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let err = {};
|
|
116
|
+
try {
|
|
117
|
+
err = await res.json();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* ignore */
|
|
121
|
+
}
|
|
122
|
+
throw new MailsError({
|
|
123
|
+
type: err.error?.type ?? "api_error",
|
|
124
|
+
code: err.error?.code ?? "unknown",
|
|
125
|
+
message: err.error?.message ?? `${method} ${path} failed with ${res.status}`,
|
|
126
|
+
status: res.status,
|
|
127
|
+
requestId,
|
|
128
|
+
param: err.error?.param,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async requestText(method, path, body, headersIn) {
|
|
133
|
+
const headers = {
|
|
134
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
135
|
+
...(headersIn ?? {}),
|
|
136
|
+
};
|
|
137
|
+
if (body !== undefined)
|
|
138
|
+
headers["Content-Type"] = "application/json";
|
|
139
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
140
|
+
method,
|
|
141
|
+
headers,
|
|
142
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
let err = {};
|
|
146
|
+
try {
|
|
147
|
+
err = await res.json();
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
throw new MailsError({
|
|
153
|
+
type: err.error?.type ?? "api_error",
|
|
154
|
+
code: err.error?.code ?? "unknown",
|
|
155
|
+
message: err.error?.message ?? `${method} ${path} failed with ${res.status}`,
|
|
156
|
+
status: res.status,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return await res.text();
|
|
160
|
+
}
|
|
161
|
+
// SSE stream helper. Returns AbortController; caller closes via abort().
|
|
162
|
+
stream(opts) {
|
|
163
|
+
const ctrl = new AbortController();
|
|
164
|
+
if (opts.signal)
|
|
165
|
+
opts.signal.addEventListener("abort", () => ctrl.abort());
|
|
166
|
+
const qs = new URLSearchParams();
|
|
167
|
+
if (opts.since)
|
|
168
|
+
qs.set("since", opts.since);
|
|
169
|
+
if (opts.event_types)
|
|
170
|
+
qs.set("event_types", opts.event_types.join(","));
|
|
171
|
+
const url = `${this.baseUrl}/api/v1/events/stream${qs.toString() ? "?" + qs.toString() : ""}`;
|
|
172
|
+
void (async () => {
|
|
173
|
+
try {
|
|
174
|
+
const res = await this.fetchImpl(url, {
|
|
175
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, Accept: "text/event-stream" },
|
|
176
|
+
signal: ctrl.signal,
|
|
177
|
+
});
|
|
178
|
+
if (!res.ok || !res.body) {
|
|
179
|
+
opts.onError?.(new Error(`stream HTTP ${res.status}`));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const reader = res.body.getReader();
|
|
183
|
+
const decoder = new TextDecoder();
|
|
184
|
+
let buffer = "";
|
|
185
|
+
// eslint-disable-next-line no-constant-condition
|
|
186
|
+
while (true) {
|
|
187
|
+
const { done, value } = await reader.read();
|
|
188
|
+
if (done)
|
|
189
|
+
break;
|
|
190
|
+
buffer += decoder.decode(value, { stream: true });
|
|
191
|
+
let idx;
|
|
192
|
+
while ((idx = buffer.indexOf("\n\n")) !== -1) {
|
|
193
|
+
const chunk = buffer.slice(0, idx);
|
|
194
|
+
buffer = buffer.slice(idx + 2);
|
|
195
|
+
const dataLine = chunk.split("\n").find((l) => l.startsWith("data: "));
|
|
196
|
+
if (dataLine) {
|
|
197
|
+
try {
|
|
198
|
+
const evt = JSON.parse(dataLine.slice(6));
|
|
199
|
+
opts.onEvent(evt);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
/* ignore parse errors */
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (!ctrl.signal.aborted)
|
|
210
|
+
opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
return ctrl;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
class MessagesResource {
|
|
217
|
+
c;
|
|
218
|
+
constructor(c) {
|
|
219
|
+
this.c = c;
|
|
220
|
+
}
|
|
221
|
+
send(agent, input) {
|
|
222
|
+
const body = {
|
|
223
|
+
agent,
|
|
224
|
+
to: input.to,
|
|
225
|
+
cc: input.cc,
|
|
226
|
+
bcc: input.bcc,
|
|
227
|
+
subject: input.subject,
|
|
228
|
+
body_text: input.body_text ?? input.body,
|
|
229
|
+
body_html: input.body_html,
|
|
230
|
+
reply_to: input.reply_to,
|
|
231
|
+
in_reply_to_message_id: input.in_reply_to_message_id,
|
|
232
|
+
references: input.references,
|
|
233
|
+
attachments: input.attachments,
|
|
234
|
+
metadata: input.metadata,
|
|
235
|
+
pool_hint: input.pool_hint,
|
|
236
|
+
scheduled_at: input.scheduled_at,
|
|
237
|
+
tags: input.tags,
|
|
238
|
+
};
|
|
239
|
+
const headers = {};
|
|
240
|
+
if (input.idempotency_key)
|
|
241
|
+
headers["Idempotency-Key"] = input.idempotency_key;
|
|
242
|
+
return this.c.request("POST", "/api/v1/messages", body, headers);
|
|
243
|
+
}
|
|
244
|
+
batch(messages, opts) {
|
|
245
|
+
const headers = {};
|
|
246
|
+
if (opts?.idempotency_key)
|
|
247
|
+
headers["Idempotency-Key"] = opts.idempotency_key;
|
|
248
|
+
return this.c.request("POST", "/api/v1/messages/batch", messages, headers);
|
|
249
|
+
}
|
|
250
|
+
reply(id, input) {
|
|
251
|
+
return this.c.request("POST", `/api/v1/messages/${encodeURIComponent(id)}/reply`, { ...input, body_text: input.body_text ?? input.body });
|
|
252
|
+
}
|
|
253
|
+
forward(id, input) {
|
|
254
|
+
return this.c.request("POST", `/api/v1/messages/${encodeURIComponent(id)}/forward`, input);
|
|
255
|
+
}
|
|
256
|
+
cancel(id) {
|
|
257
|
+
return this.c.request("POST", `/api/v1/messages/${encodeURIComponent(id)}/cancel`);
|
|
258
|
+
}
|
|
259
|
+
reschedule(id, scheduled_at) {
|
|
260
|
+
return this.c.request("PATCH", `/api/v1/messages/${encodeURIComponent(id)}`, { scheduled_at });
|
|
261
|
+
}
|
|
262
|
+
raw(id) {
|
|
263
|
+
return this.c.requestText("GET", `/api/v1/messages/${encodeURIComponent(id)}/raw`);
|
|
264
|
+
}
|
|
265
|
+
list(opts) {
|
|
266
|
+
const qs = new URLSearchParams();
|
|
267
|
+
if (opts?.limit)
|
|
268
|
+
qs.set("limit", String(opts.limit));
|
|
269
|
+
if (opts?.cursor)
|
|
270
|
+
qs.set("cursor", opts.cursor);
|
|
271
|
+
if (opts?.agent_id)
|
|
272
|
+
qs.set("agent_id", opts.agent_id);
|
|
273
|
+
if (opts?.thread_id)
|
|
274
|
+
qs.set("thread_id", opts.thread_id);
|
|
275
|
+
return this.c.request("GET", `/api/v1/messages${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
276
|
+
}
|
|
277
|
+
get(id) {
|
|
278
|
+
return this.c.request("GET", `/api/v1/messages/${encodeURIComponent(id)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
class ReceivedResource {
|
|
282
|
+
c;
|
|
283
|
+
constructor(c) {
|
|
284
|
+
this.c = c;
|
|
285
|
+
}
|
|
286
|
+
list(opts) {
|
|
287
|
+
const qs = new URLSearchParams();
|
|
288
|
+
if (opts?.limit)
|
|
289
|
+
qs.set("limit", String(opts.limit));
|
|
290
|
+
if (opts?.cursor)
|
|
291
|
+
qs.set("cursor", opts.cursor);
|
|
292
|
+
if (opts?.agent_id)
|
|
293
|
+
qs.set("agent_id", opts.agent_id);
|
|
294
|
+
if (opts?.thread_id)
|
|
295
|
+
qs.set("thread_id", opts.thread_id);
|
|
296
|
+
return this.c.request("GET", `/api/v1/messages/received${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
297
|
+
}
|
|
298
|
+
get(id) {
|
|
299
|
+
return this.c.request("GET", `/api/v1/messages/received/${encodeURIComponent(id)}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
class ThreadsResource {
|
|
303
|
+
c;
|
|
304
|
+
constructor(c) {
|
|
305
|
+
this.c = c;
|
|
306
|
+
}
|
|
307
|
+
list(opts) {
|
|
308
|
+
const qs = new URLSearchParams();
|
|
309
|
+
if (opts?.limit)
|
|
310
|
+
qs.set("limit", String(opts.limit));
|
|
311
|
+
if (opts?.cursor)
|
|
312
|
+
qs.set("cursor", opts.cursor);
|
|
313
|
+
if (opts?.agent_id)
|
|
314
|
+
qs.set("agent_id", opts.agent_id);
|
|
315
|
+
if (opts?.status)
|
|
316
|
+
qs.set("status", opts.status);
|
|
317
|
+
return this.c.request("GET", `/api/v1/threads${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
318
|
+
}
|
|
319
|
+
get(id) {
|
|
320
|
+
return this.c.request("GET", `/api/v1/threads/${encodeURIComponent(id)}`);
|
|
321
|
+
}
|
|
322
|
+
update(id, patch) {
|
|
323
|
+
return this.c.request("PATCH", `/api/v1/threads/${encodeURIComponent(id)}`, patch);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
class DraftsResource {
|
|
327
|
+
c;
|
|
328
|
+
constructor(c) {
|
|
329
|
+
this.c = c;
|
|
330
|
+
}
|
|
331
|
+
create(input) {
|
|
332
|
+
return this.c.request("POST", "/api/v1/drafts", input);
|
|
333
|
+
}
|
|
334
|
+
list(opts) {
|
|
335
|
+
const qs = new URLSearchParams();
|
|
336
|
+
if (opts?.limit)
|
|
337
|
+
qs.set("limit", String(opts.limit));
|
|
338
|
+
if (opts?.cursor)
|
|
339
|
+
qs.set("cursor", opts.cursor);
|
|
340
|
+
if (opts?.status)
|
|
341
|
+
qs.set("status", opts.status);
|
|
342
|
+
return this.c.request("GET", `/api/v1/drafts${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
343
|
+
}
|
|
344
|
+
get(id) {
|
|
345
|
+
return this.c.request("GET", `/api/v1/drafts/${encodeURIComponent(id)}`);
|
|
346
|
+
}
|
|
347
|
+
update(id, patch) {
|
|
348
|
+
return this.c.request("PATCH", `/api/v1/drafts/${encodeURIComponent(id)}`, patch);
|
|
349
|
+
}
|
|
350
|
+
remove(id) {
|
|
351
|
+
return this.c.request("DELETE", `/api/v1/drafts/${encodeURIComponent(id)}`);
|
|
352
|
+
}
|
|
353
|
+
send(id, opts) {
|
|
354
|
+
return this.c.request("POST", `/api/v1/drafts/${encodeURIComponent(id)}/send`, opts ?? {});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
class EventsResource {
|
|
358
|
+
c;
|
|
359
|
+
constructor(c) {
|
|
360
|
+
this.c = c;
|
|
361
|
+
}
|
|
362
|
+
list(opts) {
|
|
363
|
+
const qs = new URLSearchParams();
|
|
364
|
+
if (opts?.limit)
|
|
365
|
+
qs.set("limit", String(opts.limit));
|
|
366
|
+
if (opts?.cursor)
|
|
367
|
+
qs.set("cursor", opts.cursor);
|
|
368
|
+
if (opts?.event_type)
|
|
369
|
+
qs.set("event_type", opts.event_type);
|
|
370
|
+
if (opts?.agent_id)
|
|
371
|
+
qs.set("agent_id", opts.agent_id);
|
|
372
|
+
if (opts?.since)
|
|
373
|
+
qs.set("since", opts.since);
|
|
374
|
+
return this.c.request("GET", `/api/v1/events${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
375
|
+
}
|
|
376
|
+
get(id) {
|
|
377
|
+
return this.c.request("GET", `/api/v1/events/${encodeURIComponent(id)}`);
|
|
378
|
+
}
|
|
379
|
+
redeliver(id) {
|
|
380
|
+
return this.c.request("POST", `/api/v1/events/${encodeURIComponent(id)}/redeliver`, {});
|
|
381
|
+
}
|
|
382
|
+
// Convenience — wraps client.stream
|
|
383
|
+
stream(opts) {
|
|
384
|
+
return this.c.stream(opts);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
class AgentsResource {
|
|
388
|
+
c;
|
|
389
|
+
constructor(c) {
|
|
390
|
+
this.c = c;
|
|
391
|
+
}
|
|
392
|
+
create(name, opts) {
|
|
393
|
+
return this.c.request("POST", "/api/v1/agents", { name, ...opts });
|
|
394
|
+
}
|
|
395
|
+
list(opts) {
|
|
396
|
+
const qs = new URLSearchParams();
|
|
397
|
+
if (opts?.limit)
|
|
398
|
+
qs.set("limit", String(opts.limit));
|
|
399
|
+
if (opts?.cursor)
|
|
400
|
+
qs.set("cursor", opts.cursor);
|
|
401
|
+
return this.c.request("GET", `/api/v1/agents${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
402
|
+
}
|
|
403
|
+
get(id) {
|
|
404
|
+
return this.c.request("GET", `/api/v1/agents/${encodeURIComponent(id)}`);
|
|
405
|
+
}
|
|
406
|
+
update(id, patch) {
|
|
407
|
+
return this.c.request("PATCH", `/api/v1/agents/${encodeURIComponent(id)}`, patch);
|
|
408
|
+
}
|
|
409
|
+
archive(id) {
|
|
410
|
+
return this.c.request("DELETE", `/api/v1/agents/${encodeURIComponent(id)}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
class SuppressionResource {
|
|
414
|
+
c;
|
|
415
|
+
constructor(c) {
|
|
416
|
+
this.c = c;
|
|
417
|
+
}
|
|
418
|
+
check(address) {
|
|
419
|
+
return this.c.request("GET", `/api/v1/suppression?address=${encodeURIComponent(address)}`);
|
|
420
|
+
}
|
|
421
|
+
list(opts) {
|
|
422
|
+
const qs = new URLSearchParams();
|
|
423
|
+
if (opts?.limit)
|
|
424
|
+
qs.set("limit", String(opts.limit));
|
|
425
|
+
if (opts?.cursor)
|
|
426
|
+
qs.set("cursor", opts.cursor);
|
|
427
|
+
return this.c.request("GET", `/api/v1/suppression${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
428
|
+
}
|
|
429
|
+
allow(address, attestation) {
|
|
430
|
+
return this.c.request("POST", "/api/v1/suppression/allow", { address, attestation });
|
|
431
|
+
}
|
|
432
|
+
revoke(id) {
|
|
433
|
+
return this.c.request("DELETE", `/api/v1/suppression/allow/${encodeURIComponent(id)}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
class ApiKeysResource {
|
|
437
|
+
c;
|
|
438
|
+
constructor(c) {
|
|
439
|
+
this.c = c;
|
|
440
|
+
}
|
|
441
|
+
create(input) {
|
|
442
|
+
return this.c.request("POST", "/api/v1/api-keys", input);
|
|
443
|
+
}
|
|
444
|
+
list(opts) {
|
|
445
|
+
const qs = new URLSearchParams();
|
|
446
|
+
if (opts?.limit)
|
|
447
|
+
qs.set("limit", String(opts.limit));
|
|
448
|
+
if (opts?.cursor)
|
|
449
|
+
qs.set("cursor", opts.cursor);
|
|
450
|
+
return this.c.request("GET", `/api/v1/api-keys${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
451
|
+
}
|
|
452
|
+
revoke(id) {
|
|
453
|
+
return this.c.request("DELETE", `/api/v1/api-keys/${encodeURIComponent(id)}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
class WebhooksResource {
|
|
457
|
+
c;
|
|
458
|
+
constructor(c) {
|
|
459
|
+
this.c = c;
|
|
460
|
+
}
|
|
461
|
+
create(input) {
|
|
462
|
+
return this.c.request("POST", "/api/v1/webhooks", input);
|
|
463
|
+
}
|
|
464
|
+
list(opts) {
|
|
465
|
+
const qs = new URLSearchParams();
|
|
466
|
+
if (opts?.limit)
|
|
467
|
+
qs.set("limit", String(opts.limit));
|
|
468
|
+
if (opts?.cursor)
|
|
469
|
+
qs.set("cursor", opts.cursor);
|
|
470
|
+
return this.c.request("GET", `/api/v1/webhooks${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
471
|
+
}
|
|
472
|
+
update(id, patch) {
|
|
473
|
+
return this.c.request("PATCH", `/api/v1/webhooks/${encodeURIComponent(id)}`, patch);
|
|
474
|
+
}
|
|
475
|
+
/** Send a signed test event to the endpoint to verify reachability + signature handling. */
|
|
476
|
+
test(id) {
|
|
477
|
+
return this.c.request("POST", `/api/v1/webhooks/${encodeURIComponent(id)}/test`);
|
|
478
|
+
}
|
|
479
|
+
remove(id) {
|
|
480
|
+
return this.c.request("DELETE", `/api/v1/webhooks/${encodeURIComponent(id)}`);
|
|
481
|
+
}
|
|
482
|
+
deliveries(webhookId, opts) {
|
|
483
|
+
const qs = new URLSearchParams();
|
|
484
|
+
if (opts?.limit)
|
|
485
|
+
qs.set("limit", String(opts.limit));
|
|
486
|
+
if (opts?.cursor)
|
|
487
|
+
qs.set("cursor", opts.cursor);
|
|
488
|
+
return this.c.request("GET", `/api/v1/webhooks/${encodeURIComponent(webhookId)}/deliveries${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
489
|
+
}
|
|
490
|
+
replayDelivery(deliveryId) {
|
|
491
|
+
return this.c.request("POST", `/api/v1/webhook-deliveries/${encodeURIComponent(deliveryId)}/replay`, {});
|
|
492
|
+
}
|
|
493
|
+
verify(body, signature, secret, toleranceSec = 300) {
|
|
494
|
+
try {
|
|
495
|
+
const parts = Object.fromEntries(signature.split(",").map((p) => p.split("=")));
|
|
496
|
+
const ts = parseInt(parts.t ?? "0", 10);
|
|
497
|
+
if (!Number.isFinite(ts))
|
|
498
|
+
return null;
|
|
499
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSec)
|
|
500
|
+
return null;
|
|
501
|
+
const expected = crypto.createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
|
|
502
|
+
const ok = crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1 ?? "", "hex"));
|
|
503
|
+
if (!ok)
|
|
504
|
+
return null;
|
|
505
|
+
return JSON.parse(body);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
class ReputationResource {
|
|
513
|
+
c;
|
|
514
|
+
constructor(c) {
|
|
515
|
+
this.c = c;
|
|
516
|
+
}
|
|
517
|
+
get(agentId) {
|
|
518
|
+
const qs = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
|
|
519
|
+
return this.c.request("GET", `/api/v1/reputation${qs}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
class BillingResource {
|
|
523
|
+
c;
|
|
524
|
+
constructor(c) {
|
|
525
|
+
this.c = c;
|
|
526
|
+
}
|
|
527
|
+
portal(returnUrl) {
|
|
528
|
+
return this.c.request("POST", "/api/v1/billing/portal", { return_url: returnUrl });
|
|
529
|
+
}
|
|
530
|
+
usage() {
|
|
531
|
+
return this.c.request("GET", "/api/v1/billing/usage");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
class LogsResource {
|
|
535
|
+
c;
|
|
536
|
+
constructor(c) {
|
|
537
|
+
this.c = c;
|
|
538
|
+
}
|
|
539
|
+
list(opts) {
|
|
540
|
+
const qs = new URLSearchParams();
|
|
541
|
+
if (opts?.limit)
|
|
542
|
+
qs.set("limit", String(opts.limit));
|
|
543
|
+
if (opts?.cursor)
|
|
544
|
+
qs.set("cursor", opts.cursor);
|
|
545
|
+
if (opts?.category)
|
|
546
|
+
qs.set("category", opts.category);
|
|
547
|
+
if (opts?.action)
|
|
548
|
+
qs.set("action", opts.action);
|
|
549
|
+
return this.c.request("GET", `/api/v1/logs${qs.toString() ? "?" + qs.toString() : ""}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
class Agent {
|
|
553
|
+
client;
|
|
554
|
+
name;
|
|
555
|
+
_id;
|
|
556
|
+
constructor(client, name) {
|
|
557
|
+
this.client = client;
|
|
558
|
+
this.name = name;
|
|
559
|
+
}
|
|
560
|
+
async send(input) {
|
|
561
|
+
return this.client.send(this.name, input);
|
|
562
|
+
}
|
|
563
|
+
async listReplies(opts) {
|
|
564
|
+
return this.client.events.list({ event_type: "reply.received", agent_id: this.name, ...opts });
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* List cold/first-contact inbound (`message.received`) for this agent — messages
|
|
568
|
+
* from senders that are NOT replies to one of your sends (new leads, support
|
|
569
|
+
* requests, strangers). Authenticated only; SPF+DKIM failures surface separately
|
|
570
|
+
* as `message.received.unauthenticated`. Mirrors {@link listReplies}.
|
|
571
|
+
*/
|
|
572
|
+
async listMessages(opts) {
|
|
573
|
+
return this.client.events.list({ event_type: "message.received", agent_id: this.name, ...opts });
|
|
574
|
+
}
|
|
575
|
+
async reputation() {
|
|
576
|
+
return this.client.reputation.get(this.name);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Subscribe to reply.received events for this agent. Opens an SSE stream
|
|
580
|
+
* filtered server-side to reply.received and dispatches to `callback` for
|
|
581
|
+
* events matching this agent. Resolves the agent name → id once on first
|
|
582
|
+
* matching event so subsequent events skip the lookup.
|
|
583
|
+
*
|
|
584
|
+
* @returns AbortController — call `.abort()` to stop listening. Caller can
|
|
585
|
+
* also pass `opts.signal` to chain into an existing abort flow.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* const ctrl = agent.onReply((event) => {
|
|
589
|
+
* console.log(event.intent, event.entities);
|
|
590
|
+
* });
|
|
591
|
+
* // …later
|
|
592
|
+
* ctrl.abort();
|
|
593
|
+
*/
|
|
594
|
+
onReply(callback, opts) {
|
|
595
|
+
return this.subscribe(["reply.received"], callback, opts);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Subscribe to cold/first-contact inbound (`message.received`) for this agent —
|
|
599
|
+
* new senders that are NOT replying to one of your sends. Same shape and
|
|
600
|
+
* agent-scoping as {@link onReply}; returns an AbortController.
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* const ctrl = agent.onMessage((event) => {
|
|
604
|
+
* if (event.intent === "sales_inquiry") routeToCrm(event);
|
|
605
|
+
* });
|
|
606
|
+
*/
|
|
607
|
+
onMessage(callback, opts) {
|
|
608
|
+
return this.subscribe(["message.received"], callback, opts);
|
|
609
|
+
}
|
|
610
|
+
/** Shared agent-scoped SSE subscription used by onReply/onMessage. */
|
|
611
|
+
subscribe(eventTypes, callback, opts) {
|
|
612
|
+
return this.client.stream({
|
|
613
|
+
event_types: eventTypes,
|
|
614
|
+
since: opts?.since,
|
|
615
|
+
signal: opts?.signal,
|
|
616
|
+
onError: opts?.onError,
|
|
617
|
+
onEvent: async (event) => {
|
|
618
|
+
try {
|
|
619
|
+
if (!this._id)
|
|
620
|
+
this._id = await this.resolveId();
|
|
621
|
+
if (event.agent_id && event.agent_id !== this._id)
|
|
622
|
+
return;
|
|
623
|
+
await callback(event);
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
opts?.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
async resolveId() {
|
|
632
|
+
// Look up the agent's UUID by friendly name. Cached on the instance so
|
|
633
|
+
// subsequent matches in the stream skip the round-trip.
|
|
634
|
+
const list = await this.client.agents.list({ limit: 100 });
|
|
635
|
+
const found = list.data.find((a) => a.name === this.name);
|
|
636
|
+
if (!found) {
|
|
637
|
+
throw new Error(`Agent "${this.name}" not found in this workspace. Create it with mails.agents.create("${this.name}") or verify the name.`);
|
|
638
|
+
}
|
|
639
|
+
return found.id;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
export function createClient(opts = {}) {
|
|
643
|
+
return new MailsClient(opts);
|
|
644
|
+
}
|
|
645
|
+
// Default singleton — uses env vars. Safe to import even without keys; throws on first use.
|
|
646
|
+
export const mails = {
|
|
647
|
+
agent(name, opts) {
|
|
648
|
+
return new MailsClient(opts ?? {}).agent(name);
|
|
649
|
+
},
|
|
650
|
+
send(agent, input, opts) {
|
|
651
|
+
return new MailsClient(opts ?? {}).send(agent, input);
|
|
652
|
+
},
|
|
653
|
+
webhooks: {
|
|
654
|
+
verify(body, signature, secret, toleranceSec = 300) {
|
|
655
|
+
try {
|
|
656
|
+
const parts = Object.fromEntries(signature.split(",").map((p) => p.split("=")));
|
|
657
|
+
const ts = parseInt(parts.t ?? "0", 10);
|
|
658
|
+
if (!Number.isFinite(ts))
|
|
659
|
+
return null;
|
|
660
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSec)
|
|
661
|
+
return null;
|
|
662
|
+
const expected = crypto.createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
|
|
663
|
+
const ok = crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1 ?? "", "hex"));
|
|
664
|
+
if (!ok)
|
|
665
|
+
return null;
|
|
666
|
+
return JSON.parse(body);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
export { MailsClient, Agent, MailsError };
|