@murumets-ee/auth 0.1.4 → 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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-Dvyj8Crv.d.mts","names":[],"sources":["../src/types.ts","../src/server.ts","../src/plugin.ts"],"mappings":";;;;;;;;;;;UAKiB,UAAA;;EAEf,SAAA;;EAGA,MAAA;IACE,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;IAC7B,MAAA;MAAW,QAAA;MAAkB,YAAA;IAAA;EAAA;EAAlB;EAIb,OAAA;IAAA,2DAEE,SAAA,WAEA;IAAA,SAAA;EAAA;EAWO;EAPT,aAAA;EAaoB;;;;;EANpB,MAAA,GAAS,MAAA;;ACmQX;;;ED7PE,iBAAA,GAAoB,gBAAA;EC6PoC;;;;;EDtPxD,KAAA;AAAA;;;;;;;;;;;;;;;iBCsPc,gBAAA,CAAiB,MAAA,EAAQ,UAAA,EAAY,GAAA,EAAK,UAAA,EAAY,WAAA,GAAc,WAAA,kBAAW,IAAA;sBAAA,cAAA,CAAA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;sBA7OnE,MAAA,sBAAuB,OAAA;MAAA;;2BA0BlB,MAAA,sBAAuB,OAAA;sBAS5B,MAAA,mBAAuB,GAAA,cAAc,OAAA;MAAA;;sBAkBrC,MAAA,mBAAuB,GAAA,cAAc,OAAA;MAAA;IAAA;EAAA;;kEAAA,cAAA,CAAA,iBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2PrD,IAAA,GAAO,UAAA,QAAkB,gBAAA;;;;;;;;;;;;;;;;iBCzUrB,OAAA,CAAA,GAAW,IAAA;;;;;;;;;;;;ADsQ3B;;;;;;iBC9OgB,IAAA,CAAK,MAAA,GAAQ,UAAA,GAAkB,MAAA"}
@@ -0,0 +1,2 @@
1
+ import { n as getAuth, t as auth } from "./plugin-Dvyj8Crv.mjs";
2
+ export { auth, getAuth };
@@ -0,0 +1,2 @@
1
+ let e=null;function t(){if(!e)throw Error(`@murumets-ee/auth not initialized. Add auth() to your toolkit config plugins.`);return e}function n(t={}){return{name:`@murumets-ee/auth`,init:async n=>{let{createAuthServer:r}=await import(`./server-DgG2m6uD.mjs`),i;if(t.audit!==!1){let{createAuditLogger:e,createAuditDbWriter:t,createLogger:r}=await import(`@murumets-ee/logging`);i=e({logger:r({name:`auth-audit`}),dbWriter:t(n.db.readWrite)})}e=r(t,n,i),n.logger.info(`Auth plugin initialized`)}}}export{n as auth,t as getAuth};
2
+ //# sourceMappingURL=plugin.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * Toolkit plugin implementation for @murumets-ee/auth.\n *\n * Implements the Plugin interface from @murumets-ee/core.\n * Creates and stores the better-auth server instance during init.\n */\n\nimport type { Plugin } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport type { Auth } from './server.js'\nimport type { AuthConfig } from './types.js'\n\n/** The initialized auth server instance (set during plugin init) */\nlet _auth: Auth | null = null\n\n/**\n * Get the auth server instance.\n * Throws if the auth plugin hasn't been initialized yet.\n *\n * @example\n * ```typescript\n * // app/api/auth/[...all]/route.ts (user writes this)\n * import { toNextJsHandler } from 'better-auth/next-js'\n * import { getAuth } from '@murumets-ee/auth'\n *\n * export const { GET, POST } = toNextJsHandler(getAuth())\n * ```\n */\nexport function getAuth(): Auth {\n if (!_auth) {\n throw new Error('@murumets-ee/auth not initialized. Add auth() to your toolkit config plugins.')\n }\n return _auth\n}\n\n/**\n * Create the auth toolkit plugin.\n *\n * @example\n * ```typescript\n * import { defineConfig } from '@murumets-ee/core'\n * import { auth } from '@murumets-ee/auth'\n *\n * export default defineConfig({\n * db: { url: process.env.DATABASE_URL! },\n * entities: [Article, Category],\n * plugins: [\n * auth({ providers: ['email'] }),\n * ],\n * })\n * ```\n */\nexport function auth(config: AuthConfig = {}): Plugin {\n return {\n name: '@murumets-ee/auth',\n // better-auth manages its own tables — no toolkit entities or migrations.\n // Use `npx @better-auth/cli generate` + `drizzle-kit` for schema.\n init: async (app) => {\n const { createAuthServer } = await import('./server.js')\n\n // Create audit logger for auth events (login, signup, ban, etc.)\n // unless explicitly disabled via `audit: false`\n let auditLogger: AuditLogger | undefined\n if (config.audit !== false) {\n const { createAuditLogger, createAuditDbWriter, createLogger } = await import(\n '@murumets-ee/logging'\n )\n auditLogger = createAuditLogger({\n logger: createLogger({ name: 'auth-audit' }),\n dbWriter: createAuditDbWriter(app.db.readWrite),\n })\n }\n\n _auth = createAuthServer(config, app, auditLogger)\n app.logger.info('Auth plugin initialized')\n },\n }\n}\n"],"mappings":"AAaA,IAAI,EAAqB,KAezB,SAAgB,GAAgB,CAC9B,GAAI,CAAC,EACH,MAAU,MAAM,gFAAgF,CAElG,OAAO,EAoBT,SAAgB,EAAK,EAAqB,EAAE,CAAU,CACpD,MAAO,CACL,KAAM,oBAGN,KAAM,KAAO,IAAQ,CACnB,GAAM,CAAE,oBAAqB,MAAM,OAAO,yBAItC,EACJ,GAAI,EAAO,QAAU,GAAO,CAC1B,GAAM,CAAE,oBAAmB,sBAAqB,gBAAiB,MAAM,OACrE,wBAEF,EAAc,EAAkB,CAC9B,OAAQ,EAAa,CAAE,KAAM,aAAc,CAAC,CAC5C,SAAU,EAAoB,EAAI,GAAG,UAAU,CAChD,CAAC,CAGJ,EAAQ,EAAiB,EAAQ,EAAK,EAAY,CAClD,EAAI,OAAO,KAAK,0BAA0B,EAE7C"}
@@ -0,0 +1,2 @@
1
+ import{a as e,l as t}from"./permissions-DH0BNEtU.mjs";import{createAccessControl as n}from"better-auth/plugins/access";import{betterAuth as r}from"better-auth";import{drizzleAdapter as i}from"better-auth/adapters/drizzle";import{APIError as a,createAuthMiddleware as o}from"better-auth/api";import{nextCookies as s}from"better-auth/next-js";import{admin as c}from"better-auth/plugins";import{organization as l}from"better-auth/plugins/organization";import{sql as u}from"drizzle-orm";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,t){function n(e){t?.log(e).catch(()=>{})}let r=null;return{user:{create:{after:async t=>{try{await e.readWrite.execute(u`UPDATE "user" SET role = 'admin' WHERE id = ${t.id} AND (SELECT COUNT(*) FROM "user") = 1`)}catch{}n({action:`auth.signup`,entityType:`user`,entityId:t.id,userId:t.id,userName:t.name,changes:{fields:{name:t.name,email:t.email}}})}},update:{before:async e=>{let t={};for(let[n,r]of Object.entries(e))f.has(n)||n===`id`||(t[n]=r);r=Object.keys(t).length>0?t:null},after:async(e,t)=>{let i=d(t),a=r;r=null,n({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,t)=>{let r=d(t);n({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:o(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 o(async t=>{if(t.path!==`/sign-up/email`)return;let n=await e.readWrite.execute(u`SELECT COUNT(*)::text as count FROM "user"`);if(Number(n[0]?.count)>0)throw new a(`FORBIDDEN`,{message:`Sign-up is closed`})})}function y(a,o,u){let d=[...o.entities.values()],f=n(t(d)),m=e(f,d),h={};return a.social?.google&&(h.google=a.social.google),a.social?.github&&(h.github=a.social.github),r({database:i(o.db.readWrite,{provider:`pg`,...a.schema?{schema:a.schema}:{}}),emailAndPassword:{enabled:a.providers?.includes(`email`)??!0},socialProviders:h,session:{expiresIn:a.session?.expiresIn??3600*2,updateAge:a.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(o.db,u),hooks:{before:v(o.db),...u?_(u):{}},plugins:[c({ac:f,roles:m,defaultRole:`authenticated`}),...a.organizations?[l({ac:f,roles:m})]:[],...a.betterAuthPlugins??[],s()]})}export{y as createAuthServer};
2
+ //# sourceMappingURL=server-DgG2m6uD.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-DgG2m6uD.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["/**\n * Server-side auth instance factory.\n *\n * Creates a configured `betterAuth()` instance using the toolkit's database\n * connection and entity definitions. This is server-only code.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport type { AuditLogger } from '@murumets-ee/logging'\nimport { 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 { sql as drizzleSql } from 'drizzle-orm'\nimport { buildDefaultRoles, buildStatements } from './permissions.js'\nimport type { 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(db: ToolkitApp['db'], 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 // Auto-promote first user to admin (fresh DB setup).\n // Single atomic statement — if two signups race, only one sees count=1.\n try {\n await db.readWrite.execute(\n drizzleSql`UPDATE \"user\" SET role = 'admin' WHERE id = ${user.id as string} AND (SELECT COUNT(*) FROM \"user\") = 1`,\n )\n } catch {\n // Non-fatal — don't block signup if promotion fails\n }\n\n // Signup — actor is the new user themselves\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 — closes registration after the first user exists\n// ---------------------------------------------------------------------------\n\nfunction buildSignupGate(db: ToolkitApp['db']) {\n return createAuthMiddleware(async (ctx) => {\n if (ctx.path !== '/sign-up/email') return\n const result = await db.readWrite.execute<{ count: string }>(\n drizzleSql`SELECT COUNT(*)::text as count FROM \"user\"`,\n )\n if (Number(result[0]?.count) > 0) {\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 */\nexport function createAuthServer(config: AuthConfig, app: ToolkitApp, auditLogger?: AuditLogger) {\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 return betterAuth({\n database: drizzleAdapter(app.db.readWrite, {\n provider: 'pg',\n ...(config.schema ? { schema: config.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(app.db, auditLogger),\n hooks: {\n before: buildSignupGate(app.db),\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\n/** Type of the auth server instance */\nexport type Auth = ReturnType<typeof createAuthServer>\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "name": "@murumets-ee/auth",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./dist/index.d.ts",
9
- "import": "./dist/index.js"
8
+ "types": "./dist/index.d.mts",
9
+ "import": "./dist/index.mjs"
10
10
  },
11
11
  "./client": {
12
- "types": "./dist/client.d.ts",
13
- "import": "./dist/client.js"
12
+ "types": "./dist/client.d.mts",
13
+ "import": "./dist/client.mjs"
14
14
  },
15
15
  "./plugin": {
16
- "types": "./dist/plugin.d.ts",
17
- "import": "./dist/plugin.js"
16
+ "types": "./dist/plugin.d.mts",
17
+ "import": "./dist/plugin.mjs"
18
18
  },
19
19
  "./admin": {
20
- "types": "./dist/admin/index.d.ts",
21
- "import": "./dist/admin/index.js"
20
+ "types": "./dist/admin/index.d.mts",
21
+ "import": "./dist/admin/index.mjs"
22
22
  }
23
23
  },
24
24
  "files": [
@@ -28,20 +28,20 @@
28
28
  "better-auth": "^1.4.0",
29
29
  "drizzle-orm": "^0.45.0",
30
30
  "server-only": "^0.0.1",
31
- "@murumets-ee/entity": "0.1.4",
32
- "@murumets-ee/core": "0.1.5",
33
- "@murumets-ee/db": "0.1.4",
34
- "@murumets-ee/logging": "0.1.5"
31
+ "@murumets-ee/core": "0.1.6",
32
+ "@murumets-ee/db": "0.1.5",
33
+ "@murumets-ee/logging": "0.1.6",
34
+ "@murumets-ee/entity": "0.1.5"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.10.5",
38
- "tsup": "^8.3.5",
38
+ "tsdown": "^0.21.7",
39
39
  "typescript": "^5.7.3",
40
40
  "vitest": "^2.1.8"
41
41
  },
42
42
  "scripts": {
43
- "build": "tsup",
44
- "dev": "tsup --watch",
43
+ "build": "tsdown",
44
+ "dev": "tsdown --watch",
45
45
  "test": "vitest"
46
46
  }
47
47
  }
@@ -1 +0,0 @@
1
- import{d as f}from"../chunk-NH6AU5Z6.js";var P=/^[a-z][a-z0-9-]{0,49}$/;function u(d,c=200){return new Response(JSON.stringify(d),{status:c,headers:{"Content-Type":"application/json"}})}function r(d,c){return u({error:d},c)}function T(d){let{getStatements:c,loadRoles:g,saveRoles:R,onSave:b}=d;return{prefix:"permissions",resource:"permissions",actions:["view","create","update","delete"],handlers:{GET:async(m,{segments:s})=>{let t=c(),i=await g()??{};return s.length===1&&s[0]==="roles"?u({roles:Object.keys(i),builtInRoles:[...f]}):u({statements:t,roles:i,builtInRoles:[...f]})},PATCH:async(m,{user:s,audit:t})=>{let i=await m.json(),{roles:e}=i;if(!e||typeof e!="object")return r('Body must contain "roles" object',400);if("admin"in e)return r("Cannot modify admin role permissions (admin always has full access)",400);if(s.role&&s.role!=="admin"&&s.role in e)return r("Cannot modify permissions for your own role",403);let n=await g()??{};for(let o of Object.keys(e))if(!(o in n))return r(`Role '${o}' does not exist. Create it first via POST /permissions/roles`,400);let a=c();for(let[o,l]of Object.entries(e)){if(typeof o!="string"||!o)return r("Role names must be non-empty strings",400);if(typeof l!="object"||l===null||Array.isArray(l))return r(`Permissions for role '${o}' must be an object`,400);for(let[p,w]of Object.entries(l)){if(!Array.isArray(w)||!w.every(y=>typeof y=="string"))return r(`Actions for '${p}' in role '${o}' must be a string array`,400);let h=a[p];if(!h)return r(`Unknown resource: ${p}`,400);for(let y of w)if(!h.includes(y))return r(`Invalid action '${y}' for resource '${p}'. Valid: ${h.join(", ")}`,400)}}let j={...n};for(let[o,l]of Object.entries(e))j[o]=l;return await R(j),b?.(),t?.({action:"permissions.update",entityType:"permissions",userId:s.id,userName:s.name,changes:{roles:e},metadata:{rolesModified:Object.keys(e)}}),u({ok:!0})},POST:async(m,{segments:s,user:t,audit:i})=>{if(s.length!==1||s[0]!=="roles")return r("POST only supported at /permissions/roles",400);let e=await m.json(),{name:n}=e;if(!n||typeof n!="string")return r('Body must contain "name" string',400);if(!P.test(n))return r("Role name must be lowercase alphanumeric with hyphens, start with a letter, max 50 chars",400);if(f.includes(n))return r(`Cannot create role with built-in name: ${n}`,400);let a=await g()??{};return n in a?r(`Role already exists: ${n}`,409):(a[n]={},await R(a),b?.(),i?.({action:"permissions.role.create",entityType:"permissions",userId:t.id,userName:t.name,changes:{roleName:n}}),u({name:n,permissions:{}},201))},DELETE:async(m,{segments:s,user:t,audit:i})=>{if(s.length!==2||s[0]!=="roles")return r("DELETE only supported at /permissions/roles/:name",400);let e=s[1];if(f.includes(e))return r(`Cannot delete built-in role: ${e}`,400);let n=await g()??{};if(!(e in n))return r(`Role not found: ${e}`,404);let a=n[e];return delete n[e],await R(n),b?.(),i?.({action:"permissions.role.delete",entityType:"permissions",userId:t.id,userName:t.name,changes:{roleName:e,permissions:a}}),u({deleted:e})}}}}export{T as permissionRoutes};
@@ -1 +0,0 @@
1
- var e=null;function a(){if(!e)throw new Error("@murumets-ee/auth not initialized. Add auth() to your toolkit config plugins.");return e}function m(r={}){return{name:"@murumets-ee/auth",init:async t=>{let{createAuthServer:u}=await import("./server-NWTKDUCB.js"),i;if(r.audit!==!1){let{createAuditLogger:o,createAuditDbWriter:n,createLogger:g}=await import("@murumets-ee/logging");i=o({logger:g({name:"auth-audit"}),dbWriter:n(t.db.readWrite)})}e=u(r,t,i),t.logger.info("Auth plugin initialized")}}}export{a,m as b};
@@ -1 +0,0 @@
1
- import{createAccessControl as p}from"better-auth/plugins/access";var u=["view","create","update","delete"],d=["view","create","update","delete","publish"];var g=["admin","public","authenticated"],m={GET:"view",POST:"create",PATCH:"update",DELETE:"delete"};function f(){return{public:{},authenticated:{}}}function b(n){let r=new Map;for(let[e,t]of Object.entries(n)){let s=new Map;for(let[c,a]of Object.entries(t))s.set(c,new Set(a));r.set(e,s)}return(e,t,s)=>e==="admin"?!0:r.get(e)?.get(t)?.has(s)??!1}function i(n){return n.behaviors?.some(r=>r.name==="publishable")??!1}function R(n,r){let e={};for(let t of n)e[t.name]=i(t)?["view","create","update","delete","publish"]:["view","create","update","delete"];if(r)for(let t of r)t.resource&&t.actions&&!(t.resource in e)&&(e[t.resource]=[...t.actions]);return e}var o={user:["create","list","set-role","ban","impersonate","delete","set-password","get","update"],session:["list","revoke","delete"]};function h(n){let r={...o};for(let e of n)r[e.name]=i(e)?d:u;return r}function T(n,r){let e={},t={};e.user=[...o.user],e.session=[...o.session],t.user=[],t.session=[];for(let s of r)e[s.name]=i(s)?["view","create","update","delete","publish"]:["view","create","update","delete"],t[s.name]=["view"];return{admin:n.newRole(e),authenticated:n.newRole(t)}}export{p as a,u as b,d as c,g as d,m as e,f,b as g,R as h,h as i,T as j};