coss 0.4.0 → 0.5.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 +23 -0
- package/dist/authjs.d.ts +64 -0
- package/dist/authjs.js +97 -0
- package/dist/better-auth.d.ts +128 -0
- package/dist/better-auth.js +269 -0
- package/dist/client-CCVBtkpV.js +2364 -0
- package/dist/client-avnHVMry.d.ts +3850 -0
- package/dist/index.d.ts +3 -3849
- package/dist/index.js +1 -2355
- package/dist/integration-p3vtLNgq.d.ts +35 -0
- package/dist/mime-C8bhhjvD.js +152 -0
- package/dist/nodemailer.d.ts +75 -0
- package/dist/nodemailer.js +193 -0
- package/package.json +20 -2
package/README.md
CHANGED
|
@@ -391,6 +391,29 @@ To make a send safe across process restarts too, pass your own key, derived from
|
|
|
391
391
|
cancel a call. Aborting stops the request in flight and any pending retry, and returns
|
|
392
392
|
`{ type: "connection_error", code: "aborted" }`.
|
|
393
393
|
|
|
394
|
+
## Integrations
|
|
395
|
+
|
|
396
|
+
To send from libraries you already use, import the matching entry. They need no other package
|
|
397
|
+
than the library itself, and they throw a `CossSendError` when coss.com refuses an email.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
// Nodemailer
|
|
401
|
+
import nodemailer from "nodemailer";
|
|
402
|
+
import { cossTransport } from "coss/nodemailer";
|
|
403
|
+
const transporter = nodemailer.createTransport(cossTransport());
|
|
404
|
+
|
|
405
|
+
// Better Auth: verification, password reset, magic link, one-time code and invitation emails
|
|
406
|
+
import { cossEmail } from "coss/better-auth";
|
|
407
|
+
const email = cossEmail({ from: "Acme <auth@acme.com>", appName: "Acme" });
|
|
408
|
+
// betterAuth({ emailVerification: { sendVerificationEmail: email.sendVerificationEmail }, … })
|
|
409
|
+
|
|
410
|
+
// Auth.js (NextAuth.js v5): the email provider
|
|
411
|
+
import Coss from "coss/authjs";
|
|
412
|
+
// NextAuth({ adapter, providers: [Coss({ from: "Acme <auth@acme.com>" })] })
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Guides: [coss.com/docs/integrations](https://coss.com/docs/integrations).
|
|
416
|
+
|
|
394
417
|
## Runtime support
|
|
395
418
|
|
|
396
419
|
The SDK works on Node.js ≥ 22, Bun, Deno and Cloudflare Workers. It only needs the standard `fetch`,
|
package/dist/authjs.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { n as IntegrationClientOptions, t as CossSendError } from "./integration-p3vtLNgq.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-C8bhhjvD.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, "​.");
|
|
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 { Ci as Email, Wi as SendEmailParams } from "./client-avnHVMry.js";
|
|
2
|
+
import { n as IntegrationClientOptions, t as CossSendError } from "./integration-p3vtLNgq.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-C8bhhjvD.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 };
|