@anchrd/intel-api 0.2.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 (114) hide show
  1. package/README.md +96 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +21 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +39 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +8 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +200 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +29 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +22 -0
  11. package/dist/adapters/content/content.types.d.ts +14 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-flows.d.ts +6 -0
  14. package/dist/adapters/db/db-flows.js +466 -0
  15. package/dist/adapters/db/db-indexing.d.ts +3 -0
  16. package/dist/adapters/db/db-indexing.js +70 -0
  17. package/dist/adapters/db/db-oauth.d.ts +3 -0
  18. package/dist/adapters/db/db-oauth.js +26 -0
  19. package/dist/adapters/db/db-tools.d.ts +6 -0
  20. package/dist/adapters/db/db-tools.js +200 -0
  21. package/dist/adapters/db/db.d.ts +6 -0
  22. package/dist/adapters/db/db.js +755 -0
  23. package/dist/adapters/db/db.types.d.ts +16 -0
  24. package/dist/adapters/db/db.types.js +1 -0
  25. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  26. package/dist/adapters/document-converter/document-converter.js +25 -0
  27. package/dist/adapters/document-converter/document-converter.types.d.ts +9 -0
  28. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  29. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  30. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  31. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  32. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  33. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  34. package/dist/adapters/index-queue/index-queue.js +12 -0
  35. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  36. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  37. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  38. package/dist/adapters/json-schema/json-schema.js +31 -0
  39. package/dist/adapters/openid/openid.d.ts +5 -0
  40. package/dist/adapters/openid/openid.js +159 -0
  41. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  42. package/dist/adapters/portal-tokens/portal-tokens.js +71 -0
  43. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  44. package/dist/adapters/remote-tools/remote-tools.js +109 -0
  45. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  46. package/dist/adapters/semantic-index/semantic-index.js +57 -0
  47. package/dist/adapters/semantic-index/semantic-index.types.d.ts +32 -0
  48. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  49. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  50. package/dist/adapters/session-cookie/session-cookie.js +81 -0
  51. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  52. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  53. package/dist/auth/auth.d.ts +2 -0
  54. package/dist/auth/auth.js +176 -0
  55. package/dist/auth/auth.types.d.ts +104 -0
  56. package/dist/auth/auth.types.js +1 -0
  57. package/dist/build/build.d.ts +2 -0
  58. package/dist/build/build.js +125 -0
  59. package/dist/build/build.types.d.ts +17 -0
  60. package/dist/build/build.types.js +1 -0
  61. package/dist/cli/cli.d.ts +4 -0
  62. package/dist/cli/cli.js +141 -0
  63. package/dist/cli/cli.types.d.ts +14 -0
  64. package/dist/cli/cli.types.js +1 -0
  65. package/dist/flows/flows.d.ts +4 -0
  66. package/dist/flows/flows.js +409 -0
  67. package/dist/flows/flows.types.d.ts +108 -0
  68. package/dist/flows/flows.types.js +1 -0
  69. package/dist/http/http.d.ts +8 -0
  70. package/dist/http/http.js +286 -0
  71. package/dist/http/http.types.d.ts +14 -0
  72. package/dist/http/http.types.js +1 -0
  73. package/dist/indexing/indexing.d.ts +6 -0
  74. package/dist/indexing/indexing.js +48 -0
  75. package/dist/indexing/indexing.types.d.ts +10 -0
  76. package/dist/indexing/indexing.types.js +1 -0
  77. package/dist/intel/intel.d.ts +3 -0
  78. package/dist/intel/intel.js +77 -0
  79. package/dist/intel/intel.types.d.ts +14 -0
  80. package/dist/intel/intel.types.js +1 -0
  81. package/dist/knowledge/knowledge.d.ts +2 -0
  82. package/dist/knowledge/knowledge.js +479 -0
  83. package/dist/knowledge/knowledge.types.d.ts +162 -0
  84. package/dist/knowledge/knowledge.types.js +1 -0
  85. package/dist/mcp/mcp.d.ts +2 -0
  86. package/dist/mcp/mcp.js +484 -0
  87. package/dist/mcp/mcp.types.d.ts +10 -0
  88. package/dist/mcp/mcp.types.js +1 -0
  89. package/dist/prepare/prepare.d.ts +4 -0
  90. package/dist/prepare/prepare.js +16 -0
  91. package/dist/prepare/prepare.types.d.ts +9 -0
  92. package/dist/prepare/prepare.types.js +1 -0
  93. package/dist/shared/gate-authorization/gate-authorization.d.ts +4 -0
  94. package/dist/shared/gate-authorization/gate-authorization.js +19 -0
  95. package/dist/shared/intel-error/intel-error.d.ts +5 -0
  96. package/dist/shared/intel-error/intel-error.js +10 -0
  97. package/dist/shared/problem-details/problem-details.d.ts +8 -0
  98. package/dist/shared/problem-details/problem-details.js +4 -0
  99. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  100. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  101. package/dist/shared/sha256/sha256.d.ts +1 -0
  102. package/dist/shared/sha256/sha256.js +9 -0
  103. package/dist/tools/tools.d.ts +2 -0
  104. package/dist/tools/tools.js +107 -0
  105. package/dist/tools/tools.types.d.ts +56 -0
  106. package/dist/tools/tools.types.js +1 -0
  107. package/examples/branding/favicon.svg +5 -0
  108. package/examples/branding/logo.svg +5 -0
  109. package/examples/branding/theme.css +16 -0
  110. package/examples/dev.vars.example +7 -0
  111. package/examples/intel.json +9 -0
  112. package/migrations/0000_intel_foundation.sql +203 -0
  113. package/migrations/0001_portal_native_tools.sql +21 -0
  114. package/package.json +62 -0
