@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.
- package/README.md +96 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +21 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +39 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +8 -0
- package/dist/adapters/cloudflare/cloudflare.js +200 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +22 -0
- package/dist/adapters/content/content.types.d.ts +14 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-flows.d.ts +6 -0
- package/dist/adapters/db/db-flows.js +466 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +70 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-tools.d.ts +6 -0
- package/dist/adapters/db/db-tools.js +200 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +755 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +25 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +9 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +31 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +159 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +71 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +109 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +57 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +32 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +81 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +176 -0
- package/dist/auth/auth.types.d.ts +104 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +125 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +141 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/flows/flows.d.ts +4 -0
- package/dist/flows/flows.js +409 -0
- package/dist/flows/flows.types.d.ts +108 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +8 -0
- package/dist/http/http.js +286 -0
- package/dist/http/http.types.d.ts +14 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +48 -0
- package/dist/indexing/indexing.types.d.ts +10 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +77 -0
- package/dist/intel/intel.types.d.ts +14 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/knowledge/knowledge.d.ts +2 -0
- package/dist/knowledge/knowledge.js +479 -0
- package/dist/knowledge/knowledge.types.d.ts +162 -0
- package/dist/knowledge/knowledge.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +484 -0
- package/dist/mcp/mcp.types.d.ts +10 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +4 -0
- package/dist/shared/gate-authorization/gate-authorization.js +19 -0
- package/dist/shared/intel-error/intel-error.d.ts +5 -0
- package/dist/shared/intel-error/intel-error.js +10 -0
- package/dist/shared/problem-details/problem-details.d.ts +8 -0
- package/dist/shared/problem-details/problem-details.js +4 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +1 -0
- package/dist/shared/sha256/sha256.js +9 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +107 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- 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
|
+
}
|