coss 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -292,6 +292,79 @@ Opens and clicks arrive as `email.opened` and `email.clicked` events (in `listEv
292
292
  webhooks), with `data.open` / `data.click`. They name a recipient only when the email had exactly
293
293
  one. Mail clients that preload images and link scanners can inflate the numbers.
294
294
 
295
+ ## Marketing email
296
+
297
+ Contacts belong to an environment. Each has an `email`, names, a `status` (`subscribed`,
298
+ `unsubscribed`, or `pending` while waiting for a double opt-in confirmation), custom properties you
299
+ define first, and a choice per topic. Audiences are static lists of contacts; segments are saved
300
+ filters over them.
301
+
302
+ ```ts
303
+ await coss.email.contacts.properties.create({ key: "plan", type: "string", fallback: "free" });
304
+
305
+ // Creates the contact for this address, or updates it (201 vs 200).
306
+ const { data: contact } = await coss.email.contacts.upsert({
307
+ email: "ada@example.com",
308
+ first_name: "Ada",
309
+ properties: { plan: "pro" },
310
+ topics: [{ topic: "newsletter", status: "opted_in" }],
311
+ // Stays `pending` until they click the confirmation email's link (sent from the marketing
312
+ // settings' `confirmation_from`).
313
+ double_opt_in: true,
314
+ });
315
+
316
+ // Up to 1,000 per request; the SDK sends larger lists in batches.
317
+ await coss.email.contacts.import({
318
+ contacts: [{ email: "joe@example.com", properties: { plan: "free" } }],
319
+ audience_id: "aud_01k…",
320
+ });
321
+ const { data: csv } = await coss.email.contacts.export();
322
+ ```
323
+
324
+ A single marketing email goes to one recipient with `stream: "marketing"`. It needs your postal
325
+ address (`coss.email.marketingSettings.update({ mailing_address: "…" })`), and coss.com adds
326
+ one-click unsubscribe headers and a footer. Contacts who unsubscribed, or opted out of the email's
327
+ topic (its `category`), are skipped.
328
+
329
+ ```ts
330
+ await coss.email.send({
331
+ from: "Acme <news@acme.com>",
332
+ to: "ada@example.com",
333
+ subject: "You're invited",
334
+ html: "<p>Join us on Thursday.</p>",
335
+ stream: "marketing",
336
+ category: "events",
337
+ });
338
+ ```
339
+
340
+ ### Broadcasts
341
+
342
+ A broadcast sends one email to every eligible contact of an audience or a segment, each filled in
343
+ with their own values (`{{{contact.<key>|fallback}}}`), at your plan's marketing rate or slower.
344
+
345
+ ```ts
346
+ const { data: broadcast } = await coss.email.broadcasts.create({
347
+ name: "October newsletter",
348
+ segment_id: "seg_01k…",
349
+ topic: "newsletter",
350
+ from: "Acme <news@acme.com>",
351
+ subject: "What's new, {{{contact.first_name|there}}}",
352
+ html: "<p>Hi {{{contact.first_name|there}}}, …</p>",
353
+ });
354
+ await coss.email.broadcasts.test(broadcast.id, { to: ["me@acme.com"] });
355
+ await coss.email.broadcasts.send(broadcast.id, { scheduled_at: "2026-11-01T09:00:00Z" });
356
+
357
+ const { data: sent } = await coss.email.broadcasts.get(broadcast.id);
358
+ // sent.stats: { recipients, sent, delivered, bounced, complained, opened, clicked, unsubscribed, … }
359
+ await coss.email.broadcasts.pause(broadcast.id); // then resume() or cancel()
360
+ ```
361
+
362
+ Guides: [contacts](https://coss.com/docs/email/contacts),
363
+ [audiences and segments](https://coss.com/docs/email/audiences-and-segments),
364
+ [topics and preferences](https://coss.com/docs/email/topics-and-preferences),
365
+ [broadcasts](https://coss.com/docs/email/broadcasts) and
366
+ [streams](https://coss.com/docs/email/streams).
367
+
295
368
  ## Reputation
296
369
 
297
370
  coss.com watches your live bounce and complaint rates. When one gets too high your organization is
@@ -309,7 +382,8 @@ const { data } = await coss.reputation.get();
309
382
  Webhook endpoints receive `email.sent`, `email.delivered`, `email.delivery_delayed`,
310
383
  `email.bounced`, `email.complained`, `email.failed`, `email.suppressed`, `email.opened` and
311
384
  `email.clicked` events, plus `account.reputation_warning`, `account.reputation_recovered`,
312
- `account.suspended` and `account.reinstated` for live environments, signed with
385
+ `account.suspended`, `account.reinstated`, `account.marketing_paused` and
386
+ `account.marketing_reinstated` for live environments, signed with
313
387
  [Standard Webhooks](https://www.standardwebhooks.com) headers. Verify them with the raw body:
314
388
 
315
389
  ```ts
@@ -391,6 +465,29 @@ To make a send safe across process restarts too, pass your own key, derived from
391
465
  cancel a call. Aborting stops the request in flight and any pending retry, and returns
392
466
  `{ type: "connection_error", code: "aborted" }`.
393
467
 
468
+ ## Integrations
469
+
470
+ To send from libraries you already use, import the matching entry. They need no other package
471
+ than the library itself, and they throw a `CossSendError` when coss.com refuses an email.
472
+
473
+ ```ts
474
+ // Nodemailer
475
+ import nodemailer from "nodemailer";
476
+ import { cossTransport } from "coss/nodemailer";
477
+ const transporter = nodemailer.createTransport(cossTransport());
478
+
479
+ // Better Auth: verification, password reset, magic link, one-time code and invitation emails
480
+ import { cossEmail } from "coss/better-auth";
481
+ const email = cossEmail({ from: "Acme <auth@acme.com>", appName: "Acme" });
482
+ // betterAuth({ emailVerification: { sendVerificationEmail: email.sendVerificationEmail }, … })
483
+
484
+ // Auth.js (NextAuth.js v5): the email provider
485
+ import Coss from "coss/authjs";
486
+ // NextAuth({ adapter, providers: [Coss({ from: "Acme <auth@acme.com>" })] })
487
+ ```
488
+
489
+ Guides: [coss.com/docs/integrations](https://coss.com/docs/integrations).
490
+
394
491
  ## Runtime support
395
492
 
396
493
  The SDK works on Node.js ≥ 22, Bun, Deno and Cloudflare Workers. It only needs the standard `fetch`,
@@ -0,0 +1,64 @@
1
+ import { n as IntegrationClientOptions, t as CossSendError } from "./integration-CZAhrrnc.js";
2
+ //#region src/authjs.d.ts
3
+ /** What Auth.js passes to `sendVerificationRequest` (the fields the provider reads). */
4
+ export interface CossVerificationRequestParams {
5
+ /** The address to send the link to. */
6
+ identifier: string;
7
+ url: string;
8
+ token: string;
9
+ expires: Date;
10
+ provider: {
11
+ apiKey?: string;
12
+ from?: string;
13
+ };
14
+ theme: {
15
+ brandColor?: string;
16
+ buttonText?: string;
17
+ };
18
+ }
19
+ /** An email's content, as `render` returns it. */
20
+ export interface CossVerificationEmail {
21
+ subject: string;
22
+ html: string;
23
+ text: string;
24
+ }
25
+ export interface CossProviderConfig extends IntegrationClientOptions {
26
+ /** The sender, e.g. `"Acme <auth@acme.com>"`, on a domain verified in coss.com (live keys). */
27
+ from?: string;
28
+ /** How long a link is valid, in seconds. Default 24 hours. */
29
+ maxAge?: number;
30
+ /** Replace the default email (Auth.js's layout). */
31
+ render?: (params: CossVerificationRequestParams & {
32
+ host: string;
33
+ }) => CossVerificationEmail;
34
+ /** Defaults to `"account"`. */
35
+ category?: string;
36
+ /** Provider id, default `"coss"` (also names the env variable: `AUTH_COSS_KEY`). */
37
+ id?: string;
38
+ /** Shown on Auth.js's sign-in page. Default `"Email"`. */
39
+ name?: string;
40
+ }
41
+ /** The provider object Auth.js reads. */
42
+ export interface CossEmailProvider {
43
+ id: string;
44
+ type: "email";
45
+ name: string;
46
+ from?: string;
47
+ maxAge: number;
48
+ apiKey?: string;
49
+ sendVerificationRequest: (params: CossVerificationRequestParams) => Promise<void>;
50
+ /** What Auth.js merges over the provider (a type literal: Auth.js wants a `Record`). */
51
+ options: {
52
+ id?: string;
53
+ name?: string;
54
+ from?: string;
55
+ maxAge?: number;
56
+ };
57
+ }
58
+ /**
59
+ * An Auth.js email provider. Links go out with `category: "account"` and open and click tracking
60
+ * off: click tracking would route the secret link through coss.com's redirect.
61
+ */
62
+ export declare function CossProvider(config?: CossProviderConfig): CossEmailProvider;
63
+ //#endregion
64
+ export { CossProvider as default, CossSendError };
package/dist/authjs.js ADDED
@@ -0,0 +1,97 @@
1
+ import { c as CossSendError, l as lazyClient, o as sha256Hex, r as escapeHtml } from "./mime-m5sgODwn.js";
2
+ //#region src/authjs.ts
3
+ /**
4
+ * An Auth.js email provider. Links go out with `category: "account"` and open and click tracking
5
+ * off: click tracking would route the secret link through coss.com's redirect.
6
+ */
7
+ function CossProvider(config = {}) {
8
+ const clients = /* @__PURE__ */ new Map();
9
+ const clientFor = (apiKey) => {
10
+ const key = apiKey ?? config.apiKey ?? "";
11
+ let client = clients.get(key);
12
+ if (!client) {
13
+ client = lazyClient("authjs", config, () => apiKey ?? config.apiKey);
14
+ clients.set(key, client);
15
+ }
16
+ return client();
17
+ };
18
+ return {
19
+ id: config.id ?? "coss",
20
+ type: "email",
21
+ name: config.name ?? "Email",
22
+ from: config.from,
23
+ maxAge: config.maxAge ?? 86400,
24
+ apiKey: config.apiKey,
25
+ async sendVerificationRequest(params) {
26
+ const from = params.provider.from ?? config.from;
27
+ if (!from) throw new Error("coss/authjs: set `from`, e.g. Coss({ from: \"Acme <auth@acme.com>\" }).");
28
+ const host = new URL(params.url).host;
29
+ const email = (config.render ?? defaultEmail)({
30
+ ...params,
31
+ host
32
+ });
33
+ const result = await clientFor(params.provider.apiKey).email.send({
34
+ from,
35
+ to: params.identifier,
36
+ subject: email.subject,
37
+ html: email.html,
38
+ text: email.text,
39
+ category: config.category ?? "account",
40
+ tracking: {
41
+ opens: false,
42
+ clicks: false
43
+ }
44
+ }, { idempotencyKey: `authjs_${await sha256Hex(params.token)}` });
45
+ if (result.error) throw new CossSendError(result.error);
46
+ },
47
+ options: {
48
+ id: config.id,
49
+ name: config.name,
50
+ from: config.from,
51
+ maxAge: config.maxAge
52
+ }
53
+ };
54
+ }
55
+ /** Auth.js's own sign-in email, with the host's dots broken so clients don't link them. */
56
+ function defaultEmail({ url, host, theme }) {
57
+ const escapedHost = escapeHtml(host).replace(/\./g, "&#8203;.");
58
+ const brandColor = escapeHtml(theme.brandColor || "#346df1");
59
+ const buttonText = escapeHtml(theme.buttonText || "#fff");
60
+ const html = `<body style="background: #f9f9f9;">
61
+ <table width="100%" border="0" cellspacing="20" cellpadding="0"
62
+ style="background: #fff; max-width: 600px; margin: auto; border-radius: 10px;">
63
+ <tr>
64
+ <td align="center"
65
+ style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #444;">
66
+ Sign in to <strong>${escapedHost}</strong>
67
+ </td>
68
+ </tr>
69
+ <tr>
70
+ <td align="center" style="padding: 20px 0;">
71
+ <table border="0" cellspacing="0" cellpadding="0">
72
+ <tr>
73
+ <td align="center" style="border-radius: 5px;" bgcolor="${brandColor}"><a href="${escapeHtml(url)}"
74
+ target="_blank"
75
+ style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonText}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${brandColor}; display: inline-block; font-weight: bold;">Sign
76
+ in</a></td>
77
+ </tr>
78
+ </table>
79
+ </td>
80
+ </tr>
81
+ <tr>
82
+ <td align="center"
83
+ style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: #444;">
84
+ If you did not request this email you can safely ignore it.
85
+ </td>
86
+ </tr>
87
+ </table>
88
+ </body>
89
+ `;
90
+ return {
91
+ subject: `Sign in to ${host}`,
92
+ html,
93
+ text: `Sign in to ${host}\n${url}\n\n`
94
+ };
95
+ }
96
+ //#endregion
97
+ export { CossProvider, CossProvider as default, CossSendError };
@@ -0,0 +1,128 @@
1
+ import { Va as Email, lo as SendEmailParams } from "./client-Bx6bKUVV.js";
2
+ import { n as IntegrationClientOptions, t as CossSendError } from "./integration-CZAhrrnc.js";
3
+ //#region src/better-auth.d.ts
4
+ /** An email's content: what a template returns. */
5
+ export interface EmailContent {
6
+ subject: string;
7
+ html: string;
8
+ text: string;
9
+ }
10
+ /** The user Better Auth passes to its callbacks (only the fields the templates read). */
11
+ export interface BetterAuthUser {
12
+ email: string;
13
+ name?: string | null;
14
+ }
15
+ export interface VerificationEmailData {
16
+ user: BetterAuthUser;
17
+ url: string;
18
+ token: string;
19
+ }
20
+ export interface ResetPasswordData {
21
+ user: BetterAuthUser;
22
+ url: string;
23
+ token: string;
24
+ }
25
+ export interface ChangeEmailConfirmationData {
26
+ user: BetterAuthUser;
27
+ newEmail: string;
28
+ url: string;
29
+ token: string;
30
+ }
31
+ export interface DeleteAccountVerificationData {
32
+ user: BetterAuthUser;
33
+ url: string;
34
+ token: string;
35
+ }
36
+ export interface MagicLinkData {
37
+ email: string;
38
+ url: string;
39
+ token: string;
40
+ }
41
+ export interface VerificationOtpData {
42
+ email: string;
43
+ otp: string;
44
+ type: "sign-in" | "email-verification" | "forget-password" | "change-email";
45
+ }
46
+ export interface InvitationData {
47
+ id: string;
48
+ email: string;
49
+ role: string;
50
+ organization: {
51
+ name: string;
52
+ };
53
+ inviter: {
54
+ user: BetterAuthUser;
55
+ };
56
+ }
57
+ /** Template data: what Better Auth passed, plus your `appName`. */
58
+ type WithApp<T> = T & {
59
+ appName: string | undefined;
60
+ };
61
+ /** One template per email; each returns the subject and both bodies. */
62
+ export interface CossEmailTemplates {
63
+ verifyEmail: (data: WithApp<VerificationEmailData>) => EmailContent;
64
+ resetPassword: (data: WithApp<ResetPasswordData>) => EmailContent;
65
+ changeEmailConfirmation: (data: WithApp<ChangeEmailConfirmationData>) => EmailContent;
66
+ deleteAccount: (data: WithApp<DeleteAccountVerificationData>) => EmailContent;
67
+ magicLink: (data: WithApp<MagicLinkData>) => EmailContent;
68
+ verificationOtp: (data: WithApp<VerificationOtpData>) => EmailContent;
69
+ invitation: (data: WithApp<InvitationData & {
70
+ url: string;
71
+ }>) => EmailContent;
72
+ }
73
+ export interface CossEmailOptions extends IntegrationClientOptions {
74
+ /** The sender, e.g. `"Acme <auth@acme.com>"`, on a domain verified in coss.com (live keys). */
75
+ from: string;
76
+ /** Your product's name, used by the default templates ("Sign in to Acme"). */
77
+ appName?: string;
78
+ /** Replace any of the default templates. */
79
+ templates?: Partial<CossEmailTemplates>;
80
+ /**
81
+ * The page where an invitee accepts an organization invitation, e.g.
82
+ * `({ id }) => \`https://acme.com/accept-invitation/${id}\``. Better Auth passes no URL to
83
+ * `sendInvitationEmail`, so it needs this.
84
+ */
85
+ invitationUrl?: (data: InvitationData) => string;
86
+ /** Defaults to `"account"`, so suppressions scoped to other categories never block sign-in mail. */
87
+ category?: string;
88
+ }
89
+ /**
90
+ * Parameters of `send`: `from`, `category` and tracking default to the `cossEmail` options. Like
91
+ * `coss.email.send`, a send has its own content or names a template.
92
+ */
93
+ export type CossEmailSendParams = SendEmailParams extends (infer Params) ? Params extends SendEmailParams ? Omit<Params, "from"> & {
94
+ from?: string;
95
+ } : never : never;
96
+ /** The callbacks `cossEmail` returns; pass each to the matching Better Auth option. */
97
+ export interface CossEmail {
98
+ /** `emailVerification.sendVerificationEmail` */
99
+ sendVerificationEmail: (data: VerificationEmailData, request?: unknown) => Promise<void>;
100
+ /** `emailAndPassword.sendResetPassword` */
101
+ sendResetPassword: (data: ResetPasswordData, request?: unknown) => Promise<void>;
102
+ /** `user.changeEmail.sendChangeEmailConfirmation` (sent to the current address) */
103
+ sendChangeEmailConfirmation: (data: ChangeEmailConfirmationData, request?: unknown) => Promise<void>;
104
+ /** `user.deleteUser.sendDeleteAccountVerification` */
105
+ sendDeleteAccountVerification: (data: DeleteAccountVerificationData, request?: unknown) => Promise<void>;
106
+ /** `magicLink({ sendMagicLink })` */
107
+ sendMagicLink: (data: MagicLinkData, ctx?: unknown) => Promise<void>;
108
+ /** `emailOTP({ sendVerificationOTP })` */
109
+ sendVerificationOTP: (data: VerificationOtpData, ctx?: unknown) => Promise<void>;
110
+ /** `organization({ sendInvitationEmail })`; needs the `invitationUrl` option. */
111
+ sendInvitationEmail: (data: InvitationData, request?: unknown) => Promise<void>;
112
+ /** Sends any other email with the same defaults. Throws `CossSendError` on failure. */
113
+ send: (params: CossEmailSendParams, options?: {
114
+ idempotencyKey?: string;
115
+ }) => Promise<Email>;
116
+ }
117
+ /**
118
+ * Better Auth email callbacks that send through coss.com. Each throws `CossSendError` when coss.com
119
+ * refuses the email.
120
+ *
121
+ * Auth emails go out with `category: "account"` and open and click tracking off: click tracking
122
+ * would route the secret links through coss.com's redirect.
123
+ */
124
+ export declare function cossEmail(options: CossEmailOptions): CossEmail;
125
+ /** The default templates: plain, unbranded, with a text version of each. */
126
+ export declare const DEFAULT_TEMPLATES: CossEmailTemplates;
127
+ //#endregion
128
+ export { CossSendError };
@@ -0,0 +1,269 @@
1
+ import { c as CossSendError, l as lazyClient, o as sha256Hex, r as escapeHtml } from "./mime-m5sgODwn.js";
2
+ //#region src/better-auth.ts
3
+ /**
4
+ * `coss/better-auth`: Better Auth's email callbacks, sent through coss.com.
5
+ *
6
+ * ```ts
7
+ * import { betterAuth } from "better-auth";
8
+ * import { cossEmail } from "coss/better-auth";
9
+ *
10
+ * const email = cossEmail({ from: "Acme <auth@acme.com>", appName: "Acme" });
11
+ *
12
+ * export const auth = betterAuth({
13
+ * emailVerification: { sendVerificationEmail: email.sendVerificationEmail },
14
+ * emailAndPassword: { enabled: true, sendResetPassword: email.sendResetPassword },
15
+ * });
16
+ * ```
17
+ *
18
+ * The types here describe only what the callbacks read, so the SDK has no dependency on Better
19
+ * Auth; `test/integration-types.ts` checks them against Better Auth's own.
20
+ */
21
+ /**
22
+ * Better Auth email callbacks that send through coss.com. Each throws `CossSendError` when coss.com
23
+ * refuses the email.
24
+ *
25
+ * Auth emails go out with `category: "account"` and open and click tracking off: click tracking
26
+ * would route the secret links through coss.com's redirect.
27
+ */
28
+ function cossEmail(options) {
29
+ const client = lazyClient("better-auth", options);
30
+ const templates = {
31
+ ...DEFAULT_TEMPLATES,
32
+ ...options.templates
33
+ };
34
+ const appName = options.appName?.trim() || void 0;
35
+ async function send(params, sendOptions) {
36
+ const result = await client().email.send({
37
+ category: options.category ?? "account",
38
+ tracking: {
39
+ opens: false,
40
+ clicks: false
41
+ },
42
+ ...params,
43
+ from: params.from ?? options.from
44
+ }, sendOptions);
45
+ if (result.error) throw new CossSendError(result.error);
46
+ return result.data;
47
+ }
48
+ /**
49
+ * Sends one auth email. A link's token makes a stable idempotency key, so the same callback
50
+ * run twice sends once; the token itself is hashed, never sent as the key.
51
+ */
52
+ async function sendAuthEmail(to, content, token) {
53
+ const idempotencyKey = token ? `better-auth_${await sha256Hex(token)}` : void 0;
54
+ await send({
55
+ to,
56
+ ...content
57
+ }, { idempotencyKey });
58
+ }
59
+ return {
60
+ sendVerificationEmail: (data) => sendAuthEmail(data.user.email, templates.verifyEmail({
61
+ ...data,
62
+ appName
63
+ }), data.token),
64
+ sendResetPassword: (data) => sendAuthEmail(data.user.email, templates.resetPassword({
65
+ ...data,
66
+ appName
67
+ }), data.token),
68
+ sendChangeEmailConfirmation: (data) => sendAuthEmail(data.user.email, templates.changeEmailConfirmation({
69
+ ...data,
70
+ appName
71
+ }), data.token),
72
+ sendDeleteAccountVerification: (data) => sendAuthEmail(data.user.email, templates.deleteAccount({
73
+ ...data,
74
+ appName
75
+ }), data.token),
76
+ sendMagicLink: (data) => sendAuthEmail(data.email, templates.magicLink({
77
+ ...data,
78
+ appName
79
+ }), data.token),
80
+ sendVerificationOTP: (data) => sendAuthEmail(data.email, templates.verificationOtp({
81
+ ...data,
82
+ appName
83
+ })),
84
+ sendInvitationEmail: async (data) => {
85
+ if (!options.invitationUrl) throw new Error("coss/better-auth: sendInvitationEmail needs the `invitationUrl` option (Better Auth passes no URL for invitations).");
86
+ const url = options.invitationUrl(data);
87
+ await sendAuthEmail(data.email, templates.invitation({
88
+ ...data,
89
+ url,
90
+ appName
91
+ }));
92
+ },
93
+ send
94
+ };
95
+ }
96
+ /** A plain, unbranded email: a heading, a paragraph, a button, and a closing line. */
97
+ function layout(parts) {
98
+ const action = parts.action ? `<p style="margin:24px 0"><a href="${escapeHtml(parts.action.url)}" style="display:inline-block;padding:10px 18px;border-radius:6px;background:#111;color:#fff;text-decoration:none;font-weight:600">${escapeHtml(parts.action.label)}</a></p>
99
+ <p style="color:#666;font-size:13px">Or paste this link into your browser:<br><a href="${escapeHtml(parts.action.url)}" style="color:#666;word-break:break-all">${escapeHtml(parts.action.url)}</a></p>` : "";
100
+ const code = parts.code ? `<p style="margin:24px 0;font-size:28px;letter-spacing:6px;font-weight:600;font-family:ui-monospace,Menlo,monospace">${escapeHtml(parts.code)}</p>` : "";
101
+ return `<!doctype html>
102
+ <html><body style="margin:0;padding:24px;background:#fff;color:#111;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.5">
103
+ <div style="max-width:480px;margin:0 auto">
104
+ <h1 style="font-size:20px;margin:0 0 16px">${escapeHtml(parts.heading)}</h1>
105
+ <p>${escapeHtml(parts.intro)}</p>
106
+ ${action}${code}
107
+ <p style="color:#666;font-size:13px">${escapeHtml(parts.outro)}</p>
108
+ </div>
109
+ </body></html>`;
110
+ }
111
+ function text(lines) {
112
+ return `${lines.filter((line) => line !== void 0).join("\n\n")}\n`;
113
+ }
114
+ const IGNORE = "If you didn't ask for this, you can ignore this email.";
115
+ function on(appName) {
116
+ return appName ? ` on ${appName}` : "";
117
+ }
118
+ const OTP_SUBJECTS = {
119
+ "sign-in": "Your sign-in code",
120
+ "email-verification": "Your verification code",
121
+ "forget-password": "Your password reset code",
122
+ "change-email": "Your code to change your email address"
123
+ };
124
+ /** The default templates: plain, unbranded, with a text version of each. */
125
+ const DEFAULT_TEMPLATES = {
126
+ verifyEmail: ({ url, appName }) => {
127
+ const intro = `Confirm this is your email address to finish setting up your account${on(appName)}.`;
128
+ return {
129
+ subject: "Verify your email address",
130
+ html: layout({
131
+ heading: "Verify your email address",
132
+ intro,
133
+ action: {
134
+ label: "Verify email",
135
+ url
136
+ },
137
+ outro: IGNORE
138
+ }),
139
+ text: text([
140
+ intro,
141
+ url,
142
+ IGNORE
143
+ ])
144
+ };
145
+ },
146
+ resetPassword: ({ url, appName }) => {
147
+ const intro = `Someone asked to reset the password of your account${on(appName)}. Choose a new one with this link.`;
148
+ return {
149
+ subject: "Reset your password",
150
+ html: layout({
151
+ heading: "Reset your password",
152
+ intro,
153
+ action: {
154
+ label: "Reset password",
155
+ url
156
+ },
157
+ outro: `${IGNORE} Your password won't change.`
158
+ }),
159
+ text: text([
160
+ intro,
161
+ url,
162
+ `${IGNORE} Your password won't change.`
163
+ ])
164
+ };
165
+ },
166
+ changeEmailConfirmation: ({ newEmail, url, appName }) => {
167
+ const intro = `Someone asked to change the email address of your account${on(appName)} to ${newEmail}. Confirm the change with this link.`;
168
+ return {
169
+ subject: "Confirm your new email address",
170
+ html: layout({
171
+ heading: "Confirm your new email address",
172
+ intro,
173
+ action: {
174
+ label: "Confirm change",
175
+ url
176
+ },
177
+ outro: IGNORE
178
+ }),
179
+ text: text([
180
+ intro,
181
+ url,
182
+ IGNORE
183
+ ])
184
+ };
185
+ },
186
+ deleteAccount: ({ url, appName }) => {
187
+ const intro = `Someone asked to delete your account${on(appName)}. This can't be undone. Confirm with this link.`;
188
+ return {
189
+ subject: "Confirm account deletion",
190
+ html: layout({
191
+ heading: "Confirm account deletion",
192
+ intro,
193
+ action: {
194
+ label: "Delete my account",
195
+ url
196
+ },
197
+ outro: `${IGNORE} Your account stays as it is.`
198
+ }),
199
+ text: text([
200
+ intro,
201
+ url,
202
+ `${IGNORE} Your account stays as it is.`
203
+ ])
204
+ };
205
+ },
206
+ magicLink: ({ url, appName }) => {
207
+ const heading = appName ? `Sign in to ${appName}` : "Sign in";
208
+ const intro = "Use this link to sign in. It works once and expires soon.";
209
+ return {
210
+ subject: heading,
211
+ html: layout({
212
+ heading,
213
+ intro,
214
+ action: {
215
+ label: "Sign in",
216
+ url
217
+ },
218
+ outro: IGNORE
219
+ }),
220
+ text: text([
221
+ intro,
222
+ url,
223
+ IGNORE
224
+ ])
225
+ };
226
+ },
227
+ verificationOtp: ({ otp, type, appName }) => {
228
+ const subject = OTP_SUBJECTS[type] ?? "Your verification code";
229
+ const intro = `Enter this code${on(appName)}. It expires in a few minutes.`;
230
+ return {
231
+ subject,
232
+ html: layout({
233
+ heading: subject,
234
+ intro,
235
+ code: otp,
236
+ outro: IGNORE
237
+ }),
238
+ text: text([
239
+ intro,
240
+ otp,
241
+ IGNORE
242
+ ])
243
+ };
244
+ },
245
+ invitation: ({ organization, inviter, url, appName }) => {
246
+ const who = inviter.user.name?.trim() || inviter.user.email;
247
+ const subject = `Join ${organization.name}${on(appName)}`;
248
+ const intro = `${who} invited you to join ${organization.name}${on(appName)}.`;
249
+ return {
250
+ subject,
251
+ html: layout({
252
+ heading: subject,
253
+ intro,
254
+ action: {
255
+ label: "Accept invitation",
256
+ url
257
+ },
258
+ outro: "If you weren't expecting this invitation, you can ignore this email."
259
+ }),
260
+ text: text([
261
+ intro,
262
+ url,
263
+ "If you weren't expecting this invitation, you can ignore this email."
264
+ ])
265
+ };
266
+ }
267
+ };
268
+ //#endregion
269
+ export { CossSendError, DEFAULT_TEMPLATES, cossEmail };