coss 0.4.0 → 0.6.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.
@@ -0,0 +1,2893 @@
1
+ //#region src/api-version.ts
2
+ /**
3
+ * API versions this SDK knows, oldest first: a hand-written mirror of `API_VERSIONS` in the API
4
+ * (`apps/api/test/sdk-contract.test.ts` keeps them equal).
5
+ */
6
+ const API_VERSIONS = ["2026-10-10"];
7
+ /** The version this SDK's types describe, sent with every request unless `apiVersion` overrides it. */
8
+ const API_VERSION = "2026-10-10";
9
+ //#endregion
10
+ //#region src/version.ts
11
+ /** SDK version, sent in the User-Agent. A test keeps it equal to package.json's `version`. */
12
+ const VERSION = "0.6.0";
13
+ //#endregion
14
+ //#region src/core/errors.ts
15
+ const API_ERROR_TYPES = /* @__PURE__ */ new Set([
16
+ "invalid_request_error",
17
+ "authentication_error",
18
+ "permission_error",
19
+ "not_found_error",
20
+ "idempotency_error",
21
+ "rate_limit_error",
22
+ "billing_error",
23
+ "api_error"
24
+ ]);
25
+ function isRecord(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ function parseIssues(value) {
29
+ if (!Array.isArray(value)) return void 0;
30
+ const issues = [];
31
+ for (const item of value) if (isRecord(item) && typeof item.path === "string" && typeof item.message === "string") issues.push({
32
+ path: item.path,
33
+ message: item.message
34
+ });
35
+ return issues;
36
+ }
37
+ /**
38
+ * Maps a non-2xx response to a `CossError`. Bodies that are not the API's error envelope (a proxy's
39
+ * HTML 502, an empty 503) become `api_error` / `unexpected_response`.
40
+ */
41
+ function errorFromResponse(status, statusText, body, headerRequestId) {
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(body);
45
+ } catch {
46
+ parsed = void 0;
47
+ }
48
+ const envelope = isRecord(parsed) ? parsed.error : void 0;
49
+ if (!isRecord(envelope) || typeof envelope.message !== "string") return {
50
+ type: "api_error",
51
+ code: "unexpected_response",
52
+ message: `The coss.com API returned an unexpected HTTP ${status}${statusText ? ` (${statusText})` : ""} response.`,
53
+ status,
54
+ request_id: headerRequestId
55
+ };
56
+ const error = {
57
+ type: typeof envelope.type === "string" && API_ERROR_TYPES.has(envelope.type) ? envelope.type : "api_error",
58
+ code: typeof envelope.code === "string" ? envelope.code : "unknown_error",
59
+ message: envelope.message,
60
+ status,
61
+ request_id: typeof envelope.request_id === "string" ? envelope.request_id : headerRequestId
62
+ };
63
+ if (typeof envelope.param === "string") error.param = envelope.param;
64
+ const issues = parseIssues(envelope.issues);
65
+ if (issues) error.issues = issues;
66
+ return error;
67
+ }
68
+ /** Builds an error for a failure that happened on the client (no HTTP response to read). */
69
+ function clientError(type, code, message) {
70
+ return {
71
+ type,
72
+ code,
73
+ message,
74
+ status: null,
75
+ request_id: null
76
+ };
77
+ }
78
+ //#endregion
79
+ //#region src/core/http.ts
80
+ const INITIAL_RETRY_DELAY_MS = 500;
81
+ const MAX_RETRY_DELAY_MS = 8e3;
82
+ const MAX_RETRY_AFTER_MS = 6e4;
83
+ /** Exponential backoff (500ms, 1s, 2s … capped at 8s) minus up to 25% random jitter. */
84
+ function backoffDelay(retry, random = Math.random) {
85
+ return Math.min(INITIAL_RETRY_DELAY_MS * 2 ** retry, MAX_RETRY_DELAY_MS) * (1 - random() * .25);
86
+ }
87
+ /** `Retry-After` as milliseconds. Accepts delay-seconds or an HTTP date; `null` if absent/invalid. */
88
+ function parseRetryAfter(value, now = Date.now()) {
89
+ const trimmed = value?.trim();
90
+ if (!trimmed) return null;
91
+ const seconds = Number(trimmed);
92
+ if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1e3 : null;
93
+ const date = Date.parse(trimmed);
94
+ return Number.isNaN(date) ? null : Math.max(0, date - now);
95
+ }
96
+ /** 409 (idempotent request still in progress), 429 and 5xx are transient. */
97
+ function isRetryableStatus(status) {
98
+ return status === 409 || status === 429 || status >= 500;
99
+ }
100
+ function sleep(ms, signal) {
101
+ return new Promise((resolve) => {
102
+ if (signal?.aborted) {
103
+ resolve();
104
+ return;
105
+ }
106
+ const done = () => {
107
+ clearTimeout(timer);
108
+ signal?.removeEventListener("abort", done);
109
+ resolve();
110
+ };
111
+ const timer = setTimeout(done, ms);
112
+ signal?.addEventListener("abort", done, { once: true });
113
+ });
114
+ }
115
+ /** Error message plus the underlying cause (undici's "fetch failed" hides ECONNREFUSED there). */
116
+ function describe(error) {
117
+ if (!(error instanceof Error)) return String(error);
118
+ const cause = error.cause instanceof Error ? error.cause.message : void 0;
119
+ return cause && cause !== error.message ? `${error.message} (${cause})` : error.message;
120
+ }
121
+ function abortedError() {
122
+ return clientError("connection_error", "aborted", "The request was aborted.");
123
+ }
124
+ /**
125
+ * The request pipeline shared by all resources: auth + JSON, a per-attempt timeout, retries with
126
+ * backoff, and mapping every failure to a `CossError`. It never throws for API or network errors.
127
+ */
128
+ var HttpClient = class {
129
+ #apiKey;
130
+ #baseUrl;
131
+ #fetch;
132
+ #timeout;
133
+ #maxRetries;
134
+ #apiVersion;
135
+ #userAgent;
136
+ #sleep;
137
+ constructor(options) {
138
+ this.#apiKey = options.apiKey;
139
+ this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
140
+ this.#fetch = options.fetch;
141
+ this.#timeout = options.timeout;
142
+ this.#maxRetries = options.maxRetries;
143
+ this.#apiVersion = options.apiVersion;
144
+ this.#userAgent = options.integration ? `coss-typescript/${VERSION} (${options.integration})` : `coss-typescript/${VERSION}`;
145
+ this.#sleep = options.sleep ?? sleep;
146
+ }
147
+ async request(request) {
148
+ let url = `${this.#baseUrl}${request.path}`;
149
+ const query = new URLSearchParams();
150
+ for (const [name, value] of Object.entries(request.query ?? {})) if (value !== void 0) query.set(name, String(value));
151
+ const search = query.toString();
152
+ if (search) url += `?${search}`;
153
+ const headers = {
154
+ Accept: request.responseType === "text" || request.responseType === "bytes" ? "*/*" : "application/json",
155
+ Authorization: `Bearer ${this.#apiKey}`,
156
+ "User-Agent": this.#userAgent,
157
+ "Coss-Version": this.#apiVersion
158
+ };
159
+ let body;
160
+ if (request.body !== void 0) {
161
+ headers["Content-Type"] = "application/json";
162
+ body = JSON.stringify(request.body);
163
+ }
164
+ if (request.method === "POST") headers["Idempotency-Key"] = request.idempotencyKey ?? crypto.randomUUID();
165
+ try {
166
+ new Headers(headers);
167
+ } catch (cause) {
168
+ return {
169
+ data: null,
170
+ error: clientError("invalid_request_error", "invalid_header", `Invalid request header: ${describe(cause)}`)
171
+ };
172
+ }
173
+ const init = {
174
+ method: request.method,
175
+ headers,
176
+ body
177
+ };
178
+ for (let retry = 0;; retry++) {
179
+ if (request.signal?.aborted) return {
180
+ data: null,
181
+ error: abortedError()
182
+ };
183
+ const attempt = await this.#attempt(url, init, request.signal, request.responseType);
184
+ if (request.retry === false || !attempt.retry || retry >= this.#maxRetries) return attempt.result;
185
+ const delay = attempt.retryAfterMs ?? backoffDelay(retry);
186
+ if (delay > MAX_RETRY_AFTER_MS) return attempt.result;
187
+ await this.#sleep(delay, request.signal);
188
+ }
189
+ }
190
+ async #attempt(url, init, callerSignal, responseType) {
191
+ const timeoutSignal = AbortSignal.timeout(this.#timeout);
192
+ const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
193
+ let response;
194
+ let text;
195
+ let bytes = null;
196
+ try {
197
+ response = await this.#fetch(url, {
198
+ ...init,
199
+ signal
200
+ });
201
+ if (responseType === "bytes") {
202
+ bytes = new Uint8Array(await response.arrayBuffer());
203
+ text = response.ok ? "" : new TextDecoder().decode(bytes);
204
+ } else text = await response.text();
205
+ } catch (cause) {
206
+ if (callerSignal?.aborted) return {
207
+ result: failed(abortedError()),
208
+ retry: false,
209
+ retryAfterMs: null
210
+ };
211
+ const timeoutMessage = `Request timed out after ${this.#timeout}ms.`;
212
+ return {
213
+ result: failed(timeoutSignal.aborted ? clientError("timeout_error", "request_timeout", timeoutMessage) : clientError("connection_error", "network_error", `Could not reach the coss.com API at ${this.#baseUrl}: ${describe(cause)}`)),
214
+ retry: true,
215
+ retryAfterMs: null
216
+ };
217
+ }
218
+ const requestId = response.headers.get("x-request-id");
219
+ if (!response.ok) return {
220
+ result: failed(errorFromResponse(response.status, response.statusText, text, requestId)),
221
+ retry: isRetryableStatus(response.status),
222
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
223
+ };
224
+ if (responseType === "text") return {
225
+ result: {
226
+ data: text,
227
+ error: null
228
+ },
229
+ retry: false,
230
+ retryAfterMs: null
231
+ };
232
+ if (responseType === "bytes") return {
233
+ result: {
234
+ data: bytes,
235
+ error: null
236
+ },
237
+ retry: false,
238
+ retryAfterMs: null
239
+ };
240
+ try {
241
+ return {
242
+ result: {
243
+ data: JSON.parse(text),
244
+ error: null
245
+ },
246
+ retry: false,
247
+ retryAfterMs: null
248
+ };
249
+ } catch {
250
+ return {
251
+ result: failed({
252
+ type: "api_error",
253
+ code: "invalid_response",
254
+ message: `The coss.com API returned an HTTP ${response.status} response that is not valid JSON.`,
255
+ status: response.status,
256
+ request_id: requestId
257
+ }),
258
+ retry: false,
259
+ retryAfterMs: null
260
+ };
261
+ }
262
+ }
263
+ };
264
+ function failed(error) {
265
+ return {
266
+ data: null,
267
+ error
268
+ };
269
+ }
270
+ //#endregion
271
+ //#region src/resources/api-key.ts
272
+ /** `coss.apiKey`: the API key the client was created with. */
273
+ var ApiKeyResource = class {
274
+ #http;
275
+ /** @internal Use `new Coss().apiKey`. */
276
+ constructor(http) {
277
+ this.#http = http;
278
+ }
279
+ /** Revokes the client's own API key, at once: every later request with it fails. */
280
+ revoke(options) {
281
+ return this.#http.request({
282
+ method: "DELETE",
283
+ path: "/v1/api_key",
284
+ signal: options?.signal
285
+ });
286
+ }
287
+ };
288
+ //#endregion
289
+ //#region src/resources/list-query.ts
290
+ /** One query parameter: lists are comma-joined (an empty one is left out), dates ISO 8601. */
291
+ function joined(value) {
292
+ if (value === void 0) return void 0;
293
+ if (value instanceof Date) return value.toISOString();
294
+ if (Array.isArray(value)) return value.length === 0 ? void 0 : value.join(",");
295
+ return String(value);
296
+ }
297
+ /** The query string of a list method whose filters accept arrays and dates. */
298
+ function listQuery(params) {
299
+ const query = {};
300
+ for (const [name, value] of Object.entries(params)) query[name] = joined(value);
301
+ return query;
302
+ }
303
+ //#endregion
304
+ //#region src/resources/audit-events.ts
305
+ /**
306
+ * `coss.auditEvents`: the organization's audit log, newest first: changes made by members, API
307
+ * keys and coss.com support. Organization-wide, whichever environment the key belongs to.
308
+ */
309
+ var AuditEventsResource = class {
310
+ #http;
311
+ /** @internal Use `new Coss().auditEvents`. */
312
+ constructor(http) {
313
+ this.#http = http;
314
+ }
315
+ /** Lists audit events, newest first. Pass the last `id` as `after` while `has_more` is true. */
316
+ list(params = {}, options) {
317
+ return this.#http.request({
318
+ method: "GET",
319
+ path: "/v1/audit_events",
320
+ query: listQuery(params),
321
+ signal: options?.signal
322
+ });
323
+ }
324
+ /** Retrieves one audit event. */
325
+ get(id, options) {
326
+ return this.#http.request({
327
+ method: "GET",
328
+ path: `/v1/audit_events/${encodeURIComponent(id)}`,
329
+ signal: options?.signal
330
+ });
331
+ }
332
+ };
333
+ //#endregion
334
+ //#region src/resources/billing.ts
335
+ /** `coss.billing.invoices`: the organization's invoices, newest first. */
336
+ var BillingInvoicesResource = class {
337
+ #http;
338
+ /** @internal Use `new Coss().billing.invoices`. */
339
+ constructor(http) {
340
+ this.#http = http;
341
+ }
342
+ /** Lists invoices (without lines). Pass the last `id` as `after` while `has_more` is true. */
343
+ list(params = {}, options) {
344
+ return this.#http.request({
345
+ method: "GET",
346
+ path: "/v1/billing/invoices",
347
+ query: listQuery(params),
348
+ signal: options?.signal
349
+ });
350
+ }
351
+ /** Retrieves one invoice with its lines. */
352
+ get(id, options) {
353
+ return this.#http.request({
354
+ method: "GET",
355
+ path: `/v1/billing/invoices/${encodeURIComponent(id)}`,
356
+ signal: options?.signal
357
+ });
358
+ }
359
+ };
360
+ /** `coss.billing.credits`: the organization's credits and what's left of them. */
361
+ var BillingCreditsResource = class {
362
+ #http;
363
+ /** @internal Use `new Coss().billing.credits`. */
364
+ constructor(http) {
365
+ this.#http = http;
366
+ }
367
+ /** Lists credits, newest first (usable ones by default). */
368
+ list(params = {}, options) {
369
+ return this.#http.request({
370
+ method: "GET",
371
+ path: "/v1/billing/credits",
372
+ query: listQuery(params),
373
+ signal: options?.signal
374
+ });
375
+ }
376
+ };
377
+ /**
378
+ * `coss.billing`: the organization's plan, usage, invoices and credits (read only; plan changes
379
+ * are made in the dashboard). Organization-wide, whichever environment the key belongs to.
380
+ */
381
+ var BillingResource = class {
382
+ #http;
383
+ invoices;
384
+ credits;
385
+ /** @internal Use `new Coss().billing`. */
386
+ constructor(http) {
387
+ this.#http = http;
388
+ this.invoices = new BillingInvoicesResource(http);
389
+ this.credits = new BillingCreditsResource(http);
390
+ }
391
+ /** The plan, the current month's usage and estimate, standing and what the plan includes. */
392
+ get(options) {
393
+ return this.#http.request({
394
+ method: "GET",
395
+ path: "/v1/billing",
396
+ signal: options?.signal
397
+ });
398
+ }
399
+ /** Billable usage per meter and bucket (the current month, by day, by default), oldest first. */
400
+ usage(params = {}, options) {
401
+ return this.#http.request({
402
+ method: "GET",
403
+ path: "/v1/billing/usage",
404
+ query: listQuery(params),
405
+ signal: options?.signal
406
+ });
407
+ }
408
+ };
409
+ //#endregion
410
+ //#region src/resources/dmarc.ts
411
+ const dmarcPath = (domainId) => `/v1/domains/${encodeURIComponent(domainId)}/dmarc`;
412
+ /** `coss.domains.dmarc.reports`: the aggregate reports received about a domain. */
413
+ var DmarcReportsResource = class {
414
+ #http;
415
+ /** @internal Use `new Coss().domains.dmarc.reports`. */
416
+ constructor(http) {
417
+ this.#http = http;
418
+ }
419
+ /** Lists the domain's reports, newest first, without their rows. */
420
+ list(domainId, params = {}, options) {
421
+ return this.#http.request({
422
+ method: "GET",
423
+ path: `${dmarcPath(domainId)}/reports`,
424
+ query: listQuery(params),
425
+ signal: options?.signal
426
+ });
427
+ }
428
+ /** Retrieves one report with its rows. */
429
+ get(domainId, reportId, options) {
430
+ return this.#http.request({
431
+ method: "GET",
432
+ path: `${dmarcPath(domainId)}/reports/${encodeURIComponent(reportId)}`,
433
+ signal: options?.signal
434
+ });
435
+ }
436
+ };
437
+ /**
438
+ * `coss.domains.dmarc`: a domain's DMARC record, the guided path from `p=none` to `p=reject`, and
439
+ * the aggregate reports mailbox providers send to its `report_address`.
440
+ */
441
+ var DmarcResource = class {
442
+ #http;
443
+ /** The domain's aggregate reports. */
444
+ reports;
445
+ /** @internal Use `new Coss().domains.dmarc`. */
446
+ constructor(http) {
447
+ this.#http = http;
448
+ this.reports = new DmarcReportsResource(http);
449
+ }
450
+ /**
451
+ * The domain's DMARC status: its published record, the record to publish next and whether
452
+ * the last 14 days of reports say that is safe (`ready`), and a summary of those reports.
453
+ */
454
+ get(domainId, options) {
455
+ return this.#http.request({
456
+ method: "GET",
457
+ path: dmarcPath(domainId),
458
+ signal: options?.signal
459
+ });
460
+ }
461
+ };
462
+ //#endregion
463
+ //#region src/resources/receiving-domains.ts
464
+ const receivingPath = (domainId) => `/v1/domains/${encodeURIComponent(domainId)}/receiving`;
465
+ /**
466
+ * `coss.domains.receiving`: a domain's receiving hostname (the domain itself or a subdomain, e.g.
467
+ * `inbound.acme.com`). Email sent to any address at it is received by the API key's environment.
468
+ */
469
+ var ReceivingDomainsResource = class {
470
+ #http;
471
+ /** @internal Use `new Coss().domains.receiving`. */
472
+ constructor(http) {
473
+ this.#http = http;
474
+ }
475
+ /**
476
+ * Starts receiving at a hostname of the domain, for the API key's environment, and returns the
477
+ * domain with the MX record to publish in `data.receiving.record`.
478
+ */
479
+ create(domainId, params, options) {
480
+ return this.#http.request({
481
+ method: "POST",
482
+ path: receivingPath(domainId),
483
+ body: params,
484
+ idempotencyKey: options?.idempotencyKey,
485
+ signal: options?.signal
486
+ });
487
+ }
488
+ /** Stops receiving at the domain's hostname. Emails already received are kept. */
489
+ delete(domainId, options) {
490
+ return this.#http.request({
491
+ method: "DELETE",
492
+ path: receivingPath(domainId),
493
+ signal: options?.signal
494
+ });
495
+ }
496
+ };
497
+ //#endregion
498
+ //#region src/resources/tracking-domains.ts
499
+ const trackingPath = (domainId) => `/v1/domains/${encodeURIComponent(domainId)}/tracking`;
500
+ /**
501
+ * `coss.domains.tracking`: a domain's custom tracking domain (e.g. `links.acme.com`). Open pixels
502
+ * and click redirects of emails sent from the domain use it while it is `active`.
503
+ */
504
+ var TrackingDomainsResource = class {
505
+ #http;
506
+ /** @internal Use `new Coss().domains.tracking`. */
507
+ constructor(http) {
508
+ this.#http = http;
509
+ }
510
+ /**
511
+ * Sets up a subdomain of the domain as its tracking domain and returns the domain, with the
512
+ * CNAME to publish in `data.tracking.record`. Setup gives up after 72 hours.
513
+ */
514
+ create(domainId, params, options) {
515
+ return this.#http.request({
516
+ method: "POST",
517
+ path: trackingPath(domainId),
518
+ body: params,
519
+ idempotencyKey: options?.idempotencyKey,
520
+ signal: options?.signal
521
+ });
522
+ }
523
+ /**
524
+ * Removes the domain's tracking domain. Later emails use the default tracking host; links in
525
+ * emails already sent through it stop working.
526
+ */
527
+ delete(domainId, options) {
528
+ return this.#http.request({
529
+ method: "DELETE",
530
+ path: trackingPath(domainId),
531
+ signal: options?.signal
532
+ });
533
+ }
534
+ };
535
+ //#endregion
536
+ //#region src/resources/domains.ts
537
+ /**
538
+ * `coss.domains`: the domains live (`coss_live_…`) keys may send from. Domains belong to the
539
+ * organization, so every API key of the organization sees the same ones.
540
+ */
541
+ var DomainsResource = class {
542
+ #http;
543
+ /** Each domain's custom tracking domain: add and remove it. */
544
+ tracking;
545
+ /** Each domain's receiving hostname: start and stop receiving email at it. */
546
+ receiving;
547
+ /** Each domain's DMARC status, guidance and aggregate reports. */
548
+ dmarc;
549
+ /** @internal Use `new Coss().domains`. */
550
+ constructor(http) {
551
+ this.#http = http;
552
+ this.tracking = new TrackingDomainsResource(http);
553
+ this.receiving = new ReceivingDomainsResource(http);
554
+ this.dmarc = new DmarcResource(http);
555
+ }
556
+ /**
557
+ * Adds a domain and returns the DNS records to publish (`data.records`). The records are
558
+ * checked in the background; verification gives up after 72 hours.
559
+ */
560
+ create(params, options) {
561
+ return this.#http.request({
562
+ method: "POST",
563
+ path: "/v1/domains",
564
+ body: params,
565
+ idempotencyKey: options?.idempotencyKey,
566
+ signal: options?.signal
567
+ });
568
+ }
569
+ /** Lists the organization's domains, ordered by name. */
570
+ list(options) {
571
+ return this.#http.request({
572
+ method: "GET",
573
+ path: "/v1/domains",
574
+ signal: options?.signal
575
+ });
576
+ }
577
+ /** Retrieves a domain with the result of the last DNS check. */
578
+ get(id, options) {
579
+ return this.#http.request({
580
+ method: "GET",
581
+ path: `/v1/domains/${encodeURIComponent(id)}`,
582
+ signal: options?.signal
583
+ });
584
+ }
585
+ /**
586
+ * Checks the domain's records now instead of waiting for the next background check. A `failed`
587
+ * domain starts verification over with fresh DKIM records, which have to be published again.
588
+ * Its tracking domain, if any, is checked too.
589
+ */
590
+ verify(id, options) {
591
+ return this.#http.request({
592
+ method: "POST",
593
+ path: `/v1/domains/${encodeURIComponent(id)}/verify`,
594
+ idempotencyKey: options?.idempotencyKey,
595
+ signal: options?.signal
596
+ });
597
+ }
598
+ /** Removes the domain. Emails already sent are unaffected; live keys can no longer use it. */
599
+ delete(id, options) {
600
+ return this.#http.request({
601
+ method: "DELETE",
602
+ path: `/v1/domains/${encodeURIComponent(id)}`,
603
+ signal: options?.signal
604
+ });
605
+ }
606
+ };
607
+ //#endregion
608
+ //#region src/react.ts
609
+ /**
610
+ * React Email: a send's `react` is rendered into `html` (and `text`, unless given) by
611
+ * `@react-email/render`, an optional peer dependency imported only when a send has `react`, so the
612
+ * SDK itself keeps zero dependencies.
613
+ */
614
+ let renderer;
615
+ /** Imports `@react-email/render` once; `null` when it isn't installed. */
616
+ function loadReactEmailRenderer() {
617
+ renderer ??= (async () => {
618
+ try {
619
+ return await import("@react-email/render");
620
+ } catch {
621
+ return null;
622
+ }
623
+ })();
624
+ return renderer;
625
+ }
626
+ /**
627
+ * Turns `react` into `html` and `text`; params without it pass through. Rendering mistakes come back
628
+ * as `invalid_request_error`s naming `param`, like the API's validation errors, never thrown.
629
+ */
630
+ async function renderReactEmail(params, param, load = loadReactEmailRenderer) {
631
+ const { react, ...body } = params;
632
+ if (react === void 0) return {
633
+ body,
634
+ error: null
635
+ };
636
+ if (body.template_id !== void 0) return failure("validation_error", "Pass either `react` or `template_id`, not both.", param);
637
+ if (body.html !== void 0) return failure("validation_error", "Pass either `react` or `html`, not both.", param);
638
+ const renderer = await load();
639
+ if (!renderer) return failure("react_renderer_missing", "Sending `react` needs the `@react-email/render` package: install it next to `coss`.", param);
640
+ try {
641
+ const html = await renderer.render(react);
642
+ if (body.text !== void 0) return {
643
+ body: {
644
+ ...body,
645
+ html
646
+ },
647
+ error: null
648
+ };
649
+ const text = renderer.toPlainText ? renderer.toPlainText(html) : await renderer.render(react, { plainText: true });
650
+ return {
651
+ body: {
652
+ ...body,
653
+ html,
654
+ text
655
+ },
656
+ error: null
657
+ };
658
+ } catch (cause) {
659
+ return failure("react_render_failed", `Rendering \`react\` failed: ${cause instanceof Error ? cause.message : String(cause)}`, param);
660
+ }
661
+ }
662
+ function failure(code, message, param) {
663
+ return {
664
+ body: null,
665
+ error: {
666
+ ...clientError("invalid_request_error", code, message),
667
+ param
668
+ }
669
+ };
670
+ }
671
+ //#endregion
672
+ //#region src/resources/broadcasts.ts
673
+ const path$1 = (id) => encodeURIComponent(id);
674
+ /**
675
+ * `coss.email.broadcasts`: one marketing email to an audience or a segment, sent to each
676
+ * subscribed contact at the plan's marketing rate, with per-broadcast stats.
677
+ */
678
+ var BroadcastsResource = class {
679
+ #http;
680
+ /** @internal Use `new Coss().email.broadcasts`. */
681
+ constructor(http) {
682
+ this.#http = http;
683
+ }
684
+ /** Newest first, with their stats. */
685
+ list(params = {}, options) {
686
+ return this.#http.request({
687
+ method: "GET",
688
+ path: "/v1/broadcasts",
689
+ query: { ...params },
690
+ signal: options?.signal
691
+ });
692
+ }
693
+ /**
694
+ * Creates a draft, or with `send` / `scheduled_at` a broadcast that starts sending. Retries
695
+ * reuse one `Idempotency-Key`, so a retried create never makes two.
696
+ */
697
+ create(params, options) {
698
+ return this.#http.request({
699
+ method: "POST",
700
+ path: "/v1/broadcasts",
701
+ body: params,
702
+ idempotencyKey: options?.idempotencyKey,
703
+ signal: options?.signal
704
+ });
705
+ }
706
+ get(id, options) {
707
+ return this.#http.request({
708
+ method: "GET",
709
+ path: `/v1/broadcasts/${path$1(id)}`,
710
+ signal: options?.signal
711
+ });
712
+ }
713
+ /** Changes a draft. */
714
+ update(id, params, options) {
715
+ return this.#http.request({
716
+ method: "PATCH",
717
+ path: `/v1/broadcasts/${path$1(id)}`,
718
+ body: params,
719
+ signal: options?.signal
720
+ });
721
+ }
722
+ /** Deletes a draft (or a broadcast canceled before it sent anything). */
723
+ remove(id, options) {
724
+ return this.#http.request({
725
+ method: "DELETE",
726
+ path: `/v1/broadcasts/${path$1(id)}`,
727
+ signal: options?.signal
728
+ });
729
+ }
730
+ /** Starts sending a draft, now or at `scheduled_at`. */
731
+ send(id, params = {}, options) {
732
+ return this.#http.request({
733
+ method: "POST",
734
+ path: `/v1/broadcasts/${path$1(id)}/send`,
735
+ body: params,
736
+ signal: options?.signal
737
+ });
738
+ }
739
+ /** Stops it for good; emails not yet handed to the provider are dropped. */
740
+ cancel(id, options) {
741
+ return this.#action(id, "cancel", options);
742
+ }
743
+ pause(id, options) {
744
+ return this.#action(id, "pause", options);
745
+ }
746
+ /** Resumes a paused broadcast where it stopped. */
747
+ resume(id, options) {
748
+ return this.#action(id, "resume", options);
749
+ }
750
+ /**
751
+ * Sends its content to up to 5 addresses (subject prefixed `[Test]`). Not retried
752
+ * automatically: test emails are not deduplicated.
753
+ */
754
+ test(id, params, options) {
755
+ return this.#http.request({
756
+ method: "POST",
757
+ path: `/v1/broadcasts/${path$1(id)}/test`,
758
+ body: params,
759
+ retry: false,
760
+ signal: options?.signal
761
+ });
762
+ }
763
+ /** The emails made so far, one per contact, newest first. */
764
+ listEmails(id, params = {}, options) {
765
+ return this.#http.request({
766
+ method: "GET",
767
+ path: `/v1/broadcasts/${path$1(id)}/emails`,
768
+ query: { ...params },
769
+ signal: options?.signal
770
+ });
771
+ }
772
+ #action(id, action, options) {
773
+ return this.#http.request({
774
+ method: "POST",
775
+ path: `/v1/broadcasts/${path$1(id)}/${action}`,
776
+ signal: options?.signal
777
+ });
778
+ }
779
+ };
780
+ //#endregion
781
+ //#region src/resources/contacts.ts
782
+ /** The API's per-request maximum for imports. */
783
+ const IMPORT_BATCH_SIZE$1 = 1e3;
784
+ const path = (...parts) => parts.map(encodeURIComponent).join("/");
785
+ /** `coss.email.contacts.properties`: the environment's custom contact properties. */
786
+ var ContactPropertiesResource = class {
787
+ #http;
788
+ /** @internal Use `new Coss().email.contacts.properties`. */
789
+ constructor(http) {
790
+ this.#http = http;
791
+ }
792
+ /** Every property (at most 50), by key. */
793
+ list(options) {
794
+ return this.#http.request({
795
+ method: "GET",
796
+ path: "/v1/contact_properties",
797
+ signal: options?.signal
798
+ });
799
+ }
800
+ /** Defines a property contacts can have; its key and type can't change. */
801
+ create(params, options) {
802
+ return this.#http.request({
803
+ method: "POST",
804
+ path: "/v1/contact_properties",
805
+ retry: false,
806
+ body: params,
807
+ signal: options?.signal
808
+ });
809
+ }
810
+ get(id, options) {
811
+ return this.#http.request({
812
+ method: "GET",
813
+ path: `/v1/contact_properties/${path(id)}`,
814
+ signal: options?.signal
815
+ });
816
+ }
817
+ /** Changes the fallback templates use for contacts without a value. */
818
+ update(id, params, options) {
819
+ return this.#http.request({
820
+ method: "PATCH",
821
+ path: `/v1/contact_properties/${path(id)}`,
822
+ body: params,
823
+ signal: options?.signal
824
+ });
825
+ }
826
+ /** Deletes the property and its values; refused while a segment filters on it. */
827
+ remove(id, options) {
828
+ return this.#http.request({
829
+ method: "DELETE",
830
+ path: `/v1/contact_properties/${path(id)}`,
831
+ signal: options?.signal
832
+ });
833
+ }
834
+ };
835
+ /**
836
+ * `coss.email.contacts`: the people marketing email goes to, in the API key's environment, with
837
+ * custom properties and consent (overall, and per topic). Methods taking an id also take the
838
+ * contact's address.
839
+ */
840
+ var ContactsResource = class {
841
+ #http;
842
+ /** Custom contact properties. */
843
+ properties;
844
+ /** @internal Use `new Coss().email.contacts`. */
845
+ constructor(http) {
846
+ this.#http = http;
847
+ this.properties = new ContactPropertiesResource(http);
848
+ }
849
+ /**
850
+ * Creates the contact with this address, or updates the existing one (names replaced,
851
+ * properties merged, audiences added, topic choices recorded; `status` only when passed).
852
+ * Safe to retry.
853
+ */
854
+ upsert(params, options) {
855
+ return this.#http.request({
856
+ method: "POST",
857
+ path: "/v1/contacts",
858
+ body: params,
859
+ signal: options?.signal
860
+ });
861
+ }
862
+ /** Lists contacts, newest first. Pass the last `id` as `after` while `has_more` is true. */
863
+ list(params = {}, options) {
864
+ return this.#http.request({
865
+ method: "GET",
866
+ path: "/v1/contacts",
867
+ query: { ...params },
868
+ signal: options?.signal
869
+ });
870
+ }
871
+ get(idOrEmail, options) {
872
+ return this.#http.request({
873
+ method: "GET",
874
+ path: `/v1/contacts/${path(idOrEmail)}`,
875
+ signal: options?.signal
876
+ });
877
+ }
878
+ update(idOrEmail, params, options) {
879
+ return this.#http.request({
880
+ method: "PATCH",
881
+ path: `/v1/contacts/${path(idOrEmail)}`,
882
+ body: params,
883
+ signal: options?.signal
884
+ });
885
+ }
886
+ /** Deletes the contact (its suppressions stay: they are about the address). */
887
+ remove(idOrEmail, options) {
888
+ return this.#http.request({
889
+ method: "DELETE",
890
+ path: `/v1/contacts/${path(idOrEmail)}`,
891
+ signal: options?.signal
892
+ });
893
+ }
894
+ /**
895
+ * Creates or updates many contacts, 1,000 per request. Invalid entries are skipped and reported
896
+ * in `invalid`; an import never re-subscribes an unsubscribed contact. If a request fails, the
897
+ * earlier batches stay imported: importing everything again is safe.
898
+ */
899
+ async import(params, options) {
900
+ const total = {
901
+ object: "contact_import",
902
+ created: 0,
903
+ updated: 0,
904
+ kept_unsubscribed: 0,
905
+ invalid: []
906
+ };
907
+ for (let start = 0; start === 0 || start < params.contacts.length;) {
908
+ const result = await this.#http.request({
909
+ method: "POST",
910
+ path: "/v1/contacts/import",
911
+ body: {
912
+ contacts: params.contacts.slice(start, start + IMPORT_BATCH_SIZE$1),
913
+ ...params.audience_id === void 0 ? {} : { audience_id: params.audience_id }
914
+ },
915
+ signal: options?.signal
916
+ });
917
+ if (result.error) return result;
918
+ total.created += result.data.created;
919
+ total.updated += result.data.updated;
920
+ total.kept_unsubscribed += result.data.kept_unsubscribed;
921
+ for (const entry of result.data.invalid) total.invalid.push({
922
+ ...entry,
923
+ index: entry.index + start
924
+ });
925
+ start += IMPORT_BATCH_SIZE$1;
926
+ }
927
+ return {
928
+ data: total,
929
+ error: null
930
+ };
931
+ }
932
+ /** The contacts as CSV (one column per custom property), which `import` accepts again. */
933
+ export(params = {}, options) {
934
+ return this.#http.request({
935
+ method: "GET",
936
+ path: "/v1/contacts/export",
937
+ query: { ...params },
938
+ responseType: "text",
939
+ signal: options?.signal
940
+ });
941
+ }
942
+ /** Every topic with the contact's choice and whether its emails reach them. */
943
+ getTopics(idOrEmail, options) {
944
+ return this.#http.request({
945
+ method: "GET",
946
+ path: `/v1/contacts/${path(idOrEmail)}/topics`,
947
+ signal: options?.signal
948
+ });
949
+ }
950
+ /**
951
+ * Sends a pending contact (double opt-in) its confirmation email again: at most once a day and
952
+ * three times before it confirms (`status` says what happened).
953
+ */
954
+ resendConfirmation(idOrEmail, options) {
955
+ return this.#http.request({
956
+ method: "POST",
957
+ path: `/v1/contacts/${path(idOrEmail)}/confirmation`,
958
+ signal: options?.signal
959
+ });
960
+ }
961
+ /** The contact's preference center and unsubscribe links, for emails you build yourself. */
962
+ createPreferenceLinks(idOrEmail, params = {}, options) {
963
+ return this.#http.request({
964
+ method: "POST",
965
+ path: `/v1/contacts/${path(idOrEmail)}/preference_links`,
966
+ body: params,
967
+ signal: options?.signal
968
+ });
969
+ }
970
+ /** Records the contact's choices for some topics. */
971
+ updateTopics(idOrEmail, params, options) {
972
+ return this.#http.request({
973
+ method: "PATCH",
974
+ path: `/v1/contacts/${path(idOrEmail)}/topics`,
975
+ body: params,
976
+ signal: options?.signal
977
+ });
978
+ }
979
+ };
980
+ /** `coss.email.audiences`: static lists of contacts. */
981
+ var AudiencesResource = class {
982
+ #http;
983
+ /** @internal Use `new Coss().email.audiences`. */
984
+ constructor(http) {
985
+ this.#http = http;
986
+ }
987
+ list(params = {}, options) {
988
+ return this.#http.request({
989
+ method: "GET",
990
+ path: "/v1/audiences",
991
+ query: { ...params },
992
+ signal: options?.signal
993
+ });
994
+ }
995
+ create(params, options) {
996
+ return this.#http.request({
997
+ method: "POST",
998
+ path: "/v1/audiences",
999
+ retry: false,
1000
+ body: params,
1001
+ signal: options?.signal
1002
+ });
1003
+ }
1004
+ get(id, options) {
1005
+ return this.#http.request({
1006
+ method: "GET",
1007
+ path: `/v1/audiences/${path(id)}`,
1008
+ signal: options?.signal
1009
+ });
1010
+ }
1011
+ /** Renames the audience. */
1012
+ update(id, params, options) {
1013
+ return this.#http.request({
1014
+ method: "PATCH",
1015
+ path: `/v1/audiences/${path(id)}`,
1016
+ body: params,
1017
+ signal: options?.signal
1018
+ });
1019
+ }
1020
+ /** Deletes the audience (its contacts stay); refused while a segment filters on it. */
1021
+ remove(id, options) {
1022
+ return this.#http.request({
1023
+ method: "DELETE",
1024
+ path: `/v1/audiences/${path(id)}`,
1025
+ signal: options?.signal
1026
+ });
1027
+ }
1028
+ /** The audience's members, newest first. */
1029
+ listContacts(id, params = {}, options) {
1030
+ return this.#http.request({
1031
+ method: "GET",
1032
+ path: `/v1/audiences/${path(id)}/contacts`,
1033
+ query: { ...params },
1034
+ signal: options?.signal
1035
+ });
1036
+ }
1037
+ /** Adds existing contacts, by id or address (up to 1,000 per call). */
1038
+ addContacts(id, params, options) {
1039
+ return this.#http.request({
1040
+ method: "POST",
1041
+ path: `/v1/audiences/${path(id)}/contacts`,
1042
+ body: params,
1043
+ signal: options?.signal
1044
+ });
1045
+ }
1046
+ removeContact(id, contactIdOrEmail, options) {
1047
+ return this.#http.request({
1048
+ method: "DELETE",
1049
+ path: `/v1/audiences/${path(id)}/contacts/${path(contactIdOrEmail)}`,
1050
+ signal: options?.signal
1051
+ });
1052
+ }
1053
+ };
1054
+ /** `coss.email.segments`: filters over the environment's contacts, always current. */
1055
+ var SegmentsResource = class {
1056
+ #http;
1057
+ /** @internal Use `new Coss().email.segments`. */
1058
+ constructor(http) {
1059
+ this.#http = http;
1060
+ }
1061
+ list(params = {}, options) {
1062
+ return this.#http.request({
1063
+ method: "GET",
1064
+ path: "/v1/segments",
1065
+ query: { ...params },
1066
+ signal: options?.signal
1067
+ });
1068
+ }
1069
+ create(params, options) {
1070
+ return this.#http.request({
1071
+ method: "POST",
1072
+ path: "/v1/segments",
1073
+ retry: false,
1074
+ body: params,
1075
+ signal: options?.signal
1076
+ });
1077
+ }
1078
+ get(id, options) {
1079
+ return this.#http.request({
1080
+ method: "GET",
1081
+ path: `/v1/segments/${path(id)}`,
1082
+ signal: options?.signal
1083
+ });
1084
+ }
1085
+ update(id, params, options) {
1086
+ return this.#http.request({
1087
+ method: "PATCH",
1088
+ path: `/v1/segments/${path(id)}`,
1089
+ body: params,
1090
+ signal: options?.signal
1091
+ });
1092
+ }
1093
+ remove(id, options) {
1094
+ return this.#http.request({
1095
+ method: "DELETE",
1096
+ path: `/v1/segments/${path(id)}`,
1097
+ signal: options?.signal
1098
+ });
1099
+ }
1100
+ /** The contacts the filter matches now, whatever their status, newest first. */
1101
+ listContacts(id, params = {}, options) {
1102
+ return this.#http.request({
1103
+ method: "GET",
1104
+ path: `/v1/segments/${path(id)}/contacts`,
1105
+ query: { ...params },
1106
+ signal: options?.signal
1107
+ });
1108
+ }
1109
+ };
1110
+ /** `coss.email.topics`: what contacts subscribe to (a topic's key is a send's `category`). */
1111
+ var TopicsResource = class {
1112
+ #http;
1113
+ /** @internal Use `new Coss().email.topics`. */
1114
+ constructor(http) {
1115
+ this.#http = http;
1116
+ }
1117
+ list(params = {}, options) {
1118
+ return this.#http.request({
1119
+ method: "GET",
1120
+ path: "/v1/topics",
1121
+ query: { ...params },
1122
+ signal: options?.signal
1123
+ });
1124
+ }
1125
+ create(params, options) {
1126
+ return this.#http.request({
1127
+ method: "POST",
1128
+ path: "/v1/topics",
1129
+ retry: false,
1130
+ body: params,
1131
+ signal: options?.signal
1132
+ });
1133
+ }
1134
+ get(id, options) {
1135
+ return this.#http.request({
1136
+ method: "GET",
1137
+ path: `/v1/topics/${path(id)}`,
1138
+ signal: options?.signal
1139
+ });
1140
+ }
1141
+ update(id, params, options) {
1142
+ return this.#http.request({
1143
+ method: "PATCH",
1144
+ path: `/v1/topics/${path(id)}`,
1145
+ body: params,
1146
+ signal: options?.signal
1147
+ });
1148
+ }
1149
+ /** Deletes the topic and every contact's choice for it. */
1150
+ remove(id, options) {
1151
+ return this.#http.request({
1152
+ method: "DELETE",
1153
+ path: `/v1/topics/${path(id)}`,
1154
+ signal: options?.signal
1155
+ });
1156
+ }
1157
+ };
1158
+ /** `coss.email.marketingSettings`: the sender name and postal address of marketing email. */
1159
+ var MarketingSettingsResource = class {
1160
+ #http;
1161
+ /** @internal Use `new Coss().email.marketingSettings`. */
1162
+ constructor(http) {
1163
+ this.#http = http;
1164
+ }
1165
+ get(options) {
1166
+ return this.#http.request({
1167
+ method: "GET",
1168
+ path: "/v1/marketing_settings",
1169
+ signal: options?.signal
1170
+ });
1171
+ }
1172
+ /** Changes the fields given (`null` clears one). */
1173
+ update(params, options) {
1174
+ return this.#http.request({
1175
+ method: "PATCH",
1176
+ path: "/v1/marketing_settings",
1177
+ body: params,
1178
+ signal: options?.signal
1179
+ });
1180
+ }
1181
+ };
1182
+ //#endregion
1183
+ //#region src/resources/dedicated-ips.ts
1184
+ /**
1185
+ * `coss.email.dedicatedIps`: the organization's own sending IPs (the Dedicated IPs add-on),
1186
+ * managed and warmed up automatically.
1187
+ */
1188
+ var DedicatedIpsResource = class {
1189
+ #http;
1190
+ /** @internal Use `new Coss().email.dedicatedIps`. */
1191
+ constructor(http) {
1192
+ this.#http = http;
1193
+ }
1194
+ /** The pool's status and IPs (`not_enabled` without the add-on). */
1195
+ get(options) {
1196
+ return this.#http.request({
1197
+ method: "GET",
1198
+ path: "/v1/dedicated_ips",
1199
+ signal: options?.signal
1200
+ });
1201
+ }
1202
+ };
1203
+ //#endregion
1204
+ //#region src/resources/email-templates.ts
1205
+ const templatePath = (template) => `/v1/email_templates/${encodeURIComponent(template)}`;
1206
+ /** `coss.email.templates.versions`: a template's published versions. */
1207
+ var EmailTemplateVersionsResource = class {
1208
+ #http;
1209
+ /** @internal Use `new Coss().email.templates.versions`. */
1210
+ constructor(http) {
1211
+ this.#http = http;
1212
+ }
1213
+ /** Published versions, newest first. */
1214
+ list(template, params = {}, options) {
1215
+ return this.#http.request({
1216
+ method: "GET",
1217
+ path: `${templatePath(template)}/versions`,
1218
+ query: listQuery(params),
1219
+ signal: options?.signal
1220
+ });
1221
+ }
1222
+ get(template, version, options) {
1223
+ return this.#http.request({
1224
+ method: "GET",
1225
+ path: `${templatePath(template)}/versions/${version}`,
1226
+ signal: options?.signal
1227
+ });
1228
+ }
1229
+ /**
1230
+ * Replaces the draft with this version's content; publish the draft to send it again. Never
1231
+ * automatically retried: every restore is a new draft revision, so a repeat could overwrite an
1232
+ * edit saved in between.
1233
+ */
1234
+ restore(template, version, options) {
1235
+ return this.#http.request({
1236
+ method: "POST",
1237
+ path: `${templatePath(template)}/versions/${version}/restore`,
1238
+ retry: false,
1239
+ signal: options?.signal
1240
+ });
1241
+ }
1242
+ };
1243
+ /**
1244
+ * `coss.email.templates`: the environment's email templates. Templates are addressed by id or
1245
+ * alias. Edit the draft, publish it, then send with `coss.email.send({ template_id, variables })`.
1246
+ */
1247
+ var EmailTemplatesResource = class {
1248
+ #http;
1249
+ /** Published versions. */
1250
+ versions;
1251
+ /** @internal Use `new Coss().email.templates`. */
1252
+ constructor(http) {
1253
+ this.#http = http;
1254
+ this.versions = new EmailTemplateVersionsResource(http);
1255
+ }
1256
+ /**
1257
+ * Creates a template and its draft. Never automatically retried: the API doesn't deduplicate it
1258
+ * (a template with an `alias` can't be created twice, though).
1259
+ */
1260
+ create(params, options) {
1261
+ return this.#http.request({
1262
+ method: "POST",
1263
+ path: "/v1/email_templates",
1264
+ retry: false,
1265
+ body: params,
1266
+ idempotencyKey: options?.idempotencyKey,
1267
+ signal: options?.signal
1268
+ });
1269
+ }
1270
+ /** The environment's templates, newest first. */
1271
+ list(params = {}, options) {
1272
+ return this.#http.request({
1273
+ method: "GET",
1274
+ path: "/v1/email_templates",
1275
+ query: listQuery(params),
1276
+ signal: options?.signal
1277
+ });
1278
+ }
1279
+ get(template, options) {
1280
+ return this.#http.request({
1281
+ method: "GET",
1282
+ path: templatePath(template),
1283
+ signal: options?.signal
1284
+ });
1285
+ }
1286
+ /** Changes its name, alias or description. */
1287
+ update(template, params, options) {
1288
+ return this.#http.request({
1289
+ method: "PATCH",
1290
+ path: templatePath(template),
1291
+ body: params,
1292
+ signal: options?.signal
1293
+ });
1294
+ }
1295
+ /** Deletes it with every version. Emails already sent keep their content. */
1296
+ delete(template, options) {
1297
+ return this.#http.request({
1298
+ method: "DELETE",
1299
+ path: templatePath(template),
1300
+ signal: options?.signal
1301
+ });
1302
+ }
1303
+ /** The draft, with `problems` that keep it from being published. */
1304
+ getDraft(template, options) {
1305
+ return this.#http.request({
1306
+ method: "GET",
1307
+ path: `${templatePath(template)}/draft`,
1308
+ signal: options?.signal
1309
+ });
1310
+ }
1311
+ /**
1312
+ * Replaces the whole draft. Sends keep using the published version until you publish. Never
1313
+ * automatically retried: every save is a new revision, so a repeat whose first attempt was saved
1314
+ * would fail with `template_revision_mismatch` (or, without `revision`, overwrite an edit saved
1315
+ * in between). Read the draft again before retrying.
1316
+ */
1317
+ updateDraft(template, params, options) {
1318
+ return this.#http.request({
1319
+ method: "PUT",
1320
+ path: `${templatePath(template)}/draft`,
1321
+ retry: false,
1322
+ body: params,
1323
+ signal: options?.signal
1324
+ });
1325
+ }
1326
+ /**
1327
+ * Publishes the draft as the next version, which sends use from then on. Automatically retried
1328
+ * only with a `revision`: a draft without changes since the last publish returns that version,
1329
+ * and one saved again since is refused. Without it, a retry could publish someone else's newer
1330
+ * edits as yet another version.
1331
+ */
1332
+ publish(template, params = {}, options) {
1333
+ return this.#http.request({
1334
+ method: "POST",
1335
+ path: `${templatePath(template)}/publish`,
1336
+ retry: params.revision !== void 0,
1337
+ body: params,
1338
+ signal: options?.signal
1339
+ });
1340
+ }
1341
+ };
1342
+ //#endregion
1343
+ //#region src/resources/received-emails.ts
1344
+ const emailPath = (id) => `/v1/received_emails/${encodeURIComponent(id)}`;
1345
+ /**
1346
+ * `coss.email.received`: emails received by the API key's environment, at its inbound address
1347
+ * (`address()`) or at a receiving domain (`coss.domains.receiving`). An `email.received` webhook
1348
+ * announces each one.
1349
+ */
1350
+ var ReceivedEmailsResource = class {
1351
+ #http;
1352
+ /** @internal Use `new Coss().email.received`. */
1353
+ constructor(http) {
1354
+ this.#http = http;
1355
+ }
1356
+ /** Lists received emails, newest first, without bodies. Pass the last `id` as `after` while `has_more` is true. */
1357
+ list(params = {}, options) {
1358
+ return this.#http.request({
1359
+ method: "GET",
1360
+ path: "/v1/received_emails",
1361
+ query: listQuery(params),
1362
+ signal: options?.signal
1363
+ });
1364
+ }
1365
+ /** Retrieves a received email with its headers and bodies. */
1366
+ get(id, options) {
1367
+ return this.#http.request({
1368
+ method: "GET",
1369
+ path: emailPath(id),
1370
+ signal: options?.signal
1371
+ });
1372
+ }
1373
+ /** The whole message as received (`message/rfc822` bytes). */
1374
+ getRaw(id, options) {
1375
+ return this.#http.request({
1376
+ method: "GET",
1377
+ path: `${emailPath(id)}/raw`,
1378
+ responseType: "bytes",
1379
+ signal: options?.signal
1380
+ });
1381
+ }
1382
+ /** An attachment's bytes (untrusted: its declared type is the attachment's `content_type`). */
1383
+ getAttachment(id, attachmentId, options) {
1384
+ return this.#http.request({
1385
+ method: "GET",
1386
+ path: `${emailPath(id)}/attachments/${encodeURIComponent(attachmentId)}`,
1387
+ responseType: "bytes",
1388
+ signal: options?.signal
1389
+ });
1390
+ }
1391
+ /**
1392
+ * The environment's inbound domain: email to any address at it (`support@<domain>`) is
1393
+ * received, with no DNS setup.
1394
+ */
1395
+ address(options) {
1396
+ return this.#http.request({
1397
+ method: "GET",
1398
+ path: "/v1/received_emails/address",
1399
+ signal: options?.signal
1400
+ });
1401
+ }
1402
+ /** Deletes a received email with its raw message and attachments right away. */
1403
+ delete(id, options) {
1404
+ return this.#http.request({
1405
+ method: "DELETE",
1406
+ path: emailPath(id),
1407
+ signal: options?.signal
1408
+ });
1409
+ }
1410
+ };
1411
+ //#endregion
1412
+ //#region src/resources/suppressions.ts
1413
+ /** The API's per-request maximum for imports. */
1414
+ const IMPORT_BATCH_SIZE = 1e3;
1415
+ /**
1416
+ * `coss.email.suppressions`: the suppression list of the API key's environment. Sends skip
1417
+ * suppressed recipients; permanent bounces and complaints are added automatically.
1418
+ */
1419
+ var SuppressionResource = class {
1420
+ #http;
1421
+ /** @internal Use `new Coss().email.suppressions`. */
1422
+ constructor(http) {
1423
+ this.#http = http;
1424
+ }
1425
+ /**
1426
+ * Suppresses an address for every email (`scope: "*"`, the default) or for one category, e.g.
1427
+ * when a recipient unsubscribes. Returns the existing entry if there already is one.
1428
+ */
1429
+ add(params, options) {
1430
+ return this.#http.request({
1431
+ method: "POST",
1432
+ path: "/v1/suppressions",
1433
+ body: params,
1434
+ idempotencyKey: options?.idempotencyKey,
1435
+ signal: options?.signal
1436
+ });
1437
+ }
1438
+ /**
1439
+ * Adds many entries at once (reason `import`), 1,000 per request. Invalid entries are skipped
1440
+ * and reported in `invalid`; entries already on the list count as `existing`. If a request
1441
+ * fails, the earlier batches stay imported: importing everything again is safe.
1442
+ */
1443
+ async import(params, options) {
1444
+ const total = {
1445
+ object: "suppression_import",
1446
+ created: 0,
1447
+ existing: 0,
1448
+ invalid: []
1449
+ };
1450
+ for (let start = 0; start === 0 || start < params.suppressions.length;) {
1451
+ const batch = params.suppressions.slice(start, start + IMPORT_BATCH_SIZE);
1452
+ const result = await this.#http.request({
1453
+ method: "POST",
1454
+ path: "/v1/suppressions/import",
1455
+ body: { suppressions: batch },
1456
+ signal: options?.signal
1457
+ });
1458
+ if (result.error) return result;
1459
+ total.created += result.data.created;
1460
+ total.existing += result.data.existing;
1461
+ for (const entry of result.data.invalid) total.invalid.push({
1462
+ ...entry,
1463
+ index: entry.index + start
1464
+ });
1465
+ start += IMPORT_BATCH_SIZE;
1466
+ }
1467
+ return {
1468
+ data: total,
1469
+ error: null
1470
+ };
1471
+ }
1472
+ /**
1473
+ * The whole list as CSV (`address,scope,reason,created_at,email_id,id`, oldest first), which
1474
+ * `import` (or the dashboard) accepts again.
1475
+ */
1476
+ export(params = {}, options) {
1477
+ return this.#http.request({
1478
+ method: "GET",
1479
+ path: "/v1/suppressions/export",
1480
+ query: { ...params },
1481
+ responseType: "text",
1482
+ signal: options?.signal
1483
+ });
1484
+ }
1485
+ /** Lists entries, newest first. Pass the last `id` as `after` while `has_more` is true. */
1486
+ list(params = {}, options) {
1487
+ return this.#http.request({
1488
+ method: "GET",
1489
+ path: "/v1/suppressions",
1490
+ query: { ...params },
1491
+ signal: options?.signal
1492
+ });
1493
+ }
1494
+ /** Whether a send to `address` (with `category`, if given) would skip it. */
1495
+ check(params, options) {
1496
+ return this.#http.request({
1497
+ method: "GET",
1498
+ path: "/v1/suppressions/check",
1499
+ query: { ...params },
1500
+ signal: options?.signal
1501
+ });
1502
+ }
1503
+ /** Retrieves one entry. */
1504
+ get(id, options) {
1505
+ return this.#http.request({
1506
+ method: "GET",
1507
+ path: `/v1/suppressions/${encodeURIComponent(id)}`,
1508
+ signal: options?.signal
1509
+ });
1510
+ }
1511
+ /** Removes an entry, so later sends reach the address again (e.g. when a recipient resubscribes). */
1512
+ remove(id, options) {
1513
+ return this.#http.request({
1514
+ method: "DELETE",
1515
+ path: `/v1/suppressions/${encodeURIComponent(id)}`,
1516
+ signal: options?.signal
1517
+ });
1518
+ }
1519
+ };
1520
+ //#endregion
1521
+ //#region src/resources/tracking.ts
1522
+ const categoryPath = (category) => `/v1/tracking_settings/categories/${encodeURIComponent(category)}`;
1523
+ /**
1524
+ * `coss.email.tracking`: which emails of the API key's environment have their opens and clicks
1525
+ * tracked and get one-click unsubscribe headers. Emails keep the settings that applied when they
1526
+ * were sent; a send's own `tracking` option wins over them.
1527
+ */
1528
+ var TrackingResource = class {
1529
+ #http;
1530
+ /** @internal Use `new Coss().email.tracking`. */
1531
+ constructor(http) {
1532
+ this.#http = http;
1533
+ }
1534
+ /** The environment default and the category rules. */
1535
+ get(options) {
1536
+ return this.#http.request({
1537
+ method: "GET",
1538
+ path: "/v1/tracking_settings",
1539
+ signal: options?.signal
1540
+ });
1541
+ }
1542
+ /**
1543
+ * Sets the environment default, which applies to emails without a rule for their category.
1544
+ * Fields left out keep their value; a new default starts with both off.
1545
+ */
1546
+ updateDefault(params, options) {
1547
+ return this.#http.request({
1548
+ method: "PATCH",
1549
+ path: "/v1/tracking_settings/default",
1550
+ body: params,
1551
+ signal: options?.signal
1552
+ });
1553
+ }
1554
+ /** Removes the environment default: emails without a category rule are then not tracked. */
1555
+ removeDefault(options) {
1556
+ return this.#http.request({
1557
+ method: "DELETE",
1558
+ path: "/v1/tracking_settings/default",
1559
+ signal: options?.signal
1560
+ });
1561
+ }
1562
+ /**
1563
+ * Sets the rule for emails sent with `category`, which wins over the default. Fields left out
1564
+ * keep their value; a new rule starts with both off.
1565
+ */
1566
+ updateCategory(category, params, options) {
1567
+ return this.#http.request({
1568
+ method: "PATCH",
1569
+ path: categoryPath(category),
1570
+ body: params,
1571
+ signal: options?.signal
1572
+ });
1573
+ }
1574
+ /** Removes a category's rule: its emails follow the environment default again. */
1575
+ removeCategory(category, options) {
1576
+ return this.#http.request({
1577
+ method: "DELETE",
1578
+ path: categoryPath(category),
1579
+ signal: options?.signal
1580
+ });
1581
+ }
1582
+ };
1583
+ //#endregion
1584
+ //#region src/resources/email.ts
1585
+ /** `coss.email`: transactional email. */
1586
+ var EmailResource = class {
1587
+ #http;
1588
+ /** The environment's suppression list. */
1589
+ suppressions;
1590
+ /** The environment's open and click tracking settings. */
1591
+ tracking;
1592
+ /** Emails the environment received. */
1593
+ received;
1594
+ /** The environment's email templates. */
1595
+ templates;
1596
+ /** The organization's dedicated IPs. */
1597
+ dedicatedIps;
1598
+ /** The environment's contacts, with custom properties and consent. */
1599
+ contacts;
1600
+ /** Static lists of contacts. */
1601
+ audiences;
1602
+ /** Filters over the contacts. */
1603
+ segments;
1604
+ /** What contacts subscribe to. */
1605
+ topics;
1606
+ /** The sender name and postal address of marketing email. */
1607
+ marketingSettings;
1608
+ /** Marketing emails to an audience or a segment. */
1609
+ broadcasts;
1610
+ /** @internal Use `new Coss().email`. */
1611
+ constructor(http) {
1612
+ this.#http = http;
1613
+ this.suppressions = new SuppressionResource(http);
1614
+ this.tracking = new TrackingResource(http);
1615
+ this.received = new ReceivedEmailsResource(http);
1616
+ this.templates = new EmailTemplatesResource(http);
1617
+ this.dedicatedIps = new DedicatedIpsResource(http);
1618
+ this.contacts = new ContactsResource(http);
1619
+ this.audiences = new AudiencesResource(http);
1620
+ this.segments = new SegmentsResource(http);
1621
+ this.topics = new TopicsResource(http);
1622
+ this.marketingSettings = new MarketingSettingsResource(http);
1623
+ this.broadcasts = new BroadcastsResource(http);
1624
+ }
1625
+ /**
1626
+ * Sends an email, with its own subject and body or from a published template (`template_id`
1627
+ * and `variables`). It is returned `queued` and handed to the delivery provider moments later:
1628
+ * `get` it or listen for the `email.sent` / `email.failed` webhooks for the outcome. Safe to
1629
+ * retry: the request always carries an `Idempotency-Key` (yours, or a generated one reused
1630
+ * across automatic retries), so the API never sends it twice. A `react` component is rendered
1631
+ * once, before the first attempt.
1632
+ */
1633
+ async send(params, options) {
1634
+ const { body, error } = await renderReactEmail(params, "react");
1635
+ if (error) return {
1636
+ data: null,
1637
+ error
1638
+ };
1639
+ return this.#http.request({
1640
+ method: "POST",
1641
+ path: "/v1/emails",
1642
+ body,
1643
+ idempotencyKey: options?.idempotencyKey,
1644
+ signal: options?.signal
1645
+ });
1646
+ }
1647
+ /**
1648
+ * Sends up to 100 emails in one request, each as `send` would. The batch is all or nothing: if
1649
+ * any email is invalid (the error's `issues` name it, e.g. `emails.3.to`), none is sent. Its
1650
+ * recipients count against the send quota together. Safe to retry: one `Idempotency-Key` covers
1651
+ * the whole batch. Larger lists need several calls, each with its own key.
1652
+ */
1653
+ async sendBatch(emails, options) {
1654
+ const rendered = await Promise.all(emails.map((email, index) => renderReactEmail(email, `emails.${index}.react`)));
1655
+ const bodies = [];
1656
+ for (const { body, error } of rendered) {
1657
+ if (error) return {
1658
+ data: null,
1659
+ error
1660
+ };
1661
+ bodies.push(body);
1662
+ }
1663
+ return this.#http.request({
1664
+ method: "POST",
1665
+ path: "/v1/emails/batch",
1666
+ body: { emails: bodies },
1667
+ idempotencyKey: options?.idempotencyKey,
1668
+ signal: options?.signal
1669
+ });
1670
+ }
1671
+ /** Retrieves a batch sent by `sendBatch`, with its emails' current statuses in request order. */
1672
+ getBatch(id, options) {
1673
+ return this.#http.request({
1674
+ method: "GET",
1675
+ path: `/v1/emails/batch/${encodeURIComponent(id)}`,
1676
+ signal: options?.signal
1677
+ });
1678
+ }
1679
+ /** Retrieves an email sent from the same environment as the API key. */
1680
+ get(id, options) {
1681
+ return this.#http.request({
1682
+ method: "GET",
1683
+ path: `/v1/emails/${encodeURIComponent(id)}`,
1684
+ signal: options?.signal
1685
+ });
1686
+ }
1687
+ /**
1688
+ * Lists emails sent from the same environment as the API key, newest first and without their
1689
+ * content (`get` one for `html`, `text`, `headers` and `attachments`). Pass the last `id` as
1690
+ * `after` while `has_more` is true.
1691
+ */
1692
+ list(params = {}, options) {
1693
+ return this.#http.request({
1694
+ method: "GET",
1695
+ path: "/v1/emails",
1696
+ query: listQuery(params),
1697
+ signal: options?.signal
1698
+ });
1699
+ }
1700
+ /**
1701
+ * Cancels a scheduled email. Canceling an already canceled email returns it; any other email
1702
+ * fails with `email_not_cancelable`.
1703
+ */
1704
+ cancel(id, options) {
1705
+ return this.#http.request({
1706
+ method: "POST",
1707
+ path: `/v1/emails/${encodeURIComponent(id)}/cancel`,
1708
+ signal: options?.signal
1709
+ });
1710
+ }
1711
+ /**
1712
+ * Moves a scheduled email to a new time and records an `email.rescheduled` event. The time it
1713
+ * already has returns it unchanged; an email that is no longer scheduled fails with
1714
+ * `email_not_reschedulable`.
1715
+ */
1716
+ reschedule(id, params, options) {
1717
+ return this.#http.request({
1718
+ method: "POST",
1719
+ path: `/v1/emails/${encodeURIComponent(id)}/reschedule`,
1720
+ body: params,
1721
+ signal: options?.signal
1722
+ });
1723
+ }
1724
+ /** Lists an email's delivery events, oldest first. */
1725
+ listEvents(id, options) {
1726
+ return this.#http.request({
1727
+ method: "GET",
1728
+ path: `/v1/emails/${encodeURIComponent(id)}/events`,
1729
+ signal: options?.signal
1730
+ });
1731
+ }
1732
+ /** Lists the links tracked for clicks in an email, in document order, with click counts. */
1733
+ listLinks(id, options) {
1734
+ return this.#http.request({
1735
+ method: "GET",
1736
+ path: `/v1/emails/${encodeURIComponent(id)}/links`,
1737
+ signal: options?.signal
1738
+ });
1739
+ }
1740
+ };
1741
+ //#endregion
1742
+ //#region src/resources/environment.ts
1743
+ /** `coss.environment`: which environment, project and organization the API key belongs to. */
1744
+ var EnvironmentResource = class {
1745
+ #http;
1746
+ /** @internal Use `new Coss().environment`. */
1747
+ constructor(http) {
1748
+ this.#http = http;
1749
+ }
1750
+ /** The API key's environment, its project and organization, and the key itself. */
1751
+ get(options) {
1752
+ return this.#http.request({
1753
+ method: "GET",
1754
+ path: "/v1/environment",
1755
+ signal: options?.signal
1756
+ });
1757
+ }
1758
+ };
1759
+ //#endregion
1760
+ //#region src/resources/hooks.ts
1761
+ const segment = encodeURIComponent;
1762
+ const applicationPath = (application) => `/v1/hooks/applications/${segment(application)}`;
1763
+ const endpointPath = (application, id) => `${applicationPath(application)}/endpoints/${segment(id)}`;
1764
+ /** `coss.hooks.applications`: your customers, each with their own endpoints and portal. */
1765
+ var HookApplicationsResource = class {
1766
+ #http;
1767
+ /** @internal Use `new Coss().hooks.applications`. */
1768
+ constructor(http) {
1769
+ this.#http = http;
1770
+ }
1771
+ /** Creates an application. Never automatically retried (the API does not deduplicate it). */
1772
+ create(params, options) {
1773
+ return this.#http.request({
1774
+ method: "POST",
1775
+ path: "/v1/hooks/applications",
1776
+ retry: false,
1777
+ body: params,
1778
+ idempotencyKey: options?.idempotencyKey,
1779
+ signal: options?.signal
1780
+ });
1781
+ }
1782
+ /** Lists the environment's applications, newest first. */
1783
+ list(params = {}, options) {
1784
+ return this.#http.request({
1785
+ method: "GET",
1786
+ path: "/v1/hooks/applications",
1787
+ query: listQuery(params),
1788
+ signal: options?.signal
1789
+ });
1790
+ }
1791
+ /** Retrieves an application by its id or uid. */
1792
+ get(application, options) {
1793
+ return this.#http.request({
1794
+ method: "GET",
1795
+ path: applicationPath(application),
1796
+ signal: options?.signal
1797
+ });
1798
+ }
1799
+ update(application, params, options) {
1800
+ return this.#http.request({
1801
+ method: "PATCH",
1802
+ path: applicationPath(application),
1803
+ body: params,
1804
+ signal: options?.signal
1805
+ });
1806
+ }
1807
+ /** Deletes the application with its endpoints and portal sessions. */
1808
+ delete(application, options) {
1809
+ return this.#http.request({
1810
+ method: "DELETE",
1811
+ path: applicationPath(application),
1812
+ signal: options?.signal
1813
+ });
1814
+ }
1815
+ };
1816
+ /** `coss.hooks.eventTypes`: the catalog of messages you send, which your users subscribe from. */
1817
+ var HookEventTypesResource = class {
1818
+ #http;
1819
+ /** @internal Use `new Coss().hooks.eventTypes`. */
1820
+ constructor(http) {
1821
+ this.#http = http;
1822
+ }
1823
+ /** Creates an event type. Never automatically retried (a retry would find it existing). */
1824
+ create(params, options) {
1825
+ return this.#http.request({
1826
+ method: "POST",
1827
+ path: "/v1/hooks/event_types",
1828
+ retry: false,
1829
+ body: params,
1830
+ idempotencyKey: options?.idempotencyKey,
1831
+ signal: options?.signal
1832
+ });
1833
+ }
1834
+ /** Lists event types by name (archived ones only with `include_archived`). */
1835
+ list(params = {}, options) {
1836
+ return this.#http.request({
1837
+ method: "GET",
1838
+ path: "/v1/hooks/event_types",
1839
+ query: listQuery(params),
1840
+ signal: options?.signal
1841
+ });
1842
+ }
1843
+ get(name, options) {
1844
+ return this.#http.request({
1845
+ method: "GET",
1846
+ path: `/v1/hooks/event_types/${segment(name)}`,
1847
+ signal: options?.signal
1848
+ });
1849
+ }
1850
+ /** Changes its description, schema or examples, or archives or restores it. */
1851
+ update(name, params, options) {
1852
+ return this.#http.request({
1853
+ method: "PATCH",
1854
+ path: `/v1/hooks/event_types/${segment(name)}`,
1855
+ body: params,
1856
+ signal: options?.signal
1857
+ });
1858
+ }
1859
+ /** Archives it: hidden from the portal and new subscriptions. */
1860
+ archive(name, options) {
1861
+ return this.#http.request({
1862
+ method: "DELETE",
1863
+ path: `/v1/hooks/event_types/${segment(name)}`,
1864
+ signal: options?.signal
1865
+ });
1866
+ }
1867
+ };
1868
+ /** `coss.hooks.endpoints`: an application's endpoints (usually added by its users in the portal). */
1869
+ var HookEndpointsResource = class {
1870
+ #http;
1871
+ /** @internal Use `new Coss().hooks.endpoints`. */
1872
+ constructor(http) {
1873
+ this.#http = http;
1874
+ }
1875
+ /** Creates an endpoint; the result carries its secret. Never automatically retried. */
1876
+ create(application, params, options) {
1877
+ return this.#http.request({
1878
+ method: "POST",
1879
+ path: `${applicationPath(application)}/endpoints`,
1880
+ retry: false,
1881
+ body: params,
1882
+ idempotencyKey: options?.idempotencyKey,
1883
+ signal: options?.signal
1884
+ });
1885
+ }
1886
+ list(application, options) {
1887
+ return this.#http.request({
1888
+ method: "GET",
1889
+ path: `${applicationPath(application)}/endpoints`,
1890
+ signal: options?.signal
1891
+ });
1892
+ }
1893
+ get(application, id, options) {
1894
+ return this.#http.request({
1895
+ method: "GET",
1896
+ path: endpointPath(application, id),
1897
+ signal: options?.signal
1898
+ });
1899
+ }
1900
+ update(application, id, params, options) {
1901
+ return this.#http.request({
1902
+ method: "PATCH",
1903
+ path: endpointPath(application, id),
1904
+ body: params,
1905
+ signal: options?.signal
1906
+ });
1907
+ }
1908
+ delete(application, id, options) {
1909
+ return this.#http.request({
1910
+ method: "DELETE",
1911
+ path: endpointPath(application, id),
1912
+ signal: options?.signal
1913
+ });
1914
+ }
1915
+ /** The endpoint's current signing secret. */
1916
+ getSecret(application, id, options) {
1917
+ return this.#http.request({
1918
+ method: "GET",
1919
+ path: `${endpointPath(application, id)}/secret`,
1920
+ signal: options?.signal
1921
+ });
1922
+ }
1923
+ /** Replaces the signing secret. Never automatically retried (a second roll would stop the first). */
1924
+ rollSecret(application, id, params = {}, options) {
1925
+ return this.#http.request({
1926
+ method: "POST",
1927
+ path: `${endpointPath(application, id)}/roll_secret`,
1928
+ retry: false,
1929
+ body: params,
1930
+ idempotencyKey: options?.idempotencyKey,
1931
+ signal: options?.signal
1932
+ });
1933
+ }
1934
+ /** Requeues the endpoint's failed deliveries of messages sent since `since` (in the background). */
1935
+ recover(application, id, params, options) {
1936
+ const since = params.since instanceof Date ? params.since.toISOString() : params.since;
1937
+ return this.#http.request({
1938
+ method: "POST",
1939
+ path: `${endpointPath(application, id)}/recover`,
1940
+ body: { since },
1941
+ idempotencyKey: options?.idempotencyKey,
1942
+ signal: options?.signal
1943
+ });
1944
+ }
1945
+ getRecovery(application, id, recoveryId, options) {
1946
+ return this.#http.request({
1947
+ method: "GET",
1948
+ path: `${endpointPath(application, id)}/recoveries/${segment(recoveryId)}`,
1949
+ signal: options?.signal
1950
+ });
1951
+ }
1952
+ /** Sends a message to this endpoint only. Never automatically retried. */
1953
+ sendTest(application, id, params, options) {
1954
+ return this.#http.request({
1955
+ method: "POST",
1956
+ path: `${endpointPath(application, id)}/test`,
1957
+ retry: false,
1958
+ body: params,
1959
+ idempotencyKey: options?.idempotencyKey,
1960
+ signal: options?.signal
1961
+ });
1962
+ }
1963
+ /** The endpoint's deliveries, newest first. */
1964
+ listDeliveries(application, id, params = {}, options) {
1965
+ return this.#http.request({
1966
+ method: "GET",
1967
+ path: `${endpointPath(application, id)}/deliveries`,
1968
+ query: listQuery(params),
1969
+ signal: options?.signal
1970
+ });
1971
+ }
1972
+ };
1973
+ /** `coss.hooks.messages`: send a message once; coss.com delivers it to every subscribed endpoint. */
1974
+ var HookMessagesResource = class {
1975
+ #http;
1976
+ /** @internal Use `new Coss().hooks.messages`. */
1977
+ constructor(http) {
1978
+ this.#http = http;
1979
+ }
1980
+ /**
1981
+ * Sends a message to an application. Safe to retry: every attempt carries the same
1982
+ * `Idempotency-Key`, so a retried send is delivered once.
1983
+ */
1984
+ send(params, options) {
1985
+ const { application, ...body } = params;
1986
+ return this.#http.request({
1987
+ method: "POST",
1988
+ path: `${applicationPath(application)}/messages`,
1989
+ body,
1990
+ idempotencyKey: options?.idempotencyKey,
1991
+ signal: options?.signal
1992
+ });
1993
+ }
1994
+ /** The application's messages, newest first. */
1995
+ list(application, params = {}, options) {
1996
+ return this.#http.request({
1997
+ method: "GET",
1998
+ path: `${applicationPath(application)}/messages`,
1999
+ query: listQuery(params),
2000
+ signal: options?.signal
2001
+ });
2002
+ }
2003
+ get(application, id, options) {
2004
+ return this.#http.request({
2005
+ method: "GET",
2006
+ path: `${applicationPath(application)}/messages/${segment(id)}`,
2007
+ signal: options?.signal
2008
+ });
2009
+ }
2010
+ /** One delivery per endpoint the message went to. */
2011
+ listDeliveries(application, id, options) {
2012
+ return this.#http.request({
2013
+ method: "GET",
2014
+ path: `${applicationPath(application)}/messages/${segment(id)}/deliveries`,
2015
+ signal: options?.signal
2016
+ });
2017
+ }
2018
+ };
2019
+ /** `coss.hooks.deliveries`: one message's delivery to one endpoint. */
2020
+ var HookDeliveriesResource = class {
2021
+ #http;
2022
+ /** @internal Use `new Coss().hooks.deliveries`. */
2023
+ constructor(http) {
2024
+ this.#http = http;
2025
+ }
2026
+ get(application, id, options) {
2027
+ return this.#http.request({
2028
+ method: "GET",
2029
+ path: `${applicationPath(application)}/deliveries/${segment(id)}`,
2030
+ signal: options?.signal
2031
+ });
2032
+ }
2033
+ /** Every HTTP request of the delivery, oldest first. */
2034
+ listAttempts(application, id, options) {
2035
+ return this.#http.request({
2036
+ method: "GET",
2037
+ path: `${applicationPath(application)}/deliveries/${segment(id)}/attempts`,
2038
+ signal: options?.signal
2039
+ });
2040
+ }
2041
+ /** Queues a finished delivery for one more attempt. Never automatically retried. */
2042
+ retry(application, id, options) {
2043
+ return this.#http.request({
2044
+ method: "POST",
2045
+ path: `${applicationPath(application)}/deliveries/${segment(id)}/retry`,
2046
+ retry: false,
2047
+ idempotencyKey: options?.idempotencyKey,
2048
+ signal: options?.signal
2049
+ });
2050
+ }
2051
+ };
2052
+ /** `coss.hooks.portal`: a ready-made page for an application's user to manage their endpoints. */
2053
+ var HookPortalResource = class {
2054
+ #http;
2055
+ /** @internal Use `new Coss().hooks.portal`. */
2056
+ constructor(http) {
2057
+ this.#http = http;
2058
+ }
2059
+ /**
2060
+ * Signs the application's user in to the portal: open or embed the returned `url`. Call it from
2061
+ * your backend each time the user opens the portal.
2062
+ */
2063
+ createSession(params, options) {
2064
+ const { application, ...body } = params;
2065
+ return this.#http.request({
2066
+ method: "POST",
2067
+ path: `${applicationPath(application)}/portal_sessions`,
2068
+ body,
2069
+ idempotencyKey: options?.idempotencyKey,
2070
+ signal: options?.signal
2071
+ });
2072
+ }
2073
+ };
2074
+ /**
2075
+ * `coss.hooks`: webhooks as a service. Send a message to one of your applications once
2076
+ * (`coss.hooks.messages.send`); coss.com delivers it to every endpoint its users registered, signed
2077
+ * (Standard Webhooks), with retries, and gives them a portal to manage it all.
2078
+ */
2079
+ var HooksResource = class {
2080
+ applications;
2081
+ eventTypes;
2082
+ endpoints;
2083
+ messages;
2084
+ deliveries;
2085
+ portal;
2086
+ /** @internal Use `new Coss().hooks`. */
2087
+ constructor(http) {
2088
+ this.applications = new HookApplicationsResource(http);
2089
+ this.eventTypes = new HookEventTypesResource(http);
2090
+ this.endpoints = new HookEndpointsResource(http);
2091
+ this.messages = new HookMessagesResource(http);
2092
+ this.deliveries = new HookDeliveriesResource(http);
2093
+ this.portal = new HookPortalResource(http);
2094
+ }
2095
+ };
2096
+ //#endregion
2097
+ //#region src/resources/log-drains.ts
2098
+ const drainPath = (id) => `/v1/log_drains/${encodeURIComponent(id)}`;
2099
+ /** `coss.logDrains.batches`: a drain's delivery log, each batch with every HTTP attempt. */
2100
+ var LogDrainBatchesResource = class {
2101
+ #http;
2102
+ /** @internal Use `new Coss().logDrains.batches`. */
2103
+ constructor(http) {
2104
+ this.#http = http;
2105
+ }
2106
+ /** Lists the drain's batches, newest first. Pass the last `id` as `after` while `has_more` is true. */
2107
+ list(drainId, params = {}, options) {
2108
+ return this.#http.request({
2109
+ method: "GET",
2110
+ path: `${drainPath(drainId)}/batches`,
2111
+ query: { ...params },
2112
+ signal: options?.signal
2113
+ });
2114
+ }
2115
+ /** Retrieves one batch of the drain. */
2116
+ get(drainId, id, options) {
2117
+ return this.#http.request({
2118
+ method: "GET",
2119
+ path: `${drainPath(drainId)}/batches/${encodeURIComponent(id)}`,
2120
+ signal: options?.signal
2121
+ });
2122
+ }
2123
+ };
2124
+ /**
2125
+ * `coss.logDrains`: HTTPS endpoints the environment's log is streamed to (API requests, email
2126
+ * events, webhook attempts and audit events), in signed NDJSON batches. Drains belong to the API
2127
+ * key's own environment, so a `coss_test_…` key only ever manages sandbox drains.
2128
+ */
2129
+ var LogDrainsResource = class {
2130
+ #http;
2131
+ /** Each drain's delivery log. */
2132
+ batches;
2133
+ /** @internal Use `new Coss().logDrains`. */
2134
+ constructor(http) {
2135
+ this.#http = http;
2136
+ this.batches = new LogDrainBatchesResource(http);
2137
+ }
2138
+ /**
2139
+ * Creates a drain. The result carries its `secret` (returned only here) — store it and pass it
2140
+ * to `verifyLogDrainBatch` for every request. Creation is never automatically retried: the API
2141
+ * does not deduplicate it. After a lost response, list drains and remove the one with the
2142
+ * unknown secret before creating a replacement.
2143
+ */
2144
+ create(params, options) {
2145
+ return this.#http.request({
2146
+ method: "POST",
2147
+ path: "/v1/log_drains",
2148
+ retry: false,
2149
+ body: params,
2150
+ idempotencyKey: options?.idempotencyKey,
2151
+ signal: options?.signal
2152
+ });
2153
+ }
2154
+ /** Lists the environment's drains, oldest first. Secrets are never included. */
2155
+ list(options) {
2156
+ return this.#http.request({
2157
+ method: "GET",
2158
+ path: "/v1/log_drains",
2159
+ signal: options?.signal
2160
+ });
2161
+ }
2162
+ /** Retrieves a drain of the API key's environment. */
2163
+ get(id, options) {
2164
+ return this.#http.request({
2165
+ method: "GET",
2166
+ path: drainPath(id),
2167
+ signal: options?.signal
2168
+ });
2169
+ }
2170
+ /**
2171
+ * Changes the URL, description, sources or `enabled` flag. The signing secret stays the same.
2172
+ * Disabling drops the lines still waiting for the drain; enabling it again (also after coss.com
2173
+ * disabled it for failing) sends lines logged from then on.
2174
+ */
2175
+ update(id, params, options) {
2176
+ return this.#http.request({
2177
+ method: "PATCH",
2178
+ path: drainPath(id),
2179
+ body: params,
2180
+ signal: options?.signal
2181
+ });
2182
+ }
2183
+ /** Deletes the drain and its batch log. To pause it instead, update `enabled` to false. */
2184
+ delete(id, options) {
2185
+ return this.#http.request({
2186
+ method: "DELETE",
2187
+ path: drainPath(id),
2188
+ signal: options?.signal
2189
+ });
2190
+ }
2191
+ };
2192
+ //#endregion
2193
+ //#region src/resources/logs.ts
2194
+ /**
2195
+ * `coss.logs`: the API key's environment log, newest first: API requests made with its keys,
2196
+ * email events and webhook delivery attempts.
2197
+ */
2198
+ var LogsResource = class {
2199
+ #http;
2200
+ /** @internal Use `new Coss().logs`. */
2201
+ constructor(http) {
2202
+ this.#http = http;
2203
+ }
2204
+ /**
2205
+ * Lists log lines, newest first (the last hour unless `from`/`to` say otherwise). Pass the last
2206
+ * `id` as `after` while `has_more` is true.
2207
+ */
2208
+ list(params = {}, options) {
2209
+ return this.#http.request({
2210
+ method: "GET",
2211
+ path: "/v1/logs",
2212
+ query: listQuery(params),
2213
+ signal: options?.signal
2214
+ });
2215
+ }
2216
+ /** Retrieves one log line. */
2217
+ get(id, options) {
2218
+ return this.#http.request({
2219
+ method: "GET",
2220
+ path: `/v1/logs/${encodeURIComponent(id)}`,
2221
+ signal: options?.signal
2222
+ });
2223
+ }
2224
+ };
2225
+ //#endregion
2226
+ //#region src/resources/reputation.ts
2227
+ /**
2228
+ * `coss.reputation`: the organization's standing with the coss.com reputation guard, which suspends
2229
+ * live sending automatically when bounce or complaint rates cross its thresholds.
2230
+ */
2231
+ var ReputationResource = class {
2232
+ #http;
2233
+ /** @internal Use `new Coss().reputation`. */
2234
+ constructor(http) {
2235
+ this.#http = http;
2236
+ }
2237
+ /** The current status and the latest rates measured over live environments. */
2238
+ get(options) {
2239
+ return this.#http.request({
2240
+ method: "GET",
2241
+ path: "/v1/reputation",
2242
+ signal: options?.signal
2243
+ });
2244
+ }
2245
+ };
2246
+ //#endregion
2247
+ //#region src/resources/risk.ts
2248
+ const LIST_ENTRY_TYPES = [
2249
+ "email",
2250
+ "domain",
2251
+ "ip",
2252
+ "asn",
2253
+ "country",
2254
+ "user"
2255
+ ];
2256
+ /** `coss.risk.assessments`: reading past assessments (`assess` lives on `coss.risk`). */
2257
+ var RiskAssessmentsResource = class {
2258
+ #http;
2259
+ /** @internal Use `new Coss().risk.assessments`. */
2260
+ constructor(http) {
2261
+ this.#http = http;
2262
+ }
2263
+ /** Retrieves one assessment. */
2264
+ get(id, options) {
2265
+ return this.#http.request({
2266
+ method: "GET",
2267
+ path: `/v1/risk/assessments/${encodeURIComponent(id)}`,
2268
+ signal: options?.signal
2269
+ });
2270
+ }
2271
+ /**
2272
+ * Lists assessments, newest first (`review_status: "open"` is the review queue). Pass the last
2273
+ * `id` as `after` while `has_more` is true.
2274
+ */
2275
+ list(params = {}, options) {
2276
+ return this.#http.request({
2277
+ method: "GET",
2278
+ path: "/v1/risk/assessments",
2279
+ query: { ...params },
2280
+ signal: options?.signal
2281
+ });
2282
+ }
2283
+ };
2284
+ /** `coss.risk.lists`: the environment's allow and block lists. */
2285
+ var RiskListsResource = class {
2286
+ #http;
2287
+ /** @internal Use `new Coss().risk.lists`. */
2288
+ constructor(http) {
2289
+ this.#http = http;
2290
+ }
2291
+ /**
2292
+ * Adds an entry: `coss.risk.lists.add("block", { domain: "evil.example" })`. An allow-list match
2293
+ * makes decisions `allow`, a block-list match `block`. Returns the existing entry if there is one.
2294
+ */
2295
+ add(list, entry, options) {
2296
+ const type = LIST_ENTRY_TYPES.find((name) => name in entry);
2297
+ const body = {
2298
+ list,
2299
+ type,
2300
+ value: String(entry[type] ?? ""),
2301
+ ...entry.note === void 0 ? {} : { note: entry.note },
2302
+ ...entry.expires_at === void 0 ? {} : { expires_at: entry.expires_at instanceof Date ? entry.expires_at.toISOString() : entry.expires_at }
2303
+ };
2304
+ return this.#http.request({
2305
+ method: "POST",
2306
+ path: "/v1/risk/list_entries",
2307
+ body,
2308
+ idempotencyKey: options?.idempotencyKey,
2309
+ signal: options?.signal
2310
+ });
2311
+ }
2312
+ /** Lists entries, newest first. Pass the last `id` as `after` while `has_more` is true. */
2313
+ list(params = {}, options) {
2314
+ return this.#http.request({
2315
+ method: "GET",
2316
+ path: "/v1/risk/list_entries",
2317
+ query: { ...params },
2318
+ signal: options?.signal
2319
+ });
2320
+ }
2321
+ /** Retrieves one entry. */
2322
+ get(id, options) {
2323
+ return this.#http.request({
2324
+ method: "GET",
2325
+ path: `/v1/risk/list_entries/${encodeURIComponent(id)}`,
2326
+ signal: options?.signal
2327
+ });
2328
+ }
2329
+ /** Removes an entry. */
2330
+ remove(id, options) {
2331
+ return this.#http.request({
2332
+ method: "DELETE",
2333
+ path: `/v1/risk/list_entries/${encodeURIComponent(id)}`,
2334
+ signal: options?.signal
2335
+ });
2336
+ }
2337
+ };
2338
+ /** `coss.risk.settings`: the environment's thresholds, disabled reasons and captcha provider. */
2339
+ var RiskSettingsResource = class {
2340
+ #http;
2341
+ /** @internal Use `new Coss().risk.settings`. */
2342
+ constructor(http) {
2343
+ this.#http = http;
2344
+ }
2345
+ get(options) {
2346
+ return this.#http.request({
2347
+ method: "GET",
2348
+ path: "/v1/risk/settings",
2349
+ signal: options?.signal
2350
+ });
2351
+ }
2352
+ /** Changes the fields given. The captcha secret is write-only. */
2353
+ update(params, options) {
2354
+ return this.#http.request({
2355
+ method: "PATCH",
2356
+ path: "/v1/risk/settings",
2357
+ body: params,
2358
+ signal: options?.signal
2359
+ });
2360
+ }
2361
+ };
2362
+ /**
2363
+ * `coss.risk.rules`: the environment's rules. After the lists, the first enabled rule whose
2364
+ * conditions match an event decides it.
2365
+ */
2366
+ var RiskRulesResource = class {
2367
+ #http;
2368
+ /** @internal Use `new Coss().risk.rules`. */
2369
+ constructor(http) {
2370
+ this.#http = http;
2371
+ }
2372
+ /** Creates a rule. Not retried automatically: a retry could create it twice. */
2373
+ create(params, options) {
2374
+ return this.#http.request({
2375
+ method: "POST",
2376
+ path: "/v1/risk/rules",
2377
+ body: params,
2378
+ retry: false,
2379
+ signal: options?.signal
2380
+ });
2381
+ }
2382
+ /** Every rule, in evaluation order. */
2383
+ list(options) {
2384
+ return this.#http.request({
2385
+ method: "GET",
2386
+ path: "/v1/risk/rules",
2387
+ signal: options?.signal
2388
+ });
2389
+ }
2390
+ get(id, options) {
2391
+ return this.#http.request({
2392
+ method: "GET",
2393
+ path: `/v1/risk/rules/${encodeURIComponent(id)}`,
2394
+ signal: options?.signal
2395
+ });
2396
+ }
2397
+ /** Changes the fields given; `conditions` replaces the whole list. */
2398
+ update(id, params, options) {
2399
+ return this.#http.request({
2400
+ method: "PATCH",
2401
+ path: `/v1/risk/rules/${encodeURIComponent(id)}`,
2402
+ body: params,
2403
+ signal: options?.signal
2404
+ });
2405
+ }
2406
+ delete(id, options) {
2407
+ return this.#http.request({
2408
+ method: "DELETE",
2409
+ path: `/v1/risk/rules/${encodeURIComponent(id)}`,
2410
+ signal: options?.signal
2411
+ });
2412
+ }
2413
+ };
2414
+ /**
2415
+ * `coss.risk.limitBuckets`: your rate limit buckets, which `coss.risk.limit()` charges per key.
2416
+ */
2417
+ var RiskLimitBucketsResource = class {
2418
+ #http;
2419
+ /** @internal Use `new Coss().risk.limitBuckets`. */
2420
+ constructor(http) {
2421
+ this.#http = http;
2422
+ }
2423
+ /** Creates a bucket. Not retried automatically: a retry would find its name taken. */
2424
+ create(params, options) {
2425
+ return this.#http.request({
2426
+ method: "POST",
2427
+ path: "/v1/risk/limit_buckets",
2428
+ body: params,
2429
+ retry: false,
2430
+ signal: options?.signal
2431
+ });
2432
+ }
2433
+ /** Every bucket, by name. */
2434
+ list(options) {
2435
+ return this.#http.request({
2436
+ method: "GET",
2437
+ path: "/v1/risk/limit_buckets",
2438
+ signal: options?.signal
2439
+ });
2440
+ }
2441
+ get(id, options) {
2442
+ return this.#http.request({
2443
+ method: "GET",
2444
+ path: `/v1/risk/limit_buckets/${encodeURIComponent(id)}`,
2445
+ signal: options?.signal
2446
+ });
2447
+ }
2448
+ /** Changes the fields given; the next check uses them. */
2449
+ update(id, params, options) {
2450
+ return this.#http.request({
2451
+ method: "PATCH",
2452
+ path: `/v1/risk/limit_buckets/${encodeURIComponent(id)}`,
2453
+ body: params,
2454
+ signal: options?.signal
2455
+ });
2456
+ }
2457
+ delete(id, options) {
2458
+ return this.#http.request({
2459
+ method: "DELETE",
2460
+ path: `/v1/risk/limit_buckets/${encodeURIComponent(id)}`,
2461
+ signal: options?.signal
2462
+ });
2463
+ }
2464
+ };
2465
+ /** `coss.risk.users`: what coss.com remembers about your users. */
2466
+ var RiskUsersResource = class {
2467
+ #http;
2468
+ /** @internal Use `new Coss().risk.users`. */
2469
+ constructor(http) {
2470
+ this.#http = http;
2471
+ }
2472
+ /**
2473
+ * Call when you delete a user: coss.com deletes what identifies them (their mailbox, fraud reports,
2474
+ * and the user id, email, IP and user agent on their assessments). Safe to repeat.
2475
+ */
2476
+ forget(userId, options) {
2477
+ return this.#http.request({
2478
+ method: "DELETE",
2479
+ path: `/v1/risk/users/${encodeURIComponent(userId)}`,
2480
+ signal: options?.signal
2481
+ });
2482
+ }
2483
+ };
2484
+ /**
2485
+ * `coss.risk`: assess sign-ups, logins and other events. Call it from your server and treat an
2486
+ * error as `allow` (fail open), so an outage never locks your users out.
2487
+ *
2488
+ * ```ts
2489
+ * const { data } = await coss.risk.assess({ event: "signup", user: { email }, ip, user_agent });
2490
+ * if (data?.decision === "block") return refuse();
2491
+ * ```
2492
+ */
2493
+ var RiskResource = class {
2494
+ /** Past assessments (`get`, `list`). */
2495
+ assessments;
2496
+ /** Allow and block lists (`add`, `list`, `get`, `remove`). */
2497
+ lists;
2498
+ /** Forgetting deleted users. */
2499
+ users;
2500
+ /** Thresholds, disabled reasons and the captcha provider (`get`, `update`). */
2501
+ settings;
2502
+ /** Rules (`create`, `list`, `get`, `update`, `delete`). */
2503
+ rules;
2504
+ /** Your rate limit buckets (`create`, `list`, `get`, `update`, `delete`). */
2505
+ limitBuckets;
2506
+ #http;
2507
+ /** @internal Use `new Coss().risk`. */
2508
+ constructor(http) {
2509
+ this.#http = http;
2510
+ this.assessments = new RiskAssessmentsResource(http);
2511
+ this.lists = new RiskListsResource(http);
2512
+ this.users = new RiskUsersResource(http);
2513
+ this.settings = new RiskSettingsResource(http);
2514
+ this.rules = new RiskRulesResource(http);
2515
+ this.limitBuckets = new RiskLimitBucketsResource(http);
2516
+ }
2517
+ /**
2518
+ * Assesses an event and returns the decision with its score and reasons. Retries reuse one
2519
+ * `Idempotency-Key`, so an event is never counted twice.
2520
+ */
2521
+ assess(params, options) {
2522
+ return this.#http.request({
2523
+ method: "POST",
2524
+ path: "/v1/risk/assessments",
2525
+ body: params,
2526
+ idempotencyKey: options?.idempotencyKey,
2527
+ signal: options?.signal
2528
+ });
2529
+ }
2530
+ /**
2531
+ * Reports what an assessed user turned out to be: `fraud` and `abuse` teach later assessments to
2532
+ * flag the same mailbox, user, card, device, IP and subnet; `legitimate` takes that back.
2533
+ * Resolves a review, and counts in `calibration()`.
2534
+ */
2535
+ feedback(id, params, options) {
2536
+ return this.#http.request({
2537
+ method: "POST",
2538
+ path: `/v1/risk/assessments/${encodeURIComponent(id)}/feedback`,
2539
+ body: params,
2540
+ idempotencyKey: options?.idempotencyKey,
2541
+ signal: options?.signal
2542
+ });
2543
+ }
2544
+ /**
2545
+ * How often each reason and rule was right over the last 90 days, by your feedback, and the
2546
+ * reason weights that supports. Apply them with `settings.update({ reason_weights })`.
2547
+ */
2548
+ calibration(options) {
2549
+ return this.#http.request({
2550
+ method: "GET",
2551
+ path: "/v1/risk/calibration",
2552
+ signal: options?.signal
2553
+ });
2554
+ }
2555
+ /**
2556
+ * Checks an email address on its own: syntax, disposable domains, deliverability, role
2557
+ * accounts, free providers, the canonical mailbox, typos and domain age. Nothing is stored.
2558
+ */
2559
+ checkEmail(email, options) {
2560
+ return this.#http.request({
2561
+ method: "POST",
2562
+ path: "/v1/risk/email/check",
2563
+ body: { email },
2564
+ signal: options?.signal
2565
+ });
2566
+ }
2567
+ /**
2568
+ * Checks an IP address on its own: Tor, cloud, hosting and VPN networks, ASN and country, and
2569
+ * VPN, proxy and relay exits when coss.com's privacy data is available.
2570
+ */
2571
+ checkIp(ip, options) {
2572
+ return this.#http.request({
2573
+ method: "POST",
2574
+ path: "/v1/risk/ip/check",
2575
+ body: { ip },
2576
+ signal: options?.signal
2577
+ });
2578
+ }
2579
+ /**
2580
+ * Scores text for spam, phishing, malware and fraud. Scores only, never a verdict: you choose
2581
+ * the thresholds. The text is neither stored nor logged.
2582
+ */
2583
+ classify(params, options) {
2584
+ return this.#http.request({
2585
+ method: "POST",
2586
+ path: "/v1/risk/content/classify",
2587
+ body: params,
2588
+ signal: options?.signal
2589
+ });
2590
+ }
2591
+ /**
2592
+ * Takes `cost` tokens (default 1) from `key`'s share of one of your buckets
2593
+ * (`coss.risk.limitBuckets`). Retries reuse one `Idempotency-Key`, so they are charged once.
2594
+ *
2595
+ * ```ts
2596
+ * const { data } = await coss.risk.limit({ bucket: "password_reset", key: email });
2597
+ * if (data && !data.allowed) return tooManyRequests(data.retry_after_ms);
2598
+ * ```
2599
+ */
2600
+ limit(params, options) {
2601
+ return this.#http.request({
2602
+ method: "POST",
2603
+ path: "/v1/risk/limits",
2604
+ body: params,
2605
+ idempotencyKey: options?.idempotencyKey,
2606
+ signal: options?.signal
2607
+ });
2608
+ }
2609
+ };
2610
+ //#endregion
2611
+ //#region src/resources/webhook-deliveries.ts
2612
+ const deliveriesPath = (endpointId) => `/v1/webhook_endpoints/${encodeURIComponent(endpointId)}/deliveries`;
2613
+ /**
2614
+ * `coss.webhookEndpoints.deliveries`: an endpoint's delivery log. Each delivery carries every
2615
+ * HTTP attempt and the payload as sent.
2616
+ */
2617
+ var WebhookDeliveriesResource = class {
2618
+ #http;
2619
+ /** @internal Use `new Coss().webhookEndpoints.deliveries`. */
2620
+ constructor(http) {
2621
+ this.#http = http;
2622
+ }
2623
+ /** Lists the endpoint's deliveries, newest first. Pass the last `id` as `after` while `has_more` is true. */
2624
+ list(endpointId, params = {}, options) {
2625
+ return this.#http.request({
2626
+ method: "GET",
2627
+ path: deliveriesPath(endpointId),
2628
+ query: { ...params },
2629
+ signal: options?.signal
2630
+ });
2631
+ }
2632
+ /** Retrieves one delivery of the endpoint. */
2633
+ get(endpointId, id, options) {
2634
+ return this.#http.request({
2635
+ method: "GET",
2636
+ path: `${deliveriesPath(endpointId)}/${encodeURIComponent(id)}`,
2637
+ signal: options?.signal
2638
+ });
2639
+ }
2640
+ /**
2641
+ * Queues a finished delivery for one more attempt, made within seconds. A delivery that is
2642
+ * still `pending` is returned unchanged (it is already scheduled for `next_attempt_at`).
2643
+ * Never automatically retried: the API does not deduplicate replay requests, even with an
2644
+ * idempotency key.
2645
+ */
2646
+ retry(endpointId, id, options) {
2647
+ return this.#http.request({
2648
+ method: "POST",
2649
+ path: `${deliveriesPath(endpointId)}/${encodeURIComponent(id)}/retry`,
2650
+ retry: false,
2651
+ idempotencyKey: options?.idempotencyKey,
2652
+ signal: options?.signal
2653
+ });
2654
+ }
2655
+ };
2656
+ //#endregion
2657
+ //#region src/resources/webhook-endpoints.ts
2658
+ /**
2659
+ * `coss.webhookEndpoints`: the URLs that receive signed event notifications. Endpoints belong to
2660
+ * the API key's own environment, so a `coss_test_…` key only ever manages sandbox endpoints.
2661
+ */
2662
+ var WebhookEndpointsResource = class {
2663
+ #http;
2664
+ /** Each endpoint's delivery log: list, retrieve and retry deliveries. */
2665
+ deliveries;
2666
+ /** @internal Use `new Coss().webhookEndpoints`. */
2667
+ constructor(http) {
2668
+ this.#http = http;
2669
+ this.deliveries = new WebhookDeliveriesResource(http);
2670
+ }
2671
+ /**
2672
+ * Creates an endpoint. The result carries its `secret` (returned again only by `rollSecret`) —
2673
+ * store it and pass it to `verifyWebhook` for every delivery. Creation is never automatically retried:
2674
+ * the API does not deduplicate it, even with an idempotency key. After a lost response,
2675
+ * list endpoints and remove the endpoint with the unknown secret before creating a replacement.
2676
+ */
2677
+ create(params, options) {
2678
+ return this.#http.request({
2679
+ method: "POST",
2680
+ path: "/v1/webhook_endpoints",
2681
+ retry: false,
2682
+ body: params,
2683
+ idempotencyKey: options?.idempotencyKey,
2684
+ signal: options?.signal
2685
+ });
2686
+ }
2687
+ /** Lists the environment's endpoints, oldest first. Secrets are never included. */
2688
+ list(options) {
2689
+ return this.#http.request({
2690
+ method: "GET",
2691
+ path: "/v1/webhook_endpoints",
2692
+ signal: options?.signal
2693
+ });
2694
+ }
2695
+ /** Retrieves an endpoint of the API key's environment. */
2696
+ get(id, options) {
2697
+ return this.#http.request({
2698
+ method: "GET",
2699
+ path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
2700
+ signal: options?.signal
2701
+ });
2702
+ }
2703
+ /**
2704
+ * Changes the URL, description, subscribed event types or `enabled` flag. The signing secret
2705
+ * stays the same (see `rollSecret`), so receivers keep verifying deliveries.
2706
+ */
2707
+ update(id, params, options) {
2708
+ return this.#http.request({
2709
+ method: "PATCH",
2710
+ path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
2711
+ body: params,
2712
+ signal: options?.signal
2713
+ });
2714
+ }
2715
+ /**
2716
+ * Replaces the endpoint's signing secret; the result carries the new `secret`. Until
2717
+ * `previous_secret_expires_at` (a day by default), deliveries are signed with both secrets, so
2718
+ * `verifyWebhook` accepts them with either: deploy the new one, then let the old one expire.
2719
+ * Never automatically retried (a second roll would stop the original secret at once): after a
2720
+ * lost response, roll again and deploy that secret.
2721
+ */
2722
+ rollSecret(id, params = {}, options) {
2723
+ return this.#http.request({
2724
+ method: "POST",
2725
+ path: `/v1/webhook_endpoints/${encodeURIComponent(id)}/roll_secret`,
2726
+ retry: false,
2727
+ body: params,
2728
+ idempotencyKey: options?.idempotencyKey,
2729
+ signal: options?.signal
2730
+ });
2731
+ }
2732
+ /**
2733
+ * Queues the endpoint's deliveries that failed since `since` for one more attempt each (at most
2734
+ * 1,000 per call; while `has_more` is true, call again with the same `since`), e.g. after fixing a
2735
+ * receiver that was down. The endpoint must be enabled.
2736
+ */
2737
+ recover(id, params, options) {
2738
+ const since = params.since instanceof Date ? params.since.toISOString() : params.since;
2739
+ return this.#http.request({
2740
+ method: "POST",
2741
+ path: `/v1/webhook_endpoints/${encodeURIComponent(id)}/recover`,
2742
+ body: { since },
2743
+ idempotencyKey: options?.idempotencyKey,
2744
+ signal: options?.signal
2745
+ });
2746
+ }
2747
+ /** Deletes the endpoint and its delivery log. To pause it instead, update `enabled` to false. */
2748
+ delete(id, options) {
2749
+ return this.#http.request({
2750
+ method: "DELETE",
2751
+ path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
2752
+ signal: options?.signal
2753
+ });
2754
+ }
2755
+ };
2756
+ //#endregion
2757
+ //#region src/resources/webhook-listeners.ts
2758
+ /**
2759
+ * `coss.webhookListeners`: what `coss listen` uses to forward an environment's events to a local
2760
+ * server. A listener receives events created after it opens, while it is polled; it is closed by
2761
+ * `delete` or after 10 minutes without a poll.
2762
+ */
2763
+ var WebhookListenersResource = class {
2764
+ #http;
2765
+ /** @internal Use `new Coss().webhookListeners`. */
2766
+ constructor(http) {
2767
+ this.#http = http;
2768
+ }
2769
+ /** Opens a listener. Never retried automatically: a lost answer leaves one to expire. */
2770
+ create(params = {}, options) {
2771
+ return this.#http.request({
2772
+ method: "POST",
2773
+ path: "/v1/webhook_listeners",
2774
+ retry: false,
2775
+ body: params,
2776
+ signal: options?.signal
2777
+ });
2778
+ }
2779
+ /**
2780
+ * Acknowledges earlier deliveries and waits (up to `wait_ms`) for more. Never retried
2781
+ * automatically: call it in a loop, and keep unacknowledged acks for the next call after an error.
2782
+ */
2783
+ poll(id, params = {}, options) {
2784
+ return this.#http.request({
2785
+ method: "POST",
2786
+ path: `/v1/webhook_listeners/${encodeURIComponent(id)}/poll`,
2787
+ retry: false,
2788
+ body: params,
2789
+ signal: options?.signal
2790
+ });
2791
+ }
2792
+ /** Closes a listener: it receives nothing more. */
2793
+ delete(id, options) {
2794
+ return this.#http.request({
2795
+ method: "DELETE",
2796
+ path: `/v1/webhook_listeners/${encodeURIComponent(id)}`,
2797
+ signal: options?.signal
2798
+ });
2799
+ }
2800
+ };
2801
+ //#endregion
2802
+ //#region src/client.ts
2803
+ /**
2804
+ * @internal Names the integration (`coss/nodemailer`, …) that created a client, for its
2805
+ * `User-Agent`. A symbol rather than a `CossOptions` field so it stays out of the public options.
2806
+ */
2807
+ const INTEGRATION = Symbol("coss.integration");
2808
+ const DEFAULT_BASE_URL = "https://api.coss.com";
2809
+ const DEFAULT_TIMEOUT_MS = 3e4;
2810
+ const DEFAULT_MAX_RETRIES = 2;
2811
+ const MAX_TIMEOUT_MS = 2147483647;
2812
+ /** Reads an env variable where one exists (Node, Bun, Deno's `process`), never throwing. */
2813
+ function readEnv(name) {
2814
+ try {
2815
+ return (globalThis.process?.env)?.[name]?.trim() || void 0;
2816
+ } catch {
2817
+ return;
2818
+ }
2819
+ }
2820
+ function retries(value) {
2821
+ return value !== void 0 && Number.isFinite(value) && value >= 0 ? Math.floor(value) : DEFAULT_MAX_RETRIES;
2822
+ }
2823
+ function timeoutMs(value) {
2824
+ return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.min(Math.ceil(value), MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
2825
+ }
2826
+ /**
2827
+ * The coss.com client. One API key covers every product.
2828
+ *
2829
+ * ```ts
2830
+ * const coss = new Coss({ apiKey: "coss_test_…" });
2831
+ * const { data, error } = await coss.email.send({ from, to, subject, html });
2832
+ * ```
2833
+ */
2834
+ var Coss = class {
2835
+ email;
2836
+ /** The API key's environment, project and organization (`coss.environment.get()`). */
2837
+ environment;
2838
+ /** The client's own API key (`coss.apiKey.revoke()`). */
2839
+ apiKey;
2840
+ /** Sending domains of the organization (`coss.domains.create`, `.verify`, …). */
2841
+ domains;
2842
+ /** Webhook endpoints of the API key's environment. */
2843
+ webhookEndpoints;
2844
+ /** Temporary listeners `coss listen` pulls the environment's events from. */
2845
+ webhookListeners;
2846
+ /** The organization's sending reputation (`coss.reputation.get()`). */
2847
+ reputation;
2848
+ /** The environment's log: API requests, email events, webhook attempts (`coss.logs.list()`). */
2849
+ logs;
2850
+ /** HTTPS endpoints the environment's log is streamed to (`coss.logDrains.create()`). */
2851
+ logDrains;
2852
+ /** The organization's audit log: changes by members, API keys and coss.com support (`coss.auditEvents.list()`). */
2853
+ auditEvents;
2854
+ /** Hooks, webhooks as a service: `coss.hooks.messages.send()` delivers to your users' endpoints. */
2855
+ hooks;
2856
+ /** Risk assessments of sign-ups, logins and other events (`coss.risk.assess()`). */
2857
+ risk;
2858
+ /** The organization's plan, usage, invoices and credits (`coss.billing.get()`). */
2859
+ billing;
2860
+ /**
2861
+ * Throws only for programmer errors: no API key, or running in a browser without
2862
+ * `dangerouslyAllowBrowser`. Request failures are returned as `{ data: null, error }`.
2863
+ */
2864
+ constructor(options = {}) {
2865
+ if (typeof globalThis.document !== "undefined" && !options.dangerouslyAllowBrowser) throw new Error("Refusing to create a coss.com client in a browser-like environment: coss.com API keys are secret, and any key sent to the browser can be read by anyone using the page. Call coss.com from your server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
2866
+ const apiKey = options.apiKey?.trim() || readEnv("COSS_API_KEY");
2867
+ if (!apiKey) throw new Error("Missing coss.com API key. Pass `apiKey` to `new Coss({ apiKey })` or set the COSS_API_KEY environment variable.");
2868
+ const http = new HttpClient({
2869
+ apiKey,
2870
+ baseUrl: options.baseUrl?.trim() || readEnv("COSS_BASE_URL") || DEFAULT_BASE_URL,
2871
+ fetch: options.fetch ?? ((input, init) => globalThis.fetch(input, init)),
2872
+ timeout: timeoutMs(options.timeout),
2873
+ maxRetries: retries(options.maxRetries),
2874
+ apiVersion: options.apiVersion?.trim() || "2026-10-10",
2875
+ integration: options[INTEGRATION]
2876
+ });
2877
+ this.email = new EmailResource(http);
2878
+ this.environment = new EnvironmentResource(http);
2879
+ this.apiKey = new ApiKeyResource(http);
2880
+ this.domains = new DomainsResource(http);
2881
+ this.webhookEndpoints = new WebhookEndpointsResource(http);
2882
+ this.webhookListeners = new WebhookListenersResource(http);
2883
+ this.reputation = new ReputationResource(http);
2884
+ this.logs = new LogsResource(http);
2885
+ this.logDrains = new LogDrainsResource(http);
2886
+ this.auditEvents = new AuditEventsResource(http);
2887
+ this.hooks = new HooksResource(http);
2888
+ this.risk = new RiskResource(http);
2889
+ this.billing = new BillingResource(http);
2890
+ }
2891
+ };
2892
+ //#endregion
2893
+ export { API_VERSIONS as a, API_VERSION as i, INTEGRATION as n, VERSION as r, Coss as t };