@objectstack/plugin-audit 4.0.4 → 4.1.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/index.mjs CHANGED
@@ -1,98 +1,267 @@
1
- // src/objects/sys-audit-log.object.ts
2
- import { ObjectSchema, Field } from "@objectstack/spec/data";
3
- var SysAuditLog = ObjectSchema.create({
4
- namespace: "sys",
5
- name: "audit_log",
6
- label: "Audit Log",
7
- pluralLabel: "Audit Logs",
8
- icon: "scroll-text",
9
- isSystem: true,
10
- description: "Immutable audit trail for platform events",
11
- titleFormat: "{action} on {object_name} by {user_id}",
12
- compactLayout: ["action", "object_name", "user_id", "created_at"],
13
- fields: {
14
- id: Field.text({
15
- label: "Audit Log ID",
16
- required: true,
17
- readonly: true
18
- }),
19
- created_at: Field.datetime({
20
- label: "Timestamp",
21
- required: true,
22
- defaultValue: "NOW()",
23
- readonly: true
24
- }),
25
- user_id: Field.text({
26
- label: "User ID",
27
- required: false,
28
- description: "User who performed the action (null for system actions)"
29
- }),
30
- action: Field.select(["create", "update", "delete", "restore", "login", "logout", "permission_change", "config_change", "export", "import"], {
31
- label: "Action",
32
- required: true,
33
- description: "Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import"
34
- }),
35
- object_name: Field.text({
36
- label: "Object Name",
37
- required: false,
38
- maxLength: 255,
39
- description: "Target object (e.g. sys_user, project_task)"
40
- }),
41
- record_id: Field.text({
42
- label: "Record ID",
43
- required: false,
44
- description: "ID of the affected record"
45
- }),
46
- old_value: Field.textarea({
47
- label: "Old Value",
48
- required: false,
49
- description: "JSON-serialized previous state"
50
- }),
51
- new_value: Field.textarea({
52
- label: "New Value",
53
- required: false,
54
- description: "JSON-serialized new state"
55
- }),
56
- ip_address: Field.text({
57
- label: "IP Address",
58
- required: false,
59
- maxLength: 45
60
- }),
61
- user_agent: Field.textarea({
62
- label: "User Agent",
63
- required: false
64
- }),
65
- tenant_id: Field.text({
66
- label: "Tenant ID",
67
- required: false,
68
- description: "Tenant context for multi-tenant isolation"
69
- }),
70
- metadata: Field.textarea({
71
- label: "Metadata",
72
- required: false,
73
- description: "JSON-serialized additional context"
74
- })
75
- },
76
- indexes: [
77
- { fields: ["created_at"] },
78
- { fields: ["user_id"] },
79
- { fields: ["object_name", "record_id"] },
80
- { fields: ["action"] },
81
- { fields: ["tenant_id"] }
82
- ],
83
- enable: {
84
- trackHistory: false,
85
- // Audit logs are themselves the audit trail
86
- searchable: true,
87
- apiEnabled: true,
88
- apiMethods: ["get", "list"],
89
- // Read-only audit logs are immutable; creation happens via internal system hooks only
90
- trash: false,
91
- // Never soft-delete audit logs
92
- mru: false,
93
- clone: false
1
+ // src/audit-plugin.ts
2
+ import { SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification } from "@objectstack/platform-objects/audit";
3
+
4
+ // src/audit-writers.ts
5
+ var SKIP_OBJECTS = /* @__PURE__ */ new Set([
6
+ "sys_audit_log",
7
+ "sys_activity",
8
+ "sys_comment",
9
+ "sys_session",
10
+ "sys_presence",
11
+ "sys_account",
12
+ "sys_account_session",
13
+ "sys_account_verification",
14
+ "sys_account_account"
15
+ ]);
16
+ var NOISE_FIELDS = /* @__PURE__ */ new Set([
17
+ "updated_at",
18
+ "updated_by",
19
+ "created_at",
20
+ "created_by"
21
+ ]);
22
+ function actionFor(event) {
23
+ if (event === "afterInsert") return "create";
24
+ if (event === "afterUpdate") return "update";
25
+ if (event === "afterDelete") return "delete";
26
+ return null;
27
+ }
28
+ function activityTypeFor(action) {
29
+ return action === "create" ? "created" : action === "update" ? "updated" : "deleted";
30
+ }
31
+ function recordLabel(record, id) {
32
+ if (!record || typeof record !== "object") return id;
33
+ const candidates = ["name", "subject", "title", "full_name", "label", "first_name", "company", "email"];
34
+ for (const k of candidates) {
35
+ const v = record[k];
36
+ if (typeof v === "string" && v.trim()) return v.trim();
37
+ }
38
+ return id;
39
+ }
40
+ function diff(before, after) {
41
+ const oldOut = {};
42
+ const newOut = {};
43
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})]);
44
+ for (const k of keys) {
45
+ if (NOISE_FIELDS.has(k)) continue;
46
+ const b = before?.[k];
47
+ const a = after?.[k];
48
+ if (safeStringify(b) !== safeStringify(a)) {
49
+ oldOut[k] = b ?? null;
50
+ newOut[k] = a ?? null;
51
+ }
52
+ }
53
+ return { old: oldOut, next: newOut };
54
+ }
55
+ function safeStringify(v) {
56
+ try {
57
+ return JSON.stringify(v);
58
+ } catch {
59
+ return String(v);
60
+ }
61
+ }
62
+ function installAuditWriters(engine, packageId = "com.objectstack.audit") {
63
+ if (!engine || typeof engine.registerHook !== "function") return;
64
+ if (typeof engine.unregisterHooksByPackage === "function") {
65
+ engine.unregisterHooksByPackage(packageId);
66
+ }
67
+ const captureBefore = async (ctx) => {
68
+ if (SKIP_OBJECTS.has(ctx.object)) return;
69
+ const id = ctx.input?.id;
70
+ if (!id) return;
71
+ try {
72
+ const trx = ctx.transaction;
73
+ const ql = ctx.ql ?? ctx.api?.engine;
74
+ if (ql?.findOne) {
75
+ const prev2 = await ql.findOne(ctx.object, {
76
+ where: { id },
77
+ context: { isSystem: true, ...trx ? { transaction: trx } : {} }
78
+ });
79
+ if (prev2) ctx.__previous = prev2;
80
+ return;
81
+ }
82
+ const api = ctx.api;
83
+ if (!api?.sudo) return;
84
+ const prev = await api.sudo().object(ctx.object).findOne({ where: { id } });
85
+ if (prev) ctx.__previous = prev;
86
+ } catch {
87
+ }
88
+ };
89
+ engine.registerHook("beforeUpdate", captureBefore, { packageId });
90
+ engine.registerHook("beforeDelete", captureBefore, { packageId });
91
+ const writeAudit = async (ctx) => {
92
+ if (SKIP_OBJECTS.has(ctx.object)) return;
93
+ const action = actionFor(ctx.event);
94
+ if (!action) return;
95
+ const api = ctx.api;
96
+ if (!api?.sudo) return;
97
+ const after = ctx.result;
98
+ const before = ctx.__previous ?? ctx.previous ?? null;
99
+ let recordId = typeof after === "object" && after?.id || typeof before === "object" && before?.id || ctx.input?.id;
100
+ if (recordId !== void 0) recordId = String(recordId);
101
+ const sess = ctx.session ?? {};
102
+ const userId = sess.userId;
103
+ const recordOrgId = typeof ctx.result?.organization_id === "string" && ctx.result.organization_id || typeof ctx.__previous?.organization_id === "string" && ctx.__previous.organization_id || void 0;
104
+ const tenantId = sess.tenantId ?? recordOrgId;
105
+ let oldValue = null;
106
+ let newValue = null;
107
+ if (action === "create") {
108
+ newValue = after && typeof after === "object" ? { ...after } : null;
109
+ } else if (action === "update") {
110
+ const d = diff(before || {}, after || {});
111
+ oldValue = d.old;
112
+ newValue = d.next;
113
+ if (Object.keys(newValue).length === 0) return;
114
+ } else if (action === "delete") {
115
+ oldValue = before && typeof before === "object" ? { ...before } : null;
116
+ }
117
+ const auditRow = {
118
+ action,
119
+ user_id: userId ?? null,
120
+ object_name: ctx.object,
121
+ record_id: recordId ?? null,
122
+ old_value: oldValue ? safeStringify(oldValue) : null,
123
+ new_value: newValue ? safeStringify(newValue) : null,
124
+ // `tenant_id` is the schema-declared "tenant context" lookup; the
125
+ // platform-default `organization_id` column is what RLS gates on
126
+ // (`organization_id = current_user.organization_id`). The audit
127
+ // writer runs through `api.sudo()` which bypasses the
128
+ // SecurityPlugin's auto-stamping of `organization_id`, so we
129
+ // stamp both columns explicitly here. Without `organization_id`,
130
+ // non-admin members would see 0 rows on Setup dashboards because
131
+ // RLS would deny every audit row as wrong-tenant.
132
+ organization_id: tenantId ?? null,
133
+ tenant_id: tenantId ?? null
134
+ };
135
+ const label = recordLabel(after ?? before, recordId ?? "");
136
+ const summary = action === "create" ? `Created ${ctx.object} "${label}"` : action === "update" ? `Updated ${ctx.object} "${label}"` : `Deleted ${ctx.object} "${label}"`;
137
+ const activityRow = {
138
+ type: activityTypeFor(action),
139
+ // Explicit ISO timestamp — `defaultValue: 'NOW()'` on the column
140
+ // isn't resolved by every driver and would otherwise leak the
141
+ // literal string "NOW()" into the row.
142
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
143
+ summary,
144
+ actor_id: userId ?? null,
145
+ object_name: ctx.object,
146
+ record_id: recordId ?? null,
147
+ record_label: label,
148
+ metadata: newValue || oldValue ? safeStringify({ old: oldValue, new: newValue }) : null,
149
+ // Same rationale as auditRow: stamp the tenant column so RLS
150
+ // matches the recipient's organization on read.
151
+ organization_id: tenantId ?? null
152
+ };
153
+ try {
154
+ const sys = api.sudo();
155
+ await sys.object("sys_audit_log").create(auditRow);
156
+ await sys.object("sys_activity").create(activityRow);
157
+ await writeAssignmentNotifications(sys, {
158
+ object: ctx.object,
159
+ recordId: recordId ?? null,
160
+ label,
161
+ action,
162
+ before,
163
+ after,
164
+ actorId: userId ?? null,
165
+ tenantId: tenantId ?? null
166
+ });
167
+ } catch (err) {
168
+ try {
169
+ engine.logger?.warn?.("Audit write failed", { object: ctx.object, action, err: String(err?.message ?? err) });
170
+ } catch {
171
+ }
172
+ }
173
+ };
174
+ engine.registerHook("afterInsert", writeAudit, { packageId });
175
+ engine.registerHook("afterUpdate", writeAudit, { packageId });
176
+ engine.registerHook("afterDelete", writeAudit, { packageId });
177
+ const writeCommentMentions = async (ctx) => {
178
+ if (ctx.object !== "sys_comment") return;
179
+ if (ctx.event !== "afterInsert") return;
180
+ const api = ctx.api;
181
+ if (!api?.sudo) return;
182
+ const row = ctx.result;
183
+ if (!row || typeof row !== "object") return;
184
+ let mentions = row.mentions;
185
+ if (typeof mentions === "string") {
186
+ try {
187
+ mentions = JSON.parse(mentions);
188
+ } catch {
189
+ mentions = null;
190
+ }
191
+ }
192
+ if (!Array.isArray(mentions) || mentions.length === 0) return;
193
+ const userIds = mentions.map((m) => typeof m === "string" ? m : m?.id).filter((id) => typeof id === "string" && id.length > 0);
194
+ if (userIds.length === 0) return;
195
+ const [source_object, source_id] = String(row.thread_id ?? "").split(":");
196
+ const actorId = row.author_id ?? null;
197
+ const actorName = row.author_name ?? null;
198
+ const bodyPreview = String(row.body ?? "").slice(0, 240);
199
+ const sess = ctx.session ?? {};
200
+ const tenantId = sess.tenantId ?? row.organization_id ?? null;
201
+ const sys = api.sudo();
202
+ for (const uid of userIds) {
203
+ if (uid === actorId) continue;
204
+ try {
205
+ await sys.object("sys_notification").create({
206
+ recipient_id: uid,
207
+ type: "mention",
208
+ title: actorName ? `${actorName} mentioned you` : "You were mentioned",
209
+ body: bodyPreview,
210
+ source_object: source_object || null,
211
+ source_id: source_id || null,
212
+ actor_id: actorId,
213
+ actor_name: actorName,
214
+ is_read: false,
215
+ // Stamp tenant so the recipient's RLS sees it (see writeAssignmentNotifications).
216
+ organization_id: tenantId
217
+ });
218
+ } catch (err) {
219
+ try {
220
+ engine.logger?.warn?.("Mention notification write failed", { uid, err: String(err?.message ?? err) });
221
+ } catch {
222
+ }
223
+ }
224
+ }
225
+ };
226
+ engine.registerHook("afterInsert", writeCommentMentions, { packageId });
227
+ }
228
+ var OWNER_FIELDS = ["owner_id", "assigned_to", "assignee_id", "owner", "assignee"];
229
+ function pickOwner(rec) {
230
+ if (!rec || typeof rec !== "object") return null;
231
+ for (const f of OWNER_FIELDS) {
232
+ const v = rec[f];
233
+ if (typeof v === "string" && v.length > 0) return v;
234
+ }
235
+ return null;
236
+ }
237
+ async function writeAssignmentNotifications(sys, params) {
238
+ if (params.action === "delete") return;
239
+ if (!params.recordId) return;
240
+ const newOwner = pickOwner(params.after);
241
+ const oldOwner = pickOwner(params.before);
242
+ if (!newOwner) return;
243
+ if (params.action === "update" && newOwner === oldOwner) return;
244
+ if (newOwner === params.actorId) return;
245
+ try {
246
+ await sys.object("sys_notification").create({
247
+ recipient_id: newOwner,
248
+ type: "assignment",
249
+ title: `${params.object} "${params.label}" assigned to you`,
250
+ body: null,
251
+ source_object: params.object,
252
+ source_id: params.recordId,
253
+ actor_id: params.actorId,
254
+ actor_name: null,
255
+ is_read: false,
256
+ // Stamp organization_id so the recipient (who lives in the same
257
+ // tenant as the action) sees the notification through RLS. Without
258
+ // this, sys_notification rows insert with NULL organization_id and
259
+ // the recipient's `tenant_isolation` policy denies them.
260
+ organization_id: params.tenantId
261
+ });
262
+ } catch {
94
263
  }
95
- });
264
+ }
96
265
 
