@intelligo-dev/audit 1.0.0-beta.1 → 1.0.0-beta.13
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/NOTICE +6 -0
- package/README.md +73 -0
- package/dist/db/schema.d.ts +4 -8
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +10 -10
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +9 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/memory-audit.d.ts +9 -36
- package/dist/memory-audit.d.ts.map +1 -1
- package/dist/memory-audit.js +7 -31
- package/dist/memory-audit.js.map +1 -1
- package/package.json +36 -10
- package/src/db/schema.ts +54 -0
- package/src/index.ts +204 -0
- package/src/memory-audit.ts +43 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Intelligo
|
|
2
|
+
Copyright 2026 Turtuvshin Byambaa and the Intelligo contributors
|
|
3
|
+
|
|
4
|
+
This product is licensed under the Apache License, Version 2.0 (see
|
|
5
|
+
LICENSE). The Intelligo name and logo are trademarks; see TRADEMARK.md
|
|
6
|
+
in the source repository, https://github.com/intelligo-dev/intelligo.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @intelligo-dev/audit
|
|
2
|
+
|
|
3
|
+
Append-only audit events for multi-tenant apps, with a Postgres trigger that refuses every update and delete.
|
|
4
|
+
|
|
5
|
+
Part of [Intelligo](https://intelligo.dev), an application framework and
|
|
6
|
+
operational platform for vertical AI SaaS products. Every `@intelligo-dev/*`
|
|
7
|
+
package is released at one version and shares one database schema;
|
|
8
|
+
`pnpm dlx @intelligo-dev/cli@beta create my-app` installs the set an
|
|
9
|
+
application needs. Documentation:
|
|
10
|
+
[intelligo.dev/docs/packages/audit](https://intelligo.dev/docs/packages/audit).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @intelligo-dev/audit@beta drizzle-orm
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`drizzle-orm` is a peer. The `audit_events` table and its trigger are part of
|
|
19
|
+
the framework's schema, which `intelligo migrate` from
|
|
20
|
+
[`@intelligo-dev/cli`](https://www.npmjs.com/package/@intelligo-dev/cli)
|
|
21
|
+
applies.
|
|
22
|
+
|
|
23
|
+
## Use
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { recordAuditEvent } from "@intelligo-dev/audit";
|
|
27
|
+
|
|
28
|
+
await recordAuditEvent({
|
|
29
|
+
workspaceId,
|
|
30
|
+
actorId: user.id,
|
|
31
|
+
action: "member.removed",
|
|
32
|
+
resourceKind: "member",
|
|
33
|
+
resourceId: memberId,
|
|
34
|
+
metadata: { role: "admin" },
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
An event with no `actorId` is a system actor's — a cron, a webhook, the job
|
|
39
|
+
runner; `actorKind: "support"` marks an operator acting on a tenant. `outcome`
|
|
40
|
+
is `"ok"` unless the caller says `"failed"`.
|
|
41
|
+
|
|
42
|
+
`recordAuditEvent` never throws — an audit failure must not fail the action it
|
|
43
|
+
records. `recordAuditEventOrThrow` exists for the callers where the record _is_
|
|
44
|
+
the action: impersonation, a destructive support operation.
|
|
45
|
+
|
|
46
|
+
`queryAuditEvents({ workspaceId, action, resourceKind, resourceId, before, limit })`
|
|
47
|
+
reads the trail most recent first, paginated by `before`.
|
|
48
|
+
|
|
49
|
+
## Append-only, in the database
|
|
50
|
+
|
|
51
|
+
`audit_events` refuses `UPDATE` and `DELETE` at the database level, with one
|
|
52
|
+
exception: the `ON DELETE SET NULL` the foreign keys perform, so deleting a
|
|
53
|
+
workspace does not require deleting its trail.
|
|
54
|
+
|
|
55
|
+
## Sinks
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { registerAuditSink } from "@intelligo-dev/audit";
|
|
59
|
+
|
|
60
|
+
registerAuditSink("siem", async (event) => {
|
|
61
|
+
await ship(event);
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A sink registered from the composition root receives every event after it is
|
|
66
|
+
durably recorded, with failure isolation: an exporter with an expired
|
|
67
|
+
credential breaks neither the audited operation nor the other sinks. It is how
|
|
68
|
+
tamper-evidence, a retention policy or a SIEM export attaches without this
|
|
69
|
+
package knowing about it.
|
|
70
|
+
|
|
71
|
+
## Licence
|
|
72
|
+
|
|
73
|
+
Apache-2.0
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @intelligo-dev/audit database schema.
|
|
3
|
-
*
|
|
4
2
|
* One append-only table recording who did what to which resource.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Scanned by drizzle-kit alongside the core schema directory (see
|
|
4
|
+
* packages/core/drizzle.config.ts) so the migration history stays
|
|
5
|
+
* unified in one database.
|
|
8
6
|
*
|
|
9
7
|
* Distinct from `user_memory_audit` in core/identity, which records
|
|
10
|
-
* mutations to a user's memory graph
|
|
11
|
-
* folds in here once the memory subsystem's ownership is settled
|
|
12
|
-
* (see docs/inventory/export-classification.md).
|
|
8
|
+
* mutations to a user's memory graph.
|
|
13
9
|
*/
|
|
14
10
|
export declare const auditEvents: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
15
11
|
name: "audit_events";
|
package/dist/db/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqCvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC;AACzD,MAAM,MAAM,gBAAgB,GAAG,OAAO,WAAW,CAAC,YAAY,CAAC"}
|
package/dist/db/schema.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @intelligo-dev/audit database schema.
|
|
3
|
-
*
|
|
4
2
|
* One append-only table recording who did what to which resource.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Scanned by drizzle-kit alongside the core schema directory (see
|
|
4
|
+
* packages/core/drizzle.config.ts) so the migration history stays
|
|
5
|
+
* unified in one database.
|
|
8
6
|
*
|
|
9
7
|
* Distinct from `user_memory_audit` in core/identity, which records
|
|
10
|
-
* mutations to a user's memory graph
|
|
11
|
-
* folds in here once the memory subsystem's ownership is settled
|
|
12
|
-
* (see docs/inventory/export-classification.md).
|
|
8
|
+
* mutations to a user's memory graph.
|
|
13
9
|
*/
|
|
14
10
|
import { pgTable, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
|
15
11
|
import { organization, users } from "@intelligo-dev/core/db/schema";
|
|
16
12
|
export const auditEvents = pgTable("audit_events", {
|
|
17
13
|
id: text("id").primaryKey(),
|
|
18
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Null for platform-level events with no tenant — and for events
|
|
16
|
+
* whose workspace was since deleted: the trail outlives the tenant
|
|
17
|
+
* (set null, not cascade).
|
|
18
|
+
*/
|
|
19
19
|
workspaceId: text("workspace_id").references(() => organization.id, {
|
|
20
|
-
onDelete: "
|
|
20
|
+
onDelete: "set null",
|
|
21
21
|
}),
|
|
22
22
|
/** Null for system actors (cron, webhook, job runner). */
|
|
23
23
|
actorId: text("actor_id").references(() => users.id, {
|
package/dist/db/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AAEpE,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAChC,cAAc,EACd;IACE,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B;;;;OAIG;IACH,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE;QAClE,QAAQ,EAAE,UAAU;KACrB,CAAC;IACF,0DAA0D;IAC1D,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE;QACnD,QAAQ,EAAE,UAAU;KACrB,CAAC;IACF,gEAAgE;IAChE,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;IACvD,gEAAgE;IAChE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE;IAChC,uEAAuE;IACvE,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,OAAO,EAAE;IAC7C,kEAAkE;IAClE,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;IAC/B,uDAAuD;IACvD,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;IAChD,QAAQ,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,KAAK,EAA2B;IAC5D,SAAS,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE;CAC1D,EACD,CAAC,KAAK,EAAE,EAAE,CAAC;IACT,KAAK,CAAC,oCAAoC,CAAC,CAAC,EAAE,CAC5C,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,SAAS,CAChB;IACD,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC;IAClE,KAAK,CAAC,2BAA2B,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,UAAU,CAAC;CAC5E,CACF,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,18 +4,13 @@
|
|
|
4
4
|
* Emitting is deliberately failure-tolerant: an audit write must never
|
|
5
5
|
* break the operation being audited. `recordAuditEvent` swallows and
|
|
6
6
|
* logs; callers that need the guarantee (impersonation, destructive
|
|
7
|
-
* support actions
|
|
7
|
+
* support actions) use `recordAuditEventOrThrow`.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* Called after an event is durably recorded.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* (ADR-0005). What it exists for is the governance work that does not
|
|
15
|
-
* belong in a permissively-licensed package — tamper-evidence,
|
|
16
|
-
* retention policy, shipping the trail to a customer's SIEM — so that
|
|
17
|
-
* a closed module can add it without this package knowing it exists,
|
|
18
|
-
* and without anyone forking this file.
|
|
10
|
+
* Called after an event is durably recorded. Sinks are registered by
|
|
11
|
+
* the composition root, never by being imported; they exist for
|
|
12
|
+
* governance work outside this package — tamper-evidence, retention
|
|
13
|
+
* policy, shipping the trail to a SIEM.
|
|
19
14
|
*
|
|
20
15
|
* A sink runs after the write, so it cannot corrupt or delay the audit
|
|
21
16
|
* row itself, and its failures are isolated: an exporter with an
|
|
@@ -83,7 +78,7 @@ export type QueryAuditEventsOptions = {
|
|
|
83
78
|
before?: Date;
|
|
84
79
|
limit?: number;
|
|
85
80
|
};
|
|
86
|
-
/** Most-recent-first query for the admin console
|
|
81
|
+
/** Most-recent-first query for the admin console. */
|
|
87
82
|
export declare function queryAuditEvents(options?: QueryAuditEventsOptions): Promise<{
|
|
88
83
|
id: string;
|
|
89
84
|
workspaceId: string | null;
|
|
@@ -96,7 +91,7 @@ export declare function queryAuditEvents(options?: QueryAuditEventsOptions): Pro
|
|
|
96
91
|
metadata: Record<string, unknown> | null;
|
|
97
92
|
createdAt: Date;
|
|
98
93
|
}[]>;
|
|
99
|
-
export { auditEvents } from "./db/schema";
|
|
100
|
-
export type { AuditEvent, InsertAuditEvent } from "./db/schema";
|
|
101
|
-
export type { AuditTargetKind as MemoryAuditTargetKind, AuditAction as MemoryAuditAction, AuditActorKind as MemoryAuditActorKind, UserMemoryAuditRow, InsertUserMemoryAudit, RecordMemoryAuditInput, } from "./memory-audit";
|
|
94
|
+
export { auditEvents } from "./db/schema.js";
|
|
95
|
+
export type { AuditEvent, InsertAuditEvent } from "./db/schema.js";
|
|
96
|
+
export type { AuditTargetKind as MemoryAuditTargetKind, AuditAction as MemoryAuditAction, AuditActorKind as MemoryAuditActorKind, UserMemoryAuditRow, InsertUserMemoryAudit, RecordMemoryAuditInput, } from "./memory-audit.js";
|
|
102
97
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAE5E;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzC,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAIF,wDAAwD;AACxD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAErE;AAED,wBAAgB,oBAAoB,IAAI,MAAM,EAAE,CAE/C;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAoBD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,QAAQ,CAAC;AAE3C,MAAM,MAAM,eAAe,GAAG;IAC5B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,CAAC;AAgBF;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5E;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,IAAI,CAAC,CASf;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,qDAAqD;AACrD,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,uBAA4B;;;;;;;;;;;KAqB3E;AAED,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAIhE,YAAY,EACV,eAAe,IAAI,qBAAqB,EACxC,WAAW,IAAI,iBAAiB,EAChC,cAAc,IAAI,oBAAoB,EACtC,kBAAkB,EAClB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* Emitting is deliberately failure-tolerant: an audit write must never
|
|
5
5
|
* break the operation being audited. `recordAuditEvent` swallows and
|
|
6
6
|
* logs; callers that need the guarantee (impersonation, destructive
|
|
7
|
-
* support actions
|
|
7
|
+
* support actions) use `recordAuditEventOrThrow`.
|
|
8
8
|
*/
|
|
9
9
|
import { db } from "@intelligo-dev/core/db";
|
|
10
10
|
import { createLogger } from "@intelligo-dev/core/logger";
|
|
11
|
+
import { createRegistry } from "@intelligo-dev/core/registry";
|
|
11
12
|
import { and, desc, eq, lt } from "drizzle-orm";
|
|
12
|
-
import { auditEvents } from "./db/schema";
|
|
13
|
+
import { auditEvents } from "./db/schema.js";
|
|
13
14
|
const log = createLogger("Audit");
|
|
14
|
-
const sinks =
|
|
15
|
+
const sinks = createRegistry("audit/sinks");
|
|
15
16
|
/** Register (or replace) a sink under a stable name. */
|
|
16
17
|
export function registerAuditSink(name, sink) {
|
|
17
18
|
sinks.set(name, sink);
|
|
@@ -84,7 +85,7 @@ export async function recordAuditEventOrThrow(event) {
|
|
|
84
85
|
// and every destructive support action.
|
|
85
86
|
await fanOut({ ...row, createdAt: new Date() });
|
|
86
87
|
}
|
|
87
|
-
/** Most-recent-first query for the admin console
|
|
88
|
+
/** Most-recent-first query for the admin console. */
|
|
88
89
|
export async function queryAuditEvents(options = {}) {
|
|
89
90
|
const filters = [
|
|
90
91
|
options.workspaceId
|
|
@@ -106,5 +107,5 @@ export async function queryAuditEvents(options = {}) {
|
|
|
106
107
|
.orderBy(desc(auditEvents.createdAt))
|
|
107
108
|
.limit(Math.min(options.limit ?? 50, 500));
|
|
108
109
|
}
|
|
109
|
-
export { auditEvents } from "./db/schema";
|
|
110
|
+
export { auditEvents } from "./db/schema.js";
|
|
110
111
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,EAAE,EAAE,MAAM,wBAAwB,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/memory-audit.d.ts
CHANGED
|
@@ -1,45 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory-audit event contract
|
|
2
|
+
* Memory-audit event contract.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `@intelligo-dev/core`
|
|
10
|
-
* constraint `packages/core/src/documents/service.ts` and
|
|
11
|
-
* `.../conversations/service.ts` already document for
|
|
12
|
-
* `@intelligo-dev/auth`. `user_facts`/`user_memories`/
|
|
13
|
-
* `user_profile_snapshots`/`user_memory_audit` are owned by
|
|
14
|
-
* `packages/core/src/identity` (see that module's doc comment), and
|
|
15
|
-
* that module cannot import this package to call a writer defined
|
|
16
|
-
* here — the allowlist only grants the audit -> core edge, never
|
|
17
|
-
* core -> audit.
|
|
18
|
-
*
|
|
19
|
-
* What actually lands here, then, is the event *contract*: the shape a
|
|
20
|
-
* memory-audit write takes, re-exported from the schema module that
|
|
21
|
-
* already owns it. The write itself is a faithful, in-package port at
|
|
22
|
-
* `packages/core/src/identity/audit.ts`, used internally by the
|
|
23
|
-
* identity service (`deleteFact`, `exportIdentity`) so it never
|
|
24
|
-
* crosses the disallowed core -> audit edge. This module is the public
|
|
25
|
-
* seam for anyone else who depends on `@intelligo-dev/audit` (executions,
|
|
26
|
-
* admin, billing) and wants to describe a memory-audit event without
|
|
27
|
-
* reaching into `@intelligo-dev/core/identity`'s internals.
|
|
28
|
-
*
|
|
29
|
-
* `packages/audit/src/db/schema.ts` already documents where this
|
|
30
|
-
* settles: `user_memory_audit` folds into `audit_events` once the
|
|
31
|
-
* memory subsystem's ownership question closes (see
|
|
32
|
-
* docs/inventory/export-classification.md), and this contract
|
|
33
|
-
* graduates to backing that table's writer directly.
|
|
4
|
+
* `@intelligo-dev/core` may depend on nothing, so the identity module
|
|
5
|
+
* that owns `user_memory_audit` cannot call a writer defined here; the
|
|
6
|
+
* write lives in `packages/core/src/identity/audit.ts`. This module is
|
|
7
|
+
* the public type for packages that depend on `@intelligo-dev/audit`
|
|
8
|
+
* and want to describe a memory-audit event without reaching into
|
|
9
|
+
* `@intelligo-dev/core/identity`'s internals.
|
|
34
10
|
*/
|
|
35
11
|
import type { AuditTargetKind, AuditAction, AuditActorKind, UserMemoryAuditRow, InsertUserMemoryAudit } from "@intelligo-dev/core/db/schema";
|
|
36
12
|
export type { AuditTargetKind, AuditAction, AuditActorKind, UserMemoryAuditRow, InsertUserMemoryAudit, };
|
|
37
13
|
/**
|
|
38
|
-
* The shape `recordMemoryAudit`
|
|
39
|
-
*
|
|
40
|
-
* `@intelligo-dev/core/identity` — so a package that depends on
|
|
41
|
-
* `@intelligo-dev/audit` but not on the identity service directly has a
|
|
42
|
-
* stable type to code against.
|
|
14
|
+
* The shape `recordMemoryAudit` (`packages/core/src/identity/audit.ts`)
|
|
15
|
+
* accepts.
|
|
43
16
|
*/
|
|
44
17
|
export type RecordMemoryAuditInput = {
|
|
45
18
|
userId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-audit.d.ts","sourceRoot":"","sources":["../src/memory-audit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"memory-audit.d.ts","sourceRoot":"","sources":["../src/memory-audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,+BAA+B,CAAC;AAEvC,YAAY,EACV,eAAe,EACf,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,qBAAqB,GACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,EAAE,cAAc,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC"}
|
package/dist/memory-audit.js
CHANGED
|
@@ -1,36 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory-audit event contract
|
|
2
|
+
* Memory-audit event contract.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `@intelligo-dev/core`
|
|
10
|
-
* constraint `packages/core/src/documents/service.ts` and
|
|
11
|
-
* `.../conversations/service.ts` already document for
|
|
12
|
-
* `@intelligo-dev/auth`. `user_facts`/`user_memories`/
|
|
13
|
-
* `user_profile_snapshots`/`user_memory_audit` are owned by
|
|
14
|
-
* `packages/core/src/identity` (see that module's doc comment), and
|
|
15
|
-
* that module cannot import this package to call a writer defined
|
|
16
|
-
* here — the allowlist only grants the audit -> core edge, never
|
|
17
|
-
* core -> audit.
|
|
18
|
-
*
|
|
19
|
-
* What actually lands here, then, is the event *contract*: the shape a
|
|
20
|
-
* memory-audit write takes, re-exported from the schema module that
|
|
21
|
-
* already owns it. The write itself is a faithful, in-package port at
|
|
22
|
-
* `packages/core/src/identity/audit.ts`, used internally by the
|
|
23
|
-
* identity service (`deleteFact`, `exportIdentity`) so it never
|
|
24
|
-
* crosses the disallowed core -> audit edge. This module is the public
|
|
25
|
-
* seam for anyone else who depends on `@intelligo-dev/audit` (executions,
|
|
26
|
-
* admin, billing) and wants to describe a memory-audit event without
|
|
27
|
-
* reaching into `@intelligo-dev/core/identity`'s internals.
|
|
28
|
-
*
|
|
29
|
-
* `packages/audit/src/db/schema.ts` already documents where this
|
|
30
|
-
* settles: `user_memory_audit` folds into `audit_events` once the
|
|
31
|
-
* memory subsystem's ownership question closes (see
|
|
32
|
-
* docs/inventory/export-classification.md), and this contract
|
|
33
|
-
* graduates to backing that table's writer directly.
|
|
4
|
+
* `@intelligo-dev/core` may depend on nothing, so the identity module
|
|
5
|
+
* that owns `user_memory_audit` cannot call a writer defined here; the
|
|
6
|
+
* write lives in `packages/core/src/identity/audit.ts`. This module is
|
|
7
|
+
* the public type for packages that depend on `@intelligo-dev/audit`
|
|
8
|
+
* and want to describe a memory-audit event without reaching into
|
|
9
|
+
* `@intelligo-dev/core/identity`'s internals.
|
|
34
10
|
*/
|
|
35
11
|
export {};
|
|
36
12
|
//# sourceMappingURL=memory-audit.js.map
|
package/dist/memory-audit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-audit.js","sourceRoot":"","sources":["../src/memory-audit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"memory-audit.js","sourceRoot":"","sources":["../src/memory-audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
|
package/package.json
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intelligo-dev/audit",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.13",
|
|
4
|
+
"description": "Append-only audit events for multi-tenant apps, with a Postgres trigger that refuses every update and delete.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"intelligo",
|
|
7
|
+
"ai-saas",
|
|
8
|
+
"saas",
|
|
9
|
+
"typescript",
|
|
10
|
+
"audit-log",
|
|
11
|
+
"append-only",
|
|
12
|
+
"postgres"
|
|
13
|
+
],
|
|
4
14
|
"license": "Apache-2.0",
|
|
15
|
+
"author": "Turtuvshin Byambaa <toroo.byamba@gmail.com>",
|
|
5
16
|
"repository": {
|
|
6
17
|
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/intelligo-
|
|
18
|
+
"url": "git+https://github.com/intelligo-dev/intelligo.git",
|
|
8
19
|
"directory": "packages/audit"
|
|
9
20
|
},
|
|
10
|
-
"homepage": "https://
|
|
21
|
+
"homepage": "https://intelligo.dev/docs/packages/audit",
|
|
11
22
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/intelligo-
|
|
23
|
+
"url": "https://github.com/intelligo-dev/intelligo/issues"
|
|
13
24
|
},
|
|
14
25
|
"type": "module",
|
|
15
26
|
"exports": {
|
|
@@ -23,22 +34,37 @@
|
|
|
23
34
|
}
|
|
24
35
|
},
|
|
25
36
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
|
|
37
|
+
"@intelligo-dev/core": "1.0.0-beta.13"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"drizzle-orm": "^0.45.1"
|
|
28
41
|
},
|
|
29
42
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^
|
|
31
|
-
"
|
|
43
|
+
"@types/node": "^26.6.1",
|
|
44
|
+
"drizzle-orm": "^0.45.2",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
32
46
|
},
|
|
33
47
|
"files": [
|
|
34
|
-
"dist"
|
|
48
|
+
"dist",
|
|
49
|
+
"src",
|
|
50
|
+
"!src/**/*.test.ts",
|
|
51
|
+
"!src/**/*.test.tsx",
|
|
52
|
+
"!src/**/__tests__/**",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"NOTICE",
|
|
55
|
+
"README.md",
|
|
56
|
+
"!dist/**/*.tsbuildinfo"
|
|
35
57
|
],
|
|
36
58
|
"publishConfig": {
|
|
37
59
|
"access": "public"
|
|
38
60
|
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=22.14"
|
|
63
|
+
},
|
|
64
|
+
"sideEffects": false,
|
|
39
65
|
"scripts": {
|
|
40
66
|
"type-check": "tsc --noEmit",
|
|
41
67
|
"lint": "eslint .",
|
|
42
|
-
"build": "tsc -p tsconfig.build.json"
|
|
68
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/fix-esm-extensions.mjs dist"
|
|
43
69
|
}
|
|
44
70
|
}
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One append-only table recording who did what to which resource.
|
|
3
|
+
* Scanned by drizzle-kit alongside the core schema directory (see
|
|
4
|
+
* packages/core/drizzle.config.ts) so the migration history stays
|
|
5
|
+
* unified in one database.
|
|
6
|
+
*
|
|
7
|
+
* Distinct from `user_memory_audit` in core/identity, which records
|
|
8
|
+
* mutations to a user's memory graph.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pgTable, text, timestamp, jsonb, index } from "drizzle-orm/pg-core";
|
|
12
|
+
import { organization, users } from "@intelligo-dev/core/db/schema";
|
|
13
|
+
|
|
14
|
+
export const auditEvents = pgTable(
|
|
15
|
+
"audit_events",
|
|
16
|
+
{
|
|
17
|
+
id: text("id").primaryKey(),
|
|
18
|
+
/**
|
|
19
|
+
* Null for platform-level events with no tenant — and for events
|
|
20
|
+
* whose workspace was since deleted: the trail outlives the tenant
|
|
21
|
+
* (set null, not cascade).
|
|
22
|
+
*/
|
|
23
|
+
workspaceId: text("workspace_id").references(() => organization.id, {
|
|
24
|
+
onDelete: "set null",
|
|
25
|
+
}),
|
|
26
|
+
/** Null for system actors (cron, webhook, job runner). */
|
|
27
|
+
actorId: text("actor_id").references(() => users.id, {
|
|
28
|
+
onDelete: "set null",
|
|
29
|
+
}),
|
|
30
|
+
/** "user" | "system" | "support" — who initiated the action. */
|
|
31
|
+
actorKind: text("actor_kind").notNull().default("user"),
|
|
32
|
+
/** Dotted verb: "execution.completed", "credits.reserved", … */
|
|
33
|
+
action: text("action").notNull(),
|
|
34
|
+
/** Resource type the action applied to: "execution", "workspace", … */
|
|
35
|
+
resourceKind: text("resource_kind").notNull(),
|
|
36
|
+
/** Identifier of that resource; free-form (uuid, slug, email). */
|
|
37
|
+
resourceId: text("resource_id"),
|
|
38
|
+
/** "ok" | "failed" — outcome of the audited action. */
|
|
39
|
+
outcome: text("outcome").notNull().default("ok"),
|
|
40
|
+
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
|
41
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
42
|
+
},
|
|
43
|
+
(table) => [
|
|
44
|
+
index("audit_events_workspace_created_idx").on(
|
|
45
|
+
table.workspaceId,
|
|
46
|
+
table.createdAt
|
|
47
|
+
),
|
|
48
|
+
index("audit_events_action_idx").on(table.action, table.createdAt),
|
|
49
|
+
index("audit_events_resource_idx").on(table.resourceKind, table.resourceId),
|
|
50
|
+
]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export type AuditEvent = typeof auditEvents.$inferSelect;
|
|
54
|
+
export type InsertAuditEvent = typeof auditEvents.$inferInsert;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @intelligo-dev/audit — append-only record of who did what.
|
|
3
|
+
*
|
|
4
|
+
* Emitting is deliberately failure-tolerant: an audit write must never
|
|
5
|
+
* break the operation being audited. `recordAuditEvent` swallows and
|
|
6
|
+
* logs; callers that need the guarantee (impersonation, destructive
|
|
7
|
+
* support actions) use `recordAuditEventOrThrow`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { db } from "@intelligo-dev/core/db";
|
|
11
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
12
|
+
import { createRegistry } from "@intelligo-dev/core/registry";
|
|
13
|
+
import { and, desc, eq, lt } from "drizzle-orm";
|
|
14
|
+
|
|
15
|
+
import { auditEvents } from "./db/schema";
|
|
16
|
+
|
|
17
|
+
const log = createLogger("Audit");
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Sinks
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Called after an event is durably recorded. Sinks are registered by
|
|
25
|
+
* the composition root, never by being imported; they exist for
|
|
26
|
+
* governance work outside this package — tamper-evidence, retention
|
|
27
|
+
* policy, shipping the trail to a SIEM.
|
|
28
|
+
*
|
|
29
|
+
* A sink runs after the write, so it cannot corrupt or delay the audit
|
|
30
|
+
* row itself, and its failures are isolated: an exporter with an
|
|
31
|
+
* expired credential must not break the operation being audited, nor
|
|
32
|
+
* the other sinks.
|
|
33
|
+
*/
|
|
34
|
+
export type AuditSink = (event: RecordedAuditEvent) => Promise<void> | void;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The row as written, which is what a sink needs to act on.
|
|
38
|
+
*
|
|
39
|
+
* Spelled out rather than inferred from `toRow`: inference gives `id`
|
|
40
|
+
* the template-literal type of `crypto.randomUUID()`, which is not
|
|
41
|
+
* what comes back out of the database, so every consumer would have to
|
|
42
|
+
* widen it.
|
|
43
|
+
*/
|
|
44
|
+
export type RecordedAuditEvent = {
|
|
45
|
+
id: string;
|
|
46
|
+
workspaceId: string | null;
|
|
47
|
+
actorId: string | null;
|
|
48
|
+
actorKind: string;
|
|
49
|
+
action: string;
|
|
50
|
+
resourceKind: string;
|
|
51
|
+
resourceId: string | null;
|
|
52
|
+
outcome: string;
|
|
53
|
+
metadata: Record<string, unknown> | null;
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const sinks = createRegistry<AuditSink>("audit/sinks");
|
|
58
|
+
|
|
59
|
+
/** Register (or replace) a sink under a stable name. */
|
|
60
|
+
export function registerAuditSink(name: string, sink: AuditSink): void {
|
|
61
|
+
sinks.set(name, sink);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registeredAuditSinks(): string[] {
|
|
65
|
+
return [...sinks.keys()];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function clearAuditSinks(): void {
|
|
69
|
+
sinks.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function fanOut(event: RecordedAuditEvent): Promise<void> {
|
|
73
|
+
if (sinks.size === 0) return;
|
|
74
|
+
|
|
75
|
+
await Promise.all(
|
|
76
|
+
[...sinks.entries()].map(async ([name, sink]) => {
|
|
77
|
+
try {
|
|
78
|
+
await sink(event);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
log.error("Audit sink failed", {
|
|
81
|
+
sink: name,
|
|
82
|
+
action: event.action,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type AuditActorKind = "user" | "system" | "support";
|
|
91
|
+
export type AuditOutcome = "ok" | "failed";
|
|
92
|
+
|
|
93
|
+
export type AuditEventInput = {
|
|
94
|
+
/** Omit for platform-level events with no tenant. */
|
|
95
|
+
workspaceId?: string | null;
|
|
96
|
+
/** Omit for system actors (cron, webhook, job runner). */
|
|
97
|
+
actorId?: string | null;
|
|
98
|
+
actorKind?: AuditActorKind;
|
|
99
|
+
/** Dotted verb, e.g. "execution.completed". */
|
|
100
|
+
action: string;
|
|
101
|
+
/** Resource type, e.g. "execution". */
|
|
102
|
+
resourceKind: string;
|
|
103
|
+
resourceId?: string | null;
|
|
104
|
+
outcome?: AuditOutcome;
|
|
105
|
+
metadata?: Record<string, unknown>;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function toRow(event: AuditEventInput) {
|
|
109
|
+
return {
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
workspaceId: event.workspaceId ?? null,
|
|
112
|
+
actorId: event.actorId ?? null,
|
|
113
|
+
actorKind: event.actorKind ?? (event.actorId ? "user" : "system"),
|
|
114
|
+
action: event.action,
|
|
115
|
+
resourceKind: event.resourceKind,
|
|
116
|
+
resourceId: event.resourceId ?? null,
|
|
117
|
+
outcome: event.outcome ?? "ok",
|
|
118
|
+
metadata: event.metadata ?? null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record an audit event. Never throws — a failed audit write is logged
|
|
124
|
+
* and swallowed so it cannot take down the audited operation.
|
|
125
|
+
*/
|
|
126
|
+
export async function recordAuditEvent(event: AuditEventInput): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
const row = toRow(event);
|
|
129
|
+
await db.insert(auditEvents).values(row);
|
|
130
|
+
await fanOut({ ...row, createdAt: new Date() });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
log.error("Audit write failed", {
|
|
133
|
+
action: event.action,
|
|
134
|
+
resourceKind: event.resourceKind,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Record an audit event, propagating failures. Use where the audit
|
|
142
|
+
* trail is part of the contract (impersonation, destructive support
|
|
143
|
+
* actions) and the operation must abort if it cannot be recorded.
|
|
144
|
+
*/
|
|
145
|
+
export async function recordAuditEventOrThrow(
|
|
146
|
+
event: AuditEventInput
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
const row = toRow(event);
|
|
149
|
+
await db.insert(auditEvents).values(row);
|
|
150
|
+
// Sinks stay failure-isolated even here. The contract this function
|
|
151
|
+
// adds is that the *event was recorded*; a downstream exporter being
|
|
152
|
+
// unreachable does not make it unrecorded, and failing the caller
|
|
153
|
+
// over it would mean an expired SIEM token could block impersonation
|
|
154
|
+
// and every destructive support action.
|
|
155
|
+
await fanOut({ ...row, createdAt: new Date() });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type QueryAuditEventsOptions = {
|
|
159
|
+
workspaceId?: string;
|
|
160
|
+
action?: string;
|
|
161
|
+
resourceKind?: string;
|
|
162
|
+
resourceId?: string;
|
|
163
|
+
/** Keyset pagination: return events created strictly before this. */
|
|
164
|
+
before?: Date;
|
|
165
|
+
limit?: number;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/** Most-recent-first query for the admin console. */
|
|
169
|
+
export async function queryAuditEvents(options: QueryAuditEventsOptions = {}) {
|
|
170
|
+
const filters = [
|
|
171
|
+
options.workspaceId
|
|
172
|
+
? eq(auditEvents.workspaceId, options.workspaceId)
|
|
173
|
+
: undefined,
|
|
174
|
+
options.action ? eq(auditEvents.action, options.action) : undefined,
|
|
175
|
+
options.resourceKind
|
|
176
|
+
? eq(auditEvents.resourceKind, options.resourceKind)
|
|
177
|
+
: undefined,
|
|
178
|
+
options.resourceId
|
|
179
|
+
? eq(auditEvents.resourceId, options.resourceId)
|
|
180
|
+
: undefined,
|
|
181
|
+
options.before ? lt(auditEvents.createdAt, options.before) : undefined,
|
|
182
|
+
].filter(Boolean);
|
|
183
|
+
|
|
184
|
+
return db
|
|
185
|
+
.select()
|
|
186
|
+
.from(auditEvents)
|
|
187
|
+
.where(filters.length > 0 ? and(...filters) : undefined)
|
|
188
|
+
.orderBy(desc(auditEvents.createdAt))
|
|
189
|
+
.limit(Math.min(options.limit ?? 50, 500));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export { auditEvents } from "./db/schema";
|
|
193
|
+
export type { AuditEvent, InsertAuditEvent } from "./db/schema";
|
|
194
|
+
|
|
195
|
+
// Memory-audit event contract — see ./memory-audit.ts for
|
|
196
|
+
// why this is a type-only re-export rather than a second writer.
|
|
197
|
+
export type {
|
|
198
|
+
AuditTargetKind as MemoryAuditTargetKind,
|
|
199
|
+
AuditAction as MemoryAuditAction,
|
|
200
|
+
AuditActorKind as MemoryAuditActorKind,
|
|
201
|
+
UserMemoryAuditRow,
|
|
202
|
+
InsertUserMemoryAudit,
|
|
203
|
+
RecordMemoryAuditInput,
|
|
204
|
+
} from "./memory-audit";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-audit event contract.
|
|
3
|
+
*
|
|
4
|
+
* `@intelligo-dev/core` may depend on nothing, so the identity module
|
|
5
|
+
* that owns `user_memory_audit` cannot call a writer defined here; the
|
|
6
|
+
* write lives in `packages/core/src/identity/audit.ts`. This module is
|
|
7
|
+
* the public type for packages that depend on `@intelligo-dev/audit`
|
|
8
|
+
* and want to describe a memory-audit event without reaching into
|
|
9
|
+
* `@intelligo-dev/core/identity`'s internals.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
AuditTargetKind,
|
|
14
|
+
AuditAction,
|
|
15
|
+
AuditActorKind,
|
|
16
|
+
UserMemoryAuditRow,
|
|
17
|
+
InsertUserMemoryAudit,
|
|
18
|
+
} from "@intelligo-dev/core/db/schema";
|
|
19
|
+
|
|
20
|
+
export type {
|
|
21
|
+
AuditTargetKind,
|
|
22
|
+
AuditAction,
|
|
23
|
+
AuditActorKind,
|
|
24
|
+
UserMemoryAuditRow,
|
|
25
|
+
InsertUserMemoryAudit,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The shape `recordMemoryAudit` (`packages/core/src/identity/audit.ts`)
|
|
30
|
+
* accepts.
|
|
31
|
+
*/
|
|
32
|
+
export type RecordMemoryAuditInput = {
|
|
33
|
+
userId: string;
|
|
34
|
+
workspaceId: string;
|
|
35
|
+
targetKind: AuditTargetKind;
|
|
36
|
+
targetId: string;
|
|
37
|
+
action: AuditAction;
|
|
38
|
+
actorKind: AuditActorKind;
|
|
39
|
+
actorId?: string;
|
|
40
|
+
beforeValue?: unknown;
|
|
41
|
+
afterValue?: unknown;
|
|
42
|
+
reason?: string;
|
|
43
|
+
};
|