@bevel-software/platform-core-backend 0.19.0 → 0.20.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 (255) hide show
  1. package/dist/core/create-core-server.d.ts.map +1 -1
  2. package/dist/core/create-core-server.js +22 -0
  3. package/dist/core/create-core-server.js.map +1 -1
  4. package/dist/core/create-core-services.d.ts +3 -0
  5. package/dist/core/create-core-services.d.ts.map +1 -1
  6. package/dist/core/create-core-services.js +10 -1
  7. package/dist/core/create-core-services.js.map +1 -1
  8. package/dist/modules/audit/agent-audit.service.d.ts +54 -0
  9. package/dist/modules/audit/agent-audit.service.d.ts.map +1 -0
  10. package/dist/modules/audit/agent-audit.service.js +290 -0
  11. package/dist/modules/audit/agent-audit.service.js.map +1 -0
  12. package/dist/modules/audit/agent-rest-audit.middleware.d.ts +40 -0
  13. package/dist/modules/audit/agent-rest-audit.middleware.d.ts.map +1 -0
  14. package/dist/modules/audit/agent-rest-audit.middleware.js +75 -0
  15. package/dist/modules/audit/agent-rest-audit.middleware.js.map +1 -0
  16. package/dist/modules/audit/audit.contract.d.ts +145 -0
  17. package/dist/modules/audit/audit.contract.d.ts.map +1 -0
  18. package/dist/modules/audit/audit.contract.js +35 -0
  19. package/dist/modules/audit/audit.contract.js.map +1 -0
  20. package/dist/modules/audit/audit.routes.d.ts +21 -0
  21. package/dist/modules/audit/audit.routes.d.ts.map +1 -0
  22. package/dist/modules/audit/audit.routes.js +157 -0
  23. package/dist/modules/audit/audit.routes.js.map +1 -0
  24. package/dist/modules/audit/event-classifier.d.ts +52 -0
  25. package/dist/modules/audit/event-classifier.d.ts.map +1 -0
  26. package/dist/modules/audit/event-classifier.js +123 -0
  27. package/dist/modules/audit/event-classifier.js.map +1 -0
  28. package/dist/modules/audit/index.d.ts +7 -0
  29. package/dist/modules/audit/index.d.ts.map +1 -0
  30. package/dist/modules/audit/index.js +7 -0
  31. package/dist/modules/audit/index.js.map +1 -0
  32. package/dist/modules/audit/request-audit.d.ts +57 -0
  33. package/dist/modules/audit/request-audit.d.ts.map +1 -0
  34. package/dist/modules/audit/request-audit.js +117 -0
  35. package/dist/modules/audit/request-audit.js.map +1 -0
  36. package/dist/modules/database/core-schema.d.ts +414 -0
  37. package/dist/modules/database/core-schema.d.ts.map +1 -1
  38. package/dist/modules/database/core-schema.js +99 -0
  39. package/dist/modules/database/core-schema.js.map +1 -1
  40. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  41. package/dist/modules/diff/diff.service.js +6 -1
  42. package/dist/modules/diff/diff.service.js.map +1 -1
  43. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts +9 -1
  44. package/dist/modules/kb-fs/git-guarded-filesystem.d.ts.map +1 -1
  45. package/dist/modules/kb-fs/git-guarded-filesystem.js +16 -3
  46. package/dist/modules/kb-fs/git-guarded-filesystem.js.map +1 -1
  47. package/dist/modules/mcp/mcp-auth.middleware.d.ts +1 -0
  48. package/dist/modules/mcp/mcp-auth.middleware.d.ts.map +1 -1
  49. package/dist/modules/mcp/mcp-auth.middleware.js +32 -0
  50. package/dist/modules/mcp/mcp-auth.middleware.js.map +1 -1
  51. package/dist/modules/mcp/mcp.routes.d.ts.map +1 -1
  52. package/dist/modules/mcp/mcp.routes.js +8 -1
  53. package/dist/modules/mcp/mcp.routes.js.map +1 -1
  54. package/dist/modules/mcp/mcp.service.d.ts +9 -1
  55. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  56. package/dist/modules/mcp/mcp.service.js +48 -5
  57. package/dist/modules/mcp/mcp.service.js.map +1 -1
  58. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts +59 -0
  59. package/dist/modules/mcp/oauth/bevel-oauth-provider.d.ts.map +1 -1
  60. package/dist/modules/mcp/oauth/bevel-oauth-provider.js +180 -8
  61. package/dist/modules/mcp/oauth/bevel-oauth-provider.js.map +1 -1
  62. package/dist/modules/settings/deployment-settings.service.d.ts +10 -2
  63. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -1
  64. package/dist/modules/settings/deployment-settings.service.js +32 -6
  65. package/dist/modules/settings/deployment-settings.service.js.map +1 -1
  66. package/dist/modules/tool-auth/external-api-key.interface.d.ts +21 -5
  67. package/dist/modules/tool-auth/external-api-key.interface.d.ts.map +1 -1
  68. package/dist/modules/tool-auth/external-api-key.interface.js.map +1 -1
  69. package/dist/modules/tool-auth/external-api-key.service.d.ts +1 -0
  70. package/dist/modules/tool-auth/external-api-key.service.d.ts.map +1 -1
  71. package/dist/modules/tool-auth/external-api-key.service.js +22 -9
  72. package/dist/modules/tool-auth/external-api-key.service.js.map +1 -1
  73. package/dist/modules/tool-auth/internal-token.service.d.ts +10 -0
  74. package/dist/modules/tool-auth/internal-token.service.d.ts.map +1 -1
  75. package/dist/modules/tool-auth/internal-token.service.js +1 -0
  76. package/dist/modules/tool-auth/internal-token.service.js.map +1 -1
  77. package/dist/modules/tool-auth/tool-auth.middleware.d.ts +8 -0
  78. package/dist/modules/tool-auth/tool-auth.middleware.d.ts.map +1 -1
  79. package/dist/modules/tool-auth/tool-auth.middleware.js +9 -1
  80. package/dist/modules/tool-auth/tool-auth.middleware.js.map +1 -1
  81. package/dist/modules/workspace/workspace.service.d.ts +8 -0
  82. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  83. package/dist/modules/workspace/workspace.service.js +9 -0
  84. package/dist/modules/workspace/workspace.service.js.map +1 -1
  85. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  86. package/dist/modules/workspace/workspace.tools.js +49 -13
  87. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  88. package/dist/shared/git-internals.d.ts +7 -1
  89. package/dist/shared/git-internals.d.ts.map +1 -1
  90. package/dist/shared/git-internals.js +67 -9
  91. package/dist/shared/git-internals.js.map +1 -1
  92. package/migrations/0012_agent_audit.sql +41 -0
  93. package/migrations/0013_agent_identity_and_key_soft_delete.sql +29 -0
  94. package/migrations/meta/0012_snapshot.json +2247 -0
  95. package/migrations/meta/0013_snapshot.json +2259 -0
  96. package/migrations/meta/_journal.json +14 -0
  97. package/package.json +4 -4
  98. package/src/core/create-core-server.ts +28 -0
  99. package/src/core/create-core-services.ts +13 -0
  100. package/src/modules/audit/__tests__/agent-audit.service.test.ts +370 -0
  101. package/src/modules/audit/__tests__/agent-rest-audit.middleware.test.ts +113 -0
  102. package/src/modules/audit/__tests__/audit.routes.test.ts +201 -0
  103. package/src/modules/audit/__tests__/event-classifier.test.ts +121 -0
  104. package/src/modules/audit/agent-audit.service.ts +316 -0
  105. package/src/modules/audit/agent-rest-audit.middleware.ts +83 -0
  106. package/src/modules/audit/audit.contract.ts +164 -0
  107. package/src/modules/audit/audit.routes.ts +168 -0
  108. package/src/modules/audit/event-classifier.ts +155 -0
  109. package/src/modules/audit/index.ts +14 -0
  110. package/src/modules/audit/request-audit.ts +141 -0
  111. package/src/modules/database/core-schema.ts +104 -0
  112. package/src/modules/diff/diff.service.ts +6 -1
  113. package/src/modules/kb-fs/git-guarded-filesystem.ts +16 -3
  114. package/src/modules/mcp/__tests__/bevel-oauth-provider.test.ts +183 -13
  115. package/src/modules/mcp/__tests__/mcp-auth.middleware.test.ts +60 -2
  116. package/src/modules/mcp/__tests__/mcp.routes.local-token.test.ts +24 -4
  117. package/src/modules/mcp/__tests__/mcp.routes.stateless.test.ts +1 -1
  118. package/src/modules/mcp/__tests__/mcp.service.test.ts +120 -3
  119. package/src/modules/mcp/mcp-auth.middleware.ts +31 -0
  120. package/src/modules/mcp/mcp.routes.ts +11 -1
  121. package/src/modules/mcp/mcp.service.ts +62 -4
  122. package/src/modules/mcp/oauth/bevel-oauth-provider.ts +184 -6
  123. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +35 -0
  124. package/src/modules/settings/deployment-settings.service.ts +47 -8
  125. package/src/modules/tool-auth/__tests__/external-api-key.service.test.ts +29 -8
  126. package/src/modules/tool-auth/__tests__/internal-token.service.test.ts +6 -0
  127. package/src/modules/tool-auth/external-api-key.interface.ts +22 -5
  128. package/src/modules/tool-auth/external-api-key.service.ts +25 -8
  129. package/src/modules/tool-auth/internal-token.service.ts +11 -0
  130. package/src/modules/tool-auth/tool-auth.middleware.ts +17 -1
  131. package/src/modules/workspace/__tests__/git-internals.security.test.ts +135 -44
  132. package/src/modules/workspace/workspace.service.ts +9 -0
  133. package/src/modules/workspace/workspace.tools.ts +47 -11
  134. package/src/shared/__tests__/git-internals.test.ts +41 -0
  135. package/src/shared/git-internals.ts +61 -6
  136. package/dist/modules/access/access-errors.d.ts +0 -34
  137. package/dist/modules/access/access-errors.d.ts.map +0 -1
  138. package/dist/modules/access/access-errors.js +0 -40
  139. package/dist/modules/access/access-errors.js.map +0 -1
  140. package/dist/modules/access/access-splice.d.ts +0 -140
  141. package/dist/modules/access/access-splice.d.ts.map +0 -1
  142. package/dist/modules/access/access-splice.js +0 -389
  143. package/dist/modules/access/access-splice.js.map +0 -1
  144. package/dist/modules/access/group-files.d.ts +0 -83
  145. package/dist/modules/access/group-files.d.ts.map +0 -1
  146. package/dist/modules/access/group-files.js +0 -167
  147. package/dist/modules/access/group-files.js.map +0 -1
  148. package/dist/modules/access/kb-read-filter.d.ts +0 -41
  149. package/dist/modules/access/kb-read-filter.d.ts.map +0 -1
  150. package/dist/modules/access/kb-read-filter.js +0 -60
  151. package/dist/modules/access/kb-read-filter.js.map +0 -1
  152. package/dist/modules/access/render-roles-yaml.d.ts +0 -22
  153. package/dist/modules/access/render-roles-yaml.d.ts.map +0 -1
  154. package/dist/modules/access/render-roles-yaml.js +0 -56
  155. package/dist/modules/access/render-roles-yaml.js.map +0 -1
  156. package/dist/modules/access/roles-yaml-guard.d.ts +0 -56
  157. package/dist/modules/access/roles-yaml-guard.d.ts.map +0 -1
  158. package/dist/modules/access/roles-yaml-guard.js +0 -79
  159. package/dist/modules/access/roles-yaml-guard.js.map +0 -1
  160. package/dist/modules/diff/diff-paths.d.ts +0 -2
  161. package/dist/modules/diff/diff-paths.d.ts.map +0 -1
  162. package/dist/modules/diff/diff-paths.js +0 -9
  163. package/dist/modules/diff/diff-paths.js.map +0 -1
  164. package/dist/modules/groups/group-provision.service.d.ts +0 -130
  165. package/dist/modules/groups/group-provision.service.d.ts.map +0 -1
  166. package/dist/modules/groups/group-provision.service.js +0 -288
  167. package/dist/modules/groups/group-provision.service.js.map +0 -1
  168. package/dist/modules/groups/groups.contract.d.ts +0 -106
  169. package/dist/modules/groups/groups.contract.d.ts.map +0 -1
  170. package/dist/modules/groups/groups.contract.js +0 -36
  171. package/dist/modules/groups/groups.contract.js.map +0 -1
  172. package/dist/modules/groups/groups.routes.d.ts +0 -42
  173. package/dist/modules/groups/groups.routes.d.ts.map +0 -1
  174. package/dist/modules/groups/groups.routes.js +0 -379
  175. package/dist/modules/groups/groups.routes.js.map +0 -1
  176. package/dist/modules/groups/groups.service.d.ts +0 -60
  177. package/dist/modules/groups/groups.service.d.ts.map +0 -1
  178. package/dist/modules/groups/groups.service.js +0 -172
  179. package/dist/modules/groups/groups.service.js.map +0 -1
  180. package/dist/modules/groups/index.d.ts +0 -7
  181. package/dist/modules/groups/index.d.ts.map +0 -1
  182. package/dist/modules/groups/index.js +0 -6
  183. package/dist/modules/groups/index.js.map +0 -1
  184. package/dist/modules/groups/join-proposals.d.ts +0 -53
  185. package/dist/modules/groups/join-proposals.d.ts.map +0 -1
  186. package/dist/modules/groups/join-proposals.js +0 -67
  187. package/dist/modules/groups/join-proposals.js.map +0 -1
  188. package/dist/modules/groups/join-requests.service.d.ts +0 -81
  189. package/dist/modules/groups/join-requests.service.d.ts.map +0 -1
  190. package/dist/modules/groups/join-requests.service.js +0 -135
  191. package/dist/modules/groups/join-requests.service.js.map +0 -1
  192. package/dist/modules/mcp/mcp-session-store.d.ts +0 -74
  193. package/dist/modules/mcp/mcp-session-store.d.ts.map +0 -1
  194. package/dist/modules/mcp/mcp-session-store.js +0 -131
  195. package/dist/modules/mcp/mcp-session-store.js.map +0 -1
  196. package/dist/modules/workflow/file-change-notifier.d.ts +0 -38
  197. package/dist/modules/workflow/file-change-notifier.d.ts.map +0 -1
  198. package/dist/modules/workflow/file-change-notifier.js +0 -22
  199. package/dist/modules/workflow/file-change-notifier.js.map +0 -1
  200. package/dist/modules/workflow/git/branch-name.d.ts +0 -10
  201. package/dist/modules/workflow/git/branch-name.d.ts.map +0 -1
  202. package/dist/modules/workflow/git/branch-name.js +0 -76
  203. package/dist/modules/workflow/git/branch-name.js.map +0 -1
  204. package/dist/modules/workflow/git/clone-config.d.ts +0 -61
  205. package/dist/modules/workflow/git/clone-config.d.ts.map +0 -1
  206. package/dist/modules/workflow/git/clone-config.js +0 -69
  207. package/dist/modules/workflow/git/clone-config.js.map +0 -1
  208. package/dist/modules/workflow/git/mutex.d.ts +0 -11
  209. package/dist/modules/workflow/git/mutex.d.ts.map +0 -1
  210. package/dist/modules/workflow/git/mutex.js +0 -23
  211. package/dist/modules/workflow/git/mutex.js.map +0 -1
  212. package/dist/modules/workflow/locking-filesystem.d.ts +0 -137
  213. package/dist/modules/workflow/locking-filesystem.d.ts.map +0 -1
  214. package/dist/modules/workflow/locking-filesystem.js +0 -553
  215. package/dist/modules/workflow/locking-filesystem.js.map +0 -1
  216. package/dist/modules/workflow/read-only-filesystem.d.ts +0 -24
  217. package/dist/modules/workflow/read-only-filesystem.d.ts.map +0 -1
  218. package/dist/modules/workflow/read-only-filesystem.js +0 -39
  219. package/dist/modules/workflow/read-only-filesystem.js.map +0 -1
  220. package/dist/modules/workflow/workflow.errors.d.ts +0 -197
  221. package/dist/modules/workflow/workflow.errors.d.ts.map +0 -1
  222. package/dist/modules/workflow/workflow.errors.js +0 -298
  223. package/dist/modules/workflow/workflow.errors.js.map +0 -1
  224. package/dist/modules/workspace/bevel-ignore.d.ts +0 -30
  225. package/dist/modules/workspace/bevel-ignore.d.ts.map +0 -1
  226. package/dist/modules/workspace/bevel-ignore.js +0 -61
  227. package/dist/modules/workspace/bevel-ignore.js.map +0 -1
  228. package/dist/modules/workspace/kb-seed.interface.d.ts +0 -36
  229. package/dist/modules/workspace/kb-seed.interface.d.ts.map +0 -1
  230. package/dist/modules/workspace/kb-seed.interface.js +0 -2
  231. package/dist/modules/workspace/kb-seed.interface.js.map +0 -1
  232. package/dist/modules/workspace/kb-seed.service.d.ts +0 -125
  233. package/dist/modules/workspace/kb-seed.service.d.ts.map +0 -1
  234. package/dist/modules/workspace/kb-seed.service.js +0 -535
  235. package/dist/modules/workspace/kb-seed.service.js.map +0 -1
  236. package/dist/modules/workspace/plugins-migration.d.ts +0 -50
  237. package/dist/modules/workspace/plugins-migration.d.ts.map +0 -1
  238. package/dist/modules/workspace/plugins-migration.js +0 -379
  239. package/dist/modules/workspace/plugins-migration.js.map +0 -1
  240. package/dist/shared/fs-errors.d.ts +0 -8
  241. package/dist/shared/fs-errors.d.ts.map +0 -1
  242. package/dist/shared/fs-errors.js +0 -11
  243. package/dist/shared/fs-errors.js.map +0 -1
  244. package/dist/shared/fs-walk.d.ts +0 -23
  245. package/dist/shared/fs-walk.d.ts.map +0 -1
  246. package/dist/shared/fs-walk.js +0 -42
  247. package/dist/shared/fs-walk.js.map +0 -1
  248. package/dist/shared/hash-email.d.ts +0 -11
  249. package/dist/shared/hash-email.d.ts.map +0 -1
  250. package/dist/shared/hash-email.js +0 -14
  251. package/dist/shared/hash-email.js.map +0 -1
  252. package/dist/shared/kb-walk.d.ts +0 -52
  253. package/dist/shared/kb-walk.d.ts.map +0 -1
  254. package/dist/shared/kb-walk.js +0 -76
  255. package/dist/shared/kb-walk.js.map +0 -1