97
266
  // src/audit-plugin.ts
98
267
  var AuditPlugin = class {
@@ -103,32 +272,51 @@ var AuditPlugin = class {
103
272
  this.dependencies = ["com.objectstack.engine.objectql"];
104
273
  }
105
274
  async init(ctx) {
275
+ process.stderr.write("[AuditPlugin] init() called\n");
106
276
  ctx.getService("manifest").register({
107
277
  id: "com.objectstack.audit",
108
278
  name: "Audit",
109
279
  version: "1.0.0",
110
280
  type: "plugin",
281
+ scope: "system",
282
+ defaultDatasource: "cloud",
111
283
  namespace: "sys",
112
- objects: [SysAuditLog]
284
+ objects: [SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification]
113
285
  });
114
- try {
115
- const setupNav = ctx.getService("setupNav");
116
- if (setupNav) {
117
- setupNav.contribute({
118
- areaId: "area_system",
119
- items: [
120
- { id: "nav_audit_logs", type: "object", label: "Audit Logs", objectName: "audit_log", icon: "scroll-text", order: 10 }
121
- ]
122
- });
123
- ctx.logger.info("Audit navigation items contributed to Setup App");
124
- }
125
- } catch {
126
- }
127
286
  ctx.logger.info("Audit Plugin initialized");
128
287
  }
288
+ async start(ctx) {
289
+ process.stderr.write("[AuditPlugin] start() called, registering kernel:ready hook\n");
290
+ ctx.hook("kernel:ready", async () => {
291
+ process.stderr.write("[AuditPlugin] kernel:ready fired\n");
292
+ let engine = null;
293
+ try {
294
+ engine = ctx.getService("objectql");
295
+ process.stderr.write(`[AuditPlugin] objectql engine = ${engine ? "OK" : "null"} registerHook? ${typeof engine?.registerHook}
296
+ `);
297
+ } catch (err) {
298
+ process.stderr.write(`[AuditPlugin] getService(objectql) threw: ${err.message}
299
+ `);
300
+ try {
301
+ engine = ctx.getService("data");
302
+ process.stderr.write(`[AuditPlugin] data engine = ${engine ? "OK" : "null"}
303
+ `);
304
+ } catch {
305
+ }
306
+ }
307
+ if (!engine) {
308
+ process.stderr.write("[AuditPlugin] NO ENGINE \u2014 bailing\n");
309
+ ctx.logger.warn("AuditPlugin: ObjectQL engine not available \u2014 audit writers NOT installed");
310
+ return;
311
+ }
312
+ installAuditWriters(engine, this.name);
313
+ process.stderr.write("[AuditPlugin] writers installed\n");
314
+ ctx.logger.info("AuditPlugin: audit + activity writers installed");
315
+ });
316
+ }
129
317
  };
