@murumets-ee/logging 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.d.mts +55 -29
- package/dist/admin.d.mts.map +1 -1
- package/dist/admin.mjs +1 -1
- package/dist/admin.mjs.map +1 -1
- package/dist/{schema-D9vWDK2q.mjs → audit-table-CrPP2ZZ9.mjs} +1 -1
- package/dist/audit-table-CrPP2ZZ9.mjs.map +1 -0
- package/dist/index.d.mts +38 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/package.json +4 -8
- package/dist/schema-D9AdiYql.d.mts +0 -32
- package/dist/schema-D9AdiYql.d.mts.map +0 -1
- package/dist/schema-D9vWDK2q.mjs.map +0 -1
- package/dist/schema.d.mts +0 -2
- package/dist/schema.mjs +0 -1
package/dist/admin.d.mts
CHANGED
|
@@ -1,23 +1,54 @@
|
|
|
1
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
|
|
3
|
+
//#region src/audit-client.d.ts
|
|
4
|
+
interface AuditLogQueryOptions {
|
|
5
|
+
action?: string | undefined;
|
|
6
|
+
entityType?: string | undefined;
|
|
7
|
+
entityId?: string | undefined;
|
|
8
|
+
userId?: string | undefined;
|
|
9
|
+
dateFrom?: Date | undefined;
|
|
10
|
+
dateTo?: Date | undefined;
|
|
11
|
+
/** Free-text search across action, entityType, userName */
|
|
12
|
+
search?: string | undefined;
|
|
13
|
+
/** Default 50, max 100 */
|
|
14
|
+
limit?: number | undefined;
|
|
15
|
+
offset?: number | undefined;
|
|
16
|
+
sortField?: 'createdAt' | 'action' | 'entityType' | undefined;
|
|
17
|
+
sortDirection?: 'asc' | 'desc' | undefined;
|
|
18
|
+
}
|
|
19
|
+
interface AuditLogEntry {
|
|
20
|
+
id: string;
|
|
21
|
+
action: string;
|
|
22
|
+
entityType: string | null;
|
|
23
|
+
entityId: string | null;
|
|
24
|
+
userId: string | null;
|
|
25
|
+
userName: string | null;
|
|
26
|
+
changes: Record<string, unknown> | null;
|
|
27
|
+
metadata: Record<string, unknown> | null;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
}
|
|
30
|
+
interface AuditLogListResult {
|
|
31
|
+
items: AuditLogEntry[];
|
|
32
|
+
total: number;
|
|
33
|
+
}
|
|
34
|
+
declare class AuditLogClient {
|
|
35
|
+
private client;
|
|
36
|
+
constructor(db: PostgresJsDatabase);
|
|
37
|
+
/** Write a single audit log entry */
|
|
38
|
+
write(entry: Omit<AuditLogEntry, 'id' | 'createdAt'>): Promise<void>;
|
|
39
|
+
/** Query audit logs with filters and pagination */
|
|
40
|
+
query(options?: AuditLogQueryOptions): Promise<AuditLogListResult>;
|
|
41
|
+
/** Get a single audit log entry by ID */
|
|
42
|
+
findById(id: string): Promise<AuditLogEntry | null>;
|
|
43
|
+
/** Get distinct entity types present in the audit log */
|
|
44
|
+
getDistinctEntityTypes(): Promise<string[]>;
|
|
45
|
+
/** Get distinct actions present in the audit log */
|
|
46
|
+
getDistinctActions(): Promise<string[]>;
|
|
47
|
+
private buildWhere;
|
|
48
|
+
private buildOrderBy;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
1
51
|
//#region src/admin/routes.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Audit log admin routes for the centralized admin API handler.
|
|
4
|
-
*
|
|
5
|
-
* Read-only: only GET handlers. Audit logs are immutable.
|
|
6
|
-
* Admin-only: explicit role check before any data access.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```typescript
|
|
10
|
-
* import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
|
|
11
|
-
* import { logRoutes } from '@murumets-ee/logging/admin'
|
|
12
|
-
* import { AuditLogClient } from '@murumets-ee/logging'
|
|
13
|
-
*
|
|
14
|
-
* const handler = createAdminApiHandler({
|
|
15
|
-
* authenticate: async (req) => { ... },
|
|
16
|
-
* entities: [...],
|
|
17
|
-
* routes: [logRoutes(() => new AuditLogClient(db))],
|
|
18
|
-
* })
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
52
|
interface AdminRoute {
|
|
22
53
|
prefix: string;
|
|
23
54
|
resource?: string;
|
|
@@ -31,16 +62,11 @@ interface AdminRoute {
|
|
|
31
62
|
checkPermission: (resource: string, action: string) => boolean;
|
|
32
63
|
}) => Promise<Response>>>;
|
|
33
64
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
findById(id: string): Promise<unknown | null>;
|
|
40
|
-
getDistinctEntityTypes(): Promise<string[]>;
|
|
41
|
-
getDistinctActions(): Promise<string[]>;
|
|
42
|
-
}
|
|
43
|
-
declare function logRoutes(getClient: () => AuditLogClientLike): AdminRoute;
|
|
65
|
+
/** Subset of `AuditLogClient` actually called by the route handlers. Using
|
|
66
|
+
* `Pick` (not a hand-written `*Like` interface) means new methods on the
|
|
67
|
+
* real client don't silently degrade these to `unknown`. */
|
|
68
|
+
type AuditLogClientForRoutes = Pick<AuditLogClient, 'query' | 'findById' | 'getDistinctEntityTypes' | 'getDistinctActions'>;
|
|
69
|
+
declare function logRoutes(getClient: () => AuditLogClientForRoutes): AdminRoute;
|
|
44
70
|
//#endregion
|
|
45
71
|
export { logRoutes };
|
|
46
72
|
//# sourceMappingURL=admin.d.mts.map
|
package/dist/admin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.d.mts","names":[],"sources":["../src/admin/routes.ts"],"mappings":"
|
|
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"}
|
package/dist/admin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{z as e}from"zod";const t=/^[a-zA-Z0-9_-]{1,255}$/,n=e.object({action:e.string().max(100).optional(),entityType:e.string().max(100).optional(),entityId:e.string().regex(t,`Invalid ID format`).optional(),userId:e.string().regex(t,`Invalid user ID format`).optional(),dateFrom:e.string().datetime({offset:!0,message:`Invalid ISO date`}).optional(),dateTo:e.string().datetime({offset:!0,message:`Invalid ISO date`}).optional(),search:e.string().max(200).optional(),limit:e.coerce.number().min(1).max(100).default(50),offset:e.coerce.number().min(0).default(0),sortField:e.enum([`createdAt`,`action`,`entityType`]).default(`createdAt`),sortDirection:e.enum([`asc`,`desc`]).default(`desc`)});function
|
|
1
|
+
import{z as e}from"zod";const t=/^[a-zA-Z0-9_-]{1,255}$/,n=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,r=e.object({action:e.string().max(100).optional(),entityType:e.string().max(100).optional(),entityId:e.string().regex(t,`Invalid ID format`).optional(),userId:e.string().regex(t,`Invalid user ID format`).optional(),dateFrom:e.string().datetime({offset:!0,message:`Invalid ISO date`}).optional(),dateTo:e.string().datetime({offset:!0,message:`Invalid ISO date`}).optional(),search:e.string().max(200).optional(),limit:e.coerce.number().min(1).max(100).default(50),offset:e.coerce.number().min(0).default(0),sortField:e.enum([`createdAt`,`action`,`entityType`]).default(`createdAt`),sortDirection:e.enum([`asc`,`desc`]).default(`desc`)});function i(e,t=200){return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":`application/json`}})}function a(e,t){return i({error:e},t)}function o(e){return{prefix:`logs`,resource:`audit-logs`,actions:[`view`],handlers:{GET:async(t,{segments:o,checkPermission:s})=>{if(!s(`audit-logs`,`view`))return a(`Forbidden`,403);let c=e(),l=o[0];if(o.length===1&&l===`filters`){let[e,t]=await Promise.all([c.getDistinctEntityTypes(),c.getDistinctActions()]);return i({entityTypes:e,actions:t})}if(o.length===1&&l!==void 0){if(!n.test(l))return a(`Audit log entry not found`,404);let e=await c.findById(l);return e?i(e):a(`Audit log entry not found`,404)}if(o.length>1)return a(`Not found`,404);let u=new URL(t.url),d=Object.fromEntries(u.searchParams),f=r.safeParse(d);if(!f.success)return a(`Invalid query params: ${f.error.issues.map(e=>e.message).join(`, `)}`,400);let{dateFrom:p,dateTo:m,...h}=f.data;return i(await c.query({...h,...p&&{dateFrom:new Date(p)},...m&&{dateTo:new Date(m)}}))}}}}export{o as logRoutes};
|
|
2
2
|
//# sourceMappingURL=admin.mjs.map
|
package/dist/admin.mjs.map
CHANGED
|
@@ -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\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\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//
|
|
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,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=
|
|
2
|
+
//# sourceMappingURL=audit-table-CrPP2ZZ9.mjs.map
|
|
@@ -0,0 +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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { t as toolkitAuditLogs } from "./schema-D9AdiYql.mjs";
|
|
2
1
|
import { Logger as Logger$1 } from "pino";
|
|
3
2
|
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
3
|
|
|
@@ -15,10 +14,6 @@ interface LoggerConfig {
|
|
|
15
14
|
* If config provided, returns a child logger with the specified context
|
|
16
15
|
*/
|
|
17
16
|
declare function createLogger(config?: LoggerConfig): Logger;
|
|
18
|
-
/**
|
|
19
|
-
* Create a child logger from an existing logger with additional context
|
|
20
|
-
*/
|
|
21
|
-
declare function createChildLogger(parent: Logger, context: Record<string, unknown>): Logger;
|
|
22
17
|
//#endregion
|
|
23
18
|
//#region src/audit.d.ts
|
|
24
19
|
interface AuditEntry {
|
|
@@ -31,8 +26,26 @@ interface AuditEntry {
|
|
|
31
26
|
metadata?: Record<string, unknown>;
|
|
32
27
|
}
|
|
33
28
|
interface AuditConfig {
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* The audit logger only ever calls `info` and `error` — narrowing the
|
|
31
|
+
* accepted shape avoids forcing tests and adapters to construct or fake
|
|
32
|
+
* the full ~30-method Pino surface.
|
|
33
|
+
*/
|
|
34
|
+
logger: Pick<Logger, 'info' | 'error'>;
|
|
35
35
|
dbWriter?: (entry: AuditEntry) => Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* REPLACE the deny-list of object keys whose values are replaced with
|
|
38
|
+
* `[REDACTED]` when found anywhere inside `changes` / `metadata` before
|
|
39
|
+
* the entry is logged or persisted. Matching is case-insensitive on the
|
|
40
|
+
* exact key name (no substring match — `foo.password` matches, `myPassword`
|
|
41
|
+
* does not).
|
|
42
|
+
*
|
|
43
|
+
* **Replace, not merge.** Passing `['ssn']` removes `password`/`token`/etc.
|
|
44
|
+
* from the deny-list; if you want to *add* keys while keeping defaults,
|
|
45
|
+
* spread them yourself: `redactKeys: [...DEFAULT_REDACT_KEYS, 'ssn']`
|
|
46
|
+
* (import `DEFAULT_REDACT_KEYS` if needed; or just inline the list).
|
|
47
|
+
*/
|
|
48
|
+
redactKeys?: readonly string[];
|
|
36
49
|
}
|
|
37
50
|
interface AuditLogger {
|
|
38
51
|
log: (entry: AuditEntry) => Promise<void>;
|
|
@@ -50,31 +63,37 @@ interface AuditLogger {
|
|
|
50
63
|
*/
|
|
51
64
|
declare function createAuditDbWriter(db: PostgresJsDatabase): (entry: AuditEntry) => Promise<void>;
|
|
52
65
|
/**
|
|
53
|
-
* Create an audit logger
|
|
66
|
+
* Create an audit logger.
|
|
54
67
|
*
|
|
55
68
|
* Audit logs are written to stdout immediately (non-blocking).
|
|
56
69
|
* Optionally, they can also be written to the database if dbWriter is provided.
|
|
57
70
|
*
|
|
58
71
|
* Database writes are fire-and-forget to avoid blocking the main operation.
|
|
59
72
|
* If DB write fails, it's logged to stderr but doesn't throw.
|
|
73
|
+
*
|
|
74
|
+
* **Redaction.** Both stdout and DB writes pass `changes` and `metadata`
|
|
75
|
+
* through a recursive key-based sanitizer (see `redactKeys`). Pino's own
|
|
76
|
+
* top-level `redact` config does not match nested keys, so callers must rely
|
|
77
|
+
* on this sanitizer for nested credentials. Defense-in-depth — callers should
|
|
78
|
+
* still avoid sticking secrets into audit payloads in the first place.
|
|
60
79
|
*/
|
|
61
80
|
declare function createAuditLogger(config: AuditConfig): AuditLogger;
|
|
62
81
|
//#endregion
|
|
63
82
|
//#region src/audit-client.d.ts
|
|
64
83
|
interface AuditLogQueryOptions {
|
|
65
|
-
action?: string;
|
|
66
|
-
entityType?: string;
|
|
67
|
-
entityId?: string;
|
|
68
|
-
userId?: string;
|
|
69
|
-
dateFrom?: Date;
|
|
70
|
-
dateTo?: Date;
|
|
84
|
+
action?: string | undefined;
|
|
85
|
+
entityType?: string | undefined;
|
|
86
|
+
entityId?: string | undefined;
|
|
87
|
+
userId?: string | undefined;
|
|
88
|
+
dateFrom?: Date | undefined;
|
|
89
|
+
dateTo?: Date | undefined;
|
|
71
90
|
/** Free-text search across action, entityType, userName */
|
|
72
|
-
search?: string;
|
|
91
|
+
search?: string | undefined;
|
|
73
92
|
/** Default 50, max 100 */
|
|
74
|
-
limit?: number;
|
|
75
|
-
offset?: number;
|
|
76
|
-
sortField?: 'createdAt' | 'action' | 'entityType';
|
|
77
|
-
sortDirection?: 'asc' | 'desc';
|
|
93
|
+
limit?: number | undefined;
|
|
94
|
+
offset?: number | undefined;
|
|
95
|
+
sortField?: 'createdAt' | 'action' | 'entityType' | undefined;
|
|
96
|
+
sortDirection?: 'asc' | 'desc' | undefined;
|
|
78
97
|
}
|
|
79
98
|
interface AuditLogEntry {
|
|
80
99
|
id: string;
|
|
@@ -108,18 +127,5 @@ declare class AuditLogClient {
|
|
|
108
127
|
private buildOrderBy;
|
|
109
128
|
}
|
|
110
129
|
//#endregion
|
|
111
|
-
|
|
112
|
-
interface RequestContext {
|
|
113
|
-
requestId?: string;
|
|
114
|
-
userId?: string;
|
|
115
|
-
teamId?: string;
|
|
116
|
-
locale?: string;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Bind request context to a logger
|
|
120
|
-
* Returns a child logger with the context automatically included in all log entries
|
|
121
|
-
*/
|
|
122
|
-
declare function bindRequestContext(logger: Logger, context: RequestContext): Logger;
|
|
123
|
-
//#endregion
|
|
124
|
-
export { type AuditConfig, type AuditEntry, AuditLogClient, type AuditLogEntry, type AuditLogListResult, type AuditLogQueryOptions, type AuditLogger, type Logger, type LoggerConfig, type RequestContext, bindRequestContext, createAuditDbWriter, createAuditLogger, createChildLogger, createLogger, toolkitAuditLogs };
|
|
130
|
+
export { type AuditConfig, type AuditEntry, AuditLogClient, type AuditLogEntry, type AuditLogListResult, type AuditLogQueryOptions, type AuditLogger, type Logger, type LoggerConfig, createAuditDbWriter, createAuditLogger, createLogger };
|
|
125
131
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/logger.ts","../src/audit.ts","../src/audit-client.ts"
|
|
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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
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};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/audit-client.ts","../src/audit.ts","../src/context.ts","../src/logger.ts"],"sourcesContent":["import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport type { TableClient, WhereClause } from '@murumets-ee/db'\nimport { auditLogTable } from './audit-table.js'\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface AuditLogQueryOptions {\n action?: string\n entityType?: string\n entityId?: string\n userId?: string\n dateFrom?: Date\n dateTo?: Date\n /** Free-text search across action, entityType, userName */\n search?: string\n /** Default 50, max 100 */\n limit?: number\n offset?: number\n sortField?: 'createdAt' | 'action' | 'entityType'\n sortDirection?: 'asc' | 'desc'\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, 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: combine into a single createdAt condition\n if (options.dateFrom || options.dateTo) {\n const dateOps: Record<string, unknown> = {}\n if (options.dateFrom) dateOps.gte = options.dateFrom\n if (options.dateTo) dateOps.lte = options.dateTo\n parts.push({ createdAt: dateOps } as AuditWhere)\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 case 'createdAt':\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 logger: Logger\n dbWriter?: (entry: AuditEntry) => Promise<void>\n}\n\nexport interface AuditLogger {\n log: (entry: AuditEntry) => Promise<void>\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 */\nexport function createAuditLogger(config: AuditConfig): AuditLogger {\n return {\n log: async (entry: AuditEntry) => {\n // Log to stdout immediately (structured log with audit: true marker)\n config.logger.info(\n {\n audit: true,\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 `Audit: ${entry.action}`,\n )\n\n // Write to DB if configured (fire and forget)\n if (config.dbWriter) {\n config.dbWriter(entry).catch((err) => {\n config.logger.error({ err, auditEntry: entry }, 'Failed to write audit log to database')\n })\n }\n },\n }\n}\n","import type { Logger } from './logger.js'\n\nexport interface RequestContext {\n requestId?: string\n userId?: string\n teamId?: string\n locale?: string\n}\n\n/**\n * Bind request context to a logger\n * Returns a child logger with the context automatically included in all log entries\n */\nexport function bindRequestContext(logger: Logger, context: RequestContext): Logger {\n return logger.child(context)\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\n/**\n * Create a child logger from an existing logger with additional context\n */\nexport function createChildLogger(parent: Logger, context: Record<string, unknown>): Logger {\n return parent.child(context)\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.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;UAKU,aAAA;EACR,IAAA;EACA,MAAA;IAEE,MAAA,EAAQ,MAAA,SAAe,OAAA;EAAA;AAAA;;;;;;;;AAkB3B;;;;;;;iBAAgB,OAAA,CAAA,GAAW,aAAA"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{n as e}from"./
|
|
1
|
+
import{n as e}from"./audit-table-CrPP2ZZ9.mjs";function t(){return{name:`@murumets-ee/logging`,server:{tables:{toolkitAuditLogs:e}}}}export{t as logging};
|
|
2
2
|
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["import type { PgTable } from 'drizzle-orm/pg-core'\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).\ninterface LoggingPlugin {\n name: string\n server: {\n // biome-ignore lint/suspicious/noExplicitAny: structural PgTable union from drizzle-orm — concrete row shape varies per table\n tables: Record<string, PgTable<any>>\n }\n}\n\n/**\n * Logging plugin — registers the `toolkit_audit_logs` table so it shows up\n * in generated migrations. Zero runtime behavior; the audit logger is wired\n * up separately via `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 */\nexport function logging(): LoggingPlugin {\n return {\n name: '@murumets-ee/logging',\n server: {\n tables: { toolkitAuditLogs },\n },\n }\n}\n"],"mappings":"+CA2BA,SAAgB,GAAyB,CACvC,MAAO,CACL,KAAM,uBACN,OAAQ,CACN,OAAQ,CAAE,mBAAkB,CAC7B,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murumets-ee/logging",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
"types": "./dist/index.d.mts",
|
|
9
9
|
"import": "./dist/index.mjs"
|
|
10
10
|
},
|
|
11
|
-
"./schema": {
|
|
12
|
-
"types": "./dist/schema.d.mts",
|
|
13
|
-
"import": "./dist/schema.mjs"
|
|
14
|
-
},
|
|
15
11
|
"./admin": {
|
|
16
12
|
"types": "./dist/admin.d.mts",
|
|
17
13
|
"import": "./dist/admin.mjs"
|
|
@@ -28,15 +24,15 @@
|
|
|
28
24
|
"drizzle-orm": "^0.45.2",
|
|
29
25
|
"pino": "^9.5.0",
|
|
30
26
|
"zod": "^3.24.1",
|
|
31
|
-
"@murumets-ee/db": "0.
|
|
27
|
+
"@murumets-ee/db": "0.12.0"
|
|
32
28
|
},
|
|
33
29
|
"devDependencies": {
|
|
34
|
-
"tsdown": "^0.21.
|
|
30
|
+
"tsdown": "^0.21.10",
|
|
35
31
|
"typescript": "^5.7.2",
|
|
36
32
|
"vitest": "^2.1.8"
|
|
37
33
|
},
|
|
38
34
|
"typeCoverage": {
|
|
39
|
-
"atLeast": 99.
|
|
35
|
+
"atLeast": 99.4
|
|
40
36
|
},
|
|
41
37
|
"scripts": {
|
|
42
38
|
"build": "tsdown",
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import * as _$drizzle_orm0 from "drizzle-orm";
|
|
2
|
-
import * as _$drizzle_orm_pg_core0 from "drizzle-orm/pg-core";
|
|
3
|
-
|
|
4
|
-
//#region src/audit-table.d.ts
|
|
5
|
-
/** Backward-compatible re-export — consumers importing `toolkitAuditLogs` see the same PgTable. */
|
|
6
|
-
declare const toolkitAuditLogs: _$drizzle_orm_pg_core0.PgTableWithColumns<{
|
|
7
|
-
name: string;
|
|
8
|
-
schema: undefined;
|
|
9
|
-
columns: {
|
|
10
|
-
[x: string]: _$drizzle_orm_pg_core0.PgColumn<{
|
|
11
|
-
name: string;
|
|
12
|
-
tableName: string;
|
|
13
|
-
dataType: _$drizzle_orm0.ColumnDataType;
|
|
14
|
-
columnType: string;
|
|
15
|
-
data: unknown;
|
|
16
|
-
driverParam: unknown;
|
|
17
|
-
notNull: false;
|
|
18
|
-
hasDefault: false;
|
|
19
|
-
isPrimaryKey: false;
|
|
20
|
-
isAutoincrement: false;
|
|
21
|
-
hasRuntimeDefault: false;
|
|
22
|
-
enumValues: string[] | undefined;
|
|
23
|
-
baseColumn: never;
|
|
24
|
-
identity: undefined;
|
|
25
|
-
generated: undefined;
|
|
26
|
-
}, {}, {}>;
|
|
27
|
-
};
|
|
28
|
-
dialect: "pg";
|
|
29
|
-
}>;
|
|
30
|
-
//#endregion
|
|
31
|
-
export { toolkitAuditLogs as t };
|
|
32
|
-
//# sourceMappingURL=schema-D9AdiYql.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-D9AdiYql.d.mts","names":[],"sources":["../src/audit-table.ts"],"mappings":";;;;;cAsCa,gBAAA,yBAAgB,kBAAA;;;;;;;gBAAsB,cAAA,CAAA,cAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-D9vWDK2q.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"}
|
package/dist/schema.d.mts
DELETED
package/dist/schema.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./schema-D9vWDK2q.mjs";export{e as toolkitAuditLogs};
|