@@ -0,0 +1,755 @@
1
+ const nodeColumns = `n.id, n.parent_id, n.kind, n.title, n.description, n.context_policy,
2
+ n.owner_id, n.current_version_id, n.created_at, n.updated_at, n.archived_at`;
3
+ function mapNode(row) {
4
+ return {
5
+ id: row.id,
6
+ parentId: row.parent_id,
7
+ kind: row.kind,
8
+ title: row.title,
9
+ description: row.description,
10
+ contextPolicy: row.context_policy,
11
+ ownerId: row.owner_id,
12
+ currentVersionId: row.current_version_id,
13
+ createdAt: row.created_at,
14
+ updatedAt: row.updated_at,
15
+ archivedAt: row.archived_at,
16
+ };
17
+ }
18
+ function mapVersion(row) {
19
+ return {
20
+ id: row.id,
21
+ nodeId: row.node_id,
22
+ sequence: row.sequence,
23
+ contentKey: row.content_key,
24
+ mediaType: row.media_type,
25
+ contentHash: row.content_hash,
26
+ size: row.size,
27
+ createdBy: row.created_by,
28
+ createdAt: row.created_at,
29
+ };
30
+ }
31
+ function mapGrant(row) {
32
+ let principal;
33
+ switch (row.principal_type) {
34
+ case "user":
35
+ principal = { type: "user", id: row.principal_id };
36
+ break;
37
+ case "email":
38
+ principal = { type: "email", email: row.principal_id };
39
+ break;
40
+ case "organization":
41
+ principal = { type: "organization" };
42
+ break;
43
+ default:
44
+ throw new Error("Unsupported knowledge grant principal type");
45
+ }
46
+ return {
47
+ id: row.id,
48
+ resourceId: row.resource_id,
49
+ principal,
50
+ role: row.role,
51
+ expiresAt: row.expires_at,
52
+ createdBy: row.created_by,
53
+ createdAt: row.created_at,
54
+ };
55
+ }
56
+ function mapLink(row) {
57
+ return {
58
+ id: row.id,
59
+ sourceNodeId: row.source_node_id,
60
+ targetNodeId: row.target_node_id,
61
+ relation: row.relation,
62
+ label: row.label,
63
+ createdBy: row.created_by,
64
+ createdAt: row.created_at,
65
+ };
66
+ }
67
+ function toFtsQuery(query) {
68
+ return query
69
+ .split(/\s+/)
70
+ .filter(Boolean)
71
+ .map((token) => `"${token.replaceAll('"', '""')}"`)
72
+ .join(" AND ");
73
+ }
74
+ export function createKnowledgeRepository(deps) {
75
+ const visibleCte = `WITH RECURSIVE allowed(id, parent_id) AS (
76
+ SELECT seed.id, seed.parent_id
77
+ FROM knowledge_nodes seed
78
+ WHERE seed.owner_id = ?
79
+ OR EXISTS (
80
+ SELECT 1 FROM resource_grants grant_row
81
+ WHERE grant_row.resource_type = 'knowledge'
82
+ AND grant_row.resource_id = seed.id
83
+ AND (
84
+ (grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
85
+ OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
86
+ OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
87
+ )
88
+ AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
89
+ )
90
+ UNION
91
+ SELECT child.id, child.parent_id
92
+ FROM knowledge_nodes child
93
+ JOIN allowed parent ON child.parent_id = parent.id
94
+ )`;
95
+ return {
96
+ async listVisible(actor, input) {
97
+ const now = deps.now().toISOString();
98
+ const result = await deps.db
99
+ .prepare(`${visibleCte}
100
+ SELECT ${nodeColumns}
101
+ FROM knowledge_nodes n
102
+ JOIN allowed ON allowed.id = n.id
103
+ WHERE n.parent_id IS ? AND (? = 1 OR n.archived_at IS NULL)
104
+ ORDER BY CASE n.kind WHEN 'folder' THEN 0 ELSE 1 END, lower(n.title), n.id`)
105
+ .bind(actor.id, actor.id, actor.email, now, input.parentId, input.includeArchived ? 1 : 0)
106
+ .all();
107
+ return (result.results ?? []).map(mapNode);
108
+ },
109
+ async getVisible(actor, nodeId) {
110
+ const row = await deps.db
111
+ .prepare(`${visibleCte}
112
+ SELECT ${nodeColumns}
113
+ FROM knowledge_nodes n
114
+ JOIN allowed ON allowed.id = n.id
115
+ WHERE n.id = ?`)
116
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), nodeId)
117
+ .first();
118
+ return row ? mapNode(row) : null;
119
+ },
120
+ async canEdit(actor, nodeId) {
121
+ const row = await deps.db
122
+ .prepare(`WITH RECURSIVE ancestors(id, parent_id, owner_id) AS (
123
+ SELECT id, parent_id, owner_id FROM knowledge_nodes WHERE id = ?
124
+ UNION ALL
125
+ SELECT parent.id, parent.parent_id, parent.owner_id
126
+ FROM knowledge_nodes parent
127
+ JOIN ancestors child ON child.parent_id = parent.id
128
+ )
129
+ SELECT 1 AS allowed
130
+ FROM ancestors
131
+ WHERE owner_id = ?
132
+ OR EXISTS (
133
+ SELECT 1 FROM resource_grants grant_row
134
+ WHERE grant_row.resource_type = 'knowledge'
135
+ AND grant_row.resource_id = ancestors.id
136
+ AND (
137
+ (grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
138
+ OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
139
+ OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
140
+ )
141
+ AND grant_row.role IN ('editor', 'manager')
142
+ AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
143
+ )
144
+ LIMIT 1`)
145
+ .bind(nodeId, actor.id, actor.id, actor.email, deps.now().toISOString())
146
+ .first();
147
+ return row?.allowed === 1;
148
+ },
149
+ async canManage(actor, nodeId) {
150
+ const row = await deps.db
151
+ .prepare(`WITH RECURSIVE ancestors(id, parent_id, owner_id) AS (
152
+ SELECT id, parent_id, owner_id FROM knowledge_nodes WHERE id = ?
153
+ UNION ALL
154
+ SELECT parent.id, parent.parent_id, parent.owner_id
155
+ FROM knowledge_nodes parent
156
+ JOIN ancestors child ON child.parent_id = parent.id
157
+ )
158
+ SELECT 1 AS allowed
159
+ FROM ancestors
160
+ WHERE owner_id = ?
161
+ OR EXISTS (
162
+ SELECT 1 FROM resource_grants grant_row
163
+ WHERE grant_row.resource_type = 'knowledge'
164
+ AND grant_row.resource_id = ancestors.id
165
+ AND (
166
+ (grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
167
+ OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
168
+ OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
169
+ )
170
+ AND grant_row.role = 'manager'
171
+ AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
172
+ )
173
+ LIMIT 1`)
174
+ .bind(nodeId, actor.id, actor.id, actor.email, deps.now().toISOString())
175
+ .first();
176
+ return row?.allowed === 1;
177
+ },
178
+ async findIdempotentNode(actorId, operation, idempotencyKey) {
179
+ const row = await deps.db
180
+ .prepare(`SELECT resource_id FROM idempotency_keys
181
+ WHERE actor_id = ? AND operation = ? AND idempotency_key = ?`)
182
+ .bind(actorId, operation, idempotencyKey)
183
+ .first();
184
+ return row?.resource_id ?? null;
185
+ },
186
+ async findIdempotentRevocation(actorId, idempotencyKey) {
187
+ const row = await deps.db
188
+ .prepare(`SELECT resource_id FROM idempotency_keys
189
+ WHERE actor_id = ? AND operation = 'knowledge.revoke' AND idempotency_key = ?`)
190
+ .bind(actorId, idempotencyKey)
191
+ .first();
192
+ if (!row)
193
+ return null;
194
+ return row.resource_id.startsWith("1:");
195
+ },
196
+ async insertNode(input) {
197
+ const node = input.node;
198
+ try {
199
+ await deps.db.batch([
200
+ deps.db
201
+ .prepare(`INSERT INTO knowledge_nodes (
202
+ id, parent_id, kind, title, description, context_policy, owner_id,
203
+ current_version_id, created_at, updated_at, archived_at
204
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
205
+ .bind(node.id, node.parentId, node.kind, node.title, node.description, node.contextPolicy, node.ownerId, node.currentVersionId, node.createdAt, node.updatedAt, node.archivedAt),
206
+ deps.db
207
+ .prepare(`INSERT INTO idempotency_keys (
208
+ actor_id, operation, idempotency_key, resource_id, created_at
209
+ ) VALUES (?, 'knowledge.create', ?, ?, ?)`)
210
+ .bind(input.actorId, input.idempotencyKey, node.id, node.createdAt),
211
+ deps.db
212
+ .prepare(`INSERT INTO audit_events (
213
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
214
+ ) VALUES (?, ?, 'knowledge.create', 'knowledge', ?, ?, ?)`)
215
+ .bind(input.auditId, input.actorId, node.id, JSON.stringify({ kind: node.kind, parentId: node.parentId }), node.createdAt),
216
+ ]);
217
+ }
218
+ catch (error) {
219
+ const replayedKey = await deps.db
220
+ .prepare(`SELECT resource_id FROM idempotency_keys
221
+ WHERE actor_id = ? AND operation = 'knowledge.create' AND idempotency_key = ?`)
222
+ .bind(input.actorId, input.idempotencyKey)
223
+ .first();
224
+ if (replayedKey) {
225
+ const replayed = await deps.db
226
+ .prepare(`${visibleCte}
227
+ SELECT ${nodeColumns}
228
+ FROM knowledge_nodes n
229
+ JOIN allowed ON allowed.id = n.id
230
+ WHERE n.id = ?`)
231
+ .bind(input.actorId, input.actorId, "", deps.now().toISOString(), replayedKey.resource_id)
232
+ .first();
233
+ if (replayed)
234
+ return mapNode(replayed);
235
+ }
236
+ throw error;
237
+ }
238
+ return node;
239
+ },
240
+ async getVersion(versionId) {
241
+ const row = await deps.db
242
+ .prepare(`SELECT id, node_id, sequence, content_key, media_type, content_hash,
243
+ size, created_by, created_at FROM knowledge_versions WHERE id = ?`)
244
+ .bind(versionId)
245
+ .first();
246
+ return row ? mapVersion(row) : null;
247
+ },
248
+ async listVersions(nodeId) {
249
+ const result = await deps.db
250
+ .prepare(`SELECT id, node_id, sequence, content_key, media_type, content_hash,
251
+ size, created_by, created_at
252
+ FROM knowledge_versions
253
+ WHERE node_id = ?
254
+ ORDER BY sequence DESC`)
255
+ .bind(nodeId)
256
+ .all();
257
+ return (result.results ?? []).map(mapVersion);
258
+ },
259
+ async updateNode(input) {
260
+ const node = input.node;
261
+ try {
262
+ await deps.db.batch([
263
+ deps.db
264
+ .prepare(`WITH RECURSIVE descendants(id) AS (
265
+ SELECT id FROM knowledge_nodes WHERE id = ?
266
+ UNION ALL
267
+ SELECT child.id
268
+ FROM knowledge_nodes child
269
+ JOIN descendants parent ON child.parent_id = parent.id
270
+ )
271
+ UPDATE knowledge_nodes
272
+ SET parent_id = ?, title = ?, description = ?, context_policy = ?, updated_at = ?
273
+ WHERE id = ? AND updated_at = ?
274
+ AND (? IS NULL OR ? NOT IN (SELECT id FROM descendants))`)
275
+ .bind(node.id, node.parentId, node.title, node.description, node.contextPolicy, node.updatedAt, node.id, input.baseUpdatedAt, node.parentId, node.parentId),
276
+ deps.db
277
+ .prepare(`INSERT INTO idempotency_keys (
278
+ actor_id, operation, idempotency_key, resource_id, created_at
279
+ ) SELECT ?, 'knowledge.update', ?, ?, ?
280
+ WHERE EXISTS (
281
+ SELECT 1 FROM knowledge_nodes
282
+ WHERE id = ? AND parent_id IS ? AND title = ? AND description IS ?
283
+ AND context_policy = ? AND updated_at = ?
284
+ )`)
285
+ .bind(input.actorId, input.idempotencyKey, node.id, node.updatedAt, node.id, node.parentId, node.title, node.description, node.contextPolicy, node.updatedAt),
286
+ deps.db
287
+ .prepare(`INSERT INTO audit_events (
288
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
289
+ ) SELECT ?, ?, 'knowledge.update', 'knowledge', ?, ?, ?
290
+ WHERE EXISTS (
291
+ SELECT 1 FROM idempotency_keys
292
+ WHERE actor_id = ? AND operation = 'knowledge.update'
293
+ AND idempotency_key = ? AND resource_id = ?
294
+ )`)
295
+ .bind(input.auditId, input.actorId, node.id, JSON.stringify({
296
+ parentId: node.parentId,
297
+ title: node.title,
298
+ contextPolicy: node.contextPolicy,
299
+ }), node.updatedAt, input.actorId, input.idempotencyKey, node.id),
300
+ ]);
301
+ }
302
+ catch (error) {
303
+ const replayed = await deps.db
304
+ .prepare(`SELECT resource_id FROM idempotency_keys
305
+ WHERE actor_id = ? AND operation = 'knowledge.update' AND idempotency_key = ?`)
306
+ .bind(input.actorId, input.idempotencyKey)
307
+ .first();
308
+ if (!replayed)
309
+ throw error;
310
+ }
311
+ const replayed = await deps.db
312
+ .prepare(`SELECT resource_id FROM idempotency_keys
313
+ WHERE actor_id = ? AND operation = 'knowledge.update' AND idempotency_key = ?`)
314
+ .bind(input.actorId, input.idempotencyKey)
315
+ .first();
316
+ if (replayed) {
317
+ const row = await deps.db
318
+ .prepare(`SELECT ${nodeColumns} FROM knowledge_nodes n WHERE n.id = ?`)
319
+ .bind(replayed.resource_id)
320
+ .first();
321
+ if (row)
322
+ return mapNode(row);
323
+ }
324
+ if (node.parentId !== null) {
325
+ const cycle = await deps.db
326
+ .prepare(`WITH RECURSIVE descendants(id) AS (
327
+ SELECT id FROM knowledge_nodes WHERE id = ?
328
+ UNION ALL
329
+ SELECT child.id
330
+ FROM knowledge_nodes child
331
+ JOIN descendants parent ON child.parent_id = parent.id
332
+ )
333
+ SELECT 1 AS found FROM descendants WHERE id = ? LIMIT 1`)
334
+ .bind(node.id, node.parentId)
335
+ .first();
336
+ if (cycle?.found === 1)
337
+ return "cycle";
338
+ }
339
+ return "conflict";
340
+ },
341
+ async archiveNode(input) {
342
+ try {
343
+ await deps.db.batch([
344
+ deps.db
345
+ .prepare(`UPDATE knowledge_nodes
346
+ SET archived_at = ?, updated_at = ?
347
+ WHERE id = ? AND updated_at = ?`)
348
+ .bind(input.archivedAt, input.updatedAt, input.nodeId, input.baseUpdatedAt),
349
+ deps.db
350
+ .prepare(`INSERT INTO idempotency_keys (
351
+ actor_id, operation, idempotency_key, resource_id, created_at
352
+ ) SELECT ?, 'knowledge.archive', ?, ?, ?
353
+ WHERE EXISTS (
354
+ SELECT 1 FROM knowledge_nodes
355
+ WHERE id = ? AND archived_at IS ? AND updated_at = ?
356
+ )`)
357
+ .bind(input.actorId, input.idempotencyKey, input.nodeId, input.updatedAt, input.nodeId, input.archivedAt, input.updatedAt),
358
+ deps.db
359
+ .prepare(`INSERT INTO audit_events (
360
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
361
+ ) SELECT ?, ?, 'knowledge.archive', 'knowledge', ?, ?, ?
362
+ WHERE EXISTS (
363
+ SELECT 1 FROM idempotency_keys
364
+ WHERE actor_id = ? AND operation = 'knowledge.archive'
365
+ AND idempotency_key = ? AND resource_id = ?
366
+ )`)
367
+ .bind(input.auditId, input.actorId, input.nodeId, JSON.stringify({ archived: input.archivedAt !== null }), input.updatedAt, input.actorId, input.idempotencyKey, input.nodeId),
368
+ ]);
369
+ }
370
+ catch (error) {
371
+ const replayed = await deps.db
372
+ .prepare(`SELECT resource_id FROM idempotency_keys
373
+ WHERE actor_id = ? AND operation = 'knowledge.archive' AND idempotency_key = ?`)
374
+ .bind(input.actorId, input.idempotencyKey)
375
+ .first();
376
+ if (!replayed)
377
+ throw error;
378
+ }
379
+ const replayed = await deps.db
380
+ .prepare(`SELECT resource_id FROM idempotency_keys
381
+ WHERE actor_id = ? AND operation = 'knowledge.archive' AND idempotency_key = ?`)
382
+ .bind(input.actorId, input.idempotencyKey)
383
+ .first();
384
+ if (!replayed)
385
+ return "conflict";
386
+ const row = await deps.db
387
+ .prepare(`SELECT ${nodeColumns} FROM knowledge_nodes n WHERE n.id = ?`)
388
+ .bind(replayed.resource_id)
389
+ .first();
390
+ return row ? mapNode(row) : "conflict";
391
+ },
392
+ async appendVersion(input) {
393
+ const version = input.version;
394
+ await deps.db.batch([
395
+ deps.db
396
+ .prepare(`INSERT INTO knowledge_versions (
397
+ id, node_id, sequence, content_key, media_type, content_hash, size, created_by, created_at
398
+ ) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?
399
+ FROM knowledge_nodes
400
+ WHERE id = ? AND current_version_id IS ?`)
401
+ .bind(version.id, version.nodeId, version.sequence, version.contentKey, version.mediaType, version.contentHash, version.size, version.createdBy, version.createdAt, version.nodeId, input.baseVersionId),
402
+ deps.db
403
+ .prepare(`UPDATE knowledge_nodes
404
+ SET current_version_id = ?, updated_at = ?
405
+ WHERE id = ? AND EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
406
+ .bind(version.id, version.createdAt, version.nodeId, version.id),
407
+ deps.db
408
+ .prepare(`INSERT INTO idempotency_keys (
409
+ actor_id, operation, idempotency_key, resource_id, created_at
410
+ ) SELECT ?, 'knowledge.save', ?, ?, ?
411
+ WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
412
+ .bind(input.actorId, input.idempotencyKey, version.id, version.createdAt, version.id),
413
+ deps.db
414
+ .prepare(`INSERT INTO knowledge_index_state (
415
+ version_id, status, attempt_count, last_error, updated_at
416
+ ) SELECT ?, 'pending', 0, NULL, ?
417
+ WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
418
+ .bind(version.id, version.createdAt, version.id),
419
+ deps.db
420
+ .prepare(`INSERT INTO audit_events (
421
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
422
+ ) SELECT ?, ?, 'knowledge.save', 'knowledge', ?, ?, ?
423
+ WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
424
+ .bind(input.auditId, input.actorId, version.nodeId, JSON.stringify({ versionId: version.id, sequence: version.sequence }), version.createdAt, version.id),
425
+ ]);
426
+ const row = await deps.db
427
+ .prepare("SELECT current_version_id FROM knowledge_nodes WHERE id = ?")
428
+ .bind(version.nodeId)
429
+ .first();
430
+ return row?.current_version_id === version.id ? "saved" : "conflict";
431
+ },
432
+ async listGrants(resourceId) {
433
+ const result = await deps.db
434
+ .prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
435
+ created_by, created_at
436
+ FROM resource_grants
437
+ WHERE resource_type = 'knowledge' AND resource_id = ?
438
+ ORDER BY created_at, id`)
439
+ .bind(resourceId)
440
+ .all();
441
+ return (result.results ?? []).map(mapGrant);
442
+ },
443
+ async listLinksVisible(actor, nodeId) {
444
+ const result = await deps.db
445
+ .prepare(`${visibleCte}
446
+ SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
447
+ link.created_by, link.created_at
448
+ FROM knowledge_links link
449
+ JOIN allowed source_access ON source_access.id = link.source_node_id
450
+ JOIN allowed target_access ON target_access.id = link.target_node_id
451
+ WHERE link.source_node_id = ? OR link.target_node_id = ?
452
+ ORDER BY link.created_at, link.id`)
453
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), nodeId, nodeId)
454
+ .all();
455
+ return (result.results ?? []).map(mapLink);
456
+ },
457
+ async getLinkVisible(actor, linkId) {
458
+ const row = await deps.db
459
+ .prepare(`${visibleCte}
460
+ SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
461
+ link.created_by, link.created_at
462
+ FROM knowledge_links link
463
+ JOIN allowed source_access ON source_access.id = link.source_node_id
464
+ JOIN allowed target_access ON target_access.id = link.target_node_id
465
+ WHERE link.id = ?`)
466
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), linkId)
467
+ .first();
468
+ return row ? mapLink(row) : null;
469
+ },
470
+ async findLinkVisible(actor, input) {
471
+ const row = await deps.db
472
+ .prepare(`${visibleCte}
473
+ SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
474
+ link.created_by, link.created_at
475
+ FROM knowledge_links link
476
+ JOIN allowed source_access ON source_access.id = link.source_node_id
477
+ JOIN allowed target_access ON target_access.id = link.target_node_id
478
+ WHERE link.source_node_id = ? AND link.target_node_id = ? AND link.relation = ?`)
479
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), input.sourceNodeId, input.targetNodeId, input.relation)
480
+ .first();
481
+ return row ? mapLink(row) : null;
482
+ },
483
+ async insertLink(input) {
484
+ const link = input.link;
485
+ try {
486
+ await deps.db.batch([
487
+ deps.db
488
+ .prepare(`INSERT INTO knowledge_links (
489
+ id, source_node_id, target_node_id, relation, label, created_by, created_at
490
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`)
491
+ .bind(link.id, link.sourceNodeId, link.targetNodeId, link.relation, link.label, link.createdBy, link.createdAt),
492
+ deps.db
493
+ .prepare(`INSERT INTO idempotency_keys (
494
+ actor_id, operation, idempotency_key, resource_id, created_at
495
+ ) VALUES (?, 'knowledge.link', ?, ?, ?)`)
496
+ .bind(input.actorId, input.idempotencyKey, link.id, link.createdAt),
497
+ deps.db
498
+ .prepare(`INSERT INTO audit_events (
499
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
500
+ ) VALUES (?, ?, 'knowledge.link', 'knowledge', ?, ?, ?)`)
501
+ .bind(input.auditId, input.actorId, link.sourceNodeId, JSON.stringify({
502
+ linkId: link.id,
503
+ targetNodeId: link.targetNodeId,
504
+ relation: link.relation,
505
+ }), link.createdAt),
506
+ ]);
507
+ return link;
508
+ }
509
+ catch (error) {
510
+ const replayed = await deps.db
511
+ .prepare(`SELECT resource_id FROM idempotency_keys
512
+ WHERE actor_id = ? AND operation = 'knowledge.link' AND idempotency_key = ?`)
513
+ .bind(input.actorId, input.idempotencyKey)
514
+ .first();
515
+ const row = await deps.db
516
+ .prepare(`SELECT id, source_node_id, target_node_id, relation, label,
517
+ created_by, created_at FROM knowledge_links
518
+ WHERE id = ? OR (source_node_id = ? AND target_node_id = ? AND relation = ?)`)
519
+ .bind(replayed?.resource_id ?? link.id, link.sourceNodeId, link.targetNodeId, link.relation)
520
+ .first();
521
+ if (row)
522
+ return mapLink(row);
523
+ throw error;
524
+ }
525
+ },
526
+ async deleteLink(input) {
527
+ const existing = await deps.db
528
+ .prepare("SELECT id FROM knowledge_links WHERE id = ? AND source_node_id = ?")
529
+ .bind(input.linkId, input.sourceNodeId)
530
+ .first();
531
+ const deleted = existing !== null;
532
+ await deps.db.batch([
533
+ deps.db
534
+ .prepare("DELETE FROM knowledge_links WHERE id = ? AND source_node_id = ?")
535
+ .bind(input.linkId, input.sourceNodeId),
536
+ deps.db
537
+ .prepare(`INSERT INTO idempotency_keys (
538
+ actor_id, operation, idempotency_key, resource_id, created_at
539
+ ) VALUES (?, 'knowledge.unlink', ?, ?, ?)`)
540
+ .bind(input.actorId, input.idempotencyKey, `${deleted ? "1" : "0"}:${input.linkId}`, input.occurredAt),
541
+ deps.db
542
+ .prepare(`INSERT INTO audit_events (
543
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
544
+ ) VALUES (?, ?, 'knowledge.unlink', 'knowledge', ?, ?, ?)`)
545
+ .bind(input.auditId, input.actorId, input.sourceNodeId, JSON.stringify({ linkId: input.linkId, deleted }), input.occurredAt),
546
+ ]);
547
+ return deleted;
548
+ },
549
+ async graphVisible(actor, input) {
550
+ const nodeResult = await deps.db
551
+ .prepare(`${visibleCte}
552
+ SELECT ${nodeColumns}
553
+ FROM knowledge_nodes n
554
+ JOIN allowed ON allowed.id = n.id
555
+ WHERE n.archived_at IS NULL
556
+ ORDER BY lower(n.title), n.id
557
+ LIMIT ?`)
558
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), input.limit)
559
+ .all();
560
+ const nodes = (nodeResult.results ?? []).map(mapNode);
561
+ if (nodes.length === 0)
562
+ return { nodes, links: [] };
563
+ const linkResult = await deps.db
564
+ .prepare(`${visibleCte}, graph_nodes(id) AS (
565
+ SELECT n.id FROM knowledge_nodes n
566
+ JOIN allowed ON allowed.id = n.id
567
+ WHERE n.archived_at IS NULL
568
+ ORDER BY lower(n.title), n.id
569
+ LIMIT ?
570
+ )
571
+ SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
572
+ link.created_by, link.created_at
573
+ FROM knowledge_links link
574
+ JOIN graph_nodes source_node ON source_node.id = link.source_node_id
575
+ JOIN graph_nodes target_node ON target_node.id = link.target_node_id
576
+ ORDER BY link.created_at, link.id`)
577
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), input.limit)
578
+ .all();
579
+ return { nodes, links: (linkResult.results ?? []).map(mapLink) };
580
+ },
581
+ async setGrant(input) {
582
+ const grant = input.grant;
583
+ const principalType = grant.principal.type;
584
+ const principalId = grant.principal.type === "user"
585
+ ? grant.principal.id
586
+ : grant.principal.type === "email"
587
+ ? grant.principal.email.toLowerCase()
588
+ : "*";
589
+ try {
590
+ await deps.db.batch([
591
+ deps.db
592
+ .prepare(`INSERT INTO resource_grants (
593
+ id, resource_type, resource_id, principal_type, principal_id, role,
594
+ expires_at, created_by, created_at
595
+ ) VALUES (?, 'knowledge', ?, ?, ?, ?, ?, ?, ?)
596
+ ON CONFLICT (resource_type, resource_id, principal_type, principal_id)
597
+ DO UPDATE SET role = excluded.role, expires_at = excluded.expires_at`)
598
+ .bind(grant.id, grant.resourceId, principalType, principalId, grant.role, grant.expiresAt, grant.createdBy, grant.createdAt),
599
+ deps.db
600
+ .prepare(`INSERT INTO idempotency_keys (
601
+ actor_id, operation, idempotency_key, resource_id, created_at
602
+ ) SELECT ?, 'knowledge.share', ?, id, ? FROM resource_grants
603
+ WHERE resource_type = 'knowledge' AND resource_id = ?
604
+ AND principal_type = ? AND principal_id = ?`)
605
+ .bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId),
606
+ deps.db
607
+ .prepare(`INSERT INTO audit_events (
608
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
609
+ ) VALUES (?, ?, 'knowledge.share', 'knowledge', ?, ?, ?)`)
610
+ .bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType, role: grant.role }), grant.createdAt),
611
+ ]);
612
+ const stored = await deps.db
613
+ .prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
614
+ created_by, created_at FROM resource_grants
615
+ WHERE resource_type = 'knowledge' AND resource_id = ?
616
+ AND principal_type = ? AND principal_id = ?`)
617
+ .bind(grant.resourceId, principalType, principalId)
618
+ .first();
619
+ if (!stored)
620
+ throw new Error("Knowledge grant disappeared after upsert");
621
+ return mapGrant(stored);
622
+ }
623
+ catch (error) {
624
+ const replayed = await deps.db
625
+ .prepare(`SELECT resource_id FROM idempotency_keys
626
+ WHERE actor_id = ? AND operation = 'knowledge.share' AND idempotency_key = ?`)
627
+ .bind(input.actorId, input.idempotencyKey)
628
+ .first();
629
+ if (replayed) {
630
+ const row = await deps.db
631
+ .prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
632
+ created_by, created_at FROM resource_grants WHERE id = ?`)
633
+ .bind(replayed.resource_id)
634
+ .first();
635
+ if (row)
636
+ return mapGrant(row);
637
+ }
638
+ throw error;
639
+ }
640
+ },
641
+ async revokeGrant(input) {
642
+ try {
643
+ await deps.db.batch([
644
+ deps.db
645
+ .prepare(`INSERT INTO idempotency_keys (
646
+ actor_id, operation, idempotency_key, resource_id, created_at
647
+ ) SELECT ?, 'knowledge.revoke', ?,
648
+ (CASE WHEN EXISTS (
649
+ SELECT 1 FROM resource_grants
650
+ WHERE id = ? AND resource_type = 'knowledge' AND resource_id = ?
651
+ ) THEN '1:' ELSE '0:' END) || ?, ?`)
652
+ .bind(input.actorId, input.idempotencyKey, input.grantId, input.resourceId, input.grantId, input.occurredAt),
653
+ deps.db
654
+ .prepare(`DELETE FROM resource_grants
655
+ WHERE id = ? AND resource_type = 'knowledge' AND resource_id = ?`)
656
+ .bind(input.grantId, input.resourceId),
657
+ deps.db
658
+ .prepare(`INSERT INTO audit_events (
659
+ id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
660
+ ) SELECT ?, ?, 'knowledge.revoke', 'knowledge', ?,
661
+ json_object(
662
+ 'grantId', ?,
663
+ 'revoked', json(CASE WHEN substr(resource_id, 1, 2) = '1:' THEN 'true' ELSE 'false' END)
664
+ ), ?
665
+ FROM idempotency_keys
666
+ WHERE actor_id = ? AND operation = 'knowledge.revoke' AND idempotency_key = ?`)
667
+ .bind(input.auditId, input.actorId, input.resourceId, input.grantId, input.occurredAt, input.actorId, input.idempotencyKey),
668
+ ]);
669
+ }
670
+ catch (error) {
671
+ const replayed = await this.findIdempotentRevocation(input.actorId, input.idempotencyKey);
672
+ if (replayed === null)
673
+ throw error;
674
+ return replayed;
675
+ }
676
+ return (await this.findIdempotentRevocation(input.actorId, input.idempotencyKey)) ?? false;
677
+ },
678
+ async searchVisible(actor, input) {
679
+ const query = toFtsQuery(input.query);
680
+ if (!query)
681
+ return [];
682
+ const result = await deps.db
683
+ .prepare(`${visibleCte}
684
+ SELECT n.id AS node_id, v.id AS version_id, n.title,
685
+ snippet(knowledge_fts, 3, '', '', ' … ', 32) AS passage,
686
+ n.updated_at AS freshness, bm25(knowledge_fts, 0.0, 0.0, 4.0, 1.0) AS rank
687
+ FROM knowledge_fts
688
+ JOIN knowledge_nodes n ON n.id = knowledge_fts.node_id
689
+ JOIN knowledge_versions v ON v.id = knowledge_fts.version_id
690
+ JOIN allowed ON allowed.id = n.id
691
+ WHERE knowledge_fts MATCH ?
692
+ AND n.current_version_id = v.id
693
+ AND n.archived_at IS NULL
694
+ ORDER BY rank, n.updated_at DESC
695
+ LIMIT ?`)
696
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), query, input.limit)
697
+ .all();
698
+ return (result.results ?? []).map((row) => ({
699
+ nodeId: row.node_id,
700
+ versionId: row.version_id,
701
+ title: row.title,
702
+ passage: row.passage,
703
+ source: `intel://knowledge/${encodeURIComponent(row.node_id)}?version=${encodeURIComponent(row.version_id)}`,
704
+ freshness: row.freshness,
705
+ score: 1 / (1 + Math.abs(row.rank)),
706
+ match: "lexical",
707
+ }));
708
+ },
709
+ async hydrateVisibleCitations(actor, nodeIds) {
710
+ const uniqueIds = [...new Set(nodeIds)].slice(0, 100);
711
+ if (uniqueIds.length === 0)
712
+ return [];
713
+ const rows = [];
714
+ for (let offset = 0; offset < uniqueIds.length; offset += 96) {
715
+ const ids = uniqueIds.slice(offset, offset + 96);
716
+ const placeholders = ids.map(() => "?").join(", ");
717
+ const result = await deps.db
718
+ .prepare(`${visibleCte}
719
+ SELECT n.id AS node_id, v.id AS version_id, n.title,
720
+ substr(knowledge_fts.content, 1, 480) AS passage,
721
+ n.updated_at AS freshness
722
+ FROM knowledge_nodes n
723
+ JOIN knowledge_versions v ON v.id = n.current_version_id
724
+ JOIN knowledge_fts ON knowledge_fts.version_id = v.id
725
+ JOIN allowed ON allowed.id = n.id
726
+ WHERE n.id IN (${placeholders}) AND n.archived_at IS NULL`)
727
+ .bind(actor.id, actor.id, actor.email, deps.now().toISOString(), ...ids)
728
+ .all();
729
+ rows.push(...(result.results ?? []));
730
+ }
731
+ const order = new Map(uniqueIds.map((id, index) => [id, index]));
732
+ rows.sort((left, right) => (order.get(left.node_id) ?? 0) - (order.get(right.node_id) ?? 0));
733
+ return rows.map((row) => ({
734
+ nodeId: row.node_id,
735
+ versionId: row.version_id,
736
+ title: row.title,
737
+ passage: row.passage,
738
+ source: `intel://knowledge/${encodeURIComponent(row.node_id)}?version=${encodeURIComponent(row.version_id)}`,
739
+ freshness: row.freshness,
740
+ score: 0,
741
+ match: "semantic",
742
+ }));
743
+ },
744
+ async listCurrentVersionIds(input) {
745
+ const result = await deps.db
746
+ .prepare(`SELECT current_version_id AS id FROM knowledge_nodes
747
+ WHERE current_version_id IS NOT NULL AND archived_at IS NULL
748
+ AND (? IS NULL OR current_version_id > ?)
749
+ ORDER BY id LIMIT ?`)
750
+ .bind(input.after, input.after, input.limit)
751
+ .all();
752
+ return (result.results ?? []).map((row) => row.id);
753
+ },
754
+ };
755
+ }