130
318
  export {
131
319
  AuditPlugin,
132
- SysAuditLog
320
+ installAuditWriters
133
321
  };
134
322
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/objects/sys-audit-log.object.ts","../src/audit-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_audit_log — System Audit Log Object\n *\n * Immutable audit trail for all significant platform events.\n * Records who did what, when, and the before/after state.\n *\n * @namespace sys\n */\nexport const SysAuditLog = ObjectSchema.create({\n namespace: 'sys',\n name: 'audit_log',\n label: 'Audit Log',\n pluralLabel: 'Audit Logs',\n icon: 'scroll-text',\n isSystem: true,\n description: 'Immutable audit trail for platform events',\n titleFormat: '{action} on {object_name} by {user_id}',\n compactLayout: ['action', 'object_name', 'user_id', 'created_at'],\n \n fields: {\n id: Field.text({\n label: 'Audit Log ID',\n required: true,\n readonly: true,\n }),\n \n created_at: Field.datetime({\n label: 'Timestamp',\n required: true,\n defaultValue: 'NOW()',\n readonly: true,\n }),\n \n user_id: Field.text({\n label: 'User ID',\n required: false,\n description: 'User who performed the action (null for system actions)',\n }),\n \n action: Field.select(['create', 'update', 'delete', 'restore', 'login', 'logout', 'permission_change', 'config_change', 'export', 'import'], {\n label: 'Action',\n required: true,\n description: 'Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import',\n }),\n \n object_name: Field.text({\n label: 'Object Name',\n required: false,\n maxLength: 255,\n description: 'Target object (e.g. sys_user, project_task)',\n }),\n \n record_id: Field.text({\n label: 'Record ID',\n required: false,\n description: 'ID of the affected record',\n }),\n \n old_value: Field.textarea({\n label: 'Old Value',\n required: false,\n description: 'JSON-serialized previous state',\n }),\n \n new_value: Field.textarea({\n label: 'New Value',\n required: false,\n description: 'JSON-serialized new state',\n }),\n \n ip_address: Field.text({\n label: 'IP Address',\n required: false,\n maxLength: 45,\n }),\n \n user_agent: Field.textarea({\n label: 'User Agent',\n required: false,\n }),\n \n tenant_id: Field.text({\n label: 'Tenant ID',\n required: false,\n description: 'Tenant context for multi-tenant isolation',\n }),\n \n metadata: Field.textarea({\n label: 'Metadata',\n required: false,\n description: 'JSON-serialized additional context',\n }),\n },\n \n indexes: [\n { fields: ['created_at'] },\n { fields: ['user_id'] },\n { fields: ['object_name', 'record_id'] },\n { fields: ['action'] },\n { fields: ['tenant_id'] },\n ],\n \n enable: {\n trackHistory: false, // Audit logs are themselves the audit trail\n searchable: true,\n apiEnabled: true,\n apiMethods: ['get', 'list'], // Read-only — audit logs are immutable; creation happens via internal system hooks only\n trash: false, // Never soft-delete audit logs\n mru: false,\n clone: false,\n },\n});\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysAuditLog } from './objects/index.js';\n\n/**\n * AuditPlugin\n *\n * Registers the sys_audit_log system object with ObjectQL so it is\n * discoverable by the studio and available for CRUD operations.\n */\nexport class AuditPlugin implements Plugin {\n name = 'com.objectstack.audit';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n async init(ctx: PluginContext): Promise<void> {\n // Register audit system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.audit',\n name: 'Audit',\n version: '1.0.0',\n type: 'plugin',\n namespace: 'sys',\n objects: [SysAuditLog],\n });\n\n // Contribute navigation items to the Setup App (if SetupPlugin is loaded).\n try {\n const setupNav = ctx.getService<{ contribute(c: any): void }>('setupNav');\n if (setupNav) {\n setupNav.contribute({\n areaId: 'area_system',\n items: [\n { id: 'nav_audit_logs', type: 'object', label: 'Audit Logs', objectName: 'audit_log', icon: 'scroll-text', order: 10 },\n ],\n });\n ctx.logger.info('Audit navigation items contributed to Setup App');\n }\n } catch {\n // SetupPlugin not loaded — skip silently\n }\n\n ctx.logger.info('Audit Plugin initialized');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/audit-plugin.ts","../src/audit-writers.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine } from '@objectstack/spec/contracts';\nimport { SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification } from '@objectstack/platform-objects/audit';\nimport { installAuditWriters } from './audit-writers.js';\n\n/**\n * AuditPlugin\n *\n * Registers the sys_audit_log / sys_activity / sys_comment system objects\n * and installs ObjectQL hook subscribers that automatically write audit\n * trail + activity stream rows on every data mutation.\n *\n * Implements ROADMAP M10.1 (CRM production-readiness).\n */\nexport class AuditPlugin implements Plugin {\n name = 'com.objectstack.audit';\n type = 'standard';\n version = '1.0.0';\n dependencies = ['com.objectstack.engine.objectql'];\n\n async init(ctx: PluginContext): Promise<void> {\n process.stderr.write('[AuditPlugin] init() called\\n');\n // Register audit system objects via the manifest service.\n ctx.getService<{ register(m: any): void }>('manifest').register({\n id: 'com.objectstack.audit',\n name: 'Audit',\n version: '1.0.0',\n type: 'plugin',\n scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification],\n });\n\n ctx.logger.info('Audit Plugin initialized');\n }\n\n async start(ctx: PluginContext): Promise<void> {\n process.stderr.write('[AuditPlugin] start() called, registering kernel:ready hook\\n');\n // ObjectQL engine is only resolvable after the kernel is ready.\n ctx.hook('kernel:ready', async () => {\n process.stderr.write('[AuditPlugin] kernel:ready fired\\n');\n let engine: IDataEngine | null = null;\n try {\n engine = ctx.getService<IDataEngine>('objectql');\n process.stderr.write(`[AuditPlugin] objectql engine = ${engine ? 'OK' : 'null'} registerHook? ${typeof (engine as any)?.registerHook}\\n`);\n } catch (err) {\n process.stderr.write(`[AuditPlugin] getService(objectql) threw: ${(err as Error).message}\\n`);\n // Fallback alias used in some kernels.\n try {\n engine = ctx.getService<IDataEngine>('data');\n process.stderr.write(`[AuditPlugin] data engine = ${engine ? 'OK' : 'null'}\\n`);\n } catch { /* ignore */ }\n }\n if (!engine) {\n process.stderr.write('[AuditPlugin] NO ENGINE — bailing\\n');\n ctx.logger.warn('AuditPlugin: ObjectQL engine not available — audit writers NOT installed');\n return;\n }\n installAuditWriters(engine as any, this.name);\n process.stderr.write('[AuditPlugin] writers installed\\n');\n ctx.logger.info('AuditPlugin: audit + activity writers installed');\n });\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { HookContext } from '@objectstack/spec/data';\nimport type { IDataEngine } from '@objectstack/spec/contracts';\n\n/**\n * Audit writer hook installer.\n *\n * Subscribes to the ObjectQL engine's wildcard `before*` / `after*` lifecycle\n * events and writes:\n *\n * - `sys_audit_log` rows — immutable, compliance-grade entries with\n * field-level `old_value` / `new_value` diffs.\n * - `sys_activity` rows — denormalized, human-readable summaries shown\n * in the dashboard recent-activity feed and per-record timelines.\n *\n * Skip rules avoid recursion and noise:\n * - Never audit the audit/activity tables themselves.\n * - Never audit session/presence/auth tables (high-frequency, low value).\n * - Read-only operations (`afterFind`) are never audited.\n *\n * All writes go through `ctx.api.sudo()` so they bypass record-level\n * permissions and always succeed regardless of the calling user's RBAC.\n */\n\n/** Tables that are intentionally excluded from audit/activity writes. */\nconst SKIP_OBJECTS = new Set<string>([\n 'sys_audit_log',\n 'sys_activity',\n 'sys_comment',\n 'sys_session',\n 'sys_presence',\n 'sys_account',\n 'sys_account_session',\n 'sys_account_verification',\n 'sys_account_account',\n]);\n\n/** Fields that are noise in diffs (always change, never user-meaningful). */\nconst NOISE_FIELDS = new Set<string>([\n 'updated_at',\n 'updated_by',\n 'created_at',\n 'created_by',\n]);\n\n/** Action name produced from a HookContext.event string. */\nfunction actionFor(event: string): 'create' | 'update' | 'delete' | null {\n if (event === 'afterInsert') return 'create';\n if (event === 'afterUpdate') return 'update';\n if (event === 'afterDelete') return 'delete';\n return null;\n}\n\n/** Activity type produced from an audit action. */\nfunction activityTypeFor(action: 'create' | 'update' | 'delete'): 'created' | 'updated' | 'deleted' {\n return action === 'create' ? 'created' : action === 'update' ? 'updated' : 'deleted';\n}\n\n/**\n * Compute the human-readable record label from a record by trying common\n * label fields. Falls back to record id.\n */\nfunction recordLabel(record: any, id: string): string {\n if (!record || typeof record !== 'object') return id;\n const candidates = ['name', 'subject', 'title', 'full_name', 'label', 'first_name', 'company', 'email'];\n for (const k of candidates) {\n const v = record[k];\n if (typeof v === 'string' && v.trim()) return v.trim();\n }\n return id;\n}\n\n/**\n * Compute a shallow JSON diff between two records. Returns only keys whose\n * value changed (and ignores keys in `NOISE_FIELDS`). Both sides are\n * serialisable via `JSON.stringify` — values that fail to serialise are\n * coerced to `String(value)`.\n */\nfunction diff(before: Record<string, any>, after: Record<string, any>): { old: Record<string, any>; next: Record<string, any> } {\n const oldOut: Record<string, any> = {};\n const newOut: Record<string, any> = {};\n const keys = new Set<string>([...Object.keys(before || {}), ...Object.keys(after || {})]);\n for (const k of keys) {\n if (NOISE_FIELDS.has(k)) continue;\n const b = before?.[k];\n const a = after?.[k];\n if (safeStringify(b) !== safeStringify(a)) {\n oldOut[k] = b ?? null;\n newOut[k] = a ?? null;\n }\n }\n return { old: oldOut, next: newOut };\n}\n\nfunction safeStringify(v: any): string {\n try { return JSON.stringify(v); } catch { return String(v); }\n}\n\n/**\n * Install audit + activity writers on the given engine. Idempotent per\n * `packageId` — calling twice with the same id replaces the previous\n * registration.\n */\nexport function installAuditWriters(engine: any, packageId = 'com.objectstack.audit'): void {\n if (!engine || typeof engine.registerHook !== 'function') return;\n\n // Remove any prior installation so we can safely re-install on hot reload.\n if (typeof engine.unregisterHooksByPackage === 'function') {\n engine.unregisterHooksByPackage(packageId);\n }\n\n /**\n * beforeUpdate / beforeDelete: capture \"previous\" snapshot via api.sudo()\n * so we can compute the diff in the afterXxx hook. We attach the snapshot\n * to the context (`(ctx as any).__previous`) since `HookContext.previous`\n * is officially typed but not always populated by the engine itself.\n */\n const captureBefore = async (ctx: HookContext) => {\n if (SKIP_OBJECTS.has(ctx.object)) return;\n const id = (ctx.input as any)?.id;\n if (!id) return; // bulk update/delete — too costly to snapshot every row here\n try {\n // Use the engine directly (not api.sudo) so we can thread the\n // active transaction through. On drivers with single-connection\n // pools (e.g. SQLite via knex) a sudo() findOne that does NOT\n // carry the open transaction will deadlock for the full\n // acquireConnectionTimeout (~60s) because the outer transaction\n // holds the only connection.\n const trx = (ctx as any).transaction;\n const ql = (ctx as any).ql ?? (ctx as any).api?.engine;\n if (ql?.findOne) {\n const prev = await ql.findOne(ctx.object, {\n where: { id },\n context: { isSystem: true, ...(trx ? { transaction: trx } : {}) },\n });\n if (prev) (ctx as any).__previous = prev;\n return;\n }\n const api: any = (ctx as any).api;\n if (!api?.sudo) return;\n const prev = await api.sudo().object(ctx.object).findOne({ where: { id } });\n if (prev) (ctx as any).__previous = prev;\n } catch {\n /* ignore — best-effort */\n }\n };\n\n engine.registerHook('beforeUpdate', captureBefore, { packageId });\n engine.registerHook('beforeDelete', captureBefore, { packageId });\n\n /**\n * afterInsert / afterUpdate / afterDelete: write audit_log + activity rows.\n * Errors are swallowed (logged) so user-facing CRUD is never broken by\n * audit failures.\n */\n const writeAudit = async (ctx: HookContext) => {\n if (SKIP_OBJECTS.has(ctx.object)) return;\n const action = actionFor(ctx.event);\n if (!action) return;\n\n const api: any = (ctx as any).api;\n if (!api?.sudo) return;\n\n const after: any = ctx.result;\n const before: any = (ctx as any).__previous ?? (ctx as any).previous ?? null;\n\n // Resolve record id from after (insert/update) or before (delete) or input.\n let recordId: string | undefined =\n (typeof after === 'object' && after?.id) ||\n (typeof before === 'object' && before?.id) ||\n ((ctx.input as any)?.id);\n if (recordId !== undefined) recordId = String(recordId);\n\n const sess: any = (ctx as any).session ?? {};\n const userId: string | undefined = sess.userId;\n // Prefer the active session tenant, but fall back to the audited\n // record's own `organization_id`. This matters in two cases:\n // 1. Background jobs / unauthenticated sudo paths where the\n // session has no `tenantId` populated.\n // 2. better-auth's `activeOrganizationId` cache miss on first\n // requests after sign-in, before the active-org has been set\n // on the session row.\n // Without this fallback, audit rows are written with\n // `organization_id=NULL` and the SecurityPlugin's RLS predicate\n // (`organization_id = current_user.organization_id`) hides them\n // forever — making the audit log UI appear permanently empty even\n // though writes succeed.\n const recordOrgId: string | undefined =\n (typeof (ctx.result as any)?.organization_id === 'string' && (ctx.result as any).organization_id) ||\n (typeof ((ctx as any).__previous as any)?.organization_id === 'string' && ((ctx as any).__previous as any).organization_id) ||\n undefined;\n const tenantId: string | undefined = sess.tenantId ?? recordOrgId;\n\n let oldValue: Record<string, any> | null = null;\n let newValue: Record<string, any> | null = null;\n if (action === 'create') {\n newValue = (after && typeof after === 'object') ? { ...after } : null;\n } else if (action === 'update') {\n const d = diff(before || {}, after || {});\n oldValue = d.old;\n newValue = d.next;\n // If nothing meaningfully changed, skip the audit row to avoid noise.\n if (Object.keys(newValue).length === 0) return;\n } else if (action === 'delete') {\n oldValue = before && typeof before === 'object' ? { ...before } : null;\n }\n\n const auditRow: Record<string, any> = {\n action,\n user_id: userId ?? null,\n object_name: ctx.object,\n record_id: recordId ?? null,\n old_value: oldValue ? safeStringify(oldValue) : null,\n new_value: newValue ? safeStringify(newValue) : null,\n // `tenant_id` is the schema-declared \"tenant context\" lookup; the\n // platform-default `organization_id` column is what RLS gates on\n // (`organization_id = current_user.organization_id`). The audit\n // writer runs through `api.sudo()` which bypasses the\n // SecurityPlugin's auto-stamping of `organization_id`, so we\n // stamp both columns explicitly here. Without `organization_id`,\n // non-admin members would see 0 rows on Setup dashboards because\n // RLS would deny every audit row as wrong-tenant.\n organization_id: tenantId ?? null,\n tenant_id: tenantId ?? null,\n };\n\n const label = recordLabel(after ?? before, recordId ?? '');\n const summary =\n action === 'create' ? `Created ${ctx.object} \"${label}\"` :\n action === 'update' ? `Updated ${ctx.object} \"${label}\"` :\n `Deleted ${ctx.object} \"${label}\"`;\n\n const activityRow: Record<string, any> = {\n type: activityTypeFor(action),\n // Explicit ISO timestamp — `defaultValue: 'NOW()'` on the column\n // isn't resolved by every driver and would otherwise leak the\n // literal string \"NOW()\" into the row.\n timestamp: new Date().toISOString(),\n summary,\n actor_id: userId ?? null,\n object_name: ctx.object,\n record_id: recordId ?? null,\n record_label: label,\n metadata: newValue || oldValue ? safeStringify({ old: oldValue, new: newValue }) : null,\n // Same rationale as auditRow: stamp the tenant column so RLS\n // matches the recipient's organization on read.\n organization_id: tenantId ?? null,\n };\n\n try {\n const sys = api.sudo();\n await sys.object('sys_audit_log').create(auditRow);\n await sys.object('sys_activity').create(activityRow);\n // M10.8: write per-user inbox notifications. Best-effort; never\n // throws into the user-facing CRUD path. Covers two common cases:\n //\n // 1. Assignment — if owner_id / assigned_to was newly set (or\n // changed to a different user) on a non-system record, drop\n // a notification into the recipient's inbox so they can see\n // \"Lead X was assigned to you\" without polling the record.\n //\n // 2. (Comment mentions are handled separately by the sys_comment\n // hook below since SKIP_OBJECTS excludes it from this writer.)\n await writeAssignmentNotifications(sys, {\n object: ctx.object,\n recordId: recordId ?? null,\n label,\n action,\n before,\n after,\n actorId: userId ?? null,\n tenantId: tenantId ?? null,\n });\n } catch (err) {\n // Log via engine logger if available, but never throw.\n try { (engine as any).logger?.warn?.('Audit write failed', { object: ctx.object, action, err: String((err as any)?.message ?? err) }); } catch {}\n }\n };\n\n engine.registerHook('afterInsert', writeAudit, { packageId });\n engine.registerHook('afterUpdate', writeAudit, { packageId });\n engine.registerHook('afterDelete', writeAudit, { packageId });\n\n /**\n * M10.8: Dedicated hook on `sys_comment` afterInsert that parses the\n * `mentions` JSON field and writes one sys_notification per mentioned\n * user. Lives outside `writeAudit` because sys_comment is in\n * SKIP_OBJECTS (we don't want audit/activity rows for comments —\n * those have their own first-class feed).\n */\n const writeCommentMentions = async (ctx: HookContext) => {\n if (ctx.object !== 'sys_comment') return;\n if (ctx.event !== 'afterInsert') return;\n const api: any = (ctx as any).api;\n if (!api?.sudo) return;\n const row: any = ctx.result;\n if (!row || typeof row !== 'object') return;\n\n // mentions is a JSON-string textarea on sys_comment. Accept either\n // a raw array of user-ids [\"u1\",\"u2\"] or an array of objects\n // [{ id: \"u1\" }, ...]; tolerate parse failures silently.\n let mentions: any = row.mentions;\n if (typeof mentions === 'string') {\n try { mentions = JSON.parse(mentions); } catch { mentions = null; }\n }\n if (!Array.isArray(mentions) || mentions.length === 0) return;\n\n const userIds = mentions\n .map((m: any) => (typeof m === 'string' ? m : m?.id))\n .filter((id: any) => typeof id === 'string' && id.length > 0);\n if (userIds.length === 0) return;\n\n const [source_object, source_id] = String(row.thread_id ?? '').split(':');\n const actorId = row.author_id ?? null;\n const actorName = row.author_name ?? null;\n const bodyPreview = String(row.body ?? '').slice(0, 240);\n const sess: any = (ctx as any).session ?? {};\n const tenantId: string | null = sess.tenantId ?? row.organization_id ?? null;\n\n const sys = api.sudo();\n for (const uid of userIds) {\n if (uid === actorId) continue; // don't notify the mention author\n try {\n await sys.object('sys_notification').create({\n recipient_id: uid,\n type: 'mention',\n title: actorName ? `${actorName} mentioned you` : 'You were mentioned',\n body: bodyPreview,\n source_object: source_object || null,\n source_id: source_id || null,\n actor_id: actorId,\n actor_name: actorName,\n is_read: false,\n // Stamp tenant so the recipient's RLS sees it (see writeAssignmentNotifications).\n organization_id: tenantId,\n });\n } catch (err) {\n try { (engine as any).logger?.warn?.('Mention notification write failed', { uid, err: String((err as any)?.message ?? err) }); } catch {}\n }\n }\n };\n engine.registerHook('afterInsert', writeCommentMentions, { packageId });\n}\n\n/**\n * Identify the assignee/owner field of a record. We accept several\n * conventional names so this works across CRM-style objects (owner_id,\n * assigned_to) and platform objects (recipient_id is handled separately).\n */\nconst OWNER_FIELDS = ['owner_id', 'assigned_to', 'assignee_id', 'owner', 'assignee'];\n\nfunction pickOwner(rec: any): string | null {\n if (!rec || typeof rec !== 'object') return null;\n for (const f of OWNER_FIELDS) {\n const v = rec[f];\n if (typeof v === 'string' && v.length > 0) return v;\n }\n return null;\n}\n\nasync function writeAssignmentNotifications(\n sys: any,\n params: {\n object: string;\n recordId: string | null;\n label: string;\n action: 'create' | 'update' | 'delete';\n before: any;\n after: any;\n actorId: string | null;\n tenantId: string | null;\n },\n): Promise<void> {\n if (params.action === 'delete') return;\n if (!params.recordId) return;\n\n const newOwner = pickOwner(params.after);\n const oldOwner = pickOwner(params.before);\n if (!newOwner) return;\n if (params.action === 'update' && newOwner === oldOwner) return;\n if (newOwner === params.actorId) return; // self-assignment is silent\n\n try {\n await sys.object('sys_notification').create({\n recipient_id: newOwner,\n type: 'assignment',\n title: `${params.object} \"${params.label}\" assigned to you`,\n body: null,\n source_object: params.object,\n source_id: params.recordId,\n actor_id: params.actorId,\n actor_name: null,\n is_read: false,\n // Stamp organization_id so the recipient (who lives in the same\n // tenant as the action) sees the notification through RLS. Without\n // this, sys_notification rows insert with NULL organization_id and\n // the recipient's `tenant_isolation` policy denies them.\n organization_id: params.tenantId,\n });\n } catch {\n // best-effort; never throw into CRUD path\n }\n}\n\n// Re-export for convenience.\nexport type { IDataEngine };\n"],"mappings":"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","names":["prev"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/plugin-audit",
3
- "version": "4.0.4",
3
+ "version": "4.1.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Audit Plugin for ObjectStack — System audit log object and audit trail",
6
6
  "main": "dist/index.js",
@@ -13,13 +13,39 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "4.0.4",
17
- "@objectstack/spec": "4.0.4"
16
+ "@objectstack/core": "4.1.0",
17
+ "@objectstack/platform-objects": "4.1.0",
18
+ "@objectstack/spec": "4.1.0"
18
19
  },
19
20
  "devDependencies": {
20
- "@types/node": "^25.6.0",
21
- "typescript": "^6.0.2",
22
- "vitest": "^4.1.4"
21
+ "@types/node": "^25.9.1",
22
+ "typescript": "^6.0.3",
23
+ "vitest": "^4.1.7"
24
+ },
25
+ "keywords": [
26
+ "objectstack",
27
+ "plugin",
28
+ "audit",
29
+ "logging",
30
+ "compliance"
31
+ ],
32
+ "author": "ObjectStack",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/objectstack-ai/framework.git",
36
+ "directory": "packages/plugins/plugin-audit"
37
+ },
38
+ "homepage": "https://objectstack.ai/docs",
39
+ "bugs": "https://github.com/objectstack-ai/framework/issues",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18.0.0"
23
49
  },
24
50
  "scripts": {
25
51
  "build": "tsup --config ../../../tsup.config.ts",
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/plugin-audit@4.0.4 build /home/runner/work/framework/framework/packages/plugins/plugin-audit
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- CJS dist/index.js 4.93 KB
14
- CJS dist/index.js.map 7.55 KB
15
- CJS ⚡️ Build success in 79ms
16
- ESM dist/index.mjs 3.75 KB
17
- ESM dist/index.mjs.map 7.10 KB
18
- ESM ⚡️ Build success in 82ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 21495ms
21
- DTS dist/index.d.mts 103.78 KB
22
- DTS dist/index.d.ts 103.78 KB