@orthacms/activity-server 0.0.0-reserve.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +16 -0
  6. package/dist/lib/activity/activity-filter.d.ts +14 -0
  7. package/dist/lib/activity/activity-filter.d.ts.map +1 -0
  8. package/dist/lib/activity/activity-filter.js +25 -0
  9. package/dist/lib/activity/activity.constants.d.ts +18 -0
  10. package/dist/lib/activity/activity.constants.d.ts.map +1 -0
  11. package/dist/lib/activity/activity.constants.js +16 -0
  12. package/dist/lib/activity/controllers/list-activity.controller.d.ts +14 -0
  13. package/dist/lib/activity/controllers/list-activity.controller.d.ts.map +1 -0
  14. package/dist/lib/activity/controllers/list-activity.controller.js +36 -0
  15. package/dist/lib/activity/dto/list-activity-query.dto.d.ts +46 -0
  16. package/dist/lib/activity/dto/list-activity-query.dto.d.ts.map +1 -0
  17. package/dist/lib/activity/dto/list-activity-query.dto.js +197 -0
  18. package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts +60 -0
  19. package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts.map +1 -0
  20. package/dist/lib/activity/infrastructure/audit-event-mapping.js +402 -0
  21. package/dist/lib/activity/infrastructure/audit-event.subscriber.d.ts +29 -0
  22. package/dist/lib/activity/infrastructure/audit-event.subscriber.d.ts.map +1 -0
  23. package/dist/lib/activity/infrastructure/audit-event.subscriber.js +54 -0
  24. package/dist/lib/activity/services/activity.service.d.ts +55 -0
  25. package/dist/lib/activity/services/activity.service.d.ts.map +1 -0
  26. package/dist/lib/activity/services/activity.service.js +147 -0
  27. package/dist/lib/activity/types/activity-view.d.ts +37 -0
  28. package/dist/lib/activity/types/activity-view.d.ts.map +1 -0
  29. package/dist/lib/activity/types/activity-view.js +2 -0
  30. package/dist/lib/activity.module.d.ts +20 -0
  31. package/dist/lib/activity.module.d.ts.map +1 -0
  32. package/dist/lib/activity.module.js +49 -0
  33. package/dist/lib/copilot/activity-tool.provider.d.ts +40 -0
  34. package/dist/lib/copilot/activity-tool.provider.d.ts.map +1 -0
  35. package/dist/lib/copilot/activity-tool.provider.js +157 -0
  36. package/dist/lib/schema/activity-events.d.ts +178 -0
  37. package/dist/lib/schema/activity-events.d.ts.map +1 -0
  38. package/dist/lib/schema/activity-events.js +38 -0
  39. package/dist/lib/schema/index.d.ts +2 -0
  40. package/dist/lib/schema/index.d.ts.map +1 -0
  41. package/dist/lib/schema/index.js +5 -0
  42. package/dist/lib/utils/activity-plugin.d.ts +26 -0
  43. package/dist/lib/utils/activity-plugin.d.ts.map +1 -0
  44. package/dist/lib/utils/activity-plugin.js +38 -0
  45. package/migrations/0000_init.sql +15 -0
  46. package/migrations/meta/0000_snapshot.json +159 -0
  47. package/migrations/meta/_journal.json +13 -0
  48. package/package.json +46 -0
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivityService = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const drizzle_orm_1 = require("drizzle-orm");
7
+ const database_1 = require("@orthacms/database");
8
+ const utils_server_1 = require("@orthacms/utils-server");
9
+ const schema_1 = require("../../schema");
10
+ const activity_filter_1 = require("../activity-filter");
11
+ const activity_constants_1 = require("../activity.constants");
12
+ /** The sortable wire fields mapped to their `activity_events` columns. */
13
+ const SORT_COLUMNS = {
14
+ at: schema_1.activityEvents.at,
15
+ kind: schema_1.activityEvents.kind
16
+ };
17
+ /**
18
+ * Owns the audit trail: appends events (`record`) and serves the paginated
19
+ * read API (`list`). Implements {@link ActivityRecorder} so foundational
20
+ * plugins can record via the `ACTIVITY_RECORDER` token without depending on
21
+ * this package. Uses the shared Drizzle client directly (no repository
22
+ * wrapper, by repo convention).
23
+ */
24
+ let ActivityService = class ActivityService {
25
+ db;
26
+ constructor(db) {
27
+ this.db = db;
28
+ }
29
+ /**
30
+ * Appends one audit row. Pass `executor = tx` to record **in-band** with a
31
+ * mutation, so the audit row commits iff the mutation does; defaults to the
32
+ * root client. `at` defaults to now; `actorId`/`actorEmail`/`meta` are
33
+ * nullable.
34
+ *
35
+ * @deprecated Wave 3 moved auditing onto the outbox `AuditEventSubscriber`,
36
+ * the single live writer. This method (and the `ACTIVITY_RECORDER` binding)
37
+ * is retained only for a stable public surface — no caller writes through it
38
+ * anymore. Emit a domain event and let the subscriber record it instead.
39
+ */
40
+ async record(input, executor = this.db) {
41
+ await executor.insert(schema_1.activityEvents).values({
42
+ kind: input.kind,
43
+ subjectType: input.subjectType,
44
+ subjectId: input.subjectId,
45
+ actorId: input.actorId ?? null,
46
+ actorEmail: input.actorEmail ?? null,
47
+ meta: input.meta ?? null,
48
+ at: input.at ?? new Date()
49
+ });
50
+ }
51
+ /**
52
+ * One page of events matching the filters, with a stable order: the
53
+ * whitelisted sort column (default `at desc`) plus an `id` tiebreaker so
54
+ * paging is deterministic across equal timestamps. `created_at` is never
55
+ * selected — the immutable write time stays off the wire.
56
+ */
57
+ async list(query) {
58
+ const page = query.page ?? 1;
59
+ const pageSize = query.pageSize ?? activity_constants_1.DEFAULT_PAGE_SIZE;
60
+ const where = await this.listWhere(query);
61
+ const sortColumn = SORT_COLUMNS[query.sort ?? 'at'];
62
+ const direction = query.order === 'asc' ? drizzle_orm_1.asc : drizzle_orm_1.desc;
63
+ // Count and page rows share the same WHERE but are otherwise
64
+ // independent; run them concurrently so a list request pays the max
65
+ // of the two query times, not their sum.
66
+ const [[{ total }], rows] = await Promise.all([
67
+ this.db
68
+ .select({ total: (0, drizzle_orm_1.count)() })
69
+ .from(schema_1.activityEvents)
70
+ .where(where),
71
+ this.db
72
+ .select({
73
+ id: schema_1.activityEvents.id,
74
+ kind: schema_1.activityEvents.kind,
75
+ subjectType: schema_1.activityEvents.subjectType,
76
+ subjectId: schema_1.activityEvents.subjectId,
77
+ actorId: schema_1.activityEvents.actorId,
78
+ actorEmail: schema_1.activityEvents.actorEmail,
79
+ meta: schema_1.activityEvents.meta,
80
+ at: schema_1.activityEvents.at
81
+ })
82
+ .from(schema_1.activityEvents)
83
+ .where(where)
84
+ .orderBy(direction(sortColumn), (0, drizzle_orm_1.desc)(schema_1.activityEvents.id))
85
+ .limit(pageSize)
86
+ .offset((page - 1) * pageSize)
87
+ ]);
88
+ return {
89
+ items: rows.map((row) => ({
90
+ ...row,
91
+ meta: row.meta ?? null
92
+ })),
93
+ total,
94
+ page,
95
+ pageSize
96
+ };
97
+ }
98
+ /**
99
+ * The full `where` for the list: the structured params (`listPredicate`)
100
+ * AND-ed with the optional query-builder `?filter=` tree. The filter is
101
+ * parsed and translated against {@link ACTIVITY_FILTER_SCHEMA}; a malformed
102
+ * filter throws a `FilterException` (HTTP 400). `and(undefined, …)`
103
+ * collapses cleanly, so an unfiltered list still scans everything.
104
+ */
105
+ async listWhere(query) {
106
+ const tree = (0, utils_server_1.parseFilterTree)(query.filter, activity_filter_1.ACTIVITY_FILTER_SCHEMA);
107
+ const filterSql = await (0, utils_server_1.applyFilterTree)(tree, activity_filter_1.ACTIVITY_FILTER_SCHEMA, schema_1.activityEvents, this.db);
108
+ return (0, drizzle_orm_1.and)(this.listPredicate(query), filterSql);
109
+ }
110
+ /**
111
+ * The structured-param `where` for the list: every supplied filter
112
+ * intersected (AND). `and(undefined, …)` collapses to no filter, so an
113
+ * unfiltered list scans everything.
114
+ */
115
+ listPredicate(query) {
116
+ return (0, drizzle_orm_1.and)(query.subjectType
117
+ ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.subjectType, query.subjectType)
118
+ : undefined, query.subjectId
119
+ ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.subjectId, query.subjectId)
120
+ : undefined, query.actorId
121
+ ? (0, drizzle_orm_1.eq)(schema_1.activityEvents.actorId, query.actorId)
122
+ : undefined, query.kind && query.kind.length > 0
123
+ ? (0, drizzle_orm_1.inArray)(schema_1.activityEvents.kind, query.kind)
124
+ : undefined, this.actorEmailPredicate(query.actorEmail), query.from
125
+ ? (0, drizzle_orm_1.gte)(schema_1.activityEvents.at, new Date(query.from))
126
+ : undefined, query.to ? (0, drizzle_orm_1.lte)(schema_1.activityEvents.at, new Date(query.to)) : undefined);
127
+ }
128
+ /**
129
+ * Case-insensitive substring match on the actor email snapshot, or
130
+ * `undefined` for no filter. LIKE metacharacters in the needle are escaped
131
+ * so a literal `%`/`_` search behaves literally.
132
+ */
133
+ actorEmailPredicate(search) {
134
+ const needle = search?.trim();
135
+ if (!needle) {
136
+ return undefined;
137
+ }
138
+ const escaped = needle.replace(/[\\%_]/g, '\\$&');
139
+ return (0, drizzle_orm_1.ilike)(schema_1.activityEvents.actorEmail, `%${escaped}%`);
140
+ }
141
+ };
142
+ exports.ActivityService = ActivityService;
143
+ exports.ActivityService = ActivityService = tslib_1.__decorate([
144
+ (0, common_1.Injectable)(),
145
+ tslib_1.__param(0, (0, database_1.InjectDatabase)()),
146
+ tslib_1.__metadata("design:paramtypes", [Object])
147
+ ], ActivityService);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * One audit event as returned by `GET /api/activity`. Mirrors the
3
+ * `activity_events` row minus `createdAt` (the immutable write time is an
4
+ * internal detail and never reaches the wire). `kind` is an open string and
5
+ * `meta` an open record — each emitting plugin owns its own kinds/shapes, and
6
+ * the admin restates the ones it renders.
7
+ */
8
+ export interface ActivityEventView {
9
+ /** Event primary key. */
10
+ id: string;
11
+ /** The `domain.action` kind, owned by the emitting plugin. */
12
+ kind: string;
13
+ /** The kind of entity acted upon (e.g. `'user'`). */
14
+ subjectType: string;
15
+ /** The acted-upon entity's id (text). */
16
+ subjectId: string;
17
+ /** Who performed it, or `null` for a system-initiated event. */
18
+ actorId: string | null;
19
+ /** Frozen email snapshot of the actor, or `null`. */
20
+ actorEmail: string | null;
21
+ /** Open per-kind payload, or `null`. */
22
+ meta: Record<string, unknown> | null;
23
+ /** Logical event time. */
24
+ at: Date;
25
+ }
26
+ /** One page of audit events, as returned by `GET /api/activity`. */
27
+ export interface ActivityListView {
28
+ /** The events on this page. */
29
+ items: ActivityEventView[];
30
+ /** Total events matching the filters, across all pages. */
31
+ total: number;
32
+ /** 1-based page number echoed back. */
33
+ page: number;
34
+ /** Page size echoed back. */
35
+ pageSize: number;
36
+ }
37
+ //# sourceMappingURL=activity-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-view.d.ts","sourceRoot":"","sources":["../../../../src/lib/activity/types/activity-view.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAC9B,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrC,0BAA0B;IAC1B,EAAE,EAAE,IAAI,CAAC;CACZ;AAED,oEAAoE;AACpE,MAAM,WAAW,gBAAgB;IAC7B,+BAA+B;IAC/B,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,20 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ /**
3
+ * NestJS module for the activity plugin. Mounts the read API under
4
+ * `/api/activity`, provides {@link ActivityService}, and provides the
5
+ * {@link AuditEventSubscriber} — the **live** audit writer, which self-registers
6
+ * with the outbox dispatcher on bootstrap so every audited domain event becomes
7
+ * an `activity_events` row (Wave 3 moved auditing off in-band recording).
8
+ *
9
+ * **Global**, so any plugin can read/record without re-importing the module. It
10
+ * still binds `ActivityService` to the `ACTIVITY_RECORDER` token — kept for a
11
+ * stable public surface but **deprecated**; nothing writes through it anymore.
12
+ * The Drizzle client comes from `@orthacms/database`'s global `DatabaseModule`
13
+ * (which also provides the `OutboxDispatcher`); authorization from identity's
14
+ * `PermissionsGuard`.
15
+ */
16
+ export declare class ActivityModule {
17
+ /** Creates the dynamic module: the read controller, the recorder, the subscriber. */
18
+ static forRoot(): DynamicModule;
19
+ }
20
+ //# sourceMappingURL=activity.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity.module.d.ts","sourceRoot":"","sources":["../../src/lib/activity.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAU,MAAM,gBAAgB,CAAC;AAOvD;;;;;;;;;;;;;GAaG;AACH,qBACa,cAAc;IACvB,qFAAqF;IACrF,MAAM,CAAC,OAAO,IAAI,aAAa;CAiBlC"}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var ActivityModule_1;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ActivityModule = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const common_1 = require("@nestjs/common");
7
+ const identity_server_1 = require("@orthacms/identity-server");
8
+ const list_activity_controller_1 = require("./activity/controllers/list-activity.controller");
9
+ const activity_service_1 = require("./activity/services/activity.service");
10
+ const audit_event_subscriber_1 = require("./activity/infrastructure/audit-event.subscriber");
11
+ const activity_tool_provider_1 = require("./copilot/activity-tool.provider");
12
+ /**
13
+ * NestJS module for the activity plugin. Mounts the read API under
14
+ * `/api/activity`, provides {@link ActivityService}, and provides the
15
+ * {@link AuditEventSubscriber} — the **live** audit writer, which self-registers
16
+ * with the outbox dispatcher on bootstrap so every audited domain event becomes
17
+ * an `activity_events` row (Wave 3 moved auditing off in-band recording).
18
+ *
19
+ * **Global**, so any plugin can read/record without re-importing the module. It
20
+ * still binds `ActivityService` to the `ACTIVITY_RECORDER` token — kept for a
21
+ * stable public surface but **deprecated**; nothing writes through it anymore.
22
+ * The Drizzle client comes from `@orthacms/database`'s global `DatabaseModule`
23
+ * (which also provides the `OutboxDispatcher`); authorization from identity's
24
+ * `PermissionsGuard`.
25
+ */
26
+ let ActivityModule = ActivityModule_1 = class ActivityModule {
27
+ /** Creates the dynamic module: the read controller, the recorder, the subscriber. */
28
+ static forRoot() {
29
+ return {
30
+ module: ActivityModule_1,
31
+ global: true,
32
+ controllers: [list_activity_controller_1.ListActivityController],
33
+ providers: [
34
+ activity_service_1.ActivityService,
35
+ audit_event_subscriber_1.AuditEventSubscriber,
36
+ { provide: identity_server_1.ACTIVITY_RECORDER, useExisting: activity_service_1.ActivityService },
37
+ // The copilot's audit-log read. Both no-op when no copilot
38
+ // plugin is registered — the registrar injects the registry
39
+ // optionally.
40
+ activity_tool_provider_1.ActivityCopilotToolProvider
41
+ ],
42
+ exports: [activity_service_1.ActivityService, identity_server_1.ACTIVITY_RECORDER]
43
+ };
44
+ }
45
+ };
46
+ exports.ActivityModule = ActivityModule;
47
+ exports.ActivityModule = ActivityModule = ActivityModule_1 = tslib_1.__decorate([
48
+ (0, common_1.Module)({})
49
+ ], ActivityModule);
@@ -0,0 +1,40 @@
1
+ import { type OnModuleInit } from '@nestjs/common';
2
+ import { ToolRegistry } from '@orthacms/tools-server';
3
+ import type { ToolDefinition, ToolProvider } from '@orthacms/tools-server';
4
+ import { ActivityService } from '../activity/services/activity.service';
5
+ /**
6
+ * The activity plugin's contribution to the copilot's tool catalogue —
7
+ * `activity_recent`, a thin wrapper over the same `ActivityService.list` the
8
+ * read route calls.
9
+ *
10
+ * **This tool is deployment-wide, not workspace-scoped, and that is not an
11
+ * oversight.** `activity_events` has no workspace column: the trail records
12
+ * user invites, role changes and workspace lifecycle alongside content edits,
13
+ * and several of those events belong to no workspace at all. So there is
14
+ * nothing to scope by, and the tool says so in its description rather than
15
+ * implying a boundary it cannot enforce.
16
+ *
17
+ * What bounds it instead is `activity:read`, which the v1 role matrix grants to
18
+ * **admins only** — the same key guarding `GET /api/activity`. The capability
19
+ * profile withholds the tool from everyone else at *offer* time, so a viewer's
20
+ * or contributor's run is never told it exists (ADR-0005 §3). An admin asking
21
+ * their copilot about the audit log reads exactly what they could already read
22
+ * by opening the Activity page.
23
+ */
24
+ export declare class ActivityCopilotToolProvider implements ToolProvider, OnModuleInit {
25
+ private readonly activity;
26
+ private readonly toolRegistry?;
27
+ constructor(activity: ActivityService, toolRegistry?: ToolRegistry | undefined);
28
+ /**
29
+ * Register with the shared tool registry once the DI graph is built —
30
+ * the same catalogue the MCP endpoint serves, narrowed to the `copilot`
31
+ * surface by each tool's `surfaces`. `@Optional()` because a deployment
32
+ * may run neither consumer, in which case these simply go unregistered.
33
+ */
34
+ onModuleInit(): void;
35
+ /** The one activity read tool. */
36
+ tools(): readonly ToolDefinition[];
37
+ /** `activity_recent` — a page of the audit trail, newest first. */
38
+ private recent;
39
+ }
40
+ //# sourceMappingURL=activity-tool.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-tool.provider.d.ts","sourceRoot":"","sources":["../../../src/lib/copilot/activity-tool.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAKxE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,2BAA4B,YAAW,YAAY,EAAE,YAAY;IAEtE,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACb,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;gBADzB,QAAQ,EAAE,eAAe,EACb,YAAY,CAAC,EAAE,YAAY,YAAA;IAG5D;;;;;OAKG;IACH,YAAY,IAAI,IAAI;IAIpB,kCAAkC;IAClC,KAAK,IAAI,SAAS,cAAc,EAAE;IAIlC,mEAAmE;IACnE,OAAO,CAAC,MAAM;CAwHjB"}
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActivityCopilotToolProvider = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const identity_server_1 = require("@orthacms/identity-server");
7
+ const tools_server_1 = require("@orthacms/tools-server");
8
+ const activity_service_1 = require("../activity/services/activity.service");
9
+ /** Events a single `activity_recent` call may return. */
10
+ const MAX_TOOL_PAGE_SIZE = 25;
11
+ /**
12
+ * The activity plugin's contribution to the copilot's tool catalogue —
13
+ * `activity_recent`, a thin wrapper over the same `ActivityService.list` the
14
+ * read route calls.
15
+ *
16
+ * **This tool is deployment-wide, not workspace-scoped, and that is not an
17
+ * oversight.** `activity_events` has no workspace column: the trail records
18
+ * user invites, role changes and workspace lifecycle alongside content edits,
19
+ * and several of those events belong to no workspace at all. So there is
20
+ * nothing to scope by, and the tool says so in its description rather than
21
+ * implying a boundary it cannot enforce.
22
+ *
23
+ * What bounds it instead is `activity:read`, which the v1 role matrix grants to
24
+ * **admins only** — the same key guarding `GET /api/activity`. The capability
25
+ * profile withholds the tool from everyone else at *offer* time, so a viewer's
26
+ * or contributor's run is never told it exists (ADR-0005 §3). An admin asking
27
+ * their copilot about the audit log reads exactly what they could already read
28
+ * by opening the Activity page.
29
+ */
30
+ let ActivityCopilotToolProvider = class ActivityCopilotToolProvider {
31
+ activity;
32
+ toolRegistry;
33
+ constructor(activity, toolRegistry) {
34
+ this.activity = activity;
35
+ this.toolRegistry = toolRegistry;
36
+ }
37
+ /**
38
+ * Register with the shared tool registry once the DI graph is built —
39
+ * the same catalogue the MCP endpoint serves, narrowed to the `copilot`
40
+ * surface by each tool's `surfaces`. `@Optional()` because a deployment
41
+ * may run neither consumer, in which case these simply go unregistered.
42
+ */
43
+ onModuleInit() {
44
+ this.toolRegistry?.register(this);
45
+ }
46
+ /** The one activity read tool. */
47
+ tools() {
48
+ return [this.recent()];
49
+ }
50
+ /** `activity_recent` — a page of the audit trail, newest first. */
51
+ recent() {
52
+ return {
53
+ name: 'activity_recent',
54
+ title: 'Recent activity',
55
+ description: 'Read the audit trail — who did what, when. Filter by event kind ' +
56
+ '(e.g. "entry.published"), by the subject acted upon, by actor, or by a time ' +
57
+ 'window. Newest first. This log covers the whole deployment, not just the ' +
58
+ 'current workspace, and it includes account and workspace administration as ' +
59
+ 'well as content changes, so say which scope you are reporting on.',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ kind: {
64
+ type: 'array',
65
+ items: { type: 'string', maxLength: 255 },
66
+ description: 'Event kinds to include, matched exactly (e.g. ["entry.published"]). ' +
67
+ 'Kinds are owned by the emitting plugins — read one off a result ' +
68
+ 'rather than guessing.'
69
+ },
70
+ subjectType: {
71
+ type: 'string',
72
+ maxLength: 255,
73
+ description: 'Restrict to one kind of subject, e.g. "user" or "entry".'
74
+ },
75
+ subjectId: {
76
+ type: 'string',
77
+ maxLength: 255,
78
+ description: 'Restrict to one entity’s history — e.g. an entry id, to see ' +
79
+ 'everything that happened to it.'
80
+ },
81
+ actorEmail: {
82
+ type: 'string',
83
+ maxLength: 255,
84
+ description: 'Case-insensitive substring of the actor’s email, as recorded at ' +
85
+ 'the time. Use this rather than a user id when the question names ' +
86
+ 'a person.'
87
+ },
88
+ from: {
89
+ type: 'string',
90
+ description: 'Inclusive lower bound on the event time, ISO 8601 (e.g. "2026-01-01T00:00:00Z").'
91
+ },
92
+ to: {
93
+ type: 'string',
94
+ description: 'Inclusive upper bound on the event time, ISO 8601.'
95
+ },
96
+ page: {
97
+ type: 'integer',
98
+ minimum: 1,
99
+ description: '1-based page number.'
100
+ },
101
+ pageSize: {
102
+ type: 'integer',
103
+ minimum: 1,
104
+ maximum: MAX_TOOL_PAGE_SIZE,
105
+ description: `Events per page (max ${MAX_TOOL_PAGE_SIZE}).`
106
+ }
107
+ },
108
+ additionalProperties: false
109
+ },
110
+ requires: [identity_server_1.PERMISSIONS.ACTIVITY_READ],
111
+ readOnly: true,
112
+ effect: 'read',
113
+ surfaces: ['copilot'],
114
+ handler: async (input) => {
115
+ const args = (input ?? {});
116
+ // Clamped here as well as declared in the schema: the
117
+ // validator is defence in depth, not the boundary.
118
+ const pageSize = Math.min(Math.max(args.pageSize ?? 10, 1), MAX_TOOL_PAGE_SIZE);
119
+ const result = await this.activity.list({
120
+ ...(args.kind?.length ? { kind: args.kind } : {}),
121
+ ...(args.subjectType
122
+ ? { subjectType: args.subjectType }
123
+ : {}),
124
+ ...(args.subjectId ? { subjectId: args.subjectId } : {}),
125
+ ...(args.actorEmail ? { actorEmail: args.actorEmail } : {}),
126
+ ...(args.from ? { from: args.from } : {}),
127
+ ...(args.to ? { to: args.to } : {}),
128
+ page: Math.max(args.page ?? 1, 1),
129
+ pageSize,
130
+ sort: 'at',
131
+ order: 'desc'
132
+ });
133
+ return {
134
+ total: result.total,
135
+ page: result.page,
136
+ pageSize: result.pageSize,
137
+ // `meta` is an open per-kind payload each plugin owns, and
138
+ // some of it is user-authored text. It rides through the
139
+ // engine's untrusted-content fence like any other tool
140
+ // output, so it is passed on rather than stripped — the
141
+ // fence is what makes that safe, not omission.
142
+ items: result.items.map((event) => ({
143
+ ...event,
144
+ at: event.at.toISOString()
145
+ }))
146
+ };
147
+ }
148
+ };
149
+ }
150
+ };
151
+ exports.ActivityCopilotToolProvider = ActivityCopilotToolProvider;
152
+ exports.ActivityCopilotToolProvider = ActivityCopilotToolProvider = tslib_1.__decorate([
153
+ (0, common_1.Injectable)(),
154
+ tslib_1.__param(1, (0, common_1.Optional)()),
155
+ tslib_1.__metadata("design:paramtypes", [activity_service_1.ActivityService,
156
+ tools_server_1.ToolRegistry])
157
+ ], ActivityCopilotToolProvider);
@@ -0,0 +1,178 @@
1
+ /**
2
+ * The append-only audit trail. One row per recorded action.
3
+ *
4
+ * Deliberate isolation choices (the audit log must outlive what it records):
5
+ * - `actorId` is a uuid with **no FK** — the actor may later be deleted, but
6
+ * the audit row must remain.
7
+ * - `actorEmail` is a frozen snapshot of the actor's email at record time, so
8
+ * the trail stays readable even after the user row is gone or renamed.
9
+ * - `subjectId` is **text**, not uuid — subjects are not always users and not
10
+ * always uuid-keyed.
11
+ * - `meta` is an open jsonb payload; each emitting plugin owns its shape.
12
+ * - `at` is the logical event time (defaults to now); `createdAt` is the
13
+ * immutable write time, dropped from the read API.
14
+ *
15
+ * Indexed for the three query shapes the read API serves: a subject's history,
16
+ * an actor's history, and a kind's history — each time-ordered.
17
+ */
18
+ export declare const activityEvents: import("drizzle-orm/pg-core").PgTableWithColumns<{
19
+ name: "activity_events";
20
+ schema: undefined;
21
+ columns: {
22
+ id: import("drizzle-orm/pg-core").PgColumn<{
23
+ name: "id";
24
+ tableName: "activity_events";
25
+ dataType: "string";
26
+ columnType: "PgUUID";
27
+ data: string;
28
+ driverParam: string;
29
+ notNull: true;
30
+ hasDefault: true;
31
+ isPrimaryKey: true;
32
+ isAutoincrement: false;
33
+ hasRuntimeDefault: false;
34
+ enumValues: undefined;
35
+ baseColumn: never;
36
+ identity: undefined;
37
+ generated: undefined;
38
+ }, {}, {}>;
39
+ kind: import("drizzle-orm/pg-core").PgColumn<{
40
+ name: "kind";
41
+ tableName: "activity_events";
42
+ dataType: "string";
43
+ columnType: "PgText";
44
+ data: string;
45
+ driverParam: string;
46
+ notNull: true;
47
+ hasDefault: false;
48
+ isPrimaryKey: false;
49
+ isAutoincrement: false;
50
+ hasRuntimeDefault: false;
51
+ enumValues: [string, ...string[]];
52
+ baseColumn: never;
53
+ identity: undefined;
54
+ generated: undefined;
55
+ }, {}, {}>;
56
+ subjectType: import("drizzle-orm/pg-core").PgColumn<{
57
+ name: "subject_type";
58
+ tableName: "activity_events";
59
+ dataType: "string";
60
+ columnType: "PgText";
61
+ data: string;
62
+ driverParam: string;
63
+ notNull: true;
64
+ hasDefault: false;
65
+ isPrimaryKey: false;
66
+ isAutoincrement: false;
67
+ hasRuntimeDefault: false;
68
+ enumValues: [string, ...string[]];
69
+ baseColumn: never;
70
+ identity: undefined;
71
+ generated: undefined;
72
+ }, {}, {}>;
73
+ subjectId: import("drizzle-orm/pg-core").PgColumn<{
74
+ name: "subject_id";
75
+ tableName: "activity_events";
76
+ dataType: "string";
77
+ columnType: "PgText";
78
+ data: string;
79
+ driverParam: string;
80
+ notNull: true;
81
+ hasDefault: false;
82
+ isPrimaryKey: false;
83
+ isAutoincrement: false;
84
+ hasRuntimeDefault: false;
85
+ enumValues: [string, ...string[]];
86
+ baseColumn: never;
87
+ identity: undefined;
88
+ generated: undefined;
89
+ }, {}, {}>;
90
+ actorId: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "actor_id";
92
+ tableName: "activity_events";
93
+ dataType: "string";
94
+ columnType: "PgUUID";
95
+ data: string;
96
+ driverParam: string;
97
+ notNull: false;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: undefined;
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ actorEmail: import("drizzle-orm/pg-core").PgColumn<{
108
+ name: "actor_email";
109
+ tableName: "activity_events";
110
+ dataType: "string";
111
+ columnType: "PgText";
112
+ data: string;
113
+ driverParam: string;
114
+ notNull: false;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: [string, ...string[]];
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ meta: import("drizzle-orm/pg-core").PgColumn<{
125
+ name: "meta";
126
+ tableName: "activity_events";
127
+ dataType: "json";
128
+ columnType: "PgJsonb";
129
+ data: unknown;
130
+ driverParam: unknown;
131
+ notNull: false;
132
+ hasDefault: false;
133
+ isPrimaryKey: false;
134
+ isAutoincrement: false;
135
+ hasRuntimeDefault: false;
136
+ enumValues: undefined;
137
+ baseColumn: never;
138
+ identity: undefined;
139
+ generated: undefined;
140
+ }, {}, {}>;
141
+ at: import("drizzle-orm/pg-core").PgColumn<{
142
+ name: "at";
143
+ tableName: "activity_events";
144
+ dataType: "date";
145
+ columnType: "PgTimestamp";
146
+ data: Date;
147
+ driverParam: string;
148
+ notNull: true;
149
+ hasDefault: true;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: undefined;
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {}>;
158
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
159
+ name: "created_at";
160
+ tableName: "activity_events";
161
+ dataType: "date";
162
+ columnType: "PgTimestamp";
163
+ data: Date;
164
+ driverParam: string;
165
+ notNull: true;
166
+ hasDefault: true;
167
+ isPrimaryKey: false;
168
+ isAutoincrement: false;
169
+ hasRuntimeDefault: false;
170
+ enumValues: undefined;
171
+ baseColumn: never;
172
+ identity: undefined;
173
+ generated: undefined;
174
+ }, {}, {}>;
175
+ };
176
+ dialect: "pg";
177
+ }>;
178
+ //# sourceMappingURL=activity-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activity-events.d.ts","sourceRoot":"","sources":["../../../src/lib/schema/activity-events.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwB1B,CAAC"}