@protobase/server 0.15.5 → 0.16.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/dist/app-name.js +9 -0
- package/dist/better-auth/auth-mail/account-exists-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/account-exists-email.js +31 -0
- package/dist/better-auth/auth-mail/auth-email-layout.js +90 -0
- package/dist/better-auth/auth-mail/auth-emails.d.ts +7 -0
- package/dist/better-auth/auth-mail/auth-emails.js +8 -0
- package/dist/better-auth/auth-mail/invitation-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/invitation-email.js +38 -0
- package/dist/better-auth/auth-mail/mailing-app.js +11 -0
- package/dist/better-auth/auth-mail/reset-password-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/reset-password-email.js +35 -0
- package/dist/better-auth/auth-mail/send-auth-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/send-auth-email.js +15 -0
- package/dist/better-auth/auth-mail/sign-in-code-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/sign-in-code-email.js +32 -0
- package/dist/better-auth/auth-mail/sign-up-code-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/sign-up-code-email.js +32 -0
- package/dist/better-auth/auth-mail/staff-sign-in-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/staff-sign-in-email.js +39 -0
- package/dist/better-auth/auth-mail/two-factor-code-email.d.ts +1 -0
- package/dist/better-auth/auth-mail/two-factor-code-email.js +32 -0
- package/dist/better-auth/create-auth.d.ts +43 -181
- package/dist/better-auth/create-auth.js +24 -11
- package/dist/better-auth/organizations/organization-plugin.js +8 -8
- package/dist/better-auth/password-reset.js +8 -21
- package/dist/better-auth/sign-in-mail.js +14 -55
- package/dist/better-auth/sign-up/sign-up-code.js +2 -1
- package/dist/better-auth/sign-up/sign-up-plugin.d.ts +2 -0
- package/dist/better-auth/sign-up/sign-up-plugin.js +37 -22
- package/dist/better-auth/sign-up/sign-up-ticket.js +34 -0
- package/dist/better-auth/staff-plugin.js +2 -2
- package/dist/better-auth/staff-sign-in-endpoints.js +5 -6
- package/dist/check-views.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/mail/platform-mailer.js +10 -3
- package/dist/mail/smtp-mailer.js +6 -2
- package/dist/view-access.js +6 -1
- package/dist/view-map.js +12 -2
- package/package.json +5 -4
- package/dist/better-auth/organizations/invitation-mail.js +0 -17
- package/dist/better-auth/sign-up/sign-up-mail.js +0 -29
|
@@ -1,45 +1,18 @@
|
|
|
1
|
+
import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
|
|
1
2
|
const codeLifetimeMinutes = 5;
|
|
2
3
|
const codeLifetime = "5 minutes";
|
|
3
|
-
/** The email with a code to sign in with instead of a password. */
|
|
4
|
-
const signInCodeEmail = ({ to, code }) => ({
|
|
5
|
-
to,
|
|
6
|
-
subject: "Your sign-in code",
|
|
7
|
-
text: [
|
|
8
|
-
"Use this code to sign in:",
|
|
9
|
-
"",
|
|
10
|
-
code,
|
|
11
|
-
"",
|
|
12
|
-
`It works for ${codeLifetime}, once.`,
|
|
13
|
-
"If you did not try to sign in, ignore this email: nobody can sign in without the code.",
|
|
14
|
-
""
|
|
15
|
-
].join("\n")
|
|
16
|
-
});
|
|
17
|
-
/** The email with the second step of a sign-in, for an account with two-factor authentication by email. */
|
|
18
|
-
const twoFactorCodeEmail = ({ to, code }) => ({
|
|
19
|
-
to,
|
|
20
|
-
subject: "Your verification code",
|
|
21
|
-
text: [
|
|
22
|
-
"Use this code to finish signing in:",
|
|
23
|
-
"",
|
|
24
|
-
code,
|
|
25
|
-
"",
|
|
26
|
-
`It works for ${codeLifetime}, once.`,
|
|
27
|
-
"If you did not just sign in, someone else may know your password or have your sign-in code: change your password.",
|
|
28
|
-
""
|
|
29
|
-
].join("\n")
|
|
30
|
-
});
|
|
31
4
|
/**
|
|
32
|
-
* Better Auth's email OTP plugin as sign-in only: never a sign-up, codes stored hashed, sent with `
|
|
5
|
+
* Better Auth's email OTP plugin as sign-in only: never a sign-up, codes stored hashed, sent with `mail`. The
|
|
33
6
|
* plugin's other uses (email verification, its own password reset, changing the address) are not offered.
|
|
34
7
|
*/
|
|
35
|
-
const emailCodeOptions = (
|
|
8
|
+
const emailCodeOptions = (mail) => ({
|
|
36
9
|
disableSignUp: true,
|
|
37
10
|
expiresIn: 300,
|
|
38
11
|
storeOTP: "hashed",
|
|
39
|
-
sendVerificationOTP: ({ email, otp }) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
})
|
|
12
|
+
sendVerificationOTP: ({ email, otp }, ctx) => sendAuthEmail(mail, "signInCodeEmail", email, {
|
|
13
|
+
code: otp,
|
|
14
|
+
lifetime: codeLifetime
|
|
15
|
+
}, ctx?.context.baseURL)
|
|
43
16
|
});
|
|
44
17
|
/** The email OTP plugin's endpoints Protobase does not use; `createAuth` turns them off. */
|
|
45
18
|
const unusedEmailCodePaths = [
|
|
@@ -51,28 +24,14 @@ const unusedEmailCodePaths = [
|
|
|
51
24
|
"/email-otp/request-email-change",
|
|
52
25
|
"/email-otp/change-email"
|
|
53
26
|
];
|
|
54
|
-
/** The two-factor plugin's emailed code, sent with `
|
|
55
|
-
const twoFactorCodeOptions = (
|
|
27
|
+
/** The two-factor plugin's emailed code, sent with `mail`; stored hashed. */
|
|
28
|
+
const twoFactorCodeOptions = (mail) => ({
|
|
56
29
|
period: codeLifetimeMinutes,
|
|
57
30
|
storeOTP: "hashed",
|
|
58
|
-
sendOTP: ({ user, otp }) =>
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
62
|
-
});
|
|
63
|
-
/** The email to someone whose app lets staff sign in as people only when they are told: who, why, and until when. */
|
|
64
|
-
const staffSignInEmail = ({ to, staff, reason, expiresAt }) => ({
|
|
65
|
-
to,
|
|
66
|
-
subject: "Staff signed in as you",
|
|
67
|
-
text: [
|
|
68
|
-
`${staff}, of the team that runs this app, signed in as you to help. The reason they gave:`,
|
|
69
|
-
"",
|
|
70
|
-
reason,
|
|
71
|
-
"",
|
|
72
|
-
`Their session ends by ${expiresAt.toUTCString()}. The app's admins see it in the app's log of staff sign-ins.`,
|
|
73
|
-
"If you did not expect this, tell your admin.",
|
|
74
|
-
""
|
|
75
|
-
].join("\n")
|
|
31
|
+
sendOTP: ({ user, otp }, ctx) => sendAuthEmail(mail, "twoFactorCodeEmail", user.email, {
|
|
32
|
+
code: otp,
|
|
33
|
+
lifetime: codeLifetime
|
|
34
|
+
}, ctx?.context.baseURL)
|
|
76
35
|
});
|
|
77
36
|
//#endregion
|
|
78
|
-
export { emailCodeOptions,
|
|
37
|
+
export { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths };
|
|
@@ -2,6 +2,7 @@ import { APIError } from "better-auth/api";
|
|
|
2
2
|
const signUpCodeLifetime = "5 minutes";
|
|
3
3
|
const allowedAttempts = 3;
|
|
4
4
|
const identifierOf = (email) => `sign-up-otp-${email}`;
|
|
5
|
+
/** The SHA-256 of `secret`, base64: what the store keeps of a code or a ticket. */
|
|
5
6
|
const hashOf = async (code) => {
|
|
6
7
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code));
|
|
7
8
|
return btoa(String.fromCharCode(...new Uint8Array(digest)));
|
|
@@ -49,4 +50,4 @@ const consumeSignUpCode = async (context, email, code) => {
|
|
|
49
50
|
throw invalidCode();
|
|
50
51
|
};
|
|
51
52
|
//#endregion
|
|
52
|
-
export { consumeSignUpCode, issueSignUpCode, signUpCodeLifetime };
|
|
53
|
+
export { consumeSignUpCode, hashOf, issueSignUpCode, signUpCodeLifetime };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import "../auth-mail/send-auth-email.js";
|
|
1
2
|
import "zod";
|
|
2
3
|
//#region src/better-auth/sign-up/sign-up-plugin.d.ts
|
|
3
4
|
/** The paths of the sign-up endpoints, as `ctx.path` and the rate limits name them. */
|
|
4
5
|
export declare const signUpPaths: {
|
|
5
6
|
sendCode: string;
|
|
7
|
+
verifyCode: string;
|
|
6
8
|
create: string;
|
|
7
9
|
};
|
|
8
10
|
//#endregion
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { sendAuthEmail } from "../auth-mail/send-auth-email.js";
|
|
1
2
|
import { checkNewAccountPassword } from "../new-account-password.js";
|
|
2
3
|
import { effectiveSignInPolicy } from "../sign-in-policy.js";
|
|
3
4
|
import { readSignInPolicy } from "../policy-store.js";
|
|
4
|
-
import { consumeSignUpCode, issueSignUpCode } from "./sign-up-code.js";
|
|
5
|
-
import {
|
|
5
|
+
import { consumeSignUpCode, issueSignUpCode, signUpCodeLifetime } from "./sign-up-code.js";
|
|
6
|
+
import { consumeSignUpTicket, issueSignUpTicket } from "./sign-up-ticket.js";
|
|
6
7
|
import { APIError, createAuthEndpoint } from "better-auth/api";
|
|
7
8
|
import { setSessionCookie } from "better-auth/cookies";
|
|
8
9
|
import * as z$1 from "zod";
|
|
@@ -11,6 +12,7 @@ const address = z$1.string().trim().toLowerCase().pipe(z$1.email());
|
|
|
11
12
|
/** The paths of the sign-up endpoints, as `ctx.path` and the rate limits name them. */
|
|
12
13
|
const signUpPaths = {
|
|
13
14
|
sendCode: "/sign-up/send-code",
|
|
15
|
+
verifyCode: "/sign-up/verify-code",
|
|
14
16
|
create: "/sign-up/create"
|
|
15
17
|
};
|
|
16
18
|
const noAdminYet = () => new APIError("FORBIDDEN", {
|
|
@@ -18,12 +20,13 @@ const noAdminYet = () => new APIError("FORBIDDEN", {
|
|
|
18
20
|
message: "This app has no admin yet; accounts are created on the host until it has one."
|
|
19
21
|
});
|
|
20
22
|
/**
|
|
21
|
-
* Open sign-up: anyone makes their own account, with `role`. The address comes first,
|
|
22
|
-
* account exists only for an address its owner reads, verified from the
|
|
23
|
-
* its owner does. Asking for a code answers the same whether the address
|
|
24
|
-
*
|
|
23
|
+
* Open sign-up: anyone makes their own account, with `role`. The address comes first, then the code mailed to it on its
|
|
24
|
+
* own, and only then the name and password, so an account exists only for an address its owner reads, verified from the
|
|
25
|
+
* start, and nobody can claim an address before its owner does. Asking for a code answers the same whether the address
|
|
26
|
+
* has an account or not; one that has gets a mail saying so, with a link to sign in, instead of a code. While the store
|
|
27
|
+
* has no user, nobody signs up: the first admin is made on the host.
|
|
25
28
|
*/
|
|
26
|
-
const signUpPlugin = ({ role,
|
|
29
|
+
const signUpPlugin = ({ role, mail }) => ({
|
|
27
30
|
id: "protobase-sign-up",
|
|
28
31
|
endpoints: {
|
|
29
32
|
/** `POST /sign-up/send-code` with `{ email }`: mails a code for making an account with the address. */
|
|
@@ -34,36 +37,48 @@ const signUpPlugin = ({ role, mailer, appName }) => ({
|
|
|
34
37
|
const { email } = ctx.body;
|
|
35
38
|
const { internalAdapter, adapter } = ctx.context;
|
|
36
39
|
if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
37
|
-
const
|
|
38
|
-
to: email,
|
|
39
|
-
appName
|
|
40
|
-
}) : signUpCodeEmail({
|
|
41
|
-
to: email,
|
|
40
|
+
const sending = await internalAdapter.findUserByEmail(email) ? sendAuthEmail(mail, "accountExistsEmail", email, {}, ctx.context.baseURL) : sendAuthEmail(mail, "signUpCodeEmail", email, {
|
|
42
41
|
code: await issueSignUpCode(ctx.context, email),
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
await ctx.context.runInBackgroundOrAwait(
|
|
42
|
+
lifetime: signUpCodeLifetime
|
|
43
|
+
}, ctx.context.baseURL);
|
|
44
|
+
await ctx.context.runInBackgroundOrAwait(sending);
|
|
46
45
|
return ctx.json({ sent: true });
|
|
47
46
|
}),
|
|
48
47
|
/**
|
|
49
|
-
* `POST /sign-up/
|
|
50
|
-
* and
|
|
51
|
-
* codes; the policy's required methods are then set up as after any sign-in.
|
|
48
|
+
* `POST /sign-up/verify-code` with `{ email, code }`: takes the mailed code on its own, before anything else is asked,
|
|
49
|
+
* and answers `{ ticket }`, which makes the account for the address with the step after.
|
|
52
50
|
*/
|
|
53
|
-
|
|
51
|
+
verifySignUpCode: createAuthEndpoint(signUpPaths.verifyCode, {
|
|
54
52
|
method: "POST",
|
|
55
53
|
body: z$1.object({
|
|
56
54
|
email: address,
|
|
57
|
-
code: z$1.string().trim().min(1)
|
|
55
|
+
code: z$1.string().trim().min(1)
|
|
56
|
+
})
|
|
57
|
+
}, async (ctx) => {
|
|
58
|
+
const { email, code } = ctx.body;
|
|
59
|
+
if (await ctx.context.adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
60
|
+
await consumeSignUpCode(ctx.context, email, code);
|
|
61
|
+
return ctx.json({ ticket: await issueSignUpTicket(ctx.context, email) });
|
|
62
|
+
}),
|
|
63
|
+
/**
|
|
64
|
+
* `POST /sign-up/create` with `{ ticket, name, password? }`: makes the account for the address the ticket's code was
|
|
65
|
+
* right for, verified and with the sign-up role, and signs the person in. A password is needed unless the sign-in
|
|
66
|
+
* policy offers emailed codes; the policy's required methods are then set up as after any sign-in. A refused
|
|
67
|
+
* password leaves the ticket for another try.
|
|
68
|
+
*/
|
|
69
|
+
createSignUpAccount: createAuthEndpoint(signUpPaths.create, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: z$1.object({
|
|
72
|
+
ticket: z$1.string().min(1),
|
|
58
73
|
name: z$1.string().trim().min(1).max(200),
|
|
59
74
|
password: z$1.string().optional()
|
|
60
75
|
})
|
|
61
76
|
}, async (ctx) => {
|
|
62
|
-
const {
|
|
77
|
+
const { ticket, name, password } = ctx.body;
|
|
63
78
|
const { internalAdapter, adapter } = ctx.context;
|
|
64
79
|
if (await adapter.count({ model: "user" }) === 0) throw noAdminYet();
|
|
65
80
|
checkNewAccountPassword(effectiveSignInPolicy((await readSignInPolicy(adapter)).policy, { mail: true }), password);
|
|
66
|
-
await
|
|
81
|
+
const email = await consumeSignUpTicket(ctx.context, ticket);
|
|
67
82
|
if (await internalAdapter.findUserByEmail(email)) throw new APIError("BAD_REQUEST", {
|
|
68
83
|
code: "ACCOUNT_EXISTS",
|
|
69
84
|
message: "This address has an account already: sign in instead."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { hashOf } from "./sign-up-code.js";
|
|
2
|
+
import { APIError } from "better-auth/api";
|
|
3
|
+
//#region src/better-auth/sign-up/sign-up-ticket.ts
|
|
4
|
+
/** How long a ticket from a right code makes the account, in seconds. */
|
|
5
|
+
const signUpTicketLifetimeSeconds = 1800;
|
|
6
|
+
const identifierOf = async (ticket) => `sign-up-ticket-${await hashOf(ticket)}`;
|
|
7
|
+
const randomTicket = () => {
|
|
8
|
+
const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
|
|
9
|
+
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
10
|
+
};
|
|
11
|
+
/** A new ticket for `email`, whose code was right; the store keeps only its hash. */
|
|
12
|
+
const issueSignUpTicket = async (context, email) => {
|
|
13
|
+
const ticket = randomTicket();
|
|
14
|
+
await context.internalAdapter.createVerificationValue({
|
|
15
|
+
identifier: await identifierOf(ticket),
|
|
16
|
+
value: email,
|
|
17
|
+
expiresAt: new Date(Date.now() + signUpTicketLifetimeSeconds * 1e3)
|
|
18
|
+
});
|
|
19
|
+
return ticket;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Takes `ticket`, so it works once, and answers the address its code was right for. Taking it is atomic, so of two
|
|
23
|
+
* requests with it only one gets through. Throws `SIGN_UP_TICKET_INVALID` for a ticket that is unknown, used or expired.
|
|
24
|
+
*/
|
|
25
|
+
const consumeSignUpTicket = async (context, ticket) => {
|
|
26
|
+
const consumed = await context.internalAdapter.consumeVerificationValue(await identifierOf(ticket));
|
|
27
|
+
if (!consumed || new Date(consumed.expiresAt) < /* @__PURE__ */ new Date()) throw new APIError("BAD_REQUEST", {
|
|
28
|
+
code: "SIGN_UP_TICKET_INVALID",
|
|
29
|
+
message: "This sign-up took too long. Start again with your email address."
|
|
30
|
+
});
|
|
31
|
+
return consumed.value;
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { consumeSignUpTicket, issueSignUpTicket, signUpTicketLifetimeSeconds };
|
|
@@ -11,13 +11,13 @@ import { deleteSessionCookie } from "better-auth/cookies";
|
|
|
11
11
|
* staff sign-in is logged for the app's admins. The plugin is there without an operator provider too, so its table
|
|
12
12
|
* exists from the first auth migration.
|
|
13
13
|
*/
|
|
14
|
-
const staffSignInPlugin = ({ operator,
|
|
14
|
+
const staffSignInPlugin = ({ operator, mail }) => ({
|
|
15
15
|
id: "protobase-staff-sign-in",
|
|
16
16
|
schema: staffSignInSchema,
|
|
17
17
|
endpoints: {
|
|
18
18
|
...operator && staffSignInEndpoints({
|
|
19
19
|
operator,
|
|
20
|
-
...
|
|
20
|
+
...mail && { mail }
|
|
21
21
|
}),
|
|
22
22
|
/** `GET /staff/session`: for a staff session, who started it, as whom, why, and until when; otherwise `{ staff: null }`. */
|
|
23
23
|
getStaffSession: createAuthEndpoint("/staff/session", {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { reasonProblem, staffClaimsRefusal, staffLabel } from "./staff-checks.js";
|
|
2
2
|
import { operatorClient, randomValue } from "./operator-provider.js";
|
|
3
|
+
import { sendAuthEmail } from "./auth-mail/send-auth-email.js";
|
|
3
4
|
import { effectiveSignInPolicy } from "./sign-in-policy.js";
|
|
4
5
|
import { readSignInPolicy } from "./policy-store.js";
|
|
5
6
|
import { policyRefusal, staffSignInPath } from "./sign-in-enforcement.js";
|
|
6
|
-
import { staffSignInEmail } from "./sign-in-mail.js";
|
|
7
7
|
import { logStaffSignIn } from "./staff-log.js";
|
|
8
8
|
import { APIError, createAuthEndpoint } from "better-auth/api";
|
|
9
9
|
import { expireCookie, setSessionCookie } from "better-auth/cookies";
|
|
@@ -35,7 +35,7 @@ const withError = (url, code) => {
|
|
|
35
35
|
* permission and sign-in and starts a session as the person, of Better Auth's admin plugin (`impersonatedBy` names
|
|
36
36
|
* them). The session is short and never extended, and the person's two-factor and passkey requirements do not stop it.
|
|
37
37
|
*/
|
|
38
|
-
const staffSignInEndpoints = ({ operator,
|
|
38
|
+
const staffSignInEndpoints = ({ operator, mail }) => {
|
|
39
39
|
const clients = /* @__PURE__ */ new Map();
|
|
40
40
|
const clientOf = (context) => {
|
|
41
41
|
const callback = `${context.baseURL}/staff/callback`;
|
|
@@ -52,19 +52,18 @@ const staffSignInEndpoints = ({ operator, mailer }) => {
|
|
|
52
52
|
now: Math.floor(Date.now() / 1e3)
|
|
53
53
|
});
|
|
54
54
|
if (refusal) return refusal;
|
|
55
|
-
const policy = effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail: Boolean(
|
|
55
|
+
const policy = effectiveSignInPolicy((await readSignInPolicy(ctx.context.adapter)).policy, { mail: Boolean(mail) });
|
|
56
56
|
const off = policyRefusal(policy, { path: staffSignInPath });
|
|
57
57
|
if (off) return off;
|
|
58
58
|
const found = await ctx.context.internalAdapter.findUserByEmail(started.email.toLowerCase());
|
|
59
59
|
if (!found || found.user.banned) return userUnavailable;
|
|
60
60
|
const staff = staffLabel(claims);
|
|
61
61
|
const expiresAt = new Date(Date.now() + operator.sessionMinutes * 60 * 1e3);
|
|
62
|
-
if (policy.staffAccess === "notify" &&
|
|
63
|
-
to: found.user.email,
|
|
62
|
+
if (policy.staffAccess === "notify" && mail) await sendAuthEmail(mail, "staffSignInEmail", found.user.email, {
|
|
64
63
|
staff,
|
|
65
64
|
reason: started.reason,
|
|
66
65
|
expiresAt
|
|
67
|
-
})
|
|
66
|
+
}, ctx.context.baseURL);
|
|
68
67
|
const session = await ctx.context.internalAdapter.createSession(found.user.id, true, {
|
|
69
68
|
impersonatedBy: staff,
|
|
70
69
|
expiresAt
|
package/dist/check-views.js
CHANGED
|
@@ -37,6 +37,10 @@ const valueColorErrors = (view, model) => Object.entries(view.fields).flatMap(([
|
|
|
37
37
|
if (field?.type !== "enum") return [`value colours of "${name}" need an enum field`];
|
|
38
38
|
return Object.keys(hints.valueColors).filter((value) => !field.enumValues?.includes(value)).map((value) => `value colour "${value}" is not a value of "${name}"`);
|
|
39
39
|
});
|
|
40
|
+
const multilineErrors = (view, model) => Object.entries(view.fields).flatMap(([name, hints]) => {
|
|
41
|
+
const field = model.fields[name];
|
|
42
|
+
return hints.multiline && field && field.type !== "text" ? [`multiline "${name}" needs a text field, not ${field.type}`] : [];
|
|
43
|
+
});
|
|
40
44
|
const linkErrors = (view, model, models, pages) => view.actions.flatMap(({ name, label, run }) => {
|
|
41
45
|
if (run?.kind !== "link") return [];
|
|
42
46
|
const error = linkActionError(label, run);
|
|
@@ -98,6 +102,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
98
102
|
...unknownFieldErrors(view, model),
|
|
99
103
|
...valueLabelErrors(view, model),
|
|
100
104
|
...valueColorErrors(view, model),
|
|
105
|
+
...multilineErrors(view, model),
|
|
101
106
|
...widgetErrors(view, model, fieldTypes),
|
|
102
107
|
...related,
|
|
103
108
|
...photos,
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { RoleDefinition, RolesInput } from "./better-auth/organizations/role-def
|
|
|
6
6
|
import { OrganizationRole, StoredMember, StoredOrganization } from "./better-auth/organizations/store.js";
|
|
7
7
|
import { OrganizationsOptions } from "./better-auth/organizations/options.js";
|
|
8
8
|
import { PlatformSignIn, readPlatformSignIn } from "./better-auth/platform-sign-in.js";
|
|
9
|
+
import { EmailComponent, EmailOptions, EmailTemplate, RenderedEmail, defineEmail } from "./mail/define-email.js";
|
|
9
10
|
import { AdminAuth, CreateAuthOptions, UserCreatedHook, createAuth, normalizeRoles } from "./better-auth/create-auth.js";
|
|
10
11
|
import { EventsOptions } from "./changes/events-options.js";
|
|
11
12
|
import { StandardSchema, readJson } from "./functions/read-json.js";
|
|
@@ -78,7 +79,6 @@ import { FileRef, formatUri, variantPath } from "./files/uri.js";
|
|
|
78
79
|
import { aspectRatio, fileSize, fileType, imageSize, processor } from "./files/processors.js";
|
|
79
80
|
import { defineJob, isJobDefinition, jobDefinitions } from "./jobs/define-job.js";
|
|
80
81
|
import { FatalJobError } from "./jobs/run-job.js";
|
|
81
|
-
import { EmailComponent, EmailOptions, EmailTemplate, RenderedEmail, defineEmail } from "./mail/define-email.js";
|
|
82
82
|
import { DrainResult, JobsRuntime, StartJobsOptions, startJobs } from "./jobs/runtime.js";
|
|
83
83
|
import { jobsDrainApp } from "./jobs/drain-route.js";
|
|
84
84
|
import { checkJobsSchema, jobsSchema, jobsSchemaMigration } from "./jobs/schema.js";
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
import { readAppName } from "../app-name.js";
|
|
1
2
|
import { readMailSettings } from "./mail-settings.js";
|
|
2
3
|
import { smtpMailer } from "./smtp-mailer.js";
|
|
3
4
|
//#region src/mail/platform-mailer.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The platform's SMTP server and sender from `PROTOBASE_SMTP_URL` and `PROTOBASE_MAIL_FROM`; `undefined` when mail is off.
|
|
7
|
+
* The From header shows `name` as the sender's name, by default the app's name from `PROTOBASE_APP_NAME`, when set.
|
|
8
|
+
*/
|
|
9
|
+
const platformMailer = (env = globalThis.process?.env ?? {}, name = readAppName(env)) => {
|
|
6
10
|
const settings = readMailSettings(env);
|
|
7
|
-
return settings && smtpMailer(
|
|
11
|
+
return settings && smtpMailer({
|
|
12
|
+
...settings,
|
|
13
|
+
...name && { name }
|
|
14
|
+
});
|
|
8
15
|
};
|
|
9
16
|
//#endregion
|
|
10
17
|
export { platformMailer };
|
package/dist/mail/smtp-mailer.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The client greets the server (EHLO) with the sender's domain, the app's own mail name, instead of what the container
|
|
7
7
|
* reports, which is often just `[127.0.0.1]` and makes receiving servers trust the mail less. A `name` query parameter
|
|
8
|
-
* on the SMTP URL still overrides it.
|
|
8
|
+
* on the SMTP URL still overrides it. With a `name`, the From header carries it as the sender's display name.
|
|
9
9
|
*/
|
|
10
10
|
const smtpMailer = (settings) => {
|
|
11
11
|
let transport;
|
|
@@ -15,11 +15,15 @@ const smtpMailer = (settings) => {
|
|
|
15
15
|
name: senderDomain(settings.from)
|
|
16
16
|
}));
|
|
17
17
|
await (await transport).sendMail({
|
|
18
|
-
from: settings
|
|
18
|
+
from: sender(settings),
|
|
19
19
|
...message
|
|
20
20
|
});
|
|
21
21
|
} };
|
|
22
22
|
};
|
|
23
|
+
const sender = ({ from, name }) => name ? {
|
|
24
|
+
name,
|
|
25
|
+
address: from
|
|
26
|
+
} : from;
|
|
23
27
|
/** The domain of a sender address `readMailSettings` accepted, so always a dotted hostname. */
|
|
24
28
|
const senderDomain = (from) => from.slice(from.lastIndexOf("@") + 1);
|
|
25
29
|
//#endregion
|
package/dist/view-access.js
CHANGED
|
@@ -52,7 +52,12 @@ const listShape = (resource, models, prompted) => shapes.object({
|
|
|
52
52
|
icon: shapes.data,
|
|
53
53
|
latitude: shapes.field,
|
|
54
54
|
longitude: shapes.field,
|
|
55
|
-
cards: shapes.data
|
|
55
|
+
cards: shapes.data,
|
|
56
|
+
approximate: shapes.field,
|
|
57
|
+
exact: shapes.field,
|
|
58
|
+
radius: shapes.field,
|
|
59
|
+
defaultRadius: shapes.data,
|
|
60
|
+
area: shapes.field
|
|
56
61
|
}, { requires: ["latitude", "longitude"] }),
|
|
57
62
|
custom: shapes.object({
|
|
58
63
|
kind: shapes.data,
|
package/dist/view-map.js
CHANGED
|
@@ -18,12 +18,22 @@ const mapErrors = (item, model) => [
|
|
|
18
18
|
...item.zoom === void 0 || Number.isFinite(item.zoom) && item.zoom >= 0 && item.zoom <= maxZoom ? [] : [`zoom ${item.zoom} is not between 0 and ${maxZoom}`]
|
|
19
19
|
];
|
|
20
20
|
const filterableErrors = (model, name) => Object.hasOwn(model.fields, name) && !model.fields[name].filterable ? [`"${name}" on ${model.name} is not filterable, and the map display filters by position`] : [];
|
|
21
|
-
|
|
21
|
+
const typeErrors = (model, name, types, what) => {
|
|
22
|
+
if (name === void 0) return [];
|
|
23
|
+
const field = Object.hasOwn(model.fields, name) ? model.fields[name] : void 0;
|
|
24
|
+
if (!field) return [`unknown field "${name}" on ${model.name}`];
|
|
25
|
+
return types.includes(field.type) ? [] : [`"${name}" on ${model.name} is ${field.type}, not ${what}`];
|
|
26
|
+
};
|
|
27
|
+
/** What makes a map display unable to show the records: a field that is no number, one it cannot filter by, or one of the wrong type for how exact a position is. */
|
|
22
28
|
const mapDisplayErrors = (display, model) => [
|
|
23
29
|
...coordinateErrors(model, display.latitude),
|
|
24
30
|
...coordinateErrors(model, display.longitude),
|
|
25
31
|
...filterableErrors(model, display.latitude),
|
|
26
|
-
...filterableErrors(model, display.longitude)
|
|
32
|
+
...filterableErrors(model, display.longitude),
|
|
33
|
+
...typeErrors(model, display.approximate, ["boolean"], "a boolean"),
|
|
34
|
+
...typeErrors(model, display.exact, ["boolean"], "a boolean"),
|
|
35
|
+
...typeErrors(model, display.radius, [...coordinateTypes], "a number of metres"),
|
|
36
|
+
...typeErrors(model, display.area, ["json", "text"], "GeoJSON as json or text")
|
|
27
37
|
];
|
|
28
38
|
//#endregion
|
|
29
39
|
export { mapDisplayErrors, mapErrors };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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": {
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@protobase/layout": "0.
|
|
34
|
-
"@protobase/query": "0.
|
|
35
|
-
"@protobase/schema": "0.
|
|
33
|
+
"@protobase/layout": "0.16.0",
|
|
34
|
+
"@protobase/query": "0.16.0",
|
|
35
|
+
"@protobase/schema": "0.16.0",
|
|
36
36
|
"@react-email/render": "^2.1.0",
|
|
37
37
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
38
38
|
"@sentry/core": "^11.7.0",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"pg-boss": "12.37.1",
|
|
49
49
|
"react": "^19.3.0",
|
|
50
50
|
"react-dom": "^19.3.0",
|
|
51
|
+
"react-email": "^6.11.1",
|
|
51
52
|
"thumbhash": "^0.1.1",
|
|
52
53
|
"zod": "^4.6.5"
|
|
53
54
|
},
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
//#region src/better-auth/organizations/invitation-mail.ts
|
|
2
|
-
/** The invitation email: who invites whom to which organization, the link, and until when it works. */
|
|
3
|
-
const invitationEmail = ({ to, inviter, organization, url, expiresAt }) => ({
|
|
4
|
-
to,
|
|
5
|
-
subject: `Join ${organization}`,
|
|
6
|
-
text: [
|
|
7
|
-
`${inviter} invites you to join ${organization}. To accept, open this link:`,
|
|
8
|
-
"",
|
|
9
|
-
url,
|
|
10
|
-
"",
|
|
11
|
-
`The link works until ${new Date(expiresAt).toUTCString()}.`,
|
|
12
|
-
"If you do not want to join, ignore this email: nothing happens without you.",
|
|
13
|
-
""
|
|
14
|
-
].join("\n")
|
|
15
|
-
});
|
|
16
|
-
//#endregion
|
|
17
|
-
export { invitationEmail };
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { signUpCodeLifetime } from "./sign-up-code.js";
|
|
2
|
-
//#region src/better-auth/sign-up/sign-up-mail.ts
|
|
3
|
-
/** The email with the code that makes an account for the address. */
|
|
4
|
-
const signUpCodeEmail = ({ to, code, appName }) => ({
|
|
5
|
-
to,
|
|
6
|
-
subject: "Your code to create an account",
|
|
7
|
-
text: [
|
|
8
|
-
`Use this code to create your account for ${appName}:`,
|
|
9
|
-
"",
|
|
10
|
-
code,
|
|
11
|
-
"",
|
|
12
|
-
`It works for ${signUpCodeLifetime}, once.`,
|
|
13
|
-
"If you did not try to create an account, ignore this email: nobody can create one for your address without the code.",
|
|
14
|
-
""
|
|
15
|
-
].join("\n")
|
|
16
|
-
});
|
|
17
|
-
/** The email to an address that asked for a sign-up code while it has an account already: no code, a pointer to sign in instead. */
|
|
18
|
-
const accountExistsEmail = ({ to, appName }) => ({
|
|
19
|
-
to,
|
|
20
|
-
subject: "You already have an account",
|
|
21
|
-
text: [
|
|
22
|
-
`Someone asked to create an account for ${appName} with this address, which has one already, so no new account was made.`,
|
|
23
|
-
"",
|
|
24
|
-
"Sign in with it instead. If this was not you, ignore this email.",
|
|
25
|
-
""
|
|
26
|
-
].join("\n")
|
|
27
|
-
});
|
|
28
|
-
//#endregion
|
|
29
|
-
export { accountExistsEmail, signUpCodeEmail };
|