@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.js CHANGED
@@ -21,105 +21,274 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuditPlugin: () => AuditPlugin,
24
- SysAuditLog: () => SysAuditLog
24
+ installAuditWriters: () => installAuditWriters
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
27
 
28
- // src/objects/sys-audit-log.object.ts
29
- var import_data = require("@objectstack/spec/data");
30
- var SysAuditLog = import_data.ObjectSchema.create({
31
- namespace: "sys",
32
- name: "audit_log",
33
- label: "Audit Log",
34
- pluralLabel: "Audit Logs",
35
- icon: "scroll-text",
36
- isSystem: true,
37
- description: "Immutable audit trail for platform events",
38
- titleFormat: "{action} on {object_name} by {user_id}",
39
- compactLayout: ["action", "object_name", "user_id", "created_at"],
40
- fields: {
41
- id: import_data.Field.text({
42
- label: "Audit Log ID",
43
- required: true,
44
- readonly: true
45
- }),
46
- created_at: import_data.Field.datetime({
47
- label: "Timestamp",
48
- required: true,
49
- defaultValue: "NOW()",
50
- readonly: true
51
- }),
52
- user_id: import_data.Field.text({
53
- label: "User ID",
54
- required: false,
55
- description: "User who performed the action (null for system actions)"
56
- }),
57
- action: import_data.Field.select(["create", "update", "delete", "restore", "login", "logout", "permission_change", "config_change", "export", "import"], {
58
- label: "Action",
59
- required: true,
60
- description: "Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import"
61
- }),
62
- object_name: import_data.Field.text({
63
- label: "Object Name",
64
- required: false,
65
- maxLength: 255,
66
- description: "Target object (e.g. sys_user, project_task)"
67
- }),
68
- record_id: import_data.Field.text({
69
- label: "Record ID",
70
- required: false,
71
- description: "ID of the affected record"
72
- }),
73
- old_value: import_data.Field.textarea({
74
- label: "Old Value",
75
- required: false,
76
- description: "JSON-serialized previous state"
77
- }),
78
- new_value: import_data.Field.textarea({
79
- label: "New Value",
80
- required: false,
81
- description: "JSON-serialized new state"
82
- }),
83
- ip_address: import_data.Field.text({
84
- label: "IP Address",
85
- required: false,
86
- maxLength: 45
87
- }),
88
- user_agent: import_data.Field.textarea({
89
- label: "User Agent",
90
- required: false
91
- }),
92
- tenant_id: import_data.Field.text({
93
- label: "Tenant ID",
94
- required: false,
95
- description: "Tenant context for multi-tenant isolation"
96
- }),
97
- metadata: import_data.Field.textarea({
98
- label: "Metadata",
99
- required: false,
100
- description: "JSON-serialized additional context"
101
- })
102
- },
103
- indexes: [
104
- { fields: ["created_at"] },
105
- { fields: ["user_id"] },
106
- { fields: ["object_name", "record_id"] },
107
- { fields: ["action"] },
108
- { fields: ["tenant_id"] }
109
- ],
110
- enable: {
111
- trackHistory: false,
112
- // Audit logs are themselves the audit trail
113
- searchable: true,
114
- apiEnabled: true,
115
- apiMethods: ["get", "list"],
116
- // Read-only — audit logs are immutable; creation happens via internal system hooks only
117
- trash: false,
118
- // Never soft-delete audit logs
119
- mru: false,
120
- clone: false
28
+ // src/audit-plugin.ts
29
+ var import_audit = require("@objectstack/platform-objects/audit");
30
+
31
+ // src/audit-writers.ts
32
+ var SKIP_OBJECTS = /* @__PURE__ */ new Set([
33
+ "sys_audit_log",
34
+ "sys_activity",
35
+ "sys_comment",
36
+ "sys_session",
37
+ "sys_presence",
38
+ "sys_account",
39
+ "sys_account_session",
40
+ "sys_account_verification",
41
+ "sys_account_account"
42
+ ]);
43
+ var NOISE_FIELDS = /* @__PURE__ */ new Set([
44
+ "updated_at",
45
+ "updated_by",
46
+ "created_at",
47
+ "created_by"
48
+ ]);
49
+ function actionFor(event) {
50
+ if (event === "afterInsert") return "create";
51
+ if (event === "afterUpdate") return "update";
52
+ if (event === "afterDelete") return "delete";
53
+ return null;
54
+ }
55
+ function activityTypeFor(action) {
56
+ return action === "create" ? "created" : action === "update" ? "updated" : "deleted";
57
+ }
58
+ function recordLabel(record, id) {
59
+ if (!record || typeof record !== "object") return id;
60
+ const candidates = ["name", "subject", "title", "full_name", "label", "first_name", "company", "email"];
61
+ for (const k of candidates) {
62
+ const v = record[k];
63
+ if (typeof v === "string" && v.trim()) return v.trim();
121
64
  }
122
- });
65
+ return id;
66
+ }
67
+ function diff(before, after) {
68
+ const oldOut = {};
69
+ const newOut = {};
70
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})]);
71
+ for (const k of keys) {
72
+ if (NOISE_FIELDS.has(k)) continue;
73
+ const b = before?.[k];
74
+ const a = after?.[k];
75
+ if (safeStringify(b) !== safeStringify(a)) {
76
+ oldOut[k] = b ?? null;
77
+ newOut[k] = a ?? null;
78
+ }
79
+ }
80
+ return { old: oldOut, next: newOut };
81
+ }
82
+ function safeStringify(v) {
83
+ try {
84
+ return JSON.stringify(v);
85
+ } catch {
86
+ return String(v);
87
+ }
88
+ }
89
+ function installAuditWriters(engine, packageId = "com.objectstack.audit") {
90
+ if (!engine || typeof engine.registerHook !== "function") return;
91
+ if (typeof engine.unregisterHooksByPackage === "function") {
92
+ engine.unregisterHooksByPackage(packageId);
93
+ }
94
+ const captureBefore = async (ctx) => {
95
+ if (SKIP_OBJECTS.has(ctx.object)) return;
96
+ const id = ctx.input?.id;
97
+ if (!id) return;
98
+ try {
99
+ const trx = ctx.transaction;
100
+ const ql = ctx.ql ?? ctx.api?.engine;
101
+ if (ql?.findOne) {
102
+ const prev2 = await ql.findOne(ctx.object, {
103
+ where: { id },
104
+ context: { isSystem: true, ...trx ? { transaction: trx } : {} }
105
+ });
106
+ if (prev2) ctx.__previous = prev2;
107
+ return;
108
+ }
109
+ const api = ctx.api;
110
+ if (!api?.sudo) return;
111
+ const prev = await api.sudo().object(ctx.object).findOne({ where: { id } });
112
+ if (prev) ctx.__previous = prev;
113
+ } catch {
114
+ }
115
+ };
116
+ engine.registerHook("beforeUpdate", captureBefore, { packageId });
117
+ engine.registerHook("beforeDelete", captureBefore, { packageId });
118
+ const writeAudit = async (ctx) => {
119
+ if (SKIP_OBJECTS.has(ctx.object)) return;
120
+ const action = actionFor(ctx.event);
121
+ if (!action) return;
122
+ const api = ctx.api;
123
+ if (!api?.sudo) return;
124
+ const after = ctx.result;
125
+ const before = ctx.__previous ?? ctx.previous ?? null;
126
+ let recordId = typeof after === "object" && after?.id || typeof before === "object" && before?.id || ctx.input?.id;
127
+ if (recordId !== void 0) recordId = String(recordId);
128
+ const sess = ctx.session ?? {};
129
+ const userId = sess.userId;
130
+ const recordOrgId = typeof ctx.result?.organization_id === "string" && ctx.result.organization_id || typeof ctx.__previous?.organization_id === "string" && ctx.__previous.organization_id || void 0;
131
+ const tenantId = sess.tenantId ?? recordOrgId;
132
+ let oldValue = null;
133
+ let newValue = null;
134
+ if (action === "create") {
135
+ newValue = after && typeof after === "object" ? { ...after } : null;
136
+ } else if (action === "update") {
137
+ const d = diff(before || {}, after || {});
138
+ oldValue = d.old;
139
+ newValue = d.next;
140
+ if (Object.keys(newValue).length === 0) return;
141
+ } else if (action === "delete") {
142
+ oldValue = before && typeof before === "object" ? { ...before } : null;
143
+ }
144
+ const auditRow = {
145
+ action,
146
+ user_id: userId ?? null,
147
+ object_name: ctx.object,
148
+ record_id: recordId ?? null,
149
+ old_value: oldValue ? safeStringify(oldValue) : null,
150
+ new_value: newValue ? safeStringify(newValue) : null,
151
+ // `tenant_id` is the schema-declared "tenant context" lookup; the
152
+ // platform-default `organization_id` column is what RLS gates on
153
+ // (`organization_id = current_user.organization_id`). The audit
154
+ // writer runs through `api.sudo()` which bypasses the
155
+ // SecurityPlugin's auto-stamping of `organization_id`, so we
156
+ // stamp both columns explicitly here. Without `organization_id`,
157
+ // non-admin members would see 0 rows on Setup dashboards because
158
+ // RLS would deny every audit row as wrong-tenant.
159
+ organization_id: tenantId ?? null,
160
+ tenant_id: tenantId ?? null
161
+ };
162
+ const label = recordLabel(after ?? before, recordId ?? "");
163
+ const summary = action === "create" ? `Created ${ctx.object} "${label}"` : action === "update" ? `Updated ${ctx.object} "${label}"` : `Deleted ${ctx.object} "${label}"`;
164
+ const activityRow = {
165
+ type: activityTypeFor(action),
166
+ // Explicit ISO timestamp — `defaultValue: 'NOW()'` on the column
167
+ // isn't resolved by every driver and would otherwise leak the
168
+ // literal string "NOW()" into the row.
169
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
170
+ summary,
171
+ actor_id: userId ?? null,
172
+ object_name: ctx.object,
173
+ record_id: recordId ?? null,
174
+ record_label: label,
175
+ metadata: newValue || oldValue ? safeStringify({ old: oldValue, new: newValue }) : null,
176
+ // Same rationale as auditRow: stamp the tenant column so RLS
177
+ // matches the recipient's organization on read.
178
+ organization_id: tenantId ?? null
179
+ };
180
+ try {
181
+ const sys = api.sudo();
182
+ await sys.object("sys_audit_log").create(auditRow);
183
+ await sys.object("sys_activity").create(activityRow);
184
+ await writeAssignmentNotifications(sys, {
185
+ object: ctx.object,
186
+ recordId: recordId ?? null,
187
+ label,
188
+ action,
189
+ before,
190
+ after,
191
+ actorId: userId ?? null,
192
+ tenantId: tenantId ?? null
193
+ });
194
+ } catch (err) {
195
+ try {
196
+ engine.logger?.warn?.("Audit write failed", { object: ctx.object, action, err: String(err?.message ?? err) });
197
+ } catch {
198
+ }
199
+ }
200
+ };
201
+ engine.registerHook("afterInsert", writeAudit, { packageId });
202
+ engine.registerHook("afterUpdate", writeAudit, { packageId });
203
+ engine.registerHook("afterDelete", writeAudit, { packageId });
204
+ const writeCommentMentions = async (ctx) => {
205
+ if (ctx.object !== "sys_comment") return;
206
+ if (ctx.event !== "afterInsert") return;
207
+ const api = ctx.api;
208
+ if (!api?.sudo) return;
209
+ const row = ctx.result;
210
+ if (!row || typeof row !== "object") return;
211
+ let mentions = row.mentions;
212
+ if (typeof mentions === "string") {
213
+ try {
214
+ mentions = JSON.parse(mentions);
215
+ } catch {
216
+ mentions = null;
217
+ }
218
+ }
219
+ if (!Array.isArray(mentions) || mentions.length === 0) return;
220
+ const userIds = mentions.map((m) => typeof m === "string" ? m : m?.id).filter((id) => typeof id === "string" && id.length > 0);
221
+ if (userIds.length === 0) return;
222
+ const [source_object, source_id] = String(row.thread_id ?? "").split(":");
223
+ const actorId = row.author_id ?? null;
224
+ const actorName = row.author_name ?? null;
225
+ const bodyPreview = String(row.body ?? "").slice(0, 240);
226
+ const sess = ctx.session ?? {};
227
+ const tenantId = sess.tenantId ?? row.organization_id ?? null;
228
+ const sys = api.sudo();
229
+ for (const uid of userIds) {
230
+ if (uid === actorId) continue;
231
+ try {
232
+ await sys.object("sys_notification").create({
233
+ recipient_id: uid,
234
+ type: "mention",
235
+ title: actorName ? `${actorName} mentioned you` : "You were mentioned",
236
+ body: bodyPreview,
237
+ source_object: source_object || null,
238
+ source_id: source_id || null,
239
+ actor_id: actorId,
240
+ actor_name: actorName,
241
+ is_read: false,
242
+ // Stamp tenant so the recipient's RLS sees it (see writeAssignmentNotifications).
243
+ organization_id: tenantId
244
+ });
245
+ } catch (err) {
246
+ try {
247
+ engine.logger?.warn?.("Mention notification write failed", { uid, err: String(err?.message ?? err) });
248
+ } catch {
249
+ }
250
+ }
251
+ }
252
+ };
253
+ engine.registerHook("afterInsert", writeCommentMentions, { packageId });
254
+ }
255
+ var OWNER_FIELDS = ["owner_id", "assigned_to", "assignee_id", "owner", "assignee"];
256
+ function pickOwner(rec) {
257
+ if (!rec || typeof rec !== "object") return null;
258
+ for (const f of OWNER_FIELDS) {
259
+ const v = rec[f];
260
+ if (typeof v === "string" && v.length > 0) return v;
261
+ }
262
+ return null;
263
+ }
264
+ async function writeAssignmentNotifications(sys, params) {
265
+ if (params.action === "delete") return;
266
+ if (!params.recordId) return;
267
+ const newOwner = pickOwner(params.after);
268
+ const oldOwner = pickOwner(params.before);
269
+ if (!newOwner) return;
270
+ if (params.action === "update" && newOwner === oldOwner) return;
271
+ if (newOwner === params.actorId) return;
272
+ try {
273
+ await sys.object("sys_notification").create({
274
+ recipient_id: newOwner,
275
+ type: "assignment",
276
+ title: `${params.object} "${params.label}" assigned to you`,
277
+ body: null,
278
+ source_object: params.object,
279
+ source_id: params.recordId,
280
+ actor_id: params.actorId,
281
+ actor_name: null,
282
+ is_read: false,
283
+ // Stamp organization_id so the recipient (who lives in the same
284
+ // tenant as the action) sees the notification through RLS. Without
285
+ // this, sys_notification rows insert with NULL organization_id and
286
+ // the recipient's `tenant_isolation` policy denies them.
287
+ organization_id: params.tenantId
288
+ });
289
+ } catch {
290
+ }
291
+ }
123
292
 
