@murumets-ee/auth 0.11.0 → 0.12.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/admin/index.mjs +1 -1
- package/dist/admin/index.mjs.map +1 -1
- package/dist/client.d.mts +6 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +3 -49
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/permissions-BbVD2CmT.mjs +2 -0
- package/dist/{permissions-DLMd-3dc.mjs.map → permissions-BbVD2CmT.mjs.map} +1 -1
- package/dist/runtime-Cj8SEWhk.mjs +2 -0
- package/dist/runtime-Cj8SEWhk.mjs.map +1 -0
- package/dist/{runtime-DRrHqQiA.d.mts → runtime-D0zIyEjn.d.mts} +2 -2
- package/dist/runtime-D0zIyEjn.d.mts.map +1 -0
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +1 -1
- package/dist/{types-Dl_sE_9S.d.mts → types-Bkei6fxa.d.mts} +42 -2
- package/dist/types-Bkei6fxa.d.mts.map +1 -0
- package/package.json +8 -8
- package/dist/permissions-DLMd-3dc.mjs +0 -2
- package/dist/runtime-Bma4eOJY.mjs +0 -2
- package/dist/runtime-Bma4eOJY.mjs.map +0 -1
- package/dist/runtime-DRrHqQiA.d.mts.map +0 -1
- package/dist/types-Dl_sE_9S.d.mts.map +0 -1
package/dist/admin/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{t as e}from"../permissions-BbVD2CmT.mjs";const t=/^[a-z][a-z0-9-]{0,49}$/;function n(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function r(e,t){return n({error:e},t)}function i(i){let{getStatements:a,loadRoles:o,saveRoles:s,onSave:c}=i;return{prefix:`permissions`,resource:`permissions`,actions:[`view`,`create`,`update`,`delete`],handlers:{GET:async(t,{segments:r})=>{let i=a(),s=await o()??{};return r.length===1&&r[0]===`roles`?n({roles:Object.keys(s),builtInRoles:[...e]}):n({statements:i,roles:s,builtInRoles:[...e]})},PATCH:async(e,{user:t,audit:i})=>{let{roles:l}=await e.json();if(!l||typeof l!=`object`)return r(`Body must contain "roles" object`,400);if(`admin`in l)return r(`Cannot modify admin role permissions (admin always has full access)`,400);if(t.role&&t.role!==`admin`&&t.role in l)return r(`Cannot modify permissions for your own role`,403);let u=await o()??{};for(let e of Object.keys(l))if(!(e in u))return r(`Role '${e}' does not exist. Create it first via POST /permissions/roles`,400);let d=a();for(let[e,t]of Object.entries(l)){if(typeof e!=`string`||!e)return r(`Role names must be non-empty strings`,400);if(typeof t!=`object`||!t||Array.isArray(t))return r(`Permissions for role '${e}' must be an object`,400);for(let[n,i]of Object.entries(t)){if(!Array.isArray(i)||!i.every(e=>typeof e==`string`))return r(`Actions for '${n}' in role '${e}' must be a string array`,400);let t=d[n];if(!t)return r(`Unknown resource: ${n}`,400);for(let e of i)if(!t.includes(e))return r(`Invalid action '${e}' for resource '${n}'. Valid: ${t.join(`, `)}`,400)}}let f={...u};for(let[e,t]of Object.entries(l))f[e]=t;return await s(f),c?.(),i?.({action:`permissions.update`,entityType:`permissions`,userId:t.id,...t.name!==void 0&&{userName:t.name},changes:{roles:l},metadata:{rolesModified:Object.keys(l)}}),n({ok:!0})},POST:async(i,{segments:a,user:l,audit:u})=>{if(a.length!==1||a[0]!==`roles`)return r(`POST only supported at /permissions/roles`,400);let{name:d}=await i.json();if(!d||typeof d!=`string`)return r(`Body must contain "name" string`,400);if(!t.test(d))return r(`Role name must be lowercase alphanumeric with hyphens, start with a letter, max 50 chars`,400);if(e.includes(d))return r(`Cannot create role with built-in name: ${d}`,400);let f=await o()??{};return d in f?r(`Role already exists: ${d}`,409):(f[d]={},await s(f),c?.(),u?.({action:`permissions.role.create`,entityType:`permissions`,userId:l.id,...l.name!==void 0&&{userName:l.name},changes:{roleName:d}}),n({name:d,permissions:{}},201))},DELETE:async(t,{segments:i,user:a,audit:l})=>{if(i.length!==2||i[0]!==`roles`)return r(`DELETE only supported at /permissions/roles/:name`,400);let u=i[1];if(!u)return r(`Missing role name`,400);if(e.includes(u))return r(`Cannot delete built-in role: ${u}`,400);let d=await o()??{};if(!(u in d))return r(`Role not found: ${u}`,404);let f=d[u];return delete d[u],await s(d),c?.(),l?.({action:`permissions.role.delete`,entityType:`permissions`,userId:a.id,...a.name!==void 0&&{userName:a.name},changes:{roleName:u,permissions:f}}),n({deleted:u})}}}}export{i as permissionRoutes};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/admin/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/admin/routes.ts"],"sourcesContent":["/**\n * Permission management admin routes for the centralized admin API handler.\n *\n * Provides CRUD for role definitions and permission assignments.\n * Admin-only by default (resource: 'permissions', only admin has access).\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { permissionRoutes } from '@murumets-ee/auth/admin'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [...],\n * routes: [\n * permissionRoutes({\n * getStatements: () => catalog,\n * loadRoles: () => client.get('roles'),\n * saveRoles: (roles) => client.set('roles', roles),\n * onSave: () => { cachedChecker = null },\n * }),\n * ],\n * })\n * ```\n */\n\nimport { BUILT_IN_ROLES } from '../permissions.js'\n\n// ---------------------------------------------------------------------------\n// Local route type — avoids circular build dep: auth → admin-ui → core → auth.\n// Structurally compatible with AdminRoute from @murumets-ee/core.\n// ---------------------------------------------------------------------------\n\n/** Fire-and-forget audit log function — structurally matches AuditLogFn from @murumets-ee/core */\ntype AuditLogFn = (entry: {\n action: string\n entityType?: string\n entityId?: string\n userId?: string\n userName?: string\n changes?: Record<string, unknown>\n metadata?: Record<string, unknown>\n}) => void\n\ninterface AdminRoute {\n prefix: string\n resource?: string\n actions?: readonly string[]\n handlers: Partial<\n Record<\n string,\n (\n req: Request,\n ctx: {\n segments: string[]\n user: { id: string; role?: string; name?: string }\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ) => Promise<Response>\n >\n >\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst ROLE_NAME_REGEX = /^[a-z][a-z0-9-]{0,49}$/\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfunction json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction errorJson(message: string, status: number): Response {\n return json({ error: message }, status)\n}\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\nexport interface PermissionRoutesConfig {\n /** Returns the resource catalog: resource → available actions */\n getStatements: () => Record<string, readonly string[]>\n /** Load saved role definitions from settings (null if first run) */\n loadRoles: () => Promise<Record<string, Record<string, string[]>> | null>\n /** Save role definitions to settings */\n saveRoles: (roles: Record<string, Record<string, string[]>>) => Promise<void>\n /** Called after save to invalidate cached permission checker */\n onSave?: () => void\n}\n\n/**\n * Create admin API routes for permission management.\n *\n * Routes (all prefixed with `/api/admin/permissions`):\n * - `GET /permissions` — Get statements + roles + builtInRoles\n * - `PATCH /permissions` — Update role permissions\n * - `POST /permissions/roles` — Create a custom role\n * - `DELETE /permissions/roles/:name` — Delete a custom role\n */\nexport function permissionRoutes(config: PermissionRoutesConfig): AdminRoute {\n const { getStatements, loadRoles, saveRoles, onSave } = config\n\n return {\n prefix: 'permissions',\n resource: 'permissions',\n actions: ['view', 'create', 'update', 'delete'],\n handlers: {\n // GET /permissions — full permission data for the editor\n GET: async (_req, { segments }) => {\n const statements = getStatements()\n const roles = (await loadRoles()) ?? {}\n\n // GET /permissions/roles — just role names\n if (segments.length === 1 && segments[0] === 'roles') {\n return json({\n roles: Object.keys(roles),\n builtInRoles: [...BUILT_IN_ROLES],\n })\n }\n\n return json({\n statements,\n roles,\n builtInRoles: [...BUILT_IN_ROLES],\n })\n },\n\n // PATCH /permissions — update role permissions\n PATCH: async (req, { user, audit }) => {\n const body = await req.json()\n const { roles } = body as { roles?: Record<string, Record<string, string[]>> }\n\n if (!roles || typeof roles !== 'object') {\n return errorJson('Body must contain \"roles\" object', 400)\n }\n\n // Validate: admin cannot be modified\n if ('admin' in roles) {\n return errorJson(\n 'Cannot modify admin role permissions (admin always has full access)',\n 400,\n )\n }\n\n // Prevent self-escalation: non-admin users cannot modify their own role\n if (user.role && user.role !== 'admin' && user.role in roles) {\n return errorJson('Cannot modify permissions for your own role', 403)\n }\n\n // Validate: only existing roles can be modified (use POST /roles to create)\n const existing = (await loadRoles()) ?? {}\n for (const roleName of Object.keys(roles)) {\n if (!(roleName in existing)) {\n return errorJson(\n `Role '${roleName}' does not exist. Create it first via POST /permissions/roles`,\n 400,\n )\n }\n }\n\n // Validate: actions must be in statements\n const statements = getStatements()\n for (const [roleName, resources] of Object.entries(roles)) {\n if (typeof roleName !== 'string' || !roleName) {\n return errorJson('Role names must be non-empty strings', 400)\n }\n if (typeof resources !== 'object' || resources === null || Array.isArray(resources)) {\n return errorJson(`Permissions for role '${roleName}' must be an object`, 400)\n }\n for (const [resource, actions] of Object.entries(resources)) {\n if (!Array.isArray(actions) || !actions.every((a) => typeof a === 'string')) {\n return errorJson(\n `Actions for '${resource}' in role '${roleName}' must be a string array`,\n 400,\n )\n }\n const validActions = statements[resource]\n if (!validActions) {\n return errorJson(`Unknown resource: ${resource}`, 400)\n }\n for (const action of actions) {\n if (!validActions.includes(action)) {\n return errorJson(\n `Invalid action '${action}' for resource '${resource}'. Valid: ${validActions.join(', ')}`,\n 400,\n )\n }\n }\n }\n }\n\n // Merge into existing roles (don't drop roles not in the payload)\n const merged = { ...existing }\n for (const [roleName, resources] of Object.entries(roles)) {\n merged[roleName] = resources\n }\n\n await saveRoles(merged)\n onSave?.()\n\n audit?.({\n action: 'permissions.update',\n entityType: 'permissions',\n userId: user.id,\n userName: user.name,\n changes: { roles },\n metadata: { rolesModified: Object.keys(roles) },\n })\n\n return json({ ok: true })\n },\n\n // POST /permissions/roles — create a new custom role\n POST: async (req, { segments, user, audit }) => {\n if (segments.length !== 1 || segments[0] !== 'roles') {\n return errorJson('POST only supported at /permissions/roles', 400)\n }\n\n const body = await req.json()\n const { name } = body as { name?: string }\n\n if (!name || typeof name !== 'string') {\n return errorJson('Body must contain \"name\" string', 400)\n }\n\n if (!ROLE_NAME_REGEX.test(name)) {\n return errorJson(\n 'Role name must be lowercase alphanumeric with hyphens, start with a letter, max 50 chars',\n 400,\n )\n }\n\n if ((BUILT_IN_ROLES as readonly string[]).includes(name)) {\n return errorJson(`Cannot create role with built-in name: ${name}`, 400)\n }\n\n const existing = (await loadRoles()) ?? {}\n\n if (name in existing) {\n return errorJson(`Role already exists: ${name}`, 409)\n }\n\n // New role starts with zero permissions\n existing[name] = {}\n await saveRoles(existing)\n onSave?.()\n\n audit?.({\n action: 'permissions.role.create',\n entityType: 'permissions',\n userId: user.id,\n userName: user.name,\n changes: { roleName: name },\n })\n\n return json({ name, permissions: {} }, 201)\n },\n\n // DELETE /permissions/roles/:name — delete a custom role\n DELETE: async (_req, { segments, user, audit }) => {\n if (segments.length !== 2 || segments[0] !== 'roles') {\n return errorJson('DELETE only supported at /permissions/roles/:name', 400)\n }\n\n const name = segments[1]\n\n if ((BUILT_IN_ROLES as readonly string[]).includes(name)) {\n return errorJson(`Cannot delete built-in role: ${name}`, 400)\n }\n\n const existing = (await loadRoles()) ?? {}\n\n if (!(name in existing)) {\n return errorJson(`Role not found: ${name}`, 404)\n }\n\n const deletedPermissions = existing[name]\n delete existing[name]\n await saveRoles(existing)\n onSave?.()\n\n audit?.({\n action: 'permissions.role.delete',\n entityType: 'permissions',\n userId: user.id,\n userName: user.name,\n changes: { roleName: name, permissions: deletedPermissions },\n })\n\n return json({ deleted: name })\n },\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/admin/routes.ts"],"sourcesContent":["/**\n * Permission management admin routes for the centralized admin API handler.\n *\n * Provides CRUD for role definitions and permission assignments.\n * Admin-only by default (resource: 'permissions', only admin has access).\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { permissionRoutes } from '@murumets-ee/auth/admin'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [...],\n * routes: [\n * permissionRoutes({\n * getStatements: () => catalog,\n * loadRoles: () => client.get('roles'),\n * saveRoles: (roles) => client.set('roles', roles),\n * onSave: () => { cachedChecker = null },\n * }),\n * ],\n * })\n * ```\n */\n\nimport { BUILT_IN_ROLES } from '../permissions.js'\n\n// ---------------------------------------------------------------------------\n// Local route type — avoids circular build dep: auth → admin-ui → core → auth.\n// Structurally compatible with AdminRoute from @murumets-ee/core.\n// ---------------------------------------------------------------------------\n\n/** Fire-and-forget audit log function — structurally matches AuditLogFn from @murumets-ee/core */\ntype AuditLogFn = (entry: {\n action: string\n entityType?: string\n entityId?: string\n userId?: string\n userName?: string\n changes?: Record<string, unknown>\n metadata?: Record<string, unknown>\n}) => void\n\ninterface AdminRoute {\n prefix: string\n resource?: string\n actions?: readonly string[]\n handlers: Partial<\n Record<\n string,\n (\n req: Request,\n ctx: {\n segments: string[]\n user: { id: string; role?: string; name?: string }\n audit?: AuditLogFn\n checkPermission: (resource: string, action: string) => boolean\n },\n ) => Promise<Response>\n >\n >\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\nconst ROLE_NAME_REGEX = /^[a-z][a-z0-9-]{0,49}$/\n\n// ---------------------------------------------------------------------------\n// Response helpers\n// ---------------------------------------------------------------------------\n\nfunction json(data: unknown, status = 200): Response {\n return new Response(JSON.stringify(data), {\n status,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\nfunction errorJson(message: string, status: number): Response {\n return json({ error: message }, status)\n}\n\n// ---------------------------------------------------------------------------\n// Route factory\n// ---------------------------------------------------------------------------\n\nexport interface PermissionRoutesConfig {\n /** Returns the resource catalog: resource → available actions */\n getStatements: () => Record<string, readonly string[]>\n /** Load saved role definitions from settings (null if first run) */\n loadRoles: () => Promise<Record<string, Record<string, string[]>> | null>\n /** Save role definitions to settings */\n saveRoles: (roles: Record<string, Record<string, string[]>>) => Promise<void>\n /** Called after save to invalidate cached permission checker */\n onSave?: () => void\n}\n\n/**\n * Create admin API routes for permission management.\n *\n * Routes (all prefixed with `/api/admin/permissions`):\n * - `GET /permissions` — Get statements + roles + builtInRoles\n * - `PATCH /permissions` — Update role permissions\n * - `POST /permissions/roles` — Create a custom role\n * - `DELETE /permissions/roles/:name` — Delete a custom role\n */\nexport function permissionRoutes(config: PermissionRoutesConfig): AdminRoute {\n const { getStatements, loadRoles, saveRoles, onSave } = config\n\n return {\n prefix: 'permissions',\n resource: 'permissions',\n actions: ['view', 'create', 'update', 'delete'],\n handlers: {\n // GET /permissions — full permission data for the editor\n GET: async (_req, { segments }) => {\n const statements = getStatements()\n const roles = (await loadRoles()) ?? {}\n\n // GET /permissions/roles — just role names\n if (segments.length === 1 && segments[0] === 'roles') {\n return json({\n roles: Object.keys(roles),\n builtInRoles: [...BUILT_IN_ROLES],\n })\n }\n\n return json({\n statements,\n roles,\n builtInRoles: [...BUILT_IN_ROLES],\n })\n },\n\n // PATCH /permissions — update role permissions\n PATCH: async (req, { user, audit }) => {\n const body = await req.json()\n const { roles } = body as { roles?: Record<string, Record<string, string[]>> }\n\n if (!roles || typeof roles !== 'object') {\n return errorJson('Body must contain \"roles\" object', 400)\n }\n\n // Validate: admin cannot be modified\n if ('admin' in roles) {\n return errorJson(\n 'Cannot modify admin role permissions (admin always has full access)',\n 400,\n )\n }\n\n // Prevent self-escalation: non-admin users cannot modify their own role\n if (user.role && user.role !== 'admin' && user.role in roles) {\n return errorJson('Cannot modify permissions for your own role', 403)\n }\n\n // Validate: only existing roles can be modified (use POST /roles to create)\n const existing = (await loadRoles()) ?? {}\n for (const roleName of Object.keys(roles)) {\n if (!(roleName in existing)) {\n return errorJson(\n `Role '${roleName}' does not exist. Create it first via POST /permissions/roles`,\n 400,\n )\n }\n }\n\n // Validate: actions must be in statements\n const statements = getStatements()\n for (const [roleName, resources] of Object.entries(roles)) {\n if (typeof roleName !== 'string' || !roleName) {\n return errorJson('Role names must be non-empty strings', 400)\n }\n if (typeof resources !== 'object' || resources === null || Array.isArray(resources)) {\n return errorJson(`Permissions for role '${roleName}' must be an object`, 400)\n }\n for (const [resource, actions] of Object.entries(resources)) {\n if (!Array.isArray(actions) || !actions.every((a) => typeof a === 'string')) {\n return errorJson(\n `Actions for '${resource}' in role '${roleName}' must be a string array`,\n 400,\n )\n }\n const validActions = statements[resource]\n if (!validActions) {\n return errorJson(`Unknown resource: ${resource}`, 400)\n }\n for (const action of actions) {\n if (!validActions.includes(action)) {\n return errorJson(\n `Invalid action '${action}' for resource '${resource}'. Valid: ${validActions.join(', ')}`,\n 400,\n )\n }\n }\n }\n }\n\n // Merge into existing roles (don't drop roles not in the payload)\n const merged = { ...existing }\n for (const [roleName, resources] of Object.entries(roles)) {\n merged[roleName] = resources\n }\n\n await saveRoles(merged)\n onSave?.()\n\n audit?.({\n action: 'permissions.update',\n entityType: 'permissions',\n userId: user.id,\n ...(user.name !== undefined && { userName: user.name }),\n changes: { roles },\n metadata: { rolesModified: Object.keys(roles) },\n })\n\n return json({ ok: true })\n },\n\n // POST /permissions/roles — create a new custom role\n POST: async (req, { segments, user, audit }) => {\n if (segments.length !== 1 || segments[0] !== 'roles') {\n return errorJson('POST only supported at /permissions/roles', 400)\n }\n\n const body = await req.json()\n const { name } = body as { name?: string }\n\n if (!name || typeof name !== 'string') {\n return errorJson('Body must contain \"name\" string', 400)\n }\n\n if (!ROLE_NAME_REGEX.test(name)) {\n return errorJson(\n 'Role name must be lowercase alphanumeric with hyphens, start with a letter, max 50 chars',\n 400,\n )\n }\n\n if ((BUILT_IN_ROLES as readonly string[]).includes(name)) {\n return errorJson(`Cannot create role with built-in name: ${name}`, 400)\n }\n\n const existing = (await loadRoles()) ?? {}\n\n if (name in existing) {\n return errorJson(`Role already exists: ${name}`, 409)\n }\n\n // New role starts with zero permissions\n existing[name] = {}\n await saveRoles(existing)\n onSave?.()\n\n audit?.({\n action: 'permissions.role.create',\n entityType: 'permissions',\n userId: user.id,\n ...(user.name !== undefined && { userName: user.name }),\n changes: { roleName: name },\n })\n\n return json({ name, permissions: {} }, 201)\n },\n\n // DELETE /permissions/roles/:name — delete a custom role\n DELETE: async (_req, { segments, user, audit }) => {\n if (segments.length !== 2 || segments[0] !== 'roles') {\n return errorJson('DELETE only supported at /permissions/roles/:name', 400)\n }\n\n const name = segments[1]\n if (!name) {\n return errorJson('Missing role name', 400)\n }\n\n if ((BUILT_IN_ROLES as readonly string[]).includes(name)) {\n return errorJson(`Cannot delete built-in role: ${name}`, 400)\n }\n\n const existing = (await loadRoles()) ?? {}\n\n if (!(name in existing)) {\n return errorJson(`Role not found: ${name}`, 404)\n }\n\n const deletedPermissions = existing[name]\n delete existing[name]\n await saveRoles(existing)\n onSave?.()\n\n audit?.({\n action: 'permissions.role.delete',\n entityType: 'permissions',\n userId: user.id,\n ...(user.name !== undefined && { userName: user.name }),\n changes: { roleName: name, permissions: deletedPermissions },\n })\n\n return json({ deleted: name })\n },\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as AdminUser } from "./types-
|
|
1
|
+
import { t as AdminUser } from "./types-Bkei6fxa.mjs";
|
|
2
2
|
import { createAuthClient } from "better-auth/react";
|
|
3
3
|
|
|
4
4
|
//#region src/client.d.ts
|
|
@@ -74,6 +74,11 @@ interface AuthClientWithAdmin {
|
|
|
74
74
|
email: string;
|
|
75
75
|
password: string;
|
|
76
76
|
role: string;
|
|
77
|
+
/** Extra fields spread into the new user row by better-auth's admin
|
|
78
|
+
* plugin. We use this to set `emailVerified: true` for admin-created
|
|
79
|
+
* accounts so they aren't locked out when public-signup deployments
|
|
80
|
+
* default `requireEmailVerification: true`. */
|
|
81
|
+
data?: Record<string, unknown>;
|
|
77
82
|
}) => Promise<BetterAuthResponse>;
|
|
78
83
|
updateUser: (opts: {
|
|
79
84
|
userId: string;
|
package/dist/client.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;UAWiB,iBAAA;EAgCD;EA9Bd,OAAA;;EAEA,aAAA;AAAA;;;;;;;;;;;;AAsCD;;;;;;;;;;;AAYkB;;;iBAtBH,YAAA,CACd,OAAA,GAAU,iBAAA,GACT,UAAA,QAAkB,gBAAA,IAAoB,mBAAA;;;;;UAkB/B,kBAAA;EACR,IAAA,GAAO,CAAA;EACP,KAAA;IAAU,OAAA;EAAA;AAAA;;;;;;;;;;UAYF,mBAAA;EACR,KAAA;IACE,SAAA,GAAY,IAAA;MACV,KAAA;QACE,KAAA;QACA,MAAA;QACA,MAAA;QACA,aAAA;QACA,WAAA;QACA,WAAA;QACA,cAAA;MAAA;IAAA,MAEE,OAAA,CAAQ,kBAAA;MAAqB,KAAA,EAAO,SAAA;MAAa,KAAA;IAAA;IACvD,UAAA,GAAa,IAAA;MACX,IAAA;MACA,KAAA;MACA,QAAA;MACA,IAAA;IAAA,
|
|
1
|
+
{"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;UAWiB,iBAAA;EAgCD;EA9Bd,OAAA;;EAEA,aAAA;AAAA;;;;;;;;;;;;AAsCD;;;;;;;;;;;AAYkB;;;iBAtBH,YAAA,CACd,OAAA,GAAU,iBAAA,GACT,UAAA,QAAkB,gBAAA,IAAoB,mBAAA;;;;;UAkB/B,kBAAA;EACR,IAAA,GAAO,CAAA;EACP,KAAA;IAAU,OAAA;EAAA;AAAA;;;;;;;;;;UAYF,mBAAA;EACR,KAAA;IACE,SAAA,GAAY,IAAA;MACV,KAAA;QACE,KAAA;QACA,MAAA;QACA,MAAA;QACA,aAAA;QACA,WAAA;QACA,WAAA;QACA,cAAA;MAAA;IAAA,MAEE,OAAA,CAAQ,kBAAA;MAAqB,KAAA,EAAO,SAAA;MAAa,KAAA;IAAA;IACvD,UAAA,GAAa,IAAA;MACX,IAAA;MACA,KAAA;MACA,QAAA;MACA,IAAA;MAAA;;;;MAKA,IAAA,GAAO,MAAA;IAAA,MACH,OAAA,CAAQ,kBAAA;IACd,UAAA,GAAa,IAAA;MACX,MAAA;MACA,IAAA,EAAM,MAAA;IAAA,MACF,OAAA,CAAQ,kBAAA;IACd,UAAA,GAAa,IAAA;MAAQ,MAAA;IAAA,MAAqB,OAAA,CAAQ,kBAAA;IAClD,OAAA,GAAU,IAAA;MAAQ,MAAA;MAAgB,IAAA;IAAA,MAAmB,OAAA,CAAQ,kBAAA;IAC7D,OAAA,GAAU,IAAA;MACR,MAAA;MACA,SAAA;MACA,YAAA;IAAA,MACI,OAAA,CAAQ,kBAAA;IACd,SAAA,GAAY,IAAA;MAAQ,MAAA;IAAA,MAAqB,OAAA,CAAQ,kBAAA;IACjD,kBAAA,GAAqB,IAAA;MAAQ,MAAA;IAAA,MAAqB,OAAA,CAAQ,kBAAA;EAAA;AAAA;AAAA,UAIpD,cAAA;EACR,KAAA;EACA,MAAA;EACA,MAAA;EACA,aAAA;EACA,WAAA;EACA,WAAA;EACA,cAAA;AAAA;AAAA,UAGQ,cAAA;EACR,EAAA;EACA,IAAA;EACA,KAAA;EACA,aAAA;EACA,KAAA;EACA,SAAA;EACA,SAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA;AAAA;;;;;AAdc;;;;;;;;;;;;iBAiCA,cAAA,CAAe,MAAA,EAAQ,mBAAA;cAYjB,cAAA,GAAiB,OAAA;IAAU,KAAA,EAAO,cAAA;IAAkB,KAAA;EAAA;;IAsBjD,IAAA;IAAc,KAAA;IAAe,QAAA;IAAkB,IAAA;EAAA,IAAc,OAAA;yBAcvD,IAAA;IAAU,IAAA;EAAA,IAAe,OAAA;0BAKzB,OAAA;0BAKC,IAAA,WAAc,OAAA;sBAKlB,OAAA;IAAc,MAAA;IAAiB,SAAA;EAAA,IAAoB,OAAA;yBASjD,OAAA;kCAKS,OAAA;AAAA"}
|
package/dist/client.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{adminClient as e,organizationClient as t}from"better-auth/client/plugins";import{createAuthClient as n}from"better-auth/react";function r(r){return n({baseURL:r?.baseURL,plugins:[e(),...r?.organizations?[t()]:[]]})}function i(e){function t(e){if(e.error)throw Error(e.error.message??`Unknown error`);return e.data}return{async list(n){let r=t(await e.admin.listUsers({query:{limit:n.limit,offset:n.offset,...n.sortBy?{sortBy:n.sortBy,sortDirection:n.sortDirection}:{},...n.searchValue?{searchValue:n.searchValue,searchField:n.searchField??`email`,searchOperator:n.searchOperator??`contains`}:{}}}));return{users:(r.users??[]).map(a),total:r.total??0}},async create(n){t(await e.admin.createUser({name:n.name,email:n.email,password:n.password,role:n.role}))},async update(n,r){t(await e.admin.updateUser({userId:n,data:r}))},async remove(n){t(await e.admin.removeUser({userId:n}))},async setRole(n,r){t(await e.admin.setRole({userId:n,role:r}))},async ban(n,r){t(await e.admin.banUser({userId:n,...r?.reason?{banReason:r.reason}:{},...r?.expiresIn?{banExpiresIn:r.expiresIn}:{}}))},async unban(n){t(await e.admin.unbanUser({userId:n}))},async revokeSessions(n){t(await e.admin.revokeUserSessions({userId:n}))}}}function a(e){return{id:e.id,name:e.name??``,email:e.email,emailVerified:e.emailVerified??!1,image:e.image??null,createdAt:e.createdAt instanceof Date?e.createdAt.toISOString():String(e.createdAt??``),updatedAt:e.updatedAt instanceof Date?e.updatedAt.toISOString():String(e.updatedAt??``),role:e.role??null,banned:e.banned??null,banReason:e.banReason??null,banExpires:e.banExpires instanceof Date?e.banExpires.toISOString():e.banExpires?String(e.banExpires):null}}export{r as createClient,i as createUsersApi};
|
|
1
|
+
import{adminClient as e,organizationClient as t}from"better-auth/client/plugins";import{createAuthClient as n}from"better-auth/react";function r(r){return n({baseURL:r?.baseURL,plugins:[e(),...r?.organizations?[t()]:[]]})}function i(e){function t(e){if(e.error)throw Error(e.error.message??`Unknown error`);return e.data}return{async list(n){let r=t(await e.admin.listUsers({query:{limit:n.limit,offset:n.offset,...n.sortBy?{sortBy:n.sortBy,sortDirection:n.sortDirection}:{},...n.searchValue?{searchValue:n.searchValue,searchField:n.searchField??`email`,searchOperator:n.searchOperator??`contains`}:{}}}));return{users:(r.users??[]).map(a),total:r.total??0}},async create(n){t(await e.admin.createUser({name:n.name,email:n.email,password:n.password,role:n.role,data:{emailVerified:!0}}))},async update(n,r){t(await e.admin.updateUser({userId:n,data:r}))},async remove(n){t(await e.admin.removeUser({userId:n}))},async setRole(n,r){t(await e.admin.setRole({userId:n,role:r}))},async ban(n,r){t(await e.admin.banUser({userId:n,...r?.reason?{banReason:r.reason}:{},...r?.expiresIn?{banExpiresIn:r.expiresIn}:{}}))},async unban(n){t(await e.admin.unbanUser({userId:n}))},async revokeSessions(n){t(await e.admin.revokeUserSessions({userId:n}))}}}function a(e){return{id:e.id,name:e.name??``,email:e.email,emailVerified:e.emailVerified??!1,image:e.image??null,createdAt:e.createdAt instanceof Date?e.createdAt.toISOString():String(e.createdAt??``),updatedAt:e.updatedAt instanceof Date?e.updatedAt.toISOString():String(e.updatedAt??``),role:e.role??null,banned:e.banned??null,banReason:e.banReason??null,banExpires:e.banExpires instanceof Date?e.banExpires.toISOString():e.banExpires?String(e.banExpires):null}}export{r as createClient,i as createUsersApi};
|
|
2
2
|
//# sourceMappingURL=client.mjs.map
|
package/dist/client.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Browser-safe auth client factory.\n *\n * This module has NO server-only imports and is safe to use in React components.\n * Import from '@murumets-ee/auth/client'.\n */\n\nimport { adminClient, organizationClient } from 'better-auth/client/plugins'\nimport { createAuthClient } from 'better-auth/react'\nimport type { AdminUser } from './types.js'\n\nexport interface AuthClientOptions {\n /** Base URL of the auth API. Defaults to window.location.origin. */\n baseURL?: string\n /** Enable organization client plugin (must match server config) */\n organizations?: boolean\n}\n\n/**\n * Create a typed auth client for use in React components.\n *\n * The explicit `ReturnType<typeof createAuthClient>` annotation is required\n * because better-auth 1.6's client return type references internal modules\n * (`client/path-to-object`, `client/query`) and zod@4 types that tsdown's\n * dts generator cannot name portably across pnpm's symlink paths (TS2742).\n *\n * Widening to the base `createAuthClient` return type loses the plugin-\n * specific client fields (`client.admin.listUsers`, etc.) at type level.\n * Consumers who need typed access to admin endpoints should use\n * `createUsersApi(client)` which bridges to a stable `UsersApi` interface\n * the `@murumets-ee/admin-ui/users` package expects.\n *\n * @example\n * ```typescript\n * // lib/auth-client.ts\n * import { createClient } from '@murumets-ee/auth/client'\n *\n * export const authClient = createClient()\n *\n * // In a component:\n * const { data: session } = authClient.useSession()\n * ```\n */\nexport function createClient(\n options?: AuthClientOptions,\n): ReturnType<typeof createAuthClient> & AuthClientWithAdmin {\n // The intersection adds admin methods to the widened base type.\n // Safe: adminClient() plugin adds these methods at runtime — the\n // base annotation just can't declare them (TS2742 on internals).\n return createAuthClient({\n baseURL: options?.baseURL,\n plugins: [adminClient(), ...(options?.organizations ? [organizationClient()] : [])],\n }) as ReturnType<typeof createAuthClient> & AuthClientWithAdmin\n}\n\n// ---------------------------------------------------------------------------\n// UsersApi adapter — bridges better-auth admin client → @murumets-ee/admin-ui/users\n// ---------------------------------------------------------------------------\n\n/**\n * Better-auth's client response shape: discriminated union with data or error.\n * Defined here so we don't depend on better-auth's internal response types.\n */\ninterface BetterAuthResponse<T = unknown> {\n data?: T\n error?: { message?: string }\n}\n\n/**\n * Structural interface for the admin client methods we use.\n *\n * better-auth's `createAuthClient({ plugins: [adminClient()] })` adds an\n * `admin` namespace at runtime with these methods. The widened return type\n * annotation on `createClient()` loses them (TS2742), so we define just the\n * shape we need. TypeScript verifies at call sites that the passed client\n * structurally matches.\n */\ninterface AuthClientWithAdmin {\n admin: {\n listUsers: (opts: {\n query: {\n limit: number\n offset: number\n sortBy?: string\n sortDirection?: 'asc' | 'desc'\n searchValue?: string\n searchField?: string\n searchOperator?: string\n }\n }) => Promise<BetterAuthResponse<{ users: AdminUser[]; total: number }>>\n createUser: (opts: {\n name: string\n email: string\n password: string\n role: string\n }) => Promise<BetterAuthResponse>\n updateUser: (opts: {\n userId: string\n data: Record<string, unknown>\n }) => Promise<BetterAuthResponse>\n removeUser: (opts: { userId: string }) => Promise<BetterAuthResponse>\n setRole: (opts: { userId: string; role: string }) => Promise<BetterAuthResponse>\n banUser: (opts: {\n userId: string\n banReason?: string\n banExpiresIn?: number\n }) => Promise<BetterAuthResponse>\n unbanUser: (opts: { userId: string }) => Promise<BetterAuthResponse>\n revokeUserSessions: (opts: { userId: string }) => Promise<BetterAuthResponse>\n }\n}\n\ninterface UsersListQuery {\n limit: number\n offset: number\n sortBy?: string\n sortDirection?: 'asc' | 'desc'\n searchValue?: string\n searchField?: 'email' | 'name'\n searchOperator?: 'contains' | 'starts_with' | 'ends_with'\n}\n\ninterface NormalizedUser {\n id: string\n name: string\n email: string\n emailVerified: boolean\n image: string | null\n createdAt: string\n updatedAt: string\n role: string | null\n banned: boolean | null\n banReason: string | null\n banExpires: string | null\n}\n\n/**\n * Create a users API adapter from a better-auth client that includes the\n * `adminClient()` plugin. The returned object is structurally compatible\n * with the `UsersApi` interface from `@murumets-ee/admin-ui/users`.\n *\n * @example\n * ```tsx\n * import { createClient, createUsersApi } from '@murumets-ee/auth/client'\n * import { UsersManagement } from '@murumets-ee/admin-ui/users'\n *\n * const authClient = createClient()\n * const usersApi = createUsersApi(authClient)\n *\n * <UsersManagement api={usersApi} currentUserId={session.user.id} />\n * ```\n */\nexport function createUsersApi(client: AuthClientWithAdmin) {\n /**\n * Unwrap better-auth's discriminated union response.\n * On error branch, `error` exists with optional `message`.\n * On success branch, `data` exists.\n */\n function unwrap<T>(res: BetterAuthResponse<T>): T {\n if (res.error) throw new Error(res.error.message ?? 'Unknown error')\n return res.data as T\n }\n\n return {\n async list(query: UsersListQuery): Promise<{ users: NormalizedUser[]; total: number }> {\n const res = await client.admin.listUsers({\n query: {\n limit: query.limit,\n offset: query.offset,\n ...(query.sortBy ? { sortBy: query.sortBy, sortDirection: query.sortDirection } : {}),\n ...(query.searchValue\n ? {\n searchValue: query.searchValue,\n searchField: query.searchField ?? 'email',\n searchOperator: query.searchOperator ?? 'contains',\n }\n : {}),\n },\n })\n const data = unwrap(res)\n return {\n users: (data.users ?? []).map(mapUser),\n total: data.total ?? 0,\n }\n },\n\n async create(data: { name: string; email: string; password: string; role: string }) {\n const res = await client.admin.createUser({\n name: data.name,\n email: data.email,\n password: data.password,\n role: data.role,\n })\n unwrap(res)\n },\n\n async update(userId: string, data: { name?: string }) {\n const res = await client.admin.updateUser({ userId, data })\n unwrap(res)\n },\n\n async remove(userId: string) {\n const res = await client.admin.removeUser({ userId })\n unwrap(res)\n },\n\n async setRole(userId: string, role: string) {\n const res = await client.admin.setRole({ userId, role })\n unwrap(res)\n },\n\n async ban(userId: string, options?: { reason?: string; expiresIn?: number }) {\n const res = await client.admin.banUser({\n userId,\n ...(options?.reason ? { banReason: options.reason } : {}),\n ...(options?.expiresIn ? { banExpiresIn: options.expiresIn } : {}),\n })\n unwrap(res)\n },\n\n async unban(userId: string) {\n const res = await client.admin.unbanUser({ userId })\n unwrap(res)\n },\n\n async revokeSessions(userId: string) {\n const res = await client.admin.revokeUserSessions({ userId })\n unwrap(res)\n },\n }\n}\n\n/** Normalize a better-auth admin user to a serializable shape (Dates → strings). */\nfunction mapUser(u: AdminUser): NormalizedUser {\n return {\n id: u.id,\n name: u.name ?? '',\n email: u.email,\n emailVerified: u.emailVerified ?? false,\n image: u.image ?? null,\n createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : String(u.createdAt ?? ''),\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : String(u.updatedAt ?? ''),\n role: u.role ?? null,\n banned: u.banned ?? null,\n banReason: u.banReason ?? null,\n banExpires: u.banExpires instanceof Date ? u.banExpires.toISOString() : u.banExpires ? String(u.banExpires) : null,\n }\n}\n"],"mappings":"sIA2CA,SAAgB,EACd,EAC2D,CAI3D,OAAO,EAAiB,CACtB,QAAS,GAAS,QAClB,QAAS,CAAC,GAAa,CAAE,GAAI,GAAS,cAAgB,CAAC,GAAoB,CAAC,CAAG,EAAE,CAAE,CACpF,CAAC,CAoGJ,SAAgB,EAAe,EAA6B,CAM1D,SAAS,EAAU,EAA+B,CAChD,GAAI,EAAI,MAAO,MAAU,MAAM,EAAI,MAAM,SAAW,gBAAgB,CACpE,OAAO,EAAI,KAGb,MAAO,CACL,MAAM,KAAK,EAA4E,CAerF,IAAM,EAAO,EAdD,MAAM,EAAO,MAAM,UAAU,CACvC,MAAO,CACL,MAAO,EAAM,MACb,OAAQ,EAAM,OACd,GAAI,EAAM,OAAS,CAAE,OAAQ,EAAM,OAAQ,cAAe,EAAM,cAAe,CAAG,EAAE,CACpF,GAAI,EAAM,YACN,CACE,YAAa,EAAM,YACnB,YAAa,EAAM,aAAe,QAClC,eAAgB,EAAM,gBAAkB,WACzC,CACD,EAAE,CACP,CACF,CAAC,CACsB,CACxB,MAAO,CACL,OAAQ,EAAK,OAAS,EAAE,EAAE,IAAI,EAAQ,CACtC,MAAO,EAAK,OAAS,EACtB,EAGH,MAAM,OAAO,EAAuE,CAOlF,EANY,MAAM,EAAO,MAAM,WAAW,CACxC,KAAM,EAAK,KACX,MAAO,EAAK,MACZ,SAAU,EAAK,SACf,KAAM,EAAK,KACZ,CAAC,CACS,EAGb,MAAM,OAAO,EAAgB,EAAyB,CAEpD,EADY,MAAM,EAAO,MAAM,WAAW,CAAE,SAAQ,OAAM,CAAC,CAChD,EAGb,MAAM,OAAO,EAAgB,CAE3B,EADY,MAAM,EAAO,MAAM,WAAW,CAAE,SAAQ,CAAC,CAC1C,EAGb,MAAM,QAAQ,EAAgB,EAAc,CAE1C,EADY,MAAM,EAAO,MAAM,QAAQ,CAAE,SAAQ,OAAM,CAAC,CAC7C,EAGb,MAAM,IAAI,EAAgB,EAAmD,CAM3E,EALY,MAAM,EAAO,MAAM,QAAQ,CACrC,SACA,GAAI,GAAS,OAAS,CAAE,UAAW,EAAQ,OAAQ,CAAG,EAAE,CACxD,GAAI,GAAS,UAAY,CAAE,aAAc,EAAQ,UAAW,CAAG,EAAE,CAClE,CAAC,CACS,EAGb,MAAM,MAAM,EAAgB,CAE1B,EADY,MAAM,EAAO,MAAM,UAAU,CAAE,SAAQ,CAAC,CACzC,EAGb,MAAM,eAAe,EAAgB,CAEnC,EADY,MAAM,EAAO,MAAM,mBAAmB,CAAE,SAAQ,CAAC,CAClD,EAEd,CAIH,SAAS,EAAQ,EAA8B,CAC7C,MAAO,CACL,GAAI,EAAE,GACN,KAAM,EAAE,MAAQ,GAChB,MAAO,EAAE,MACT,cAAe,EAAE,eAAiB,GAClC,MAAO,EAAE,OAAS,KAClB,UAAW,EAAE,qBAAqB,KAAO,EAAE,UAAU,aAAa,CAAG,OAAO,EAAE,WAAa,GAAG,CAC9F,UAAW,EAAE,qBAAqB,KAAO,EAAE,UAAU,aAAa,CAAG,OAAO,EAAE,WAAa,GAAG,CAC9F,KAAM,EAAE,MAAQ,KAChB,OAAQ,EAAE,QAAU,KACpB,UAAW,EAAE,WAAa,KAC1B,WAAY,EAAE,sBAAsB,KAAO,EAAE,WAAW,aAAa,CAAG,EAAE,WAAa,OAAO,EAAE,WAAW,CAAG,KAC/G"}
|
|
1
|
+
{"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Browser-safe auth client factory.\n *\n * This module has NO server-only imports and is safe to use in React components.\n * Import from '@murumets-ee/auth/client'.\n */\n\nimport { adminClient, organizationClient } from 'better-auth/client/plugins'\nimport { createAuthClient } from 'better-auth/react'\nimport type { AdminUser } from './types.js'\n\nexport interface AuthClientOptions {\n /** Base URL of the auth API. Defaults to window.location.origin. */\n baseURL?: string\n /** Enable organization client plugin (must match server config) */\n organizations?: boolean\n}\n\n/**\n * Create a typed auth client for use in React components.\n *\n * The explicit `ReturnType<typeof createAuthClient>` annotation is required\n * because better-auth 1.6's client return type references internal modules\n * (`client/path-to-object`, `client/query`) and zod@4 types that tsdown's\n * dts generator cannot name portably across pnpm's symlink paths (TS2742).\n *\n * Widening to the base `createAuthClient` return type loses the plugin-\n * specific client fields (`client.admin.listUsers`, etc.) at type level.\n * Consumers who need typed access to admin endpoints should use\n * `createUsersApi(client)` which bridges to a stable `UsersApi` interface\n * the `@murumets-ee/admin-ui/users` package expects.\n *\n * @example\n * ```typescript\n * // lib/auth-client.ts\n * import { createClient } from '@murumets-ee/auth/client'\n *\n * export const authClient = createClient()\n *\n * // In a component:\n * const { data: session } = authClient.useSession()\n * ```\n */\nexport function createClient(\n options?: AuthClientOptions,\n): ReturnType<typeof createAuthClient> & AuthClientWithAdmin {\n // The intersection adds admin methods to the widened base type.\n // Safe: adminClient() plugin adds these methods at runtime — the\n // base annotation just can't declare them (TS2742 on internals).\n return createAuthClient({\n baseURL: options?.baseURL,\n plugins: [adminClient(), ...(options?.organizations ? [organizationClient()] : [])],\n }) as ReturnType<typeof createAuthClient> & AuthClientWithAdmin\n}\n\n// ---------------------------------------------------------------------------\n// UsersApi adapter — bridges better-auth admin client → @murumets-ee/admin-ui/users\n// ---------------------------------------------------------------------------\n\n/**\n * Better-auth's client response shape: discriminated union with data or error.\n * Defined here so we don't depend on better-auth's internal response types.\n */\ninterface BetterAuthResponse<T = unknown> {\n data?: T\n error?: { message?: string }\n}\n\n/**\n * Structural interface for the admin client methods we use.\n *\n * better-auth's `createAuthClient({ plugins: [adminClient()] })` adds an\n * `admin` namespace at runtime with these methods. The widened return type\n * annotation on `createClient()` loses them (TS2742), so we define just the\n * shape we need. TypeScript verifies at call sites that the passed client\n * structurally matches.\n */\ninterface AuthClientWithAdmin {\n admin: {\n listUsers: (opts: {\n query: {\n limit: number\n offset: number\n sortBy?: string\n sortDirection?: 'asc' | 'desc'\n searchValue?: string\n searchField?: string\n searchOperator?: string\n }\n }) => Promise<BetterAuthResponse<{ users: AdminUser[]; total: number }>>\n createUser: (opts: {\n name: string\n email: string\n password: string\n role: string\n /** Extra fields spread into the new user row by better-auth's admin\n * plugin. We use this to set `emailVerified: true` for admin-created\n * accounts so they aren't locked out when public-signup deployments\n * default `requireEmailVerification: true`. */\n data?: Record<string, unknown>\n }) => Promise<BetterAuthResponse>\n updateUser: (opts: {\n userId: string\n data: Record<string, unknown>\n }) => Promise<BetterAuthResponse>\n removeUser: (opts: { userId: string }) => Promise<BetterAuthResponse>\n setRole: (opts: { userId: string; role: string }) => Promise<BetterAuthResponse>\n banUser: (opts: {\n userId: string\n banReason?: string\n banExpiresIn?: number\n }) => Promise<BetterAuthResponse>\n unbanUser: (opts: { userId: string }) => Promise<BetterAuthResponse>\n revokeUserSessions: (opts: { userId: string }) => Promise<BetterAuthResponse>\n }\n}\n\ninterface UsersListQuery {\n limit: number\n offset: number\n sortBy?: string\n sortDirection?: 'asc' | 'desc'\n searchValue?: string\n searchField?: 'email' | 'name'\n searchOperator?: 'contains' | 'starts_with' | 'ends_with'\n}\n\ninterface NormalizedUser {\n id: string\n name: string\n email: string\n emailVerified: boolean\n image: string | null\n createdAt: string\n updatedAt: string\n role: string | null\n banned: boolean | null\n banReason: string | null\n banExpires: string | null\n}\n\n/**\n * Create a users API adapter from a better-auth client that includes the\n * `adminClient()` plugin. The returned object is structurally compatible\n * with the `UsersApi` interface from `@murumets-ee/admin-ui/users`.\n *\n * @example\n * ```tsx\n * import { createClient, createUsersApi } from '@murumets-ee/auth/client'\n * import { UsersManagement } from '@murumets-ee/admin-ui/users'\n *\n * const authClient = createClient()\n * const usersApi = createUsersApi(authClient)\n *\n * <UsersManagement api={usersApi} currentUserId={session.user.id} />\n * ```\n */\nexport function createUsersApi(client: AuthClientWithAdmin) {\n /**\n * Unwrap better-auth's discriminated union response.\n * On error branch, `error` exists with optional `message`.\n * On success branch, `data` exists.\n */\n function unwrap<T>(res: BetterAuthResponse<T>): T {\n if (res.error) throw new Error(res.error.message ?? 'Unknown error')\n return res.data as T\n }\n\n return {\n async list(query: UsersListQuery): Promise<{ users: NormalizedUser[]; total: number }> {\n const res = await client.admin.listUsers({\n query: {\n limit: query.limit,\n offset: query.offset,\n ...(query.sortBy ? { sortBy: query.sortBy, sortDirection: query.sortDirection } : {}),\n ...(query.searchValue\n ? {\n searchValue: query.searchValue,\n searchField: query.searchField ?? 'email',\n searchOperator: query.searchOperator ?? 'contains',\n }\n : {}),\n },\n })\n const data = unwrap(res)\n return {\n users: (data.users ?? []).map(mapUser),\n total: data.total ?? 0,\n }\n },\n\n async create(data: { name: string; email: string; password: string; role: string }) {\n const res = await client.admin.createUser({\n name: data.name,\n email: data.email,\n password: data.password,\n role: data.role,\n // An admin manually creating a user is implicitly vouching for the\n // address — skip email verification so the account is usable\n // immediately, even when `requireEmailVerification: true` is set.\n data: { emailVerified: true },\n })\n unwrap(res)\n },\n\n async update(userId: string, data: { name?: string }) {\n const res = await client.admin.updateUser({ userId, data })\n unwrap(res)\n },\n\n async remove(userId: string) {\n const res = await client.admin.removeUser({ userId })\n unwrap(res)\n },\n\n async setRole(userId: string, role: string) {\n const res = await client.admin.setRole({ userId, role })\n unwrap(res)\n },\n\n async ban(userId: string, options?: { reason?: string; expiresIn?: number }) {\n const res = await client.admin.banUser({\n userId,\n ...(options?.reason ? { banReason: options.reason } : {}),\n ...(options?.expiresIn ? { banExpiresIn: options.expiresIn } : {}),\n })\n unwrap(res)\n },\n\n async unban(userId: string) {\n const res = await client.admin.unbanUser({ userId })\n unwrap(res)\n },\n\n async revokeSessions(userId: string) {\n const res = await client.admin.revokeUserSessions({ userId })\n unwrap(res)\n },\n }\n}\n\n/** Normalize a better-auth admin user to a serializable shape (Dates → strings). */\nfunction mapUser(u: AdminUser): NormalizedUser {\n return {\n id: u.id,\n name: u.name ?? '',\n email: u.email,\n emailVerified: u.emailVerified ?? false,\n image: u.image ?? null,\n createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : String(u.createdAt ?? ''),\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : String(u.updatedAt ?? ''),\n role: u.role ?? null,\n banned: u.banned ?? null,\n banReason: u.banReason ?? null,\n banExpires:\n u.banExpires instanceof Date\n ? u.banExpires.toISOString()\n : u.banExpires\n ? String(u.banExpires)\n : null,\n }\n}\n"],"mappings":"sIA2CA,SAAgB,EACd,EAC2D,CAI3D,OAAO,EAAiB,CACtB,QAAS,GAAS,QAClB,QAAS,CAAC,GAAa,CAAE,GAAI,GAAS,cAAgB,CAAC,GAAoB,CAAC,CAAG,EAAE,CAAE,CACpF,CAAC,CAyGJ,SAAgB,EAAe,EAA6B,CAM1D,SAAS,EAAU,EAA+B,CAChD,GAAI,EAAI,MAAO,MAAU,MAAM,EAAI,MAAM,SAAW,gBAAgB,CACpE,OAAO,EAAI,KAGb,MAAO,CACL,MAAM,KAAK,EAA4E,CAerF,IAAM,EAAO,EAAO,MAdF,EAAO,MAAM,UAAU,CACvC,MAAO,CACL,MAAO,EAAM,MACb,OAAQ,EAAM,OACd,GAAI,EAAM,OAAS,CAAE,OAAQ,EAAM,OAAQ,cAAe,EAAM,cAAe,CAAG,EAAE,CACpF,GAAI,EAAM,YACN,CACE,YAAa,EAAM,YACnB,YAAa,EAAM,aAAe,QAClC,eAAgB,EAAM,gBAAkB,WACzC,CACD,EAAE,CACP,CACF,CAAC,CACsB,CACxB,MAAO,CACL,OAAQ,EAAK,OAAS,EAAE,EAAE,IAAI,EAAQ,CACtC,MAAO,EAAK,OAAS,EACtB,EAGH,MAAM,OAAO,EAAuE,CAWlF,EAAO,MAVW,EAAO,MAAM,WAAW,CACxC,KAAM,EAAK,KACX,MAAO,EAAK,MACZ,SAAU,EAAK,SACf,KAAM,EAAK,KAIX,KAAM,CAAE,cAAe,GAAM,CAC9B,CAAC,CACS,EAGb,MAAM,OAAO,EAAgB,EAAyB,CAEpD,EAAO,MADW,EAAO,MAAM,WAAW,CAAE,SAAQ,OAAM,CAAC,CAChD,EAGb,MAAM,OAAO,EAAgB,CAE3B,EAAO,MADW,EAAO,MAAM,WAAW,CAAE,SAAQ,CAAC,CAC1C,EAGb,MAAM,QAAQ,EAAgB,EAAc,CAE1C,EAAO,MADW,EAAO,MAAM,QAAQ,CAAE,SAAQ,OAAM,CAAC,CAC7C,EAGb,MAAM,IAAI,EAAgB,EAAmD,CAM3E,EAAO,MALW,EAAO,MAAM,QAAQ,CACrC,SACA,GAAI,GAAS,OAAS,CAAE,UAAW,EAAQ,OAAQ,CAAG,EAAE,CACxD,GAAI,GAAS,UAAY,CAAE,aAAc,EAAQ,UAAW,CAAG,EAAE,CAClE,CAAC,CACS,EAGb,MAAM,MAAM,EAAgB,CAE1B,EAAO,MADW,EAAO,MAAM,UAAU,CAAE,SAAQ,CAAC,CACzC,EAGb,MAAM,eAAe,EAAgB,CAEnC,EAAO,MADW,EAAO,MAAM,mBAAmB,CAAE,SAAQ,CAAC,CAClD,EAEd,CAIH,SAAS,EAAQ,EAA8B,CAC7C,MAAO,CACL,GAAI,EAAE,GACN,KAAM,EAAE,MAAQ,GAChB,MAAO,EAAE,MACT,cAAe,EAAE,eAAiB,GAClC,MAAO,EAAE,OAAS,KAClB,UAAW,EAAE,qBAAqB,KAAO,EAAE,UAAU,aAAa,CAAG,OAAO,EAAE,WAAa,GAAG,CAC9F,UAAW,EAAE,qBAAqB,KAAO,EAAE,UAAU,aAAa,CAAG,OAAO,EAAE,WAAa,GAAG,CAC9F,KAAM,EAAE,MAAQ,KAChB,OAAQ,EAAE,QAAU,KACpB,UAAW,EAAE,WAAa,KAC1B,WACE,EAAE,sBAAsB,KACpB,EAAE,WAAW,aAAa,CAC1B,EAAE,WACA,OAAO,EAAE,WAAW,CACpB,KACT"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { n as AuthConfig, t as AdminUser } from "./types-
|
|
2
|
-
import { a as isSignupEnabled, c as createAuthServer, i as getAuth, n as _setAuth, o as Auth, r as _setAuthConfig, s as AuthAdminApi } from "./runtime-
|
|
3
|
-
import { createAccessControl } from "better-auth/plugins/access";
|
|
4
|
-
import * as _$better_auth_plugins0 from "better-auth/plugins";
|
|
1
|
+
import { n as AuthConfig, t as AdminUser } from "./types-Bkei6fxa.mjs";
|
|
2
|
+
import { a as isSignupEnabled, c as createAuthServer, i as getAuth, n as _setAuth, o as Auth, r as _setAuthConfig, s as AuthAdminApi } from "./runtime-D0zIyEjn.mjs";
|
|
5
3
|
import { PermissionChecker, RequestContext } from "@murumets-ee/core";
|
|
6
|
-
import { Entity } from "@murumets-ee/entity";
|
|
7
|
-
|
|
8
4
|
//#region src/context.d.ts
|
|
9
5
|
/**
|
|
10
6
|
* Resolve a better-auth session into a toolkit RequestContext.
|
|
@@ -28,8 +24,6 @@ import { Entity } from "@murumets-ee/entity";
|
|
|
28
24
|
declare function resolveAuthContext(auth: Auth, headers: Headers): Promise<RequestContext>;
|
|
29
25
|
//#endregion
|
|
30
26
|
//#region src/permissions.d.ts
|
|
31
|
-
declare const ACTIONS: readonly ["view", "create", "update", "delete"];
|
|
32
|
-
declare const ACTIONS_WITH_PUBLISH: readonly ["view", "create", "update", "delete", "publish"];
|
|
33
27
|
/** Built-in role names — cannot be deleted via the roles editor. */
|
|
34
28
|
declare const BUILT_IN_ROLES: readonly ["admin", "public", "authenticated", "agent"];
|
|
35
29
|
/** Maps HTTP methods to permission action names. */
|
|
@@ -70,46 +64,6 @@ declare function buildResourceCatalog(entities: {
|
|
|
70
64
|
resource?: string;
|
|
71
65
|
actions?: readonly string[];
|
|
72
66
|
}[]): Record<string, string[]>;
|
|
73
|
-
/**
|
|
74
|
-
* Build a permission statement object from all registered entities,
|
|
75
|
-
* plus the admin plugin's built-in user/session resources.
|
|
76
|
-
*
|
|
77
|
-
* Publishable entities get the additional `publish` action.
|
|
78
|
-
* Result shape: `{ user: [...], session: [...], article: ['view', ...], category: [...] }`
|
|
79
|
-
*/
|
|
80
|
-
declare function buildStatements(entities: Entity[]): Record<string, readonly string[]>;
|
|
81
|
-
/**
|
|
82
|
-
* Build the default toolkit roles for better-auth's access control.
|
|
83
|
-
*
|
|
84
|
-
* - **admin**: full CRUD on all entities + user/session management
|
|
85
|
-
* - **authenticated**: view only (better-auth's `defaultRole`)
|
|
86
|
-
* - **agent**: view + create + update on the ticketing surface (see
|
|
87
|
-
* `AGENT_DEFAULT_PERMISSIONS`). Non-ticketing entities get view only,
|
|
88
|
-
* matching `authenticated`.
|
|
89
|
-
*/
|
|
90
|
-
declare function buildDefaultRoles(ac: ReturnType<typeof createAccessControl>, entities: Entity[]): {
|
|
91
|
-
admin: {
|
|
92
|
-
authorize<K_1 extends string>(request: K_1 extends infer T extends K ? { [key in T]?: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key] | {
|
|
93
|
-
actions: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key];
|
|
94
|
-
connector: "OR" | "AND";
|
|
95
|
-
} | undefined } : never, connector?: "OR" | "AND"): _$better_auth_plugins0.AuthorizeResponse;
|
|
96
|
-
statements: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>;
|
|
97
|
-
};
|
|
98
|
-
authenticated: {
|
|
99
|
-
authorize<K_1 extends string>(request: K_1 extends infer T extends K ? { [key in T]?: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key] | {
|
|
100
|
-
actions: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key];
|
|
101
|
-
connector: "OR" | "AND";
|
|
102
|
-
} | undefined } : never, connector?: "OR" | "AND"): _$better_auth_plugins0.AuthorizeResponse;
|
|
103
|
-
statements: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>;
|
|
104
|
-
};
|
|
105
|
-
agent: {
|
|
106
|
-
authorize<K_1 extends string>(request: K_1 extends infer T extends K ? { [key in T]?: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key] | {
|
|
107
|
-
actions: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>[key];
|
|
108
|
-
connector: "OR" | "AND";
|
|
109
|
-
} | undefined } : never, connector?: "OR" | "AND"): _$better_auth_plugins0.AuthorizeResponse;
|
|
110
|
-
statements: _$better_auth_plugins0.Subset<string, _$better_auth_plugins0.Statements>;
|
|
111
|
-
};
|
|
112
|
-
};
|
|
113
67
|
//#endregion
|
|
114
68
|
//#region src/seed-roles.d.ts
|
|
115
69
|
/**
|
|
@@ -155,5 +109,5 @@ interface UpsertResult {
|
|
|
155
109
|
*/
|
|
156
110
|
declare function upsertBuiltInRoles(read: () => Promise<RoleMap | null | undefined>, write: (roles: RoleMap) => Promise<void>): Promise<UpsertResult>;
|
|
157
111
|
//#endregion
|
|
158
|
-
export {
|
|
112
|
+
export { type AdminUser, type Auth, type AuthAdminApi, type AuthConfig, BUILT_IN_ROLES, METHOD_TO_ACTION, type RoleMap, type UpsertResult, _setAuth, _setAuthConfig, buildInitialRoleDefinitions, buildPermissionChecker, buildResourceCatalog, createAuthServer, getAuth, isSignupEnabled, resolveAuthContext, upsertBuiltInRoles };
|
|
159
113
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.ts","../src/permissions.ts","../src/seed-roles.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.ts","../src/permissions.ts","../src/seed-roles.ts"],"mappings":";;;;AA8BA;;;;;;;;;;;;;;;;;;;AAAA,iBAAsB,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,cAAA;;;;cCLnE,cAAA;;cA2BA,gBAAA,EAAkB,MAAA;;;;;;;iBAiBf,2BAAA,CAAA,GAA+B,MAAA,SAAe,MAAA;;;AA5C9D;;;;;AA2BA;iBAiCgB,sBAAA,CACd,eAAA,EAAiB,MAAA,SAAe,MAAA,sBAC/B,iBAAA;;;;AAlBH;;;;;AAgBA;;;iBAwCgB,oBAAA,CACd,QAAA;EAAY,IAAA;EAAc,SAAA;IAAc,IAAA;EAAA;AAAA,KACxC,MAAA;EAAW,QAAA;EAAmB,OAAA;AAAA,MAC7B,MAAA;;;;;;;;;;;;ADlGH;;;;;;;;;;;;;;;;;;;;KEEY,OAAA,GAAU,MAAA,SAAe,MAAA;AAAA,UAEpB,YAAA;;EAEf,OAAA;EDXkF;ECalF,KAAA;AAAA;;;;AD+BF;;iBCvBsB,kBAAA,CACpB,IAAA,QAAY,OAAA,CAAQ,OAAA,sBACpB,KAAA,GAAQ,KAAA,EAAO,OAAA,KAAY,OAAA,SAC1B,OAAA,CAAQ,YAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,
|
|
1
|
+
import{a as e,i as t,n,o as r,t as i}from"./permissions-BbVD2CmT.mjs";import{a,i as o,n as s,r as c,t as l}from"./runtime-Cj8SEWhk.mjs";import"server-only";async function u(e,t){let n=await e.api.getSession({headers:t});if(!n)return{requestId:crypto.randomUUID()};let r=n.user.role,i=typeof r==`string`&&r.length>0?r:`public`;return{user:{id:n.user.id,groups:[i],name:n.user.name??void 0,email:n.user.email??void 0},requestId:crypto.randomUUID()}}async function d(e,n){let r=await e()??{},i=t(),a={...r},o=[];for(let[e,t]of Object.entries(i))e in a||(a[e]=t,o.push(e));return o.length>0&&await n(a),{created:o.length,roles:o}}export{i as BUILT_IN_ROLES,n as METHOD_TO_ACTION,l as _setAuth,s as _setAuthConfig,t as buildInitialRoleDefinitions,e as buildPermissionChecker,r as buildResourceCatalog,a as createAuthServer,c as getAuth,o as isSignupEnabled,u as resolveAuthContext,d as upsertBuiltInRoles};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/context.ts","../src/seed-roles.ts"],"sourcesContent":["/**\n * Bridge between better-auth sessions and the toolkit's RequestContext.\n *\n * This is the critical integration point: it resolves a better-auth session\n * from request headers and returns a toolkit RequestContext that the entity\n * system's AdminClient, QueryClient, and auditable behavior can read.\n */\n\nimport type { RequestContext } from '@murumets-ee/core'\nimport type { Auth } from './server.js'\n\n/**\n * Resolve a better-auth session into a toolkit RequestContext.\n *\n * Call this from your Next.js middleware or server component to populate\n * the toolkit's AsyncLocalStorage context.\n *\n * @example\n * ```typescript\n * // middleware.ts (user writes this — documented copy-paste)\n * import { getAuth, resolveAuthContext } from '@murumets-ee/auth'\n * import { runWithContext } from '@murumets-ee/core'\n *\n * export async function middleware(request: NextRequest) {\n * const auth = getAuth()\n * const ctx = await resolveAuthContext(auth, request.headers)\n * return runWithContext(ctx, () => NextResponse.next())\n * }\n * ```\n */\nexport async function resolveAuthContext(auth: Auth, headers: Headers): Promise<RequestContext> {\n const session = await auth.api.getSession({ headers })\n\n if (!session) {\n return { requestId: crypto.randomUUID() }\n }\n\n
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/context.ts","../src/seed-roles.ts"],"sourcesContent":["/**\n * Bridge between better-auth sessions and the toolkit's RequestContext.\n *\n * This is the critical integration point: it resolves a better-auth session\n * from request headers and returns a toolkit RequestContext that the entity\n * system's AdminClient, QueryClient, and auditable behavior can read.\n */\n\nimport type { RequestContext } from '@murumets-ee/core'\nimport type { Auth } from './server.js'\n\n/**\n * Resolve a better-auth session into a toolkit RequestContext.\n *\n * Call this from your Next.js middleware or server component to populate\n * the toolkit's AsyncLocalStorage context.\n *\n * @example\n * ```typescript\n * // middleware.ts (user writes this — documented copy-paste)\n * import { getAuth, resolveAuthContext } from '@murumets-ee/auth'\n * import { runWithContext } from '@murumets-ee/core'\n *\n * export async function middleware(request: NextRequest) {\n * const auth = getAuth()\n * const ctx = await resolveAuthContext(auth, request.headers)\n * return runWithContext(ctx, () => NextResponse.next())\n * }\n * ```\n */\nexport async function resolveAuthContext(auth: Auth, headers: Headers): Promise<RequestContext> {\n const session = await auth.api.getSession({ headers })\n\n if (!session) {\n return { requestId: crypto.randomUUID() }\n }\n\n // The admin plugin stores `role` on the user object. New users get\n // `'authenticated'` (better-auth's `defaultRole`) written to their row,\n // so this fallback should never fire in practice. The defensive default\n // of `'public'` (zero perms, in `BUILT_IN_ROLES`) means a session with a\n // missing/empty role is denied by default — fail-safe rather than\n // fail-open. The two values are intentionally different: 'authenticated'\n // is the persisted DB default; 'public' is the runtime fallback.\n const role = (session.user as Record<string, unknown>).role\n const roleName = typeof role === 'string' && role.length > 0 ? role : 'public'\n\n return {\n user: {\n id: session.user.id,\n groups: [roleName],\n name: session.user.name ?? undefined,\n email: session.user.email ?? undefined,\n },\n requestId: crypto.randomUUID(),\n }\n}\n","/**\n * Idempotent seeder that adds missing built-in roles (e.g. `agent`) to an\n * existing deployment's permission settings.\n *\n * `buildInitialRoleDefinitions` only runs on first-time admin-api-init —\n * deployments created before a new built-in role was added (like the\n * ticketing `agent` role shipped 2026-04-20) will never get it automatically.\n * This helper lets consumers wire a one-shot seeder on the Seed page that\n * merges any missing roles into their saved settings without overwriting\n * customizations.\n *\n * Usage (app-side seeder):\n *\n * ```ts\n * import { upsertBuiltInRoles } from '@murumets-ee/auth'\n * import { createSettingsClient } from '@murumets-ee/settings'\n * import { getApp } from '@murumets-ee/core'\n * import { permissionSettings } from '@/settings'\n *\n * export async function seedRoles(): Promise<{ created: number }> {\n * const client = createSettingsClient(permissionSettings, { app: getApp() })\n * const { created } = await upsertBuiltInRoles(\n * () => client.get('roles'),\n * (roles) => client.set('roles', roles),\n * )\n * return { created }\n * }\n * ```\n */\n\nimport { buildInitialRoleDefinitions } from './permissions.js'\n\nexport type RoleMap = Record<string, Record<string, string[]>>\n\nexport interface UpsertResult {\n /** Number of role keys added (0 if all built-ins were already present). */\n created: number\n /** Names of roles that were added. */\n roles: string[]\n}\n\n/**\n * Merge the default built-in role definitions into the caller's persisted\n * role map, adding any missing ones. Existing entries are left untouched —\n * the point is to bootstrap new built-ins, not to reset customizations.\n */\nexport async function upsertBuiltInRoles(\n read: () => Promise<RoleMap | null | undefined>,\n write: (roles: RoleMap) => Promise<void>,\n): Promise<UpsertResult> {\n const saved = (await read()) ?? {}\n const defaults = buildInitialRoleDefinitions()\n const next: RoleMap = { ...saved }\n const added: string[] = []\n for (const [name, perms] of Object.entries(defaults)) {\n if (!(name in next)) {\n next[name] = perms\n added.push(name)\n }\n }\n if (added.length > 0) {\n await write(next)\n }\n return { created: added.length, roles: added }\n}\n"],"mappings":"4JA8BA,eAAsB,EAAmB,EAAY,EAA2C,CAC9F,IAAM,EAAU,MAAM,EAAK,IAAI,WAAW,CAAE,UAAS,CAAC,CAEtD,GAAI,CAAC,EACH,MAAO,CAAE,UAAW,OAAO,YAAY,CAAE,CAU3C,IAAM,EAAQ,EAAQ,KAAiC,KACjD,EAAW,OAAO,GAAS,UAAY,EAAK,OAAS,EAAI,EAAO,SAEtE,MAAO,CACL,KAAM,CACJ,GAAI,EAAQ,KAAK,GACjB,OAAQ,CAAC,EAAS,CAClB,KAAM,EAAQ,KAAK,MAAQ,IAAA,GAC3B,MAAO,EAAQ,KAAK,OAAS,IAAA,GAC9B,CACD,UAAW,OAAO,YAAY,CAC/B,CCTH,eAAsB,EACpB,EACA,EACuB,CACvB,IAAM,EAAS,MAAM,GAAM,EAAK,EAAE,CAC5B,EAAW,GAA6B,CACxC,EAAgB,CAAE,GAAG,EAAO,CAC5B,EAAkB,EAAE,CAC1B,IAAK,GAAM,CAAC,EAAM,KAAU,OAAO,QAAQ,EAAS,CAC5C,KAAQ,IACZ,EAAK,GAAQ,EACb,EAAM,KAAK,EAAK,EAMpB,OAHI,EAAM,OAAS,GACjB,MAAM,EAAM,EAAK,CAEZ,CAAE,QAAS,EAAM,OAAQ,MAAO,EAAO"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import"better-auth/plugins/access";const e=[`view`,`create`,`update`,`delete`],t=[`view`,`create`,`update`,`delete`,`publish`],n=[`admin`,`public`,`authenticated`,`agent`],r={ticket:[`view`,`create`,`update`],ticket_message:[`view`,`update`],ticket_attachment:[`view`,`create`],department:[`view`],ticket_tag:[`view`]},i={GET:`view`,POST:`create`,PATCH:`update`,DELETE:`delete`};function a(){return{public:{},authenticated:{},agent:{...r}}}function o(e){let t=new Map;for(let[n,r]of Object.entries(e)){let e=new Map;for(let[t,n]of Object.entries(r))e.set(t,new Set(n));t.set(n,e)}return(e,n,r)=>e===`admin`?!0:t.get(e)?.get(n)?.has(r)??!1}function s(e){return e.behaviors?.some(e=>e.name===`publishable`)??!1}function c(e,t){let n={};for(let t of e)n[t.name]=s(t)?[`view`,`create`,`update`,`delete`,`publish`]:[`view`,`create`,`update`,`delete`];if(t)for(let e of t)e.resource&&e.actions&&!(e.resource in n)&&(n[e.resource]=[...e.actions]);return n}const l={user:[`create`,`list`,`set-role`,`ban`,`impersonate`,`delete`,`set-password`,`get`,`update`],session:[`list`,`revoke`,`delete`]};function u(n){let r={...l};for(let i of n)r[i.name]=s(i)?t:e;return r}function d(e,t){let n={},i={},a={};n.user=[...l.user],n.session=[...l.session],i.user=[],i.session=[],a.user=[],a.session=[];for(let e of t)n[e.name]=s(e)?[`view`,`create`,`update`,`delete`,`publish`]:[`view`,`create`,`update`,`delete`],i[e.name]=[`view`],a[e.name]=r[e.name]??[`view`];return{admin:e.newRole(n),authenticated:e.newRole(i),agent:e.newRole(a)}}export{o as a,a as i,i as n,c as o,d as r,u as s,n as t};
|
|
2
|
+
//# sourceMappingURL=permissions-BbVD2CmT.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-DLMd-3dc.mjs","names":[],"sources":["../src/permissions.ts"],"sourcesContent":["/**\n * Permission system — bridges entity access definitions to better-auth's access control,\n * and provides the configurable permission checker for the admin API.\n *\n * Two layers:\n * 1. **better-auth integration**: `buildStatements` / `buildDefaultRoles` — feeds into\n * better-auth's `createAccessControl` for its internal permission system.\n * 2. **Admin API enforcement**: `buildPermissionChecker` / `buildInitialRoleDefinitions` —\n * the toolkit's own firewall-model permission system, persisted in settings.\n */\n\nimport type { PermissionChecker } from '@murumets-ee/core'\nimport type { Entity } from '@murumets-ee/entity'\nimport { createAccessControl } from 'better-auth/plugins/access'\n\nconst ACTIONS = ['view', 'create', 'update', 'delete'] as const\nconst ACTIONS_WITH_PUBLISH = ['view', 'create', 'update', 'delete', 'publish'] as const\n\nexport { ACTIONS, ACTIONS_WITH_PUBLISH }\n\n// ---------------------------------------------------------------------------\n// Built-in roles & constants\n// ---------------------------------------------------------------------------\n\n/** Built-in role names — cannot be deleted via the roles editor. */\nexport const BUILT_IN_ROLES = ['admin', 'public', 'authenticated', 'agent'] as const\n\n/**\n * Default permissions for the built-in `agent` role.\n *\n * Agents handle the ticketing inbox: they can view + update tickets,\n * messages, and attachments, and view the metadata that scopes them\n * (departments, tags). Delete actions and all email-template management\n * stay admin-only. Consumers can add more resources via the roles editor.\n *\n * SECURITY (#82): `ticket_message:create` is admin-only by design. Agents\n * authoring replies go through `/api/admin/ticketing/reply` (gated on\n * `ticket:update`), which uses `createSystemAdminClient` to perform the\n * write under elevated permissions while still recording the real agent\n * in the audit log. Allowing `ticket_message:create` for agents would\n * re-open the generic-CRUD impersonation path where an agent can spoof\n * another agent's `senderUserId` / `senderEmail` / `senderName`.\n */\nconst AGENT_DEFAULT_PERMISSIONS: Record<string, string[]> = {\n ticket: ['view', 'create', 'update'],\n ticket_message: ['view', 'update'],\n ticket_attachment: ['view', 'create'],\n department: ['view'],\n ticket_tag: ['view'],\n}\n\n/** Maps HTTP methods to permission action names. */\nexport const METHOD_TO_ACTION: Record<string, string> = {\n GET: 'view',\n POST: 'create',\n PATCH: 'update',\n DELETE: 'delete',\n}\n\n// ---------------------------------------------------------------------------\n// Admin API permission system (firewall model)\n// ---------------------------------------------------------------------------\n\n/**\n * Build initial role definitions for first run (no settings saved yet).\n *\n * All built-in non-admin roles start with ZERO permissions.\n * Admin is never stored — it's hardcoded in the checker.\n */\nexport function buildInitialRoleDefinitions(): Record<string, Record<string, string[]>> {\n return {\n public: {},\n authenticated: {},\n agent: { ...AGENT_DEFAULT_PERMISSIONS },\n }\n}\n\n/**\n * Build a synchronous permission checker from role definitions.\n *\n * Rules:\n * - `admin` role: ALWAYS returns `true` (hardcoded safety net, ignores settings)\n * - All other roles: exact match from `roleDefinitions` (deny-by-default)\n * - Unknown role / unknown resource / unknown action → `false`\n */\nexport function buildPermissionChecker(\n roleDefinitions: Record<string, Record<string, string[]>>,\n): PermissionChecker {\n // Pre-build lookup maps for O(1) checks\n const perms = new Map<string, Map<string, Set<string>>>()\n\n for (const [role, resources] of Object.entries(roleDefinitions)) {\n const resourceMap = new Map<string, Set<string>>()\n for (const [resource, actions] of Object.entries(resources)) {\n resourceMap.set(resource, new Set(actions))\n }\n perms.set(role, resourceMap)\n }\n\n return (role: string, resource: string, action: string): boolean => {\n if (role === 'admin') return true // Safety net — admin always passes\n return perms.get(role)?.get(resource)?.has(action) ?? false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Resource catalog builder\n// ---------------------------------------------------------------------------\n\n/** Check if an entity has the publishable behavior. */\nfunction isPublishable(entity: { behaviors?: { name: string }[] }): boolean {\n return entity.behaviors?.some((b) => b.name === 'publishable') ?? false\n}\n\n/**\n * Build a complete resource catalog from entities and admin routes.\n *\n * Used by:\n * - `permissionRoutes()` config (`getStatements` callback)\n * - Server-side permission page data loaders\n *\n * Entities automatically get CRUD actions. Publishable entities also get\n * the `publish` action, which gates who can set status to 'published'.\n * Routes with `resource` and `actions` are added if not already present.\n */\nexport function buildResourceCatalog(\n entities: { name: string; behaviors?: { name: string }[] }[],\n routes?: { resource?: string; actions?: readonly string[] }[],\n): Record<string, string[]> {\n const catalog: Record<string, string[]> = {}\n\n for (const entity of entities) {\n catalog[entity.name] = isPublishable(entity)\n ? ['view', 'create', 'update', 'delete', 'publish']\n : ['view', 'create', 'update', 'delete']\n }\n\n if (routes) {\n for (const route of routes) {\n if (route.resource && route.actions && !(route.resource in catalog)) {\n catalog[route.resource] = [...route.actions]\n }\n }\n }\n\n return catalog\n}\n\n// ---------------------------------------------------------------------------\n// better-auth integration (unchanged, used by createAuthServer)\n// ---------------------------------------------------------------------------\n\n/** Admin plugin's built-in resources — must be included for listUsers, ban, etc. */\nconst ADMIN_STATEMENTS = {\n user: [\n 'create',\n 'list',\n 'set-role',\n 'ban',\n 'impersonate',\n 'delete',\n 'set-password',\n 'get',\n 'update',\n ] as const,\n session: ['list', 'revoke', 'delete'] as const,\n}\n\n/**\n * Build a permission statement object from all registered entities,\n * plus the admin plugin's built-in user/session resources.\n *\n * Publishable entities get the additional `publish` action.\n * Result shape: `{ user: [...], session: [...], article: ['view', ...], category: [...] }`\n */\nexport function buildStatements(entities: Entity[]) {\n const statement: Record<string, readonly string[]> = {\n ...ADMIN_STATEMENTS,\n }\n for (const entity of entities) {\n statement[entity.name] = isPublishable(entity) ? ACTIONS_WITH_PUBLISH : ACTIONS\n }\n return statement\n}\n\n/**\n * Build the default toolkit roles for better-auth's access control.\n *\n * - **admin**: full CRUD on all entities + user/session management\n * - **authenticated**: view only (better-auth's `defaultRole`)\n * - **agent**: view + create + update on the ticketing surface (see\n * `AGENT_DEFAULT_PERMISSIONS`). Non-ticketing entities get view only,\n * matching `authenticated`.\n */\nexport function buildDefaultRoles(ac: ReturnType<typeof createAccessControl>, entities: Entity[]) {\n const adminPerms: Record<string, string[]> = {}\n const authenticatedPerms: Record<string, string[]> = {}\n const agentPerms: Record<string, string[]> = {}\n\n // Admin gets full control of user/session management\n adminPerms.user = [...ADMIN_STATEMENTS.user]\n adminPerms.session = [...ADMIN_STATEMENTS.session]\n // Non-admin roles get no user/session management\n authenticatedPerms.user = []\n authenticatedPerms.session = []\n agentPerms.user = []\n agentPerms.session = []\n\n for (const entity of entities) {\n adminPerms[entity.name] = isPublishable(entity)\n ? ['view', 'create', 'update', 'delete', 'publish']\n : ['view', 'create', 'update', 'delete']\n authenticatedPerms[entity.name] = ['view']\n // Agents get the seeded ticketing actions where defined; everything else\n // defaults to view-only (same as authenticated).\n agentPerms[entity.name] = AGENT_DEFAULT_PERMISSIONS[entity.name] ?? ['view']\n }\n\n return {\n admin: ac.newRole(adminPerms),\n authenticated: ac.newRole(authenticatedPerms),\n agent: ac.newRole(agentPerms),\n }\n}\n\nexport { createAccessControl }\n"],"mappings":"iEAeA,MAAM,EAAU,CAAC,OAAQ,SAAU,SAAU,SAAS,CAChD,EAAuB,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CASjE,EAAiB,CAAC,QAAS,SAAU,gBAAiB,QAAQ,CAkBrE,EAAsD,CAC1D,OAAQ,CAAC,OAAQ,SAAU,SAAS,CACpC,eAAgB,CAAC,OAAQ,SAAS,CAClC,kBAAmB,CAAC,OAAQ,SAAS,CACrC,WAAY,CAAC,OAAO,CACpB,WAAY,CAAC,OAAO,CACrB,CAGY,EAA2C,CACtD,IAAK,OACL,KAAM,SACN,MAAO,SACP,OAAQ,SACT,CAYD,SAAgB,GAAwE,CACtF,MAAO,CACL,OAAQ,EAAE,CACV,cAAe,EAAE,CACjB,MAAO,CAAE,GAAG,EAA2B,CACxC,CAWH,SAAgB,EACd,EACmB,CAEnB,IAAM,EAAQ,IAAI,IAElB,IAAK,GAAM,CAAC,EAAM,KAAc,OAAO,QAAQ,EAAgB,CAAE,CAC/D,IAAM,EAAc,IAAI,IACxB,IAAK,GAAM,CAAC,EAAU,KAAY,OAAO,QAAQ,EAAU,CACzD,EAAY,IAAI,EAAU,IAAI,IAAI,EAAQ,CAAC,CAE7C,EAAM,IAAI,EAAM,EAAY,CAG9B,OAAQ,EAAc,EAAkB,IAClC,IAAS,QAAgB,GACtB,EAAM,IAAI,EAAK,EAAE,IAAI,EAAS,EAAE,IAAI,EAAO,EAAI,GAS1D,SAAS,EAAc,EAAqD,CAC1E,OAAO,EAAO,WAAW,KAAM,GAAM,EAAE,OAAS,cAAc,EAAI,GAcpE,SAAgB,EACd,EACA,EAC0B,CAC1B,IAAM,EAAoC,EAAE,CAE5C,IAAK,IAAM,KAAU,EACnB,EAAQ,EAAO,MAAQ,EAAc,EAAO,CACxC,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CACjD,CAAC,OAAQ,SAAU,SAAU,SAAS,CAG5C,GAAI,MACG,IAAM,KAAS,EACd,EAAM,UAAY,EAAM,SAAW,EAAE,EAAM,YAAY,KACzD,EAAQ,EAAM,UAAY,CAAC,GAAG,EAAM,QAAQ,EAKlD,OAAO,EAQT,MAAM,EAAmB,CACvB,KAAM,CACJ,SACA,OACA,WACA,MACA,cACA,SACA,eACA,MACA,SACD,CACD,QAAS,CAAC,OAAQ,SAAU,SAAS,CACtC,CASD,SAAgB,EAAgB,EAAoB,CAClD,IAAM,EAA+C,CACnD,GAAG,EACJ,CACD,IAAK,IAAM,KAAU,EACnB,EAAU,EAAO,MAAQ,EAAc,EAAO,CAAG,EAAuB,EAE1E,OAAO,EAYT,SAAgB,EAAkB,EAA4C,EAAoB,CAChG,IAAM,EAAuC,EAAE,CACzC,EAA+C,EAAE,CACjD,EAAuC,EAAE,CAG/C,EAAW,KAAO,CAAC,GAAG,EAAiB,KAAK,CAC5C,EAAW,QAAU,CAAC,GAAG,EAAiB,QAAQ,CAElD,EAAmB,KAAO,EAAE,CAC5B,EAAmB,QAAU,EAAE,CAC/B,EAAW,KAAO,EAAE,CACpB,EAAW,QAAU,EAAE,CAEvB,IAAK,IAAM,KAAU,EACnB,EAAW,EAAO,MAAQ,EAAc,EAAO,CAC3C,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CACjD,CAAC,OAAQ,SAAU,SAAU,SAAS,CAC1C,EAAmB,EAAO,MAAQ,CAAC,OAAO,CAG1C,EAAW,EAAO,MAAQ,EAA0B,EAAO,OAAS,CAAC,OAAO,CAG9E,MAAO,CACL,MAAO,EAAG,QAAQ,EAAW,CAC7B,cAAe,EAAG,QAAQ,EAAmB,CAC7C,MAAO,EAAG,QAAQ,EAAW,CAC9B"}
|
|
1
|
+
{"version":3,"file":"permissions-BbVD2CmT.mjs","names":[],"sources":["../src/permissions.ts"],"sourcesContent":["/**\n * Permission system — bridges entity access definitions to better-auth's access control,\n * and provides the configurable permission checker for the admin API.\n *\n * Two layers:\n * 1. **better-auth integration**: `buildStatements` / `buildDefaultRoles` — feeds into\n * better-auth's `createAccessControl` for its internal permission system.\n * 2. **Admin API enforcement**: `buildPermissionChecker` / `buildInitialRoleDefinitions` —\n * the toolkit's own firewall-model permission system, persisted in settings.\n */\n\nimport type { PermissionChecker } from '@murumets-ee/core'\nimport type { Entity } from '@murumets-ee/entity'\nimport { createAccessControl } from 'better-auth/plugins/access'\n\nconst ACTIONS = ['view', 'create', 'update', 'delete'] as const\nconst ACTIONS_WITH_PUBLISH = ['view', 'create', 'update', 'delete', 'publish'] as const\n\nexport { ACTIONS, ACTIONS_WITH_PUBLISH }\n\n// ---------------------------------------------------------------------------\n// Built-in roles & constants\n// ---------------------------------------------------------------------------\n\n/** Built-in role names — cannot be deleted via the roles editor. */\nexport const BUILT_IN_ROLES = ['admin', 'public', 'authenticated', 'agent'] as const\n\n/**\n * Default permissions for the built-in `agent` role.\n *\n * Agents handle the ticketing inbox: they can view + update tickets,\n * messages, and attachments, and view the metadata that scopes them\n * (departments, tags). Delete actions and all email-template management\n * stay admin-only. Consumers can add more resources via the roles editor.\n *\n * SECURITY (#82): `ticket_message:create` is admin-only by design. Agents\n * authoring replies go through `/api/admin/ticketing/reply` (gated on\n * `ticket:update`), which uses `createSystemAdminClient` to perform the\n * write under elevated permissions while still recording the real agent\n * in the audit log. Allowing `ticket_message:create` for agents would\n * re-open the generic-CRUD impersonation path where an agent can spoof\n * another agent's `senderUserId` / `senderEmail` / `senderName`.\n */\nconst AGENT_DEFAULT_PERMISSIONS: Record<string, string[]> = {\n ticket: ['view', 'create', 'update'],\n ticket_message: ['view', 'update'],\n ticket_attachment: ['view', 'create'],\n department: ['view'],\n ticket_tag: ['view'],\n}\n\n/** Maps HTTP methods to permission action names. */\nexport const METHOD_TO_ACTION: Record<string, string> = {\n GET: 'view',\n POST: 'create',\n PATCH: 'update',\n DELETE: 'delete',\n}\n\n// ---------------------------------------------------------------------------\n// Admin API permission system (firewall model)\n// ---------------------------------------------------------------------------\n\n/**\n * Build initial role definitions for first run (no settings saved yet).\n *\n * All built-in non-admin roles start with ZERO permissions.\n * Admin is never stored — it's hardcoded in the checker.\n */\nexport function buildInitialRoleDefinitions(): Record<string, Record<string, string[]>> {\n return {\n public: {},\n authenticated: {},\n agent: { ...AGENT_DEFAULT_PERMISSIONS },\n }\n}\n\n/**\n * Build a synchronous permission checker from role definitions.\n *\n * Rules:\n * - `admin` role: ALWAYS returns `true` (hardcoded safety net, ignores settings)\n * - All other roles: exact match from `roleDefinitions` (deny-by-default)\n * - Unknown role / unknown resource / unknown action → `false`\n */\nexport function buildPermissionChecker(\n roleDefinitions: Record<string, Record<string, string[]>>,\n): PermissionChecker {\n // Pre-build lookup maps for O(1) checks\n const perms = new Map<string, Map<string, Set<string>>>()\n\n for (const [role, resources] of Object.entries(roleDefinitions)) {\n const resourceMap = new Map<string, Set<string>>()\n for (const [resource, actions] of Object.entries(resources)) {\n resourceMap.set(resource, new Set(actions))\n }\n perms.set(role, resourceMap)\n }\n\n return (role: string, resource: string, action: string): boolean => {\n if (role === 'admin') return true // Safety net — admin always passes\n return perms.get(role)?.get(resource)?.has(action) ?? false\n }\n}\n\n// ---------------------------------------------------------------------------\n// Resource catalog builder\n// ---------------------------------------------------------------------------\n\n/** Check if an entity has the publishable behavior. */\nfunction isPublishable(entity: { behaviors?: { name: string }[] }): boolean {\n return entity.behaviors?.some((b) => b.name === 'publishable') ?? false\n}\n\n/**\n * Build a complete resource catalog from entities and admin routes.\n *\n * Used by:\n * - `permissionRoutes()` config (`getStatements` callback)\n * - Server-side permission page data loaders\n *\n * Entities automatically get CRUD actions. Publishable entities also get\n * the `publish` action, which gates who can set status to 'published'.\n * Routes with `resource` and `actions` are added if not already present.\n */\nexport function buildResourceCatalog(\n entities: { name: string; behaviors?: { name: string }[] }[],\n routes?: { resource?: string; actions?: readonly string[] }[],\n): Record<string, string[]> {\n const catalog: Record<string, string[]> = {}\n\n for (const entity of entities) {\n catalog[entity.name] = isPublishable(entity)\n ? ['view', 'create', 'update', 'delete', 'publish']\n : ['view', 'create', 'update', 'delete']\n }\n\n if (routes) {\n for (const route of routes) {\n if (route.resource && route.actions && !(route.resource in catalog)) {\n catalog[route.resource] = [...route.actions]\n }\n }\n }\n\n return catalog\n}\n\n// ---------------------------------------------------------------------------\n// better-auth integration (unchanged, used by createAuthServer)\n// ---------------------------------------------------------------------------\n\n/** Admin plugin's built-in resources — must be included for listUsers, ban, etc. */\nconst ADMIN_STATEMENTS = {\n user: [\n 'create',\n 'list',\n 'set-role',\n 'ban',\n 'impersonate',\n 'delete',\n 'set-password',\n 'get',\n 'update',\n ] as const,\n session: ['list', 'revoke', 'delete'] as const,\n}\n\n/**\n * Build a permission statement object from all registered entities,\n * plus the admin plugin's built-in user/session resources.\n *\n * Publishable entities get the additional `publish` action.\n * Result shape: `{ user: [...], session: [...], article: ['view', ...], category: [...] }`\n */\nexport function buildStatements(entities: Entity[]) {\n const statement: Record<string, readonly string[]> = {\n ...ADMIN_STATEMENTS,\n }\n for (const entity of entities) {\n statement[entity.name] = isPublishable(entity) ? ACTIONS_WITH_PUBLISH : ACTIONS\n }\n return statement\n}\n\n/**\n * Build the default toolkit roles for better-auth's access control.\n *\n * - **admin**: full CRUD on all entities + user/session management\n * - **authenticated**: view only (better-auth's `defaultRole`)\n * - **agent**: view + create + update on the ticketing surface (see\n * `AGENT_DEFAULT_PERMISSIONS`). Non-ticketing entities get view only,\n * matching `authenticated`.\n */\nexport function buildDefaultRoles(ac: ReturnType<typeof createAccessControl>, entities: Entity[]) {\n const adminPerms: Record<string, string[]> = {}\n const authenticatedPerms: Record<string, string[]> = {}\n const agentPerms: Record<string, string[]> = {}\n\n // Admin gets full control of user/session management\n adminPerms.user = [...ADMIN_STATEMENTS.user]\n adminPerms.session = [...ADMIN_STATEMENTS.session]\n // Non-admin roles get no user/session management\n authenticatedPerms.user = []\n authenticatedPerms.session = []\n agentPerms.user = []\n agentPerms.session = []\n\n for (const entity of entities) {\n adminPerms[entity.name] = isPublishable(entity)\n ? ['view', 'create', 'update', 'delete', 'publish']\n : ['view', 'create', 'update', 'delete']\n authenticatedPerms[entity.name] = ['view']\n // Agents get the seeded ticketing actions where defined; everything else\n // defaults to view-only (same as authenticated).\n agentPerms[entity.name] = AGENT_DEFAULT_PERMISSIONS[entity.name] ?? ['view']\n }\n\n return {\n admin: ac.newRole(adminPerms),\n authenticated: ac.newRole(authenticatedPerms),\n agent: ac.newRole(agentPerms),\n }\n}\n\nexport { createAccessControl }\n"],"mappings":"mCAeA,MAAM,EAAU,CAAC,OAAQ,SAAU,SAAU,SAAS,CAChD,EAAuB,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CASjE,EAAiB,CAAC,QAAS,SAAU,gBAAiB,QAAQ,CAkBrE,EAAsD,CAC1D,OAAQ,CAAC,OAAQ,SAAU,SAAS,CACpC,eAAgB,CAAC,OAAQ,SAAS,CAClC,kBAAmB,CAAC,OAAQ,SAAS,CACrC,WAAY,CAAC,OAAO,CACpB,WAAY,CAAC,OAAO,CACrB,CAGY,EAA2C,CACtD,IAAK,OACL,KAAM,SACN,MAAO,SACP,OAAQ,SACT,CAYD,SAAgB,GAAwE,CACtF,MAAO,CACL,OAAQ,EAAE,CACV,cAAe,EAAE,CACjB,MAAO,CAAE,GAAG,EAA2B,CACxC,CAWH,SAAgB,EACd,EACmB,CAEnB,IAAM,EAAQ,IAAI,IAElB,IAAK,GAAM,CAAC,EAAM,KAAc,OAAO,QAAQ,EAAgB,CAAE,CAC/D,IAAM,EAAc,IAAI,IACxB,IAAK,GAAM,CAAC,EAAU,KAAY,OAAO,QAAQ,EAAU,CACzD,EAAY,IAAI,EAAU,IAAI,IAAI,EAAQ,CAAC,CAE7C,EAAM,IAAI,EAAM,EAAY,CAG9B,OAAQ,EAAc,EAAkB,IAClC,IAAS,QAAgB,GACtB,EAAM,IAAI,EAAK,EAAE,IAAI,EAAS,EAAE,IAAI,EAAO,EAAI,GAS1D,SAAS,EAAc,EAAqD,CAC1E,OAAO,EAAO,WAAW,KAAM,GAAM,EAAE,OAAS,cAAc,EAAI,GAcpE,SAAgB,EACd,EACA,EAC0B,CAC1B,IAAM,EAAoC,EAAE,CAE5C,IAAK,IAAM,KAAU,EACnB,EAAQ,EAAO,MAAQ,EAAc,EAAO,CACxC,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CACjD,CAAC,OAAQ,SAAU,SAAU,SAAS,CAG5C,GAAI,MACG,IAAM,KAAS,EACd,EAAM,UAAY,EAAM,SAAW,EAAE,EAAM,YAAY,KACzD,EAAQ,EAAM,UAAY,CAAC,GAAG,EAAM,QAAQ,EAKlD,OAAO,EAQT,MAAM,EAAmB,CACvB,KAAM,CACJ,SACA,OACA,WACA,MACA,cACA,SACA,eACA,MACA,SACD,CACD,QAAS,CAAC,OAAQ,SAAU,SAAS,CACtC,CASD,SAAgB,EAAgB,EAAoB,CAClD,IAAM,EAA+C,CACnD,GAAG,EACJ,CACD,IAAK,IAAM,KAAU,EACnB,EAAU,EAAO,MAAQ,EAAc,EAAO,CAAG,EAAuB,EAE1E,OAAO,EAYT,SAAgB,EAAkB,EAA4C,EAAoB,CAChG,IAAM,EAAuC,EAAE,CACzC,EAA+C,EAAE,CACjD,EAAuC,EAAE,CAG/C,EAAW,KAAO,CAAC,GAAG,EAAiB,KAAK,CAC5C,EAAW,QAAU,CAAC,GAAG,EAAiB,QAAQ,CAElD,EAAmB,KAAO,EAAE,CAC5B,EAAmB,QAAU,EAAE,CAC/B,EAAW,KAAO,EAAE,CACpB,EAAW,QAAU,EAAE,CAEvB,IAAK,IAAM,KAAU,EACnB,EAAW,EAAO,MAAQ,EAAc,EAAO,CAC3C,CAAC,OAAQ,SAAU,SAAU,SAAU,UAAU,CACjD,CAAC,OAAQ,SAAU,SAAU,SAAS,CAC1C,EAAmB,EAAO,MAAQ,CAAC,OAAO,CAG1C,EAAW,EAAO,MAAQ,EAA0B,EAAO,OAAS,CAAC,OAAO,CAG9E,MAAO,CACL,MAAO,EAAG,QAAQ,EAAW,CAC7B,cAAe,EAAG,QAAQ,EAAmB,CAC7C,MAAO,EAAG,QAAQ,EAAW,CAC9B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{l as e}from"./schema-Je6e5yt2.mjs";import{r as t,s as n}from"./permissions-BbVD2CmT.mjs";import{createAccessControl as r}from"better-auth/plugins/access";import{betterAuth as i}from"better-auth";import{drizzleAdapter as a}from"better-auth/adapters/drizzle";import{nextCookies as o}from"better-auth/next-js";import{admin as s}from"better-auth/plugins";import{organization as c}from"better-auth/plugins/organization";import{APIError as l,createAuthMiddleware as u}from"better-auth/api";function d(e,t){return n=>{e?.log(n).catch(e=>{t.warn({err:e,action:n.action},`audit log write failed`)})}}function f(e){let t=e?.context?.session??void 0,n=t?.user;return{id:n?.id??t?.userId,name:n?.name}}function p(e){return e.context.returned?.status}function m(e){return e.body??void 0}function h(e){if(typeof e==`string`)return e.length<=256?e:e.slice(0,256)}const g=new Set([`updatedAt`,`createdAt`]);function _(e){let t=new WeakMap;return{user:{create:{after:async t=>{e({action:`auth.signup`,entityType:`user`,...t.id!==void 0&&{entityId:t.id,userId:t.id},...t.name!==void 0&&{userName:t.name},changes:{fields:{name:t.name,email:t.email}}})}},update:{before:async(e,n)=>{if(!n)return;let r={};for(let[t,n]of Object.entries(e))g.has(t)||t===`id`||(r[t]=n);Object.keys(r).length>0&&t.set(n,r)},after:async(n,r)=>{let i=f(r),a=null;r&&(a=t.get(r)??null,a&&t.delete(r)),e({action:`auth.user.update`,entityType:`user`,...n.id!==void 0&&{entityId:n.id},...i.id!==void 0&&{userId:i.id},...i.name!==void 0&&{userName:i.name},...a?{changes:{fields:a}}:{},metadata:{targetUser:n.name}})}},delete:{after:async(t,n)=>{let r=f(n);e({action:`auth.user.delete`,entityType:`user`,...t.id!==void 0&&{entityId:t.id},...r.id!==void 0&&{userId:r.id},...r.name!==void 0&&{userName:r.name},metadata:{targetUser:t.name}})}}}}}const v=[`/sign-in/email`,`/sign-in/social`],y={"/change-password":`auth.password.change`,"/forget-password":`auth.password.reset_request`,"/reset-password":`auth.password.reset`},b={"/admin/set-role":`auth.admin.set_role`,"/admin/ban-user":`auth.admin.ban`,"/admin/unban-user":`auth.admin.unban`,"/admin/create-user":`auth.admin.create_user`,"/admin/remove-user":`auth.admin.remove_user`,"/admin/impersonate-user":`auth.admin.impersonate`,"/admin/stop-impersonating":`auth.admin.stop_impersonating`,"/admin/revoke-session":`auth.admin.revoke_session`,"/admin/revoke-sessions":`auth.admin.revoke_sessions`};function x(e){return{after:u(async t=>{let n=t;if(v.some(e=>n.path.startsWith(e))){let t=p(n);if(!t)return;if(t>=400){let r=h(m(n)?.email);e({action:`auth.login.failed`,metadata:{...r?{email:r}:{},status:t,path:n.path}})}else{let t=n.context.newSession;t?.user?.id&&e({action:`auth.login`,entityType:`user`,entityId:t.user.id,userId:t.user.id,...t.user.name!==void 0&&{userName:t.user.name}})}return}if(n.path===`/sign-out`){let t=p(n);if(!t||t<400){let t=f(n);t.id&&e({action:`auth.logout`,entityType:`user`,entityId:t.id,userId:t.id,...t.name!==void 0&&{userName:t.name}})}return}let r=y[n.path];if(r){let t=p(n);if(!t||t<400){let t=f(n),i=h(m(n)?.email);e({action:r,entityType:`user`,...t.id!==void 0&&{userId:t.id},...t.name!==void 0&&{userName:t.name},metadata:{...i?{email:i}:{},path:n.path}})}return}let i=b[n.path];if(!i)return;let a=p(n);if(a&&a>=400)return;let o=f(n),s=m(n);e({action:i,entityType:`user`,...typeof s?.userId==`string`&&{entityId:s.userId},...o.id!==void 0&&{userId:o.id},...o.name!==void 0&&{userName:o.name},metadata:{...typeof s?.userId==`string`?{targetUserId:s.userId}:{},...typeof s?.role==`string`?{role:s.role}:{},path:n.path}})})}}function S(e){let{enabled:t,auditLogger:n,logger:r}=e,i=d(n,r);return u(async e=>{let n=e;if(n.path!==`/sign-up/email`||t)return;let r=n.body??void 0,a=f(n),o=r?.email,s=typeof o==`string`&&o.length>0?o.slice(0,256):void 0;throw i({action:`auth.signup.blocked`,metadata:{path:n.path,...s?{email:s}:{},...a.id?{actorId:a.id}:{}}}),new l(`FORBIDDEN`,{message:`Sign-up is closed`})})}function C(e,t){let n={};return e.social?.google&&(n.google={...e.social.google,...t?{disableSignUp:!0}:{}}),e.social?.github&&(n.github={...e.social.github,...t?{disableSignUp:!0}:{}}),n}function w(e,t=process.env){if(t.NODE_ENV===`production`&&!(t.NEXT_PHASE===`phase-production-build`||t.NEXT_PHASE===`phase-export`)){if(!t.BETTER_AUTH_SECRET)throw Error("@murumets-ee/auth: BETTER_AUTH_SECRET is required in production. Generate one with `openssl rand -base64 32` and set it in the environment.");if(!e.baseURL&&!t.BETTER_AUTH_URL)throw Error('@murumets-ee/auth: a public baseURL is required in production. Set `auth({ baseURL: "https://..." })` or the `BETTER_AUTH_URL` env var.')}}function T(i,l,u,f=process.env){let p=[...l.entities.values()],m=r(n(p)),h=t(m,p),g=i.baseURL??f.BETTER_AUTH_URL,v=i.signup?.enabled??!1,y=C(i,!v),b=i.schema??e,w=d(u,l.logger),T=i.rateLimit?.storage??`memory`;return{database:a(l.db.readWrite,{provider:`pg`,schema:b}),...g?{baseURL:g}:{},...i.trustedOrigins?{trustedOrigins:i.trustedOrigins}:{},emailAndPassword:{enabled:i.providers?.includes(`email`)??!0,requireEmailVerification:i.signup?.requireEmailVerification??v},socialProviders:y,session:{expiresIn:i.session?.expiresIn??3600*2,updateAge:i.session?.updateAge??3600},rateLimit:{enabled:!0,window:60,max:100,storage:T,customRules:{"/sign-in/email":{window:60,max:5},"/sign-in/social":{window:60,max:10},"/sign-up/email":{window:60,max:3},"/forget-password":{window:60,max:3},"/reset-password":{window:60,max:5},"/admin/*":{window:60,max:20}}},databaseHooks:_(w),hooks:{before:S({enabled:v,auditLogger:u,logger:l.logger}),...u?x(w):{}},plugins:[s({ac:m,roles:h,defaultRole:`authenticated`}),...i.organizations?[c({ac:m,roles:h})]:[],...i.betterAuthPlugins??[],o()]}}function E(e,t,n){if(globalThis.window!==void 0)throw Error(`@murumets-ee/auth: createAuthServer must not run in a browser environment. Construct the auth server from server code only (RSC, route handlers, CLI, worker).`);return w(e),i(T(e,t,n))}let D=null,O=null;function k(e){D=e}function A(e){O=e}function j(){if(!D)throw Error(`@murumets-ee/auth not initialized. Add auth() to your plugins array.`);return D}function M(){return O?.signup?.enabled===!0}export{E as a,M as i,A as n,j as r,k as t};
|
|
2
|
+
//# sourceMappingURL=runtime-Cj8SEWhk.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-Cj8SEWhk.mjs","names":["defaultAuthSchema"],"sources":["../src/audit-hooks.ts","../src/signup-gate.ts","../src/server.ts","../src/runtime.ts"],"sourcesContent":["/**\n * Audit hooks for better-auth events.\n *\n * Two layers:\n * 1. `buildDatabaseHooks` — fires on user CRUD via better-auth's `databaseHooks`.\n * Captures `auth.signup`, `auth.user.update`, `auth.user.delete`.\n * 2. `buildRequestHooks` — fires on HTTP request lifecycle via the `hooks.after`\n * middleware. Captures `auth.login`, `auth.login.failed`, `auth.logout`,\n * `auth.password.*`, and `auth.admin.*` operations.\n *\n * All audit writes are fire-and-forget — they never block auth operations,\n * but failures are logged via the application logger so silent loss of audit\n * trail does not pass unnoticed.\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport { createAuthMiddleware } from 'better-auth/api'\n\n// ---------------------------------------------------------------------------\n// Structural types\n// ---------------------------------------------------------------------------\n//\n// We avoid importing `GenericEndpointContext` / `User` from better-auth\n// because their generic parameters and zod-internal references trip\n// tsdown's DTS emit (TS2742). Instead, we declare exactly the shape these\n// hooks read — narrower than `Record<string, unknown>`, fully typed.\n\n/** Subset of better-auth's `User` row we read in audit payloads. */\ninterface UserRow {\n id?: string\n name?: string\n email?: string\n}\n\n/** Subset of better-auth's `GenericEndpointContext` we read in audit hooks. */\nexport interface AuthHookCtx {\n path: string\n body?: unknown\n context: {\n session?: { user?: UserRow; userId?: string } | null\n returned?: { status?: number } | unknown\n newSession?: { user?: UserRow } | null\n }\n}\n\n/** Body fields the hooks inspect for audit metadata. */\ninterface AuthBody {\n email?: unknown\n userId?: unknown\n role?: unknown\n}\n\n/** Audit-log entry shape (mirrors AuditLogger.log signature). */\ntype AuditEntry = Parameters<AuditLogger['log']>[0]\n\n/** Fire-and-forget audit emitter. Logs swallow reasons via `logger.warn` so\n * audit-write failures aren't entirely silent (CLAUDE.md: empty `.catch`\n * needs justification, and \"logger.warn on swallow\" IS the justification). */\nexport function makeAuditFn(\n auditLogger: AuditLogger | undefined,\n logger: Logger,\n): (entry: AuditEntry) => void {\n return (entry) => {\n auditLogger?.log(entry).catch((err: unknown) => {\n logger.warn({ err, action: entry.action }, 'audit log write failed')\n })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Read the actor (current session user) from a hook context. */\nexport function getActor(ctx: AuthHookCtx | null | undefined): {\n id: string | undefined\n name: string | undefined\n} {\n const session = ctx?.context?.session ?? undefined\n const user = session?.user\n return {\n id: user?.id ?? session?.userId,\n name: user?.name,\n }\n}\n\n/** Read the response status the route handler produced (if any). */\nfunction getReturnedStatus(ctx: AuthHookCtx): number | undefined {\n const returned = ctx.context.returned as { status?: number } | undefined\n return returned?.status\n}\n\n/** Read the request body as a typed-but-narrow `AuthBody`. */\nfunction getBody(ctx: AuthHookCtx): AuthBody | undefined {\n return (ctx.body ?? undefined) as AuthBody | undefined\n}\n\n/** Maximum length for user-supplied strings written to the audit log.\n * Caps the blast-radius of a malicious client sending megabyte-sized\n * inputs (`email`, etc.) into the audit table. RFC 5321 caps email at\n * 254 chars; we round to 256. */\nconst AUDIT_STRING_CAP = 256\n\n/** Cap a user-supplied string for audit-log emission. Returns undefined\n * for non-strings so callers can spread it into metadata conditionally. */\nfunction capForAudit(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n return value.length <= AUDIT_STRING_CAP ? value : value.slice(0, AUDIT_STRING_CAP)\n}\n\n/** Fields stripped from user-update audit payloads (noise). */\nconst SKIP_USER_UPDATE_FIELDS = new Set(['updatedAt', 'createdAt'])\n\n// ---------------------------------------------------------------------------\n// Database hooks (user CRUD)\n// ---------------------------------------------------------------------------\n//\n// HIGH-2 fix (#TBD): the previous implementation used a closure-scoped\n// `pendingFields` variable to bridge `before` → `after`. That bridge was\n// shared across ALL concurrent requests handled by the same Node process,\n// causing audit-log integrity bugs (request A's diff could be logged\n// against user B). Replaced with a `WeakMap` keyed by the per-request\n// context object — better-auth passes the same `GenericEndpointContext`\n// instance to `before` and `after` of the same operation.\n\ninterface DatabaseHookCallbacks {\n user: {\n create: { after: (user: UserRow, ctx: AuthHookCtx | null) => Promise<void> }\n update: {\n before: (userData: Record<string, unknown>, ctx: AuthHookCtx | null) => Promise<void>\n after: (user: UserRow, ctx: AuthHookCtx | null) => Promise<void>\n }\n delete: { after: (user: UserRow, ctx: AuthHookCtx | null) => Promise<void> }\n }\n}\n\nexport function buildDatabaseHooks(audit: (entry: AuditEntry) => void): DatabaseHookCallbacks {\n /** Per-request scratch space bridging `update.before` → `update.after`.\n * Keyed by the better-auth hook context object — concurrent requests get\n * isolated entries because better-auth passes the same context instance\n * to `before` and `after` of the same operation. */\n const pendingFieldsByCtx = new WeakMap<object, Record<string, unknown>>()\n\n return {\n user: {\n create: {\n after: async (user) => {\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 ...(user.id !== undefined && { entityId: user.id, userId: user.id }),\n ...(user.name !== undefined && { userName: user.name }),\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 // When ctx is null (internal-adapter writes outside an HTTP request,\n // e.g. background jobs) we skip the bridge entirely and let `after`\n // fire without a diff. The previous fallback used a closure variable\n // shared across all ctx-less calls, which would race the same way\n // the WeakMap fix removed for HTTP requests.\n before: async (userData, ctx) => {\n if (!ctx) return\n const fields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(userData)) {\n if (SKIP_USER_UPDATE_FIELDS.has(key) || key === 'id') continue\n fields[key] = value\n }\n if (Object.keys(fields).length > 0) {\n pendingFieldsByCtx.set(ctx, fields)\n }\n },\n // `after` has full user (name) + ctx (actor session) — log everything.\n after: async (user, ctx) => {\n const actor = getActor(ctx)\n let fields: Record<string, unknown> | null = null\n if (ctx) {\n fields = pendingFieldsByCtx.get(ctx) ?? null\n if (fields) pendingFieldsByCtx.delete(ctx)\n }\n audit({\n action: 'auth.user.update',\n entityType: 'user',\n ...(user.id !== undefined && { entityId: user.id }),\n ...(actor.id !== undefined && { userId: actor.id }),\n ...(actor.name !== undefined && { userName: actor.name }),\n ...(fields ? { changes: { fields } } : {}),\n metadata: {\n targetUser: user.name,\n },\n })\n },\n },\n delete: {\n after: async (user, ctx) => {\n const actor = getActor(ctx)\n audit({\n action: 'auth.user.delete',\n entityType: 'user',\n ...(user.id !== undefined && { entityId: user.id }),\n ...(actor.id !== undefined && { userId: actor.id }),\n ...(actor.name !== undefined && { userName: actor.name }),\n metadata: {\n targetUser: user.name,\n },\n })\n },\n },\n },\n }\n}\n\n// ---------------------------------------------------------------------------\n// Request hooks (auth lifecycle: login, logout, password, admin ops)\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'] as const\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\nexport function buildRequestHooks(audit: (entry: AuditEntry) => void): {\n after: ReturnType<typeof createAuthMiddleware>\n} {\n return {\n after: createAuthMiddleware(async (rawCtx) => {\n const ctx = rawCtx as unknown as AuthHookCtx\n\n // --- Login attempt logging ---\n if (SIGN_IN_PATHS.some((p) => ctx.path.startsWith(p))) {\n const status = getReturnedStatus(ctx)\n if (!status) return\n\n if (status >= 400) {\n const email = capForAudit(getBody(ctx)?.email)\n audit({\n action: 'auth.login.failed',\n metadata: {\n ...(email ? { email } : {}),\n status,\n path: ctx.path,\n },\n })\n } else {\n const newSession = ctx.context.newSession\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 ...(newSession.user.name !== undefined && { userName: newSession.user.name }),\n })\n }\n }\n return\n }\n\n // --- Logout logging ---\n if (ctx.path === SIGN_OUT_PATH) {\n const status = getReturnedStatus(ctx)\n if (!status || 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 ...(actor.name !== undefined && { 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 status = getReturnedStatus(ctx)\n if (!status || status < 400) {\n const actor = getActor(ctx)\n const body = getBody(ctx)\n const email = capForAudit(body?.email)\n audit({\n action: passwordAction,\n entityType: 'user',\n ...(actor.id !== undefined && { userId: actor.id }),\n ...(actor.name !== undefined && { userName: actor.name }),\n metadata: {\n // For reset requests, log the email (not sensitive — it's the input)\n ...(email ? { 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 status = getReturnedStatus(ctx)\n if (status && status >= 400) return // failed — skip\n\n const actor = getActor(ctx)\n const body = getBody(ctx)\n audit({\n action: auditAction,\n entityType: 'user',\n ...(typeof body?.userId === 'string' && { entityId: body.userId }),\n ...(actor.id !== undefined && { userId: actor.id }),\n ...(actor.name !== undefined && { userName: actor.name }),\n metadata: {\n ...(typeof body?.userId === 'string' ? { targetUserId: body.userId } : {}),\n ...(typeof body?.role === 'string' ? { role: body.role } : {}),\n path: ctx.path,\n },\n })\n }),\n }\n}\n","/**\n * Signup gate — rejects public registration on `/sign-up/email` unless\n * explicitly enabled via `auth({ signup: { enabled: true } })`.\n *\n * Closed by default. The first admin is bootstrapped via `lumi create-admin`,\n * which writes through `auth.$context.internalAdapter` below the HTTP layer\n * so this gate never fires for CLI bootstrap.\n *\n * Note: this gate covers `/sign-up/email` only. Social-provider signup is\n * gated separately by passing `disableSignUp: true` per provider in\n * `createAuthServer` when `signup.enabled === false` (see SECURITY-HIGH-1).\n */\n\nimport type { Logger } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport { APIError, createAuthMiddleware } from 'better-auth/api'\nimport type { AuthHookCtx } from './audit-hooks.js'\nimport { getActor, makeAuditFn } from './audit-hooks.js'\n\nconst SIGN_UP_EMAIL_PATH = '/sign-up/email'\n\nexport interface SignupGateOptions {\n enabled: boolean\n /** Optional audit logger — when present, blocked attempts are logged. */\n auditLogger?: AuditLogger | undefined\n /** Application logger used to swallow audit-write failures non-silently. */\n logger: Logger\n}\n\nexport function buildSignupGate(\n options: SignupGateOptions,\n): ReturnType<typeof createAuthMiddleware> {\n const { enabled, auditLogger, logger } = options\n const audit = makeAuditFn(auditLogger, logger)\n\n return createAuthMiddleware(async (rawCtx) => {\n const ctx = rawCtx as unknown as AuthHookCtx\n if (ctx.path !== SIGN_UP_EMAIL_PATH) return\n if (enabled) return\n\n // Log the blocked attempt before throwing — gives the SOC visibility\n // into pattern-of-attempts without relying solely on rate-limit counters.\n const body = (ctx.body ?? undefined) as { email?: unknown } | undefined\n const actor = getActor(ctx)\n // Cap the user-supplied email at 256 chars — caps blast radius of a\n // malicious client sending a megabyte-sized `email` field into our\n // audit table. RFC 5321 caps real emails at 254.\n const rawEmail = body?.email\n const email =\n typeof rawEmail === 'string' && rawEmail.length > 0 ? rawEmail.slice(0, 256) : undefined\n audit({\n action: 'auth.signup.blocked',\n metadata: {\n path: ctx.path,\n ...(email ? { email } : {}),\n ...(actor.id ? { actorId: actor.id } : {}),\n },\n })\n\n throw new APIError('FORBIDDEN', { message: 'Sign-up is closed' })\n })\n}\n","/**\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 { 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 { buildDatabaseHooks, buildRequestHooks, makeAuditFn } from './audit-hooks.js'\nimport { buildDefaultRoles, buildStatements } from './permissions.js'\nimport * as defaultAuthSchema from './schema.js'\nimport { buildSignupGate } from './signup-gate.js'\nimport type { AdminUser, AuthConfig } from './types.js'\n\n// ---------------------------------------------------------------------------\n// Provider config — wires the closed-signup contract through to OAuth\n// ---------------------------------------------------------------------------\n\nexport interface SocialProviderConfig {\n clientId: string\n clientSecret: string\n /** When `true`, OAuth login refuses to create an account for an unknown\n * email. Existing users sign in normally. SECURITY-HIGH-1: required when\n * `signup.enabled === false`, otherwise the closed-signup gate is bypassed. */\n disableSignUp?: boolean\n}\n\n/** @internal Exported for unit tests — not part of the public API. */\nexport function buildSocialProviders(\n config: AuthConfig,\n signupClosed: boolean,\n): Record<string, SocialProviderConfig> {\n const out: Record<string, SocialProviderConfig> = {}\n if (config.social?.google) {\n out.google = {\n ...config.social.google,\n ...(signupClosed ? { disableSignUp: true } : {}),\n }\n }\n if (config.social?.github) {\n out.github = {\n ...config.social.github,\n ...(signupClosed ? { disableSignUp: true } : {}),\n }\n }\n return out\n}\n\n// ---------------------------------------------------------------------------\n// Production env asserts (SECURITY-MED-1, SECURITY-MED-5)\n// ---------------------------------------------------------------------------\n\n/**\n * Throw if production *runtime* is missing the env vars better-auth needs\n * for stable cookies and CSRF anchoring.\n *\n * Called from `createAuthServer` at plugin init time so a misconfigured\n * deploy crashes loudly instead of silently degrading (random per-process\n * secret = sessions invalidate on every restart; missing baseURL = CSRF\n * Origin check anchored to whatever header arrives first).\n *\n * NOTE: skipped during `next build` (`NEXT_PHASE=phase-production-build`).\n * Build-time page-data collection initializes the toolkit and would\n * otherwise crash builds where env vars are injected at runtime only\n * (typical Docker setup). The assert still fires when the production\n * server actually starts (`phase-production-server`) — that's where a\n * misconfigured deploy actually matters. Same reasoning for static\n * export (`phase-export`).\n *\n * @internal Exported for unit tests — not part of the public API.\n */\nexport function assertProductionEnv(\n config: AuthConfig,\n env: NodeJS.ProcessEnv = process.env,\n): void {\n if (env.NODE_ENV !== 'production') return\n if (env.NEXT_PHASE === 'phase-production-build' || env.NEXT_PHASE === 'phase-export') return\n if (!env.BETTER_AUTH_SECRET) {\n throw new Error(\n '@murumets-ee/auth: BETTER_AUTH_SECRET is required in production. ' +\n 'Generate one with `openssl rand -base64 32` and set it in the environment.',\n )\n }\n if (!config.baseURL && !env.BETTER_AUTH_URL) {\n throw new Error(\n '@murumets-ee/auth: a public baseURL is required in production. ' +\n 'Set `auth({ baseURL: \"https://...\" })` or the `BETTER_AUTH_URL` env var.',\n )\n }\n}\n\n// ---------------------------------------------------------------------------\n// Pure option builder (so tests can exercise wiring without `betterAuth()`)\n// ---------------------------------------------------------------------------\n\n/**\n * Build the `BetterAuthOptions` that `createAuthServer` hands to `betterAuth()`.\n *\n * Pure, side-effect-free, no env-var reads except `BETTER_AUTH_URL` as a\n * fallback for `config.baseURL`. Extracted so unit tests can inspect the\n * resulting options object (rate-limit storage, social-provider gating,\n * email-verification default) without invoking better-auth's full setup.\n *\n * @internal Exported for unit tests — not part of the public API.\n */\nexport function buildAuthOptions(\n config: AuthConfig,\n app: ToolkitApp,\n auditLogger?: AuditLogger,\n env: NodeJS.ProcessEnv = process.env,\n): BetterAuthOptions {\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 const baseURL = config.baseURL ?? env.BETTER_AUTH_URL\n const signupEnabled = config.signup?.enabled ?? false\n const socialProviders = buildSocialProviders(config, !signupEnabled)\n const schema = config.schema ?? defaultAuthSchema\n const audit = makeAuditFn(auditLogger, app.logger)\n const rateLimitStorage: 'memory' | 'database' | 'secondary-storage' =\n config.rateLimit?.storage ?? 'memory'\n\n return {\n database: drizzleAdapter(app.db.readWrite, {\n provider: 'pg',\n schema,\n }),\n\n ...(baseURL ? { baseURL } : {}),\n ...(config.trustedOrigins ? { trustedOrigins: config.trustedOrigins } : {}),\n\n emailAndPassword: {\n enabled: config.providers?.includes('email') ?? true,\n // SECURITY-MED-3: when public signup is enabled, default to\n // requiring email verification — without it, attackers can register\n // any email and immediately sign in.\n requireEmailVerification: config.signup?.requireEmailVerification ?? signupEnabled,\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: rateLimitStorage,\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(audit),\n hooks: {\n before: buildSignupGate({ enabled: signupEnabled, auditLogger, logger: app.logger }),\n ...(auditLogger ? buildRequestHooks(audit) : {}),\n },\n\n plugins: [\n admin({ ac, roles, defaultRole: 'authenticated' }),\n ...(config.organizations ? [organization({ ac, roles })] : []),\n ...(config.betterAuthPlugins ?? []),\n // nextCookies() must be last — required for Next.js server actions.\n nextCookies(),\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 // Runtime belt-and-suspenders: `createAuthServer` is exposed via the\n // `/runtime` subpath (which has no `server-only` marker so the CLI / worker\n // can load `lumi.config.ts` under jiti / tsx). The marker-less export trades\n // a clean build-time error for a cryptic bundler error if a client\n // component ever tries to import it. This guard adds a runtime failure\n // with a clear message in case that reaches a browser somehow.\n if (typeof (globalThis as { window?: unknown }).window !== 'undefined') {\n throw new Error(\n '@murumets-ee/auth: createAuthServer must not run in a browser environment. ' +\n 'Construct the auth server from server code only (RSC, route handlers, CLI, worker).',\n )\n }\n\n // SECURITY-MED-1 + SECURITY-MED-5: refuse to start a production deploy\n // missing `BETTER_AUTH_SECRET` or a stable `baseURL`. Catches the\n // misconfig early instead of after the first session-invalidation\n // surprise.\n assertProductionEnv(config)\n\n // Widen to BetterAuthOptions so betterAuth() returns Auth<BetterAuthOptions>\n // (= BetterAuthBase). Required because:\n // 1. Without the 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 erases plugin-specific API types at compile time, but plugin\n // endpoints (listUsers, etc.) are fully functional at runtime.\n return betterAuth(buildAuthOptions(config, app, auditLogger))\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","/**\n * Auth runtime — CLI-safe subpath for the plugin factory and for code that\n * needs to read auth singleton state without pulling in the `server-only`\n * marker from the main `@murumets-ee/auth` entry. Imported by:\n *\n * - `@murumets-ee/auth-ui/plugin` (the factory — needs `createAuthServer`\n * + the state setters + the schema tables)\n * - `@murumets-ee/cli` `create-admin` command (needs `getAuth` under jiti\n * without triggering `server-only`)\n *\n * Everything re-exported here is `server-only`-clean by construction:\n * - `createAuthServer`, `Auth`, `AuthAdminApi` — from `./server.js`\n * - `AuthConfig`, `AdminUser` — from `./types.js`\n * - better-auth Drizzle tables — from `./schema.js`\n */\n\nexport {\n account,\n invitation,\n member,\n organization,\n session,\n user,\n verification,\n} from './schema.js'\nexport type { Auth, AuthAdminApi } from './server.js'\nexport { createAuthServer } from './server.js'\nexport type { AdminUser, AuthConfig } from './types.js'\n\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 */\nexport type 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 * Internal: store the resolved better-auth server instance. Called from the\n * `auth()` plugin factory's `server.init`; not part of the public API.\n *\n * @internal\n */\nexport function _setAuth(auth: AuthWithAdmin): void {\n _auth = auth\n}\n\n/**\n * Internal: capture the plugin config. Invoked by the `auth()` factory at\n * construction time (not init) so `isSignupEnabled()` works during RSC\n * render on cold boot, before async init completes.\n *\n * @internal\n */\nexport function _setAuthConfig(config: AuthConfig): void {\n _authConfig = config\n}\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\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 plugins array.')\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 lumi.config.ts to open.\n */\nexport function isSignupEnabled(): boolean {\n return _authConfig?.signup?.enabled === true\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as AuthConfig, t as AdminUser } from "./types-
|
|
1
|
+
import { n as AuthConfig, t as AdminUser } from "./types-Bkei6fxa.mjs";
|
|
2
2
|
import { Auth } from "better-auth";
|
|
3
3
|
import { ToolkitApp } from "@murumets-ee/core";
|
|
4
4
|
import { AuditLogger } from "@murumets-ee/logging";
|
|
@@ -95,4 +95,4 @@ declare function getAuth(): AuthWithAdmin;
|
|
|
95
95
|
declare function isSignupEnabled(): boolean;
|
|
96
96
|
//#endregion
|
|
97
97
|
export { isSignupEnabled as a, createAuthServer as c, getAuth as i, _setAuth as n, Auth$1 as o, _setAuthConfig as r, AuthAdminApi as s, AuthWithAdmin as t };
|
|
98
|
-
//# sourceMappingURL=runtime-
|
|
98
|
+
//# sourceMappingURL=runtime-D0zIyEjn.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-D0zIyEjn.d.mts","names":[],"sources":["../src/server.ts","../src/runtime.ts"],"mappings":";;;;;;;;;;ACiCA;;;;;;;;;AAgBA;;;;;AAWA;;iBDyJgB,gBAAA,CACd,MAAA,EAAQ,UAAA,EACR,GAAA,EAAK,UAAA,EACL,WAAA,GAAc,WAAA,GACb,IAAA;;;AC5IH;KD8KY,MAAA,GAAO,IAAA;;;;ACjKnB;;;;;;UD4KiB,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;;;;KC7O1B,aAAA,GAAgB,MAAA;EAAS,GAAA,EAAK,YAAA;AAAA;AD0N1C;;;;;AAWA;AAXA,iBC1MgB,QAAA,CAAS,IAAA,EAAM,aAAA;;;;;;;;iBAWf,cAAA,CAAe,MAAA,EAAQ,UAAA;;;;;;;;;;;;;;iBAiBvB,OAAA,CAAA,GAAW,aAAA;;AA5C3B;;;;;iBAyDgB,eAAA,CAAA"}
|
package/dist/runtime.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as AuthConfig, t as AdminUser } from "./types-
|
|
2
|
-
import { a as isSignupEnabled, c as createAuthServer, i as getAuth, n as _setAuth, o as Auth, r as _setAuthConfig, s as AuthAdminApi, t as AuthWithAdmin } from "./runtime-
|
|
1
|
+
import { n as AuthConfig, t as AdminUser } from "./types-Bkei6fxa.mjs";
|
|
2
|
+
import { a as isSignupEnabled, c as createAuthServer, i as getAuth, n as _setAuth, o as Auth, r as _setAuthConfig, s as AuthAdminApi, t as AuthWithAdmin } from "./runtime-D0zIyEjn.mjs";
|
|
3
3
|
import { a as member, d as user, l as session, p as verification, r as invitation, s as organization, t as account } from "./schema-CSXsOlBD.mjs";
|
|
4
4
|
export { AdminUser, Auth, AuthAdminApi, AuthConfig, AuthWithAdmin, _setAuth, _setAuthConfig, account, createAuthServer, getAuth, invitation, isSignupEnabled, member, organization, session, user, verification };
|
package/dist/runtime.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
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";import{a as s,i as c,n as l,r as u,t as d}from"./runtime-
|
|
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";import{a as s,i as c,n as l,r as u,t as d}from"./runtime-Cj8SEWhk.mjs";export{d as _setAuth,l as _setAuthConfig,a as account,s as createAuthServer,u as getAuth,r as invitation,c as isSignupEnabled,e as member,i as organization,o as session,t as user,n as verification};
|
|
@@ -75,11 +75,51 @@ interface AuthConfig {
|
|
|
75
75
|
* Open registration carries real risk (anyone reaching the signup page
|
|
76
76
|
* becomes a user), so this is opt-in per-deployment rather than runtime-
|
|
77
77
|
* togglable via settings.
|
|
78
|
+
*
|
|
79
|
+
* When `enabled: false` (the default), social-provider signup is also
|
|
80
|
+
* blocked: every configured social provider is wired with `disableSignUp:
|
|
81
|
+
* true` so an unknown email completing OAuth cannot create a new account.
|
|
82
|
+
* Existing users still sign in normally.
|
|
78
83
|
*/
|
|
79
84
|
signup?: {
|
|
80
|
-
/** Allow public POST to /sign-up/email. Default: false (closed). */enabled?: boolean;
|
|
85
|
+
/** Allow public POST to /sign-up/email and social-provider signup. Default: false (closed). */enabled?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Require email verification before sign-in works (better-auth's
|
|
88
|
+
* `emailAndPassword.requireEmailVerification`). Defaults to `true` when
|
|
89
|
+
* `signup.enabled === true`, `false` when signup is closed (irrelevant —
|
|
90
|
+
* no public sign-ups happen in that mode).
|
|
91
|
+
*/
|
|
92
|
+
requireEmailVerification?: boolean;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Public origin for the deployment, e.g. `https://cms.example.com`.
|
|
96
|
+
*
|
|
97
|
+
* Threaded into better-auth's `baseURL` and used to validate the `Origin`
|
|
98
|
+
* header on every state-changing request (CSRF defense). Falls back to
|
|
99
|
+
* the `BETTER_AUTH_URL` env var; in production at least one MUST be set
|
|
100
|
+
* — `createAuthServer` throws at startup otherwise.
|
|
101
|
+
*/
|
|
102
|
+
baseURL?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Additional origins allowed alongside `baseURL` (better-auth's
|
|
105
|
+
* `trustedOrigins`). Useful for previews, mobile schemes, or multiple
|
|
106
|
+
* domains served by the same instance.
|
|
107
|
+
*
|
|
108
|
+
* The function form must accept an optional `Request` (better-auth may
|
|
109
|
+
* call it during context init before a request arrives) and may return
|
|
110
|
+
* `null`/`undefined` entries which better-auth filters out.
|
|
111
|
+
*/
|
|
112
|
+
trustedOrigins?: string[] | ((request?: Request) => (string | null | undefined)[] | Promise<(string | null | undefined)[]>);
|
|
113
|
+
/**
|
|
114
|
+
* Rate-limit configuration. Defaults to in-memory storage with sensible
|
|
115
|
+
* per-path caps. **In-memory storage is single-process only** — multi-
|
|
116
|
+
* instance deployments MUST set `storage: 'database'` (or `'secondary-
|
|
117
|
+
* storage'` for Redis) so counters are shared across replicas.
|
|
118
|
+
*/
|
|
119
|
+
rateLimit?: {
|
|
120
|
+
storage?: 'memory' | 'database' | 'secondary-storage';
|
|
81
121
|
};
|
|
82
122
|
}
|
|
83
123
|
//#endregion
|
|
84
124
|
export { AuthConfig as n, AdminUser as t };
|
|
85
|
-
//# sourceMappingURL=types-
|
|
125
|
+
//# sourceMappingURL=types-Bkei6fxa.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-Bkei6fxa.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAUA;;;;;;UAAiB,SAAA;EACf,EAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;EACX,KAAA;EACA,aAAA;EACA,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA,GAAa,IAAA;AAAA;;;;UAME,UAAA;EANE;EAQjB,SAAA;EAFyB;EAKzB,MAAA;IACE,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;IAC7B,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;EAAA;EADlB;EAKb,OAAA;IAJE,2DAMA,SAAA,WAN6B;IAQ7B,SAAA;EAAA;EAAA;EAIF,aAAA;EASA;;;;;;;EAAA,MAAA,GAAS,MAAA;EAmDT;;;;EA7CA,iBAAA,GAAoB,gBAAA;EAoEpB;;;;;EA7DA,KAAA;;;;;;;;;;;;;;;;;EAkBA,MAAA;mGAEE,OAAA;;;;;;;IAOA,wBAAA;EAAA;;;;;;;;;EAWF,OAAA;;;;;;;;;;EAWA,cAAA,gBAGM,OAAA,GAAU,OAAA,qCACyB,OAAA;;;;;;;EAQzC,SAAA;IACE,OAAA;EAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
"dist"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"better-auth": "^1.6.
|
|
32
|
+
"better-auth": "^1.6.9",
|
|
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.12.0",
|
|
36
|
+
"@murumets-ee/db": "0.12.0",
|
|
37
|
+
"@murumets-ee/entity": "0.12.0",
|
|
38
|
+
"@murumets-ee/logging": "0.12.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@types/node": "^
|
|
42
|
-
"tsdown": "^0.21.
|
|
41
|
+
"@types/node": "^20.19.39",
|
|
42
|
+
"tsdown": "^0.21.10",
|
|
43
43
|
"typescript": "^5.7.3",
|
|
44
44
|
"vitest": "^2.1.8"
|
|
45
45
|
},
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{createAccessControl as e}from"better-auth/plugins/access";const t=[`view`,`create`,`update`,`delete`],n=[`view`,`create`,`update`,`delete`,`publish`],r=[`admin`,`public`,`authenticated`,`agent`],i={ticket:[`view`,`create`,`update`],ticket_message:[`view`,`update`],ticket_attachment:[`view`,`create`],department:[`view`],ticket_tag:[`view`]},a={GET:`view`,POST:`create`,PATCH:`update`,DELETE:`delete`};function o(){return{public:{},authenticated:{},agent:{...i}}}function s(e){let t=new Map;for(let[n,r]of Object.entries(e)){let e=new Map;for(let[t,n]of Object.entries(r))e.set(t,new Set(n));t.set(n,e)}return(e,n,r)=>e===`admin`?!0:t.get(e)?.get(n)?.has(r)??!1}function c(e){return e.behaviors?.some(e=>e.name===`publishable`)??!1}function l(e,t){let n={};for(let t of e)n[t.name]=c(t)?[`view`,`create`,`update`,`delete`,`publish`]:[`view`,`create`,`update`,`delete`];if(t)for(let e of t)e.resource&&e.actions&&!(e.resource in n)&&(n[e.resource]=[...e.actions]);return n}const u={user:[`create`,`list`,`set-role`,`ban`,`impersonate`,`delete`,`set-password`,`get`,`update`],session:[`list`,`revoke`,`delete`]};function d(e){let r={...u};for(let i of e)r[i.name]=c(i)?n:t;return r}function f(e,t){let n={},r={},a={};n.user=[...u.user],n.session=[...u.session],r.user=[],r.session=[],a.user=[],a.session=[];for(let e of t)n[e.name]=c(e)?[`view`,`create`,`update`,`delete`,`publish`]:[`view`,`create`,`update`,`delete`],r[e.name]=[`view`],a[e.name]=i[e.name]??[`view`];return{admin:e.newRole(n),authenticated:e.newRole(r),agent:e.newRole(a)}}export{f as a,l as c,a as i,d as l,n,o,r,s,t,e as u};
|
|
2
|
-
//# sourceMappingURL=permissions-DLMd-3dc.mjs.map
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{l as e}from"./schema-Je6e5yt2.mjs";import{a as t,l as n}from"./permissions-DLMd-3dc.mjs";import{createAccessControl as r}from"better-auth/plugins/access";import{betterAuth as i}from"better-auth";import{drizzleAdapter as a}from"better-auth/adapters/drizzle";import{APIError as o,createAuthMiddleware as s}from"better-auth/api";import{nextCookies as c}from"better-auth/next-js";import{admin as l}from"better-auth/plugins";import{organization as u}from"better-auth/plugins/organization";function d(e){let t=e?.context?.session,n=t?.user;return{id:n?.id??t?.userId,name:n?.name}}const f=new Set([`updatedAt`,`createdAt`]);function p(e){function t(t){e?.log(t).catch(()=>{})}let n=null;return{user:{create:{after:async e=>{t({action:`auth.signup`,entityType:`user`,entityId:e.id,userId:e.id,userName:e.name,changes:{fields:{name:e.name,email:e.email}}})}},update:{before:async e=>{let t={};for(let[n,r]of Object.entries(e))f.has(n)||n===`id`||(t[n]=r);n=Object.keys(t).length>0?t:null},after:async(e,r)=>{let i=d(r),a=n;n=null,t({action:`auth.user.update`,entityType:`user`,entityId:e.id,userId:i.id,userName:i.name,changes:a?{fields:a}:void 0,metadata:{targetUser:e.name}})}},delete:{after:async(e,n)=>{let r=d(n);t({action:`auth.user.delete`,entityType:`user`,entityId:e.id,userId:r.id,userName:r.name,metadata:{targetUser:e.name}})}}}}}const m=[`/sign-in/email`,`/sign-in/social`],h={"/change-password":`auth.password.change`,"/forget-password":`auth.password.reset_request`,"/reset-password":`auth.password.reset`},g={"/admin/set-role":`auth.admin.set_role`,"/admin/ban-user":`auth.admin.ban`,"/admin/unban-user":`auth.admin.unban`,"/admin/create-user":`auth.admin.create_user`,"/admin/remove-user":`auth.admin.remove_user`,"/admin/impersonate-user":`auth.admin.impersonate`,"/admin/stop-impersonating":`auth.admin.stop_impersonating`,"/admin/revoke-session":`auth.admin.revoke_session`,"/admin/revoke-sessions":`auth.admin.revoke_sessions`};function _(e){function t(t){e.log(t).catch(()=>{})}return{after:s(async e=>{if(m.some(t=>e.path.startsWith(t))){let n=e.context.returned?.status;if(!n)return;if(n>=400){let r=e.body?.email;t({action:`auth.login.failed`,metadata:{...typeof r==`string`?{email:r}:{},status:n,path:e.path}})}else{let n=e.context.newSession;n?.user?.id&&t({action:`auth.login`,entityType:`user`,entityId:n.user.id,userId:n.user.id,userName:n.user.name})}return}if(e.path===`/sign-out`){let n=e.context.returned;if(!n?.status||n.status<400){let n=d(e);n.id&&t({action:`auth.logout`,entityType:`user`,entityId:n.id,userId:n.id,userName:n.name})}return}let n=h[e.path];if(n){let r=e.context.returned;if(!r?.status||r.status<400){let r=d(e),i=e.body;t({action:n,entityType:`user`,userId:r.id,userName:r.name,metadata:{...typeof i?.email==`string`?{email:i.email}:{},path:e.path}})}return}let r=g[e.path];if(!r)return;let i=e.context.returned;if(i?.status&&i.status>=400)return;let a=d(e),o=e.body;t({action:r,entityType:`user`,entityId:o?.userId??void 0,userId:a.id,userName:a.name,metadata:{targetUserId:o?.userId,...o?.role?{role:o.role}:{},path:e.path}})})}}function v(e){return s(async t=>{if(t.path===`/sign-up/email`&&!e)throw new o(`FORBIDDEN`,{message:`Sign-up is closed`})})}function y(o,s,d){if(globalThis.window!==void 0)throw Error(`@murumets-ee/auth: createAuthServer must not run in a browser environment. Construct the auth server from server code only (RSC, route handlers, CLI, worker).`);let f=[...s.entities.values()],m=r(n(f)),h=t(m,f),g={};o.social?.google&&(g.google=o.social.google),o.social?.github&&(g.github=o.social.github);let y=o.schema??e;return i({database:a(s.db.readWrite,{provider:`pg`,schema:y}),emailAndPassword:{enabled:o.providers?.includes(`email`)??!0},socialProviders:g,session:{expiresIn:o.session?.expiresIn??3600*2,updateAge:o.session?.updateAge??3600},rateLimit:{enabled:!0,window:60,max:100,storage:`memory`,customRules:{"/sign-in/email":{window:60,max:5},"/sign-in/social":{window:60,max:10},"/sign-up/email":{window:60,max:3},"/forget-password":{window:60,max:3},"/reset-password":{window:60,max:5},"/admin/*":{window:60,max:20}}},databaseHooks:p(d),hooks:{before:v(o.signup?.enabled??!1),...d?_(d):{}},plugins:[l({ac:m,roles:h,defaultRole:`authenticated`}),...o.organizations?[u({ac:m,roles:h})]:[],...o.betterAuthPlugins??[],c()]})}let b=null,x=null;function S(e){b=e}function C(e){x=e}function w(){if(!b)throw Error(`@murumets-ee/auth not initialized. Add auth() to your plugins array.`);return b}function T(){return x?.signup?.enabled===!0}export{y as a,T as i,C as n,w as r,S as t};
|
|
2
|
-
//# sourceMappingURL=runtime-Bma4eOJY.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-Bma4eOJY.mjs","names":["defaultAuthSchema"],"sources":["../src/server.ts","../src/runtime.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 `lumi.config.ts`.\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 // Runtime belt-and-suspenders: `createAuthServer` is exposed via the\n // `/runtime` subpath (which has no `server-only` marker so the CLI / worker\n // can load `lumi.config.ts` under jiti / tsx). The marker-less export trades\n // a clean build-time error for a cryptic bundler error if a client\n // component ever tries to import it. This guard adds a runtime failure\n // with a clear message in case that reaches a browser somehow.\n if (typeof (globalThis as { window?: unknown }).window !== 'undefined') {\n throw new Error(\n '@murumets-ee/auth: createAuthServer must not run in a browser environment. ' +\n 'Construct the auth server from server code only (RSC, route handlers, CLI, worker).',\n )\n }\n\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","/**\n * Auth runtime — CLI-safe subpath for the plugin factory and for code that\n * needs to read auth singleton state without pulling in the `server-only`\n * marker from the main `@murumets-ee/auth` entry. Imported by:\n *\n * - `@murumets-ee/auth-ui/plugin` (the factory — needs `createAuthServer`\n * + the state setters + the schema tables)\n * - `@murumets-ee/cli` `create-admin` command (needs `getAuth` under jiti\n * without triggering `server-only`)\n *\n * Everything re-exported here is `server-only`-clean by construction:\n * - `createAuthServer`, `Auth`, `AuthAdminApi` — from `./server.js`\n * - `AuthConfig`, `AdminUser` — from `./types.js`\n * - better-auth Drizzle tables — from `./schema.js`\n */\n\nexport { createAuthServer } from './server.js'\nexport type { Auth, AuthAdminApi } from './server.js'\nexport type { AdminUser, AuthConfig } from './types.js'\nexport {\n account,\n invitation,\n member,\n organization,\n session,\n user,\n verification,\n} from './schema.js'\n\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 */\nexport type 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 * Internal: store the resolved better-auth server instance. Called from the\n * `auth()` plugin factory's `server.init`; not part of the public API.\n *\n * @internal\n */\nexport function _setAuth(auth: AuthWithAdmin): void {\n _auth = auth\n}\n\n/**\n * Internal: capture the plugin config. Invoked by the `auth()` factory at\n * construction time (not init) so `isSignupEnabled()` works during RSC\n * render on cold boot, before async init completes.\n *\n * @internal\n */\nexport function _setAuthConfig(config: AuthConfig): void {\n _authConfig = config\n}\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\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 plugins array.')\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 lumi.config.ts to open.\n */\nexport function isSignupEnabled(): boolean {\n return _authConfig?.signup?.enabled === true\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-DRrHqQiA.d.mts","names":[],"sources":["../src/server.ts","../src/runtime.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA6YA;;;;;iBAvGgB,gBAAA,CACd,MAAA,EAAQ,UAAA,EACR,GAAA,EAAK,UAAA,EACL,WAAA,GAAc,WAAA,GACb,IAAA;;;;KAmGS,MAAA,GAAO,IAAA;;;;;;;;;;UAWF,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;;;;KC/X1B,aAAA,GAAgB,MAAA;EAAS,GAAA,EAAK,YAAA;AAAA;AD4W1C;;;;;AAWA;AAXA,iBC5VgB,QAAA,CAAS,IAAA,EAAM,aAAA;;;;;;;;iBAWf,cAAA,CAAe,MAAA,EAAQ,UAAA;;;;;;;;;;;;;;iBAiBvB,OAAA,CAAA,GAAW,aAAA;;AA5C3B;;;;;iBAyDgB,eAAA,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types-Dl_sE_9S.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAUA;;;;;;UAAiB,SAAA;EACf,EAAA;EACA,SAAA,EAAW,IAAA;EACX,SAAA,EAAW,IAAA;EACX,KAAA;EACA,aAAA;EACA,IAAA;EACA,KAAA;EACA,IAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA,GAAa,IAAA;AAAA;;;;UAME,UAAA;EANE;EAQjB,SAAA;EAFyB;EAKzB,MAAA;IACE,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;IAC7B,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;EAAA;EAI/B;EAAA,OAAA;IAIE,2DAFA,SAAA,WAeF;IAbE,SAAA;EAAA;EAmBkB;EAfpB,aAAA;EAmCA;;;;;;;EA1BA,MAAA,GAAS,MAAA;;;;;EAMT,iBAAA,GAAoB,gBAAA;;;;;;EAOpB,KAAA;;;;;;;;;;;;EAaA,MAAA;wEAEE,OAAA;EAAA;AAAA"}
|