@murumets-ee/auth 0.5.1 → 0.6.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/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{server-juwZw1e-.mjs → runtime-CGqmBcZB.mjs} +2 -2
- package/dist/runtime-CGqmBcZB.mjs.map +1 -0
- package/dist/runtime-DRrHqQiA.d.mts +98 -0
- package/dist/runtime-DRrHqQiA.d.mts.map +1 -0
- package/dist/runtime.d.mts +4 -0
- package/dist/runtime.mjs +1 -0
- package/dist/schema-CSXsOlBD.d.mts +1105 -0
- package/dist/schema-CSXsOlBD.d.mts.map +1 -0
- package/dist/schema.d.mts +2 -1105
- package/package.json +8 -8
- package/dist/plugin-BY4sYim9.d.mts +0 -83
- package/dist/plugin-BY4sYim9.d.mts.map +0 -1
- package/dist/plugin.d.mts +0 -2
- package/dist/plugin.mjs +0 -2
- package/dist/plugin.mjs.map +0 -1
- package/dist/schema.d.mts.map +0 -1
- package/dist/server-juwZw1e-.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"types": "./dist/client.d.mts",
|
|
13
13
|
"import": "./dist/client.mjs"
|
|
14
14
|
},
|
|
15
|
-
"./
|
|
16
|
-
"types": "./dist/
|
|
17
|
-
"import": "./dist/
|
|
15
|
+
"./runtime": {
|
|
16
|
+
"types": "./dist/runtime.d.mts",
|
|
17
|
+
"import": "./dist/runtime.mjs"
|
|
18
18
|
},
|
|
19
19
|
"./schema": {
|
|
20
20
|
"types": "./dist/schema.d.mts",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"better-auth": "^1.6.5",
|
|
33
33
|
"drizzle-orm": "^0.45.2",
|
|
34
34
|
"server-only": "^0.0.1",
|
|
35
|
-
"@murumets-ee/core": "0.
|
|
36
|
-
"@murumets-ee/db": "0.
|
|
37
|
-
"@murumets-ee/entity": "0.
|
|
38
|
-
"@murumets-ee/logging": "0.
|
|
35
|
+
"@murumets-ee/core": "0.6.0",
|
|
36
|
+
"@murumets-ee/db": "0.6.0",
|
|
37
|
+
"@murumets-ee/entity": "0.6.0",
|
|
38
|
+
"@murumets-ee/logging": "0.6.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^22.10.5",
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { n as AuthConfig, t as AdminUser } from "./types-Dl_sE_9S.mjs";
|
|
2
|
-
import { Auth } from "better-auth";
|
|
3
|
-
import { Plugin } from "@murumets-ee/core";
|
|
4
|
-
//#region src/server.d.ts
|
|
5
|
-
/** Type of the auth server instance. Aliased from better-auth's generic
|
|
6
|
-
* `Auth` because the explicit annotation on `createAuthServer` means
|
|
7
|
-
* `ReturnType<typeof createAuthServer>` is already `BetterAuthBase`. */
|
|
8
|
-
type Auth$1 = Auth;
|
|
9
|
-
/**
|
|
10
|
-
* Structural interface for the server-side admin API methods.
|
|
11
|
-
*
|
|
12
|
-
* The widened `Auth` type (BetterAuthBase) doesn't expose admin plugin
|
|
13
|
-
* endpoints. This interface describes just the methods consumers need
|
|
14
|
-
* so they can access them without `as any`.
|
|
15
|
-
*
|
|
16
|
-
* Usage: `(auth.api as AuthAdminApi).listUsers(...)`
|
|
17
|
-
*/
|
|
18
|
-
interface AuthAdminApi {
|
|
19
|
-
listUsers: (opts: {
|
|
20
|
-
headers: Headers;
|
|
21
|
-
query: {
|
|
22
|
-
limit: number;
|
|
23
|
-
sortBy: string;
|
|
24
|
-
sortDirection: 'asc' | 'desc';
|
|
25
|
-
};
|
|
26
|
-
}) => Promise<{
|
|
27
|
-
users: AdminUser[];
|
|
28
|
-
total: number;
|
|
29
|
-
}>;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/plugin.d.ts
|
|
33
|
-
/** Auth with admin API — admin plugin is always loaded in createAuthServer */
|
|
34
|
-
type AuthWithAdmin = Auth$1 & {
|
|
35
|
-
api: AuthAdminApi;
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Get the auth server instance.
|
|
39
|
-
* Throws if the auth plugin hasn't been initialized yet.
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* ```typescript
|
|
43
|
-
* // app/api/auth/[...all]/route.ts (user writes this)
|
|
44
|
-
* import { toNextJsHandler } from 'better-auth/next-js'
|
|
45
|
-
* import { getAuth } from '@murumets-ee/auth'
|
|
46
|
-
*
|
|
47
|
-
* export const { GET, POST } = toNextJsHandler(getAuth())
|
|
48
|
-
* ```
|
|
49
|
-
*/
|
|
50
|
-
declare function getAuth(): AuthWithAdmin;
|
|
51
|
-
/**
|
|
52
|
-
* Is public sign-up allowed in this deployment?
|
|
53
|
-
*
|
|
54
|
-
* Defaults to `false` — first admin is bootstrapped via `lumi create-admin`.
|
|
55
|
-
* Set `auth({ signup: { enabled: true } })` in toolkit.config.ts to open.
|
|
56
|
-
*
|
|
57
|
-
* Use this to gate sign-up routes, hide "Sign up" links, and decide whether
|
|
58
|
-
* the sign-up form should render at all. Security-critical: when the flag is
|
|
59
|
-
* off, the signup HTTP gate also rejects `/sign-up/email` — this helper and
|
|
60
|
-
* the server-side gate must agree.
|
|
61
|
-
*/
|
|
62
|
-
declare function isSignupEnabled(): boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Create the auth toolkit plugin.
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* import { defineConfig } from '@murumets-ee/core'
|
|
69
|
-
* import { auth } from '@murumets-ee/auth'
|
|
70
|
-
*
|
|
71
|
-
* export default defineConfig({
|
|
72
|
-
* db: { url: process.env.DATABASE_URL! },
|
|
73
|
-
* entities: [Article, Category],
|
|
74
|
-
* plugins: [
|
|
75
|
-
* auth({ providers: ['email'] }),
|
|
76
|
-
* ],
|
|
77
|
-
* })
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
|
-
declare function auth(config?: AuthConfig): Plugin;
|
|
81
|
-
//#endregion
|
|
82
|
-
export { AuthAdminApi as a, Auth$1 as i, getAuth as n, isSignupEnabled as r, auth as t };
|
|
83
|
-
//# sourceMappingURL=plugin-BY4sYim9.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-BY4sYim9.d.mts","names":[],"sources":["../src/server.ts","../src/plugin.ts"],"mappings":";;;;;;;KAgYY,MAAA,GAAO,IAAA;;AC7WyB;;;;;;;;UDwX3B,YAAA;EACf,SAAA,GAAY,IAAA;IACV,OAAA,EAAS,OAAA;IACT,KAAA;MACE,KAAA;MACA,MAAA;MACA,aAAA;IAAA;EAAA,MAEE,OAAA;IAAU,KAAA,EAAO,SAAA;IAAa,KAAA;EAAA;AAAA;;;;KC7XjC,aAAA,GAAgB,MAAA;EAAS,GAAA,EAAK,YAAA;AAAA;;;;;;;;;;;;;;iBAuBnB,OAAA,CAAA,GAAW,aAAA;;;;;;;;;;AA1BiB;;iBA4C5B,eAAA,CAAA;;;;;;;AAlBhB;;;;;AAkBA;;;;;AAqBA;iBAAgB,IAAA,CAAK,MAAA,GAAQ,UAAA,GAAkB,MAAA"}
|
package/dist/plugin.d.mts
DELETED
package/dist/plugin.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as e,f as t,m as n,r,s as i,t as a,u as o}from"./schema-Je6e5yt2.mjs";let s=null,c=null;function l(){if(!s)throw Error(`@murumets-ee/auth not initialized. Add auth() to your toolkit config plugins.`);return s}function u(){return c?.signup?.enabled===!0}function d(l={}){return c=l,{name:`@murumets-ee/auth`,tables:{user:t,session:o,account:a,verification:n,organization:i,member:e,invitation:r},init:async e=>{let{createAuthServer:t}=await import(`./server-juwZw1e-.mjs`),n;if(l.audit!==!1){let{createAuditLogger:t,createAuditDbWriter:r,createLogger:i}=await import(`@murumets-ee/logging`);n=t({logger:i({name:`auth-audit`}),dbWriter:r(e.db.readWrite)})}s=t(l,e,n),e.logger.info(`Auth plugin initialized`)}}}export{d as auth,l as getAuth,u as isSignupEnabled};
|
|
2
|
-
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Toolkit plugin implementation for @murumets-ee/auth.\n *\n * Implements the Plugin interface from @murumets-ee/core.\n * Creates and stores the better-auth server instance during init.\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport {\n account,\n invitation,\n member,\n organization,\n session,\n user,\n verification,\n} from './schema.js'\nimport type { Auth, AuthAdminApi } from './server.js'\nimport type { AuthConfig } from './types.js'\n\n/** Auth with admin API — admin plugin is always loaded in createAuthServer */\ntype AuthWithAdmin = Auth & { api: AuthAdminApi }\n\n/** The initialized auth server instance (set during plugin init) */\nlet _auth: AuthWithAdmin | null = null\n\n/** The auth plugin config — captured at plugin() call so server-side code\n * (page.tsx guards, admin UIs) can read runtime flags like `signup.enabled`\n * without threading config through every layer. */\nlet _authConfig: AuthConfig | null = null\n\n/**\n * Get the auth server instance.\n * Throws if the auth plugin hasn't been initialized yet.\n *\n * @example\n * ```typescript\n * // app/api/auth/[...all]/route.ts (user writes this)\n * import { toNextJsHandler } from 'better-auth/next-js'\n * import { getAuth } from '@murumets-ee/auth'\n *\n * export const { GET, POST } = toNextJsHandler(getAuth())\n * ```\n */\nexport function getAuth(): AuthWithAdmin {\n if (!_auth) {\n throw new Error('@murumets-ee/auth not initialized. Add auth() to your toolkit config plugins.')\n }\n return _auth\n}\n\n/**\n * Is public sign-up allowed in this deployment?\n *\n * Defaults to `false` — first admin is bootstrapped via `lumi create-admin`.\n * Set `auth({ signup: { enabled: true } })` in toolkit.config.ts to open.\n *\n * Use this to gate sign-up routes, hide \"Sign up\" links, and decide whether\n * the sign-up form should render at all. Security-critical: when the flag is\n * off, the signup HTTP gate also rejects `/sign-up/email` — this helper and\n * the server-side gate must agree.\n */\nexport function isSignupEnabled(): boolean {\n return _authConfig?.signup?.enabled === true\n}\n\n/**\n * Create the auth toolkit plugin.\n *\n * @example\n * ```typescript\n * import { defineConfig } from '@murumets-ee/core'\n * import { auth } from '@murumets-ee/auth'\n *\n * export default defineConfig({\n * db: { url: process.env.DATABASE_URL! },\n * entities: [Article, Category],\n * plugins: [\n * auth({ providers: ['email'] }),\n * ],\n * })\n * ```\n */\nexport function auth(config: AuthConfig = {}): Plugin {\n // Capture the config eagerly so isSignupEnabled() works before init()\n // runs (server components that read it during render may resolve before\n // the plugin's async init completes on cold boot — the config is known\n // synchronously from the moment auth() is called).\n _authConfig = config\n return {\n name: '@murumets-ee/auth',\n // Expose better-auth's Drizzle tables so `lumi migrate` picks them up\n // automatically. The canonical schema lives in\n // `@murumets-ee/auth/schema` — committed, not codegen-generated.\n tables: { user, session, account, verification, organization, member, invitation },\n init: async (app) => {\n const { createAuthServer } = await import('./server.js')\n\n // Create audit logger for auth events (login, signup, ban, etc.)\n // unless explicitly disabled via `audit: false`\n let auditLogger: AuditLogger | undefined\n if (config.audit !== false) {\n const { createAuditLogger, createAuditDbWriter, createLogger } = await import(\n '@murumets-ee/logging'\n )\n auditLogger = createAuditLogger({\n logger: createLogger({ name: 'auth-audit' }),\n dbWriter: createAuditDbWriter(app.db.readWrite),\n })\n }\n\n // createAuthServer always loads the admin plugin, so listUsers etc.\n // exist at runtime. The return type is widened to BetterAuthBase for\n // DTS portability (TS2742 — zod@4 internal paths). This single\n // assertion bridges the gap so getAuth() consumers get typed admin API.\n _auth = createAuthServer(config, app, auditLogger) as AuthWithAdmin\n app.logger.info('Auth plugin initialized')\n },\n }\n}\n"],"mappings":"+EAyBA,IAAI,EAA8B,KAK9B,EAAiC,KAerC,SAAgB,GAAyB,CACvC,GAAI,CAAC,EACH,MAAU,MAAM,gFAAgF,CAElG,OAAO,EAcT,SAAgB,GAA2B,CACzC,OAAO,GAAa,QAAQ,UAAY,GAoB1C,SAAgB,EAAK,EAAqB,EAAE,CAAU,CAMpD,MADA,GAAc,EACP,CACL,KAAM,oBAIN,OAAQ,CAAE,OAAM,UAAS,UAAS,eAAc,eAAc,SAAQ,aAAY,CAClF,KAAM,KAAO,IAAQ,CACnB,GAAM,CAAE,oBAAqB,MAAM,OAAO,yBAItC,EACJ,GAAI,EAAO,QAAU,GAAO,CAC1B,GAAM,CAAE,oBAAmB,sBAAqB,gBAAiB,MAAM,OACrE,wBAEF,EAAc,EAAkB,CAC9B,OAAQ,EAAa,CAAE,KAAM,aAAc,CAAC,CAC5C,SAAU,EAAoB,EAAI,GAAG,UAAU,CAChD,CAAC,CAOJ,EAAQ,EAAiB,EAAQ,EAAK,EAAY,CAClD,EAAI,OAAO,KAAK,0BAA0B,EAE7C"}
|
package/dist/schema.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.mts","names":[],"sources":["../src/schema.ts"],"mappings":";;;;;;;;AA0BA;;;;;;;;;;;;;;;;;;cAAa,IAAA,yBAAI,kBAAA;;;;QAef,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEW,OAAA,yBAAO,kBAAA;;;;QAmBnB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,OAAA,yBAAO,kBAAA;;;;QAsBnB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,YAAA,yBAAY,kBAAA;;;;QAcxB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,YAAA,yBAAY,kBAAA;;;;QAWxB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,MAAA,yBAAM,kBAAA;;;;QAiBlB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,UAAA,yBAAU,kBAAA;;;;QAoBtB,sBAAA,CAAA,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAEY,aAAA,iBAAa,SAAA;YAKvB,cAAA,CAAA,IAAA;;;;;cAEU,gBAAA,EAKV,cAAA,CAL0B,SAAA;QAK1B,cAAA,CAAA,GAAA;AAAA;AAAA,cAEU,gBAAA,EAKV,cAAA,CAL0B,SAAA;QAK1B,cAAA,CAAA,GAAA;AAAA;AAAA,cAEU,qBAAA,iBAAqB,SAAA;WAG/B,cAAA,CAAA,IAAA;;;cAEU,eAAA,iBAAe,SAAA;gBASzB,cAAA,CAAA,GAAA;;;cAEU,mBAAA,iBAAmB,SAAA;gBAS7B,cAAA,CAAA,GAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"server-juwZw1e-.mjs","names":["defaultAuthSchema"],"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side auth instance factory.\n *\n * Creates a configured `betterAuth()` instance using the toolkit's database\n * connection and entity definitions. This is server-only code.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport type { Auth as BetterAuthBase, BetterAuthOptions } from 'better-auth'\nimport { betterAuth } from 'better-auth'\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle'\nimport { APIError, createAuthMiddleware } from 'better-auth/api'\nimport { nextCookies } from 'better-auth/next-js'\nimport { admin } from 'better-auth/plugins'\nimport type { Role } from 'better-auth/plugins/access'\nimport { createAccessControl } from 'better-auth/plugins/access'\nimport { organization } from 'better-auth/plugins/organization'\nimport { buildDefaultRoles, buildStatements } from './permissions.js'\nimport * as defaultAuthSchema from './schema.js'\nimport type { AdminUser, AuthConfig } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Audit hooks — wired into better-auth's databaseHooks\n// ---------------------------------------------------------------------------\n\n/** Extract acting admin's id and name from better-auth hook context.\n * ctx is GenericEndpointContext | null — we access session safely via optional chaining. */\nfunction getActor(ctx: unknown) {\n const session = (ctx as { context?: { session?: Record<string, unknown> } } | null)?.context\n ?.session\n const user = session?.user as { id?: string; name?: string } | undefined\n return {\n id: user?.id ?? (session?.userId as string | undefined),\n name: user?.name,\n }\n}\n\n/** Fields to strip from user update audit payloads */\nconst SKIP_FIELDS = new Set(['updatedAt', 'createdAt'])\n\nfunction buildDatabaseHooks(auditLogger?: AuditLogger) {\n /** Fire-and-forget audit — never block auth operations */\n function audit(entry: Parameters<AuditLogger['log']>[0]) {\n auditLogger?.log(entry).catch(() => {})\n }\n\n // `before` captures changed fields, `after` has full user + actor ctx.\n // Bridge with a simple variable — updates are sequential per request.\n let pendingFields: Record<string, unknown> | null = null\n\n return {\n user: {\n create: {\n after: async (user: Record<string, unknown>) => {\n // Signup — actor is the new user themselves.\n // NOTE: first-user auto-promotion was removed. Bootstrap the first\n // admin via `lumi create-admin` (requires shell access). Public\n // signup is closed by default; see AuthConfig.signup.enabled.\n audit({\n action: 'auth.signup',\n entityType: 'user',\n entityId: user.id as string,\n userId: user.id as string,\n userName: user.name as string,\n changes: {\n fields: { name: user.name, email: user.email },\n },\n })\n },\n },\n update: {\n // `before` receives only the changed fields — capture them\n before: async (userData: Record<string, unknown>) => {\n const fields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(userData)) {\n if (SKIP_FIELDS.has(key) || key === 'id') continue\n fields[key] = value\n }\n pendingFields = Object.keys(fields).length > 0 ? fields : null\n },\n // `after` has full user (name) + ctx (actor session) — log everything\n after: async (user: Record<string, unknown>, ctx: unknown) => {\n const actor = getActor(ctx)\n const fields = pendingFields\n pendingFields = null\n audit({\n action: 'auth.user.update',\n entityType: 'user',\n entityId: user.id as string,\n userId: actor.id,\n userName: actor.name,\n changes: fields ? { fields } : undefined,\n metadata: {\n targetUser: user.name as string | undefined,\n },\n })\n },\n },\n delete: {\n after: async (user: Record<string, unknown>, ctx: unknown) => {\n const actor = getActor(ctx)\n audit({\n action: 'auth.user.delete',\n entityType: 'user',\n entityId: user.id as string,\n userId: actor.id,\n userName: actor.name,\n metadata: {\n targetUser: user.name as string | undefined,\n },\n })\n },\n },\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Request-level hooks — catches failed logins, password changes, etc.\n// ---------------------------------------------------------------------------\n\n/** Auth paths where a non-2xx response means a failed attempt worth logging */\nconst SIGN_IN_PATHS = ['/sign-in/email', '/sign-in/social']\n\n/** Auth paths for session/password lifecycle events */\nconst SIGN_OUT_PATH = '/sign-out'\nconst PASSWORD_PATHS: Record<string, string> = {\n '/change-password': 'auth.password.change',\n '/forget-password': 'auth.password.reset_request',\n '/reset-password': 'auth.password.reset',\n}\n\n/** Paths that should be audit-logged when successful */\nconst AUDIT_ADMIN_PATHS: Record<string, string> = {\n '/admin/set-role': 'auth.admin.set_role',\n '/admin/ban-user': 'auth.admin.ban',\n '/admin/unban-user': 'auth.admin.unban',\n '/admin/create-user': 'auth.admin.create_user',\n '/admin/remove-user': 'auth.admin.remove_user',\n '/admin/impersonate-user': 'auth.admin.impersonate',\n '/admin/stop-impersonating': 'auth.admin.stop_impersonating',\n '/admin/revoke-session': 'auth.admin.revoke_session',\n '/admin/revoke-sessions': 'auth.admin.revoke_sessions',\n}\n\nfunction buildRequestHooks(auditLogger: AuditLogger) {\n function audit(entry: Parameters<AuditLogger['log']>[0]) {\n auditLogger.log(entry).catch(() => {})\n }\n\n return {\n after: createAuthMiddleware(async (ctx) => {\n // --- Login attempt logging ---\n const isSignIn = SIGN_IN_PATHS.some((p) => ctx.path.startsWith(p))\n if (isSignIn) {\n const returned = ctx.context.returned as { status?: number } | undefined\n const status = returned?.status\n if (!status) return\n\n if (status >= 400) {\n const email = (ctx.body as Record<string, unknown> | undefined)?.email\n audit({\n action: 'auth.login.failed',\n metadata: {\n ...(typeof email === 'string' ? { email } : {}),\n status,\n path: ctx.path,\n },\n })\n } else {\n const newSession = ctx.context.newSession as\n | { user?: { id?: string; name?: string } }\n | undefined\n if (newSession?.user?.id) {\n audit({\n action: 'auth.login',\n entityType: 'user',\n entityId: newSession.user.id,\n userId: newSession.user.id,\n userName: newSession.user.name,\n })\n }\n }\n return\n }\n\n // --- Logout logging ---\n if (ctx.path === SIGN_OUT_PATH) {\n const returned = ctx.context.returned as { status?: number } | undefined\n if (!returned?.status || returned.status < 400) {\n const actor = getActor(ctx)\n if (actor.id) {\n audit({\n action: 'auth.logout',\n entityType: 'user',\n entityId: actor.id,\n userId: actor.id,\n userName: actor.name,\n })\n }\n }\n return\n }\n\n // --- Password change/reset logging ---\n const passwordAction = PASSWORD_PATHS[ctx.path]\n if (passwordAction) {\n const returned = ctx.context.returned as { status?: number } | undefined\n if (!returned?.status || returned.status < 400) {\n const actor = getActor(ctx)\n const body = ctx.body as Record<string, unknown> | undefined\n audit({\n action: passwordAction,\n entityType: 'user',\n userId: actor.id,\n userName: actor.name,\n metadata: {\n // For reset requests, log the email (not sensitive — it's the input)\n ...(typeof body?.email === 'string' ? { email: body.email } : {}),\n path: ctx.path,\n },\n })\n }\n return\n }\n\n // --- Admin operation audit logging (impersonation, role changes, bans, etc.) ---\n const auditAction = AUDIT_ADMIN_PATHS[ctx.path]\n if (!auditAction) return\n\n const returned = ctx.context.returned as { status?: number } | undefined\n if (returned?.status && returned.status >= 400) return // failed — skip\n\n const actor = getActor(ctx)\n const body = ctx.body as Record<string, unknown> | undefined\n audit({\n action: auditAction,\n entityType: 'user',\n entityId: (body?.userId as string) ?? undefined,\n userId: actor.id,\n userName: actor.name,\n metadata: {\n targetUserId: body?.userId as string | undefined,\n ...(body?.role ? { role: body.role as string } : {}),\n path: ctx.path,\n },\n })\n }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Signup gate — rejects public registration unless explicitly enabled.\n//\n// Closed by default. The first admin is created via `lumi create-admin`\n// (runs below the HTTP/middleware layer via auth.$context.internalAdapter,\n// so this gate never fires for CLI bootstrap). To open public registration\n// later, set `auth({ signup: { enabled: true } })` in the toolkit config.\n// ---------------------------------------------------------------------------\n\nfunction buildSignupGate(signupEnabled: boolean) {\n return createAuthMiddleware(async (ctx) => {\n if (ctx.path !== '/sign-up/email') return\n if (!signupEnabled) {\n throw new APIError('FORBIDDEN', { message: 'Sign-up is closed' })\n }\n })\n}\n\n// ---------------------------------------------------------------------------\n// Server factory\n// ---------------------------------------------------------------------------\n\n/**\n * Create a better-auth server instance wired to the toolkit.\n *\n * Called during plugin init — the returned instance powers:\n * - `auth.api.getSession()` for session resolution\n * - `auth.api.listUsers()` for admin user management\n * - Route handler via `toNextJsHandler(auth)`\n *\n * IMPORTANT: Plugins are inlined in the `betterAuth()` call so TypeScript\n * preserves the literal plugin types. Extracting them into a `BetterAuthPlugin[]`\n * variable erases specific endpoint types (admin, organization, etc.).\n *\n * The explicit `BetterAuthBase` return type annotation is required because\n * better-auth 1.6's internals use zod@4 types that tsdown's dts generator\n * cannot name portably across pnpm's `.pnpm/zod@4.x` symlink paths (TS2742).\n * The annotation widens to better-auth's generic `Auth` type — specific\n * plugin endpoint types (`auth.api.listUsers` etc.) are still accessible\n * because better-auth's `InferAPI` helper resolves them at the consumer's\n * compile time against their installed better-auth version.\n */\nexport function createAuthServer(\n config: AuthConfig,\n app: ToolkitApp,\n auditLogger?: AuditLogger,\n): BetterAuthBase {\n const entities = [...app.entities.values()]\n const statement = buildStatements(entities)\n const ac = createAccessControl(statement)\n const roles: Record<string, Role> = buildDefaultRoles(ac, entities)\n\n // Build social provider config\n const socialProviders: Record<string, { clientId: string; clientSecret: string }> = {}\n if (config.social?.google) {\n socialProviders.google = config.social.google\n }\n if (config.social?.github) {\n socialProviders.github = config.social.github\n }\n\n // Plugins are inlined so TS infers the literal tuple type.\n // DO NOT extract into a typed variable — `BetterAuthPlugin[]` erases endpoint types.\n // nextCookies() must be last — required for Next.js server actions.\n // Default to the pre-baked schema from `@murumets-ee/auth/schema` so\n // consumers don't need to generate one with `@better-auth/cli`. They\n // can still override via `auth({ schema: customSchema })` if they've\n // extended better-auth with plugins that add tables.\n const schema = config.schema ?? defaultAuthSchema\n\n // Widen the config to BetterAuthOptions so betterAuth() returns\n // Auth<BetterAuthOptions> (= BetterAuthBase). This is required because:\n // 1. Without a return type annotation, tsdown fails with TS2742\n // (zod@4 internal types can't be named portably in .d.mts)\n // 2. The admin plugin with custom ac/roles makes Auth<SpecificConfig>\n // structurally incompatible with Auth (better-auth#8855)\n // Widening the config erases plugin-specific API types at compile time,\n // but plugin endpoints (listUsers, etc.) are fully functional at runtime.\n // Consumers already access them via typed wrappers in AdminPagesConfig.\n const authOptions: BetterAuthOptions = {\n database: drizzleAdapter(app.db.readWrite, {\n provider: 'pg',\n schema,\n }),\n\n emailAndPassword: {\n enabled: config.providers?.includes('email') ?? true,\n },\n\n socialProviders,\n\n session: {\n expiresIn: config.session?.expiresIn ?? 60 * 60 * 2, // 2 hours (admin CMS — short-lived)\n updateAge: config.session?.updateAge ?? 60 * 60, // 1 hour\n },\n\n // Rate limiting — strict on sensitive paths, relaxed global default\n rateLimit: {\n enabled: true,\n window: 60, // 60s global window\n max: 100, // 100 req/min default\n storage: 'memory',\n customRules: {\n '/sign-in/email': { window: 60, max: 5 }, // 5 login attempts/min\n '/sign-in/social': { window: 60, max: 10 },\n '/sign-up/email': { window: 60, max: 3 }, // 3 signups/min\n '/forget-password': { window: 60, max: 3 }, // 3 resets/min\n '/reset-password': { window: 60, max: 5 },\n '/admin/*': { window: 60, max: 20 }, // admin ops capped\n },\n },\n\n databaseHooks: buildDatabaseHooks(auditLogger),\n hooks: {\n before: buildSignupGate(config.signup?.enabled ?? false),\n ...(auditLogger ? buildRequestHooks(auditLogger) : {}),\n },\n\n plugins: [\n admin({ ac, roles, defaultRole: 'authenticated' }),\n ...(config.organizations ? [organization({ ac, roles })] : []),\n ...(config.betterAuthPlugins ?? []),\n nextCookies(),\n ],\n }\n\n return betterAuth(authOptions)\n}\n\n/** Type of the auth server instance. Aliased from better-auth's generic\n * `Auth` because the explicit annotation on `createAuthServer` means\n * `ReturnType<typeof createAuthServer>` is already `BetterAuthBase`. */\nexport type Auth = BetterAuthBase\n\n/**\n * Structural interface for the server-side admin API methods.\n *\n * The widened `Auth` type (BetterAuthBase) doesn't expose admin plugin\n * endpoints. This interface describes just the methods consumers need\n * so they can access them without `as any`.\n *\n * Usage: `(auth.api as AuthAdminApi).listUsers(...)`\n */\nexport interface AuthAdminApi {\n listUsers: (opts: {\n headers: Headers\n query: {\n limit: number\n sortBy: string\n sortDirection: 'asc' | 'desc'\n }\n }) => Promise<{ users: AdminUser[]; total: number }>\n}\n"],"mappings":"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"}
|