@murumets-ee/auth 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ //#region src/admin/routes.d.ts
1
2
  /**
2
3
  * Permission management admin routes for the centralized admin API handler.
3
4
  *
@@ -25,38 +26,38 @@
25
26
  */
26
27
  /** Fire-and-forget audit log function — structurally matches AuditLogFn from @murumets-ee/core */
27
28
  type AuditLogFn = (entry: {
28
- action: string;
29
- entityType?: string;
30
- entityId?: string;
31
- userId?: string;
32
- userName?: string;
33
- changes?: Record<string, unknown>;
34
- metadata?: Record<string, unknown>;
29
+ action: string;
30
+ entityType?: string;
31
+ entityId?: string;
32
+ userId?: string;
33
+ userName?: string;
34
+ changes?: Record<string, unknown>;
35
+ metadata?: Record<string, unknown>;
35
36
  }) => void;
36
37
  interface AdminRoute {
37
- prefix: string;
38
- resource?: string;
39
- actions?: readonly string[];
40
- handlers: Partial<Record<string, (req: Request, ctx: {
41
- segments: string[];
42
- user: {
43
- id: string;
44
- role?: string;
45
- name?: string;
46
- };
47
- audit?: AuditLogFn;
48
- checkPermission: (resource: string, action: string) => boolean;
49
- }) => Promise<Response>>>;
38
+ prefix: string;
39
+ resource?: string;
40
+ actions?: readonly string[];
41
+ handlers: Partial<Record<string, (req: Request, ctx: {
42
+ segments: string[];
43
+ user: {
44
+ id: string;
45
+ role?: string;
46
+ name?: string;
47
+ };
48
+ audit?: AuditLogFn;
49
+ checkPermission: (resource: string, action: string) => boolean;
50
+ }) => Promise<Response>>>;
50
51
  }
51
52
  interface PermissionRoutesConfig {
52
- /** Returns the resource catalog: resource → available actions */
53
- getStatements: () => Record<string, readonly string[]>;
54
- /** Load saved role definitions from settings (null if first run) */
55
- loadRoles: () => Promise<Record<string, Record<string, string[]>> | null>;
56
- /** Save role definitions to settings */
57
- saveRoles: (roles: Record<string, Record<string, string[]>>) => Promise<void>;
58
- /** Called after save to invalidate cached permission checker */
59
- onSave?: () => void;
53
+ /** Returns the resource catalog: resource → available actions */
54
+ getStatements: () => Record<string, readonly string[]>;
55
+ /** Load saved role definitions from settings (null if first run) */
56
+ loadRoles: () => Promise<Record<string, Record<string, string[]>> | null>;
57
+ /** Save role definitions to settings */
58
+ saveRoles: (roles: Record<string, Record<string, string[]>>) => Promise<void>;
59
+ /** Called after save to invalidate cached permission checker */
60
+ onSave?: () => void;
60
61
  }
61
62
  /**
62
63
  * Create admin API routes for permission management.
@@ -68,5 +69,6 @@ interface PermissionRoutesConfig {
68
69
  * - `DELETE /permissions/roles/:name` — Delete a custom role
69
70
  */
70
71
  declare function permissionRoutes(config: PermissionRoutesConfig): AdminRoute;
71
-
72
+ //#endregion
72
73
  export { permissionRoutes };
74
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/admin/routes.ts"],"mappings":";;;;;;;;;;;;;;;;;;AA0CC;;;;;;;;;KARI,UAAA,IAAc,KAAA;EACjB,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,GAAU,MAAA;EACV,QAAA,GAAW,MAAA;AAAA;AAAA,UAGH,UAAA;EACR,MAAA;EACA,QAAA;EACA,OAAA;EACA,QAAA,EAAU,OAAA,CACR,MAAA,UAGI,GAAA,EAAK,OAAA,EACL,GAAA;IACE,QAAA;IACA,IAAA;MAAQ,EAAA;MAAY,IAAA;MAAe,IAAA;IAAA;IACnC,KAAA,GAAQ,UAAA;IACR,eAAA,GAAkB,QAAA,UAAkB,MAAA;EAAA,MAEnC,OAAA,CAAQ,QAAA;AAAA;AAAA,UA8BF,sBAAA;EAAA;EAEf,aAAA,QAAqB,MAAA;;EAErB,SAAA,QAAiB,OAAA,CAAQ,MAAA,SAAe,MAAA;EAAA;EAExC,SAAA,GAAY,KAAA,EAAO,MAAA,SAAe,MAAA,wBAA8B,OAAA;EAF/C;EAIjB,MAAA;AAAA;;;;;;;;;;iBAYc,gBAAA,CAAiB,MAAA,EAAQ,sBAAA,GAAyB,UAAA"}
@@ -0,0 +1,2 @@
1
+ import{r as e}from"../permissions-DH0BNEtU.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,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,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(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,userName:a.name,changes:{roleName:u,permissions:f}}),n({deleted:u})}}}}export{i as permissionRoutes};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}