@fonderie/audit 5.0.0 → 5.0.1

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/README.md CHANGED
@@ -30,7 +30,7 @@ import type { IAuditQuery, IAuditPageDTO } from '@fonderie/audit';
30
30
  You've shipped this plumbing before — auth, teams, billing, messaging —
31
31
  and the next project will ask for it again. Fonderie packages it once:
32
32
  plain TypeScript modules for
33
- [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
33
+ [`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
34
34
  PostgreSQL-backed, self-hosted, MIT. No external control plane, no
35
35
  per-seat anything. Register the modules you need; skip the ones you don't.
36
36
 
@@ -38,7 +38,7 @@ per-seat anything. Register the modules you need; skip the ones you don't.
38
38
  human-readable, workspace-scoped activity trail.
39
39
 
40
40
  Browse the whole set at
41
- [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
41
+ [fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
42
42
  [@fonderiejs](https://x.com/fonderiejs)
43
43
 
44
44
  ## License
package/brain/outcomes.md CHANGED
@@ -11,4 +11,4 @@ downloading tarballs.
11
11
 
12
12
  | Method | Path | Middleware chain (auth / validation / handler) |
13
13
  |---|---|---|
14
- | GET | `/audit` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const url = new URL(ctx.request.url); const params = url.searchParams; const limit = Math.min(Number(params.get('limit') ?? 50), 200); const type = params.get('type') ?? undefined; const actor = params.get('actorId') ?? undefined; const from = params.get('from') ? new Date(params.get('from')!) : undefined; const to = params.get('to') ? new Date(params.get('to')!) : undefined; const cursor = params.get('cursor') ?? undefined; const query: Parameters<AuditEventModel['list']>[0] = { workspaceId: ctx.workspace.id, limit: limit + 1, }; if (type) query.type = type; if (actor) query.actorId = actor; if (from) query.from = from; if (to) query.to = to; if (cursor) query.cursor = cursor; const events = await new AuditEventModel(store).list(query); // fetch one extra to determine if there's a next page const hasMore = events.length > limit; const page = hasMore ? events.slice(0, limit) : events; const lastEvent = page[page.length - 1]; const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null; return setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', { events: page.map(toAuditEventDTO), nextCursor, }); }` |
14
+ | GET | `/audit` | `requireAuth → async (ctx) => { if (!ctx.workspace) return setApiResponse( HTTP.UNPROCESSABLE, 'MISSING_WORKSPACE', 'Workspace context required', ); const url = new URL(ctx.request.url); const params = url.searchParams; const rawLimit = Number(params.get('limit') ?? 50); const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50; const type = params.get('type') ?? undefined; const actor = params.get('actorId') ?? undefined; const from = params.get('from') ? new Date(params.get('from')!) : undefined; const to = params.get('to') ? new Date(params.get('to')!) : undefined; const cursor = params.get('cursor') ?? undefined; const query: Parameters<AuditEventModel['list']>[0] = { workspaceId: ctx.workspace.id, limit, }; if (type) query.type = type; if (actor) query.actorId = actor; if (from) query.from = from; if (to) query.to = to; if (cursor) query.cursor = cursor; // The model over-fetches internally to detect a next page; the // cursor carries the row's created_at::text at full microsecond // precision so same-millisecond events can't be skipped. const { events, hasMore } = await new AuditEventModel(store).list(query); const lastEvent = events[events.length - 1]; const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null; return setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', { events: events.map(toAuditEventDTO), nextCursor, }); }` |
package/dist/index.cjs CHANGED
@@ -39,17 +39,21 @@ function toAuditEventDTO(e) {
39
39
  createdAt: e.createdAt.toISOString()
40
40
  };
41
41
  }
42
+ var CURSOR_TS_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}(:?\d{2})?)?$/;
43
+ var CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
42
44
  function encodeCursor(createdAt, id) {
43
- return Buffer.from(`${createdAt.toISOString()},${id}`).toString("base64");
45
+ const ts = typeof createdAt === "string" ? createdAt : createdAt.toISOString();
46
+ return Buffer.from(`${ts},${id}`).toString("base64");
44
47
  }
