@podosoft/podokit 1.6.3 → 1.6.4

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.
@@ -17,18 +17,14 @@ import { authBaseUrl, authSecret } from "./auth-environment";
17
17
  import { isMaintenanceCommand } from "./maintenance";
18
18
  import { CLIENT_ADDRESS_HEADER } from "../core/client-address";
19
19
  import { revokeEveryOtherSession } from "./revoke-other-sessions";
20
+ import { adminEmailsFrom, becomesAdministrator } from "./admin-emails";
20
21
  // podokit:begin:auth-imports
21
22
  // podokit:end:auth-imports
22
23
 
23
24
  // Web origin(s) where the browser runs (WebAuthn ceremonies must match these).
24
25
  const webOrigins = (process.env.CORS_ORIGIN ?? "http://localhost:5001").split(",").map((o) => o.trim());
25
26
 
26
- const adminEmails = new Set(
27
- (process.env.ADMIN_EMAILS ?? "")
28
- .split(",")
29
- .map((email) => email.trim().toLowerCase())
30
- .filter(Boolean),
31
- );
27
+ const adminEmails = adminEmailsFrom(process.env.ADMIN_EMAILS);
32
28
 
33
29
  // Enabled social providers, from the resolved config (DB-first, env fallback).
34
30
  // Admins add/edit/remove providers dynamically (Settings page), so this maps the
@@ -45,6 +41,56 @@ function buildSocial(config: AuthConfig): NonNullable<BetterAuthOptions["socialP
45
41
  return providers as NonNullable<BetterAuthOptions["socialProviders"]>;
46
42
  }
47
43
 
44
+ // The account and session rules of buildAuth: who may be created, who starts as
45
+ // an administrator or approved, and who may hold a session.
46
+ export function databaseHooksFor(
47
+ config: Pick<AuthConfig, "requireSignupApproval">,
48
+ administrators: ReadonlySet<string>,
49
+ signupOpen: () => Promise<boolean>,
50
+ ): NonNullable<BetterAuthOptions["databaseHooks"]> {
51
+ return {
52
+ user: {
53
+ create: {
54
+ before: async (user, context) => {
55
+ const createdByAdmin = context?.path === "/admin/create-user";
56
+ assertUserCreationAllowed(await readSignupOpen(signupOpen), context?.path);
57
+ const adminEmail = becomesAdministrator(user, createdByAdmin, administrators);
58
+ return {
59
+ data: {
60
+ ...user,
61
+ // The admin plugin contributes `role` before this hook. Preserve an
62
+ // explicitly requested admin-created role, while ADMIN_EMAILS
63
+ // addresses always become administrators.
64
+ ...(Object.prototype.hasOwnProperty.call(user, "role")
65
+ ? { role: adminEmail ? "admin" : user.role }
66
+ : {}),
67
+ // Existing rows are treated as approved through the field default.
68
+ // Only self-registration while the policy is enabled starts pending.
69
+ signupApproved: adminEmail || createdByAdmin || !config.requireSignupApproval,
70
+ },
71
+ };
72
+ },
73
+ },
74
+ },
75
+ session: {
76
+ create: {
77
+ before: async (session, context) => {
78
+ if (!context) return;
79
+ const user = (await context.context.internalAdapter.findUserById(session.userId)) as {
80
+ signupApproved?: boolean | null;
81
+ } | null;
82
+ if (user?.signupApproved === false) {
83
+ throw APIError.from("FORBIDDEN", {
84
+ code: SIGNUP_APPROVAL_REQUIRED,
85
+ message: "Your account is waiting for administrator approval.",
86
+ });
87
+ }
88
+ },
89
+ },
90
+ },
91
+ };
92
+ }
93
+
48
94
  // Build a better-auth instance from resolved config. Called at boot and rebuilt by
49
95
  // auth-provider.ts when the DB config changes, so admin edits (OAuth credentials,
50
96
  // server-enforced toggles) apply without a restart. Everything below the injection
