coss 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -10
- package/dist/device.d.ts +11 -0
- package/dist/device.js +71 -0
- package/dist/index.d.ts +1211 -143
- package/dist/index.js +558 -25
- package/package.json +15 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# coss
|
|
2
2
|
|
|
3
|
-
The official TypeScript SDK for
|
|
3
|
+
The official TypeScript SDK for coss.com: one API key for email, and soon SMS, video, scheduling and more.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install coss
|
|
@@ -10,7 +10,7 @@ Published from [cosscom/coss](https://github.com/cosscom/coss) by CI, with
|
|
|
10
10
|
[provenance](https://docs.npmjs.com/generating-provenance-statements). Versions 0.0.1–0.0.5 of the npm package
|
|
11
11
|
are an earlier, unrelated package by a previous owner (deprecated).
|
|
12
12
|
|
|
13
|
-
- Zero dependencies, ESM-only, fully typed.
|
|
13
|
+
- Zero dependencies (React Email support is an optional peer), ESM-only, fully typed.
|
|
14
14
|
- `coss.email`, `coss.domains`, `coss.webhookEndpoints` and `coss.reputation` behind one API key.
|
|
15
15
|
- Every method returns `{ data, error }`. API and network failures are returned, never thrown.
|
|
16
16
|
- Retries transient failures automatically, and sends are idempotent.
|
|
@@ -25,7 +25,7 @@ const coss = new Coss({ apiKey: process.env.COSS_API_KEY });
|
|
|
25
25
|
const { data, error } = await coss.email.send({
|
|
26
26
|
from: "Acme <onboarding@example.com>",
|
|
27
27
|
to: "delivered@example.com",
|
|
28
|
-
subject: "Hello from
|
|
28
|
+
subject: "Hello from coss.com",
|
|
29
29
|
html: "<p>It works!</p>",
|
|
30
30
|
});
|
|
31
31
|
|
|
@@ -73,7 +73,7 @@ The constructor throws in only two cases, both programmer errors:
|
|
|
73
73
|
- It runs where a `document` global exists (a browser, or a jsdom or happy-dom test environment)
|
|
74
74
|
without `dangerouslyAllowBrowser: true`.
|
|
75
75
|
|
|
76
|
-
API keys are secrets, so call
|
|
76
|
+
API keys are secrets, so call coss.com from your server.
|
|
77
77
|
|
|
78
78
|
Each method also takes per-request options:
|
|
79
79
|
|
|
@@ -99,6 +99,35 @@ import { API_VERSION } from "coss";
|
|
|
99
99
|
await coss.webhookEndpoints.update("whep_01k…", { api_version: API_VERSION });
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
+
## React Email
|
|
103
|
+
|
|
104
|
+
Pass a [React Email](https://react.email) component as `react` instead of `html`. The SDK renders it
|
|
105
|
+
with `@react-email/render`, an optional peer dependency you install yourself (with `react` and
|
|
106
|
+
`react-dom`, which your components need anyway):
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
npm install @react-email/render
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { WelcomeEmail } from "./emails/welcome";
|
|
114
|
+
|
|
115
|
+
await coss.email.send({
|
|
116
|
+
from: "Acme <hello@acme.com>",
|
|
117
|
+
to: "ada@example.com",
|
|
118
|
+
subject: "Welcome to Acme",
|
|
119
|
+
react: <WelcomeEmail name="Ada" />,
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The component becomes `html`, and `text` too unless you pass your own. Rendering happens once, before
|
|
124
|
+
the first attempt, so retries resend the same body. `sendBatch` renders each email's `react` the same way.
|
|
125
|
+
Nothing throws: `react` together with `html` returns `validation_error`, a missing
|
|
126
|
+
`@react-email/render` returns `react_renderer_missing`, and a component that throws returns
|
|
127
|
+
`react_render_failed`, all `invalid_request_error`s with `param` naming the email (`react`,
|
|
128
|
+
`emails.3.react`) and no request made. The SDK itself still has no dependencies: the renderer is imported
|
|
129
|
+
only by a send that has `react`.
|
|
130
|
+
|
|
102
131
|
## Attachments
|
|
103
132
|
|
|
104
133
|
Attach up to 20 files (25 MB combined) as base64. The content type defaults from the extension.
|
|
@@ -120,7 +149,7 @@ await coss.email.send({
|
|
|
120
149
|
```
|
|
121
150
|
|
|
122
151
|
Emails only return attachment metadata (`id`, `filename`, `content_type`, `content_id`, `size`).
|
|
123
|
-
|
|
152
|
+
coss.com discards the content once the email is sent. Executables and other file types the delivery
|
|
124
153
|
provider refuses (`.exe`, `.js`, `.bat`, …) are rejected with a `validation_error`.
|
|
125
154
|
|
|
126
155
|
## Domains
|
|
@@ -148,7 +177,8 @@ await coss.domains.delete(domain!.id);
|
|
|
148
177
|
rate limited: a second call within a few seconds answers `429` (the SDK waits out `Retry-After` and
|
|
149
178
|
retries). Verification gives up after 72 hours and the domain becomes `failed`; calling `verify`
|
|
150
179
|
again then starts over with fresh DKIM records, which have to be published too. The `dmarc` record
|
|
151
|
-
is recommended rather than required (`required: false`).
|
|
180
|
+
is recommended rather than required (`required: false`). `bimi` reports whether mailbox providers can show
|
|
181
|
+
your brand logo (your own BIMI record and VMC or CMC certificate, with DMARC at enforcement).
|
|
152
182
|
|
|
153
183
|
## Webhook endpoints
|
|
154
184
|
|
|
@@ -241,7 +271,7 @@ Imported entries have the reason `import`. The dashboard imports and exports the
|
|
|
241
271
|
### Open and click tracking
|
|
242
272
|
|
|
243
273
|
Tracking is off unless the environment turns it on in the dashboard (a default, plus rules per
|
|
244
|
-
category) or a send asks for it.
|
|
274
|
+
category) or a send asks for it. coss.com then adds an invisible image to the HTML body and sends its
|
|
245
275
|
links through a redirect. Links with a `data-coss-no-track` attribute are left as they are.
|
|
246
276
|
|
|
247
277
|
```ts
|
|
@@ -264,9 +294,9 @@ one. Mail clients that preload images and link scanners can inflate the numbers.
|
|
|
264
294
|
|
|
265
295
|
## Reputation
|
|
266
296
|
|
|
267
|
-
|
|
297
|
+
coss.com watches your live bounce and complaint rates. When one gets too high your organization is
|
|
268
298
|
warned, and past the suspension threshold live sending is suspended automatically (sends fail with
|
|
269
|
-
`permission_error` / `account_suspended`; sandbox keys keep working) until
|
|
299
|
+
`permission_error` / `account_suspended`; sandbox keys keep working) until coss.com support reinstates it.
|
|
270
300
|
|
|
271
301
|
```ts
|
|
272
302
|
const { data } = await coss.reputation.get();
|
|
@@ -325,7 +355,7 @@ interface CossError {
|
|
|
325
355
|
| `not_found_error` | 404 | No such resource in the key's environment |
|
|
326
356
|
| `idempotency_error` | 409 / 422 | `request_in_progress` (retried automatically), or `idempotency_key_reused` with a different body |
|
|
327
357
|
| `rate_limit_error` | 429 | Too many requests with the key (`rate_limited`), the send quota is used up (`quota_exceeded`), or a domain was checked moments ago (`domain_recently_checked`) |
|
|
328
|
-
| `api_error` | 5xx, other | Server error. Also used for responses that are not a
|
|
358
|
+
| `api_error` | 5xx, other | Server error. Also used for responses that are not a coss.com error envelope (`unexpected_response`) |
|
|
329
359
|
| `connection_error` | — | Network failure (`network_error`), or you aborted (`aborted`) |
|
|
330
360
|
| `timeout_error` | — | An attempt exceeded `timeout` (`request_timeout`) |
|
|
331
361
|
|
package/dist/device.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/device.d.ts
|
|
2
|
+
export interface CollectDeviceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Where the device id is kept: `local` (default) remembers it in `localStorage`; `memory`
|
|
5
|
+
* keeps it only for this page (for example until the user consents to storage).
|
|
6
|
+
*/
|
|
7
|
+
storage?: "local" | "memory";
|
|
8
|
+
}
|
|
9
|
+
/** Returns a token for `device.token` in `coss.risk.assess()`. Make a new one per submission. */
|
|
10
|
+
export declare function collectDevice(options?: CollectDeviceOptions): string;
|
|
11
|
+
//#endregion
|
package/dist/device.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//#region src/device.ts
|
|
2
|
+
/**
|
|
3
|
+
* `coss/device`: recognizes the browser behind risk events. Call `collectDevice()` in the browser
|
|
4
|
+
* when the user submits a form, send the token to your server with it, and pass it to
|
|
5
|
+
* `coss.risk.assess({ device: { token } })`.
|
|
6
|
+
*
|
|
7
|
+
* The token carries a random id this browser keeps in `localStorage` (`coss_device_id`, no
|
|
8
|
+
* cookies) and coarse facts: time zone, languages, platform, screen size, touch support, and
|
|
9
|
+
* whether the browser is automated (WebDriver, headless). No canvas, WebGL, font or audio
|
|
10
|
+
* fingerprinting: a browser whose storage is cleared looks like a new one. coss.com keeps only a keyed
|
|
11
|
+
* hash of the id.
|
|
12
|
+
*
|
|
13
|
+
* Collects nothing until called, and never throws: outside a browser the token simply carries
|
|
14
|
+
* fewer facts.
|
|
15
|
+
*/
|
|
16
|
+
/** The token format version, checked by the API. */
|
|
17
|
+
const TOKEN_PREFIX = "cd1.";
|
|
18
|
+
const STORAGE_KEY = "coss_device_id";
|
|
19
|
+
const HEADLESS = /HeadlessChrome|PhantomJS|SlimerJS/;
|
|
20
|
+
let memoryId;
|
|
21
|
+
function randomId() {
|
|
22
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
23
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
24
|
+
return base64Url(bytes);
|
|
25
|
+
}
|
|
26
|
+
function base64Url(bytes) {
|
|
27
|
+
let binary = "";
|
|
28
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
29
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
30
|
+
}
|
|
31
|
+
function deviceId(storage) {
|
|
32
|
+
if (storage === "local") try {
|
|
33
|
+
const store = globalThis.localStorage;
|
|
34
|
+
if (store) {
|
|
35
|
+
const stored = store.getItem(STORAGE_KEY);
|
|
36
|
+
if (stored && /^[A-Za-z0-9_-]{16,64}$/.test(stored)) return stored;
|
|
37
|
+
const id = randomId();
|
|
38
|
+
store.setItem(STORAGE_KEY, id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
memoryId ??= randomId();
|
|
43
|
+
return memoryId;
|
|
44
|
+
}
|
|
45
|
+
function safely(read, fallback) {
|
|
46
|
+
try {
|
|
47
|
+
return read() ?? fallback;
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Returns a token for `device.token` in `coss.risk.assess()`. Make a new one per submission. */
|
|
53
|
+
function collectDevice(options = {}) {
|
|
54
|
+
const nav = globalThis.navigator;
|
|
55
|
+
const screen = globalThis.screen;
|
|
56
|
+
const userAgent = safely(() => nav?.userAgent ?? "", "");
|
|
57
|
+
const payload = {
|
|
58
|
+
id: deviceId(options.storage ?? "local"),
|
|
59
|
+
t: Date.now(),
|
|
60
|
+
tz: safely(() => Intl.DateTimeFormat().resolvedOptions().timeZone.slice(0, 64) || null, null),
|
|
61
|
+
l: safely(() => [...nav?.languages ?? []].slice(0, 10).map((l) => l.slice(0, 35)), []),
|
|
62
|
+
p: safely(() => (nav?.userAgentData?.platform || nav?.platform || "").slice(0, 64) || null, null),
|
|
63
|
+
s: safely(() => screen ? [Math.round(screen.width), Math.round(screen.height)] : null, null),
|
|
64
|
+
tp: safely(() => typeof nav?.maxTouchPoints === "number" ? Math.round(nav.maxTouchPoints) : null, null),
|
|
65
|
+
wd: safely(() => nav?.webdriver === true, false),
|
|
66
|
+
hl: HEADLESS.test(userAgent)
|
|
67
|
+
};
|
|
68
|
+
return `${TOKEN_PREFIX}${base64Url(new TextEncoder().encode(JSON.stringify(payload)))}`;
|
|
69
|
+
}
|
|
70
|
+
//#endregion
|
|
71
|
+
export { collectDevice };
|