@objectstack/plugin-audit 4.0.5 → 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.d.mts CHANGED
@@ -3,8 +3,11 @@ import { Plugin, PluginContext } from '@objectstack/core';
3
3
  /**
4
4
  * AuditPlugin
5
5
  *
6
- * Registers the sys_audit_log system object with ObjectQL so it is
7
- * discoverable by the studio and available for CRUD operations.
6
+ * Registers the sys_audit_log / sys_activity / sys_comment system objects
7
+ * and installs ObjectQL hook subscribers that automatically write audit
8
+ * trail + activity stream rows on every data mutation.
9
+ *
10
+ * Implements ROADMAP M10.1 (CRM production-readiness).
8
11
  */
9
12
  declare class AuditPlugin implements Plugin {
10
13
  name: string;
@@ -12,6 +15,14 @@ declare class AuditPlugin implements Plugin {
12
15
  version: string;
13
16
  dependencies: string[];
14
17
  init(ctx: PluginContext): Promise<void>;
18
+ start(ctx: PluginContext): Promise<void>;
15
19
  }
16
20
 
17
- export { AuditPlugin };
21
+ /**
22
+ * Install audit + activity writers on the given engine. Idempotent per
23
+ * `packageId` — calling twice with the same id replaces the previous
24
+ * registration.
25
+ */
26
+ declare function installAuditWriters(engine: any, packageId?: string): void;
27
+
28
+ export { AuditPlugin, installAuditWriters };
package/dist/index.d.ts CHANGED
@@ -3,8 +3,11 @@ import { Plugin, PluginContext } from '@objectstack/core';
3
3
  /**
4
4
  * AuditPlugin
5
5
  *
6
- * Registers the sys_audit_log system object with ObjectQL so it is
7
- * discoverable by the studio and available for CRUD operations.
6
+ * Registers the sys_audit_log / sys_activity / sys_comment system objects
7
+ * and installs ObjectQL hook subscribers that automatically write audit
8
+ * trail + activity stream rows on every data mutation.
9
+ *
10
+ * Implements ROADMAP M10.1 (CRM production-readiness).
8
11
  */
9
12
  declare class AuditPlugin implements Plugin {
10
13
  name: string;
@@ -12,6 +15,14 @@ declare class AuditPlugin implements Plugin {
12
15
  version: string;
13
16
  dependencies: string[];
14
17
  init(ctx: PluginContext): Promise<void>;
18
+ start(ctx: PluginContext): Promise<void>;
15
19
  }
16
20
 
17
- export { AuditPlugin };
21
+ /**
22
+ * Install audit + activity writers on the given engine. Idempotent per
23
+ * `packageId` — calling twice with the same id replaces the previous
24
+ * registration.
25
+ */
26
+ declare function installAuditWriters(engine: any, packageId?: string): void;
27
+
28
+ export { AuditPlugin, installAuditWriters };
package/dist/index.js CHANGED
@@ -20,12 +20,277 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
- AuditPlugin: () => AuditPlugin
23
+ AuditPlugin: () => AuditPlugin,
24
+ installAuditWriters: () => installAuditWriters
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
26
27
 
27
28
  // src/audit-plugin.ts
28
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();
64
+ }
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
+ }
292
+
293
+ // src/audit-plugin.ts
29
294
  var AuditPlugin = class {
30
295
  constructor() {
31
296
  this.name = "com.objectstack.audit";
@@ -34,6 +299,7 @@ var AuditPlugin = class {
34
299
  this.dependencies = ["com.objectstack.engine.objectql"];
35
300
  }
36
301
  async init(ctx) {
302
+ process.stderr.write("[AuditPlugin] init() called\n");
37
303
  ctx.getService("manifest").register({
38
304
  id: "com.objectstack.audit",
39
305
  name: "Audit",
@@ -42,13 +308,43 @@ var AuditPlugin = class {
42
308
  scope: "system",
43
309
  defaultDatasource: "cloud",
44
310
  namespace: "sys",
45
- objects: [import_audit.SysAuditLog, import_audit.SysActivity, import_audit.SysComment]
311
+ objects: [import_audit.SysAuditLog, import_audit.SysActivity, import_audit.SysComment, import_audit.SysAttachment, import_audit.SysNotification]
46
312
  });
47
313
  ctx.logger.info("Audit Plugin initialized");
48
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
+ }
49
344
  };
50
345
  // Annotate the CommonJS export names for ESM import in node:
51
346
  0 && (module.exports = {
52
- AuditPlugin
347
+ AuditPlugin,
348
+ installAuditWriters
53
349
  });
54
350
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.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","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysAuditLog, SysActivity, SysComment } from '@objectstack/platform-objects/audit';\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 scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysAuditLog, SysActivity, SysComment],\n });\n\n ctx.logger.info('Audit Plugin initialized');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACGA,mBAAqD;AAQ9C,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,OAAO;AAAA,MACP,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,SAAS,CAAC,0BAAa,0BAAa,uBAAU;AAAA,IAChD,CAAC;AAED,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"]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,269 @@
1
1
  // src/audit-plugin.ts
