@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,466 @@
|
|
|
1
|
+
import { Flow, FlowGraph, FlowRun, FlowVersion, } from "@anchrd/intel-contract";
|
|
2
|
+
const flowColumns = `id, title, description, owner_id, current_version_id,
|
|
3
|
+
published_version_id, created_at, updated_at, archived_at`;
|
|
4
|
+
const runColumnNames = [
|
|
5
|
+
"id",
|
|
6
|
+
"flow_id",
|
|
7
|
+
"version_id",
|
|
8
|
+
"status",
|
|
9
|
+
"current_node_id",
|
|
10
|
+
"input_json",
|
|
11
|
+
"output_json",
|
|
12
|
+
"error",
|
|
13
|
+
"initiated_by",
|
|
14
|
+
"created_at",
|
|
15
|
+
"updated_at",
|
|
16
|
+
"completed_at",
|
|
17
|
+
];
|
|
18
|
+
const runColumns = runColumnNames.join(", ");
|
|
19
|
+
// Joined queries need the columns qualified. Deriving them from the array keeps both forms in step;
|
|
20
|
+
// splitting the rendered string would break the moment it is reformatted.
|
|
21
|
+
const qualifiedRunColumns = (alias) => runColumnNames.map((column) => `${alias}.${column}`).join(", ");
|
|
22
|
+
function accessPredicate(roles = []) {
|
|
23
|
+
const role = roles.length ? `AND grant_row.role IN (${roles.map(() => "?").join(", ")})` : "";
|
|
24
|
+
return `(flow.owner_id = ? OR EXISTS (
|
|
25
|
+
SELECT 1 FROM resource_grants grant_row
|
|
26
|
+
WHERE grant_row.resource_type = 'flow' AND grant_row.resource_id = flow.id
|
|
27
|
+
AND (
|
|
28
|
+
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
29
|
+
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
30
|
+
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
31
|
+
)
|
|
32
|
+
${role}
|
|
33
|
+
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
34
|
+
))`;
|
|
35
|
+
}
|
|
36
|
+
function accessBindings(actor, now, roles = []) {
|
|
37
|
+
return [actor.id, actor.id, actor.email, ...roles, now];
|
|
38
|
+
}
|
|
39
|
+
function mapFlow(row) {
|
|
40
|
+
return Flow.parse({
|
|
41
|
+
id: row.id,
|
|
42
|
+
title: row.title,
|
|
43
|
+
description: row.description,
|
|
44
|
+
ownerId: row.owner_id,
|
|
45
|
+
currentVersionId: row.current_version_id,
|
|
46
|
+
publishedVersionId: row.published_version_id,
|
|
47
|
+
createdAt: row.created_at,
|
|
48
|
+
updatedAt: row.updated_at,
|
|
49
|
+
archivedAt: row.archived_at,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function mapVersion(row) {
|
|
53
|
+
return FlowVersion.parse({
|
|
54
|
+
id: row.id,
|
|
55
|
+
flowId: row.flow_id,
|
|
56
|
+
sequence: row.sequence,
|
|
57
|
+
graph: FlowGraph.parse(JSON.parse(row.graph_json)),
|
|
58
|
+
createdBy: row.created_by,
|
|
59
|
+
createdAt: row.created_at,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function mapRun(row) {
|
|
63
|
+
return FlowRun.parse({
|
|
64
|
+
id: row.id,
|
|
65
|
+
flowId: row.flow_id,
|
|
66
|
+
versionId: row.version_id,
|
|
67
|
+
status: row.status,
|
|
68
|
+
currentNodeId: row.current_node_id,
|
|
69
|
+
input: JSON.parse(row.input_json),
|
|
70
|
+
output: row.output_json === null ? null : JSON.parse(row.output_json),
|
|
71
|
+
error: row.error,
|
|
72
|
+
initiatedBy: row.initiated_by,
|
|
73
|
+
createdAt: row.created_at,
|
|
74
|
+
updatedAt: row.updated_at,
|
|
75
|
+
completedAt: row.completed_at,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function mapGrant(row) {
|
|
79
|
+
let principal;
|
|
80
|
+
switch (row.principal_type) {
|
|
81
|
+
case "user":
|
|
82
|
+
principal = { type: "user", id: row.principal_id };
|
|
83
|
+
break;
|
|
84
|
+
case "email":
|
|
85
|
+
principal = { type: "email", email: row.principal_id };
|
|
86
|
+
break;
|
|
87
|
+
case "organization":
|
|
88
|
+
principal = { type: "organization" };
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
throw new Error("Unsupported flow grant principal type");
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
id: row.id,
|
|
95
|
+
resourceId: row.resource_id,
|
|
96
|
+
principal,
|
|
97
|
+
role: row.role,
|
|
98
|
+
expiresAt: row.expires_at,
|
|
99
|
+
createdBy: row.created_by,
|
|
100
|
+
createdAt: row.created_at,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function createFlowRepository(deps) {
|
|
104
|
+
const visible = accessPredicate();
|
|
105
|
+
return {
|
|
106
|
+
async listVisible(actor) {
|
|
107
|
+
const result = await deps.db
|
|
108
|
+
.prepare(`SELECT ${flowColumns} FROM flows flow
|
|
109
|
+
WHERE ${visible} AND flow.archived_at IS NULL
|
|
110
|
+
ORDER BY lower(flow.title), flow.id`)
|
|
111
|
+
.bind(...accessBindings(actor, deps.now().toISOString()))
|
|
112
|
+
.all();
|
|
113
|
+
return (result.results ?? []).map(mapFlow);
|
|
114
|
+
},
|
|
115
|
+
async getVisible(actor, flowId) {
|
|
116
|
+
const row = await deps.db
|
|
117
|
+
.prepare(`SELECT ${flowColumns} FROM flows flow WHERE flow.id = ? AND ${visible}`)
|
|
118
|
+
.bind(flowId, ...accessBindings(actor, deps.now().toISOString()))
|
|
119
|
+
.first();
|
|
120
|
+
return row ? mapFlow(row) : null;
|
|
121
|
+
},
|
|
122
|
+
async canEdit(actor, flowId) {
|
|
123
|
+
const row = await deps.db
|
|
124
|
+
.prepare(`SELECT 1 AS allowed FROM flows flow
|
|
125
|
+
WHERE flow.id = ? AND ${accessPredicate(["editor", "manager"])}`)
|
|
126
|
+
.bind(flowId, ...accessBindings(actor, deps.now().toISOString(), ["editor", "manager"]))
|
|
127
|
+
.first();
|
|
128
|
+
return row?.allowed === 1;
|
|
129
|
+
},
|
|
130
|
+
async canManage(actor, flowId) {
|
|
131
|
+
const row = await deps.db
|
|
132
|
+
.prepare(`SELECT 1 AS allowed FROM flows flow
|
|
133
|
+
WHERE flow.id = ? AND ${accessPredicate(["manager"])}`)
|
|
134
|
+
.bind(flowId, ...accessBindings(actor, deps.now().toISOString(), ["manager"]))
|
|
135
|
+
.first();
|
|
136
|
+
return row?.allowed === 1;
|
|
137
|
+
},
|
|
138
|
+
async findIdempotent(actorId, operation, key) {
|
|
139
|
+
const row = await deps.db
|
|
140
|
+
.prepare(`SELECT resource_id FROM idempotency_keys
|
|
141
|
+
WHERE actor_id = ? AND operation = ? AND idempotency_key = ?`)
|
|
142
|
+
.bind(actorId, operation, key)
|
|
143
|
+
.first();
|
|
144
|
+
return row?.resource_id ?? null;
|
|
145
|
+
},
|
|
146
|
+
async findIdempotentRevocation(actorId, key) {
|
|
147
|
+
const row = await deps.db
|
|
148
|
+
.prepare(`SELECT resource_id FROM idempotency_keys
|
|
149
|
+
WHERE actor_id = ? AND operation = 'flows.revoke' AND idempotency_key = ?`)
|
|
150
|
+
.bind(actorId, key)
|
|
151
|
+
.first();
|
|
152
|
+
if (!row)
|
|
153
|
+
return null;
|
|
154
|
+
return row.resource_id.startsWith("1:");
|
|
155
|
+
},
|
|
156
|
+
async insertFlow(input) {
|
|
157
|
+
const flow = input.flow;
|
|
158
|
+
try {
|
|
159
|
+
await deps.db.batch([
|
|
160
|
+
deps.db
|
|
161
|
+
.prepare(`INSERT INTO flows (
|
|
162
|
+
id, title, description, owner_id, current_version_id, published_version_id,
|
|
163
|
+
created_at, updated_at, archived_at
|
|
164
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
165
|
+
.bind(flow.id, flow.title, flow.description, flow.ownerId, flow.currentVersionId, flow.publishedVersionId, flow.createdAt, flow.updatedAt, flow.archivedAt),
|
|
166
|
+
deps.db
|
|
167
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
168
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
169
|
+
) VALUES (?, 'flows.create', ?, ?, ?)`)
|
|
170
|
+
.bind(input.actorId, input.idempotencyKey, flow.id, flow.createdAt),
|
|
171
|
+
deps.db
|
|
172
|
+
.prepare(`INSERT INTO audit_events (
|
|
173
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
174
|
+
) VALUES (?, ?, 'flows.create', 'flow', ?, '{}', ?)`)
|
|
175
|
+
.bind(input.auditId, input.actorId, flow.id, flow.createdAt),
|
|
176
|
+
]);
|
|
177
|
+
return flow;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const replayedId = await this.findIdempotent(input.actorId, "flows.create", input.idempotencyKey);
|
|
181
|
+
const replayed = replayedId
|
|
182
|
+
? await deps.db
|
|
183
|
+
.prepare(`SELECT ${flowColumns} FROM flows WHERE id = ?`)
|
|
184
|
+
.bind(replayedId)
|
|
185
|
+
.first()
|
|
186
|
+
: null;
|
|
187
|
+
if (replayed)
|
|
188
|
+
return mapFlow(replayed);
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async getVersion(versionId) {
|
|
193
|
+
const row = await deps.db
|
|
194
|
+
.prepare(`SELECT id, flow_id, sequence, graph_json, created_by, created_at
|
|
195
|
+
FROM flow_versions WHERE id = ?`)
|
|
196
|
+
.bind(versionId)
|
|
197
|
+
.first();
|
|
198
|
+
return row ? mapVersion(row) : null;
|
|
199
|
+
},
|
|
200
|
+
async insertVersion(input) {
|
|
201
|
+
const version = input.version;
|
|
202
|
+
try {
|
|
203
|
+
await deps.db.batch([
|
|
204
|
+
deps.db
|
|
205
|
+
.prepare(`INSERT INTO flow_versions (
|
|
206
|
+
id, flow_id, sequence, graph_json, created_by, created_at
|
|
207
|
+
) SELECT ?, ?, ?, ?, ?, ?
|
|
208
|
+
WHERE EXISTS (
|
|
209
|
+
SELECT 1 FROM flows WHERE id = ? AND current_version_id IS ?
|
|
210
|
+
)`)
|
|
211
|
+
.bind(version.id, version.flowId, version.sequence, JSON.stringify(version.graph), version.createdBy, version.createdAt, version.flowId, input.baseVersionId),
|
|
212
|
+
deps.db
|
|
213
|
+
.prepare(`UPDATE flows SET current_version_id = ?, updated_at = ?
|
|
214
|
+
WHERE id = ? AND current_version_id IS ?
|
|
215
|
+
AND EXISTS (SELECT 1 FROM flow_versions WHERE id = ?)`)
|
|
216
|
+
.bind(version.id, version.createdAt, version.flowId, input.baseVersionId, version.id),
|
|
217
|
+
deps.db
|
|
218
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
219
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
220
|
+
) SELECT ?, 'flows.save', ?, ?, ?
|
|
221
|
+
WHERE EXISTS (SELECT 1 FROM flow_versions WHERE id = ?)`)
|
|
222
|
+
.bind(input.actorId, input.idempotencyKey, version.id, version.createdAt, version.id),
|
|
223
|
+
deps.db
|
|
224
|
+
.prepare(`INSERT INTO audit_events (
|
|
225
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
226
|
+
) SELECT ?, ?, 'flows.save', 'flow', ?, ?, ?
|
|
227
|
+
WHERE EXISTS (SELECT 1 FROM flow_versions WHERE id = ?)`)
|
|
228
|
+
.bind(input.auditId, input.actorId, version.flowId, JSON.stringify({ versionId: version.id, sequence: version.sequence }), version.createdAt, version.id),
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.save", input.idempotencyKey);
|
|
233
|
+
if (!replayed)
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
const stored = await deps.db
|
|
237
|
+
.prepare("SELECT 1 AS saved FROM flow_versions WHERE id = ?")
|
|
238
|
+
.bind(version.id)
|
|
239
|
+
.first();
|
|
240
|
+
return stored ? "saved" : "conflict";
|
|
241
|
+
},
|
|
242
|
+
async publish(input) {
|
|
243
|
+
try {
|
|
244
|
+
await deps.db.batch([
|
|
245
|
+
deps.db
|
|
246
|
+
.prepare(`UPDATE flows SET published_version_id = ?, updated_at = ?
|
|
247
|
+
WHERE id = ? AND EXISTS (
|
|
248
|
+
SELECT 1 FROM flow_versions WHERE id = ? AND flow_id = ?
|
|
249
|
+
)`)
|
|
250
|
+
.bind(input.versionId, input.occurredAt, input.flowId, input.versionId, input.flowId),
|
|
251
|
+
deps.db
|
|
252
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
253
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
254
|
+
) SELECT ?, 'flows.publish', ?, ?, ?
|
|
255
|
+
WHERE EXISTS (
|
|
256
|
+
SELECT 1 FROM flows WHERE id = ? AND published_version_id = ?
|
|
257
|
+
)`)
|
|
258
|
+
.bind(input.actorId, input.idempotencyKey, input.versionId, input.occurredAt, input.flowId, input.versionId),
|
|
259
|
+
deps.db
|
|
260
|
+
.prepare(`INSERT INTO audit_events (
|
|
261
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
262
|
+
) SELECT ?, ?, 'flows.publish', 'flow', ?, ?, ?
|
|
263
|
+
WHERE EXISTS (
|
|
264
|
+
SELECT 1 FROM idempotency_keys
|
|
265
|
+
WHERE actor_id = ? AND operation = 'flows.publish' AND idempotency_key = ?
|
|
266
|
+
)`)
|
|
267
|
+
.bind(input.auditId, input.actorId, input.flowId, JSON.stringify({ versionId: input.versionId }), input.occurredAt, input.actorId, input.idempotencyKey),
|
|
268
|
+
]);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.publish", input.idempotencyKey);
|
|
272
|
+
if (!replayed)
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
return ((await this.findIdempotent(input.actorId, "flows.publish", input.idempotencyKey)) !== null);
|
|
276
|
+
},
|
|
277
|
+
async listGrants(flowId) {
|
|
278
|
+
const result = await deps.db
|
|
279
|
+
.prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
|
|
280
|
+
created_by, created_at FROM resource_grants
|
|
281
|
+
WHERE resource_type = 'flow' AND resource_id = ?
|
|
282
|
+
ORDER BY created_at, id`)
|
|
283
|
+
.bind(flowId)
|
|
284
|
+
.all();
|
|
285
|
+
return (result.results ?? []).map(mapGrant);
|
|
286
|
+
},
|
|
287
|
+
async setGrant(input) {
|
|
288
|
+
const grant = input.grant;
|
|
289
|
+
const principalType = grant.principal.type;
|
|
290
|
+
const principalId = grant.principal.type === "user"
|
|
291
|
+
? grant.principal.id
|
|
292
|
+
: grant.principal.type === "email"
|
|
293
|
+
? grant.principal.email.toLowerCase()
|
|
294
|
+
: "*";
|
|
295
|
+
try {
|
|
296
|
+
await deps.db.batch([
|
|
297
|
+
deps.db
|
|
298
|
+
.prepare(`INSERT INTO resource_grants (
|
|
299
|
+
id, resource_type, resource_id, principal_type, principal_id, role,
|
|
300
|
+
expires_at, created_by, created_at
|
|
301
|
+
) VALUES (?, 'flow', ?, ?, ?, ?, ?, ?, ?)
|
|
302
|
+
ON CONFLICT (resource_type, resource_id, principal_type, principal_id)
|
|
303
|
+
DO UPDATE SET role = excluded.role, expires_at = excluded.expires_at`)
|
|
304
|
+
.bind(grant.id, grant.resourceId, principalType, principalId, grant.role, grant.expiresAt, grant.createdBy, grant.createdAt),
|
|
305
|
+
deps.db
|
|
306
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
307
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
308
|
+
) SELECT ?, 'flows.share', ?, id, ? FROM resource_grants
|
|
309
|
+
WHERE resource_type = 'flow' AND resource_id = ?
|
|
310
|
+
AND principal_type = ? AND principal_id = ?`)
|
|
311
|
+
.bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId),
|
|
312
|
+
deps.db
|
|
313
|
+
.prepare(`INSERT INTO audit_events (
|
|
314
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
315
|
+
) VALUES (?, ?, 'flows.share', 'flow', ?, ?, ?)`)
|
|
316
|
+
.bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType, role: grant.role }), grant.createdAt),
|
|
317
|
+
]);
|
|
318
|
+
const stored = await deps.db
|
|
319
|
+
.prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
|
|
320
|
+
created_by, created_at FROM resource_grants
|
|
321
|
+
WHERE resource_type = 'flow' AND resource_id = ?
|
|
322
|
+
AND principal_type = ? AND principal_id = ?`)
|
|
323
|
+
.bind(grant.resourceId, principalType, principalId)
|
|
324
|
+
.first();
|
|
325
|
+
if (!stored)
|
|
326
|
+
throw new Error("Flow grant disappeared after upsert");
|
|
327
|
+
return mapGrant(stored);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.share", input.idempotencyKey);
|
|
331
|
+
const row = replayed
|
|
332
|
+
? await deps.db
|
|
333
|
+
.prepare(`SELECT id, resource_id, principal_type, principal_id, role, expires_at,
|
|
334
|
+
created_by, created_at FROM resource_grants WHERE id = ?`)
|
|
335
|
+
.bind(replayed)
|
|
336
|
+
.first()
|
|
337
|
+
: null;
|
|
338
|
+
if (row)
|
|
339
|
+
return mapGrant(row);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async revokeGrant(input) {
|
|
344
|
+
try {
|
|
345
|
+
await deps.db.batch([
|
|
346
|
+
deps.db
|
|
347
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
348
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
349
|
+
) SELECT ?, 'flows.revoke', ?,
|
|
350
|
+
(CASE WHEN EXISTS (
|
|
351
|
+
SELECT 1 FROM resource_grants
|
|
352
|
+
WHERE id = ? AND resource_type = 'flow' AND resource_id = ?
|
|
353
|
+
) THEN '1:' ELSE '0:' END) || ?, ?`)
|
|
354
|
+
.bind(input.actorId, input.idempotencyKey, input.grantId, input.flowId, input.grantId, input.occurredAt),
|
|
355
|
+
deps.db
|
|
356
|
+
.prepare(`DELETE FROM resource_grants
|
|
357
|
+
WHERE id = ? AND resource_type = 'flow' AND resource_id = ?`)
|
|
358
|
+
.bind(input.grantId, input.flowId),
|
|
359
|
+
deps.db
|
|
360
|
+
.prepare(`INSERT INTO audit_events (
|
|
361
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
362
|
+
) SELECT ?, ?, 'flows.revoke', 'flow', ?,
|
|
363
|
+
json_object(
|
|
364
|
+
'grantId', ?,
|
|
365
|
+
'revoked', json(CASE WHEN substr(resource_id, 1, 2) = '1:' THEN 'true' ELSE 'false' END)
|
|
366
|
+
), ?
|
|
367
|
+
FROM idempotency_keys
|
|
368
|
+
WHERE actor_id = ? AND operation = 'flows.revoke' AND idempotency_key = ?`)
|
|
369
|
+
.bind(input.auditId, input.actorId, input.flowId, input.grantId, input.occurredAt, input.actorId, input.idempotencyKey),
|
|
370
|
+
]);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
const replayed = await this.findIdempotentRevocation(input.actorId, input.idempotencyKey);
|
|
374
|
+
if (replayed === null)
|
|
375
|
+
throw error;
|
|
376
|
+
return replayed;
|
|
377
|
+
}
|
|
378
|
+
return (await this.findIdempotentRevocation(input.actorId, input.idempotencyKey)) ?? false;
|
|
379
|
+
},
|
|
380
|
+
async insertRun(input) {
|
|
381
|
+
const run = input.run;
|
|
382
|
+
try {
|
|
383
|
+
await deps.db.batch([
|
|
384
|
+
deps.db
|
|
385
|
+
.prepare(`INSERT INTO flow_runs (
|
|
386
|
+
id, flow_id, version_id, status, current_node_id, input_json, output_json,
|
|
387
|
+
error, initiated_by, created_at, updated_at, completed_at
|
|
388
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
389
|
+
.bind(run.id, run.flowId, run.versionId, run.status, run.currentNodeId, JSON.stringify(run.input), null, run.error, run.initiatedBy, run.createdAt, run.updatedAt, run.completedAt),
|
|
390
|
+
deps.db
|
|
391
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
392
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
393
|
+
) VALUES (?, 'flows.run', ?, ?, ?)`)
|
|
394
|
+
.bind(input.actorId, input.idempotencyKey, run.id, run.createdAt),
|
|
395
|
+
deps.db
|
|
396
|
+
.prepare(`INSERT INTO audit_events (
|
|
397
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
398
|
+
) VALUES (?, ?, 'flows.run', 'flow-run', ?, ?, ?)`)
|
|
399
|
+
.bind(input.auditId, input.actorId, run.id, JSON.stringify({ flowId: run.flowId, versionId: run.versionId }), run.createdAt),
|
|
400
|
+
]);
|
|
401
|
+
return run;
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const replayedId = await this.findIdempotent(input.actorId, "flows.run", input.idempotencyKey);
|
|
405
|
+
const replayed = replayedId
|
|
406
|
+
? await deps.db
|
|
407
|
+
.prepare(`SELECT ${runColumns} FROM flow_runs WHERE id = ?`)
|
|
408
|
+
.bind(replayedId)
|
|
409
|
+
.first()
|
|
410
|
+
: null;
|
|
411
|
+
if (replayed)
|
|
412
|
+
return mapRun(replayed);
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
async getRunVisible(actor, runId) {
|
|
417
|
+
const row = await deps.db
|
|
418
|
+
.prepare(`SELECT ${qualifiedRunColumns("run")} FROM flow_runs run
|
|
419
|
+
JOIN flows flow ON flow.id = run.flow_id
|
|
420
|
+
WHERE run.id = ? AND ${visible}`)
|
|
421
|
+
.bind(runId, ...accessBindings(actor, deps.now().toISOString()))
|
|
422
|
+
.first();
|
|
423
|
+
return row ? mapRun(row) : null;
|
|
424
|
+
},
|
|
425
|
+
async advanceRun(input) {
|
|
426
|
+
const run = input.run;
|
|
427
|
+
try {
|
|
428
|
+
await deps.db.batch([
|
|
429
|
+
deps.db
|
|
430
|
+
.prepare(`UPDATE flow_runs SET
|
|
431
|
+
status = ?, current_node_id = ?, output_json = ?, error = ?,
|
|
432
|
+
updated_at = ?, completed_at = ?, transition_id = ?
|
|
433
|
+
WHERE id = ? AND current_node_id = ? AND status IN ('running', 'waiting')`)
|
|
434
|
+
.bind(run.status, run.currentNodeId, run.output === null ? null : JSON.stringify(run.output), run.error, run.updatedAt, run.completedAt, input.stepId, run.id, input.expectedNodeId),
|
|
435
|
+
deps.db
|
|
436
|
+
.prepare(`INSERT INTO flow_run_steps (
|
|
437
|
+
id, run_id, node_id, outcome, branch, output_json, error, completed_by, completed_at
|
|
438
|
+
) SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
439
|
+
WHERE EXISTS (
|
|
440
|
+
SELECT 1 FROM flow_runs WHERE id = ? AND transition_id = ?
|
|
441
|
+
)`)
|
|
442
|
+
.bind(input.stepId, run.id, input.expectedNodeId, input.outcome, input.branch, JSON.stringify(input.output), input.error, input.actorId, run.updatedAt, run.id, input.stepId),
|
|
443
|
+
deps.db
|
|
444
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
445
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
446
|
+
) SELECT ?, 'flows.complete', ?, ?, ?
|
|
447
|
+
WHERE EXISTS (SELECT 1 FROM flow_run_steps WHERE id = ?)`)
|
|
448
|
+
.bind(input.actorId, input.idempotencyKey, run.id, run.updatedAt, input.stepId),
|
|
449
|
+
deps.db
|
|
450
|
+
.prepare(`INSERT INTO audit_events (
|
|
451
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
452
|
+
) SELECT ?, ?, 'flows.complete', 'flow-run', ?, ?, ?
|
|
453
|
+
WHERE EXISTS (SELECT 1 FROM flow_run_steps WHERE id = ?)`)
|
|
454
|
+
.bind(input.auditId, input.actorId, run.id, JSON.stringify({ nodeId: input.expectedNodeId, outcome: input.outcome }), run.updatedAt, input.stepId),
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.complete", input.idempotencyKey);
|
|
459
|
+
if (!replayed)
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.complete", input.idempotencyKey);
|
|
463
|
+
return replayed === run.id ? "advanced" : "conflict";
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function mapTarget(row) {
|
|
2
|
+
return {
|
|
3
|
+
nodeId: row.node_id,
|
|
4
|
+
versionId: row.version_id,
|
|
5
|
+
title: row.title,
|
|
6
|
+
contentKey: row.content_key,
|
|
7
|
+
mediaType: row.media_type,
|
|
8
|
+
kind: row.kind,
|
|
9
|
+
updatedAt: row.updated_at,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function createKnowledgeIndexRepository(db) {
|
|
13
|
+
return {
|
|
14
|
+
async getTarget(versionId) {
|
|
15
|
+
const row = await db
|
|
16
|
+
.prepare(`SELECT n.id AS node_id, v.id AS version_id, n.title, v.content_key,
|
|
17
|
+
v.media_type, n.kind, n.updated_at
|
|
18
|
+
FROM knowledge_versions v
|
|
19
|
+
JOIN knowledge_nodes n ON n.id = v.node_id
|
|
20
|
+
WHERE v.id = ? AND n.current_version_id = v.id AND n.archived_at IS NULL`)
|
|
21
|
+
.bind(versionId)
|
|
22
|
+
.first();
|
|
23
|
+
return row ? mapTarget(row) : null;
|
|
24
|
+
},
|
|
25
|
+
async replace(target, content) {
|
|
26
|
+
await db.batch([
|
|
27
|
+
db
|
|
28
|
+
.prepare(`DELETE FROM knowledge_fts WHERE node_id = ? AND EXISTS (
|
|
29
|
+
SELECT 1 FROM knowledge_nodes
|
|
30
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
31
|
+
)`)
|
|
32
|
+
.bind(target.nodeId, target.nodeId, target.versionId),
|
|
33
|
+
db
|
|
34
|
+
.prepare(`INSERT INTO knowledge_fts (version_id, node_id, title, content)
|
|
35
|
+
SELECT ?, ?, ?, ? WHERE EXISTS (
|
|
36
|
+
SELECT 1 FROM knowledge_nodes
|
|
37
|
+
WHERE id = ? AND current_version_id = ? AND archived_at IS NULL
|
|
38
|
+
)`)
|
|
39
|
+
.bind(target.versionId, target.nodeId, target.title, content, target.nodeId, target.versionId),
|
|
40
|
+
]);
|
|
41
|
+
},
|
|
42
|
+
async markIndexed(versionId, occurredAt) {
|
|
43
|
+
await db
|
|
44
|
+
.prepare(`UPDATE knowledge_index_state
|
|
45
|
+
SET status = 'indexed', attempt_count = attempt_count + 1,
|
|
46
|
+
last_error = NULL, updated_at = ?
|
|
47
|
+
WHERE version_id = ? AND EXISTS (
|
|
48
|
+
SELECT 1 FROM knowledge_versions version
|
|
49
|
+
JOIN knowledge_nodes node ON node.id = version.node_id
|
|
50
|
+
WHERE version.id = ? AND node.current_version_id = version.id
|
|
51
|
+
AND node.archived_at IS NULL
|
|
52
|
+
)`)
|
|
53
|
+
.bind(occurredAt, versionId, versionId)
|
|
54
|
+
.run();
|
|
55
|
+
},
|
|
56
|
+
async markError(versionId, message, occurredAt) {
|
|
57
|
+
await db
|
|
58
|
+
.prepare(`UPDATE knowledge_index_state
|
|
59
|
+
SET status = 'error', attempt_count = attempt_count + 1,
|
|
60
|
+
last_error = ?, updated_at = ? WHERE version_id = ? AND EXISTS (
|
|
61
|
+
SELECT 1 FROM knowledge_versions version
|
|
62
|
+
JOIN knowledge_nodes node ON node.id = version.node_id
|
|
63
|
+
WHERE version.id = ? AND node.current_version_id = version.id
|
|
64
|
+
AND node.archived_at IS NULL
|
|
65
|
+
)`)
|
|
66
|
+
.bind(message.slice(0, 500), occurredAt, versionId, versionId)
|
|
67
|
+
.run();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createOAuthClientStore(db) {
|
|
2
|
+
return {
|
|
3
|
+
async get(issuer, redirectUri) {
|
|
4
|
+
const row = await db
|
|
5
|
+
.prepare("SELECT client_id FROM oauth_clients WHERE issuer = ? AND redirect_uri = ?")
|
|
6
|
+
.bind(issuer, redirectUri)
|
|
7
|
+
.first();
|
|
8
|
+
return row?.client_id ?? null;
|
|
9
|
+
},
|
|
10
|
+
async put(input) {
|
|
11
|
+
await db
|
|
12
|
+
.prepare(`INSERT OR IGNORE INTO oauth_clients (
|
|
13
|
+
issuer, redirect_uri, client_id, created_at
|
|
14
|
+
) VALUES (?, ?, ?, ?)`)
|
|
15
|
+
.bind(input.issuer, input.redirectUri, input.clientId, input.createdAt)
|
|
16
|
+
.run();
|
|
17
|
+
const stored = await db
|
|
18
|
+
.prepare("SELECT client_id FROM oauth_clients WHERE issuer = ? AND redirect_uri = ?")
|
|
19
|
+
.bind(input.issuer, input.redirectUri)
|
|
20
|
+
.first();
|
|
21
|
+
if (!stored)
|
|
22
|
+
throw new Error("OAuth client registration was not stored");
|
|
23
|
+
return stored.client_id;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|