@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/lib/activity/activity-filter.d.ts +14 -0
- package/dist/lib/activity/activity-filter.d.ts.map +1 -0
- package/dist/lib/activity/activity-filter.js +25 -0
- package/dist/lib/activity/activity.constants.d.ts +18 -0
- package/dist/lib/activity/activity.constants.d.ts.map +1 -0
- package/dist/lib/activity/activity.constants.js +16 -0
- package/dist/lib/activity/controllers/list-activity.controller.d.ts +14 -0
- package/dist/lib/activity/controllers/list-activity.controller.d.ts.map +1 -0
- package/dist/lib/activity/controllers/list-activity.controller.js +36 -0
- package/dist/lib/activity/dto/list-activity-query.dto.d.ts +46 -0
- package/dist/lib/activity/dto/list-activity-query.dto.d.ts.map +1 -0
- package/dist/lib/activity/dto/list-activity-query.dto.js +197 -0
- package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts +60 -0
- package/dist/lib/activity/infrastructure/audit-event-mapping.d.ts.map +1 -0
- package/dist/lib/activity/infrastructure/audit-event-mapping.js +402 -0
- package/dist/lib/activity/infrastructure/audit-event.subscriber.d.ts +29 -0
- package/dist/lib/activity/infrastructure/audit-event.subscriber.d.ts.map +1 -0
- package/dist/lib/activity/infrastructure/audit-event.subscriber.js +54 -0
- package/dist/lib/activity/services/activity.service.d.ts +55 -0
- package/dist/lib/activity/services/activity.service.d.ts.map +1 -0
- package/dist/lib/activity/services/activity.service.js +147 -0
- package/dist/lib/activity/types/activity-view.d.ts +37 -0
- package/dist/lib/activity/types/activity-view.d.ts.map +1 -0
- package/dist/lib/activity/types/activity-view.js +2 -0
- package/dist/lib/activity.module.d.ts +20 -0
- package/dist/lib/activity.module.d.ts.map +1 -0
- package/dist/lib/activity.module.js +49 -0
- package/dist/lib/copilot/activity-tool.provider.d.ts +40 -0
- package/dist/lib/copilot/activity-tool.provider.d.ts.map +1 -0
- package/dist/lib/copilot/activity-tool.provider.js +157 -0
- package/dist/lib/schema/activity-events.d.ts +178 -0
- package/dist/lib/schema/activity-events.d.ts.map +1 -0
- package/dist/lib/schema/activity-events.js +38 -0
- package/dist/lib/schema/index.d.ts +2 -0
- package/dist/lib/schema/index.d.ts.map +1 -0
- package/dist/lib/schema/index.js +5 -0
- package/dist/lib/utils/activity-plugin.d.ts +26 -0
- package/dist/lib/utils/activity-plugin.d.ts.map +1 -0
- package/dist/lib/utils/activity-plugin.js +38 -0
- package/migrations/0000_init.sql +15 -0
- package/migrations/meta/0000_snapshot.json +159 -0
- package/migrations/meta/_journal.json +13 -0
- 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,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"}
|