2
- import { SysAuditLog, SysActivity, SysComment } from "@objectstack/platform-objects/audit";
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 {
263
+ }
264
+ }
265
+
266
+ // src/audit-plugin.ts
3
267
  var AuditPlugin = class {
4
268
  constructor() {
5
269
  this.name = "com.objectstack.audit";
@@ -8,6 +272,7 @@ var AuditPlugin = class {
8
272
  this.dependencies = ["com.objectstack.engine.objectql"];
9
273
  }
10
274
  async init(ctx) {
275
+ process.stderr.write("[AuditPlugin] init() called\n");
11
276
  ctx.getService("manifest").register({
12
277
  id: "com.objectstack.audit",
13
278
  name: "Audit",
@@ -16,12 +281,42 @@ var AuditPlugin = class {
16
281
  scope: "system",
17
282
  defaultDatasource: "cloud",
18
283
  namespace: "sys",
19
- objects: [SysAuditLog, SysActivity, SysComment]
284
+ objects: [SysAuditLog, SysActivity, SysComment, SysAttachment, SysNotification]
20
285
  });
21
286
  ctx.logger.info("Audit Plugin initialized");
22
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
+ }
23
317
  };
24
318
  export {
25
- AuditPlugin
319
+ AuditPlugin,
320
+ installAuditWriters
26
321
  };
27
322
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/audit-plugin.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport { SysAuditLog, SysActivity, SysComment } from '@objectstack/platform-objects/audit';\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 scope: 'system',\n defaultDatasource: 'cloud',\n namespace: 'sys',\n objects: [SysAuditLog, SysActivity, SysComment],\n });\n\n ctx.logger.info('Audit Plugin initialized');\n }\n}\n"],"mappings":";AAGA,SAAS,aAAa,aAAa,kBAAkB;AAQ9C,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,OAAO;AAAA,MACP,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,SAAS,CAAC,aAAa,aAAa,UAAU;AAAA,IAChD,CAAC;AAED,QAAI,OAAO,KAAK,0BAA0B;AAAA,EAC5C;AACF;","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":";AAIA,SAAS,aAAa,aAAa,YAAY,eAAe,uBAAuB;;;ACsBrF,IAAM,eAAe,oBAAI,IAAY;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,eAAe,oBAAI,IAAY;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,SAAS,UAAU,OAAsD;AACvE,MAAI,UAAU,cAAe,QAAO;AACpC,MAAI,UAAU,cAAe,QAAO;AACpC,MAAI,UAAU,cAAe,QAAO;AACpC,SAAO;AACT;AAGA,SAAS,gBAAgB,QAA2E;AAClG,SAAO,WAAW,WAAW,YAAY,WAAW,WAAW,YAAY;AAC7E;AAMA,SAAS,YAAY,QAAa,IAAoB;AACpD,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,QAAM,aAAa,CAAC,QAAQ,WAAW,SAAS,aAAa,SAAS,cAAc,WAAW,OAAO;AACtG,aAAW,KAAK,YAAY;AAC1B,UAAM,IAAI,OAAO,CAAC;AAClB,QAAI,OAAO,MAAM,YAAY,EAAE,KAAK,EAAG,QAAO,EAAE,KAAK;AAAA,EACvD;AACA,SAAO;AACT;AAQA,SAAS,KAAK,QAA6B,OAAqF;AAC9H,QAAM,SAA8B,CAAC;AACrC,QAAM,SAA8B,CAAC;AACrC,QAAM,OAAO,oBAAI,IAAY,CAAC,GAAG,OAAO,KAAK,UAAU,CAAC,CAAC,GAAG,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC;AACxF,aAAW,KAAK,MAAM;AACpB,QAAI,aAAa,IAAI,CAAC,EAAG;AACzB,UAAM,IAAI,SAAS,CAAC;AACpB,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,cAAc,CAAC,MAAM,cAAc,CAAC,GAAG;AACzC,aAAO,CAAC,IAAI,KAAK;AACjB,aAAO,CAAC,IAAI,KAAK;AAAA,IACnB;AAAA,EACF;AACA,SAAO,EAAE,KAAK,QAAQ,MAAM,OAAO;AACrC;AAEA,SAAS,cAAc,GAAgB;AACrC,MAAI;AAAE,WAAO,KAAK,UAAU,CAAC;AAAA,EAAG,QAAQ;AAAE,WAAO,OAAO,CAAC;AAAA,EAAG;AAC9D;AAOO,SAAS,oBAAoB,QAAa,YAAY,yBAA+B;AAC1F,MAAI,CAAC,UAAU,OAAO,OAAO,iBAAiB,WAAY;AAG1D,MAAI,OAAO,OAAO,6BAA6B,YAAY;AACzD,WAAO,yBAAyB,SAAS;AAAA,EAC3C;AAQA,QAAM,gBAAgB,OAAO,QAAqB;AAChD,QAAI,aAAa,IAAI,IAAI,MAAM,EAAG;AAClC,UAAM,KAAM,IAAI,OAAe;AAC/B,QAAI,CAAC,GAAI;AACT,QAAI;AAOF,YAAM,MAAO,IAAY;AACzB,YAAM,KAAM,IAAY,MAAO,IAAY,KAAK;AAChD,UAAI,IAAI,SAAS;AACf,cAAMA,QAAO,MAAM,GAAG,QAAQ,IAAI,QAAQ;AAAA,UACxC,OAAO,EAAE,GAAG;AAAA,UACZ,SAAS,EAAE,UAAU,MAAM,GAAI,MAAM,EAAE,aAAa,IAAI,IAAI,CAAC,EAAG;AAAA,QAClE,CAAC;AACD,YAAIA,MAAM,CAAC,IAAY,aAAaA;AACpC;AAAA,MACF;AACA,YAAM,MAAY,IAAY;AAC9B,UAAI,CAAC,KAAK,KAAM;AAChB,YAAM,OAAO,MAAM,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAC1E,UAAI,KAAM,CAAC,IAAY,aAAa;AAAA,IACtC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,SAAO,aAAa,gBAAgB,eAAe,EAAE,UAAU,CAAC;AAChE,SAAO,aAAa,gBAAgB,eAAe,EAAE,UAAU,CAAC;AAOhE,QAAM,aAAa,OAAO,QAAqB;AAC7C,QAAI,aAAa,IAAI,IAAI,MAAM,EAAG;AAClC,UAAM,SAAS,UAAU,IAAI,KAAK;AAClC,QAAI,CAAC,OAAQ;AAEb,UAAM,MAAY,IAAY;AAC9B,QAAI,CAAC,KAAK,KAAM;AAEhB,UAAM,QAAa,IAAI;AACvB,UAAM,SAAe,IAAY,cAAe,IAAY,YAAY;AAGxE,QAAI,WACD,OAAO,UAAU,YAAY,OAAO,MACpC,OAAO,WAAW,YAAY,QAAQ,MACrC,IAAI,OAAe;AACvB,QAAI,aAAa,OAAW,YAAW,OAAO,QAAQ;AAEtD,UAAM,OAAa,IAAY,WAAW,CAAC;AAC3C,UAAM,SAA6B,KAAK;AAaxC,UAAM,cACH,OAAQ,IAAI,QAAgB,oBAAoB,YAAa,IAAI,OAAe,mBAChF,OAAS,IAAY,YAAoB,oBAAoB,YAAc,IAAY,WAAmB,mBAC3G;AACF,UAAM,WAA+B,KAAK,YAAY;AAEtD,QAAI,WAAuC;AAC3C,QAAI,WAAuC;AAC3C,QAAI,WAAW,UAAU;AACvB,iBAAY,SAAS,OAAO,UAAU,WAAY,EAAE,GAAG,MAAM,IAAI;AAAA,IACnE,WAAW,WAAW,UAAU;AAC9B,YAAM,IAAI,KAAK,UAAU,CAAC,GAAG,SAAS,CAAC,CAAC;AACxC,iBAAW,EAAE;AACb,iBAAW,EAAE;AAEb,UAAI,OAAO,KAAK,QAAQ,EAAE,WAAW,EAAG;AAAA,IAC1C,WAAW,WAAW,UAAU;AAC9B,iBAAW,UAAU,OAAO,WAAW,WAAW,EAAE,GAAG,OAAO,IAAI;AAAA,IACpE;AAEA,UAAM,WAAgC;AAAA,MACpC;AAAA,MACA,SAAS,UAAU;AAAA,MACnB,aAAa,IAAI;AAAA,MACjB,WAAW,YAAY;AAAA,MACvB,WAAW,WAAW,cAAc,QAAQ,IAAI;AAAA,MAChD,WAAW,WAAW,cAAc,QAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAShD,iBAAiB,YAAY;AAAA,MAC7B,WAAW,YAAY;AAAA,IACzB;AAEA,UAAM,QAAQ,YAAY,SAAS,QAAQ,YAAY,EAAE;AACzD,UAAM,UACJ,WAAW,WAAW,WAAW,IAAI,MAAM,KAAK,KAAK,MACrD,WAAW,WAAW,WAAW,IAAI,MAAM,KAAK,KAAK,MAC/B,WAAW,IAAI,MAAM,KAAK,KAAK;AAEvD,UAAM,cAAmC;AAAA,MACvC,MAAM,gBAAgB,MAAM;AAAA;AAAA;AAAA;AAAA,MAI5B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC;AAAA,MACA,UAAU,UAAU;AAAA,MACpB,aAAa,IAAI;AAAA,MACjB,WAAW,YAAY;AAAA,MACvB,cAAc;AAAA,MACd,UAAU,YAAY,WAAW,cAAc,EAAE,KAAK,UAAU,KAAK,SAAS,CAAC,IAAI;AAAA;AAAA;AAAA,MAGnF,iBAAiB,YAAY;AAAA,IAC/B;AAEA,QAAI;AACF,YAAM,MAAM,IAAI,KAAK;AACrB,YAAM,IAAI,OAAO,eAAe,EAAE,OAAO,QAAQ;AACjD,YAAM,IAAI,OAAO,cAAc,EAAE,OAAO,WAAW;AAWnD,YAAM,6BAA6B,KAAK;AAAA,QACtC,QAAQ,IAAI;AAAA,QACZ,UAAU,YAAY;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,UAAU,YAAY;AAAA,MACxB,CAAC;AAAA,IACH,SAAS,KAAK;AAEZ,UAAI;AAAE,QAAC,OAAe,QAAQ,OAAO,sBAAsB,EAAE,QAAQ,IAAI,QAAQ,QAAQ,KAAK,OAAQ,KAAa,WAAW,GAAG,EAAE,CAAC;AAAA,MAAG,QAAQ;AAAA,MAAC;AAAA,IAClJ;AAAA,EACF;AAEA,SAAO,aAAa,eAAe,YAAY,EAAE,UAAU,CAAC;AAC5D,SAAO,aAAa,eAAe,YAAY,EAAE,UAAU,CAAC;AAC5D,SAAO,aAAa,eAAe,YAAY,EAAE,UAAU,CAAC;AAS5D,QAAM,uBAAuB,OAAO,QAAqB;AACvD,QAAI,IAAI,WAAW,cAAe;AAClC,QAAI,IAAI,UAAU,cAAe;AACjC,UAAM,MAAY,IAAY;AAC9B,QAAI,CAAC,KAAK,KAAM;AAChB,UAAM,MAAW,IAAI;AACrB,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU;AAKrC,QAAI,WAAgB,IAAI;AACxB,QAAI,OAAO,aAAa,UAAU;AAChC,UAAI;AAAE,mBAAW,KAAK,MAAM,QAAQ;AAAA,MAAG,QAAQ;AAAE,mBAAW;AAAA,MAAM;AAAA,IACpE;AACA,QAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,SAAS,WAAW,EAAG;AAEvD,UAAM,UAAU,SACb,IAAI,CAAC,MAAY,OAAO,MAAM,WAAW,IAAI,GAAG,EAAG,EACnD,OAAO,CAAC,OAAY,OAAO,OAAO,YAAY,GAAG,SAAS,CAAC;AAC9D,QAAI,QAAQ,WAAW,EAAG;AAE1B,UAAM,CAAC,eAAe,SAAS,IAAI,OAAO,IAAI,aAAa,EAAE,EAAE,MAAM,GAAG;AACxE,UAAM,UAAU,IAAI,aAAa;AACjC,UAAM,YAAY,IAAI,eAAe;AACrC,UAAM,cAAc,OAAO,IAAI,QAAQ,EAAE,EAAE,MAAM,GAAG,GAAG;AACvD,UAAM,OAAa,IAAY,WAAW,CAAC;AAC3C,UAAM,WAA0B,KAAK,YAAY,IAAI,mBAAmB;AAExE,UAAM,MAAM,IAAI,KAAK;AACrB,eAAW,OAAO,SAAS;AACzB,UAAI,QAAQ,QAAS;AACrB,UAAI;AACF,cAAM,IAAI,OAAO,kBAAkB,EAAE,OAAO;AAAA,UAC1C,cAAc;AAAA,UACd,MAAM;AAAA,UACN,OAAO,YAAY,GAAG,SAAS,mBAAmB;AAAA,UAClD,MAAM;AAAA,UACN,eAAe,iBAAiB;AAAA,UAChC,WAAW,aAAa;AAAA,UACxB,UAAU;AAAA,UACV,YAAY;AAAA,UACZ,SAAS;AAAA;AAAA,UAET,iBAAiB;AAAA,QACnB,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,YAAI;AAAE,UAAC,OAAe,QAAQ,OAAO,qCAAqC,EAAE,KAAK,KAAK,OAAQ,KAAa,WAAW,GAAG,EAAE,CAAC;AAAA,QAAG,QAAQ;AAAA,QAAC;AAAA,MAC1I;AAAA,IACF;AAAA,EACF;AACA,SAAO,aAAa,eAAe,sBAAsB,EAAE,UAAU,CAAC;AACxE;AAOA,IAAM,eAAe,CAAC,YAAY,eAAe,eAAe,SAAS,UAAU;AAEnF,SAAS,UAAU,KAAyB;AAC1C,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,aAAW,KAAK,cAAc;AAC5B,UAAM,IAAI,IAAI,CAAC;AACf,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,EAAG,QAAO;AAAA,EACpD;AACA,SAAO;AACT;AAEA,eAAe,6BACb,KACA,QAUe;AACf,MAAI,OAAO,WAAW,SAAU;AAChC,MAAI,CAAC,OAAO,SAAU;AAEtB,QAAM,WAAW,UAAU,OAAO,KAAK;AACvC,QAAM,WAAW,UAAU,OAAO,MAAM;AACxC,MAAI,CAAC,SAAU;AACf,MAAI,OAAO,WAAW,YAAY,aAAa,SAAU;AACzD,MAAI,aAAa,OAAO,QAAS;AAEjC,MAAI;AACF,UAAM,IAAI,OAAO,kBAAkB,EAAE,OAAO;AAAA,MAC1C,cAAc;AAAA,MACd,MAAM;AAAA,MACN,OAAO,GAAG,OAAO,MAAM,KAAK,OAAO,KAAK;AAAA,MACxC,MAAM;AAAA,MACN,eAAe,OAAO;AAAA,MACtB,WAAW,OAAO;AAAA,MAClB,UAAU,OAAO;AAAA,MACjB,YAAY;AAAA,MACZ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT,iBAAiB,OAAO;AAAA,IAC1B,CAAC;AAAA,EACH,QAAQ;AAAA,EAER;AACF;;;ADnYO,IAAM,cAAN,MAAoC;AAAA,EAApC;AACL,gBAAO;AACP,gBAAO;AACP,mBAAU;AACV,wBAAe,CAAC,iCAAiC;AAAA;AAAA,EAEjD,MAAM,KAAK,KAAmC;AAC5C,YAAQ,OAAO,MAAM,+BAA+B;AAEpD,QAAI,WAAuC,UAAU,EAAE,SAAS;AAAA,MAC9D,IAAI;AAAA,MACJ,MAAM;AAAA,MACN,SAAS;AAAA,MACT,MAAM;AAAA,MACN,OAAO;AAAA,MACP,mBAAmB;AAAA,MACnB,WAAW;AAAA,MACX,SAAS,CAAC,aAAa,aAAa,YAAY,eAAe,eAAe;AAAA,IAChF,CAAC;AAED,QAAI,OAAO,KAAK,0BAA0B;AAAA,EAC5C;AAAA,EAEA,MAAM,MAAM,KAAmC;AAC7C,YAAQ,OAAO,MAAM,+DAA+D;AAEpF,QAAI,KAAK,gBAAgB,YAAY;AACnC,cAAQ,OAAO,MAAM,oCAAoC;AACzD,UAAI,SAA6B;AACjC,UAAI;AACF,iBAAS,IAAI,WAAwB,UAAU;AAC/C,gBAAQ,OAAO,MAAM,mCAAmC,SAAS,OAAO,MAAM,kBAAkB,OAAQ,QAAgB,YAAY;AAAA,CAAI;AAAA,MAC1I,SAAS,KAAK;AACZ,gBAAQ,OAAO,MAAM,6CAA8C,IAAc,OAAO;AAAA,CAAI;AAE5F,YAAI;AACF,mBAAS,IAAI,WAAwB,MAAM;AAC3C,kBAAQ,OAAO,MAAM,+BAA+B,SAAS,OAAO,MAAM;AAAA,CAAI;AAAA,QAChF,QAAQ;AAAA,QAAe;AAAA,MACzB;AACA,UAAI,CAAC,QAAQ;AACX,gBAAQ,OAAO,MAAM,0CAAqC;AAC1D,YAAI,OAAO,KAAK,+EAA0E;AAC1F;AAAA,MACF;AACA,0BAAoB,QAAe,KAAK,IAAI;AAC5C,cAAQ,OAAO,MAAM,mCAAmC;AACxD,UAAI,OAAO,KAAK,iDAAiD;AAAA,IACnE,CAAC;AAAA,EACH;AACF;","names":["prev"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/plugin-audit",
3
- "version": "4.0.5",
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,14 +13,14 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "4.0.5",
17
- "@objectstack/platform-objects": "4.0.5",
18
- "@objectstack/spec": "4.0.5"
16
+ "@objectstack/core": "4.1.0",
17
+ "@objectstack/platform-objects": "4.1.0",
18
+ "@objectstack/spec": "4.1.0"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^25.6.2",
21
+ "@types/node": "^25.9.1",
22
22
  "typescript": "^6.0.3",
23
- "vitest": "^4.1.5"
23
+ "vitest": "^4.1.7"
24
24
  },
25
25
  "keywords": [
26
26
  "objectstack",