@anchrd/intel-api 0.3.1 → 0.3.2
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/dist/adapters/cloudflare/cloudflare.js +11 -12
- package/dist/adapters/db/db-flows.js +367 -193
- package/dist/adapters/db/db-grants.d.ts +47 -0
- package/dist/adapters/db/db-grants.js +125 -0
- package/dist/adapters/db/db-indexing.js +14 -3
- package/dist/adapters/db/db.js +236 -202
- package/dist/flows/flows.d.ts +31 -1
- package/dist/flows/flows.js +939 -72
- package/dist/flows/flows.types.d.ts +92 -28
- package/dist/http/http.js +130 -53
- package/dist/indexing/indexing.js +14 -3
- package/dist/knowledge/document-links/document-links.d.ts +15 -0
- package/dist/knowledge/document-links/document-links.js +52 -0
- package/dist/knowledge/knowledge.js +347 -74
- package/dist/knowledge/knowledge.types.d.ts +58 -27
- package/dist/mcp/mcp.js +153 -71
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +1 -0
- package/dist/shared/gate-authorization/gate-authorization.js +7 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +39 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ResourceVerb } from "@anchrd/intel-contract";
|
|
2
|
+
export interface GrantActor {
|
|
3
|
+
id: string;
|
|
4
|
+
email: string;
|
|
5
|
+
isAdmin?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const subtreeCte: string;
|
|
8
|
+
export declare function subtreeBindings(actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
9
|
+
/**
|
|
10
|
+
* Both walks in one statement: what the actor may open, and what they may run. Its bindings are
|
|
11
|
+
* `subtreeBindings(actor, "read", now)` followed by `subtreeBindings(actor, "execute", now)`, in
|
|
12
|
+
* that order.
|
|
13
|
+
*/
|
|
14
|
+
export declare const readableOrRunnableCte: string;
|
|
15
|
+
/**
|
|
16
|
+
* A flow the actor may open *or* may run, for a statement carrying `readableOrRunnableCte`. It is
|
|
17
|
+
* the callable rule of ADR-0004 §2/§3 — `execute` without `read` is the library, a building block
|
|
18
|
+
* anyone may run and few may open — asked of a whole list of flows at once instead of one flow at a
|
|
19
|
+
* time. Its bindings follow the two CTEs' and are `flowInSubtreeBindings(actor)`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const flowCallable = "(\n ? = 1\n OR flow.owner_id = ?\n OR flow.parent_id IN (SELECT id FROM readable)\n OR flow.parent_id IN (SELECT id FROM runnable)\n)";
|
|
22
|
+
/**
|
|
23
|
+
* The point check for one Knowledge node: the node itself and every ancestor above it. Cheaper than
|
|
24
|
+
* the subtree walk and the same answer, because a grant reaches down and never sideways.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ `UNION`, never `UNION ALL`. The service refuses to move a node into its own descendant, but a
|
|
27
|
+
* cycle in `parent_id` can still reach the table — a restore, a repair, a future writer. `UNION ALL`
|
|
28
|
+
* would walk such a ring without end, and this query runs on the authorization path of every
|
|
29
|
+
* request, so the damage lands in the database rather than in one tab. `UNION` folds the repeated
|
|
30
|
+
* row away and the recursion stops on its own.
|
|
31
|
+
*/
|
|
32
|
+
export declare const nodeVerbQuery: string;
|
|
33
|
+
export declare function nodeVerbBindings(nodeId: string, actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
34
|
+
/**
|
|
35
|
+
* The same question for a flow. A flow carries no grant of its own any more: what reaches it is the
|
|
36
|
+
* folder it is filed in and that folder's ancestors. Its owner keeps it, the way a Knowledge node's
|
|
37
|
+
* owner keeps theirs — otherwise a flow at the root of the tree would be unreachable by the person
|
|
38
|
+
* who created it. `UNION` for the same reason as above.
|
|
39
|
+
*/
|
|
40
|
+
export declare const flowVerbQuery: string;
|
|
41
|
+
export declare function flowVerbBindings(flowId: string, actor: GrantActor, verb: ResourceVerb, now: string): unknown[];
|
|
42
|
+
/**
|
|
43
|
+
* The predicate a query over `flows` uses when it already carries `subtreeCte` for the same verb.
|
|
44
|
+
* Its bindings follow the CTE's.
|
|
45
|
+
*/
|
|
46
|
+
export declare const flowInSubtree = "(? = 1 OR flow.owner_id = ? OR flow.parent_id IN (SELECT id FROM allowed))";
|
|
47
|
+
export declare function flowInSubtreeBindings(actor: GrantActor): unknown[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const principalMatch = `(
|
|
2
|
+
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
3
|
+
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
4
|
+
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
5
|
+
)`;
|
|
6
|
+
function grantExists(nodeColumn) {
|
|
7
|
+
return `EXISTS (
|
|
8
|
+
SELECT 1 FROM tree_grants grant_row
|
|
9
|
+
WHERE grant_row.node_id = ${nodeColumn}
|
|
10
|
+
AND ${principalMatch}
|
|
11
|
+
AND grant_row.verb = ?
|
|
12
|
+
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
13
|
+
)`;
|
|
14
|
+
}
|
|
15
|
+
// Bindings for one `grantExists`: principal (twice), verb, and the moment expiry is measured
|
|
16
|
+
// against. Callers concatenate these in SQL order; the helpers below say which order that is.
|
|
17
|
+
function grantBindings(actor, verb, now) {
|
|
18
|
+
return [actor.id, actor.email, verb, now];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Seeds every node the actor reaches for one verb and walks downwards, which is what "a grant
|
|
22
|
+
* applies to everything beneath it" means in SQL. The name is a parameter because one statement can
|
|
23
|
+
* need two of these — asking `read` and `execute` at once is what turns "one query per callee" into
|
|
24
|
+
* one query (#30) — and a second walk written out by hand would be a second copy of the rule.
|
|
25
|
+
*/
|
|
26
|
+
function subtreeWalk(name) {
|
|
27
|
+
return `${name}(id, parent_id) AS (
|
|
28
|
+
SELECT seed.id, seed.parent_id
|
|
29
|
+
FROM knowledge_nodes seed
|
|
30
|
+
WHERE ? = 1
|
|
31
|
+
OR seed.owner_id = ?
|
|
32
|
+
OR ${grantExists("seed.id")}
|
|
33
|
+
UNION
|
|
34
|
+
SELECT child.id, child.parent_id
|
|
35
|
+
FROM knowledge_nodes child
|
|
36
|
+
JOIN ${name} parent ON child.parent_id = parent.id
|
|
37
|
+
)`;
|
|
38
|
+
}
|
|
39
|
+
// The walk under the name every single-verb statement already uses. Callers append their own SELECT
|
|
40
|
+
// and may append further CTEs with a comma.
|
|
41
|
+
export const subtreeCte = `WITH RECURSIVE ${subtreeWalk("allowed")}`;
|
|
42
|
+
export function subtreeBindings(actor, verb, now) {
|
|
43
|
+
return [actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Both walks in one statement: what the actor may open, and what they may run. Its bindings are
|
|
47
|
+
* `subtreeBindings(actor, "read", now)` followed by `subtreeBindings(actor, "execute", now)`, in
|
|
48
|
+
* that order.
|
|
49
|
+
*/
|
|
50
|
+
export const readableOrRunnableCte = `WITH RECURSIVE ${subtreeWalk("readable")},
|
|
51
|
+
${subtreeWalk("runnable")}`;
|
|
52
|
+
/**
|
|
53
|
+
* A flow the actor may open *or* may run, for a statement carrying `readableOrRunnableCte`. It is
|
|
54
|
+
* the callable rule of ADR-0004 §2/§3 — `execute` without `read` is the library, a building block
|
|
55
|
+
* anyone may run and few may open — asked of a whole list of flows at once instead of one flow at a
|
|
56
|
+
* time. Its bindings follow the two CTEs' and are `flowInSubtreeBindings(actor)`.
|
|
57
|
+
*/
|
|
58
|
+
export const flowCallable = `(
|
|
59
|
+
? = 1
|
|
60
|
+
OR flow.owner_id = ?
|
|
61
|
+
OR flow.parent_id IN (SELECT id FROM readable)
|
|
62
|
+
OR flow.parent_id IN (SELECT id FROM runnable)
|
|
63
|
+
)`;
|
|
64
|
+
/**
|
|
65
|
+
* The point check for one Knowledge node: the node itself and every ancestor above it. Cheaper than
|
|
66
|
+
* the subtree walk and the same answer, because a grant reaches down and never sideways.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ `UNION`, never `UNION ALL`. The service refuses to move a node into its own descendant, but a
|
|
69
|
+
* cycle in `parent_id` can still reach the table — a restore, a repair, a future writer. `UNION ALL`
|
|
70
|
+
* would walk such a ring without end, and this query runs on the authorization path of every
|
|
71
|
+
* request, so the damage lands in the database rather than in one tab. `UNION` folds the repeated
|
|
72
|
+
* row away and the recursion stops on its own.
|
|
73
|
+
*/
|
|
74
|
+
export const nodeVerbQuery = `WITH RECURSIVE ancestors(id, parent_id, owner_id) AS (
|
|
75
|
+
SELECT id, parent_id, owner_id FROM knowledge_nodes WHERE id = ?
|
|
76
|
+
UNION
|
|
77
|
+
SELECT parent.id, parent.parent_id, parent.owner_id
|
|
78
|
+
FROM knowledge_nodes parent
|
|
79
|
+
JOIN ancestors child ON child.parent_id = parent.id
|
|
80
|
+
)
|
|
81
|
+
SELECT 1 AS allowed
|
|
82
|
+
FROM ancestors
|
|
83
|
+
WHERE ? = 1
|
|
84
|
+
OR owner_id = ?
|
|
85
|
+
OR ${grantExists("ancestors.id")}
|
|
86
|
+
LIMIT 1`;
|
|
87
|
+
export function nodeVerbBindings(nodeId, actor, verb, now) {
|
|
88
|
+
return [nodeId, actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The same question for a flow. A flow carries no grant of its own any more: what reaches it is the
|
|
92
|
+
* folder it is filed in and that folder's ancestors. Its owner keeps it, the way a Knowledge node's
|
|
93
|
+
* owner keeps theirs — otherwise a flow at the root of the tree would be unreachable by the person
|
|
94
|
+
* who created it. `UNION` for the same reason as above.
|
|
95
|
+
*/
|
|
96
|
+
export const flowVerbQuery = `WITH RECURSIVE ancestors(id, parent_id) AS (
|
|
97
|
+
SELECT folder.id, folder.parent_id
|
|
98
|
+
FROM knowledge_nodes folder
|
|
99
|
+
JOIN flows flow ON flow.parent_id = folder.id
|
|
100
|
+
WHERE flow.id = ?
|
|
101
|
+
UNION
|
|
102
|
+
SELECT parent.id, parent.parent_id
|
|
103
|
+
FROM knowledge_nodes parent
|
|
104
|
+
JOIN ancestors child ON child.parent_id = parent.id
|
|
105
|
+
)
|
|
106
|
+
SELECT 1 AS allowed
|
|
107
|
+
FROM flows flow
|
|
108
|
+
WHERE flow.id = ?
|
|
109
|
+
AND (
|
|
110
|
+
? = 1
|
|
111
|
+
OR flow.owner_id = ?
|
|
112
|
+
OR EXISTS (SELECT 1 FROM ancestors WHERE ${grantExists("ancestors.id")})
|
|
113
|
+
)
|
|
114
|
+
LIMIT 1`;
|
|
115
|
+
export function flowVerbBindings(flowId, actor, verb, now) {
|
|
116
|
+
return [flowId, flowId, actor.isAdmin ? 1 : 0, actor.id, ...grantBindings(actor, verb, now)];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The predicate a query over `flows` uses when it already carries `subtreeCte` for the same verb.
|
|
120
|
+
* Its bindings follow the CTE's.
|
|
121
|
+
*/
|
|
122
|
+
export const flowInSubtree = `(? = 1 OR flow.owner_id = ? OR flow.parent_id IN (SELECT id FROM allowed))`;
|
|
123
|
+
export function flowInSubtreeBindings(actor) {
|
|
124
|
+
return [actor.isAdmin ? 1 : 0, actor.id];
|
|
125
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
function mapTarget(row) {
|
|
1
|
+
function mapTarget(row, contentKeys) {
|
|
2
2
|
return {
|
|
3
3
|
nodeId: row.node_id,
|
|
4
4
|
versionId: row.version_id,
|
|
5
5
|
title: row.title,
|
|
6
|
-
|
|
6
|
+
contentKeys,
|
|
7
7
|
mediaType: row.media_type,
|
|
8
8
|
kind: row.kind,
|
|
9
9
|
updatedAt: row.updated_at,
|
|
@@ -20,7 +20,18 @@ export function createKnowledgeIndexRepository(db) {
|
|
|
20
20
|
WHERE v.id = ? AND n.current_version_id = v.id AND n.archived_at IS NULL`)
|
|
21
21
|
.bind(versionId)
|
|
22
22
|
.first();
|
|
23
|
-
|
|
23
|
+
if (!row)
|
|
24
|
+
return null;
|
|
25
|
+
if (row.kind !== "table")
|
|
26
|
+
return mapTarget(row, [row.content_key]);
|
|
27
|
+
// A table's rows live in every version, so indexing the newest one alone would make a search
|
|
28
|
+
// find only what the last append added (#40).
|
|
29
|
+
const segments = await db
|
|
30
|
+
.prepare(`SELECT content_key FROM knowledge_versions
|
|
31
|
+
WHERE node_id = ? ORDER BY sequence`)
|
|
32
|
+
.bind(row.node_id)
|
|
33
|
+
.all();
|
|
34
|
+
return mapTarget(row, (segments.results ?? []).map((segment) => segment.content_key));
|
|
24
35
|
},
|
|
25
36
|
async replace(target, content) {
|
|
26
37
|
await db.batch([
|