45
48
  function decodeCursor(cursor) {
49
+ if (cursor.length > 256) return null;
46
50
  try {
47
51
  const raw = Buffer.from(cursor, "base64").toString();
48
52
  const commaIdx = raw.indexOf(",");
49
53
  if (commaIdx === -1) return null;
50
54
  const createdAt = raw.slice(0, commaIdx);
51
55
  const id = raw.slice(commaIdx + 1);
52
- if (!id || isNaN(Date.parse(createdAt))) return null;
56
+ if (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;
53
57
  return { createdAt, id };
54
58
  } catch {
55
59
  return null;
@@ -63,6 +67,10 @@ var AuditEventModel = class {
63
67
  this.store = store;
64
68
  }
65
69
  store;
70
+ // `query.limit` is the CLIENT-facing page size. The +1 over-fetch that
71
+ // detects a next page happens inside this method — keeping it here means
72
+ // no outer clamp can shave it off (which previously made nextCursor
73
+ // unreachable at the max page size).
66
74
  async list(query) {
67
75
  const limit = Math.min(query.limit ?? 50, MAX_LIMIT);
68
76
  const params = [query.workspaceId];
@@ -92,15 +100,17 @@ var AuditEventModel = class {
92
100
  );
93
101
  }
94
102
  }
95
- params.push(limit);
96
- return this.store.query(
97
- `SELECT id, type, payload, meta, created_at as "createdAt"
103
+ params.push(limit + 1);
104
+ const rows = await this.store.query(
105
+ `SELECT id, type, payload, meta, created_at as "createdAt",
106
+ created_at::text as "createdAtRaw"
98
107
  FROM fonderie_events
99
108
  WHERE ${where.join(" AND ")}
100
109
  ORDER BY created_at DESC, id DESC
101
110
  LIMIT $${params.length}`,
102
111
  params
103
112
  );
113
+ return { events: rows.slice(0, limit), hasMore: rows.length > limit };
104
114
  }
105
115
  };
106
116
 
@@ -120,7 +130,8 @@ function buildAuditRoutes(store) {
120
130
  );
121
131
  const url = new URL(ctx.request.url);
122
132
  const params = url.searchParams;
123
- const limit = Math.min(Number(params.get("limit") ?? 50), 200);
133
+ const rawLimit = Number(params.get("limit") ?? 50);
134
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
124
135
  const type = params.get("type") ?? void 0;
125
136
  const actor = params.get("actorId") ?? void 0;
126
137
  const from = params.get("from") ? new Date(params.get("from")) : void 0;
@@ -128,20 +139,18 @@ function buildAuditRoutes(store) {
128
139
  const cursor = params.get("cursor") ?? void 0;
129
140
  const query = {
130
141
  workspaceId: ctx.workspace.id,
131
- limit: limit + 1
142
+ limit
132
143
  };
133
144
  if (type) query.type = type;
134
145
  if (actor) query.actorId = actor;
135
146
  if (from) query.from = from;
136
147
  if (to) query.to = to;
137
148
  if (cursor) query.cursor = cursor;
138
- const events = await new AuditEventModel(store).list(query);
139
- const hasMore = events.length > limit;
140
- const page = hasMore ? events.slice(0, limit) : events;
141
- const lastEvent = page[page.length - 1];
142
- const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;
149
+ const { events, hasMore } = await new AuditEventModel(store).list(query);
150
+ const lastEvent = events[events.length - 1];
151
+ const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null;
143
152
  return (0, import_core.setApiResponse)(import_core.HTTP.OK, "AUDIT_FETCHED", "Audit events retrieved.", {
144
- events: page.map(toAuditEventDTO),
153
+ events: events.map(toAuditEventDTO),
145
154
  nextCursor
146
155
  });
147
156
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["export { AuditModule } from './module';\nexport type { IAuditEvent, IAuditQuery } from './types';\nexport type { IAuditEventDTO, IAuditPageDTO } from './dtos/audit';\n","import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst limit = Math.min(Number(params.get('limit') ?? 50), 200);\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\tconst events = await new AuditEventModel(store).list(query);\n\n\t\t\t\t// fetch one extra to determine if there's a next page\n\t\t\t\tconst hasMore = events.length > limit;\n\t\t\t\tconst page = hasMore ? events.slice(0, limit) : events;\n\t\t\t\tconst lastEvent = page[page.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: page.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function encodeCursor(createdAt: Date, id: string): string {\n\treturn Buffer.from(`${createdAt.toISOString()},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\tif (!id || isNaN(Date.parse(createdAt))) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync list(query: IAuditQuery): Promise<IAuditEvent[]> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit);\n\n\t\treturn this.store.query<IAuditEvent>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["export { AuditModule } from './module';\nexport type { IAuditEvent, IAuditQuery } from './types';\nexport type { IAuditEventDTO, IAuditPageDTO } from './dtos/audit';\n","import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\t// The model over-fetches internally to detect a next page; the\n\t\t\t\t// cursor carries the row's created_at::text at full microsecond\n\t\t\t\t// precision so same-millisecond events can't be skipped.\n\t\t\t\tconst { events, hasMore } = await new AuditEventModel(store).list(query);\n\t\t\t\tconst lastEvent = events[events.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent\n\t\t\t\t\t\t? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id)\n\t\t\t\t\t\t: null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: events.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\n// Cursors accept ISO timestamps and Postgres' own text format (microsecond\n// precision, e.g. '2026-09-05 00:38:31.123456+00') — the keyset carries the\n// latter, because round-tripping through a millisecond JS Date truncates\n// sub-millisecond digits and silently skips same-millisecond events (rows\n// created in one transaction all share now() exactly) between pages.\nconst CURSOR_TS_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function encodeCursor(createdAt: Date | string, id: string): string {\n\tconst ts = typeof createdAt === 'string' ? createdAt : createdAt.toISOString();\n\treturn Buffer.from(`${ts},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\t// Both halves feed ::timestamptz / ::uuid casts — validate here so a\n\t\t// crafted cursor yields an empty page, not a Postgres cast error.\n\t\tif (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport type IAuditEventRow = IAuditEvent & {\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make\n\t// the cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n};\n\nexport interface IAuditEventPage {\n\tevents: IAuditEventRow[];\n\thasMore: boolean;\n}\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\t// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that\n\t// detects a next page happens inside this method — keeping it here means\n\t// no outer clamp can shave it off (which previously made nextCursor\n\t// unreachable at the max page size).\n\tasync list(query: IAuditQuery): Promise<IAuditEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit + 1);\n\n\t\tconst rows = await this.store.query<IAuditEventRow>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\",\n\t\t\t created_at::text as \"createdAtRaw\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -13,17 +13,21 @@ function toAuditEventDTO(e) {
13
13
  createdAt: e.createdAt.toISOString()
14
14
  };
15
15
  }
16
+ var CURSOR_TS_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d{1,6})?(Z|[+-]\d{2}(:?\d{2})?)?$/;
17
+ var CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
16
18
  function encodeCursor(createdAt, id) {
17
- return Buffer.from(`${createdAt.toISOString()},${id}`).toString("base64");
19
+ const ts = typeof createdAt === "string" ? createdAt : createdAt.toISOString();
20
+ return Buffer.from(`${ts},${id}`).toString("base64");
18
21
  }