124
293
  // src/audit-plugin.ts
125
294
  var AuditPlugin = class {
@@ -130,33 +299,52 @@ var AuditPlugin = class {
130
299
  this.dependencies = ["com.objectstack.engine.objectql"];
131
300
  }
132
301
  async init(ctx) {
302
+ process.stderr.write("[AuditPlugin] init() called\n");
133
303
  ctx.getService("manifest").register({
134
304
  id: "com.objectstack.audit",
135
305
  name: "Audit",
136
306
  version: "1.0.0",
137
307
  type: "plugin",
308
+ scope: "system",
309
+ defaultDatasource: "cloud",
138
310
  namespace: "sys",
139
- objects: [SysAuditLog]
311
+ objects: [import_audit.SysAuditLog, import_audit.SysActivity, import_audit.SysComment, import_audit.SysAttachment, import_audit.SysNotification]
140
312
  });
141
- try {
142
- const setupNav = ctx.getService("setupNav");
143
- if (setupNav) {
144
- setupNav.contribute({
145
- areaId: "area_system",
146
- items: [
147
- { id: "nav_audit_logs", type: "object", label: "Audit Logs", objectName: "audit_log", icon: "scroll-text", order: 10 }
148
- ]
149
- });
150
- ctx.logger.info("Audit navigation items contributed to Setup App");
151
- }
152
- } catch {
153
- }
154
313
  ctx.logger.info("Audit Plugin initialized");
155
314
  }
315
+ async start(ctx) {
316
+ process.stderr.write("[AuditPlugin] start() called, registering kernel:ready hook\n");
317
+ ctx.hook("kernel:ready", async () => {
318
+ process.stderr.write("[AuditPlugin] kernel:ready fired\n");
319
+ let engine = null;
320
+ try {
321
+ engine = ctx.getService("objectql");
322
+ process.stderr.write(`[AuditPlugin] objectql engine = ${engine ? "OK" : "null"} registerHook? ${typeof engine?.registerHook}
323
+ `);
324
+ } catch (err) {
325
+ process.stderr.write(`[AuditPlugin] getService(objectql) threw: ${err.message}
326
+ `);
327
+ try {
328
+ engine = ctx.getService("data");
329
+ process.stderr.write(`[AuditPlugin] data engine = ${engine ? "OK" : "null"}
330
+ `);
331
+ } catch {
332
+ }
333
+ }
334
+ if (!engine) {
335
+ process.stderr.write("[AuditPlugin] NO ENGINE \u2014 bailing\n");
336
+ ctx.logger.warn("AuditPlugin: ObjectQL engine not available \u2014 audit writers NOT installed");
337
+ return;
338
+ }
339
+ installAuditWriters(engine, this.name);
340
+ process.stderr.write("[AuditPlugin] writers installed\n");
341
+ ctx.logger.info("AuditPlugin: audit + activity writers installed");
342
+ });
343
+ }
156
344
  };
