groundmail 0.0.0-stage → 0.14.1

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
@@ -1,3 +1,49 @@
1
- # Temporary Holding Version
1
+ # groundmail
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official Node.js / TypeScript client for the [Groundmail](https://groundmail.dev) transactional email API.
4
+
5
+ ```bash
6
+ npm install groundmail
7
+ ```
8
+
9
+ ```bash
10
+ export GROUNDMAIL_API_KEY=gm_your_key_here
11
+ ```
12
+
13
+ ```ts
14
+ import { Groundmail } from 'groundmail'
15
+
16
+ const tx = new Groundmail(process.env.GROUNDMAIL_API_KEY!)
17
+
18
+ const email = await tx.emails.send({
19
+ from: 'ops@yourdomain.com',
20
+ to: 'user@example.com',
21
+ subject: 'Your receipt',
22
+ html: '<p>Thanks for your purchase.</p>',
23
+ })
24
+
25
+ console.log(email.id, email.status)
26
+ ```
27
+
28
+ ## Adapters
29
+
30
+ Optional entry points (install peer deps as needed):
31
+
32
+ | Import | Package / peer |
33
+ |--------|----------------|
34
+ | `groundmail/nodemailer` | [groundmail-nodemailer](https://www.npmjs.com/package/groundmail-nodemailer) + `nodemailer` |
35
+ | `groundmail/better-auth` | [groundmail-better-auth](https://www.npmjs.com/package/groundmail-better-auth) |
36
+ | `groundmail/authjs` | [groundmail-authjs](https://www.npmjs.com/package/groundmail-authjs) |
37
+ | `groundmail/clerk` | [groundmail-clerk](https://www.npmjs.com/package/groundmail-clerk) |
38
+ | `groundmail/payload` | [groundmail-payload](https://www.npmjs.com/package/groundmail-payload) |
39
+
40
+ ## Docs
41
+
42
+ - Quickstart: https://docs.groundmail.dev/quickstart
43
+ - Node / Nodemailer: https://docs.groundmail.dev/frameworks/node
44
+ - API reference: https://docs.groundmail.dev/api/emails
45
+ - Product: https://groundmail.dev
46
+
47
+ ## License
48
+
49
+ MIT
@@ -0,0 +1,72 @@
1
+ import { G as Groundmail } from './client-DdrSVDkA.js';
2
+
3
+ /** Subset of Auth.js theme used for the default magic-link email. */
4
+ type AuthjsTheme = {
5
+ brandColor?: string;
6
+ buttonText?: string;
7
+ };
8
+ /**
9
+ * Minimal Auth.js email-provider config shape.
10
+ * Compatible with `next-auth` / `@auth/core` `EmailConfig` without requiring those packages at build time.
11
+ */
12
+ type AuthjsEmailConfig = {
13
+ id: string;
14
+ type: 'email';
15
+ name: string;
16
+ from: string;
17
+ maxAge: number;
18
+ apiKey?: string;
19
+ sendVerificationRequest: (params: {
20
+ identifier: string;
21
+ url: string;
22
+ expires: Date;
23
+ provider: AuthjsEmailConfig;
24
+ token: string;
25
+ theme: AuthjsTheme;
26
+ request: Request;
27
+ }) => Promise<void>;
28
+ options: GroundmailAuthjsOptions;
29
+ };
30
+ type GroundmailAuthjsOptions = {
31
+ /** Verified sender, e.g. `Auth <noreply@yourdomain.com>`. Defaults to a placeholder that you must override for production. */
32
+ from?: string;
33
+ /** Groundmail API key (`gm_…`). Falls back to `AUTH_GROUNDMAIL_KEY` then `GROUNDMAIL_API_KEY`. */
34
+ apiKey?: string;
35
+ /** Override API base URL (tests / self-hosted). */
36
+ baseUrl?: string;
37
+ /** Injected client (tests). */
38
+ client?: Groundmail;
39
+ /** Token lifetime in seconds (Auth.js default is 24h). */
40
+ maxAge?: number;
41
+ /** Override the default HTML/text templates. */
42
+ sendVerificationRequest?: AuthjsEmailConfig['sendVerificationRequest'];
43
+ /** Provider id in Auth.js (default `groundmail`). */
44
+ id?: string;
45
+ /** Display name in Auth.js (default `Groundmail`). */
46
+ name?: string;
47
+ };
48
+ /**
49
+ * Auth.js / NextAuth email provider for Groundmail.
50
+ *
51
+ * Drop-in for magic-link sign-in. Requires a database adapter (Auth.js stores
52
+ * verification tokens).
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import NextAuth from 'next-auth'
57
+ * import Groundmail from 'groundmail-authjs'
58
+ *
59
+ * export const { handlers, auth, signIn, signOut } = NextAuth({
60
+ * adapter: ...,
61
+ * providers: [
62
+ * Groundmail({
63
+ * from: 'noreply@yourdomain.com',
64
+ * // apiKey: process.env.GROUNDMAIL_API_KEY, // or AUTH_GROUNDMAIL_KEY
65
+ * }),
66
+ * ],
67
+ * })
68
+ * ```
69
+ */
70
+ declare function GroundmailAuthjs(config?: GroundmailAuthjsOptions): AuthjsEmailConfig;
71
+
72
+ export { type AuthjsEmailConfig, type AuthjsTheme, GroundmailAuthjs, type GroundmailAuthjsOptions, GroundmailAuthjs as default };
package/dist/authjs.js ADDED
@@ -0,0 +1,101 @@
1
+ import {
2
+ DEFAULT_API_BASE_URL,
3
+ Groundmail
4
+ } from "./chunk-IZYE7HGT.js";
5
+
6
+ // src/authjs.ts
7
+ function escapeHtml(value) {
8
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
9
+ }
10
+ function signInHtml(params) {
11
+ const { url, host, theme } = params;
12
+ const escapedHost = escapeHtml(host.replace(/\./g, ".\u200B"));
13
+ const brandColor = theme.brandColor || "#0f766e";
14
+ const buttonText = theme.buttonText || "#fff";
15
+ const safeUrl = escapeHtml(url);
16
+ return `
17
+ <body style="background:#f8fafc;">
18
+ <table width="100%" border="0" cellspacing="20" cellpadding="0"
19
+ style="background:#ffffff; max-width:600px; margin:auto; border-radius:10px;">
20
+ <tr>
21
+ <td align="center"
22
+ style="padding:10px 0; font-size:22px; font-family:Helvetica,Arial,sans-serif; color:#334155;">
23
+ Sign in to <strong>${escapedHost}</strong>
24
+ </td>
25
+ </tr>
26
+ <tr>
27
+ <td align="center" style="padding:20px 0;">
28
+ <a href="${safeUrl}" target="_blank"
29
+ 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; background:${brandColor};">
30
+ Sign in
31
+ </a>
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td align="center"
36
+ style="padding:0 0 10px; font-size:16px; line-height:22px; font-family:Helvetica,Arial,sans-serif; color:#64748b;">
37
+ If you did not request this email you can safely ignore it.
38
+ </td>
39
+ </tr>
40
+ </table>
41
+ </body>
42
+ `;
43
+ }
44
+ function signInText(params) {
45
+ return `Sign in to ${params.host}
46
+ ${params.url}
47
+
48
+ `;
49
+ }
50
+ function resolveApiKey(options) {
51
+ return options.apiKey ?? (typeof process !== "undefined" ? process.env.AUTH_GROUNDMAIL_KEY ?? process.env.GROUNDMAIL_API_KEY : void 0);
52
+ }
53
+ function GroundmailAuthjs(config = {}) {
54
+ const apiKey = resolveApiKey(config);
55
+ const from = (config.from ?? "Groundmail <noreply@mail.groundmail.dev>").trim();
56
+ if (!from) {
57
+ throw new Error("GroundmailAuthjs requires a non-empty `from` address");
58
+ }
59
+ const provider = {
60
+ id: config.id ?? "groundmail",
61
+ type: "email",
62
+ name: config.name ?? "Groundmail",
63
+ from,
64
+ maxAge: config.maxAge ?? 24 * 60 * 60,
65
+ apiKey,
66
+ options: config,
67
+ async sendVerificationRequest(params) {
68
+ if (config.sendVerificationRequest) {
69
+ await config.sendVerificationRequest(params);
70
+ return;
71
+ }
72
+ const { identifier: to, provider: prov, url, theme } = params;
73
+ const client = config.client ?? (() => {
74
+ const key = prov.apiKey ?? resolveApiKey(config);
75
+ if (!key) {
76
+ throw new Error(
77
+ "Groundmail Auth.js provider requires `apiKey`, AUTH_GROUNDMAIL_KEY, or GROUNDMAIL_API_KEY"
78
+ );
79
+ }
80
+ return new Groundmail(key, {
81
+ baseUrl: config.baseUrl ?? DEFAULT_API_BASE_URL
82
+ });
83
+ })();
84
+ const { host } = new URL(url);
85
+ await client.emails.send({
86
+ from: prov.from,
87
+ to,
88
+ subject: `Sign in to ${host}`,
89
+ html: signInHtml({ url, host, theme }),
90
+ text: signInText({ url, host }),
91
+ tags: [{ name: "auth", value: "authjs-magic-link" }]
92
+ });
93
+ }
94
+ };
95
+ return provider;
96
+ }
97
+ var authjs_default = GroundmailAuthjs;
98
+ export {
99
+ GroundmailAuthjs,
100
+ authjs_default as default
101
+ };
@@ -0,0 +1,122 @@
1
+ import { S as SendEmailResult, G as Groundmail } from './client-DdrSVDkA.js';
2
+
3
+ type BetterAuthEmailUser = {
4
+ email: string;
5
+ name?: string | null;
6
+ id?: string;
7
+ };
8
+ type BetterAuthEmailContext = {
9
+ user: BetterAuthEmailUser;
10
+ url: string;
11
+ token?: string;
12
+ };
13
+ type BetterAuthMagicLinkContext = {
14
+ email: string;
15
+ url: string;
16
+ token?: string;
17
+ metadata?: unknown;
18
+ };
19
+ type BetterAuthEmailOtpType = 'sign-in' | 'email-verification' | 'forget-password' | string;
20
+ type BetterAuthEmailOtpContext = {
21
+ email: string;
22
+ otp: string;
23
+ type: BetterAuthEmailOtpType;
24
+ };
25
+ type BetterAuthInvitationContext = {
26
+ email: string;
27
+ id?: string;
28
+ role?: string;
29
+ organization?: {
30
+ name?: string | null;
31
+ };
32
+ inviter?: {
33
+ user?: {
34
+ name?: string | null;
35
+ email?: string | null;
36
+ };
37
+ };
38
+ invitation?: {
39
+ id?: string;
40
+ };
41
+ };
42
+ type BetterAuthEmailOptions = {
43
+ /** Verified sender, e.g. `noreply@yourdomain.com`. */
44
+ from: string;
45
+ apiKey?: string;
46
+ baseUrl?: string;
47
+ /** Prebuilt client (tests / advanced wiring). */
48
+ client?: Groundmail;
49
+ /**
50
+ * When true (default), callbacks do not await the API call so Better Auth
51
+ * request timing is less coupled to delivery. Errors are logged.
52
+ */
53
+ fireAndForget?: boolean;
54
+ /** Base URL used to build organization invite links when only an id is provided. */
55
+ appUrl?: string;
56
+ };
57
+ type BetterAuthEmailHelpers = {
58
+ /** Low-level send used by the Better Auth callbacks. */
59
+ sendAuthEmail: (params: {
60
+ to: string;
61
+ subject: string;
62
+ html: string;
63
+ text?: string;
64
+ tag?: string;
65
+ }) => Promise<SendEmailResult>;
66
+ /** Core: `emailVerification.sendVerificationEmail` */
67
+ sendVerificationEmail: (ctx: BetterAuthEmailContext, request?: unknown) => Promise<void>;
68
+ /** Core: `emailAndPassword.sendResetPassword` */
69
+ sendResetPassword: (ctx: BetterAuthEmailContext, request?: unknown) => Promise<void>;
70
+ /** Plugin: `magicLink({ sendMagicLink })` — see https://www.better-auth.com/docs/plugins/magic-link */
71
+ sendMagicLink: (ctx: BetterAuthMagicLinkContext, request?: unknown) => Promise<void>;
72
+ /** Plugin: `emailOTP({ sendVerificationOTP })` — see https://better-auth.com/docs/plugins/email-otp */
73
+ sendVerificationOTP: (ctx: BetterAuthEmailOtpContext, request?: unknown) => Promise<void>;
74
+ /** Plugin: `organization({ sendInvitationEmail })` */
75
+ sendInvitationEmail: (ctx: BetterAuthInvitationContext, request?: unknown) => Promise<void>;
76
+ /**
77
+ * Generic link email for custom flows.
78
+ * Pass your own subject + tag.
79
+ */
80
+ sendLinkEmail: (ctx: BetterAuthEmailContext & {
81
+ subject: string;
82
+ tag?: string;
83
+ }, request?: unknown) => Promise<void>;
84
+ };
85
+ /**
86
+ * Groundmail helpers for Better Auth email callbacks.
87
+ *
88
+ * Better Auth’s [plugin catalog](https://better-auth.com/docs/plugins) covers
89
+ * Magic Link, Email OTP, Organization, etc. There is no third-party “email
90
+ * provider” plugin slot. Delivery is always a callback you supply on core
91
+ * config and on those plugins. This helper returns those callbacks.
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * import { betterAuth } from 'better-auth'
96
+ * import { magicLink } from 'better-auth/plugins'
97
+ * import { emailOTP } from 'better-auth/plugins'
98
+ * import { createBetterAuthEmail } from 'groundmail-better-auth'
99
+ *
100
+ * const email = createBetterAuthEmail({
101
+ * from: 'noreply@yourdomain.com',
102
+ * apiKey: process.env.GROUNDMAIL_API_KEY!,
103
+ * })
104
+ *
105
+ * export const auth = betterAuth({
106
+ * emailVerification: {
107
+ * sendVerificationEmail: email.sendVerificationEmail,
108
+ * },
109
+ * emailAndPassword: {
110
+ * enabled: true,
111
+ * sendResetPassword: email.sendResetPassword,
112
+ * },
113
+ * plugins: [
114
+ * magicLink({ sendMagicLink: email.sendMagicLink }),
115
+ * emailOTP({ sendVerificationOTP: email.sendVerificationOTP }),
116
+ * ],
117
+ * })
118
+ * ```
119
+ */
120
+ declare function createBetterAuthEmail(options: BetterAuthEmailOptions): BetterAuthEmailHelpers;
121
+
122
+ export { type BetterAuthEmailContext, type BetterAuthEmailHelpers, type BetterAuthEmailOptions, type BetterAuthEmailOtpContext, type BetterAuthEmailOtpType, type BetterAuthEmailUser, type BetterAuthInvitationContext, type BetterAuthMagicLinkContext, createBetterAuthEmail };
@@ -0,0 +1,148 @@
1
+ import {
2
+ DEFAULT_API_BASE_URL,
3
+ Groundmail
4
+ } from "./chunk-IZYE7HGT.js";
5
+
6
+ // src/better-auth.ts
7
+ function linkHtml(label, url) {
8
+ const safe = escapeHtml(url);
9
+ return `<p>${escapeHtml(label)}: <a href="${safe}">${safe}</a></p>`;
10
+ }
11
+ function escapeHtml(value) {
12
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
13
+ }
14
+ function otpSubject(type) {
15
+ switch (type) {
16
+ case "sign-in":
17
+ return "Your sign-in code";
18
+ case "email-verification":
19
+ return "Your verification code";
20
+ case "forget-password":
21
+ return "Your password reset code";
22
+ default:
23
+ return "Your verification code";
24
+ }
25
+ }
26
+ function createBetterAuthEmail(options) {
27
+ const from = options.from.trim();
28
+ if (!from) {
29
+ throw new Error("createBetterAuthEmail requires a non-empty `from` address");
30
+ }
31
+ let client = options.client;
32
+ if (!client) {
33
+ const apiKey = options.apiKey ?? process.env.GROUNDMAIL_API_KEY;
34
+ if (!apiKey) {
35
+ throw new Error("createBetterAuthEmail requires `apiKey` or GROUNDMAIL_API_KEY");
36
+ }
37
+ client = new Groundmail(apiKey, {
38
+ baseUrl: options.baseUrl ?? DEFAULT_API_BASE_URL
39
+ });
40
+ }
41
+ const fireAndForget = options.fireAndForget !== false;
42
+ const appUrl = options.appUrl?.replace(/\/$/, "");
43
+ const sendAuthEmail = async (params) => {
44
+ return client.emails.send({
45
+ from,
46
+ to: params.to,
47
+ subject: params.subject,
48
+ html: params.html,
49
+ text: params.text,
50
+ tags: params.tag ? [{ name: "auth", value: params.tag }] : [{ name: "auth", value: "transactional" }]
51
+ });
52
+ };
53
+ const run = async (work) => {
54
+ if (!fireAndForget) {
55
+ await work();
56
+ return;
57
+ }
58
+ void work().catch((error) => {
59
+ console.error("[groundmail/better-auth] failed to send auth email", error);
60
+ });
61
+ };
62
+ const sendLinkEmail = async (ctx) => {
63
+ const to = ctx.user.email?.trim();
64
+ if (!to) throw new Error("Better Auth user is missing an email address");
65
+ await run(
66
+ () => sendAuthEmail({
67
+ to,
68
+ subject: ctx.subject,
69
+ html: linkHtml(ctx.subject, ctx.url),
70
+ text: `${ctx.subject}: ${ctx.url}`,
71
+ tag: ctx.tag
72
+ })
73
+ );
74
+ };
75
+ return {
76
+ sendAuthEmail,
77
+ sendVerificationEmail: async (ctx) => {
78
+ await sendLinkEmail({
79
+ ...ctx,
80
+ subject: "Verify your email",
81
+ tag: "verify"
82
+ });
83
+ },
84
+ sendResetPassword: async (ctx) => {
85
+ await sendLinkEmail({
86
+ ...ctx,
87
+ subject: "Reset your password",
88
+ tag: "reset"
89
+ });
90
+ },
91
+ sendMagicLink: async (ctx) => {
92
+ const to = ctx.email?.trim();
93
+ if (!to) throw new Error("Magic link email is missing");
94
+ await run(
95
+ () => sendAuthEmail({
96
+ to,
97
+ subject: "Your sign-in link",
98
+ html: linkHtml("Sign in", ctx.url),
99
+ text: `Sign in: ${ctx.url}`,
100
+ tag: "magic-link"
101
+ })
102
+ );
103
+ },
104
+ sendVerificationOTP: async (ctx) => {
105
+ const to = ctx.email?.trim();
106
+ if (!to) throw new Error("OTP email is missing");
107
+ const subject = otpSubject(ctx.type);
108
+ const otp = String(ctx.otp ?? "").trim();
109
+ if (!otp) throw new Error("OTP value is missing");
110
+ await run(
111
+ () => sendAuthEmail({
112
+ to,
113
+ subject,
114
+ html: `<p>Your code is <strong>${escapeHtml(otp)}</strong></p>`,
115
+ text: `Your code is ${otp}`,
116
+ tag: `otp-${ctx.type}`
117
+ })
118
+ );
119
+ },
120
+ sendInvitationEmail: async (ctx) => {
121
+ const to = ctx.email?.trim();
122
+ if (!to) throw new Error("Invitation email is missing");
123
+ const inviteId = ctx.id ?? ctx.invitation?.id;
124
+ const inviteLink = inviteId && appUrl ? `${appUrl}/accept-invitation/${inviteId}` : inviteId ? `/accept-invitation/${inviteId}` : null;
125
+ if (!inviteLink) {
126
+ throw new Error(
127
+ "sendInvitationEmail needs an invitation id (and ideally `appUrl` on createBetterAuthEmail)"
128
+ );
129
+ }
130
+ const org = ctx.organization?.name?.trim() || "the organization";
131
+ const inviter = ctx.inviter?.user?.name?.trim() || ctx.inviter?.user?.email?.trim() || "A teammate";
132
+ const subject = `Join ${org}`;
133
+ await run(
134
+ () => sendAuthEmail({
135
+ to,
136
+ subject,
137
+ html: `<p>${escapeHtml(inviter)} invited you to join <strong>${escapeHtml(org)}</strong>.</p>${linkHtml("Accept invitation", inviteLink)}`,
138
+ text: `${inviter} invited you to join ${org}. Accept: ${inviteLink}`,
139
+ tag: "invite"
140
+ })
141
+ );
142
+ },
143
+ sendLinkEmail
144
+ };
145
+ }
146
+ export {
147
+ createBetterAuthEmail
148
+ };