@protobase/server 0.23.8 → 0.23.10

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.
@@ -1,10 +1,10 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailButton, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailButton, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { signInPath } from "@protobase/schema";
4
4
  import { Text } from "react-email";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  //#region src/better-auth/auth-mail/account-exists-email.tsx
7
- const AccountExistsEmail = ({ app }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
7
+ const AccountExistsEmail = ({ app }) => /* @__PURE__ */ jsxs(EmailLayout, {
8
8
  app,
9
9
  preview: `Sign in to ${app.name} with this address instead`,
10
10
  heading: "You already have an account",
@@ -1,11 +1,11 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailButton, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailButton, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/invitation-email.tsx
6
6
  const InvitationEmail = ({ app, inviter, organization, url, expiresAt }) => {
7
7
  const target = organization ?? app.name;
8
- return /* @__PURE__ */ jsxs(AuthEmailLayout, {
8
+ return /* @__PURE__ */ jsxs(EmailLayout, {
9
9
  app,
10
10
  preview: `${inviter} invites you to join ${target}`,
11
11
  heading: `Join ${target}`,
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailButton, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailButton, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/reset-password-email.tsx
6
- const ResetPasswordEmail = ({ app, url, lifetime }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const ResetPasswordEmail = ({ app, url, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `Choose a new password for ${app.name}`,
9
9
  heading: "Reset your password",
@@ -1,4 +1,4 @@
1
- import { mailingApp } from "./mailing-app.js";
1
+ import { mailingApp } from "../../mail/mailing-app.js";
2
2
  //#region src/better-auth/auth-mail/send-auth-email.ts
3
3
  /**
4
4
  * Renders the auth email `name` for `data`, with the app it comes from on the origin of `requestURL` (see `mailingApp`),
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailCode, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailCode, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/sign-in-code-email.tsx
6
- const SignInCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const SignInCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `Your code to sign in to ${app.name}`,
9
9
  heading: "Your sign-in code",
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailCode, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailCode, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/sign-up-code-email.tsx
6
- const SignUpCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const SignUpCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `Your code to create an account with ${app.name}`,
9
9
  heading: "Create your account",
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/staff-sign-in-email.tsx
6
- const StaffSignInEmail = ({ app, staff, reason, expiresAt }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const StaffSignInEmail = ({ app, staff, reason, expiresAt }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `${staff} signed in as you to help`,
9
9
  heading: "Staff signed in as you",
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../../mail/define-email.js";
2
- import { AuthEmailLayout, EmailCode, EmailNote } from "./auth-email-layout.js";
2
+ import { EmailCode, EmailLayout, EmailNote } from "../../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/better-auth/auth-mail/two-factor-code-email.tsx
6
- const TwoFactorCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const TwoFactorCodeEmail = ({ app, code, lifetime }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `Your code to finish signing in to ${app.name}`,
9
9
  heading: "Finish signing in",
@@ -1,9 +1,9 @@
1
1
  import { defineEmail } from "../mail/define-email.js";
2
- import { AuthEmailLayout, EmailButton, EmailNote } from "../better-auth/auth-mail/auth-email-layout.js";
2
+ import { EmailButton, EmailLayout, EmailNote } from "../mail/email-layout.js";
3
3
  import { Text } from "react-email";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
5
  //#region src/comments/mention-email.tsx
6
- const MentionEmail = ({ app, author, record, body, url }) => /* @__PURE__ */ jsxs(AuthEmailLayout, {
6
+ const MentionEmail = ({ app, author, record, body, url }) => /* @__PURE__ */ jsxs(EmailLayout, {
7
7
  app,
8
8
  preview: `${author} mentioned you on ${record}`,
9
9
  heading: `${author} mentioned you`,
@@ -0,0 +1,3 @@
1
+ import { MailingApp } from "./mail/mailing-app.js";
2
+ import { EmailButton, EmailCode, EmailLayout, EmailLayoutProps, EmailNote } from "./mail/email-layout.js";
3
+ export { EmailButton, EmailCode, EmailLayout, type EmailLayoutProps, EmailNote, type MailingApp };
package/dist/email.js ADDED
@@ -0,0 +1,2 @@
1
+ import { EmailButton, EmailCode, EmailLayout, EmailNote } from "./mail/email-layout.js";
2
+ export { EmailButton, EmailCode, EmailLayout, EmailNote };
@@ -21,6 +21,20 @@ const queueOptions = (options) => {
21
21
  /** The queue settings `queueOptions` sets, as `getQueue` reads them back, so a changed definition is noticed. */
22
22
  const queueSettingsDiffer = (wanted, stored) => Object.keys(wanted).some((key) => key !== "policy" && stored[key] !== wanted[key]);
23
23
  /**
24
+ * The pg-boss worker options of a job: one job per fetch, `concurrency` at once. A worker fetches again at once after a
25
+ * fetch that found a job while more are ready than its workers take in one poll, by the queue's ready count that the
26
+ * queue maintenance refreshes about every minute, so a backlog drains as fast as its jobs run rather than one job per
27
+ * worker each poll.
28
+ */
29
+ const workOptions = (concurrency, pollSeconds) => ({
30
+ batchSize: 1,
31
+ localConcurrency: concurrency,
32
+ pollingIntervalSeconds: pollSeconds,
33
+ burstWhenReadyExceeds: concurrency,
34
+ includeMetadata: true,
35
+ perJobResults: true
36
+ });
37
+ /**
24
38
  * The pg-boss send options of one enqueue: when, how urgent, and the key a waiting job is found by. An ordered job's
25
39
  * group is its key in pg-boss, which keeps one group's jobs in order.
26
40
  */
@@ -41,4 +55,4 @@ const sendOptions = (options, enqueue = {}) => {
41
55
  };
42
56
  };
43
57
  //#endregion
44
- export { queueOptions, queueSettingsDiffer, sendOptions };
58
+ export { queueOptions, queueSettingsDiffer, sendOptions, workOptions };
@@ -5,7 +5,7 @@ import { runJob, validatePayload } from "./run-job.js";
5
5
  import { platformMailer } from "../mail/platform-mailer.js";
6
6
  import { runInBackground } from "../background-work.js";
7
7
  import { jobsDatabase } from "./database.js";
8
- import { queueOptions, queueSettingsDiffer, sendOptions } from "./options.js";
8
+ import { queueOptions, queueSettingsDiffer, sendOptions, workOptions } from "./options.js";
9
9
  import { createSchedules } from "./schedules.js";
10
10
  import { sql } from "kysely";
11
11
  import { PgBoss } from "pg-boss";
@@ -50,13 +50,7 @@ const startJobs = async ({ db, jobs, work = true, pollSeconds = 2, shutdownSecon
50
50
  const running = workers.get(definition.name);
51
51
  if (running?.concurrency === concurrency) return;
52
52
  if (running) await boss.offWork(definition.name);
53
- const id = await boss.work(definition.name, {
54
- batchSize: 1,
55
- localConcurrency: concurrency,
56
- pollingIntervalSeconds: pollSeconds,
57
- includeMetadata: true,
58
- perJobResults: true
59
- }, async ([job]) => runInBackground(async () => {
53
+ const id = await boss.work(definition.name, workOptions(concurrency, pollSeconds), async ([job]) => runInBackground(async () => {
60
54
  const current = definitions.get(definition.name) ?? definition;
61
55
  if (current.options.schedule !== void 0 && job.singletonKey === "protobase.schedule") await schedules.following(current, job.startAfter);
62
56
  return [await runJob(current, job, db, mail)];
@@ -10,14 +10,20 @@ export type EmailOptions<Data extends object> = {
10
10
  * It is rendered to the HTML part, and the plain-text part is made from that HTML.
11
11
  */
12
12
  component: EmailComponent<Data>;
13
+ /**
14
+ * More header fields, or how to make them from the email's data: such as `List-Unsubscribe` with the recipient's own
15
+ * unsubscribe link, and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`, which mailbox providers expect of bulk mail.
16
+ */
17
+ headers?: Record<string, string> | ((data: Data) => Record<string, string>);
13
18
  };
14
19
  /** A function component of the email's data, as JSX writes one; typed here so the server's own types need no React. */
15
20
  export type EmailComponent<Data> = (props: Data) => unknown;
16
- /** An email as `render` gives it: the subject and both parts of the body. */
21
+ /** An email as `render` gives it: the subject, both parts of the body, and its header fields when it has any. */
17
22
  export type RenderedEmail = {
18
23
  subject: string;
19
24
  html: string;
20
25
  text: string;
26
+ headers?: Record<string, string>;
21
27
  };
22
28
  export type EmailTemplate<Data extends object> = {
23
29
  readonly name: string;
@@ -18,10 +18,13 @@ const defineEmail = (options) => {
18
18
  const render = async (data) => {
19
19
  const renderer = await loadRenderer();
20
20
  const html = await renderer.render(renderer.createElement(options.component, data));
21
+ const subject = typeof options.subject === "function" ? options.subject(data) : options.subject;
22
+ const headers = typeof options.headers === "function" ? options.headers(data) : options.headers;
21
23
  return {
22
- subject: typeof options.subject === "function" ? options.subject(data) : options.subject,
24
+ subject,
23
25
  html,
24
- text: renderer.toPlainText(html)
26
+ text: renderer.toPlainText(html),
27
+ ...headers && { headers }
25
28
  };
26
29
  };
27
30
  const send = async (to, data, mailer = platformMailer()) => {
@@ -0,0 +1,26 @@
1
+ import { MailingApp } from "./mailing-app.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/mail/email-layout.d.ts
4
+ export type EmailLayoutProps = {
5
+ app: MailingApp;
6
+ /** The line inboxes show after the subject. */
7
+ preview: string;
8
+ heading: string;
9
+ children: ReactNode;
10
+ };
11
+ /** An email: its heading and body on a card, and a footer naming the app, linked to its home page. */
12
+ export declare const EmailLayout: ({ app, preview, heading, children }: EmailLayoutProps) => import("react").JSX.Element;
13
+ /** A code to type in, on a line of its own in both parts. */
14
+ export declare const EmailCode: ({ code }: {
15
+ code: string;
16
+ }) => import("react").JSX.Element;
17
+ /** A remark below the message, such as what to do when the email was not expected. */
18
+ export declare const EmailNote: ({ children }: {
19
+ children: ReactNode;
20
+ }) => import("react").JSX.Element;
21
+ /** The email's one action, such as signing in: a button, and the label with the link in the plain-text part. */
22
+ export declare const EmailButton: ({ href, children }: {
23
+ href: string;
24
+ children: ReactNode;
25
+ }) => import("react").JSX.Element;
26
+ //#endregion
@@ -1,6 +1,6 @@
1
1
  import { Body, Button, Container, Head, Heading, Hr, Html, Link, Preview, Text } from "react-email";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- //#region src/better-auth/auth-mail/auth-email-layout.tsx
3
+ //#region src/mail/email-layout.tsx
4
4
  const styles = {
5
5
  body: {
6
6
  backgroundColor: "#f4f4f5",
@@ -39,8 +39,8 @@ const styles = {
39
39
  fontWeight: 600
40
40
  }
41
41
  };
42
- /** An auth email: its heading and body on a card, and a footer naming the app, linked to its home page. */
43
- const AuthEmailLayout = ({ app, preview, heading, children }) => /* @__PURE__ */ jsxs(Html, {
42
+ /** An email: its heading and body on a card, and a footer naming the app, linked to its home page. */
43
+ const EmailLayout = ({ app, preview, heading, children }) => /* @__PURE__ */ jsxs(Html, {
44
44
  lang: "en",
45
45
  children: [
46
46
  /* @__PURE__ */ jsx(Head, {}),
@@ -87,4 +87,4 @@ const EmailButton = ({ href, children }) => /* @__PURE__ */ jsx(Button, {
87
87
  children
88
88
  });
89
89
  //#endregion
90
- export { AuthEmailLayout, EmailButton, EmailCode, EmailNote };
90
+ export { EmailButton, EmailCode, EmailLayout, EmailNote };
@@ -0,0 +1,7 @@
1
+ //#region src/mail/mailing-app.d.ts
2
+ /** The app an email comes from: its name, and its home page on the origin the request came to. */
3
+ export type MailingApp = {
4
+ name: string;
5
+ url: string;
6
+ };
7
+ //#endregion
@@ -1,4 +1,4 @@
1
- //#region src/better-auth/auth-mail/mailing-app.ts
1
+ //#region src/mail/mailing-app.ts
2
2
  /**
3
3
  * The app as an auth email names it: `appName`, and the home page on the origin of `requestURL`, a URL on the origin
4
4
  * the request came to (such as Better Auth's `ctx.context.baseURL`), or on `baseURL`'s origin without one.
@@ -6,6 +6,11 @@ export type MailMessage = {
6
6
  text: string;
7
7
  /** The HTML body, sent beside `text` as the alternative clients show when they can. */
8
8
  html?: string;
9
+ /**
10
+ * More header fields by name, such as `List-Unsubscribe` and `List-Unsubscribe-Post`, which mailbox providers expect
11
+ * of bulk mail. A value is one line.
12
+ */
13
+ headers?: Record<string, string>;
9
14
  };
10
15
  /** Sends one message; rejects when the server refuses it. */
11
16
  export type Mailer = {
@@ -10,6 +10,7 @@
10
10
  const smtpMailer = (settings) => {
11
11
  let transport;
12
12
  return { send: async (message) => {
13
+ checkHeaders(message.headers);
13
14
  transport ??= import("nodemailer").then(({ createTransport }) => createTransport({
14
15
  url: settings.smtpUrl,
15
16
  name: senderDomain(settings.from)
@@ -26,5 +27,11 @@ const sender = ({ from, name }) => name ? {
26
27
  } : from;
27
28
  /** The domain of a sender address `readMailSettings` accepted, so always a dotted hostname. */
28
29
  const senderDomain = (from) => from.slice(from.lastIndexOf("@") + 1);
30
+ const checkHeaders = (headers = {}) => {
31
+ for (const [name, value] of Object.entries(headers)) {
32
+ if (!/^[!-9;-~]+$/.test(name)) throw new Error(`Mail header name "${name}" is not a valid header field name`);
33
+ if (/[\r\n]/.test(value)) throw new Error(`Mail header ${name} has a line break; a header value is one line`);
34
+ }
35
+ };
29
36
  //#endregion
30
37
  export { smtpMailer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.23.8",
3
+ "version": "0.23.10",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -15,7 +15,8 @@
15
15
  "node": ">=22.12.0"
16
16
  },
17
17
  "exports": {
18
- ".": "./dist/index.js"
18
+ ".": "./dist/index.js",
19
+ "./email": "./dist/email.js"
19
20
  },
20
21
  "types": "./dist/index.d.ts",
21
22
  "files": [
@@ -31,9 +32,9 @@
31
32
  "dependencies": {
32
33
  "@better-auth/passkey": "^1.7.7",
33
34
  "@better-auth/sso": "^1.7.7",
34
- "@protobase/layout": "0.23.8",
35
- "@protobase/query": "0.23.8",
36
- "@protobase/schema": "0.23.8",
35
+ "@protobase/layout": "0.23.10",
36
+ "@protobase/query": "0.23.10",
37
+ "@protobase/schema": "0.23.10",
37
38
  "@react-email/render": "^2.1.0",
38
39
  "@scalar/hono-api-reference": "^0.12.10",
39
40
  "@sentry/core": "^11.7.0",