@@ -0,0 +1,290 @@
1
+ import { and, asc, count, desc, eq, isNull, lt, or } from 'drizzle-orm';
2
+ import { logger } from '../../shared/logging.js';
3
+ import { agentConnections, agentEvents, oauthTokens, users } from '../database/schema.js';
4
+ import { AuditPrincipalNotFoundError, InvalidCursorError, isUuid, parseRetentionWindow, } from './audit.contract.js';
5
+ const log = logger('audit');
6
+ /** The most events one page may carry; the routes clamp to it. */
7
+ export const MAX_EVENT_PAGE = 200;
8
+ /** How often, at most, one process sweeps events past the retention window. */
9
+ const PRUNE_INTERVAL_MS = 60 * 60_000;
10
+ /**
11
+ * The Audit log: the write half the MCP proxy records through, and the read
12
+ * and revoke half its page is built on.
13
+ *
14
+ * Writes are fire-and-forget by contract — a tool result must never wait on,
15
+ * or fail over, its own bookkeeping. Retention is enforced opportunistically
16
+ * on the write path (one sweep per process per hour, piggybacked on an
17
+ * insert), the same way the OAuth tables prune themselves on mint: no
18
+ * scheduler, and a deployment that records nothing has nothing to sweep.
19
+ *
20
+ * Connection keys are read through the key service rather than joined here,
21
+ * so their summary shape (kind, revoked-by, …) has exactly one definition.
22
+ */
23
+ export class AgentAuditService {
24
+ db;
25
+ keys;
26
+ retentionDays;
27
+ now;
28
+ lastPruneAt = 0;
29
+ constructor(db, keys,
30
+ /** Read per sweep, so a changed setting applies without a restart; null = keep forever. */
31
+ retentionDays, now = Date.now) {
32
+ this.db = db;
33
+ this.keys = keys;
34
+ this.retentionDays = retentionDays;
35
+ this.now = now;
36
+ }
37
+ record(event) {
38
+ this.db
39
+ .insert(agentEvents)
40
+ .values({
41
+ userId: event.userId,
42
+ keyId: event.principal.kind === 'key' ? event.principal.id : null,
43
+ connectionId: event.principal.kind === 'agent' ? event.principal.id : null,
44
+ kind: event.kind,
45
+ manual: event.manual,
46
+ name: event.name,
47
+ outcome: event.outcome,
48
+ durationMs: event.durationMs,
49
+ at: new Date(this.now()),
50
+ })
51
+ .then(() => this.maybePrune(), (err) => log.warn('recording an agent event failed:', { err }));
52
+ }
53
+ /**
54
+ * Sweep events older than the retention window — at most once per hour per
55
+ * process, best-effort, never awaited by a caller. A deployment that keeps
56
+ * events forever sweeps nothing.
57
+ */
58
+ async maybePrune() {
59
+ const now = this.now();
60
+ if (now - this.lastPruneAt < PRUNE_INTERVAL_MS)
61
+ return;
62
+ this.lastPruneAt = now;
63
+ const days = this.retentionDays();
64
+ if (days === null)
65
+ return;
66
+ const cutoff = new Date(now - days * 24 * 60 * 60_000);
67
+ try {
68
+ await this.db.delete(agentEvents).where(lt(agentEvents.at, cutoff));
69
+ }
70
+ catch (err) {
71
+ log.warn('agent-event prune failed (non-fatal):', { err });
72
+ }
73
+ }
74
+ async listPrincipals(scope) {
75
+ const mine = scope === 'all' ? null : scope.userId;
76
+ const [keyRows, connectionRows, countRows] = await Promise.all([
77
+ mine === null ? this.keys.listForDeployment() : this.keysForUser(mine),
78
+ this.db
79
+ .select({
80
+ connection: agentConnections,
81
+ user: { id: users.id, email: users.email, name: users.name },
82
+ })
83
+ .from(agentConnections)
84
+ .innerJoin(users, eq(agentConnections.userId, users.id))
85
+ .where(mine === null ? undefined : eq(agentConnections.userId, mine))
86
+ .orderBy(asc(users.email), desc(agentConnections.connectedAt)),
87
+ this.db
88
+ .select({ keyId: agentEvents.keyId, connectionId: agentEvents.connectionId, n: count() })
89
+ .from(agentEvents)
90
+ .where(mine === null ? undefined : eq(agentEvents.userId, mine))
91
+ .groupBy(agentEvents.keyId, agentEvents.connectionId),
92
+ ]);
93
+ const counts = new Map();
94
+ for (const row of countRows) {
95
+ const id = row.keyId ?? row.connectionId;
96
+ if (id)
97
+ counts.set(id, Number(row.n));
98
+ }
99
+ const principals = [
100
+ ...keyRows.map((k) => ({
101
+ kind: 'key',
102
+ id: k.id,
103
+ label: k.label,
104
+ keyKind: k.kind,
105
+ createdAt: k.createdAt,
106
+ lastUsedAt: k.lastUsedAt,
107
+ revokedAt: k.revokedAt,
108
+ revokedBy: k.revokedBy,
109
+ deletedAt: k.deletedAt,
110
+ eventCount: counts.get(k.id) ?? 0,
111
+ user: k.user,
112
+ })),
113
+ ...connectionRows.map(({ connection: c, user }) => ({
114
+ kind: 'agent',
115
+ id: c.id,
116
+ label: c.clientName ?? 'Unnamed agent',
117
+ createdAt: c.connectedAt.getTime(),
118
+ lastUsedAt: c.lastUsedAt ? c.lastUsedAt.getTime() : null,
119
+ revokedAt: c.revokedAt ? c.revokedAt.getTime() : null,
120
+ revokedBy: c.revokedBy ?? null,
121
+ eventCount: counts.get(c.id) ?? 0,
122
+ user,
123
+ })),
124
+ ];
125
+ // Per account (by email), live before revoked, most recently used first,
126
+ // newest first among the never-used — the order the page shows without
127
+ // re-sorting, and the one the Connection keys overview had.
128
+ principals.sort((a, b) => {
129
+ const byEmail = a.user.email.localeCompare(b.user.email);
130
+ if (byEmail !== 0)
131
+ return byEmail;
132
+ const aRevoked = a.revokedAt !== null;
133
+ const bRevoked = b.revokedAt !== null;
134
+ if (aRevoked !== bRevoked)
135
+ return aRevoked ? 1 : -1;
136
+ const byUse = (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
137
+ return byUse !== 0 ? byUse : b.createdAt - a.createdAt;
138
+ });
139
+ return principals;
140
+ }
141
+ /** One person's keys, with the owner attached the way the deployment listing attaches it. */
142
+ async keysForUser(userId) {
143
+ const [keys, [user]] = await Promise.all([
144
+ this.keys.listForUser(userId),
145
+ this.db
146
+ .select({ id: users.id, email: users.email, name: users.name })
147
+ .from(users)
148
+ .where(eq(users.id, userId))
149
+ .limit(1),
150
+ ]);
151
+ if (!user)
152
+ return [];
153
+ return keys.map((k) => ({ ...k, user }));
154
+ }
155
+ async ownerOf(principal) {
156
+ if (principal.kind === 'agent') {
157
+ const [row] = await this.db
158
+ .select({ userId: agentConnections.userId })
159
+ .from(agentConnections)
160
+ .where(eq(agentConnections.id, principal.id))
161
+ .limit(1);
162
+ return row?.userId ?? null;
163
+ }
164
+ // A key is the key service's to answer for, like every other read of one.
165
+ return this.keys.ownerOf(principal.id);
166
+ }
167
+ async listEvents(principal, page) {
168
+ const limit = Math.max(1, Math.min(MAX_EVENT_PAGE, Math.floor(page.limit)));
169
+ const byPrincipal = principal.kind === 'key'
170
+ ? eq(agentEvents.keyId, principal.id)
171
+ : eq(agentEvents.connectionId, principal.id);
172
+ // Keyset on (at, id): a page boundary between two events at the same
173
+ // instant is still a boundary, and a row inserted while the reader pages
174
+ // never shifts what the next page returns.
175
+ const cursor = page.before ? parseCursor(page.before) : null;
176
+ if (page.before && !cursor)
177
+ throw new InvalidCursorError();
178
+ const after = cursor
179
+ ? or(lt(agentEvents.at, cursor.at), and(eq(agentEvents.at, cursor.at), lt(agentEvents.id, cursor.id)))
180
+ : undefined;
181
+ // The total is counted on the FIRST page only: a reader paging back
182
+ // through a busy principal's history would otherwise pay a full count of
183
+ // the same number on every step, for a figure the page already holds.
184
+ const [rows, totals] = await Promise.all([
185
+ this.db
186
+ .select({
187
+ id: agentEvents.id,
188
+ kind: agentEvents.kind,
189
+ manual: agentEvents.manual,
190
+ name: agentEvents.name,
191
+ outcome: agentEvents.outcome,
192
+ at: agentEvents.at,
193
+ })
194
+ .from(agentEvents)
195
+ .where(and(byPrincipal, after))
196
+ .orderBy(desc(agentEvents.at), desc(agentEvents.id))
197
+ .limit(limit + 1),
198
+ cursor ? null : this.db.select({ total: count() }).from(agentEvents).where(byPrincipal),
199
+ ]);
200
+ const hasMore = rows.length > limit;
201
+ const pageRows = hasMore ? rows.slice(0, limit) : rows;
202
+ const events = pageRows.map((r) => ({
203
+ id: r.id,
204
+ kind: r.kind,
205
+ manual: r.manual,
206
+ name: r.name,
207
+ outcome: r.outcome,
208
+ at: r.at.getTime(),
209
+ }));
210
+ const last = pageRows[pageRows.length - 1];
211
+ return {
212
+ events,
213
+ total: totals ? Number(totals[0]?.total ?? 0) : null,
214
+ nextCursor: hasMore && last ? formatCursor(last.at, last.id) : null,
215
+ };
216
+ }
217
+ async revokeConnection(id, by, ownerUserId) {
218
+ const scope = ownerUserId
219
+ ? and(eq(agentConnections.id, id), eq(agentConnections.userId, ownerUserId))
220
+ : eq(agentConnections.id, id);
221
+ await this.db.transaction(async (tx) => {
222
+ const revoked = await tx
223
+ .update(agentConnections)
224
+ .set({ revokedAt: new Date(this.now()), revokedBy: by })
225
+ .where(and(scope, isNull(agentConnections.revokedAt)))
226
+ .returning({ id: agentConnections.id });
227
+ if (revoked.length === 0) {
228
+ // Already revoked stays idempotent; out of scope or unknown throws —
229
+ // the same split `ExternalApiKeyService.markRevoked` makes.
230
+ const [existing] = await tx
231
+ .select({ id: agentConnections.id })
232
+ .from(agentConnections)
233
+ .where(scope)
234
+ .limit(1);
235
+ if (!existing)
236
+ throw new AuditPrincipalNotFoundError();
237
+ return;
238
+ }
239
+ // Every live token of the connection, access and refresh alike: the
240
+ // agent's next request 401s, its refresh fails, and re-authorizing —
241
+ // through the browser, as a person — is the only way back in.
242
+ await tx
243
+ .update(oauthTokens)
244
+ .set({ revokedAt: new Date(this.now()) })
245
+ .where(and(eq(oauthTokens.connectionId, id), isNull(oauthTokens.revokedAt)));
246
+ });
247
+ }
248
+ }
249
+ /** `<epoch ms>.<uuid>` — the page's opaque-enough cursor. */
250
+ function formatCursor(at, id) {
251
+ return `${at.getTime()}.${id}`;
252
+ }
253
+ /**
254
+ * The cursor back into its parts, or null for anything this service did not
255
+ * issue: the id must be a uuid (the column is one, and Postgres refuses to
256
+ * compare it with anything else) and the instant a whole number of epoch
257
+ * milliseconds a `timestamp` can hold.
258
+ */
259
+ function parseCursor(raw) {
260
+ const dot = raw.indexOf('.');
261
+ if (dot < 0)
262
+ return null;
263
+ const msText = raw.slice(0, dot);
264
+ const id = raw.slice(dot + 1);
265
+ if (!/^\d{1,15}$/.test(msText) || !isUuid(id))
266
+ return null;
267
+ return { at: new Date(Number(msText)), id };
268
+ }
269
+ /** The last invalid setting value warned about, so a standing misconfiguration is said once, not once per prune. */
270
+ let warnedInvalidRetention = null;
271
+ /**
272
+ * The retention window the service runs on, in days — or null to keep
273
+ * events forever, which is what a blank, zero or negative setting means. A
274
+ * value that is not a window at all is kept-forever too, said out loud once:
275
+ * the Deployment page shows an environment value as the one in effect and
276
+ * would otherwise be the only place claiming a window nobody is keeping.
277
+ */
278
+ export function retentionDaysFrom(raw) {
279
+ const window = parseRetentionWindow(raw);
280
+ if (window === 'forever')
281
+ return null;
282
+ if (window !== null)
283
+ return window.days;
284
+ if (raw !== warnedInvalidRetention) {
285
+ warnedInvalidRetention = raw;
286
+ log.warn(`auditRetentionDays is "${raw}", which is not a whole number of days — keeping events forever until it is fixed.`);
287
+ }
288
+ return null;
289
+ }
290
+ //# sourceMappingURL=agent-audit.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-audit.service.js","sourceRoot":"","sources":["../../../src/modules/audit/agent-audit.service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { RequestHandler } from 'express';
2
+ import type { IAgentEventRecorder } from './audit.contract.js';
3
+ import { type SkillFolder } from './event-classifier.js';
4
+ import '../tool-auth/tool-auth.middleware.js';
5
+ export interface AgentRestAuditDeps {
6
+ recorder: IAgentEventRecorder;
7
+ /** The registered name of the platform's own manual (`KNOWLEDGE_BASE`). */
8
+ kbManualName: string;
9
+ /** The caller's skill catalog, for telling a skill read from a file read; fetched only when a read names a path. */
10
+ skillFolders: (userId: string) => Promise<readonly SkillFolder[]>;
11
+ }
12
+ /**
13
+ * Records the REST tool calls the LOCAL server makes directly — the ones that
14
+ * never pass through the hosted MCP endpoint, where every other call an agent
15
+ * makes is recorded.
16
+ *
17
+ * hexis-mcp reaches the deployment two ways. Tool calls go through the
18
+ * hosted `/api/mcp` as its remote manual, and the proxy records them there.
19
+ * But the skill catalog and a skill's body are read straight off the REST
20
+ * surface (`list_skills`, `get_skill`, `list_local_tools`, …) with the
21
+ * internal token the server exchanged its grant for — so a person loading a
22
+ * skill as a slash command in Claude Code through the local server left no
23
+ * `skill` event, while the same read through the hosted endpoint did.
24
+ *
25
+ * WHICH calls are recorded here is decided by the credential, not the route:
26
+ * only an `externalProxy` token that names an agent connection — the
27
+ * exchanged grant — is recorded. The hosted proxy's own loopback tokens name
28
+ * none, so a call the proxy has already recorded is never counted twice;
29
+ * and a connection key reaching REST directly is indistinguishable from the
30
+ * proxy passing that same key through, so keys stay recorded at the proxy
31
+ * alone. The rule is the credential's shape, which no caller chooses.
32
+ *
33
+ * Mounted in front of every `/agent/tools/:name` route and judging on
34
+ * `finish`, when the route's own auth has run and the status is known: a 2xx
35
+ * is `ok`, anything else `error`. Never on the tool's critical path — the
36
+ * classification (and the skill catalog it may need) runs after the answer
37
+ * has gone out.
38
+ */
39
+ export declare function createAgentRestAuditMiddleware(deps: AgentRestAuditDeps): RequestHandler;
40
+ //# sourceMappingURL=agent-rest-audit.middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-rest-audit.middleware.d.ts","sourceRoot":"","sources":["../../../src/modules/audit/agent-rest-audit.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,EAAmC,KAAK,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,sCAAsC,CAAC;AAI9C,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,2EAA2E;IAC3E,YAAY,EAAE,MAAM,CAAC;IACrB,oHAAoH;IACpH,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;CACnE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,kBAAkB,GAAG,cAAc,CAuCvF"}
@@ -0,0 +1,75 @@
1
+ import { logger } from '../../shared/logging.js';
2
+ import { classifyToolCall, skillReadPath } from './event-classifier.js';
3
+ import '../tool-auth/tool-auth.middleware.js'; // Express Request augmentation (req.toolAuth)
4
+ const log = logger('audit');
5
+ /**
6
+ * Records the REST tool calls the LOCAL server makes directly — the ones that
7
+ * never pass through the hosted MCP endpoint, where every other call an agent
8
+ * makes is recorded.
9
+ *
10
+ * hexis-mcp reaches the deployment two ways. Tool calls go through the
11
+ * hosted `/api/mcp` as its remote manual, and the proxy records them there.
12
+ * But the skill catalog and a skill's body are read straight off the REST
13
+ * surface (`list_skills`, `get_skill`, `list_local_tools`, …) with the
14
+ * internal token the server exchanged its grant for — so a person loading a
15
+ * skill as a slash command in Claude Code through the local server left no
16
+ * `skill` event, while the same read through the hosted endpoint did.
17
+ *
18
+ * WHICH calls are recorded here is decided by the credential, not the route:
19
+ * only an `externalProxy` token that names an agent connection — the
20
+ * exchanged grant — is recorded. The hosted proxy's own loopback tokens name
21
+ * none, so a call the proxy has already recorded is never counted twice;
22
+ * and a connection key reaching REST directly is indistinguishable from the
23
+ * proxy passing that same key through, so keys stay recorded at the proxy
24
+ * alone. The rule is the credential's shape, which no caller chooses.
25
+ *
26
+ * Mounted in front of every `/agent/tools/:name` route and judging on
27
+ * `finish`, when the route's own auth has run and the status is known: a 2xx
28
+ * is `ok`, anything else `error`. Never on the tool's critical path — the
29
+ * classification (and the skill catalog it may need) runs after the answer
30
+ * has gone out.
31
+ */
32
+ export function createAgentRestAuditMiddleware(deps) {
33
+ return (req, res, next) => {
34
+ const started = performance.now();
35
+ // Read NOW: `req.params` is rewritten for each layer Express matches, and
36
+ // by `finish` it is the route's own (empty) set, not this mount's.
37
+ const name = String(req.params.name ?? '');
38
+ res.on('finish', () => {
39
+ const auth = req.toolAuth;
40
+ if (!auth || auth.source !== 'external' || !auth.connectionId)
41
+ return;
42
+ if (!name)
43
+ return;
44
+ const durationMs = Math.max(0, Math.round(performance.now() - started));
45
+ const outcome = res.statusCode < 400 ? 'ok' : 'error';
46
+ const args = (req.body && typeof req.body === 'object' ? req.body : {});
47
+ const utcpName = `${deps.kbManualName}.${name}`;
48
+ const ctx = { kbManualName: deps.kbManualName, catalogNames: new Map() };
49
+ const record = (skills) => {
50
+ try {
51
+ deps.recorder.record({
52
+ userId: auth.userId,
53
+ principal: { kind: 'agent', id: auth.connectionId },
54
+ ...classifyToolCall(utcpName, args, ctx, skills),
55
+ outcome,
56
+ durationMs,
57
+ });
58
+ }
59
+ catch (err) {
60
+ log.warn('recording a REST agent event failed:', { err });
61
+ }
62
+ };
63
+ if (skillReadPath(utcpName, args, deps.kbManualName) === null) {
64
+ record(null);
65
+ return;
66
+ }
67
+ deps.skillFolders(auth.userId).then(record, (err) => {
68
+ log.warn('skill catalog unavailable for audit classification:', { err });
69
+ record(null);
70
+ });
71
+ });
72
+ next();
73
+ };
74
+ }
75
+ //# sourceMappingURL=agent-rest-audit.middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-rest-audit.middleware.js","sourceRoot":"","sources":["../../../src/modules/audit/agent-rest-audit.middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AAC1F,OAAO,sCAAsC,CAAC,CAAC,8CAA8C;AAE7F,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;AAU5B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAAwB;IACrE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;QAClC,0EAA0E;QAC1E,mEAAmE;QACnE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAC3C,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;YACpB,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;YAC1B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,IAAI,CAAC,YAAY;gBAAE,OAAO;YACtE,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC;YACxE,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;YACtD,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAA4B,CAAC;YACnG,MAAM,QAAQ,GAAG,GAAG,IAAI,CAAC,YAAY,IAAI,IAAI,EAAE,CAAC;YAChD,MAAM,GAAG,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,GAAG,EAAkB,EAAE,CAAC;YACzF,MAAM,MAAM,GAAG,CAAC,MAAqC,EAAE,EAAE;gBACvD,IAAI,CAAC;oBACH,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;wBACnB,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,SAAS,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,YAAa,EAAE;wBACpD,GAAG,gBAAgB,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC;wBAChD,OAAO;wBACP,UAAU;qBACX,CAAC,CAAC;gBACL,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,GAAG,CAAC,IAAI,CAAC,sCAAsC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;gBAC5D,CAAC;YACH,CAAC,CAAC;YACF,IAAI,aAAa,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC9D,MAAM,CAAC,IAAI,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;gBAClD,GAAG,CAAC,IAAI,CAAC,qDAAqD,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;gBACzE,MAAM,CAAC,IAAI,CAAC,CAAC;YACf,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QACH,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * The Audit log's contracts: what an external agent's activity is recorded
3
+ * as, who a recorded call is attributed to, and the two services around
4
+ * them — the recorder the MCP proxy writes through, and the read/revoke
5
+ * service the Audit log page is built on.
6
+ */
7
+ declare global {
8
+ namespace Express {
9
+ interface Request {
10
+ /**
11
+ * The agent connection (`agent_connections.id`) an OAuth-authenticated
12
+ * MCP request arrived through — bound by the MCP auth middleware from
13
+ * the access token, or from the `connectionId` claim of the internal
14
+ * token the local server exchanged its grant for. Unset for connection
15
+ * keys (those carry `externalApiKeyId`) and for browser sessions.
16
+ */
17
+ agentConnectionId?: string;
18
+ }
19
+ }
20
+ }
21
+ /** What was called: the platform's own tool, a manual's tool, or a skill read. */
22
+ export type AgentEventKind = 'capability' | 'tool' | 'skill';
23
+ /** How the call ended. `denied`: the caller's sign-in for the tool was missing, so nothing ran. */
24
+ export type AgentEventOutcome = 'ok' | 'error' | 'denied';
25
+ /**
26
+ * Who a call is attributed to: the connection key it was authenticated with,
27
+ * or the agent connection behind its OAuth grant. Exactly one.
28
+ */
29
+ export type AuditPrincipalRef = {
30
+ kind: 'key';
31
+ id: string;
32
+ } | {
33
+ kind: 'agent';
34
+ id: string;
35
+ };
36
+ /** One recorded call, as the recorder takes it. */
37
+ export interface AgentEventInput {
38
+ userId: string;
39
+ principal: AuditPrincipalRef;
40
+ kind: AgentEventKind;
41
+ /** The MCP server / `.tool` manual for a tool, the skill folder for a skill; null for a capability. */
42
+ manual: string | null;
43
+ name: string;
44
+ outcome: AgentEventOutcome;
45
+ durationMs: number | null;
46
+ }
47
+ /**
48
+ * The write half. `record` never throws and never blocks the call it
49
+ * describes: a recording failure is logged and the tool result still goes
50
+ * back to the agent.
51
+ */
52
+ export interface IAgentEventRecorder {
53
+ record(event: AgentEventInput): void;
54
+ }
55
+ /** One row of the Audit log: an agent connection or a connection key, with its owner. */
56
+ export interface AuditPrincipal {
57
+ kind: 'key' | 'agent';
58
+ id: string;
59
+ /** The key's label, or the agent's registered client name. */
60
+ label: string;
61
+ /**
62
+ * For a key: what it was minted as (`key` by hand, `github-link` for a
63
+ * Claude link). Absent for an agent.
64
+ */
65
+ keyKind?: string;
66
+ /** When the key was created / the agent connected. Epoch ms. */
67
+ createdAt: number;
68
+ lastUsedAt: number | null;
69
+ revokedAt: number | null;
70
+ /** Who ended it, once revoked: the owner, or an admin. */
71
+ revokedBy: 'owner' | 'admin' | null;
72
+ /** For a key: when its owner deleted it for good. Such rows reach admins only. */
73
+ deletedAt?: number | null;
74
+ /** Events currently on record for it (within the retention window). */
75
+ eventCount: number;
76
+ user: {
77
+ id: string;
78
+ email: string;
79
+ name: string;
80
+ };
81
+ }
82
+ /** One event as the page reads it. */
83
+ export interface AgentEventView {
84
+ id: string;
85
+ kind: AgentEventKind;
86
+ manual: string | null;
87
+ name: string;
88
+ outcome: AgentEventOutcome;
89
+ at: number;
90
+ }
91
+ export interface AgentEventPage {
92
+ events: AgentEventView[];
93
+ /**
94
+ * Every event on record for the principal, not just this page — counted
95
+ * on the first page only; null on a page reached through a cursor, since
96
+ * the reader already holds the figure.
97
+ */
98
+ total: number | null;
99
+ /** Pass back as `before` to load the next (older) page; null when this was the last. */
100
+ nextCursor: string | null;
101
+ }
102
+ /** Whose rows: one person's, or the whole deployment's (admins). */
103
+ export type AuditScope = {
104
+ userId: string;
105
+ } | 'all';
106
+ export interface IAgentAuditService {
107
+ listPrincipals(scope: AuditScope): Promise<AuditPrincipal[]>;
108
+ /** The account a principal belongs to, or null when there is no such principal. The routes' ownership check. */
109
+ ownerOf(principal: AuditPrincipalRef): Promise<string | null>;
110
+ listEvents(principal: AuditPrincipalRef, page: {
111
+ before?: string;
112
+ limit: number;
113
+ }): Promise<AgentEventPage>;
114
+ /**
115
+ * Cut an agent off: mark its connection revoked and revoke every live
116
+ * token it holds, so its next request fails and its refresh fails with it.
117
+ * `ownerUserId` scopes the revoke to the person's own connections; an admin
118
+ * passes none. Idempotent on an already-revoked connection; throws
119
+ * {@link AuditPrincipalNotFoundError} when nothing matches.
120
+ */
121
+ revokeConnection(id: string, by: 'owner' | 'admin', ownerUserId?: string): Promise<void>;
122
+ }
123
+ export declare class AuditPrincipalNotFoundError extends Error {
124
+ constructor();
125
+ }
126
+ /** A `before` cursor that is not one this service issued — the caller's mistake, answered as such. */
127
+ export declare class InvalidCursorError extends Error {
128
+ constructor();
129
+ }
130
+ export declare function isUuid(value: string): boolean;
131
+ /**
132
+ * The retention window's one rule, shared by the setting's validator and the
133
+ * runtime reader. A positive whole number of days is the window; blank,
134
+ * zero or a negative number is "keep forever" — the deployment's choice,
135
+ * made the same way whether typed on the Deployment page or set through
136
+ * `AUDIT_RETENTION_DAYS`. Anything else is not a window at all: the
137
+ * validator refuses the save, and the reader, which cannot refuse, keeps
138
+ * everything and says so.
139
+ */
140
+ export type RetentionWindow = {
141
+ days: number;
142
+ } | 'forever';
143
+ /** The window a retention setting names, or null when it names none this rule accepts. */
144
+ export declare function parseRetentionWindow(raw: string): RetentionWindow | null;
145
+ //# sourceMappingURL=audit.contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.contract.d.ts","sourceRoot":"","sources":["../../../src/modules/audit/audit.contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The Audit log's contracts: what an external agent's activity is recorded
3
+ * as, who a recorded call is attributed to, and the two services around
4
+ * them — the recorder the MCP proxy writes through, and the read/revoke
5
+ * service the Audit log page is built on.
6
+ */
7
+ export class AuditPrincipalNotFoundError extends Error {
8
+ constructor() {
9
+ super('No such agent or key');
10
+ this.name = 'AuditPrincipalNotFoundError';
11
+ }
12
+ }
13
+ /** A `before` cursor that is not one this service issued — the caller's mistake, answered as such. */
14
+ export class InvalidCursorError extends Error {
15
+ constructor() {
16
+ super('Invalid cursor');
17
+ this.name = 'InvalidCursorError';
18
+ }
19
+ }
20
+ /** Shape of the id columns (any uuid version; case-insensitive). */
21
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
+ export function isUuid(value) {
23
+ return UUID_RE.test(value);
24
+ }
25
+ /** The window a retention setting names, or null when it names none this rule accepts. */
26
+ export function parseRetentionWindow(raw) {
27
+ const trimmed = raw.trim();
28
+ if (trimmed === '')
29
+ return 'forever';
30
+ if (!/^-?\d+$/.test(trimmed))
31
+ return null;
32
+ const n = Number(trimmed);
33
+ return n > 0 ? { days: n } : 'forever';
34
+ }
35
+ //# sourceMappingURL=audit.contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.contract.js","sourceRoot":"","sources":["../../../src/modules/audit/audit.contract.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsHH,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IACpD;QACE,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;IAC5C,CAAC;CACF;AAED,sGAAsG;AACtG,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C;QACE,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,oEAAoE;AACpE,MAAM,OAAO,GAAG,iEAAiE,CAAC;AAElF,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC;AAaD,0FAA0F;AAC1F,MAAM,UAAU,oBAAoB,CAAC,GAAW;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACrC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACzC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import express from 'express';
2
+ import type { IAdminAccessService } from '../admin/admin.interface.js';
3
+ import type { IExternalApiKeyService } from '../tool-auth/external-api-key.interface.js';
4
+ import { type IAgentAuditService } from './audit.contract.js';
5
+ import '../auth/auth.middleware.js';
6
+ /**
7
+ * The Audit log's routes, JWT-only (mounted behind the session middleware):
8
+ *
9
+ * GET /api/audit/principals?scope=me|all — agents + keys, with owners and event counts
10
+ * GET /api/audit/principals/:kind/:id/events — one principal's events, newest first, cursor-paged
11
+ * DELETE /api/audit/agents/:id — revoke an agent (owner or admin)
12
+ * DELETE /api/audit/keys/:id — revoke a key (owner or admin)
13
+ *
14
+ * Everyone may read and revoke their OWN rows; `scope=all` and anyone else's
15
+ * rows are admins' (per request, through {@link IAdminAccessService}). Both
16
+ * revokes are recorded as the owner's or an admin's doing, so the owner's
17
+ * page can say which. Keys keep their existing per-user and admin routes;
18
+ * the one here exists so the page has a single client for both kinds.
19
+ */
20
+ export declare function createAuditRoutes(audit: IAgentAuditService, keys: Pick<IExternalApiKeyService, 'revoke' | 'revokeAny'>, adminAccess: IAdminAccessService): express.Router;
21
+ //# sourceMappingURL=audit.routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.routes.d.ts","sourceRoot":"","sources":["../../../src/modules/audit/audit.routes.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAEvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAC;AACzF,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,4BAA4B,CAAC;AAMpC;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,WAAW,CAAC,EAC1D,WAAW,EAAE,mBAAmB,GAC/B,OAAO,CAAC,MAAM,CAkIhB"}