19
22
  function decodeCursor(cursor) {
23
+ if (cursor.length > 256) return null;
20
24
  try {
21
25
  const raw = Buffer.from(cursor, "base64").toString();
22
26
  const commaIdx = raw.indexOf(",");
23
27
  if (commaIdx === -1) return null;
24
28
  const createdAt = raw.slice(0, commaIdx);
25
29
  const id = raw.slice(commaIdx + 1);
26
- if (!id || isNaN(Date.parse(createdAt))) return null;
30
+ if (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;
27
31
  return { createdAt, id };
28
32
  } catch {
29
33
  return null;
@@ -37,6 +41,10 @@ var AuditEventModel = class {
37
41
  this.store = store;
38
42
  }
39
43
  store;
44
+ // `query.limit` is the CLIENT-facing page size. The +1 over-fetch that
45
+ // detects a next page happens inside this method — keeping it here means
46
+ // no outer clamp can shave it off (which previously made nextCursor
47
+ // unreachable at the max page size).
40
48
  async list(query) {
41
49
  const limit = Math.min(query.limit ?? 50, MAX_LIMIT);
42
50
  const params = [query.workspaceId];
@@ -66,15 +74,17 @@ var AuditEventModel = class {
66
74
  );
67
75
  }
68
76
  }
69
- params.push(limit);
70
- return this.store.query(
71
- `SELECT id, type, payload, meta, created_at as "createdAt"
77
+ params.push(limit + 1);
78
+ const rows = await this.store.query(
79
+ `SELECT id, type, payload, meta, created_at as "createdAt",
80
+ created_at::text as "createdAtRaw"
72
81
  FROM fonderie_events
73
82
  WHERE ${where.join(" AND ")}
74
83
  ORDER BY created_at DESC, id DESC
75
84
  LIMIT $${params.length}`,
76
85
  params
77
86
  );
87
+ return { events: rows.slice(0, limit), hasMore: rows.length > limit };
78
88
  }
79
89
  };
80
90
 
@@ -94,7 +104,8 @@ function buildAuditRoutes(store) {
94
104
  );
95
105
  const url = new URL(ctx.request.url);
96
106
  const params = url.searchParams;
97
- const limit = Math.min(Number(params.get("limit") ?? 50), 200);
107
+ const rawLimit = Number(params.get("limit") ?? 50);
108
+ const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), 200) : 50;
98
109
  const type = params.get("type") ?? void 0;
99
110
  const actor = params.get("actorId") ?? void 0;
100
111
  const from = params.get("from") ? new Date(params.get("from")) : void 0;
@@ -102,20 +113,18 @@ function buildAuditRoutes(store) {
102
113
  const cursor = params.get("cursor") ?? void 0;
103
114
  const query = {
104
115
  workspaceId: ctx.workspace.id,
105
- limit: limit + 1
116
+ limit
106
117
  };
107
118
  if (type) query.type = type;
108
119
  if (actor) query.actorId = actor;
109
120
  if (from) query.from = from;
110
121
  if (to) query.to = to;
111
122
  if (cursor) query.cursor = cursor;
112
- const events = await new AuditEventModel(store).list(query);
113
- const hasMore = events.length > limit;
114
- const page = hasMore ? events.slice(0, limit) : events;
115
- const lastEvent = page[page.length - 1];
116
- const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;
123
+ const { events, hasMore } = await new AuditEventModel(store).list(query);
124
+ const lastEvent = events[events.length - 1];
125
+ const nextCursor = hasMore && lastEvent ? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id) : null;
117
126
  return setApiResponse(HTTP.OK, "AUDIT_FETCHED", "Audit events retrieved.", {
118
- events: page.map(toAuditEventDTO),
127
+ events: events.map(toAuditEventDTO),
119
128
  nextCursor
120
129
  });
121
130
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst limit = Math.min(Number(params.get('limit') ?? 50), 200);\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit: limit + 1,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\tconst events = await new AuditEventModel(store).list(query);\n\n\t\t\t\t// fetch one extra to determine if there's a next page\n\t\t\t\tconst hasMore = events.length > limit;\n\t\t\t\tconst page = hasMore ? events.slice(0, limit) : events;\n\t\t\t\tconst lastEvent = page[page.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent ? encodeCursor(lastEvent.createdAt, lastEvent.id) : null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: page.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\nexport function encodeCursor(createdAt: Date, id: string): string {\n\treturn Buffer.from(`${createdAt.toISOString()},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\tif (!id || isNaN(Date.parse(createdAt))) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tasync list(query: IAuditQuery): Promise<IAuditEvent[]> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit);\n\n\t\treturn this.store.query<IAuditEvent>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/routes.ts","../src/dtos/audit.ts","../src/models/event.model.ts","../src/module.ts"],"sourcesContent":["import type { Middleware } from '@fonderie/core';\nimport { setApiResponse, HTTP } from '@fonderie/core';\nimport { requireAuth } from '@fonderie/core/middlewares';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { AuditEventModel } from './models/event.model';\nimport { toAuditEventDTO, encodeCursor } from './dtos/audit';\n\ntype Route = [string, string, ...Middleware[]];\n\nexport function buildAuditRoutes(store: IStoreAdapter): Route[] {\n\treturn [\n\t\t[\n\t\t\t'GET',\n\t\t\t'/audit',\n\t\t\trequireAuth,\n\t\t\tasync (ctx) => {\n\t\t\t\tif (!ctx.workspace)\n\t\t\t\t\treturn setApiResponse(\n\t\t\t\t\t\tHTTP.UNPROCESSABLE,\n\t\t\t\t\t\t'MISSING_WORKSPACE',\n\t\t\t\t\t\t'Workspace context required',\n\t\t\t\t\t);\n\n\t\t\t\tconst url = new URL(ctx.request.url);\n\t\t\t\tconst params = url.searchParams;\n\n\t\t\t\tconst rawLimit = Number(params.get('limit') ?? 50);\n\t\t\t\tconst limit = Number.isFinite(rawLimit)\n\t\t\t\t\t? Math.min(Math.max(Math.trunc(rawLimit), 1), 200)\n\t\t\t\t\t: 50;\n\t\t\t\tconst type = params.get('type') ?? undefined;\n\t\t\t\tconst actor = params.get('actorId') ?? undefined;\n\t\t\t\tconst from = params.get('from') ? new Date(params.get('from')!) : undefined;\n\t\t\t\tconst to = params.get('to') ? new Date(params.get('to')!) : undefined;\n\t\t\t\tconst cursor = params.get('cursor') ?? undefined;\n\n\t\t\t\tconst query: Parameters<AuditEventModel['list']>[0] = {\n\t\t\t\t\tworkspaceId: ctx.workspace.id,\n\t\t\t\t\tlimit,\n\t\t\t\t};\n\t\t\t\tif (type) query.type = type;\n\t\t\t\tif (actor) query.actorId = actor;\n\t\t\t\tif (from) query.from = from;\n\t\t\t\tif (to) query.to = to;\n\t\t\t\tif (cursor) query.cursor = cursor;\n\n\t\t\t\t// The model over-fetches internally to detect a next page; the\n\t\t\t\t// cursor carries the row's created_at::text at full microsecond\n\t\t\t\t// precision so same-millisecond events can't be skipped.\n\t\t\t\tconst { events, hasMore } = await new AuditEventModel(store).list(query);\n\t\t\t\tconst lastEvent = events[events.length - 1];\n\t\t\t\tconst nextCursor =\n\t\t\t\t\thasMore && lastEvent\n\t\t\t\t\t\t? encodeCursor(lastEvent.createdAtRaw ?? lastEvent.createdAt, lastEvent.id)\n\t\t\t\t\t\t: null;\n\n\t\t\t\treturn setApiResponse(HTTP.OK, 'AUDIT_FETCHED', 'Audit events retrieved.', {\n\t\t\t\t\tevents: events.map(toAuditEventDTO),\n\t\t\t\t\tnextCursor,\n\t\t\t\t});\n\t\t\t},\n\t\t],\n\t];\n}\n","import type { IAuditEvent } from '../types';\n\nexport interface IAuditEventDTO {\n\tid: string;\n\ttype: string;\n\tactorId: string | null;\n\trequestId: string | null;\n\tpayload: Record<string, unknown>;\n\tcreatedAt: string;\n}\n\nexport interface IAuditPageDTO {\n\tevents: IAuditEventDTO[];\n\tnextCursor: string | null;\n}\n\nexport function toAuditEventDTO(e: IAuditEvent): IAuditEventDTO {\n\treturn {\n\t\tid: e.id,\n\t\ttype: e.type,\n\t\tactorId: (e.payload['userId'] as string | undefined) ?? null,\n\t\trequestId: (e.meta['requestId'] as string | undefined) ?? null,\n\t\tpayload: e.payload,\n\t\tcreatedAt: e.createdAt.toISOString(),\n\t};\n}\n\n// Cursors accept ISO timestamps and Postgres' own text format (microsecond\n// precision, e.g. '2026-09-05 00:38:31.123456+00') — the keyset carries the\n// latter, because round-tripping through a millisecond JS Date truncates\n// sub-millisecond digits and silently skips same-millisecond events (rows\n// created in one transaction all share now() exactly) between pages.\nconst CURSOR_TS_RE = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,6})?(Z|[+-]\\d{2}(:?\\d{2})?)?$/;\nconst CURSOR_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport function encodeCursor(createdAt: Date | string, id: string): string {\n\tconst ts = typeof createdAt === 'string' ? createdAt : createdAt.toISOString();\n\treturn Buffer.from(`${ts},${id}`).toString('base64');\n}\n\nexport function decodeCursor(cursor: string): { createdAt: string; id: string } | null {\n\tif (cursor.length > 256) return null;\n\ttry {\n\t\tconst raw = Buffer.from(cursor, 'base64').toString();\n\t\tconst commaIdx = raw.indexOf(',');\n\t\tif (commaIdx === -1) return null;\n\t\tconst createdAt = raw.slice(0, commaIdx);\n\t\tconst id = raw.slice(commaIdx + 1);\n\t\t// Both halves feed ::timestamptz / ::uuid casts — validate here so a\n\t\t// crafted cursor yields an empty page, not a Postgres cast error.\n\t\tif (!CURSOR_TS_RE.test(createdAt) || !CURSOR_ID_RE.test(id)) return null;\n\t\treturn { createdAt, id };\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuditEvent, IAuditQuery } from '../types';\nimport { decodeCursor } from '../dtos/audit';\n\nconst MAX_LIMIT = 200;\n\nexport type IAuditEventRow = IAuditEvent & {\n\t// created_at::text — full microsecond precision for the keyset cursor\n\t// (node-pg parses timestamptz into a millisecond Date, which would make\n\t// the cursor skip same-millisecond rows between pages).\n\tcreatedAtRaw: string;\n};\n\nexport interface IAuditEventPage {\n\tevents: IAuditEventRow[];\n\thasMore: boolean;\n}\n\nexport class AuditEventModel {\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\t// `query.limit` is the CLIENT-facing page size. The +1 over-fetch that\n\t// detects a next page happens inside this method — keeping it here means\n\t// no outer clamp can shave it off (which previously made nextCursor\n\t// unreachable at the max page size).\n\tasync list(query: IAuditQuery): Promise<IAuditEventPage> {\n\t\tconst limit = Math.min(query.limit ?? 50, MAX_LIMIT);\n\t\tconst params: unknown[] = [query.workspaceId];\n\t\tconst where: string[] = [`payload->>'workspaceId' = $1`];\n\n\t\tif (query.type) {\n\t\t\tparams.push(query.type);\n\t\t\twhere.push(`type = $${params.length}`);\n\t\t}\n\n\t\tif (query.actorId) {\n\t\t\tparams.push(query.actorId);\n\t\t\twhere.push(`payload->>'userId' = $${params.length}`);\n\t\t}\n\n\t\tif (query.from) {\n\t\t\tparams.push(query.from);\n\t\t\twhere.push(`created_at >= $${params.length}`);\n\t\t}\n\n\t\tif (query.to) {\n\t\t\tparams.push(query.to);\n\t\t\twhere.push(`created_at <= $${params.length}`);\n\t\t}\n\n\t\tif (query.cursor) {\n\t\t\tconst decoded = decodeCursor(query.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tparams.push(decoded.createdAt, decoded.id);\n\t\t\t\twhere.push(\n\t\t\t\t\t`(created_at, id) < ($${params.length - 1}::timestamptz, $${params.length}::uuid)`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tparams.push(limit + 1);\n\n\t\tconst rows = await this.store.query<IAuditEventRow>(\n\t\t\t`SELECT id, type, payload, meta, created_at as \"createdAt\",\n\t\t\t created_at::text as \"createdAtRaw\"\n\t\t\t FROM fonderie_events\n\t\t\t WHERE ${where.join(' AND ')}\n\t\t\t ORDER BY created_at DESC, id DESC\n\t\t\t LIMIT $${params.length}`,\n\t\t\tparams,\n\t\t);\n\n\t\treturn { events: rows.slice(0, limit), hasMore: rows.length > limit };\n\t}\n}\n","import type { IFonderieModule, IFonderieApp } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport { buildAuditRoutes } from './routes';\n\nexport class AuditModule implements IFonderieModule {\n\treadonly name = '@fonderie/audit';\n\treadonly deps = ['@fonderie/auth', '@fonderie/workspaces'];\n\n\tconstructor(private readonly store: IStoreAdapter) {}\n\n\tinstall(app: IFonderieApp): void {\n\t\tfor (const [method, path, ...handlers] of buildAuditRoutes(this.store)) {\n\t\t\tapp.addRoute(method, path, ...handlers);\n\t\t}\n\t}\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@fonderie/audit",
3
- "version": "5.0.0",
3
+ "version": "5.0.1",
4
4
  "description": "Workspace-scoped audit log — query fonderie_events for a human-readable activity trail.",
5
5
  "keywords": [
6
- "fonderie-js",
6
+ "fonderiejs",
7
7
  "audit",
8
8
  "events",
9
9
  "saas",
@@ -40,9 +40,9 @@
40
40
  "devDependencies": {
41
41
  "@fonderie/core": "../core",
42
42
  "@fonderie/store": "../store",
43
- "@types/node": "^25.6.0",
43
+ "@types/node": "^26.4.0",
44
44
  "tsup": "^8.5.1",
45
- "tsx": "^4.21.0",
45
+ "tsx": "^4.23.12",
46
46
  "typescript": "^6.0.3"
47
47
  },
48
48
  "publishConfig": {
@@ -56,11 +56,11 @@
56
56
  ],
57
57
  "repository": {
58
58
  "type": "git",
59
- "url": "git+https://github.com/fonderiejs/sdk.git",
59
+ "url": "git+https://github.com/fonderiejs/fonderie.git",
60
60
  "directory": "packages/audit"
61
61
  },
62
- "homepage": "https://github.com/fonderiejs/sdk/tree/main/packages/audit#readme",
62
+ "homepage": "https://github.com/fonderiejs/fonderie/tree/main/packages/audit#readme",
63
63
  "bugs": {
64
- "url": "https://github.com/fonderiejs/sdk/issues"
64
+ "url": "https://github.com/fonderiejs/fonderie/issues"
65
65
  }
66
66
  }