157
345
  // Annotate the CommonJS export names for ESM import in node:
158
346
  0 && (module.exports = {
159
347
  AuditPlugin,
160
- SysAuditLog
348
+ installAuditWriters
161
349
  });
162
350
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/objects/sys-audit-log.object.ts","../src/audit-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-audit\n *\n * Audit Plugin for ObjectStack\n * Provides the sys_audit_log system object definition for immutable audit trails.\n */\n\nexport { AuditPlugin } from './audit-plugin.js';\n\n// System Object Definitions (sys namespace)\nexport { SysAuditLog } from './objects/index.js';\n","// 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,kBAAoC;AAU7B,IAAM,cAAc,yBAAa,OAAO;AAAA,EAC7C,WAAW;AAAA,EACX,MAAM;AAAA,EACN,OAAO;AAAA,EACP,aAAa;AAAA,EACb,MAAM;AAAA,EACN,UAAU;AAAA,EACV,aAAa;AAAA,EACb,aAAa;AAAA,EACb,eAAe,CAAC,UAAU,eAAe,WAAW,YAAY;AAAA,EAEhE,QAAQ;AAAA,IACN,IAAI,kBAAM,KAAK;AAAA,MACb,OAAO;AAAA,MACP,UAAU;AAAA,MACV,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,YAAY,kBAAM,SAAS;AAAA,MACzB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,cAAc;AAAA,MACd,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,SAAS,kBAAM,KAAK;AAAA,MAClB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,QAAQ,kBAAM,OAAO,CAAC,UAAU,UAAU,UAAU,WAAW,SAAS,UAAU,qBAAqB,iBAAiB,UAAU,QAAQ,GAAG;AAAA,MAC3I,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,aAAa,kBAAM,KAAK;AAAA,MACtB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,WAAW;AAAA,MACX,aAAa;AAAA,IACf,CAAC;AAAA,IAED,WAAW,kBAAM,KAAK;AAAA,MACpB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,WAAW,kBAAM,SAAS;AAAA,MACxB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,WAAW,kBAAM,SAAS;AAAA,MACxB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,YAAY,kBAAM,KAAK;AAAA,MACrB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,WAAW;AAAA,IACb,CAAC;AAAA,IAED,YAAY,kBAAM,SAAS;AAAA,MACzB,OAAO;AAAA,MACP,UAAU;AAAA,IACZ,CAAC;AAAA,IAED,WAAW,kBAAM,KAAK;AAAA,MACpB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,IAED,UAAU,kBAAM,SAAS;AAAA,MACvB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,aAAa;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEA,SAAS;AAAA,IACP,EAAE,QAAQ,CAAC,YAAY,EAAE;AAAA,IACzB,EAAE,QAAQ,CAAC,SAAS,EAAE;AAAA,IACtB,EAAE,QAAQ,CAAC,eAAe,WAAW,EAAE;AAAA,IACvC,EAAE,QAAQ,CAAC,QAAQ,EAAE;AAAA,IACrB,EAAE,QAAQ,CAAC,WAAW,EAAE;AAAA,EAC1B;AAAA,EAEA,QAAQ;AAAA,IACN,cAAc;AAAA;AAAA,IACd,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,YAAY,CAAC,OAAO,MAAM;AAAA;AAAA,IAC1B,OAAO;AAAA;AAAA,IACP,KAAK;AAAA,IACL,OAAO;AAAA,EACT;AACF,CAAC;;;ACxGM,IAAM,cAAN,MAAoC;AAAA,EAApC;AACL,gBAAO;AACP,gBAAO;AACP,mBAAU;AACV,wBAAe,CAAC,iCAAiC;AAAA;AAAA,EAEjD,MAAM,KAAK,KAAmC;AAE5C,QAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,MAC9D,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,WAAW;AAAA,MACX,SAAS,CAAC,WAAW;AAAA,IACvB,CAAC;AAGD,QAAI;AACF,YAAM,WAAW,IAAI,WAAyC,UAAU;AACxE,UAAI,UAAU;AACZ,iBAAS,WAAW;AAAA,UAClB,QAAQ;AAAA,UACR,OAAO;AAAA,YACL,EAAE,IAAI,kBAAkB,MAAM,UAAU,OAAO,cAAc,YAAY,aAAa,MAAM,eAAe,OAAO,GAAG;AAAA,UACvH;AAAA,QACF,CAAC;AACD,YAAI,OAAO,KAAK,iDAAiD;AAAA,MACnE;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,OAAO,KAAK,0BAA0B;AAAA,EAC5C;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/audit-plugin.ts","../src/audit-writers.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * @objectstack/plugin-audit\n *\n * Audit Plugin for ObjectStack\n * Provides the sys_audit_log system object definition for immutable audit trails.\n */\n\nexport { AuditPlugin } from './audit-plugin.js';\nexport { installAuditWriters } from './audit-writers.js';\n","// 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"]}