coss 0.3.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.
@@ -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 };