coss 0.0.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +395 -0
- package/dist/index.d.ts +2823 -0
- package/dist/index.js +1917 -0
- package/package.json +34 -23
- package/.npmignore +0 -4
- package/Cakefile +0 -46
- package/bower.json +0 -25
- package/coss.js +0 -144
- package/coss.min.js +0 -1
- package/readme.md +0 -75
package/dist/index.js
ADDED
|
@@ -0,0 +1,1917 @@
|
|
|
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.2.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 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
|
+
#sleep;
|
|
136
|
+
constructor(options) {
|
|
137
|
+
this.#apiKey = options.apiKey;
|
|
138
|
+
this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
139
|
+
this.#fetch = options.fetch;
|
|
140
|
+
this.#timeout = options.timeout;
|
|
141
|
+
this.#maxRetries = options.maxRetries;
|
|
142
|
+
this.#apiVersion = options.apiVersion;
|
|
143
|
+
this.#sleep = options.sleep ?? sleep;
|
|
144
|
+
}
|
|
145
|
+
async request(request) {
|
|
146
|
+
let url = `${this.#baseUrl}${request.path}`;
|
|
147
|
+
const query = new URLSearchParams();
|
|
148
|
+
for (const [name, value] of Object.entries(request.query ?? {})) if (value !== void 0) query.set(name, String(value));
|
|
149
|
+
const search = query.toString();
|
|
150
|
+
if (search) url += `?${search}`;
|
|
151
|
+
const headers = {
|
|
152
|
+
Accept: request.responseType === "text" || request.responseType === "bytes" ? "*/*" : "application/json",
|
|
153
|
+
Authorization: `Bearer ${this.#apiKey}`,
|
|
154
|
+
"User-Agent": `coss-typescript/${VERSION}`,
|
|
155
|
+
"Coss-Version": this.#apiVersion
|
|
156
|
+
};
|
|
157
|
+
let body;
|
|
158
|
+
if (request.body !== void 0) {
|
|
159
|
+
headers["Content-Type"] = "application/json";
|
|
160
|
+
body = JSON.stringify(request.body);
|
|
161
|
+
}
|
|
162
|
+
if (request.method === "POST") headers["Idempotency-Key"] = request.idempotencyKey ?? crypto.randomUUID();
|
|
163
|
+
try {
|
|
164
|
+
new Headers(headers);
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
return {
|
|
167
|
+
data: null,
|
|
168
|
+
error: clientError("invalid_request_error", "invalid_header", `Invalid request header: ${describe(cause)}`)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const init = {
|
|
172
|
+
method: request.method,
|
|
173
|
+
headers,
|
|
174
|
+
body
|
|
175
|
+
};
|
|
176
|
+
for (let retry = 0;; retry++) {
|
|
177
|
+
if (request.signal?.aborted) return {
|
|
178
|
+
data: null,
|
|
179
|
+
error: abortedError()
|
|
180
|
+
};
|
|
181
|
+
const attempt = await this.#attempt(url, init, request.signal, request.responseType);
|
|
182
|
+
if (request.retry === false || !attempt.retry || retry >= this.#maxRetries) return attempt.result;
|
|
183
|
+
const delay = attempt.retryAfterMs ?? backoffDelay(retry);
|
|
184
|
+
if (delay > MAX_RETRY_AFTER_MS) return attempt.result;
|
|
185
|
+
await this.#sleep(delay, request.signal);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async #attempt(url, init, callerSignal, responseType) {
|
|
189
|
+
const timeoutSignal = AbortSignal.timeout(this.#timeout);
|
|
190
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
|
|
191
|
+
let response;
|
|
192
|
+
let text;
|
|
193
|
+
let bytes = null;
|
|
194
|
+
try {
|
|
195
|
+
response = await this.#fetch(url, {
|
|
196
|
+
...init,
|
|
197
|
+
signal
|
|
198
|
+
});
|
|
199
|
+
if (responseType === "bytes") {
|
|
200
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
201
|
+
text = response.ok ? "" : new TextDecoder().decode(bytes);
|
|
202
|
+
} else text = await response.text();
|
|
203
|
+
} catch (cause) {
|
|
204
|
+
if (callerSignal?.aborted) return {
|
|
205
|
+
result: failed(abortedError()),
|
|
206
|
+
retry: false,
|
|
207
|
+
retryAfterMs: null
|
|
208
|
+
};
|
|
209
|
+
const timeoutMessage = `Request timed out after ${this.#timeout}ms.`;
|
|
210
|
+
return {
|
|
211
|
+
result: failed(timeoutSignal.aborted ? clientError("timeout_error", "request_timeout", timeoutMessage) : clientError("connection_error", "network_error", `Could not reach the COSS API at ${this.#baseUrl}: ${describe(cause)}`)),
|
|
212
|
+
retry: true,
|
|
213
|
+
retryAfterMs: null
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const requestId = response.headers.get("x-request-id");
|
|
217
|
+
if (!response.ok) return {
|
|
218
|
+
result: failed(errorFromResponse(response.status, response.statusText, text, requestId)),
|
|
219
|
+
retry: isRetryableStatus(response.status),
|
|
220
|
+
retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
|
|
221
|
+
};
|
|
222
|
+
if (responseType === "text") return {
|
|
223
|
+
result: {
|
|
224
|
+
data: text,
|
|
225
|
+
error: null
|
|
226
|
+
},
|
|
227
|
+
retry: false,
|
|
228
|
+
retryAfterMs: null
|
|
229
|
+
};
|
|
230
|
+
if (responseType === "bytes") return {
|
|
231
|
+
result: {
|
|
232
|
+
data: bytes,
|
|
233
|
+
error: null
|
|
234
|
+
},
|
|
235
|
+
retry: false,
|
|
236
|
+
retryAfterMs: null
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
return {
|
|
240
|
+
result: {
|
|
241
|
+
data: JSON.parse(text),
|
|
242
|
+
error: null
|
|
243
|
+
},
|
|
244
|
+
retry: false,
|
|
245
|
+
retryAfterMs: null
|
|
246
|
+
};
|
|
247
|
+
} catch {
|
|
248
|
+
return {
|
|
249
|
+
result: failed({
|
|
250
|
+
type: "api_error",
|
|
251
|
+
code: "invalid_response",
|
|
252
|
+
message: `The COSS API returned an HTTP ${response.status} response that is not valid JSON.`,
|
|
253
|
+
status: response.status,
|
|
254
|
+
request_id: requestId
|
|
255
|
+
}),
|
|
256
|
+
retry: false,
|
|
257
|
+
retryAfterMs: null
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
function failed(error) {
|
|
263
|
+
return {
|
|
264
|
+
data: null,
|
|
265
|
+
error
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region src/resources/list-query.ts
|
|
270
|
+
/** One query parameter: lists are comma-joined (an empty one is left out), dates ISO 8601. */
|
|
271
|
+
function joined(value) {
|
|
272
|
+
if (value === void 0) return void 0;
|
|
273
|
+
if (value instanceof Date) return value.toISOString();
|
|
274
|
+
if (Array.isArray(value)) return value.length === 0 ? void 0 : value.join(",");
|
|
275
|
+
return String(value);
|
|
276
|
+
}
|
|
277
|
+
/** The query string of a list method whose filters accept arrays and dates. */
|
|
278
|
+
function listQuery(params) {
|
|
279
|
+
const query = {};
|
|
280
|
+
for (const [name, value] of Object.entries(params)) query[name] = joined(value);
|
|
281
|
+
return query;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/resources/audit-events.ts
|
|
285
|
+
/**
|
|
286
|
+
* `coss.auditEvents`: the organization's audit log, newest first: changes made by members, API
|
|
287
|
+
* keys and COSS support. Organization-wide, whichever environment the key belongs to.
|
|
288
|
+
*/
|
|
289
|
+
var AuditEventsResource = class {
|
|
290
|
+
#http;
|
|
291
|
+
/** @internal Use `new Coss().auditEvents`. */
|
|
292
|
+
constructor(http) {
|
|
293
|
+
this.#http = http;
|
|
294
|
+
}
|
|
295
|
+
/** Lists audit events, newest first. Pass the last `id` as `after` while `has_more` is true. */
|
|
296
|
+
list(params = {}, options) {
|
|
297
|
+
return this.#http.request({
|
|
298
|
+
method: "GET",
|
|
299
|
+
path: "/v1/audit_events",
|
|
300
|
+
query: listQuery(params),
|
|
301
|
+
signal: options?.signal
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
/** Retrieves one audit event. */
|
|
305
|
+
get(id, options) {
|
|
306
|
+
return this.#http.request({
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: `/v1/audit_events/${encodeURIComponent(id)}`,
|
|
309
|
+
signal: options?.signal
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/resources/billing.ts
|
|
315
|
+
/** `coss.billing.invoices`: the organization's invoices, newest first. */
|
|
316
|
+
var BillingInvoicesResource = class {
|
|
317
|
+
#http;
|
|
318
|
+
/** @internal Use `new Coss().billing.invoices`. */
|
|
319
|
+
constructor(http) {
|
|
320
|
+
this.#http = http;
|
|
321
|
+
}
|
|
322
|
+
/** Lists invoices (without lines). Pass the last `id` as `after` while `has_more` is true. */
|
|
323
|
+
list(params = {}, options) {
|
|
324
|
+
return this.#http.request({
|
|
325
|
+
method: "GET",
|
|
326
|
+
path: "/v1/billing/invoices",
|
|
327
|
+
query: listQuery(params),
|
|
328
|
+
signal: options?.signal
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
/** Retrieves one invoice with its lines. */
|
|
332
|
+
get(id, options) {
|
|
333
|
+
return this.#http.request({
|
|
334
|
+
method: "GET",
|
|
335
|
+
path: `/v1/billing/invoices/${encodeURIComponent(id)}`,
|
|
336
|
+
signal: options?.signal
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/** `coss.billing.credits`: the organization's credits and what's left of them. */
|
|
341
|
+
var BillingCreditsResource = class {
|
|
342
|
+
#http;
|
|
343
|
+
/** @internal Use `new Coss().billing.credits`. */
|
|
344
|
+
constructor(http) {
|
|
345
|
+
this.#http = http;
|
|
346
|
+
}
|
|
347
|
+
/** Lists credits, newest first (usable ones by default). */
|
|
348
|
+
list(params = {}, options) {
|
|
349
|
+
return this.#http.request({
|
|
350
|
+
method: "GET",
|
|
351
|
+
path: "/v1/billing/credits",
|
|
352
|
+
query: listQuery(params),
|
|
353
|
+
signal: options?.signal
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/**
|
|
358
|
+
* `coss.billing`: the organization's plan, usage, invoices and credits (read only; plan changes
|
|
359
|
+
* are made in the dashboard). Organization-wide, whichever environment the key belongs to.
|
|
360
|
+
*/
|
|
361
|
+
var BillingResource = class {
|
|
362
|
+
#http;
|
|
363
|
+
invoices;
|
|
364
|
+
credits;
|
|
365
|
+
/** @internal Use `new Coss().billing`. */
|
|
366
|
+
constructor(http) {
|
|
367
|
+
this.#http = http;
|
|
368
|
+
this.invoices = new BillingInvoicesResource(http);
|
|
369
|
+
this.credits = new BillingCreditsResource(http);
|
|
370
|
+
}
|
|
371
|
+
/** The plan, the current month's usage and estimate, standing and what the plan includes. */
|
|
372
|
+
get(options) {
|
|
373
|
+
return this.#http.request({
|
|
374
|
+
method: "GET",
|
|
375
|
+
path: "/v1/billing",
|
|
376
|
+
signal: options?.signal
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** Billable usage per meter and bucket (the current month, by day, by default), oldest first. */
|
|
380
|
+
usage(params = {}, options) {
|
|
381
|
+
return this.#http.request({
|
|
382
|
+
method: "GET",
|
|
383
|
+
path: "/v1/billing/usage",
|
|
384
|
+
query: listQuery(params),
|
|
385
|
+
signal: options?.signal
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/resources/receiving-domains.ts
|
|
391
|
+
const receivingPath = (domainId) => `/v1/domains/${encodeURIComponent(domainId)}/receiving`;
|
|
392
|
+
/**
|
|
393
|
+
* `coss.domains.receiving`: a domain's receiving hostname (the domain itself or a subdomain, e.g.
|
|
394
|
+
* `inbound.acme.com`). Email sent to any address at it is received by the API key's environment.
|
|
395
|
+
*/
|
|
396
|
+
var ReceivingDomainsResource = class {
|
|
397
|
+
#http;
|
|
398
|
+
/** @internal Use `new Coss().domains.receiving`. */
|
|
399
|
+
constructor(http) {
|
|
400
|
+
this.#http = http;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Starts receiving at a hostname of the domain, for the API key's environment, and returns the
|
|
404
|
+
* domain with the MX record to publish in `data.receiving.record`.
|
|
405
|
+
*/
|
|
406
|
+
create(domainId, params, options) {
|
|
407
|
+
return this.#http.request({
|
|
408
|
+
method: "POST",
|
|
409
|
+
path: receivingPath(domainId),
|
|
410
|
+
body: params,
|
|
411
|
+
idempotencyKey: options?.idempotencyKey,
|
|
412
|
+
signal: options?.signal
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/** Stops receiving at the domain's hostname. Emails already received are kept. */
|
|
416
|
+
delete(domainId, options) {
|
|
417
|
+
return this.#http.request({
|
|
418
|
+
method: "DELETE",
|
|
419
|
+
path: receivingPath(domainId),
|
|
420
|
+
signal: options?.signal
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/resources/tracking-domains.ts
|
|
426
|
+
const trackingPath = (domainId) => `/v1/domains/${encodeURIComponent(domainId)}/tracking`;
|
|
427
|
+
/**
|
|
428
|
+
* `coss.domains.tracking`: a domain's custom tracking domain (e.g. `links.acme.com`). Open pixels
|
|
429
|
+
* and click redirects of emails sent from the domain use it while it is `active`.
|
|
430
|
+
*/
|
|
431
|
+
var TrackingDomainsResource = class {
|
|
432
|
+
#http;
|
|
433
|
+
/** @internal Use `new Coss().domains.tracking`. */
|
|
434
|
+
constructor(http) {
|
|
435
|
+
this.#http = http;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Sets up a subdomain of the domain as its tracking domain and returns the domain, with the
|
|
439
|
+
* CNAME to publish in `data.tracking.record`. Setup gives up after 72 hours.
|
|
440
|
+
*/
|
|
441
|
+
create(domainId, params, options) {
|
|
442
|
+
return this.#http.request({
|
|
443
|
+
method: "POST",
|
|
444
|
+
path: trackingPath(domainId),
|
|
445
|
+
body: params,
|
|
446
|
+
idempotencyKey: options?.idempotencyKey,
|
|
447
|
+
signal: options?.signal
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Removes the domain's tracking domain. Later emails use the default tracking host; links in
|
|
452
|
+
* emails already sent through it stop working.
|
|
453
|
+
*/
|
|
454
|
+
delete(domainId, options) {
|
|
455
|
+
return this.#http.request({
|
|
456
|
+
method: "DELETE",
|
|
457
|
+
path: trackingPath(domainId),
|
|
458
|
+
signal: options?.signal
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/resources/domains.ts
|
|
464
|
+
/**
|
|
465
|
+
* `coss.domains`: the domains live (`coss_live_…`) keys may send from. Domains belong to the
|
|
466
|
+
* organization, so every API key of the organization sees the same ones.
|
|
467
|
+
*/
|
|
468
|
+
var DomainsResource = class {
|
|
469
|
+
#http;
|
|
470
|
+
/** Each domain's custom tracking domain: add and remove it. */
|
|
471
|
+
tracking;
|
|
472
|
+
/** Each domain's receiving hostname: start and stop receiving email at it. */
|
|
473
|
+
receiving;
|
|
474
|
+
/** @internal Use `new Coss().domains`. */
|
|
475
|
+
constructor(http) {
|
|
476
|
+
this.#http = http;
|
|
477
|
+
this.tracking = new TrackingDomainsResource(http);
|
|
478
|
+
this.receiving = new ReceivingDomainsResource(http);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Adds a domain and returns the DNS records to publish (`data.records`). The records are
|
|
482
|
+
* checked in the background; verification gives up after 72 hours.
|
|
483
|
+
*/
|
|
484
|
+
create(params, options) {
|
|
485
|
+
return this.#http.request({
|
|
486
|
+
method: "POST",
|
|
487
|
+
path: "/v1/domains",
|
|
488
|
+
body: params,
|
|
489
|
+
idempotencyKey: options?.idempotencyKey,
|
|
490
|
+
signal: options?.signal
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/** Lists the organization's domains, ordered by name. */
|
|
494
|
+
list(options) {
|
|
495
|
+
return this.#http.request({
|
|
496
|
+
method: "GET",
|
|
497
|
+
path: "/v1/domains",
|
|
498
|
+
signal: options?.signal
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/** Retrieves a domain with the result of the last DNS check. */
|
|
502
|
+
get(id, options) {
|
|
503
|
+
return this.#http.request({
|
|
504
|
+
method: "GET",
|
|
505
|
+
path: `/v1/domains/${encodeURIComponent(id)}`,
|
|
506
|
+
signal: options?.signal
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Checks the domain's records now instead of waiting for the next background check. A `failed`
|
|
511
|
+
* domain starts verification over with fresh DKIM records, which have to be published again.
|
|
512
|
+
* Its tracking domain, if any, is checked too.
|
|
513
|
+
*/
|
|
514
|
+
verify(id, options) {
|
|
515
|
+
return this.#http.request({
|
|
516
|
+
method: "POST",
|
|
517
|
+
path: `/v1/domains/${encodeURIComponent(id)}/verify`,
|
|
518
|
+
idempotencyKey: options?.idempotencyKey,
|
|
519
|
+
signal: options?.signal
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
/** Removes the domain. Emails already sent are unaffected; live keys can no longer use it. */
|
|
523
|
+
delete(id, options) {
|
|
524
|
+
return this.#http.request({
|
|
525
|
+
method: "DELETE",
|
|
526
|
+
path: `/v1/domains/${encodeURIComponent(id)}`,
|
|
527
|
+
signal: options?.signal
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/resources/received-emails.ts
|
|
533
|
+
const emailPath = (id) => `/v1/received_emails/${encodeURIComponent(id)}`;
|
|
534
|
+
/**
|
|
535
|
+
* `coss.email.received`: emails received by the API key's environment, at its inbound address
|
|
536
|
+
* (`address()`) or at a receiving domain (`coss.domains.receiving`). An `email.received` webhook
|
|
537
|
+
* announces each one.
|
|
538
|
+
*/
|
|
539
|
+
var ReceivedEmailsResource = class {
|
|
540
|
+
#http;
|
|
541
|
+
/** @internal Use `new Coss().email.received`. */
|
|
542
|
+
constructor(http) {
|
|
543
|
+
this.#http = http;
|
|
544
|
+
}
|
|
545
|
+
/** Lists received emails, newest first, without bodies. Pass the last `id` as `after` while `has_more` is true. */
|
|
546
|
+
list(params = {}, options) {
|
|
547
|
+
return this.#http.request({
|
|
548
|
+
method: "GET",
|
|
549
|
+
path: "/v1/received_emails",
|
|
550
|
+
query: listQuery(params),
|
|
551
|
+
signal: options?.signal
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/** Retrieves a received email with its headers and bodies. */
|
|
555
|
+
get(id, options) {
|
|
556
|
+
return this.#http.request({
|
|
557
|
+
method: "GET",
|
|
558
|
+
path: emailPath(id),
|
|
559
|
+
signal: options?.signal
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
/** The whole message as received (`message/rfc822` bytes). */
|
|
563
|
+
getRaw(id, options) {
|
|
564
|
+
return this.#http.request({
|
|
565
|
+
method: "GET",
|
|
566
|
+
path: `${emailPath(id)}/raw`,
|
|
567
|
+
responseType: "bytes",
|
|
568
|
+
signal: options?.signal
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** An attachment's bytes (untrusted: its declared type is the attachment's `content_type`). */
|
|
572
|
+
getAttachment(id, attachmentId, options) {
|
|
573
|
+
return this.#http.request({
|
|
574
|
+
method: "GET",
|
|
575
|
+
path: `${emailPath(id)}/attachments/${encodeURIComponent(attachmentId)}`,
|
|
576
|
+
responseType: "bytes",
|
|
577
|
+
signal: options?.signal
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The environment's inbound domain: email to any address at it (`support@<domain>`) is
|
|
582
|
+
* received, with no DNS setup.
|
|
583
|
+
*/
|
|
584
|
+
address(options) {
|
|
585
|
+
return this.#http.request({
|
|
586
|
+
method: "GET",
|
|
587
|
+
path: "/v1/received_emails/address",
|
|
588
|
+
signal: options?.signal
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/** Deletes a received email with its raw message and attachments right away. */
|
|
592
|
+
delete(id, options) {
|
|
593
|
+
return this.#http.request({
|
|
594
|
+
method: "DELETE",
|
|
595
|
+
path: emailPath(id),
|
|
596
|
+
signal: options?.signal
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/resources/suppressions.ts
|
|
602
|
+
/** The API's per-request maximum for imports. */
|
|
603
|
+
const IMPORT_BATCH_SIZE = 1e3;
|
|
604
|
+
/**
|
|
605
|
+
* `coss.email.suppressions`: the suppression list of the API key's environment. Sends skip
|
|
606
|
+
* suppressed recipients; permanent bounces and complaints are added automatically.
|
|
607
|
+
*/
|
|
608
|
+
var SuppressionResource = class {
|
|
609
|
+
#http;
|
|
610
|
+
/** @internal Use `new Coss().email.suppressions`. */
|
|
611
|
+
constructor(http) {
|
|
612
|
+
this.#http = http;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Suppresses an address for every email (`scope: "*"`, the default) or for one category, e.g.
|
|
616
|
+
* when a recipient unsubscribes. Returns the existing entry if there already is one.
|
|
617
|
+
*/
|
|
618
|
+
add(params, options) {
|
|
619
|
+
return this.#http.request({
|
|
620
|
+
method: "POST",
|
|
621
|
+
path: "/v1/suppressions",
|
|
622
|
+
body: params,
|
|
623
|
+
idempotencyKey: options?.idempotencyKey,
|
|
624
|
+
signal: options?.signal
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Adds many entries at once (reason `import`), 1,000 per request. Invalid entries are skipped
|
|
629
|
+
* and reported in `invalid`; entries already on the list count as `existing`. If a request
|
|
630
|
+
* fails, the earlier batches stay imported: importing everything again is safe.
|
|
631
|
+
*/
|
|
632
|
+
async import(params, options) {
|
|
633
|
+
const total = {
|
|
634
|
+
object: "suppression_import",
|
|
635
|
+
created: 0,
|
|
636
|
+
existing: 0,
|
|
637
|
+
invalid: []
|
|
638
|
+
};
|
|
639
|
+
for (let start = 0; start === 0 || start < params.suppressions.length;) {
|
|
640
|
+
const batch = params.suppressions.slice(start, start + IMPORT_BATCH_SIZE);
|
|
641
|
+
const result = await this.#http.request({
|
|
642
|
+
method: "POST",
|
|
643
|
+
path: "/v1/suppressions/import",
|
|
644
|
+
body: { suppressions: batch },
|
|
645
|
+
signal: options?.signal
|
|
646
|
+
});
|
|
647
|
+
if (result.error) return result;
|
|
648
|
+
total.created += result.data.created;
|
|
649
|
+
total.existing += result.data.existing;
|
|
650
|
+
for (const entry of result.data.invalid) total.invalid.push({
|
|
651
|
+
...entry,
|
|
652
|
+
index: entry.index + start
|
|
653
|
+
});
|
|
654
|
+
start += IMPORT_BATCH_SIZE;
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
data: total,
|
|
658
|
+
error: null
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The whole list as CSV (`address,scope,reason,created_at,email_id,id`, oldest first), which
|
|
663
|
+
* `import` (or the dashboard) accepts again.
|
|
664
|
+
*/
|
|
665
|
+
export(params = {}, options) {
|
|
666
|
+
return this.#http.request({
|
|
667
|
+
method: "GET",
|
|
668
|
+
path: "/v1/suppressions/export",
|
|
669
|
+
query: { ...params },
|
|
670
|
+
responseType: "text",
|
|
671
|
+
signal: options?.signal
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/** Lists entries, newest first. Pass the last `id` as `after` while `has_more` is true. */
|
|
675
|
+
list(params = {}, options) {
|
|
676
|
+
return this.#http.request({
|
|
677
|
+
method: "GET",
|
|
678
|
+
path: "/v1/suppressions",
|
|
679
|
+
query: { ...params },
|
|
680
|
+
signal: options?.signal
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/** Whether a send to `address` (with `category`, if given) would skip it. */
|
|
684
|
+
check(params, options) {
|
|
685
|
+
return this.#http.request({
|
|
686
|
+
method: "GET",
|
|
687
|
+
path: "/v1/suppressions/check",
|
|
688
|
+
query: { ...params },
|
|
689
|
+
signal: options?.signal
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/** Retrieves one entry. */
|
|
693
|
+
get(id, options) {
|
|
694
|
+
return this.#http.request({
|
|
695
|
+
method: "GET",
|
|
696
|
+
path: `/v1/suppressions/${encodeURIComponent(id)}`,
|
|
697
|
+
signal: options?.signal
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** Removes an entry, so later sends reach the address again (e.g. when a recipient resubscribes). */
|
|
701
|
+
remove(id, options) {
|
|
702
|
+
return this.#http.request({
|
|
703
|
+
method: "DELETE",
|
|
704
|
+
path: `/v1/suppressions/${encodeURIComponent(id)}`,
|
|
705
|
+
signal: options?.signal
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/resources/tracking.ts
|
|
711
|
+
const categoryPath = (category) => `/v1/tracking_settings/categories/${encodeURIComponent(category)}`;
|
|
712
|
+
/**
|
|
713
|
+
* `coss.email.tracking`: which emails of the API key's environment have their opens and clicks
|
|
714
|
+
* tracked. Emails keep the settings that applied when they were sent; a send's own `tracking`
|
|
715
|
+
* option wins over them.
|
|
716
|
+
*/
|
|
717
|
+
var TrackingResource = class {
|
|
718
|
+
#http;
|
|
719
|
+
/** @internal Use `new Coss().email.tracking`. */
|
|
720
|
+
constructor(http) {
|
|
721
|
+
this.#http = http;
|
|
722
|
+
}
|
|
723
|
+
/** The environment default and the category rules. */
|
|
724
|
+
get(options) {
|
|
725
|
+
return this.#http.request({
|
|
726
|
+
method: "GET",
|
|
727
|
+
path: "/v1/tracking_settings",
|
|
728
|
+
signal: options?.signal
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Sets the environment default, which applies to emails without a rule for their category.
|
|
733
|
+
* Fields left out keep their value; a new default starts with both off.
|
|
734
|
+
*/
|
|
735
|
+
updateDefault(params, options) {
|
|
736
|
+
return this.#http.request({
|
|
737
|
+
method: "PATCH",
|
|
738
|
+
path: "/v1/tracking_settings/default",
|
|
739
|
+
body: params,
|
|
740
|
+
signal: options?.signal
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
/** Removes the environment default: emails without a category rule are then not tracked. */
|
|
744
|
+
removeDefault(options) {
|
|
745
|
+
return this.#http.request({
|
|
746
|
+
method: "DELETE",
|
|
747
|
+
path: "/v1/tracking_settings/default",
|
|
748
|
+
signal: options?.signal
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Sets the rule for emails sent with `category`, which wins over the default. Fields left out
|
|
753
|
+
* keep their value; a new rule starts with both off.
|
|
754
|
+
*/
|
|
755
|
+
updateCategory(category, params, options) {
|
|
756
|
+
return this.#http.request({
|
|
757
|
+
method: "PATCH",
|
|
758
|
+
path: categoryPath(category),
|
|
759
|
+
body: params,
|
|
760
|
+
signal: options?.signal
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
/** Removes a category's rule: its emails follow the environment default again. */
|
|
764
|
+
removeCategory(category, options) {
|
|
765
|
+
return this.#http.request({
|
|
766
|
+
method: "DELETE",
|
|
767
|
+
path: categoryPath(category),
|
|
768
|
+
signal: options?.signal
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
//#endregion
|
|
773
|
+
//#region src/resources/email.ts
|
|
774
|
+
/** `coss.email`: transactional email. */
|
|
775
|
+
var EmailResource = class {
|
|
776
|
+
#http;
|
|
777
|
+
/** The environment's suppression list. */
|
|
778
|
+
suppressions;
|
|
779
|
+
/** The environment's open and click tracking settings. */
|
|
780
|
+
tracking;
|
|
781
|
+
/** Emails the environment received. */
|
|
782
|
+
received;
|
|
783
|
+
/** @internal Use `new Coss().email`. */
|
|
784
|
+
constructor(http) {
|
|
785
|
+
this.#http = http;
|
|
786
|
+
this.suppressions = new SuppressionResource(http);
|
|
787
|
+
this.tracking = new TrackingResource(http);
|
|
788
|
+
this.received = new ReceivedEmailsResource(http);
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Sends an email. It is returned `queued` and handed to the delivery provider moments later:
|
|
792
|
+
* `get` it or listen for the `email.sent` / `email.failed` webhooks for the outcome. Safe to
|
|
793
|
+
* retry: the request always carries an `Idempotency-Key` (yours, or a generated one reused
|
|
794
|
+
* across automatic retries), so the API never sends it twice.
|
|
795
|
+
*/
|
|
796
|
+
send(params, options) {
|
|
797
|
+
return this.#http.request({
|
|
798
|
+
method: "POST",
|
|
799
|
+
path: "/v1/emails",
|
|
800
|
+
body: params,
|
|
801
|
+
idempotencyKey: options?.idempotencyKey,
|
|
802
|
+
signal: options?.signal
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Sends up to 100 emails in one request, each as `send` would. The batch is all or nothing: if
|
|
807
|
+
* any email is invalid (the error's `issues` name it, e.g. `emails.3.to`), none is sent. Its
|
|
808
|
+
* recipients count against the send quota together. Safe to retry: one `Idempotency-Key` covers
|
|
809
|
+
* the whole batch. Larger lists need several calls, each with its own key.
|
|
810
|
+
*/
|
|
811
|
+
sendBatch(emails, options) {
|
|
812
|
+
return this.#http.request({
|
|
813
|
+
method: "POST",
|
|
814
|
+
path: "/v1/emails/batch",
|
|
815
|
+
body: { emails },
|
|
816
|
+
idempotencyKey: options?.idempotencyKey,
|
|
817
|
+
signal: options?.signal
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
/** Retrieves an email sent from the same environment as the API key. */
|
|
821
|
+
get(id, options) {
|
|
822
|
+
return this.#http.request({
|
|
823
|
+
method: "GET",
|
|
824
|
+
path: `/v1/emails/${encodeURIComponent(id)}`,
|
|
825
|
+
signal: options?.signal
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Cancels a scheduled email. Canceling an already canceled email returns it; any other email
|
|
830
|
+
* fails with `email_not_cancelable`.
|
|
831
|
+
*/
|
|
832
|
+
cancel(id, options) {
|
|
833
|
+
return this.#http.request({
|
|
834
|
+
method: "POST",
|
|
835
|
+
path: `/v1/emails/${encodeURIComponent(id)}/cancel`,
|
|
836
|
+
signal: options?.signal
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Moves a scheduled email to a new time and records an `email.rescheduled` event. The time it
|
|
841
|
+
* already has returns it unchanged; an email that is no longer scheduled fails with
|
|
842
|
+
* `email_not_reschedulable`.
|
|
843
|
+
*/
|
|
844
|
+
reschedule(id, params, options) {
|
|
845
|
+
return this.#http.request({
|
|
846
|
+
method: "POST",
|
|
847
|
+
path: `/v1/emails/${encodeURIComponent(id)}/reschedule`,
|
|
848
|
+
body: params,
|
|
849
|
+
signal: options?.signal
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
/** Lists an email's delivery events, oldest first. */
|
|
853
|
+
listEvents(id, options) {
|
|
854
|
+
return this.#http.request({
|
|
855
|
+
method: "GET",
|
|
856
|
+
path: `/v1/emails/${encodeURIComponent(id)}/events`,
|
|
857
|
+
signal: options?.signal
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
/** Lists the links tracked for clicks in an email, in document order, with click counts. */
|
|
861
|
+
listLinks(id, options) {
|
|
862
|
+
return this.#http.request({
|
|
863
|
+
method: "GET",
|
|
864
|
+
path: `/v1/emails/${encodeURIComponent(id)}/links`,
|
|
865
|
+
signal: options?.signal
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/resources/hooks.ts
|
|
871
|
+
const segment = encodeURIComponent;
|
|
872
|
+
const applicationPath = (application) => `/v1/hooks/applications/${segment(application)}`;
|
|
873
|
+
const endpointPath = (application, id) => `${applicationPath(application)}/endpoints/${segment(id)}`;
|
|
874
|
+
/** `coss.hooks.applications`: your customers, each with their own endpoints and portal. */
|
|
875
|
+
var HookApplicationsResource = class {
|
|
876
|
+
#http;
|
|
877
|
+
/** @internal Use `new Coss().hooks.applications`. */
|
|
878
|
+
constructor(http) {
|
|
879
|
+
this.#http = http;
|
|
880
|
+
}
|
|
881
|
+
/** Creates an application. Never automatically retried (the API does not deduplicate it). */
|
|
882
|
+
create(params, options) {
|
|
883
|
+
return this.#http.request({
|
|
884
|
+
method: "POST",
|
|
885
|
+
path: "/v1/hooks/applications",
|
|
886
|
+
retry: false,
|
|
887
|
+
body: params,
|
|
888
|
+
idempotencyKey: options?.idempotencyKey,
|
|
889
|
+
signal: options?.signal
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
/** Lists the environment's applications, newest first. */
|
|
893
|
+
list(params = {}, options) {
|
|
894
|
+
return this.#http.request({
|
|
895
|
+
method: "GET",
|
|
896
|
+
path: "/v1/hooks/applications",
|
|
897
|
+
query: listQuery(params),
|
|
898
|
+
signal: options?.signal
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/** Retrieves an application by its id or uid. */
|
|
902
|
+
get(application, options) {
|
|
903
|
+
return this.#http.request({
|
|
904
|
+
method: "GET",
|
|
905
|
+
path: applicationPath(application),
|
|
906
|
+
signal: options?.signal
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
update(application, params, options) {
|
|
910
|
+
return this.#http.request({
|
|
911
|
+
method: "PATCH",
|
|
912
|
+
path: applicationPath(application),
|
|
913
|
+
body: params,
|
|
914
|
+
signal: options?.signal
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
/** Deletes the application with its endpoints and portal sessions. */
|
|
918
|
+
delete(application, options) {
|
|
919
|
+
return this.#http.request({
|
|
920
|
+
method: "DELETE",
|
|
921
|
+
path: applicationPath(application),
|
|
922
|
+
signal: options?.signal
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
/** `coss.hooks.eventTypes`: the catalog of messages you send, which your users subscribe from. */
|
|
927
|
+
var HookEventTypesResource = class {
|
|
928
|
+
#http;
|
|
929
|
+
/** @internal Use `new Coss().hooks.eventTypes`. */
|
|
930
|
+
constructor(http) {
|
|
931
|
+
this.#http = http;
|
|
932
|
+
}
|
|
933
|
+
/** Creates an event type. Never automatically retried (a retry would find it existing). */
|
|
934
|
+
create(params, options) {
|
|
935
|
+
return this.#http.request({
|
|
936
|
+
method: "POST",
|
|
937
|
+
path: "/v1/hooks/event_types",
|
|
938
|
+
retry: false,
|
|
939
|
+
body: params,
|
|
940
|
+
idempotencyKey: options?.idempotencyKey,
|
|
941
|
+
signal: options?.signal
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
/** Lists event types by name (archived ones only with `include_archived`). */
|
|
945
|
+
list(params = {}, options) {
|
|
946
|
+
return this.#http.request({
|
|
947
|
+
method: "GET",
|
|
948
|
+
path: "/v1/hooks/event_types",
|
|
949
|
+
query: listQuery(params),
|
|
950
|
+
signal: options?.signal
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
get(name, options) {
|
|
954
|
+
return this.#http.request({
|
|
955
|
+
method: "GET",
|
|
956
|
+
path: `/v1/hooks/event_types/${segment(name)}`,
|
|
957
|
+
signal: options?.signal
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
/** Changes its description, schema or examples, or archives or restores it. */
|
|
961
|
+
update(name, params, options) {
|
|
962
|
+
return this.#http.request({
|
|
963
|
+
method: "PATCH",
|
|
964
|
+
path: `/v1/hooks/event_types/${segment(name)}`,
|
|
965
|
+
body: params,
|
|
966
|
+
signal: options?.signal
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
/** Archives it: hidden from the portal and new subscriptions. */
|
|
970
|
+
archive(name, options) {
|
|
971
|
+
return this.#http.request({
|
|
972
|
+
method: "DELETE",
|
|
973
|
+
path: `/v1/hooks/event_types/${segment(name)}`,
|
|
974
|
+
signal: options?.signal
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
/** `coss.hooks.endpoints`: an application's endpoints (usually added by its users in the portal). */
|
|
979
|
+
var HookEndpointsResource = class {
|
|
980
|
+
#http;
|
|
981
|
+
/** @internal Use `new Coss().hooks.endpoints`. */
|
|
982
|
+
constructor(http) {
|
|
983
|
+
this.#http = http;
|
|
984
|
+
}
|
|
985
|
+
/** Creates an endpoint; the result carries its secret. Never automatically retried. */
|
|
986
|
+
create(application, params, options) {
|
|
987
|
+
return this.#http.request({
|
|
988
|
+
method: "POST",
|
|
989
|
+
path: `${applicationPath(application)}/endpoints`,
|
|
990
|
+
retry: false,
|
|
991
|
+
body: params,
|
|
992
|
+
idempotencyKey: options?.idempotencyKey,
|
|
993
|
+
signal: options?.signal
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
list(application, options) {
|
|
997
|
+
return this.#http.request({
|
|
998
|
+
method: "GET",
|
|
999
|
+
path: `${applicationPath(application)}/endpoints`,
|
|
1000
|
+
signal: options?.signal
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
get(application, id, options) {
|
|
1004
|
+
return this.#http.request({
|
|
1005
|
+
method: "GET",
|
|
1006
|
+
path: endpointPath(application, id),
|
|
1007
|
+
signal: options?.signal
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
update(application, id, params, options) {
|
|
1011
|
+
return this.#http.request({
|
|
1012
|
+
method: "PATCH",
|
|
1013
|
+
path: endpointPath(application, id),
|
|
1014
|
+
body: params,
|
|
1015
|
+
signal: options?.signal
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
delete(application, id, options) {
|
|
1019
|
+
return this.#http.request({
|
|
1020
|
+
method: "DELETE",
|
|
1021
|
+
path: endpointPath(application, id),
|
|
1022
|
+
signal: options?.signal
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
/** The endpoint's current signing secret. */
|
|
1026
|
+
getSecret(application, id, options) {
|
|
1027
|
+
return this.#http.request({
|
|
1028
|
+
method: "GET",
|
|
1029
|
+
path: `${endpointPath(application, id)}/secret`,
|
|
1030
|
+
signal: options?.signal
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
/** Replaces the signing secret. Never automatically retried (a second roll would stop the first). */
|
|
1034
|
+
rollSecret(application, id, params = {}, options) {
|
|
1035
|
+
return this.#http.request({
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
path: `${endpointPath(application, id)}/roll_secret`,
|
|
1038
|
+
retry: false,
|
|
1039
|
+
body: params,
|
|
1040
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1041
|
+
signal: options?.signal
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
/** Requeues the endpoint's failed deliveries of messages sent since `since` (in the background). */
|
|
1045
|
+
recover(application, id, params, options) {
|
|
1046
|
+
const since = params.since instanceof Date ? params.since.toISOString() : params.since;
|
|
1047
|
+
return this.#http.request({
|
|
1048
|
+
method: "POST",
|
|
1049
|
+
path: `${endpointPath(application, id)}/recover`,
|
|
1050
|
+
body: { since },
|
|
1051
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1052
|
+
signal: options?.signal
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
getRecovery(application, id, recoveryId, options) {
|
|
1056
|
+
return this.#http.request({
|
|
1057
|
+
method: "GET",
|
|
1058
|
+
path: `${endpointPath(application, id)}/recoveries/${segment(recoveryId)}`,
|
|
1059
|
+
signal: options?.signal
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
/** Sends a message to this endpoint only. Never automatically retried. */
|
|
1063
|
+
sendTest(application, id, params, options) {
|
|
1064
|
+
return this.#http.request({
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
path: `${endpointPath(application, id)}/test`,
|
|
1067
|
+
retry: false,
|
|
1068
|
+
body: params,
|
|
1069
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1070
|
+
signal: options?.signal
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
/** The endpoint's deliveries, newest first. */
|
|
1074
|
+
listDeliveries(application, id, params = {}, options) {
|
|
1075
|
+
return this.#http.request({
|
|
1076
|
+
method: "GET",
|
|
1077
|
+
path: `${endpointPath(application, id)}/deliveries`,
|
|
1078
|
+
query: listQuery(params),
|
|
1079
|
+
signal: options?.signal
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
/** `coss.hooks.messages`: send a message once; COSS delivers it to every subscribed endpoint. */
|
|
1084
|
+
var HookMessagesResource = class {
|
|
1085
|
+
#http;
|
|
1086
|
+
/** @internal Use `new Coss().hooks.messages`. */
|
|
1087
|
+
constructor(http) {
|
|
1088
|
+
this.#http = http;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Sends a message to an application. Safe to retry: every attempt carries the same
|
|
1092
|
+
* `Idempotency-Key`, so a retried send is delivered once.
|
|
1093
|
+
*/
|
|
1094
|
+
send(params, options) {
|
|
1095
|
+
const { application, ...body } = params;
|
|
1096
|
+
return this.#http.request({
|
|
1097
|
+
method: "POST",
|
|
1098
|
+
path: `${applicationPath(application)}/messages`,
|
|
1099
|
+
body,
|
|
1100
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1101
|
+
signal: options?.signal
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
/** The application's messages, newest first. */
|
|
1105
|
+
list(application, params = {}, options) {
|
|
1106
|
+
return this.#http.request({
|
|
1107
|
+
method: "GET",
|
|
1108
|
+
path: `${applicationPath(application)}/messages`,
|
|
1109
|
+
query: listQuery(params),
|
|
1110
|
+
signal: options?.signal
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
get(application, id, options) {
|
|
1114
|
+
return this.#http.request({
|
|
1115
|
+
method: "GET",
|
|
1116
|
+
path: `${applicationPath(application)}/messages/${segment(id)}`,
|
|
1117
|
+
signal: options?.signal
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/** One delivery per endpoint the message went to. */
|
|
1121
|
+
listDeliveries(application, id, options) {
|
|
1122
|
+
return this.#http.request({
|
|
1123
|
+
method: "GET",
|
|
1124
|
+
path: `${applicationPath(application)}/messages/${segment(id)}/deliveries`,
|
|
1125
|
+
signal: options?.signal
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
/** `coss.hooks.deliveries`: one message's delivery to one endpoint. */
|
|
1130
|
+
var HookDeliveriesResource = class {
|
|
1131
|
+
#http;
|
|
1132
|
+
/** @internal Use `new Coss().hooks.deliveries`. */
|
|
1133
|
+
constructor(http) {
|
|
1134
|
+
this.#http = http;
|
|
1135
|
+
}
|
|
1136
|
+
get(application, id, options) {
|
|
1137
|
+
return this.#http.request({
|
|
1138
|
+
method: "GET",
|
|
1139
|
+
path: `${applicationPath(application)}/deliveries/${segment(id)}`,
|
|
1140
|
+
signal: options?.signal
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
/** Every HTTP request of the delivery, oldest first. */
|
|
1144
|
+
listAttempts(application, id, options) {
|
|
1145
|
+
return this.#http.request({
|
|
1146
|
+
method: "GET",
|
|
1147
|
+
path: `${applicationPath(application)}/deliveries/${segment(id)}/attempts`,
|
|
1148
|
+
signal: options?.signal
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
/** Queues a finished delivery for one more attempt. Never automatically retried. */
|
|
1152
|
+
retry(application, id, options) {
|
|
1153
|
+
return this.#http.request({
|
|
1154
|
+
method: "POST",
|
|
1155
|
+
path: `${applicationPath(application)}/deliveries/${segment(id)}/retry`,
|
|
1156
|
+
retry: false,
|
|
1157
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1158
|
+
signal: options?.signal
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
/** `coss.hooks.portal`: a ready-made page for an application's user to manage their endpoints. */
|
|
1163
|
+
var HookPortalResource = class {
|
|
1164
|
+
#http;
|
|
1165
|
+
/** @internal Use `new Coss().hooks.portal`. */
|
|
1166
|
+
constructor(http) {
|
|
1167
|
+
this.#http = http;
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Signs the application's user in to the portal: open or embed the returned `url`. Call it from
|
|
1171
|
+
* your backend each time the user opens the portal.
|
|
1172
|
+
*/
|
|
1173
|
+
createSession(params, options) {
|
|
1174
|
+
const { application, ...body } = params;
|
|
1175
|
+
return this.#http.request({
|
|
1176
|
+
method: "POST",
|
|
1177
|
+
path: `${applicationPath(application)}/portal_sessions`,
|
|
1178
|
+
body,
|
|
1179
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1180
|
+
signal: options?.signal
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
/**
|
|
1185
|
+
* `coss.hooks`: webhooks as a service. Send a message to one of your applications once
|
|
1186
|
+
* (`coss.hooks.messages.send`); COSS delivers it to every endpoint its users registered, signed
|
|
1187
|
+
* (Standard Webhooks), with retries, and gives them a portal to manage it all.
|
|
1188
|
+
*/
|
|
1189
|
+
var HooksResource = class {
|
|
1190
|
+
applications;
|
|
1191
|
+
eventTypes;
|
|
1192
|
+
endpoints;
|
|
1193
|
+
messages;
|
|
1194
|
+
deliveries;
|
|
1195
|
+
portal;
|
|
1196
|
+
/** @internal Use `new Coss().hooks`. */
|
|
1197
|
+
constructor(http) {
|
|
1198
|
+
this.applications = new HookApplicationsResource(http);
|
|
1199
|
+
this.eventTypes = new HookEventTypesResource(http);
|
|
1200
|
+
this.endpoints = new HookEndpointsResource(http);
|
|
1201
|
+
this.messages = new HookMessagesResource(http);
|
|
1202
|
+
this.deliveries = new HookDeliveriesResource(http);
|
|
1203
|
+
this.portal = new HookPortalResource(http);
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
//#endregion
|
|
1207
|
+
//#region src/resources/log-drains.ts
|
|
1208
|
+
const drainPath = (id) => `/v1/log_drains/${encodeURIComponent(id)}`;
|
|
1209
|
+
/** `coss.logDrains.batches`: a drain's delivery log, each batch with every HTTP attempt. */
|
|
1210
|
+
var LogDrainBatchesResource = class {
|
|
1211
|
+
#http;
|
|
1212
|
+
/** @internal Use `new Coss().logDrains.batches`. */
|
|
1213
|
+
constructor(http) {
|
|
1214
|
+
this.#http = http;
|
|
1215
|
+
}
|
|
1216
|
+
/** Lists the drain's batches, newest first. Pass the last `id` as `after` while `has_more` is true. */
|
|
1217
|
+
list(drainId, params = {}, options) {
|
|
1218
|
+
return this.#http.request({
|
|
1219
|
+
method: "GET",
|
|
1220
|
+
path: `${drainPath(drainId)}/batches`,
|
|
1221
|
+
query: { ...params },
|
|
1222
|
+
signal: options?.signal
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
/** Retrieves one batch of the drain. */
|
|
1226
|
+
get(drainId, id, options) {
|
|
1227
|
+
return this.#http.request({
|
|
1228
|
+
method: "GET",
|
|
1229
|
+
path: `${drainPath(drainId)}/batches/${encodeURIComponent(id)}`,
|
|
1230
|
+
signal: options?.signal
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
/**
|
|
1235
|
+
* `coss.logDrains`: HTTPS endpoints the environment's log is streamed to (API requests, email
|
|
1236
|
+
* events, webhook attempts and audit events), in signed NDJSON batches. Drains belong to the API
|
|
1237
|
+
* key's own environment, so a `coss_test_…` key only ever manages sandbox drains.
|
|
1238
|
+
*/
|
|
1239
|
+
var LogDrainsResource = class {
|
|
1240
|
+
#http;
|
|
1241
|
+
/** Each drain's delivery log. */
|
|
1242
|
+
batches;
|
|
1243
|
+
/** @internal Use `new Coss().logDrains`. */
|
|
1244
|
+
constructor(http) {
|
|
1245
|
+
this.#http = http;
|
|
1246
|
+
this.batches = new LogDrainBatchesResource(http);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Creates a drain. The result carries its `secret` (returned only here) — store it and pass it
|
|
1250
|
+
* to `verifyLogDrainBatch` for every request. Creation is never automatically retried: the API
|
|
1251
|
+
* does not deduplicate it. After a lost response, list drains and remove the one with the
|
|
1252
|
+
* unknown secret before creating a replacement.
|
|
1253
|
+
*/
|
|
1254
|
+
create(params, options) {
|
|
1255
|
+
return this.#http.request({
|
|
1256
|
+
method: "POST",
|
|
1257
|
+
path: "/v1/log_drains",
|
|
1258
|
+
retry: false,
|
|
1259
|
+
body: params,
|
|
1260
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1261
|
+
signal: options?.signal
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
/** Lists the environment's drains, oldest first. Secrets are never included. */
|
|
1265
|
+
list(options) {
|
|
1266
|
+
return this.#http.request({
|
|
1267
|
+
method: "GET",
|
|
1268
|
+
path: "/v1/log_drains",
|
|
1269
|
+
signal: options?.signal
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
/** Retrieves a drain of the API key's environment. */
|
|
1273
|
+
get(id, options) {
|
|
1274
|
+
return this.#http.request({
|
|
1275
|
+
method: "GET",
|
|
1276
|
+
path: drainPath(id),
|
|
1277
|
+
signal: options?.signal
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Changes the URL, description, sources or `enabled` flag. The signing secret stays the same.
|
|
1282
|
+
* Disabling drops the lines still waiting for the drain; enabling it again (also after COSS
|
|
1283
|
+
* disabled it for failing) sends lines logged from then on.
|
|
1284
|
+
*/
|
|
1285
|
+
update(id, params, options) {
|
|
1286
|
+
return this.#http.request({
|
|
1287
|
+
method: "PATCH",
|
|
1288
|
+
path: drainPath(id),
|
|
1289
|
+
body: params,
|
|
1290
|
+
signal: options?.signal
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
/** Deletes the drain and its batch log. To pause it instead, update `enabled` to false. */
|
|
1294
|
+
delete(id, options) {
|
|
1295
|
+
return this.#http.request({
|
|
1296
|
+
method: "DELETE",
|
|
1297
|
+
path: drainPath(id),
|
|
1298
|
+
signal: options?.signal
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/resources/logs.ts
|
|
1304
|
+
/**
|
|
1305
|
+
* `coss.logs`: the API key's environment log, newest first: API requests made with its keys,
|
|
1306
|
+
* email events and webhook delivery attempts.
|
|
1307
|
+
*/
|
|
1308
|
+
var LogsResource = class {
|
|
1309
|
+
#http;
|
|
1310
|
+
/** @internal Use `new Coss().logs`. */
|
|
1311
|
+
constructor(http) {
|
|
1312
|
+
this.#http = http;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Lists log lines, newest first (the last hour unless `from`/`to` say otherwise). Pass the last
|
|
1316
|
+
* `id` as `after` while `has_more` is true.
|
|
1317
|
+
*/
|
|
1318
|
+
list(params = {}, options) {
|
|
1319
|
+
return this.#http.request({
|
|
1320
|
+
method: "GET",
|
|
1321
|
+
path: "/v1/logs",
|
|
1322
|
+
query: listQuery(params),
|
|
1323
|
+
signal: options?.signal
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
/** Retrieves one log line. */
|
|
1327
|
+
get(id, options) {
|
|
1328
|
+
return this.#http.request({
|
|
1329
|
+
method: "GET",
|
|
1330
|
+
path: `/v1/logs/${encodeURIComponent(id)}`,
|
|
1331
|
+
signal: options?.signal
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/resources/reputation.ts
|
|
1337
|
+
/**
|
|
1338
|
+
* `coss.reputation`: the organization's standing with the COSS reputation guard, which suspends
|
|
1339
|
+
* live sending automatically when bounce or complaint rates cross its thresholds.
|
|
1340
|
+
*/
|
|
1341
|
+
var ReputationResource = class {
|
|
1342
|
+
#http;
|
|
1343
|
+
/** @internal Use `new Coss().reputation`. */
|
|
1344
|
+
constructor(http) {
|
|
1345
|
+
this.#http = http;
|
|
1346
|
+
}
|
|
1347
|
+
/** The current status and the latest rates measured over live environments. */
|
|
1348
|
+
get(options) {
|
|
1349
|
+
return this.#http.request({
|
|
1350
|
+
method: "GET",
|
|
1351
|
+
path: "/v1/reputation",
|
|
1352
|
+
signal: options?.signal
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/resources/risk.ts
|
|
1358
|
+
const LIST_ENTRY_TYPES = [
|
|
1359
|
+
"email",
|
|
1360
|
+
"domain",
|
|
1361
|
+
"ip",
|
|
1362
|
+
"asn",
|
|
1363
|
+
"country",
|
|
1364
|
+
"user"
|
|
1365
|
+
];
|
|
1366
|
+
/** `coss.risk.assessments`: reading past assessments (`assess` lives on `coss.risk`). */
|
|
1367
|
+
var RiskAssessmentsResource = class {
|
|
1368
|
+
#http;
|
|
1369
|
+
/** @internal Use `new Coss().risk.assessments`. */
|
|
1370
|
+
constructor(http) {
|
|
1371
|
+
this.#http = http;
|
|
1372
|
+
}
|
|
1373
|
+
/** Retrieves one assessment. */
|
|
1374
|
+
get(id, options) {
|
|
1375
|
+
return this.#http.request({
|
|
1376
|
+
method: "GET",
|
|
1377
|
+
path: `/v1/risk/assessments/${encodeURIComponent(id)}`,
|
|
1378
|
+
signal: options?.signal
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Lists assessments, newest first (`review_status: "open"` is the review queue). Pass the last
|
|
1383
|
+
* `id` as `after` while `has_more` is true.
|
|
1384
|
+
*/
|
|
1385
|
+
list(params = {}, options) {
|
|
1386
|
+
return this.#http.request({
|
|
1387
|
+
method: "GET",
|
|
1388
|
+
path: "/v1/risk/assessments",
|
|
1389
|
+
query: { ...params },
|
|
1390
|
+
signal: options?.signal
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
/** `coss.risk.lists`: the environment's allow and block lists. */
|
|
1395
|
+
var RiskListsResource = class {
|
|
1396
|
+
#http;
|
|
1397
|
+
/** @internal Use `new Coss().risk.lists`. */
|
|
1398
|
+
constructor(http) {
|
|
1399
|
+
this.#http = http;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Adds an entry: `coss.risk.lists.add("block", { domain: "evil.example" })`. An allow-list match
|
|
1403
|
+
* makes decisions `allow`, a block-list match `block`. Returns the existing entry if there is one.
|
|
1404
|
+
*/
|
|
1405
|
+
add(list, entry, options) {
|
|
1406
|
+
const type = LIST_ENTRY_TYPES.find((name) => name in entry);
|
|
1407
|
+
const body = {
|
|
1408
|
+
list,
|
|
1409
|
+
type,
|
|
1410
|
+
value: String(entry[type] ?? ""),
|
|
1411
|
+
...entry.note === void 0 ? {} : { note: entry.note },
|
|
1412
|
+
...entry.expires_at === void 0 ? {} : { expires_at: entry.expires_at instanceof Date ? entry.expires_at.toISOString() : entry.expires_at }
|
|
1413
|
+
};
|
|
1414
|
+
return this.#http.request({
|
|
1415
|
+
method: "POST",
|
|
1416
|
+
path: "/v1/risk/list_entries",
|
|
1417
|
+
body,
|
|
1418
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1419
|
+
signal: options?.signal
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
/** Lists entries, newest first. Pass the last `id` as `after` while `has_more` is true. */
|
|
1423
|
+
list(params = {}, options) {
|
|
1424
|
+
return this.#http.request({
|
|
1425
|
+
method: "GET",
|
|
1426
|
+
path: "/v1/risk/list_entries",
|
|
1427
|
+
query: { ...params },
|
|
1428
|
+
signal: options?.signal
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
/** Retrieves one entry. */
|
|
1432
|
+
get(id, options) {
|
|
1433
|
+
return this.#http.request({
|
|
1434
|
+
method: "GET",
|
|
1435
|
+
path: `/v1/risk/list_entries/${encodeURIComponent(id)}`,
|
|
1436
|
+
signal: options?.signal
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
/** Removes an entry. */
|
|
1440
|
+
remove(id, options) {
|
|
1441
|
+
return this.#http.request({
|
|
1442
|
+
method: "DELETE",
|
|
1443
|
+
path: `/v1/risk/list_entries/${encodeURIComponent(id)}`,
|
|
1444
|
+
signal: options?.signal
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
/** `coss.risk.settings`: the environment's thresholds, disabled reasons and captcha provider. */
|
|
1449
|
+
var RiskSettingsResource = class {
|
|
1450
|
+
#http;
|
|
1451
|
+
/** @internal Use `new Coss().risk.settings`. */
|
|
1452
|
+
constructor(http) {
|
|
1453
|
+
this.#http = http;
|
|
1454
|
+
}
|
|
1455
|
+
get(options) {
|
|
1456
|
+
return this.#http.request({
|
|
1457
|
+
method: "GET",
|
|
1458
|
+
path: "/v1/risk/settings",
|
|
1459
|
+
signal: options?.signal
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
/** Changes the fields given. The captcha secret is write-only. */
|
|
1463
|
+
update(params, options) {
|
|
1464
|
+
return this.#http.request({
|
|
1465
|
+
method: "PATCH",
|
|
1466
|
+
path: "/v1/risk/settings",
|
|
1467
|
+
body: params,
|
|
1468
|
+
signal: options?.signal
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* `coss.risk.rules`: the environment's rules. After the lists, the first enabled rule whose
|
|
1474
|
+
* conditions match an event decides it.
|
|
1475
|
+
*/
|
|
1476
|
+
var RiskRulesResource = class {
|
|
1477
|
+
#http;
|
|
1478
|
+
/** @internal Use `new Coss().risk.rules`. */
|
|
1479
|
+
constructor(http) {
|
|
1480
|
+
this.#http = http;
|
|
1481
|
+
}
|
|
1482
|
+
/** Creates a rule. Not retried automatically: a retry could create it twice. */
|
|
1483
|
+
create(params, options) {
|
|
1484
|
+
return this.#http.request({
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
path: "/v1/risk/rules",
|
|
1487
|
+
body: params,
|
|
1488
|
+
retry: false,
|
|
1489
|
+
signal: options?.signal
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
/** Every rule, in evaluation order. */
|
|
1493
|
+
list(options) {
|
|
1494
|
+
return this.#http.request({
|
|
1495
|
+
method: "GET",
|
|
1496
|
+
path: "/v1/risk/rules",
|
|
1497
|
+
signal: options?.signal
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
get(id, options) {
|
|
1501
|
+
return this.#http.request({
|
|
1502
|
+
method: "GET",
|
|
1503
|
+
path: `/v1/risk/rules/${encodeURIComponent(id)}`,
|
|
1504
|
+
signal: options?.signal
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
/** Changes the fields given; `conditions` replaces the whole list. */
|
|
1508
|
+
update(id, params, options) {
|
|
1509
|
+
return this.#http.request({
|
|
1510
|
+
method: "PATCH",
|
|
1511
|
+
path: `/v1/risk/rules/${encodeURIComponent(id)}`,
|
|
1512
|
+
body: params,
|
|
1513
|
+
signal: options?.signal
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
delete(id, options) {
|
|
1517
|
+
return this.#http.request({
|
|
1518
|
+
method: "DELETE",
|
|
1519
|
+
path: `/v1/risk/rules/${encodeURIComponent(id)}`,
|
|
1520
|
+
signal: options?.signal
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
/** `coss.risk.users`: what COSS remembers about your users. */
|
|
1525
|
+
var RiskUsersResource = class {
|
|
1526
|
+
#http;
|
|
1527
|
+
/** @internal Use `new Coss().risk.users`. */
|
|
1528
|
+
constructor(http) {
|
|
1529
|
+
this.#http = http;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Call when you delete a user: COSS deletes what identifies them (their mailbox, fraud reports,
|
|
1533
|
+
* and the user id, email, IP and user agent on their assessments). Safe to repeat.
|
|
1534
|
+
*/
|
|
1535
|
+
forget(userId, options) {
|
|
1536
|
+
return this.#http.request({
|
|
1537
|
+
method: "DELETE",
|
|
1538
|
+
path: `/v1/risk/users/${encodeURIComponent(userId)}`,
|
|
1539
|
+
signal: options?.signal
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
/**
|
|
1544
|
+
* `coss.risk`: assess sign-ups, logins and other events. Call it from your server and treat an
|
|
1545
|
+
* error as `allow` (fail open), so an outage never locks your users out.
|
|
1546
|
+
*
|
|
1547
|
+
* ```ts
|
|
1548
|
+
* const { data } = await coss.risk.assess({ event: "signup", user: { email }, ip, user_agent });
|
|
1549
|
+
* if (data?.decision === "block") return refuse();
|
|
1550
|
+
* ```
|
|
1551
|
+
*/
|
|
1552
|
+
var RiskResource = class {
|
|
1553
|
+
/** Past assessments (`get`, `list`). */
|
|
1554
|
+
assessments;
|
|
1555
|
+
/** Allow and block lists (`add`, `list`, `get`, `remove`). */
|
|
1556
|
+
lists;
|
|
1557
|
+
/** Forgetting deleted users. */
|
|
1558
|
+
users;
|
|
1559
|
+
/** Thresholds, disabled reasons and the captcha provider (`get`, `update`). */
|
|
1560
|
+
settings;
|
|
1561
|
+
/** Rules (`create`, `list`, `get`, `update`, `delete`). */
|
|
1562
|
+
rules;
|
|
1563
|
+
#http;
|
|
1564
|
+
/** @internal Use `new Coss().risk`. */
|
|
1565
|
+
constructor(http) {
|
|
1566
|
+
this.#http = http;
|
|
1567
|
+
this.assessments = new RiskAssessmentsResource(http);
|
|
1568
|
+
this.lists = new RiskListsResource(http);
|
|
1569
|
+
this.users = new RiskUsersResource(http);
|
|
1570
|
+
this.settings = new RiskSettingsResource(http);
|
|
1571
|
+
this.rules = new RiskRulesResource(http);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Assesses an event and returns the decision with its score and reasons. Retries reuse one
|
|
1575
|
+
* `Idempotency-Key`, so an event is never counted twice.
|
|
1576
|
+
*/
|
|
1577
|
+
assess(params, options) {
|
|
1578
|
+
return this.#http.request({
|
|
1579
|
+
method: "POST",
|
|
1580
|
+
path: "/v1/risk/assessments",
|
|
1581
|
+
body: params,
|
|
1582
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1583
|
+
signal: options?.signal
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Reports what an assessed user turned out to be: `fraud` and `abuse` teach later assessments to
|
|
1588
|
+
* flag the same mailbox, user, IP and subnet; `legitimate` takes that back. Resolves a review.
|
|
1589
|
+
*/
|
|
1590
|
+
feedback(id, params, options) {
|
|
1591
|
+
return this.#http.request({
|
|
1592
|
+
method: "POST",
|
|
1593
|
+
path: `/v1/risk/assessments/${encodeURIComponent(id)}/feedback`,
|
|
1594
|
+
body: params,
|
|
1595
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1596
|
+
signal: options?.signal
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
//#endregion
|
|
1601
|
+
//#region src/resources/webhook-deliveries.ts
|
|
1602
|
+
const deliveriesPath = (endpointId) => `/v1/webhook_endpoints/${encodeURIComponent(endpointId)}/deliveries`;
|
|
1603
|
+
/**
|
|
1604
|
+
* `coss.webhookEndpoints.deliveries`: an endpoint's delivery log. Each delivery carries every
|
|
1605
|
+
* HTTP attempt and the payload as sent.
|
|
1606
|
+
*/
|
|
1607
|
+
var WebhookDeliveriesResource = class {
|
|
1608
|
+
#http;
|
|
1609
|
+
/** @internal Use `new Coss().webhookEndpoints.deliveries`. */
|
|
1610
|
+
constructor(http) {
|
|
1611
|
+
this.#http = http;
|
|
1612
|
+
}
|
|
1613
|
+
/** Lists the endpoint's deliveries, newest first. Pass the last `id` as `after` while `has_more` is true. */
|
|
1614
|
+
list(endpointId, params = {}, options) {
|
|
1615
|
+
return this.#http.request({
|
|
1616
|
+
method: "GET",
|
|
1617
|
+
path: deliveriesPath(endpointId),
|
|
1618
|
+
query: { ...params },
|
|
1619
|
+
signal: options?.signal
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
/** Retrieves one delivery of the endpoint. */
|
|
1623
|
+
get(endpointId, id, options) {
|
|
1624
|
+
return this.#http.request({
|
|
1625
|
+
method: "GET",
|
|
1626
|
+
path: `${deliveriesPath(endpointId)}/${encodeURIComponent(id)}`,
|
|
1627
|
+
signal: options?.signal
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Queues a finished delivery for one more attempt, made within seconds. A delivery that is
|
|
1632
|
+
* still `pending` is returned unchanged (it is already scheduled for `next_attempt_at`).
|
|
1633
|
+
* Never automatically retried: the API does not deduplicate replay requests, even with an
|
|
1634
|
+
* idempotency key.
|
|
1635
|
+
*/
|
|
1636
|
+
retry(endpointId, id, options) {
|
|
1637
|
+
return this.#http.request({
|
|
1638
|
+
method: "POST",
|
|
1639
|
+
path: `${deliveriesPath(endpointId)}/${encodeURIComponent(id)}/retry`,
|
|
1640
|
+
retry: false,
|
|
1641
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1642
|
+
signal: options?.signal
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/resources/webhook-endpoints.ts
|
|
1648
|
+
/**
|
|
1649
|
+
* `coss.webhookEndpoints`: the URLs that receive signed event notifications. Endpoints belong to
|
|
1650
|
+
* the API key's own environment, so a `coss_test_…` key only ever manages sandbox endpoints.
|
|
1651
|
+
*/
|
|
1652
|
+
var WebhookEndpointsResource = class {
|
|
1653
|
+
#http;
|
|
1654
|
+
/** Each endpoint's delivery log: list, retrieve and retry deliveries. */
|
|
1655
|
+
deliveries;
|
|
1656
|
+
/** @internal Use `new Coss().webhookEndpoints`. */
|
|
1657
|
+
constructor(http) {
|
|
1658
|
+
this.#http = http;
|
|
1659
|
+
this.deliveries = new WebhookDeliveriesResource(http);
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Creates an endpoint. The result carries its `secret` (returned again only by `rollSecret`) —
|
|
1663
|
+
* store it and pass it to `verifyWebhook` for every delivery. Creation is never automatically retried:
|
|
1664
|
+
* the API does not deduplicate it, even with an idempotency key. After a lost response,
|
|
1665
|
+
* list endpoints and remove the endpoint with the unknown secret before creating a replacement.
|
|
1666
|
+
*/
|
|
1667
|
+
create(params, options) {
|
|
1668
|
+
return this.#http.request({
|
|
1669
|
+
method: "POST",
|
|
1670
|
+
path: "/v1/webhook_endpoints",
|
|
1671
|
+
retry: false,
|
|
1672
|
+
body: params,
|
|
1673
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1674
|
+
signal: options?.signal
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/** Lists the environment's endpoints, oldest first. Secrets are never included. */
|
|
1678
|
+
list(options) {
|
|
1679
|
+
return this.#http.request({
|
|
1680
|
+
method: "GET",
|
|
1681
|
+
path: "/v1/webhook_endpoints",
|
|
1682
|
+
signal: options?.signal
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
/** Retrieves an endpoint of the API key's environment. */
|
|
1686
|
+
get(id, options) {
|
|
1687
|
+
return this.#http.request({
|
|
1688
|
+
method: "GET",
|
|
1689
|
+
path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
|
|
1690
|
+
signal: options?.signal
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Changes the URL, description, subscribed event types or `enabled` flag. The signing secret
|
|
1695
|
+
* stays the same (see `rollSecret`), so receivers keep verifying deliveries.
|
|
1696
|
+
*/
|
|
1697
|
+
update(id, params, options) {
|
|
1698
|
+
return this.#http.request({
|
|
1699
|
+
method: "PATCH",
|
|
1700
|
+
path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
|
|
1701
|
+
body: params,
|
|
1702
|
+
signal: options?.signal
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Replaces the endpoint's signing secret; the result carries the new `secret`. Until
|
|
1707
|
+
* `previous_secret_expires_at` (a day by default), deliveries are signed with both secrets, so
|
|
1708
|
+
* `verifyWebhook` accepts them with either: deploy the new one, then let the old one expire.
|
|
1709
|
+
* Never automatically retried (a second roll would stop the original secret at once): after a
|
|
1710
|
+
* lost response, roll again and deploy that secret.
|
|
1711
|
+
*/
|
|
1712
|
+
rollSecret(id, params = {}, options) {
|
|
1713
|
+
return this.#http.request({
|
|
1714
|
+
method: "POST",
|
|
1715
|
+
path: `/v1/webhook_endpoints/${encodeURIComponent(id)}/roll_secret`,
|
|
1716
|
+
retry: false,
|
|
1717
|
+
body: params,
|
|
1718
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1719
|
+
signal: options?.signal
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Queues the endpoint's deliveries that failed since `since` for one more attempt each (at most
|
|
1724
|
+
* 1,000 per call; while `has_more` is true, call again with the same `since`), e.g. after fixing a
|
|
1725
|
+
* receiver that was down. The endpoint must be enabled.
|
|
1726
|
+
*/
|
|
1727
|
+
recover(id, params, options) {
|
|
1728
|
+
const since = params.since instanceof Date ? params.since.toISOString() : params.since;
|
|
1729
|
+
return this.#http.request({
|
|
1730
|
+
method: "POST",
|
|
1731
|
+
path: `/v1/webhook_endpoints/${encodeURIComponent(id)}/recover`,
|
|
1732
|
+
body: { since },
|
|
1733
|
+
idempotencyKey: options?.idempotencyKey,
|
|
1734
|
+
signal: options?.signal
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
/** Deletes the endpoint and its delivery log. To pause it instead, update `enabled` to false. */
|
|
1738
|
+
delete(id, options) {
|
|
1739
|
+
return this.#http.request({
|
|
1740
|
+
method: "DELETE",
|
|
1741
|
+
path: `/v1/webhook_endpoints/${encodeURIComponent(id)}`,
|
|
1742
|
+
signal: options?.signal
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
//#endregion
|
|
1747
|
+
//#region src/client.ts
|
|
1748
|
+
const DEFAULT_BASE_URL = "https://api.coss.com";
|
|
1749
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
1750
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
1751
|
+
const MAX_TIMEOUT_MS = 2147483647;
|
|
1752
|
+
/** Reads an env variable where one exists (Node, Bun, Deno's `process`), never throwing. */
|
|
1753
|
+
function readEnv(name) {
|
|
1754
|
+
try {
|
|
1755
|
+
return (globalThis.process?.env)?.[name]?.trim() || void 0;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function retries(value) {
|
|
1761
|
+
return value !== void 0 && Number.isFinite(value) && value >= 0 ? Math.floor(value) : DEFAULT_MAX_RETRIES;
|
|
1762
|
+
}
|
|
1763
|
+
function timeoutMs(value) {
|
|
1764
|
+
return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.min(Math.ceil(value), MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* The COSS client. One API key covers every product.
|
|
1768
|
+
*
|
|
1769
|
+
* ```ts
|
|
1770
|
+
* const coss = new Coss({ apiKey: "coss_test_…" });
|
|
1771
|
+
* const { data, error } = await coss.email.send({ from, to, subject, html });
|
|
1772
|
+
* ```
|
|
1773
|
+
*/
|
|
1774
|
+
var Coss = class {
|
|
1775
|
+
email;
|
|
1776
|
+
/** Sending domains of the organization (`coss.domains.create`, `.verify`, …). */
|
|
1777
|
+
domains;
|
|
1778
|
+
/** Webhook endpoints of the API key's environment. */
|
|
1779
|
+
webhookEndpoints;
|
|
1780
|
+
/** The organization's sending reputation (`coss.reputation.get()`). */
|
|
1781
|
+
reputation;
|
|
1782
|
+
/** The environment's log: API requests, email events, webhook attempts (`coss.logs.list()`). */
|
|
1783
|
+
logs;
|
|
1784
|
+
/** HTTPS endpoints the environment's log is streamed to (`coss.logDrains.create()`). */
|
|
1785
|
+
logDrains;
|
|
1786
|
+
/** The organization's audit log: changes by members, API keys and COSS support (`coss.auditEvents.list()`). */
|
|
1787
|
+
auditEvents;
|
|
1788
|
+
/** Hooks, webhooks as a service: `coss.hooks.messages.send()` delivers to your users' endpoints. */
|
|
1789
|
+
hooks;
|
|
1790
|
+
/** Risk assessments of sign-ups, logins and other events (`coss.risk.assess()`). */
|
|
1791
|
+
risk;
|
|
1792
|
+
/** The organization's plan, usage, invoices and credits (`coss.billing.get()`). */
|
|
1793
|
+
billing;
|
|
1794
|
+
/**
|
|
1795
|
+
* Throws only for programmer errors: no API key, or running in a browser without
|
|
1796
|
+
* `dangerouslyAllowBrowser`. Request failures are returned as `{ data: null, error }`.
|
|
1797
|
+
*/
|
|
1798
|
+
constructor(options = {}) {
|
|
1799
|
+
if (typeof globalThis.document !== "undefined" && !options.dangerouslyAllowBrowser) throw new Error("Refusing to create a COSS client in a browser-like environment: COSS API keys are secret, and any key sent to the browser can be read by anyone using the page. Call COSS from your server instead, or pass `dangerouslyAllowBrowser: true` if you understand the risk.");
|
|
1800
|
+
const apiKey = options.apiKey?.trim() || readEnv("COSS_API_KEY");
|
|
1801
|
+
if (!apiKey) throw new Error("Missing COSS API key. Pass `apiKey` to `new Coss({ apiKey })` or set the COSS_API_KEY environment variable.");
|
|
1802
|
+
const http = new HttpClient({
|
|
1803
|
+
apiKey,
|
|
1804
|
+
baseUrl: options.baseUrl?.trim() || readEnv("COSS_BASE_URL") || DEFAULT_BASE_URL,
|
|
1805
|
+
fetch: options.fetch ?? ((input, init) => globalThis.fetch(input, init)),
|
|
1806
|
+
timeout: timeoutMs(options.timeout),
|
|
1807
|
+
maxRetries: retries(options.maxRetries),
|
|
1808
|
+
apiVersion: options.apiVersion?.trim() || "2026-10-10"
|
|
1809
|
+
});
|
|
1810
|
+
this.email = new EmailResource(http);
|
|
1811
|
+
this.domains = new DomainsResource(http);
|
|
1812
|
+
this.webhookEndpoints = new WebhookEndpointsResource(http);
|
|
1813
|
+
this.reputation = new ReputationResource(http);
|
|
1814
|
+
this.logs = new LogsResource(http);
|
|
1815
|
+
this.logDrains = new LogDrainsResource(http);
|
|
1816
|
+
this.auditEvents = new AuditEventsResource(http);
|
|
1817
|
+
this.hooks = new HooksResource(http);
|
|
1818
|
+
this.risk = new RiskResource(http);
|
|
1819
|
+
this.billing = new BillingResource(http);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/webhooks.ts
|
|
1824
|
+
/** A webhook request failed verification: reject it (e.g. respond 400). */
|
|
1825
|
+
var WebhookVerificationError = class extends Error {
|
|
1826
|
+
constructor(message) {
|
|
1827
|
+
super(message);
|
|
1828
|
+
this.name = "WebhookVerificationError";
|
|
1829
|
+
}
|
|
1830
|
+
};
|
|
1831
|
+
const SECRET_PREFIX = "whsec_";
|
|
1832
|
+
const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
1833
|
+
function header(headers, name) {
|
|
1834
|
+
if (typeof headers.get === "function") return headers.get(name);
|
|
1835
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1836
|
+
if (key.toLowerCase() !== name) continue;
|
|
1837
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
1838
|
+
return typeof first === "string" ? first : null;
|
|
1839
|
+
}
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
function decodeBase64(value) {
|
|
1843
|
+
try {
|
|
1844
|
+
const binary = atob(value);
|
|
1845
|
+
const bytes = new Uint8Array(binary.length);
|
|
1846
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
1847
|
+
return bytes;
|
|
1848
|
+
} catch {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/** Throws unless one of the request's signatures matches and its timestamp is recent. */
|
|
1853
|
+
async function verifySignature(payload, headers, secret, options) {
|
|
1854
|
+
const id = header(headers, "webhook-id");
|
|
1855
|
+
const timestamp = header(headers, "webhook-timestamp");
|
|
1856
|
+
const signatures = header(headers, "webhook-signature");
|
|
1857
|
+
if (!id || !timestamp || !signatures) throw new WebhookVerificationError("Missing webhook-id, webhook-timestamp or webhook-signature.");
|
|
1858
|
+
const sentAt = Number(timestamp);
|
|
1859
|
+
const now = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
1860
|
+
const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
|
|
1861
|
+
if (!/^\d+$/.test(timestamp) || Math.abs(now - sentAt) > tolerance) throw new WebhookVerificationError("The webhook timestamp is too old or in the future.");
|
|
1862
|
+
const keyBytes = decodeBase64(secret.startsWith(SECRET_PREFIX) ? secret.slice(6) : secret);
|
|
1863
|
+
if (!keyBytes || keyBytes.length === 0) throw new WebhookVerificationError("The signing secret is not a valid whsec_… secret.");
|
|
1864
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, {
|
|
1865
|
+
name: "HMAC",
|
|
1866
|
+
hash: "SHA-256"
|
|
1867
|
+
}, false, ["verify"]);
|
|
1868
|
+
const signed = new TextEncoder().encode(`${id}.${timestamp}.${payload}`);
|
|
1869
|
+
for (const candidate of signatures.split(" ")) {
|
|
1870
|
+
const [version, value] = candidate.split(",", 2);
|
|
1871
|
+
if (version !== "v1" || !value) continue;
|
|
1872
|
+
const signature = decodeBase64(value);
|
|
1873
|
+
if (signature && await crypto.subtle.verify("HMAC", key, signature, signed)) return;
|
|
1874
|
+
}
|
|
1875
|
+
throw new WebhookVerificationError("No webhook signature matches.");
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Checks the request's signature and age, then returns the parsed event. Pass the raw request
|
|
1879
|
+
* body exactly as received (not re-serialized JSON) and the endpoint's `whsec_…` signing secret.
|
|
1880
|
+
* Throws `WebhookVerificationError` when the request must be rejected.
|
|
1881
|
+
*
|
|
1882
|
+
* ```ts
|
|
1883
|
+
* const event = await verifyWebhook(await request.text(), request.headers, process.env.COSS_WEBHOOK_SECRET);
|
|
1884
|
+
* ```
|
|
1885
|
+
*
|
|
1886
|
+
* The same check verifies messages your own users receive from COSS Hooks (`coss.hooks`): pass
|
|
1887
|
+
* the endpoint's secret and name your payload type, e.g. `verifyWebhook<InvoicePaid>(…)`.
|
|
1888
|
+
*/
|
|
1889
|
+
async function verifyWebhook(payload, headers, secret, options = {}) {
|
|
1890
|
+
await verifySignature(payload, headers, secret, options);
|
|
1891
|
+
try {
|
|
1892
|
+
return JSON.parse(payload);
|
|
1893
|
+
} catch {
|
|
1894
|
+
throw new WebhookVerificationError("The webhook body is not valid JSON.");
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Checks a log drain request's signature and age, then returns its lines (NDJSON: one log line or
|
|
1899
|
+
* audit event per line). Pass the raw request body exactly as received and the drain's `whsec_…`
|
|
1900
|
+
* signing secret. The `webhook-id` header is the batch id, the same on every retry of a batch:
|
|
1901
|
+
* use it (or each line's `id`) to skip a batch you already stored. Throws
|
|
1902
|
+
* `WebhookVerificationError` when the request must be rejected.
|
|
1903
|
+
*
|
|
1904
|
+
* ```ts
|
|
1905
|
+
* const lines = await verifyLogDrainBatch(await request.text(), request.headers, process.env.COSS_LOG_DRAIN_SECRET);
|
|
1906
|
+
* ```
|
|
1907
|
+
*/
|
|
1908
|
+
async function verifyLogDrainBatch(payload, headers, secret, options = {}) {
|
|
1909
|
+
await verifySignature(payload, headers, secret, options);
|
|
1910
|
+
try {
|
|
1911
|
+
return payload.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
1912
|
+
} catch {
|
|
1913
|
+
throw new WebhookVerificationError("The log drain body is not valid NDJSON.");
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
//#endregion
|
|
1917
|
+
export { API_VERSION, API_VERSIONS, Coss, VERSION, WebhookVerificationError, verifyLogDrainBatch, verifyWebhook };
|