@murumets-ee/logging 0.27.0 → 0.28.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.
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/audit-client.ts","../src/admin/routes.ts"],"mappings":";;;UAQiB,oBAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA,GAAW,IAAA;EACX,MAAA,GAAS,IAAA;EAJT;EAMA,MAAA;EAJA;EAMA,KAAA;EACA,MAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EACA,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,EAAS,MAAA;EACT,QAAA,EAAU,MAAA;EACV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,kBAAA;EACf,KAAA,EAAO,aAAA;EACP,KAAA;AAAA;AAAA,cAqBW,cAAA;EAAA,QACH,MAAA;cAEI,EAAA,EAAI,kBAAA;EAjChB;EAsCM,KAAA,CAAM,KAAA,EAAO,IAAA,CAAK,aAAA,wBAAqC,OAAA;EApC7D;EAiDM,KAAA,CAAM,OAAA,GAAS,oBAAA,GAA4B,OAAA,CAAQ,kBAAA;EAhDzD;EAmEM,QAAA,CAAS,EAAA,WAAa,OAAA,CAAQ,aAAA;EAlEpC;EAuEM,sBAAA,CAAA,GAA0B,OAAA;EAvEjB;EA8ET,kBAAA,CAAA,GAAsB,OAAA;EAAA,QAQpB,UAAA;EAAA,QAmCA,YAAA;AAAA;;;UC1HA,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;IAAA;IACpB,eAAA,GAAkB,QAAA,UAAkB,MAAA;EAAA,MAEnC,OAAA,CAAQ,QAAA;AAAA;;;;KAmDd,uBAAA,GAA0B,IAAA,CAC7B,cAAA;AAAA,iBAIc,SAAA,CAAU,SAAA,QAAiB,uBAAA,GAA0B,UAAA"}
1
+ {"version":3,"file":"admin.d.mts","names":[],"sources":["../src/audit-client.ts","../src/admin/routes.ts"],"mappings":";;;UAQiB,oBAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA,GAAW,IAAA;EACX,MAAA,GAAS,IAAI;EAJb;EAMA,MAAA;EAJA;EAMA,KAAA;EACA,MAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EACA,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,EAAS,MAAA;EACT,QAAA,EAAU,MAAA;EACV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,kBAAA;EACf,KAAA,EAAO,aAAa;EACpB,KAAA;AAAA;AAAA,cAqBW,cAAA;EAAA,QACH,MAAA;cAEI,EAAA,EAAI,kBAAA;EAjChB;EAsCM,KAAA,CAAM,KAAA,EAAO,IAAA,CAAK,aAAA,wBAAqC,OAAA;EApC7D;EAiDM,KAAA,CAAM,OAAA,GAAS,oBAAA,GAA4B,OAAA,CAAQ,kBAAA;EAhDzD;EAmEM,QAAA,CAAS,EAAA,WAAa,OAAA,CAAQ,aAAA;EAlEpC;EAuEM,sBAAA,CAAA,GAA0B,OAAA;EAvEjB;EA8ET,kBAAA,CAAA,GAAsB,OAAA;EAAA,QAQpB,UAAA;EAAA,QAmCA,YAAA;AAAA;;;UC1HA,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;IAAA;IACpB,eAAA,GAAkB,QAAA,UAAkB,MAAA;EAAA,MAEnC,OAAA,CAAQ,QAAA;AAAA;;;;KAmDd,uBAAA,GAA0B,IAAI,CACjC,cAAA;AAAA,iBAIc,SAAA,CAAU,SAAA,QAAiB,uBAAA,GAA0B,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"admin.mjs","names":[],"sources":["../src/admin/routes.ts"],"sourcesContent":["/**\n * Audit log admin routes for the centralized admin API handler.\n *\n * Read-only: only GET handlers. Audit logs are immutable.\n * Admin-only: explicit role check before any data access.\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { logRoutes } from '@murumets-ee/logging/admin'\n * import { AuditLogClient } from '@murumets-ee/logging'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [...],\n * routes: [logRoutes(() => new AuditLogClient(db))],\n * })\n * ```\n */\n\nimport { z } from 'zod'\n// Type-only import — keeps the runtime AuditLogClient (and its postgres-js\n// type closure) out of the /admin bundle while still tying the handler's\n// expected client surface to the real class. Drift surfaces as a TS error\n// the next time AuditLogClient gains a method.\nimport type { AuditLogClient } from '../audit-client.js'\n\n// ---------------------------------------------------------------------------\n// Local route type — avoids circular build dep: logging → core → logging.\n// Structurally compatible with AdminRoute from @murumets-ee/core.\n// ---------------------------------------------------------------------------\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 }\n checkPermission: (resource: string, action: string) => boolean\n },\n ) => Promise<Response>\n >\n >\n}\n\n// ---------------------------------------------------------------------------\n// Query param validation\n// ---------------------------------------------------------------------------\n\nconst ID_REGEX = /^[a-zA-Z0-9_-]{1,255}$/\n// Strict RFC 4122 UUID — `audit_logs.id` is `column.uuid({...})`, so a\n// non-UUID path segment otherwise reaches postgres and triggers a 500\n// from `invalid input syntax for type uuid`. Validate up front and 404.\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nconst auditLogQuerySchema = z.object({\n action: z.string().max(100).optional(),\n entityType: z.string().max(100).optional(),\n entityId: z.string().regex(ID_REGEX, 'Invalid ID format').optional(),\n userId: z.string().regex(ID_REGEX, 'Invalid user ID format').optional(),\n dateFrom: z.string().datetime({ offset: true, message: 'Invalid ISO date' }).optional(),\n dateTo: z.string().datetime({ offset: true, message: 'Invalid ISO date' }).optional(),\n search: z.string().max(200).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n offset: z.coerce.number().min(0).default(0),\n sortField: z.enum(['createdAt', 'action', 'entityType']).default('createdAt'),\n sortDirection: z.enum(['asc', 'desc']).default('desc'),\n})\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\n/** Subset of `AuditLogClient` actually called by the route handlers. Using\n * `Pick` (not a hand-written `*Like` interface) means new methods on the\n * real client don't silently degrade these to `unknown`. */\ntype AuditLogClientForRoutes = Pick<\n AuditLogClient,\n 'query' | 'findById' | 'getDistinctEntityTypes' | 'getDistinctActions'\n>\n\nexport function logRoutes(getClient: () => AuditLogClientForRoutes): AdminRoute {\n return {\n prefix: 'logs',\n resource: 'audit-logs',\n actions: ['view'],\n handlers: {\n GET: async (req, { segments, checkPermission }) => {\n // Defense in depth — the framework already gates on\n // `resource: 'audit-logs'` before invoking the handler; this re-check\n // means an accidental drop of `resource` (or a future framework\n // refactor) cannot silently make audit logs world-readable.\n if (!checkPermission('audit-logs', 'view')) {\n return errorJson('Forbidden', 403)\n }\n\n const client = getClient()\n\n const firstSegment = segments[0]\n\n // GET /logs/filters — distinct values for filter dropdowns\n if (segments.length === 1 && firstSegment === 'filters') {\n const [entityTypes, actions] = await Promise.all([\n client.getDistinctEntityTypes(),\n client.getDistinctActions(),\n ])\n return json({ entityTypes, actions })\n }\n\n // GET /logs/:id — single entry detail\n if (segments.length === 1 && firstSegment !== undefined) {\n if (!UUID_REGEX.test(firstSegment)) {\n return errorJson('Audit log entry not found', 404)\n }\n const entry = await client.findById(firstSegment)\n if (!entry) return errorJson('Audit log entry not found', 404)\n return json(entry)\n }\n\n // GET /logs — list with filters + pagination. Reject anything\n // deeper than a single id/keyword segment so unknown paths don't\n // silently fall through and return the full list.\n if (segments.length > 1) {\n return errorJson('Not found', 404)\n }\n\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const parsed = auditLogQuerySchema.safeParse(params)\n if (!parsed.success) {\n return errorJson(\n `Invalid query params: ${parsed.error.issues.map((i) => i.message).join(', ')}`,\n 400,\n )\n }\n\n const { dateFrom, dateTo, ...rest } = parsed.data\n const result = await client.query({\n ...rest,\n ...(dateFrom && { dateFrom: new Date(dateFrom) }),\n ...(dateTo && { dateTo: new Date(dateTo) }),\n })\n\n return json(result)\n },\n // No POST, PATCH, DELETE — audit logs are append-only\n },\n }\n}\n"],"mappings":"wBAuDA,MAAM,EAAW,yBAIX,EAAa,kEAEb,EAAsB,EAAE,OAAO,CACnC,OAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CACtC,WAAY,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAC1C,SAAU,EAAE,QAAQ,CAAC,MAAM,EAAU,oBAAoB,CAAC,UAAU,CACpE,OAAQ,EAAE,QAAQ,CAAC,MAAM,EAAU,yBAAyB,CAAC,UAAU,CACvE,SAAU,EAAE,QAAQ,CAAC,SAAS,CAAE,OAAQ,GAAM,QAAS,mBAAoB,CAAC,CAAC,UAAU,CACvF,OAAQ,EAAE,QAAQ,CAAC,SAAS,CAAE,OAAQ,GAAM,QAAS,mBAAoB,CAAC,CAAC,UAAU,CACrF,OAAQ,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CACtC,MAAO,EAAE,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,CAAC,QAAQ,GAAG,CACpD,OAAQ,EAAE,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAC3C,UAAW,EAAE,KAAK,CAAC,YAAa,SAAU,aAAa,CAAC,CAAC,QAAQ,YAAY,CAC7E,cAAe,EAAE,KAAK,CAAC,MAAO,OAAO,CAAC,CAAC,QAAQ,OAAO,CACvD,CAAC,CAMF,SAAS,EAAK,EAAe,EAAS,IAAe,CACnD,OAAO,IAAI,SAAS,KAAK,UAAU,EAAK,CAAE,CACxC,SACA,QAAS,CAAE,eAAgB,mBAAoB,CAChD,CAAC,CAGJ,SAAS,EAAU,EAAiB,EAA0B,CAC5D,OAAO,EAAK,CAAE,MAAO,EAAS,CAAE,EAAO,CAezC,SAAgB,EAAU,EAAsD,CAC9E,MAAO,CACL,OAAQ,OACR,SAAU,aACV,QAAS,CAAC,OAAO,CACjB,SAAU,CACR,IAAK,MAAO,EAAK,CAAE,WAAU,qBAAsB,CAKjD,GAAI,CAAC,EAAgB,aAAc,OAAO,CACxC,OAAO,EAAU,YAAa,IAAI,CAGpC,IAAM,EAAS,GAAW,CAEpB,EAAe,EAAS,GAG9B,GAAI,EAAS,SAAW,GAAK,IAAiB,UAAW,CACvD,GAAM,CAAC,EAAa,GAAW,MAAM,QAAQ,IAAI,CAC/C,EAAO,wBAAwB,CAC/B,EAAO,oBAAoB,CAC5B,CAAC,CACF,OAAO,EAAK,CAAE,cAAa,UAAS,CAAC,CAIvC,GAAI,EAAS,SAAW,GAAK,IAAiB,IAAA,GAAW,CACvD,GAAI,CAAC,EAAW,KAAK,EAAa,CAChC,OAAO,EAAU,4BAA6B,IAAI,CAEpD,IAAM,EAAQ,MAAM,EAAO,SAAS,EAAa,CAEjD,OADK,EACE,EAAK,EAAM,CADC,EAAU,4BAA6B,IAAI,CAOhE,GAAI,EAAS,OAAS,EACpB,OAAO,EAAU,YAAa,IAAI,CAGpC,IAAM,EAAM,IAAI,IAAI,EAAI,IAAI,CACtB,EAAS,OAAO,YAAY,EAAI,aAAa,CAC7C,EAAS,EAAoB,UAAU,EAAO,CACpD,GAAI,CAAC,EAAO,QACV,OAAO,EACL,yBAAyB,EAAO,MAAM,OAAO,IAAK,GAAM,EAAE,QAAQ,CAAC,KAAK,KAAK,GAC7E,IACD,CAGH,GAAM,CAAE,WAAU,SAAQ,GAAG,GAAS,EAAO,KAO7C,OAAO,EAAK,MANS,EAAO,MAAM,CAChC,GAAG,EACH,GAAI,GAAY,CAAE,SAAU,IAAI,KAAK,EAAS,CAAE,CAChD,GAAI,GAAU,CAAE,OAAQ,IAAI,KAAK,EAAO,CAAE,CAC3C,CAAC,CAEiB,EAGtB,CACF"}
1
+ {"version":3,"file":"admin.mjs","names":[],"sources":["../src/admin/routes.ts"],"sourcesContent":["/**\n * Audit log admin routes for the centralized admin API handler.\n *\n * Read-only: only GET handlers. Audit logs are immutable.\n * Admin-only: explicit role check before any data access.\n *\n * @example\n * ```typescript\n * import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\n * import { logRoutes } from '@murumets-ee/logging/admin'\n * import { AuditLogClient } from '@murumets-ee/logging'\n *\n * const handler = createAdminApiHandler({\n * authenticate: async (req) => { ... },\n * entities: [...],\n * routes: [logRoutes(() => new AuditLogClient(db))],\n * })\n * ```\n */\n\nimport { z } from 'zod'\n// Type-only import — keeps the runtime AuditLogClient (and its postgres-js\n// type closure) out of the /admin bundle while still tying the handler's\n// expected client surface to the real class. Drift surfaces as a TS error\n// the next time AuditLogClient gains a method.\nimport type { AuditLogClient } from '../audit-client.js'\n\n// ---------------------------------------------------------------------------\n// Local route type — avoids circular build dep: logging → core → logging.\n// Structurally compatible with AdminRoute from @murumets-ee/core.\n// ---------------------------------------------------------------------------\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 }\n checkPermission: (resource: string, action: string) => boolean\n },\n ) => Promise<Response>\n >\n >\n}\n\n// ---------------------------------------------------------------------------\n// Query param validation\n// ---------------------------------------------------------------------------\n\nconst ID_REGEX = /^[a-zA-Z0-9_-]{1,255}$/\n// Strict RFC 4122 UUID — `audit_logs.id` is `column.uuid({...})`, so a\n// non-UUID path segment otherwise reaches postgres and triggers a 500\n// from `invalid input syntax for type uuid`. Validate up front and 404.\nconst UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nconst auditLogQuerySchema = z.object({\n action: z.string().max(100).optional(),\n entityType: z.string().max(100).optional(),\n entityId: z.string().regex(ID_REGEX, 'Invalid ID format').optional(),\n userId: z.string().regex(ID_REGEX, 'Invalid user ID format').optional(),\n dateFrom: z.string().datetime({ offset: true, message: 'Invalid ISO date' }).optional(),\n dateTo: z.string().datetime({ offset: true, message: 'Invalid ISO date' }).optional(),\n search: z.string().max(200).optional(),\n limit: z.coerce.number().min(1).max(100).default(50),\n offset: z.coerce.number().min(0).default(0),\n sortField: z.enum(['createdAt', 'action', 'entityType']).default('createdAt'),\n sortDirection: z.enum(['asc', 'desc']).default('desc'),\n})\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\n/** Subset of `AuditLogClient` actually called by the route handlers. Using\n * `Pick` (not a hand-written `*Like` interface) means new methods on the\n * real client don't silently degrade these to `unknown`. */\ntype AuditLogClientForRoutes = Pick<\n AuditLogClient,\n 'query' | 'findById' | 'getDistinctEntityTypes' | 'getDistinctActions'\n>\n\nexport function logRoutes(getClient: () => AuditLogClientForRoutes): AdminRoute {\n return {\n prefix: 'logs',\n resource: 'audit-logs',\n actions: ['view'],\n handlers: {\n GET: async (req, { segments, checkPermission }) => {\n // Defense in depth — the framework already gates on\n // `resource: 'audit-logs'` before invoking the handler; this re-check\n // means an accidental drop of `resource` (or a future framework\n // refactor) cannot silently make audit logs world-readable.\n if (!checkPermission('audit-logs', 'view')) {\n return errorJson('Forbidden', 403)\n }\n\n const client = getClient()\n\n const firstSegment = segments[0]\n\n // GET /logs/filters — distinct values for filter dropdowns\n if (segments.length === 1 && firstSegment === 'filters') {\n const [entityTypes, actions] = await Promise.all([\n client.getDistinctEntityTypes(),\n client.getDistinctActions(),\n ])\n return json({ entityTypes, actions })\n }\n\n // GET /logs/:id — single entry detail\n if (segments.length === 1 && firstSegment !== undefined) {\n if (!UUID_REGEX.test(firstSegment)) {\n return errorJson('Audit log entry not found', 404)\n }\n const entry = await client.findById(firstSegment)\n if (!entry) return errorJson('Audit log entry not found', 404)\n return json(entry)\n }\n\n // GET /logs — list with filters + pagination. Reject anything\n // deeper than a single id/keyword segment so unknown paths don't\n // silently fall through and return the full list.\n if (segments.length > 1) {\n return errorJson('Not found', 404)\n }\n\n const url = new URL(req.url)\n const params = Object.fromEntries(url.searchParams)\n const parsed = auditLogQuerySchema.safeParse(params)\n if (!parsed.success) {\n return errorJson(\n `Invalid query params: ${parsed.error.issues.map((i) => i.message).join(', ')}`,\n 400,\n )\n }\n\n const { dateFrom, dateTo, ...rest } = parsed.data\n const result = await client.query({\n ...rest,\n ...(dateFrom && { dateFrom: new Date(dateFrom) }),\n ...(dateTo && { dateTo: new Date(dateTo) }),\n })\n\n return json(result)\n },\n // No POST, PATCH, DELETE — audit logs are append-only\n },\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
1
  import{column as e,defineTable as t}from"@murumets-ee/db";const n=t({name:`toolkit_audit_logs`,columns:{id:e.uuid({primaryKey:!0,defaultRandom:!0}),action:e.varchar({length:100,notNull:!0}),entityType:e.varchar({length:100,pgName:`entity_type`}),entityId:e.varchar({length:255,pgName:`entity_id`}),userId:e.varchar({length:255,pgName:`user_id`}),userName:e.varchar({length:255,pgName:`user_name`}),changes:e.jsonb(),metadata:e.jsonb(),createdAt:e.timestamp({notNull:!0,defaultNow:!0,withTimezone:!0,pgName:`created_at`})},indexes:[{on:[`entityType`,`createdAt`],name:`audit_entity_type_created_at_idx`},{on:[`userId`,`createdAt`],name:`audit_user_id_created_at_idx`},{on:[`action`,`createdAt`],name:`audit_action_created_at_idx`},{on:[`createdAt`],name:`audit_created_at_idx`},{on:[`entityId`],name:`audit_entity_id_idx`}]}),r=n.table;export{r as n,n as t};
2
- //# sourceMappingURL=audit-table-CrPP2ZZ9.mjs.map
2
+ //# sourceMappingURL=audit-table-BImbXt_s.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"audit-table-CrPP2ZZ9.mjs","names":[],"sources":["../src/audit-table.ts"],"sourcesContent":["/**\n * `toolkit_audit_logs` table definition via `defineTable`.\n *\n * Replaces the hand-written `pgTable` in `schema.ts`. Uses `pgName` to\n * preserve the existing snake_case Postgres column names so no migration\n * is required.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const auditLogTable = defineTable({\n name: 'toolkit_audit_logs',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n action: column.varchar({ length: 100, notNull: true }),\n entityType: column.varchar({ length: 100, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, pgName: 'entity_id' }),\n userId: column.varchar({ length: 255, pgName: 'user_id' }),\n userName: column.varchar({ length: 255, pgName: 'user_name' }),\n changes: column.jsonb<Record<string, unknown>>(),\n metadata: column.jsonb<Record<string, unknown>>(),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n },\n indexes: [\n { on: ['entityType', 'createdAt'], name: 'audit_entity_type_created_at_idx' },\n { on: ['userId', 'createdAt'], name: 'audit_user_id_created_at_idx' },\n { on: ['action', 'createdAt'], name: 'audit_action_created_at_idx' },\n { on: ['createdAt'], name: 'audit_created_at_idx' },\n { on: ['entityId'], name: 'audit_entity_id_idx' },\n ],\n})\n\n/** Backward-compatible re-export — consumers importing `toolkitAuditLogs` see the same PgTable. */\nexport const toolkitAuditLogs = auditLogTable.table\n"],"mappings":"0DAUA,MAAa,EAAgB,EAAY,CACvC,KAAM,qBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,GAAM,CAAC,CAC1D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,GAAM,CAAC,CACtD,WAAY,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,cAAe,CAAC,CAClE,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,YAAa,CAAC,CAC9D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,UAAW,CAAC,CAC1D,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,YAAa,CAAC,CAC9D,QAAS,EAAO,OAAgC,CAChD,SAAU,EAAO,OAAgC,CACjD,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,aACT,CAAC,CACH,CACD,QAAS,CACP,CAAE,GAAI,CAAC,aAAc,YAAY,CAAE,KAAM,mCAAoC,CAC7E,CAAE,GAAI,CAAC,SAAU,YAAY,CAAE,KAAM,+BAAgC,CACrE,CAAE,GAAI,CAAC,SAAU,YAAY,CAAE,KAAM,8BAA+B,CACpE,CAAE,GAAI,CAAC,YAAY,CAAE,KAAM,uBAAwB,CACnD,CAAE,GAAI,CAAC,WAAW,CAAE,KAAM,sBAAuB,CAClD,CACF,CAAC,CAGW,EAAmB,EAAc"}
1
+ {"version":3,"file":"audit-table-BImbXt_s.mjs","names":[],"sources":["../src/audit-table.ts"],"sourcesContent":["/**\n * `toolkit_audit_logs` table definition via `defineTable`.\n *\n * Replaces the hand-written `pgTable` in `schema.ts`. Uses `pgName` to\n * preserve the existing snake_case Postgres column names so no migration\n * is required.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const auditLogTable = defineTable({\n name: 'toolkit_audit_logs',\n columns: {\n id: column.uuid({ primaryKey: true, defaultRandom: true }),\n action: column.varchar({ length: 100, notNull: true }),\n entityType: column.varchar({ length: 100, pgName: 'entity_type' }),\n entityId: column.varchar({ length: 255, pgName: 'entity_id' }),\n userId: column.varchar({ length: 255, pgName: 'user_id' }),\n userName: column.varchar({ length: 255, pgName: 'user_name' }),\n changes: column.jsonb<Record<string, unknown>>(),\n metadata: column.jsonb<Record<string, unknown>>(),\n createdAt: column.timestamp({\n notNull: true,\n defaultNow: true,\n withTimezone: true,\n pgName: 'created_at',\n }),\n },\n indexes: [\n { on: ['entityType', 'createdAt'], name: 'audit_entity_type_created_at_idx' },\n { on: ['userId', 'createdAt'], name: 'audit_user_id_created_at_idx' },\n { on: ['action', 'createdAt'], name: 'audit_action_created_at_idx' },\n { on: ['createdAt'], name: 'audit_created_at_idx' },\n { on: ['entityId'], name: 'audit_entity_id_idx' },\n ],\n})\n\n/** Backward-compatible re-export — consumers importing `toolkitAuditLogs` see the same PgTable. */\nexport const toolkitAuditLogs = auditLogTable.table\n"],"mappings":"0DAUA,MAAa,EAAgB,EAAY,CACvC,KAAM,qBACN,QAAS,CACP,GAAI,EAAO,KAAK,CAAE,WAAY,GAAM,cAAe,EAAK,CAAC,EACzD,OAAQ,EAAO,QAAQ,CAAE,OAAQ,IAAK,QAAS,EAAK,CAAC,EACrD,WAAY,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,aAAc,CAAC,EACjE,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,WAAY,CAAC,EAC7D,OAAQ,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,SAAU,CAAC,EACzD,SAAU,EAAO,QAAQ,CAAE,OAAQ,IAAK,OAAQ,WAAY,CAAC,EAC7D,QAAS,EAAO,MAA+B,EAC/C,SAAU,EAAO,MAA+B,EAChD,UAAW,EAAO,UAAU,CAC1B,QAAS,GACT,WAAY,GACZ,aAAc,GACd,OAAQ,YACV,CAAC,CACH,EACA,QAAS,CACP,CAAE,GAAI,CAAC,aAAc,WAAW,EAAG,KAAM,kCAAmC,EAC5E,CAAE,GAAI,CAAC,SAAU,WAAW,EAAG,KAAM,8BAA+B,EACpE,CAAE,GAAI,CAAC,SAAU,WAAW,EAAG,KAAM,6BAA8B,EACnE,CAAE,GAAI,CAAC,WAAW,EAAG,KAAM,sBAAuB,EAClD,CAAE,GAAI,CAAC,UAAU,EAAG,KAAM,qBAAsB,CAClD,CACF,CAAC,EAGY,EAAmB,EAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/logger.ts","../src/audit.ts","../src/audit-client.ts"],"mappings":";;;;KAEY,MAAA,GAAS,QAAA;AAAA,UAEJ,YAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;AAAA;;;AAHF;;;iBAoCgB,YAAA,CAAa,MAAA,GAAS,YAAA,GAAe,MAAA;;;UCpCpC,UAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,GAAU,MAAA;EACV,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,WAAA;EDVY;;;;;ECgB3B,MAAA,EAAQ,IAAA,CAAK,MAAA;EACb,QAAA,IAAY,KAAA,EAAO,UAAA,KAAe,OAAA;EDmBR;;;;;;;;;;;ACpC5B;EA8BE,UAAA;AAAA;AAAA,UAGe,WAAA;EACf,GAAA,GAAM,KAAA,EAAO,UAAA,KAAe,OAAA;AAAA;;;;;;;;;;AAxB9B;;iBAkIgB,mBAAA,CAAoB,EAAA,EAAI,kBAAA,IAAsB,KAAA,EAAO,UAAA,KAAe,OAAA;;;;;;;;;;;;;;;;iBA6BpE,iBAAA,CAAkB,MAAA,EAAQ,WAAA,GAAc,WAAA;;;UCrKvC,oBAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA,GAAW,IAAA;EACX,MAAA,GAAS,IAAA;EFZoB;EEc7B,MAAA;EFZ2B;EEc3B,KAAA;EACA,MAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EACA,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,EAAS,MAAA;EACT,QAAA,EAAU,MAAA;EACV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,kBAAA;EACf,KAAA,EAAO,aAAA;EACP,KAAA;AAAA;AAAA,cAqBW,cAAA;EAAA,QACH,MAAA;cAEI,EAAA,EAAI,kBAAA;EDzDhB;EC8DM,KAAA,CAAM,KAAA,EAAO,IAAA,CAAK,aAAA,wBAAqC,OAAA;ED5D7D;ECyEM,KAAA,CAAM,OAAA,GAAS,oBAAA,GAA4B,OAAA,CAAQ,kBAAA;EDvEzD;EC0FM,QAAA,CAAS,EAAA,WAAa,OAAA,CAAQ,aAAA;EDzF1B;EC8FJ,sBAAA,CAAA,GAA0B,OAAA;ED7FrB;ECoGL,kBAAA,CAAA,GAAsB,OAAA;EAAA,QAQpB,UAAA;EAAA,QAmCA,YAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/logger.ts","../src/audit.ts","../src/audit-client.ts"],"mappings":";;;;KAEY,MAAA,GAAS,QAAU;AAAA,UAEd,YAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;AAAA;;AAL6B;AAE/B;;;iBAoCgB,YAAA,CAAa,MAAA,GAAS,YAAA,GAAe,MAAM;;;UCpC1C,UAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,GAAU,MAAA;EACV,QAAA,GAAW,MAAM;AAAA;AAAA,UAGF,WAAA;EDVY;;;;;ECgB3B,MAAA,EAAQ,IAAA,CAAK,MAAA;EACb,QAAA,IAAY,KAAA,EAAO,UAAA,KAAe,OAAA;EDmBR;;;;;;;AAA+B;;;;ACpC3D;EA8BE,UAAA;AAAA;AAAA,UAGe,WAAA;EACf,GAAA,GAAM,KAAA,EAAO,UAAA,KAAe,OAAO;AAAA;;;;;;;;;AA3BlB;AAGnB;;iBAkIgB,mBAAA,CAAoB,EAAA,EAAI,kBAAA,IAAsB,KAAA,EAAO,UAAA,KAAe,OAAA;;;;;;;;;;;;;;;;iBA6BpE,iBAAA,CAAkB,MAAA,EAAQ,WAAA,GAAc,WAAW;;;UCrKlD,oBAAA;EACf,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA,GAAW,IAAA;EACX,MAAA,GAAS,IAAI;EFZgB;EEc7B,MAAA;EFZ2B;EEc3B,KAAA;EACA,MAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,UAGe,aAAA;EACf,EAAA;EACA,MAAA;EACA,UAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA,EAAS,MAAA;EACT,QAAA,EAAU,MAAA;EACV,SAAA,EAAW,IAAA;AAAA;AAAA,UAGI,kBAAA;EACf,KAAA,EAAO,aAAa;EACpB,KAAA;AAAA;AAAA,cAqBW,cAAA;EAAA,QACH,MAAA;cAEI,EAAA,EAAI,kBAAA;EDzDhB;EC8DM,KAAA,CAAM,KAAA,EAAO,IAAA,CAAK,aAAA,wBAAqC,OAAA;ED5D7D;ECyEM,KAAA,CAAM,OAAA,GAAS,oBAAA,GAA4B,OAAA,CAAQ,kBAAA;EDvEzD;EC0FM,QAAA,CAAS,EAAA,WAAa,OAAA,CAAQ,aAAA;EDzF1B;EC8FJ,sBAAA,CAAA,GAA0B,OAAA;ED7FrB;ECoGL,kBAAA,CAAA,GAAsB,OAAA;EAAA,QAQpB,UAAA;EAAA,QAmCA,YAAA;AAAA"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{t as e}from"./audit-table-CrPP2ZZ9.mjs";import t from"pino";var n=class{client;constructor(t){this.client=e.makeClient(t)}async write(e){await this.client.insert({action:e.action,entityType:e.entityType,entityId:e.entityId,userId:e.userId,userName:e.userName,changes:e.changes,metadata:e.metadata})}async query(e={}){let t=Math.min(Math.max(e.limit??50,1),100),n=Math.max(e.offset??0,0),r=this.buildWhere(e),i=this.buildOrderBy(e.sortField,e.sortDirection),[a,o]=await Promise.all([this.client.findMany({...r!==void 0&&{where:r},orderBy:i,limit:t,offset:n}),this.client.count(r)]);return{items:a,total:o}}async findById(e){return this.client.findOne({id:e})}async getDistinctEntityTypes(){return this.client.distinct(`entityType`,{orderBy:`asc`})}async getDistinctActions(){return this.client.distinct(`action`,{orderBy:`asc`})}buildWhere(e){let t=[];if(e.action&&t.push({action:e.action}),e.entityType&&t.push({entityType:e.entityType}),e.entityId&&t.push({entityId:e.entityId}),e.userId&&t.push({userId:e.userId}),e.dateFrom&&t.push({createdAt:{gte:e.dateFrom}}),e.dateTo&&t.push({createdAt:{lte:e.dateTo}}),e.search&&t.push({$or:[{action:{ilike:e.search}},{entityType:{ilike:e.search}},{userName:{ilike:e.search}}]}),t.length!==0)return t.length===1?t[0]:{$and:t}}buildOrderBy(e,t){let n=t===`asc`?`asc`:`desc`;switch(e){case`action`:return[{column:`action`,dir:n}];case`entityType`:return[{column:`entityType`,dir:n}];default:return[{column:`createdAt`,dir:n}]}}};const r=[`password`,`passwordhash`,`token`,`secret`,`apikey`,`accesstoken`,`refreshtoken`,`sessiontoken`,`authorization`,`cookie`];function i(e,t,n,r=0){if(r>=8)return`[TRUNCATED]`;if(typeof e!=`object`||!e)return e;if(n.has(e))return`[CYCLE]`;if(n.add(e),Array.isArray(e))return e.map(e=>i(e,t,n,r+1));let a=Object.getPrototypeOf(e);if(a!==Object.prototype&&a!==null)return e;let o={};for(let[a,s]of Object.entries(e))t.has(a.toLowerCase())?o[a]=`[REDACTED]`:o[a]=i(s,t,n,r+1);return o}function a(e){let t=new Set((e??r).map(e=>e.toLowerCase()));return e=>{if(!e.changes&&!e.metadata)return e;let n=new WeakSet;return{...e,...e.changes?{changes:i(e.changes,t,n)}:{},...e.metadata?{metadata:i(e.metadata,t,n)}:{}}}}function o(e){let t=new n(e);return e=>t.write({action:e.action,entityType:e.entityType??null,entityId:e.entityId??null,userId:e.userId??null,userName:e.userName??null,changes:e.changes??null,metadata:e.metadata??null})}function s(e){let t=a(e.redactKeys);return{log:async n=>{let r=t(n);e.logger.info({audit:!0,action:r.action,entityType:r.entityType,entityId:r.entityId,userId:r.userId,userName:r.userName,changes:r.changes,metadata:r.metadata},`Audit: ${r.action}`),e.dbWriter&&e.dbWriter(r).catch(t=>{e.logger.error({err:t,auditEntry:r},`Failed to write audit log to database`)})}}}const c=t({level:process.env.LOG_LEVEL||`info`,formatters:{level:e=>({level:e})},timestamp:!1,serializers:{err:t.stdSerializers.err,error:t.stdSerializers.err},redact:{paths:[`password`,`token`,`secret`,`apiKey`,`accessToken`,`refreshToken`],censor:`[REDACTED]`}});function l(e){if(!e)return c;let t={};e.name&&(t.name=e.name);let n={};return e.level&&(n.level=e.level),c.child(t,n)}export{n as AuditLogClient,o as createAuditDbWriter,s as createAuditLogger,l as createLogger};
1
+ import{t as e}from"./audit-table-BImbXt_s.mjs";import t from"pino";var n=class{client;constructor(t){this.client=e.makeClient(t)}async write(e){await this.client.insert({action:e.action,entityType:e.entityType,entityId:e.entityId,userId:e.userId,userName:e.userName,changes:e.changes,metadata:e.metadata})}async query(e={}){let t=Math.min(Math.max(e.limit??50,1),100),n=Math.max(e.offset??0,0),r=this.buildWhere(e),i=this.buildOrderBy(e.sortField,e.sortDirection),[a,o]=await Promise.all([this.client.findMany({...r!==void 0&&{where:r},orderBy:i,limit:t,offset:n}),this.client.count(r)]);return{items:a,total:o}}async findById(e){return this.client.findOne({id:e})}async getDistinctEntityTypes(){return this.client.distinct(`entityType`,{orderBy:`asc`})}async getDistinctActions(){return this.client.distinct(`action`,{orderBy:`asc`})}buildWhere(e){let t=[];if(e.action&&t.push({action:e.action}),e.entityType&&t.push({entityType:e.entityType}),e.entityId&&t.push({entityId:e.entityId}),e.userId&&t.push({userId:e.userId}),e.dateFrom&&t.push({createdAt:{gte:e.dateFrom}}),e.dateTo&&t.push({createdAt:{lte:e.dateTo}}),e.search&&t.push({$or:[{action:{ilike:e.search}},{entityType:{ilike:e.search}},{userName:{ilike:e.search}}]}),t.length!==0)return t.length===1?t[0]:{$and:t}}buildOrderBy(e,t){let n=t===`asc`?`asc`:`desc`;switch(e){case`action`:return[{column:`action`,dir:n}];case`entityType`:return[{column:`entityType`,dir:n}];default:return[{column:`createdAt`,dir:n}]}}};const r=[`password`,`passwordhash`,`token`,`secret`,`apikey`,`accesstoken`,`refreshtoken`,`sessiontoken`,`authorization`,`cookie`];function i(e,t,n,r=0){if(r>=8)return`[TRUNCATED]`;if(typeof e!=`object`||!e)return e;if(n.has(e))return`[CYCLE]`;if(n.add(e),Array.isArray(e))return e.map(e=>i(e,t,n,r+1));let a=Object.getPrototypeOf(e);if(a!==Object.prototype&&a!==null)return e;let o={};for(let[a,s]of Object.entries(e))t.has(a.toLowerCase())?o[a]=`[REDACTED]`:o[a]=i(s,t,n,r+1);return o}function a(e){let t=new Set((e??r).map(e=>e.toLowerCase()));return e=>{if(!e.changes&&!e.metadata)return e;let n=new WeakSet;return{...e,...e.changes?{changes:i(e.changes,t,n)}:{},...e.metadata?{metadata:i(e.metadata,t,n)}:{}}}}function o(e){let t=new n(e);return e=>t.write({action:e.action,entityType:e.entityType??null,entityId:e.entityId??null,userId:e.userId??null,userName:e.userName??null,changes:e.changes??null,metadata:e.metadata??null})}function s(e){let t=a(e.redactKeys);return{log:async n=>{let r=t(n);e.logger.info({audit:!0,action:r.action,entityType:r.entityType,entityId:r.entityId,userId:r.userId,userName:r.userName,changes:r.changes,metadata:r.metadata},`Audit: ${r.action}`),e.dbWriter&&e.dbWriter(r).catch(t=>{e.logger.error({err:t,auditEntry:r},`Failed to write audit log to database`)})}}}const c=t({level:process.env.LOG_LEVEL||`info`,formatters:{level:e=>({level:e})},timestamp:!1,serializers:{err:t.stdSerializers.err,error:t.stdSerializers.err},redact:{paths:[`password`,`token`,`secret`,`apiKey`,`accessToken`,`refreshToken`],censor:`[REDACTED]`}});function l(e){if(!e)return c;let t={};e.name&&(t.name=e.name);let n={};return e.level&&(n.level=e.level),c.child(t,n)}export{n as AuditLogClient,o as createAuditDbWriter,s as createAuditLogger,l as createLogger};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/audit-client.ts","../src/audit.ts","../src/logger.ts"],"sourcesContent":["import type { TableClient, WhereClause } from '@murumets-ee/db'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { auditLogTable } from './audit-table.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AuditLogQueryOptions {\n action?: string | undefined\n entityType?: string | undefined\n entityId?: string | undefined\n userId?: string | undefined\n dateFrom?: Date | undefined\n dateTo?: Date | undefined\n /** Free-text search across action, entityType, userName */\n search?: string | undefined\n /** Default 50, max 100 */\n limit?: number | undefined\n offset?: number | undefined\n sortField?: 'createdAt' | 'action' | 'entityType' | undefined\n sortDirection?: 'asc' | 'desc' | undefined\n}\n\nexport interface AuditLogEntry {\n id: string\n action: string\n entityType: string | null\n entityId: string | null\n userId: string | null\n userName: string | null\n changes: Record<string, unknown> | null\n metadata: Record<string, unknown> | null\n createdAt: Date\n}\n\nexport interface AuditLogListResult {\n items: AuditLogEntry[]\n total: number\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_LIMIT = 100\nconst DEFAULT_LIMIT = 50\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ntype AuditCols = typeof auditLogTable.schema.columns\ntype AuditWhere = WhereClause<AuditCols>\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\nexport class AuditLogClient {\n private client: TableClient<AuditCols>\n\n constructor(db: PostgresJsDatabase) {\n this.client = auditLogTable.makeClient(db)\n }\n\n /** Write a single audit log entry */\n async write(entry: Omit<AuditLogEntry, 'id' | 'createdAt'>): Promise<void> {\n await this.client.insert({\n action: entry.action,\n entityType: entry.entityType,\n entityId: entry.entityId,\n userId: entry.userId,\n userName: entry.userName,\n changes: entry.changes,\n metadata: entry.metadata,\n })\n }\n\n /** Query audit logs with filters and pagination */\n async query(options: AuditLogQueryOptions = {}): Promise<AuditLogListResult> {\n const limit = Math.min(Math.max(options.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT)\n const offset = Math.max(options.offset ?? 0, 0)\n\n const where = this.buildWhere(options)\n const orderBy = this.buildOrderBy(options.sortField, options.sortDirection)\n\n const [items, total] = await Promise.all([\n this.client.findMany({ ...(where !== undefined && { where }), orderBy, limit, offset }),\n this.client.count(where),\n ])\n\n return {\n items: items as AuditLogEntry[],\n total,\n }\n }\n\n /** Get a single audit log entry by ID */\n async findById(id: string): Promise<AuditLogEntry | null> {\n return this.client.findOne({ id }) as Promise<AuditLogEntry | null>\n }\n\n /** Get distinct entity types present in the audit log */\n async getDistinctEntityTypes(): Promise<string[]> {\n // entityType is nullable, but distinct() excludes nulls by default —\n // the cast is safe because includeNull is not set.\n return this.client.distinct('entityType', { orderBy: 'asc' }) as Promise<string[]>\n }\n\n /** Get distinct actions present in the audit log */\n async getDistinctActions(): Promise<string[]> {\n return this.client.distinct('action', { orderBy: 'asc' })\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private buildWhere(options: AuditLogQueryOptions): AuditWhere | undefined {\n const parts: AuditWhere[] = []\n\n if (options.action) parts.push({ action: options.action })\n if (options.entityType) parts.push({ entityType: options.entityType })\n if (options.entityId) parts.push({ entityId: options.entityId })\n if (options.userId) parts.push({ userId: options.userId })\n\n // Date range: push each bound as its own typed clause. The\n // ColumnOperators union allows exactly one operator per object —\n // combining gte+lte requires two clauses AND-ed at the top level\n // (which buildWhere does below).\n if (options.dateFrom) {\n parts.push({ createdAt: { gte: options.dateFrom } })\n }\n if (options.dateTo) {\n parts.push({ createdAt: { lte: options.dateTo } })\n }\n\n // Free-text search across action, entityType, userName\n if (options.search) {\n parts.push({\n $or: [\n { action: { ilike: options.search } },\n { entityType: { ilike: options.search } },\n { userName: { ilike: options.search } },\n ],\n })\n }\n\n if (parts.length === 0) return undefined\n if (parts.length === 1) return parts[0]\n return { $and: parts }\n }\n\n private buildOrderBy(\n sortField?: string,\n sortDirection?: string,\n ): { column: keyof AuditCols & string; dir: 'asc' | 'desc' }[] {\n const dir: 'asc' | 'desc' = sortDirection === 'asc' ? 'asc' : 'desc'\n switch (sortField) {\n case 'action':\n return [{ column: 'action', dir }]\n case 'entityType':\n return [{ column: 'entityType', dir }]\n default:\n return [{ column: 'createdAt', dir }]\n }\n }\n}\n","import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { AuditLogClient } from './audit-client.js'\nimport type { Logger } from './logger.js'\n\nexport interface AuditEntry {\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}\n\nexport interface AuditConfig {\n /**\n * The audit logger only ever calls `info` and `error` — narrowing the\n * accepted shape avoids forcing tests and adapters to construct or fake\n * the full ~30-method Pino surface.\n */\n logger: Pick<Logger, 'info' | 'error'>\n dbWriter?: (entry: AuditEntry) => Promise<void>\n /**\n * REPLACE the deny-list of object keys whose values are replaced with\n * `[REDACTED]` when found anywhere inside `changes` / `metadata` before\n * the entry is logged or persisted. Matching is case-insensitive on the\n * exact key name (no substring match — `foo.password` matches, `myPassword`\n * does not).\n *\n * **Replace, not merge.** Passing `['ssn']` removes `password`/`token`/etc.\n * from the deny-list; if you want to *add* keys while keeping defaults,\n * spread them yourself: `redactKeys: [...DEFAULT_REDACT_KEYS, 'ssn']`\n * (import `DEFAULT_REDACT_KEYS` if needed; or just inline the list).\n */\n redactKeys?: readonly string[]\n}\n\nexport interface AuditLogger {\n log: (entry: AuditEntry) => Promise<void>\n}\n\nconst DEFAULT_REDACT_KEYS: readonly string[] = [\n 'password',\n 'passwordhash',\n 'token',\n 'secret',\n 'apikey',\n 'accesstoken',\n 'refreshtoken',\n 'sessiontoken',\n 'authorization',\n 'cookie',\n] as const\n\nconst REDACTED = '[REDACTED]'\nconst TRUNCATED = '[TRUNCATED]'\nconst CYCLE = '[CYCLE]'\n\n/** Maximum object depth walked by the redactor — defends against deeply\n * recursive caller payloads. Anything past this is replaced with TRUNCATED\n * so admins reading the log can tell \"we gave up walking\" apart from\n * \"we wiped a credential\". */\nconst MAX_REDACT_DEPTH = 8\n\n/**\n * Walk `value` and return a deep clone where any property whose key matches\n * `denyKeys` (case-insensitive exact match) is replaced with `[REDACTED]`.\n *\n * - Objects and arrays are cloned; primitives are returned as-is.\n * - Cycles are detected via a per-walk visited set and replaced with\n * `[CYCLE]` (callers should not be feeding cycles into JSONB, but the\n * redactor must not run forever or fan out exponentially if they do).\n * - Depth cap (`MAX_REDACT_DEPTH`) replaces over-deep subtrees with\n * `[TRUNCATED]`. Distinct sentinel from `[REDACTED]` so the cap is not\n * mistaken for a credential wipe.\n * - Non-plain objects (Date, Map, Set, Buffer, class instances) are returned\n * as-is — audit payloads are JSONB-serializable plain objects by contract;\n * anything else surfaces in the dbWriter, not here.\n * - `Object.create(null)` (null-prototype) IS walked, since it's a valid\n * plain-object shape commonly produced by `Object.fromEntries(...)` and\n * parsed JSON in some libraries.\n *\n * Symbol-keyed properties are NOT walked — `Object.entries` skips them, but\n * they also wouldn't survive JSONB serialization to the DB. Stdout could\n * theoretically leak one if a caller hands us a hybrid object; that's a\n * caller bug we don't try to defend against.\n */\nfunction redactDeep(\n value: unknown,\n denyKeys: ReadonlySet<string>,\n visited: WeakSet<object>,\n depth = 0,\n): unknown {\n if (depth >= MAX_REDACT_DEPTH) return TRUNCATED\n if (value === null || value === undefined) return value\n if (typeof value !== 'object') return value\n if (visited.has(value as object)) return CYCLE\n visited.add(value as object)\n if (Array.isArray(value)) {\n return value.map((v) => redactDeep(v, denyKeys, visited, depth + 1))\n }\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (denyKeys.has(k.toLowerCase())) {\n out[k] = REDACTED\n } else {\n out[k] = redactDeep(v, denyKeys, visited, depth + 1)\n }\n }\n return out\n}\n\nfunction buildSanitizer(redactKeys?: readonly string[]): (entry: AuditEntry) => AuditEntry {\n const denyKeys = new Set((redactKeys ?? DEFAULT_REDACT_KEYS).map((k) => k.toLowerCase()))\n return (entry) => {\n if (!entry.changes && !entry.metadata) return entry\n // Per-call visited set so a sanitized payload isn't poisoned by a prior\n // call's reference graph.\n const visited = new WeakSet<object>()\n return {\n ...entry,\n ...(entry.changes\n ? { changes: redactDeep(entry.changes, denyKeys, visited) as Record<string, unknown> }\n : {}),\n ...(entry.metadata\n ? { metadata: redactDeep(entry.metadata, denyKeys, visited) as Record<string, unknown> }\n : {}),\n }\n }\n}\n\n/**\n * Create a dbWriter callback wired to AuditLogClient.\n *\n * Pass the returned function as `dbWriter` to `createAuditLogger`:\n * ```typescript\n * const auditLogger = createAuditLogger({\n * logger,\n * dbWriter: createAuditDbWriter(db),\n * })\n * ```\n */\nexport function createAuditDbWriter(db: PostgresJsDatabase): (entry: AuditEntry) => Promise<void> {\n const client = new AuditLogClient(db)\n return (entry) =>\n client.write({\n action: entry.action,\n entityType: entry.entityType ?? null,\n entityId: entry.entityId ?? null,\n userId: entry.userId ?? null,\n userName: entry.userName ?? null,\n changes: entry.changes ?? null,\n metadata: entry.metadata ?? null,\n })\n}\n\n/**\n * Create an audit logger.\n *\n * Audit logs are written to stdout immediately (non-blocking).\n * Optionally, they can also be written to the database if dbWriter is provided.\n *\n * Database writes are fire-and-forget to avoid blocking the main operation.\n * If DB write fails, it's logged to stderr but doesn't throw.\n *\n * **Redaction.** Both stdout and DB writes pass `changes` and `metadata`\n * through a recursive key-based sanitizer (see `redactKeys`). Pino's own\n * top-level `redact` config does not match nested keys, so callers must rely\n * on this sanitizer for nested credentials. Defense-in-depth — callers should\n * still avoid sticking secrets into audit payloads in the first place.\n */\nexport function createAuditLogger(config: AuditConfig): AuditLogger {\n const sanitize = buildSanitizer(config.redactKeys)\n return {\n log: async (entry: AuditEntry) => {\n const safe = sanitize(entry)\n\n // Log to stdout immediately (structured log with audit: true marker)\n config.logger.info(\n {\n audit: true,\n action: safe.action,\n entityType: safe.entityType,\n entityId: safe.entityId,\n userId: safe.userId,\n userName: safe.userName,\n changes: safe.changes,\n metadata: safe.metadata,\n },\n `Audit: ${safe.action}`,\n )\n\n // Write to DB if configured (fire and forget)\n if (config.dbWriter) {\n config.dbWriter(safe).catch((err) => {\n config.logger.error({ err, auditEntry: safe }, 'Failed to write audit log to database')\n })\n }\n },\n }\n}\n","import pino, { type Logger as PinoLogger } from 'pino'\n\nexport type Logger = PinoLogger\n\nexport interface LoggerConfig {\n name?: string\n level?: string\n redact?: string[]\n}\n\n/**\n * Root logger instance\n *\n * IMPORTANT: No transports are used to avoid Next.js bundler issues.\n * Pino transports use worker threads that break webpack/turbopack.\n *\n * For dev pretty-printing, pipe to pino-pretty:\n * pnpm dev | pnpm pino-pretty\n */\nconst rootLogger = pino({\n level: process.env.LOG_LEVEL || 'info',\n formatters: {\n level: (label) => ({ level: label }),\n },\n timestamp: false,\n serializers: {\n err: pino.stdSerializers.err,\n error: pino.stdSerializers.err,\n },\n redact: {\n paths: ['password', 'token', 'secret', 'apiKey', 'accessToken', 'refreshToken'],\n censor: '[REDACTED]',\n },\n})\n\n/**\n * Create a logger instance\n * If no config provided, returns the root logger\n * If config provided, returns a child logger with the specified context\n */\nexport function createLogger(config?: LoggerConfig): Logger {\n if (!config) {\n return rootLogger\n }\n\n const bindings: Record<string, unknown> = {}\n if (config.name) {\n bindings.name = config.name\n }\n\n const options: { level?: string } = {}\n if (config.level) {\n options.level = config.level\n }\n\n return rootLogger.child(bindings, options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/audit-client.ts","../src/audit.ts","../src/logger.ts"],"sourcesContent":["import type { TableClient, WhereClause } from '@murumets-ee/db'\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { auditLogTable } from './audit-table.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AuditLogQueryOptions {\n action?: string | undefined\n entityType?: string | undefined\n entityId?: string | undefined\n userId?: string | undefined\n dateFrom?: Date | undefined\n dateTo?: Date | undefined\n /** Free-text search across action, entityType, userName */\n search?: string | undefined\n /** Default 50, max 100 */\n limit?: number | undefined\n offset?: number | undefined\n sortField?: 'createdAt' | 'action' | 'entityType' | undefined\n sortDirection?: 'asc' | 'desc' | undefined\n}\n\nexport interface AuditLogEntry {\n id: string\n action: string\n entityType: string | null\n entityId: string | null\n userId: string | null\n userName: string | null\n changes: Record<string, unknown> | null\n metadata: Record<string, unknown> | null\n createdAt: Date\n}\n\nexport interface AuditLogListResult {\n items: AuditLogEntry[]\n total: number\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_LIMIT = 100\nconst DEFAULT_LIMIT = 50\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ntype AuditCols = typeof auditLogTable.schema.columns\ntype AuditWhere = WhereClause<AuditCols>\n\n// ---------------------------------------------------------------------------\n// Client\n// ---------------------------------------------------------------------------\n\nexport class AuditLogClient {\n private client: TableClient<AuditCols>\n\n constructor(db: PostgresJsDatabase) {\n this.client = auditLogTable.makeClient(db)\n }\n\n /** Write a single audit log entry */\n async write(entry: Omit<AuditLogEntry, 'id' | 'createdAt'>): Promise<void> {\n await this.client.insert({\n action: entry.action,\n entityType: entry.entityType,\n entityId: entry.entityId,\n userId: entry.userId,\n userName: entry.userName,\n changes: entry.changes,\n metadata: entry.metadata,\n })\n }\n\n /** Query audit logs with filters and pagination */\n async query(options: AuditLogQueryOptions = {}): Promise<AuditLogListResult> {\n const limit = Math.min(Math.max(options.limit ?? DEFAULT_LIMIT, 1), MAX_LIMIT)\n const offset = Math.max(options.offset ?? 0, 0)\n\n const where = this.buildWhere(options)\n const orderBy = this.buildOrderBy(options.sortField, options.sortDirection)\n\n const [items, total] = await Promise.all([\n this.client.findMany({ ...(where !== undefined && { where }), orderBy, limit, offset }),\n this.client.count(where),\n ])\n\n return {\n items: items as AuditLogEntry[],\n total,\n }\n }\n\n /** Get a single audit log entry by ID */\n async findById(id: string): Promise<AuditLogEntry | null> {\n return this.client.findOne({ id }) as Promise<AuditLogEntry | null>\n }\n\n /** Get distinct entity types present in the audit log */\n async getDistinctEntityTypes(): Promise<string[]> {\n // entityType is nullable, but distinct() excludes nulls by default —\n // the cast is safe because includeNull is not set.\n return this.client.distinct('entityType', { orderBy: 'asc' }) as Promise<string[]>\n }\n\n /** Get distinct actions present in the audit log */\n async getDistinctActions(): Promise<string[]> {\n return this.client.distinct('action', { orderBy: 'asc' })\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private buildWhere(options: AuditLogQueryOptions): AuditWhere | undefined {\n const parts: AuditWhere[] = []\n\n if (options.action) parts.push({ action: options.action })\n if (options.entityType) parts.push({ entityType: options.entityType })\n if (options.entityId) parts.push({ entityId: options.entityId })\n if (options.userId) parts.push({ userId: options.userId })\n\n // Date range: push each bound as its own typed clause. The\n // ColumnOperators union allows exactly one operator per object —\n // combining gte+lte requires two clauses AND-ed at the top level\n // (which buildWhere does below).\n if (options.dateFrom) {\n parts.push({ createdAt: { gte: options.dateFrom } })\n }\n if (options.dateTo) {\n parts.push({ createdAt: { lte: options.dateTo } })\n }\n\n // Free-text search across action, entityType, userName\n if (options.search) {\n parts.push({\n $or: [\n { action: { ilike: options.search } },\n { entityType: { ilike: options.search } },\n { userName: { ilike: options.search } },\n ],\n })\n }\n\n if (parts.length === 0) return undefined\n if (parts.length === 1) return parts[0]\n return { $and: parts }\n }\n\n private buildOrderBy(\n sortField?: string,\n sortDirection?: string,\n ): { column: keyof AuditCols & string; dir: 'asc' | 'desc' }[] {\n const dir: 'asc' | 'desc' = sortDirection === 'asc' ? 'asc' : 'desc'\n switch (sortField) {\n case 'action':\n return [{ column: 'action', dir }]\n case 'entityType':\n return [{ column: 'entityType', dir }]\n default:\n return [{ column: 'createdAt', dir }]\n }\n }\n}\n","import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { AuditLogClient } from './audit-client.js'\nimport type { Logger } from './logger.js'\n\nexport interface AuditEntry {\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}\n\nexport interface AuditConfig {\n /**\n * The audit logger only ever calls `info` and `error` — narrowing the\n * accepted shape avoids forcing tests and adapters to construct or fake\n * the full ~30-method Pino surface.\n */\n logger: Pick<Logger, 'info' | 'error'>\n dbWriter?: (entry: AuditEntry) => Promise<void>\n /**\n * REPLACE the deny-list of object keys whose values are replaced with\n * `[REDACTED]` when found anywhere inside `changes` / `metadata` before\n * the entry is logged or persisted. Matching is case-insensitive on the\n * exact key name (no substring match — `foo.password` matches, `myPassword`\n * does not).\n *\n * **Replace, not merge.** Passing `['ssn']` removes `password`/`token`/etc.\n * from the deny-list; if you want to *add* keys while keeping defaults,\n * spread them yourself: `redactKeys: [...DEFAULT_REDACT_KEYS, 'ssn']`\n * (import `DEFAULT_REDACT_KEYS` if needed; or just inline the list).\n */\n redactKeys?: readonly string[]\n}\n\nexport interface AuditLogger {\n log: (entry: AuditEntry) => Promise<void>\n}\n\nconst DEFAULT_REDACT_KEYS: readonly string[] = [\n 'password',\n 'passwordhash',\n 'token',\n 'secret',\n 'apikey',\n 'accesstoken',\n 'refreshtoken',\n 'sessiontoken',\n 'authorization',\n 'cookie',\n] as const\n\nconst REDACTED = '[REDACTED]'\nconst TRUNCATED = '[TRUNCATED]'\nconst CYCLE = '[CYCLE]'\n\n/** Maximum object depth walked by the redactor — defends against deeply\n * recursive caller payloads. Anything past this is replaced with TRUNCATED\n * so admins reading the log can tell \"we gave up walking\" apart from\n * \"we wiped a credential\". */\nconst MAX_REDACT_DEPTH = 8\n\n/**\n * Walk `value` and return a deep clone where any property whose key matches\n * `denyKeys` (case-insensitive exact match) is replaced with `[REDACTED]`.\n *\n * - Objects and arrays are cloned; primitives are returned as-is.\n * - Cycles are detected via a per-walk visited set and replaced with\n * `[CYCLE]` (callers should not be feeding cycles into JSONB, but the\n * redactor must not run forever or fan out exponentially if they do).\n * - Depth cap (`MAX_REDACT_DEPTH`) replaces over-deep subtrees with\n * `[TRUNCATED]`. Distinct sentinel from `[REDACTED]` so the cap is not\n * mistaken for a credential wipe.\n * - Non-plain objects (Date, Map, Set, Buffer, class instances) are returned\n * as-is — audit payloads are JSONB-serializable plain objects by contract;\n * anything else surfaces in the dbWriter, not here.\n * - `Object.create(null)` (null-prototype) IS walked, since it's a valid\n * plain-object shape commonly produced by `Object.fromEntries(...)` and\n * parsed JSON in some libraries.\n *\n * Symbol-keyed properties are NOT walked — `Object.entries` skips them, but\n * they also wouldn't survive JSONB serialization to the DB. Stdout could\n * theoretically leak one if a caller hands us a hybrid object; that's a\n * caller bug we don't try to defend against.\n */\nfunction redactDeep(\n value: unknown,\n denyKeys: ReadonlySet<string>,\n visited: WeakSet<object>,\n depth = 0,\n): unknown {\n if (depth >= MAX_REDACT_DEPTH) return TRUNCATED\n if (value === null || value === undefined) return value\n if (typeof value !== 'object') return value\n if (visited.has(value as object)) return CYCLE\n visited.add(value as object)\n if (Array.isArray(value)) {\n return value.map((v) => redactDeep(v, denyKeys, visited, depth + 1))\n }\n const proto = Object.getPrototypeOf(value)\n if (proto !== Object.prototype && proto !== null) return value\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (denyKeys.has(k.toLowerCase())) {\n out[k] = REDACTED\n } else {\n out[k] = redactDeep(v, denyKeys, visited, depth + 1)\n }\n }\n return out\n}\n\nfunction buildSanitizer(redactKeys?: readonly string[]): (entry: AuditEntry) => AuditEntry {\n const denyKeys = new Set((redactKeys ?? DEFAULT_REDACT_KEYS).map((k) => k.toLowerCase()))\n return (entry) => {\n if (!entry.changes && !entry.metadata) return entry\n // Per-call visited set so a sanitized payload isn't poisoned by a prior\n // call's reference graph.\n const visited = new WeakSet<object>()\n return {\n ...entry,\n ...(entry.changes\n ? { changes: redactDeep(entry.changes, denyKeys, visited) as Record<string, unknown> }\n : {}),\n ...(entry.metadata\n ? { metadata: redactDeep(entry.metadata, denyKeys, visited) as Record<string, unknown> }\n : {}),\n }\n }\n}\n\n/**\n * Create a dbWriter callback wired to AuditLogClient.\n *\n * Pass the returned function as `dbWriter` to `createAuditLogger`:\n * ```typescript\n * const auditLogger = createAuditLogger({\n * logger,\n * dbWriter: createAuditDbWriter(db),\n * })\n * ```\n */\nexport function createAuditDbWriter(db: PostgresJsDatabase): (entry: AuditEntry) => Promise<void> {\n const client = new AuditLogClient(db)\n return (entry) =>\n client.write({\n action: entry.action,\n entityType: entry.entityType ?? null,\n entityId: entry.entityId ?? null,\n userId: entry.userId ?? null,\n userName: entry.userName ?? null,\n changes: entry.changes ?? null,\n metadata: entry.metadata ?? null,\n })\n}\n\n/**\n * Create an audit logger.\n *\n * Audit logs are written to stdout immediately (non-blocking).\n * Optionally, they can also be written to the database if dbWriter is provided.\n *\n * Database writes are fire-and-forget to avoid blocking the main operation.\n * If DB write fails, it's logged to stderr but doesn't throw.\n *\n * **Redaction.** Both stdout and DB writes pass `changes` and `metadata`\n * through a recursive key-based sanitizer (see `redactKeys`). Pino's own\n * top-level `redact` config does not match nested keys, so callers must rely\n * on this sanitizer for nested credentials. Defense-in-depth — callers should\n * still avoid sticking secrets into audit payloads in the first place.\n */\nexport function createAuditLogger(config: AuditConfig): AuditLogger {\n const sanitize = buildSanitizer(config.redactKeys)\n return {\n log: async (entry: AuditEntry) => {\n const safe = sanitize(entry)\n\n // Log to stdout immediately (structured log with audit: true marker)\n config.logger.info(\n {\n audit: true,\n action: safe.action,\n entityType: safe.entityType,\n entityId: safe.entityId,\n userId: safe.userId,\n userName: safe.userName,\n changes: safe.changes,\n metadata: safe.metadata,\n },\n `Audit: ${safe.action}`,\n )\n\n // Write to DB if configured (fire and forget)\n if (config.dbWriter) {\n config.dbWriter(safe).catch((err) => {\n config.logger.error({ err, auditEntry: safe }, 'Failed to write audit log to database')\n })\n }\n },\n }\n}\n","import pino, { type Logger as PinoLogger } from 'pino'\n\nexport type Logger = PinoLogger\n\nexport interface LoggerConfig {\n name?: string\n level?: string\n redact?: string[]\n}\n\n/**\n * Root logger instance\n *\n * IMPORTANT: No transports are used to avoid Next.js bundler issues.\n * Pino transports use worker threads that break webpack/turbopack.\n *\n * For dev pretty-printing, pipe to pino-pretty:\n * pnpm dev | pnpm pino-pretty\n */\nconst rootLogger = pino({\n level: process.env.LOG_LEVEL || 'info',\n formatters: {\n level: (label) => ({ level: label }),\n },\n timestamp: false,\n serializers: {\n err: pino.stdSerializers.err,\n error: pino.stdSerializers.err,\n },\n redact: {\n paths: ['password', 'token', 'secret', 'apiKey', 'accessToken', 'refreshToken'],\n censor: '[REDACTED]',\n },\n})\n\n/**\n * Create a logger instance\n * If no config provided, returns the root logger\n * If config provided, returns a child logger with the specified context\n */\nexport function createLogger(config?: LoggerConfig): Logger {\n if (!config) {\n return rootLogger\n }\n\n const bindings: Record<string, unknown> = {}\n if (config.name) {\n bindings.name = config.name\n }\n\n const options: { level?: string } = {}\n if (config.level) {\n options.level = config.level\n }\n\n return rootLogger.child(bindings, options)\n}\n"],"mappings":"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"}
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{n as e}from"./audit-table-CrPP2ZZ9.mjs";function t(e){return e}function n(){return t({name:`@murumets-ee/logging`,server:{tables:{toolkitAuditLogs:e}},shared:{pluginResources:[{name:`audit-logs`,actions:[`view`]}]}})}export{n as logging};
1
+ import{n as e}from"./audit-table-BImbXt_s.mjs";function t(e){return e}function n(){return t({name:`@murumets-ee/logging`,server:{tables:{toolkitAuditLogs:e}},shared:{pluginResources:[{name:`audit-logs`,actions:[`view`]}]}})}export{n as logging};
2
2
  //# sourceMappingURL=plugin.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import type { Table } from 'drizzle-orm'\nimport { toolkitAuditLogs } from './audit-table.js'\n\n// Structural Plugin shape — kept in sync with @murumets-ee/core. Not imported\n// from core because core depends on logging (circular dep would fail).\n//\n// `Table` (the dialect-agnostic base from `drizzle-orm`) is used instead of\n// `PgTable<any>` because `PgTable<any>` is invariant on its config (the\n// `<any>` kills covariant variance through `_.config`), and narrow\n// `PgTableWithColumns<{...}>` literals captured by `<const P>` inference\n// in `definePlugin` would fail to assign to it. `Table` is drizzle's own\n// canonical accept-type — see drizzle-zod's `createSelectSchema(entity:\n// Table)` and `getTableColumns<T extends Table>`. `readonly` arrays match\n// the widened `Plugin.shared.pluginResources?` so the literal-tuple\n// preservation reaches the `ResolvedPermissions` reader. Issue #357 PR 2.\ninterface LoggingPlugin {\n name: string\n server: {\n tables: Record<string, Table>\n }\n shared: {\n pluginResources: readonly { name: string; actions: readonly string[] }[]\n }\n}\n\n/**\n * Logging plugin — registers the `toolkit_audit_logs` table so it shows up\n * in generated migrations AND contributes the `audit-logs` permission\n * resource so admins can grant audit-log read access via the Permissions\n * UI without hand-declaring the resource in their app's pluginResources\n * array. The audit logger itself is wired up separately via\n * `createAuditLogger()`.\n *\n * @example\n * ```typescript\n * import { logging } from '@murumets-ee/logging/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [logging()],\n * })\n * ```\n */\n// Local `<const T>` identity helper — same pattern as `definePlugin` in\n// `@murumets-ee/core`, inlined here because logging can't import from\n// core (circular dep). Captures the literal types of `name` and\n// `pluginResources` for `ResolvedPermissions<C>` (#357 PR 2).\nfunction defineLoggingPlugin<const T extends LoggingPlugin>(plugin: T): T {\n return plugin\n}\n\nexport function logging() {\n return defineLoggingPlugin({\n name: '@murumets-ee/logging',\n server: {\n tables: { toolkitAuditLogs },\n },\n shared: {\n pluginResources: [{ name: 'audit-logs', actions: ['view'] }],\n },\n })\n}\n"],"mappings":"+CA8CA,SAAS,EAAmD,EAAc,CACxE,OAAO,EAGT,SAAgB,GAAU,CACxB,OAAO,EAAoB,CACzB,KAAM,uBACN,OAAQ,CACN,OAAQ,CAAE,mBAAkB,CAC7B,CACD,OAAQ,CACN,gBAAiB,CAAC,CAAE,KAAM,aAAc,QAAS,CAAC,OAAO,CAAE,CAAC,CAC7D,CACF,CAAC"}
1
+ {"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import type { Table } from 'drizzle-orm'\nimport { toolkitAuditLogs } from './audit-table.js'\n\n// Structural Plugin shape — kept in sync with @murumets-ee/core. Not imported\n// from core because core depends on logging (circular dep would fail).\n//\n// `Table` (the dialect-agnostic base from `drizzle-orm`) is used instead of\n// `PgTable<any>` because `PgTable<any>` is invariant on its config (the\n// `<any>` kills covariant variance through `_.config`), and narrow\n// `PgTableWithColumns<{...}>` literals captured by `<const P>` inference\n// in `definePlugin` would fail to assign to it. `Table` is drizzle's own\n// canonical accept-type — see drizzle-zod's `createSelectSchema(entity:\n// Table)` and `getTableColumns<T extends Table>`. `readonly` arrays match\n// the widened `Plugin.shared.pluginResources?` so the literal-tuple\n// preservation reaches the `ResolvedPermissions` reader. Issue #357 PR 2.\ninterface LoggingPlugin {\n name: string\n server: {\n tables: Record<string, Table>\n }\n shared: {\n pluginResources: readonly { name: string; actions: readonly string[] }[]\n }\n}\n\n/**\n * Logging plugin — registers the `toolkit_audit_logs` table so it shows up\n * in generated migrations AND contributes the `audit-logs` permission\n * resource so admins can grant audit-log read access via the Permissions\n * UI without hand-declaring the resource in their app's pluginResources\n * array. The audit logger itself is wired up separately via\n * `createAuditLogger()`.\n *\n * @example\n * ```typescript\n * import { logging } from '@murumets-ee/logging/plugin'\n *\n * export default defineLumiConfig({\n * plugins: [logging()],\n * })\n * ```\n */\n// Local `<const T>` identity helper — same pattern as `definePlugin` in\n// `@murumets-ee/core`, inlined here because logging can't import from\n// core (circular dep). Captures the literal types of `name` and\n// `pluginResources` for `ResolvedPermissions<C>` (#357 PR 2).\nfunction defineLoggingPlugin<const T extends LoggingPlugin>(plugin: T): T {\n return plugin\n}\n\nexport function logging() {\n return defineLoggingPlugin({\n name: '@murumets-ee/logging',\n server: {\n tables: { toolkitAuditLogs },\n },\n shared: {\n pluginResources: [{ name: 'audit-logs', actions: ['view'] }],\n },\n })\n}\n"],"mappings":"+CA8CA,SAAS,EAAmD,EAAc,CACxE,OAAO,CACT,CAEA,SAAgB,GAAU,CACxB,OAAO,EAAoB,CACzB,KAAM,uBACN,OAAQ,CACN,OAAQ,CAAE,kBAAiB,CAC7B,EACA,OAAQ,CACN,gBAAiB,CAAC,CAAE,KAAM,aAAc,QAAS,CAAC,MAAM,CAAE,CAAC,CAC7D,CACF,CAAC,CACH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/logging",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -24,10 +24,10 @@
24
24
  "drizzle-orm": "^0.45.2",
25
25
  "pino": "^9.5.0",
26
26
  "zod": "^3.24.1",
27
- "@murumets-ee/db": "0.27.0"
27
+ "@murumets-ee/db": "0.28.0"
28
28
  },
29
29
  "devDependencies": {
30
- "tsdown": "^0.21.10",
30
+ "tsdown": "^0.22.2",
31
31
  "typescript": "^5.7.2",
32
32
  "vitest": "^2.1.8"
33
33
  },