@@ -147,47 +193,7 @@ export function buildAuth(config: AuthConfig) {
147
193
  // podokit:begin:auth-plugins
148
194
  // podokit:end:auth-plugins
149
195
 
150
- const databaseHooks: NonNullable<BetterAuthOptions["databaseHooks"]> = {
151
- user: {
152
- create: {
153
- before: async (user, context) => {
154
- const adminEmail = adminEmails.has(user.email.toLowerCase());
155
- const createdByAdmin = context?.path === "/admin/create-user";
156
- assertUserCreationAllowed(await readSignupOpen(isSignupOpen), context?.path);
157
- return {
158
- data: {
159
- ...user,
160
- // The admin plugin contributes `role` before this hook. Preserve an
161
- // explicitly requested admin-created role, while bootstrap emails
162
- // always become administrators.
163
- ...(Object.prototype.hasOwnProperty.call(user, "role")
164
- ? { role: adminEmail ? "admin" : user.role }
165
- : {}),
166
- // Existing rows are treated as approved through the field default.
167
- // Only self-registration while the policy is enabled starts pending.
168
- signupApproved: adminEmail || createdByAdmin || !config.requireSignupApproval,
169
- },
170
- };
171
- },
172
- },
173
- },
174
- session: {
175
- create: {
176
- before: async (session, context) => {
177
- if (!context) return;
178
- const user = (await context.context.internalAdapter.findUserById(session.userId)) as {
179
- signupApproved?: boolean | null;
180
- } | null;
181
- if (user?.signupApproved === false) {
182
- throw APIError.from("FORBIDDEN", {
183
- code: SIGNUP_APPROVAL_REQUIRED,
184
- message: "Your account is waiting for administrator approval.",
185
- });
186
- }
187
- },
188
- },
189
- },
190
- };
196
+ const databaseHooks = databaseHooksFor(config, adminEmails, isSignupOpen);
191
197
 
192
198
  // Request-time hooks. The feature gate turns the admin Settings toggles into a
193
199
  // real server boundary (disabled features 404) for endpoint calls, including
@@ -98,7 +98,7 @@ export MAIL_FROM="Example <noreply@example.com>"
98
98
  2. Open `/login` and confirm the provider button appears.
99
99
  3. With sign-up approval enabled, register a disposable account and confirm it reaches `/pending-approval` without a session.
100
100
  4. Open `/admin/users`, filter by **Pending approval**, approve the account, and confirm sign-in succeeds.
101
- 5. Confirm administrator-created users and emails in `ADMIN_EMAILS` do not enter the approval queue.
101
+ 5. Confirm administrator-created users and administrators made from `ADMIN_EMAILS` do not enter the approval queue, and that an email and password sign-up with an `ADMIN_EMAILS` address is refused.
102
102
  6. Send a real verification or password-reset email and inspect SPF, DKIM, and DMARC results in the receiving mailbox.
103
103
 
104
104
  ## Preserve provider independence
@@ -9,8 +9,15 @@ Do not create the first administrator through the public sign-up page.
9
9
 
10
10
  Set `ADMIN_EMAILS` in the same deployment environment used by the API. Include
11
11
  the exact lowercase-normalized address selected for the initial administrator.
12
- Keep this value after bootstrap so future matching sign-ups retain the same role
13
- policy.
12
+ Use the administrators' real addresses: with `NODE_ENV=production` the API
13
+ refuses to start when an entry is not an address or is on a reserved
14
+ documentation name such as `example.com`, and `podo deploy doctor` fails on it.
15
+
16
+ Keep this value after bootstrap. A later account with a listed address becomes
17
+ an administrator only when its address is proven as the account is created: by
18
+ `admin:bootstrap`, an administrator, a social provider that reports the email
19
+ verified, a magic link, or an email code. An email and password sign-up with a
20
+ listed address is refused, so nobody can claim it before its owner.
14
21
 
15
22
  ## Inject credentials for one command
16
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podosoft/podokit",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "An opinionated Bun and Elysia full-stack TypeScript starter toolkit and CLI with SvelteKit, TailwindCSS, shadcn-svelte, Docker, and k3s.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {