@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,211 @@
|
|
|
1
|
+
-- ADR-0004 §2: a grant sits on a node of the shared tree and applies to everything beneath it, and
|
|
2
|
+
-- the verbs `read` / `write` / `execute` / `share` are granted independently. `role` could not carry
|
|
3
|
+
-- that: it is a ladder (viewer < editor < manager), and the case the ADR is built around — /crm is
|
|
4
|
+
-- readable for sales and executable only for billing — is not a rung on it.
|
|
5
|
+
--
|
|
6
|
+
-- `resource_grants` is deliberately left in place and untouched. The previous version of the Worker
|
|
7
|
+
-- reads it and keeps answering correctly until it is replaced; nothing in this version reads it any
|
|
8
|
+
-- more. Once no old Worker is left, the table holds only history and can be dropped.
|
|
9
|
+
|
|
10
|
+
CREATE TABLE tree_grants (
|
|
11
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
12
|
+
-- Any node of the shared tree, not only a folder: a grant that was migrated from a document has
|
|
13
|
+
-- to stay exactly where it was. Moving it up to the parent folder would hand its principal the
|
|
14
|
+
-- folder's other contents as well, and a migration must never widen access. A grant on a leaf is
|
|
15
|
+
-- simply a subtree of one.
|
|
16
|
+
node_id TEXT NOT NULL REFERENCES knowledge_nodes(id),
|
|
17
|
+
principal_type TEXT NOT NULL CHECK (principal_type IN ('user', 'email', 'organization')),
|
|
18
|
+
principal_id TEXT NOT NULL,
|
|
19
|
+
verb TEXT NOT NULL CHECK (verb IN ('read', 'write', 'execute', 'share')),
|
|
20
|
+
expires_at TEXT,
|
|
21
|
+
created_by TEXT NOT NULL,
|
|
22
|
+
created_at TEXT NOT NULL,
|
|
23
|
+
-- The verb belongs in the key. Without it one principal could hold only one verb per node, which
|
|
24
|
+
-- is the ladder again under a new name.
|
|
25
|
+
UNIQUE (node_id, principal_type, principal_id, verb)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE INDEX tree_grants_principal_idx ON tree_grants(
|
|
29
|
+
principal_type,
|
|
30
|
+
principal_id,
|
|
31
|
+
verb,
|
|
32
|
+
expires_at
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE INDEX tree_grants_node_idx ON tree_grants(node_id, verb);
|
|
36
|
+
|
|
37
|
+
-- `principal_type` and `principal_id` are carried over verbatim, and that is a decision rather than
|
|
38
|
+
-- laziness. Both resolvers read a principal the same way: a user by ID, an email folded to lower
|
|
39
|
+
-- case on both sides, and the whole organization as the literal `*` that `setGrant` has always
|
|
40
|
+
-- written. Rewriting any of them here would be a migration changing who a grant means.
|
|
41
|
+
--
|
|
42
|
+
-- The one case worth naming is an `organization` row whose `principal_id` is not `*`. Nothing in
|
|
43
|
+
-- Intel writes such a row, but a repair or an import could have. It is copied as it stands, so it
|
|
44
|
+
-- matches nobody and the access is gone; normalizing it to `*` would turn a row of unknown scope
|
|
45
|
+
-- into a grant for everyone. Narrow is a lost grant somebody notices, wide is access nobody sees.
|
|
46
|
+
|
|
47
|
+
-- A flow grant has no folder to move to that would be honest. Its own parent folder holds other
|
|
48
|
+
-- things, so putting the grant there would show the principal everything else in that folder. The
|
|
49
|
+
-- narrow reading is a folder that contains exactly this one flow: the flow moves into it, the grant
|
|
50
|
+
-- lands on it, and nothing else is inside. The ID is derived from the flow so a re-run finds the
|
|
51
|
+
-- same row, the same way 0002 derived one folder per owner.
|
|
52
|
+
INSERT OR IGNORE INTO knowledge_nodes (
|
|
53
|
+
id, parent_id, kind, title, description, context_policy, owner_id,
|
|
54
|
+
current_version_id, created_at, updated_at, archived_at
|
|
55
|
+
)
|
|
56
|
+
SELECT
|
|
57
|
+
'folder-flow-' || flow.id,
|
|
58
|
+
flow.parent_id,
|
|
59
|
+
'folder',
|
|
60
|
+
flow.title,
|
|
61
|
+
'Holds one flow that was shared on its own before shares moved to the folder.',
|
|
62
|
+
'explicit',
|
|
63
|
+
flow.owner_id,
|
|
64
|
+
NULL,
|
|
65
|
+
'2026-08-01T00:00:00.000Z',
|
|
66
|
+
'2026-08-01T00:00:00.000Z',
|
|
67
|
+
NULL
|
|
68
|
+
FROM flows flow
|
|
69
|
+
WHERE EXISTS (
|
|
70
|
+
SELECT 1 FROM resource_grants grant_row
|
|
71
|
+
WHERE grant_row.resource_type = 'flow' AND grant_row.resource_id = flow.id
|
|
72
|
+
)
|
|
73
|
+
AND flow.parent_id IS NOT 'folder-flow-' || flow.id;
|
|
74
|
+
|
|
75
|
+
UPDATE flows SET parent_id = 'folder-flow-' || id
|
|
76
|
+
WHERE EXISTS (
|
|
77
|
+
SELECT 1 FROM resource_grants grant_row
|
|
78
|
+
WHERE grant_row.resource_type = 'flow' AND grant_row.resource_id = flows.id
|
|
79
|
+
)
|
|
80
|
+
AND EXISTS (SELECT 1 FROM knowledge_nodes node WHERE node.id = 'folder-flow-' || flows.id);
|
|
81
|
+
|
|
82
|
+
-- Seeing a flow and starting it were the same permission until now: any flow grant plus the Gate
|
|
83
|
+
-- `flows/run` capability could start a run. Splitting the two without carrying `execute` over would
|
|
84
|
+
-- quietly take away something people have today, so every migrated flow grant keeps it.
|
|
85
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
86
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
87
|
+
)
|
|
88
|
+
SELECT
|
|
89
|
+
grant_row.id || ':read',
|
|
90
|
+
'folder-flow-' || grant_row.resource_id,
|
|
91
|
+
grant_row.principal_type,
|
|
92
|
+
grant_row.principal_id,
|
|
93
|
+
'read',
|
|
94
|
+
grant_row.expires_at,
|
|
95
|
+
grant_row.created_by,
|
|
96
|
+
grant_row.created_at
|
|
97
|
+
FROM resource_grants grant_row
|
|
98
|
+
WHERE grant_row.resource_type = 'flow'
|
|
99
|
+
AND EXISTS (
|
|
100
|
+
SELECT 1 FROM knowledge_nodes node WHERE node.id = 'folder-flow-' || grant_row.resource_id
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
104
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
105
|
+
)
|
|
106
|
+
SELECT
|
|
107
|
+
grant_row.id || ':execute',
|
|
108
|
+
'folder-flow-' || grant_row.resource_id,
|
|
109
|
+
grant_row.principal_type,
|
|
110
|
+
grant_row.principal_id,
|
|
111
|
+
'execute',
|
|
112
|
+
grant_row.expires_at,
|
|
113
|
+
grant_row.created_by,
|
|
114
|
+
grant_row.created_at
|
|
115
|
+
FROM resource_grants grant_row
|
|
116
|
+
WHERE grant_row.resource_type = 'flow'
|
|
117
|
+
AND EXISTS (
|
|
118
|
+
SELECT 1 FROM knowledge_nodes node WHERE node.id = 'folder-flow-' || grant_row.resource_id
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
122
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
123
|
+
)
|
|
124
|
+
SELECT
|
|
125
|
+
grant_row.id || ':write',
|
|
126
|
+
'folder-flow-' || grant_row.resource_id,
|
|
127
|
+
grant_row.principal_type,
|
|
128
|
+
grant_row.principal_id,
|
|
129
|
+
'write',
|
|
130
|
+
grant_row.expires_at,
|
|
131
|
+
grant_row.created_by,
|
|
132
|
+
grant_row.created_at
|
|
133
|
+
FROM resource_grants grant_row
|
|
134
|
+
WHERE grant_row.resource_type = 'flow'
|
|
135
|
+
AND grant_row.role IN ('editor', 'manager')
|
|
136
|
+
AND EXISTS (
|
|
137
|
+
SELECT 1 FROM knowledge_nodes node WHERE node.id = 'folder-flow-' || grant_row.resource_id
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
141
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
142
|
+
)
|
|
143
|
+
SELECT
|
|
144
|
+
grant_row.id || ':share',
|
|
145
|
+
'folder-flow-' || grant_row.resource_id,
|
|
146
|
+
grant_row.principal_type,
|
|
147
|
+
grant_row.principal_id,
|
|
148
|
+
'share',
|
|
149
|
+
grant_row.expires_at,
|
|
150
|
+
grant_row.created_by,
|
|
151
|
+
grant_row.created_at
|
|
152
|
+
FROM resource_grants grant_row
|
|
153
|
+
WHERE grant_row.resource_type = 'flow'
|
|
154
|
+
AND grant_row.role = 'manager'
|
|
155
|
+
AND EXISTS (
|
|
156
|
+
SELECT 1 FROM knowledge_nodes node WHERE node.id = 'folder-flow-' || grant_row.resource_id
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
-- Knowledge grants stay on the node they were set on and keep exactly the verbs their role implied.
|
|
160
|
+
-- `execute` is not among them for any role: a Knowledge grant never allowed anyone to start a run,
|
|
161
|
+
-- and handing it out here would be the wide reading of an unambiguous case. `commenter` has no verb
|
|
162
|
+
-- of its own yet, so it maps to `read` — the least it could have meant.
|
|
163
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
164
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
165
|
+
)
|
|
166
|
+
SELECT
|
|
167
|
+
grant_row.id || ':read',
|
|
168
|
+
grant_row.resource_id,
|
|
169
|
+
grant_row.principal_type,
|
|
170
|
+
grant_row.principal_id,
|
|
171
|
+
'read',
|
|
172
|
+
grant_row.expires_at,
|
|
173
|
+
grant_row.created_by,
|
|
174
|
+
grant_row.created_at
|
|
175
|
+
FROM resource_grants grant_row
|
|
176
|
+
WHERE grant_row.resource_type = 'knowledge'
|
|
177
|
+
AND EXISTS (SELECT 1 FROM knowledge_nodes node WHERE node.id = grant_row.resource_id);
|
|
178
|
+
|
|
179
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
180
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
181
|
+
)
|
|
182
|
+
SELECT
|
|
183
|
+
grant_row.id || ':write',
|
|
184
|
+
grant_row.resource_id,
|
|
185
|
+
grant_row.principal_type,
|
|
186
|
+
grant_row.principal_id,
|
|
187
|
+
'write',
|
|
188
|
+
grant_row.expires_at,
|
|
189
|
+
grant_row.created_by,
|
|
190
|
+
grant_row.created_at
|
|
191
|
+
FROM resource_grants grant_row
|
|
192
|
+
WHERE grant_row.resource_type = 'knowledge'
|
|
193
|
+
AND grant_row.role IN ('editor', 'manager')
|
|
194
|
+
AND EXISTS (SELECT 1 FROM knowledge_nodes node WHERE node.id = grant_row.resource_id);
|
|
195
|
+
|
|
196
|
+
INSERT OR IGNORE INTO tree_grants (
|
|
197
|
+
id, node_id, principal_type, principal_id, verb, expires_at, created_by, created_at
|
|
198
|
+
)
|
|
199
|
+
SELECT
|
|
200
|
+
grant_row.id || ':share',
|
|
201
|
+
grant_row.resource_id,
|
|
202
|
+
grant_row.principal_type,
|
|
203
|
+
grant_row.principal_id,
|
|
204
|
+
'share',
|
|
205
|
+
grant_row.expires_at,
|
|
206
|
+
grant_row.created_by,
|
|
207
|
+
grant_row.created_at
|
|
208
|
+
FROM resource_grants grant_row
|
|
209
|
+
WHERE grant_row.resource_type = 'knowledge'
|
|
210
|
+
AND grant_row.role = 'manager'
|
|
211
|
+
AND EXISTS (SELECT 1 FROM knowledge_nodes node WHERE node.id = grant_row.resource_id);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- ADR-0004 §3/§4: a flow may call another flow. The called flow gets a run of its own — its own
|
|
2
|
+
-- immutable version, its own steps, its own authorization against the user — and these two columns
|
|
3
|
+
-- are the only thing that ties it back to the step that called it.
|
|
4
|
+
--
|
|
5
|
+
-- A single run row that "descends into" the callee was the alternative. It would have had to carry
|
|
6
|
+
-- two versions at once, and the step history would no longer say which graph a node belonged to.
|
|
7
|
+
|
|
8
|
+
ALTER TABLE flow_runs ADD COLUMN parent_run_id TEXT REFERENCES flow_runs(id);
|
|
9
|
+
ALTER TABLE flow_runs ADD COLUMN parent_node_id TEXT;
|
|
10
|
+
|
|
11
|
+
-- One call site starts at most one run: the index is unique so a retried start cannot leave two
|
|
12
|
+
-- results behind for the same step, with nothing to say which one flows back.
|
|
13
|
+
CREATE UNIQUE INDEX flow_runs_call_site_idx ON flow_runs(parent_run_id, parent_node_id)
|
|
14
|
+
WHERE parent_run_id IS NOT NULL;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- #39. Two fields leave the flow graph, so the graphs already in the table are rewritten to match
|
|
2
|
+
-- the contract that will read them. Both are removals of things that never did anything, which is
|
|
3
|
+
-- why the rewrite can be unconditional rather than a decision per flow.
|
|
4
|
+
--
|
|
5
|
+
-- `description` on a node: a node has a title, and what it should do stands in the instruction. Two
|
|
6
|
+
-- free texts beside each other were both kept half up to date.
|
|
7
|
+
--
|
|
8
|
+
-- A trigger's mode: `webhook` and `schedule` triggered nothing and, after the decision in #32, never
|
|
9
|
+
-- will — a flow is carried out by an external agent that brings its own schedule. `manual` is the
|
|
10
|
+
-- honest reading of every trigger that is stored today, because manual is what all of them did.
|
|
11
|
+
--
|
|
12
|
+
-- ⚠️ Written as one pass over the node array rather than a targeted UPDATE: `json_remove` is a no-op
|
|
13
|
+
-- where the key is absent, so re-running this changes nothing. Only `flow_versions` holds a graph.
|
|
14
|
+
UPDATE flow_versions
|
|
15
|
+
SET graph_json = json_set(
|
|
16
|
+
graph_json,
|
|
17
|
+
'$.nodes',
|
|
18
|
+
(
|
|
19
|
+
SELECT json_group_array(
|
|
20
|
+
CASE
|
|
21
|
+
WHEN json_extract(node.value, '$.kind') = 'trigger'
|
|
22
|
+
THEN json_set(json_remove(node.value, '$.description'), '$.configuration', json_object('mode', 'manual'))
|
|
23
|
+
ELSE json_remove(node.value, '$.description')
|
|
24
|
+
END
|
|
25
|
+
)
|
|
26
|
+
FROM json_each(flow_versions.graph_json, '$.nodes') AS node
|
|
27
|
+
)
|
|
28
|
+
);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
-- #40: a fourth kind in the shared tree — `table`. It is a node like the other three: same
|
|
2
|
+
-- `parent_id`, same folder grants, same immutable `knowledge_versions` rows, same R2 body
|
|
3
|
+
-- (ADR-0004 §1). Nothing here creates a second content model; only the CHECK has to learn the word.
|
|
4
|
+
--
|
|
5
|
+
-- SQLite cannot alter a CHECK constraint, so the table is rebuilt. Dropping the old table would
|
|
6
|
+
-- otherwise trip the foreign keys of `knowledge_versions`, `knowledge_links`, `tree_grants` and
|
|
7
|
+
-- `flows`; deferring them to the end of the migration lets the rename put the same IDs back under
|
|
8
|
+
-- the same name before anything is checked.
|
|
9
|
+
PRAGMA defer_foreign_keys = TRUE;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE knowledge_nodes_next (
|
|
12
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
13
|
+
parent_id TEXT REFERENCES knowledge_nodes_next(id),
|
|
14
|
+
kind TEXT NOT NULL CHECK (kind IN ('folder', 'document', 'attachment', 'table')),
|
|
15
|
+
title TEXT NOT NULL CHECK (length(title) BETWEEN 1 AND 240),
|
|
16
|
+
description TEXT CHECK (description IS NULL OR length(description) <= 2000),
|
|
17
|
+
context_policy TEXT NOT NULL CHECK (context_policy IN ('pinned', 'relevant', 'explicit')),
|
|
18
|
+
owner_id TEXT NOT NULL,
|
|
19
|
+
current_version_id TEXT,
|
|
20
|
+
created_at TEXT NOT NULL,
|
|
21
|
+
updated_at TEXT NOT NULL,
|
|
22
|
+
archived_at TEXT
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
INSERT INTO knowledge_nodes_next (
|
|
26
|
+
id, parent_id, kind, title, description, context_policy, owner_id,
|
|
27
|
+
current_version_id, created_at, updated_at, archived_at
|
|
28
|
+
)
|
|
29
|
+
SELECT
|
|
30
|
+
id, parent_id, kind, title, description, context_policy, owner_id,
|
|
31
|
+
current_version_id, created_at, updated_at, archived_at
|
|
32
|
+
FROM knowledge_nodes;
|
|
33
|
+
|
|
34
|
+
DROP TABLE knowledge_nodes;
|
|
35
|
+
|
|
36
|
+
ALTER TABLE knowledge_nodes_next RENAME TO knowledge_nodes;
|
|
37
|
+
|
|
38
|
+
CREATE INDEX knowledge_nodes_parent_idx ON knowledge_nodes(parent_id, archived_at, title);
|
|
39
|
+
CREATE INDEX knowledge_nodes_owner_idx ON knowledge_nodes(owner_id, archived_at);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- #41: a relationship between two documents is written where it is meant — in the text — and the
|
|
2
|
+
-- dialog that used to create one is gone. Saving a document now rewrites the links it contains.
|
|
3
|
+
--
|
|
4
|
+
-- That rewrite must not touch the links people made in the dialog before there was another way, so
|
|
5
|
+
-- every link learns where it came from. Existing rows are `manual`: they are history, they stay
|
|
6
|
+
-- readable as outgoing links and as backlinks, and nothing writes another one.
|
|
7
|
+
--
|
|
8
|
+
-- Deliberately not a migration of the data. Rewriting the old `relation` values into one kind, or
|
|
9
|
+
-- deleting the rows outright, would throw away a distinction somebody chose on purpose; keeping the
|
|
10
|
+
-- column costs nothing and keeps every existing relationship exactly as it was.
|
|
11
|
+
--
|
|
12
|
+
-- SQLite cannot add a CHECK with ALTER TABLE, so the constraint on this column lives in the
|
|
13
|
+
-- contract (`KnowledgeLinkOrigin`) and in the one repository method that writes it. There is no
|
|
14
|
+
-- table rebuild for it: `knowledge_links` is the target of no foreign key, but it is the source of
|
|
15
|
+
-- two, and a rebuild would be a lot of risk for a constraint the only writer already enforces.
|
|
16
|
+
ALTER TABLE knowledge_links ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
|
17
|
+
|
|
18
|
+
-- The reconciliation on save reads and deletes by source and origin, and the backlink list reads by
|
|
19
|
+
-- target; both existing indexes stay useful, and this one keeps the delete off a table scan.
|
|
20
|
+
CREATE INDEX knowledge_links_origin_idx ON knowledge_links(source_node_id, origin);
|