@anchrd/intel-api 0.2.0 → 0.2.1
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 +11 -9
- package/dist/build/build.js +17 -4
- package/dist/indexing/indexing.js +4 -1
- package/dist/intel/intel.js +11 -3
- package/package.json +1 -1
- package/dist/adapters/db/db-tools.d.ts +0 -6
- package/dist/adapters/db/db-tools.js +0 -200
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ customer CLI.
|
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
|
-
- A [Gate](https://www.npmjs.com/package/@anchrd/gate-sdk) instance — Gate owns identity
|
|
20
|
-
|
|
19
|
+
- A [Gate](https://www.npmjs.com/package/@anchrd/gate-sdk) instance — Gate owns identity and
|
|
20
|
+
capabilities, and is the only required Anchrd dependency
|
|
21
21
|
- A Cloudflare account with Workers, D1, R2, Queues and Workflows (Vectorize and Workers AI are
|
|
22
22
|
optional and only add semantic search and attachment conversion)
|
|
23
23
|
- Node 22 or newer
|
|
@@ -71,17 +71,19 @@ uses PKCE and dynamic public-client registration; browser access tokens stay ins
|
|
|
71
71
|
`HttpOnly` cookie and are never exposed to the UI bundle or D1. Copy
|
|
72
72
|
`node_modules/@anchrd/intel-api/examples/dev.vars.example` to `.dev.vars` for a local customer Worker.
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
registers with Gate or Cloudflare Access, and completes a separate PKCE flow.
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
The portal endpoint exposes RFC 9728 metadata. The Tools UI follows that metadata, dynamically
|
|
75
|
+
registers with Gate or Cloudflare Access, and completes a separate PKCE flow. The only tool secret
|
|
76
|
+
Intel stores is the resulting per-user access token for its own portal endpoint, sealed with a key
|
|
77
|
+
derived from `INTEL_SESSION_SECRET` and kept in the `portal_tokens` table of your D1; provider
|
|
78
|
+
credentials stay with the portal and never reach Intel. The Intel audience token is never forwarded
|
|
79
|
+
to another OAuth resource.
|
|
78
80
|
|
|
79
81
|
The reference Wrangler deployment binds `DB`, `CONTENT`, `INDEXING`, `AI`, `SEARCH`, `FLOWS`, and
|
|
80
82
|
`ASSETS`. Create `SEARCH` as a 1024-dimension cosine Vectorize index for the default multilingual
|
|
81
83
|
Workers AI `@cf/baai/bge-m3` embedding adapter. `FLOWS` targets the exported
|
|
82
|
-
`IntelFlowWorkflow` class.
|
|
83
|
-
|
|
84
|
-
browser, Gate, or provider credentials.
|
|
84
|
+
`IntelFlowWorkflow` class. The portal speaks standard Streamable HTTP MCP; Intel caches schemas and
|
|
85
|
+
unseals the caller's portal token from `portal_tokens` just in time, refreshing it shortly before
|
|
86
|
+
use. Durable Flow state contains no browser, Gate, or provider credentials.
|
|
85
87
|
|
|
86
88
|
## Related packages
|
|
87
89
|
|
package/dist/build/build.js
CHANGED
|
@@ -12,6 +12,12 @@ const IntelConfig = z.strictObject({
|
|
|
12
12
|
const Catalog = z.record(z.string(), z.string());
|
|
13
13
|
const defaultTheme = `/* Generated by \`intel build\`. Intel defaults are active. */\n`;
|
|
14
14
|
const defaultLogo = `export const customLogoUrl: string | null = null;\n`;
|
|
15
|
+
// Customer paths from intel.json are written into generated CSS and TypeScript comments. A path
|
|
16
|
+
// containing `*/` or a newline would close the comment early, leaving broken output or — in
|
|
17
|
+
// custom-logo.ts — customer-controlled text outside the comment and inside the UI bundle.
|
|
18
|
+
function asComment(path) {
|
|
19
|
+
return path.replaceAll("*/", "*\\/").replace(/[\r\n]+/g, " ");
|
|
20
|
+
}
|
|
15
21
|
function validationMessage(error) {
|
|
16
22
|
return error.issues
|
|
17
23
|
.map((issue) => `${issue.path.join(".") || "intel.json"}: ${issue.message}`)
|
|
@@ -64,7 +70,14 @@ export function createBuild(deps) {
|
|
|
64
70
|
const config = parsed.data.ui;
|
|
65
71
|
const englishPath = `${location.dir}/src/i18n/en.json`;
|
|
66
72
|
const englishRaw = await readRequired(englishPath, "built-in English catalog");
|
|
67
|
-
|
|
73
|
+
let englishJson;
|
|
74
|
+
try {
|
|
75
|
+
englishJson = JSON.parse(englishRaw);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error(`${englishPath} is not valid JSON`);
|
|
79
|
+
}
|
|
80
|
+
const englishParsed = Catalog.safeParse(englishJson);
|
|
68
81
|
if (!englishParsed.success)
|
|
69
82
|
throw new Error("Intel's built-in English catalog is invalid");
|
|
70
83
|
const english = englishParsed.data;
|
|
@@ -91,15 +104,15 @@ export function createBuild(deps) {
|
|
|
91
104
|
throw new Error(`ui.defaultLanguage ${JSON.stringify(config.defaultLanguage)} is not listed in ui.languages`);
|
|
92
105
|
}
|
|
93
106
|
const theme = config.theme
|
|
94
|
-
? `/* Generated by \`intel build\` from ${config.theme}. */\n${await readRequired(config.theme, "ui.theme")}\n`
|
|
107
|
+
? `/* Generated by \`intel build\` from ${asComment(config.theme)}. */\n${await readRequired(config.theme, "ui.theme")}\n`
|
|
95
108
|
: defaultTheme;
|
|
96
109
|
const logoSvg = config.logo ? await readSvg(config.logo, "ui.logo") : null;
|
|
97
110
|
const faviconPath = config.favicon ?? config.logo;
|
|
98
111
|
const faviconSvg = faviconPath
|
|
99
112
|
? await readSvg(faviconPath, config.favicon ? "ui.favicon" : "ui.logo")
|
|
100
113
|
: await readRequired(`${location.dir}/src/branding/favicon.default.svg`, "default favicon");
|
|
101
|
-
const logoModule = logoSvg
|
|
102
|
-
? `/* Generated by \`intel build\` from ${config.logo}. */\nexport const customLogoUrl = ${JSON.stringify(asSvgDataUrl(logoSvg))};\n`
|
|
114
|
+
const logoModule = config.logo && logoSvg
|
|
115
|
+
? `/* Generated by \`intel build\` from ${asComment(config.logo)}. */\nexport const customLogoUrl = ${JSON.stringify(asSvgDataUrl(logoSvg))};\n`
|
|
103
116
|
: defaultLogo;
|
|
104
117
|
const languageBundle = `${JSON.stringify({ defaultLanguage: config.defaultLanguage, catalogs }, null, 2)}\n`;
|
|
105
118
|
await deps.writeTextFile(`${location.dir}/src/theme/custom.css`, theme);
|
|
@@ -40,7 +40,10 @@ export function createIndexing(deps) {
|
|
|
40
40
|
await deps.repository.markIndexed(versionId, deps.now().toISOString());
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
|
-
|
|
43
|
+
// Only Intel's own permanent messages are stored. A converter or fetch failure can carry
|
|
44
|
+
// content excerpts or URLs with query parameters, and this column is read by operators and
|
|
45
|
+
// shown in the UI, so an arbitrary upstream message must not be persisted here.
|
|
46
|
+
await deps.repository.markError(versionId, error instanceof PermanentIndexingError ? error.message.slice(0, 500) : "indexing_failed", deps.now().toISOString());
|
|
44
47
|
throw error;
|
|
45
48
|
}
|
|
46
49
|
},
|
package/dist/intel/intel.js
CHANGED
|
@@ -7,7 +7,11 @@ import { problemDetails } from "../shared/problem-details/problem-details.js";
|
|
|
7
7
|
export function createIntel(deps) {
|
|
8
8
|
const baseUrl = deps.baseUrl.replace(/\/+$/, "");
|
|
9
9
|
const resource = `${baseUrl}/mcp`;
|
|
10
|
-
|
|
10
|
+
// RFC 9728 inserts the well-known segment between host and resource path: the document for a
|
|
11
|
+
// resource ending in /mcp belongs at /.well-known/oauth-protected-resource/mcp. A client that
|
|
12
|
+
// constructs that URL itself — Intel's own portal client in adapters/openid is one — finds
|
|
13
|
+
// nothing under the bare root path, and only a client that follows WWW-Authenticate gets through.
|
|
14
|
+
const resourceMetadataUrl = `${baseUrl}/.well-known/oauth-protected-resource/mcp`;
|
|
11
15
|
const app = new Hono();
|
|
12
16
|
app.onError((error, context) => {
|
|
13
17
|
if (error instanceof IntelError) {
|
|
@@ -16,11 +20,15 @@ export function createIntel(deps) {
|
|
|
16
20
|
return context.json(problemDetails(500, "internal_error", "Internal server error"), 500);
|
|
17
21
|
});
|
|
18
22
|
app.get("/health", (context) => context.json({ status: "ok" }));
|
|
19
|
-
|
|
23
|
+
const resourceMetadata = {
|
|
20
24
|
resource,
|
|
21
25
|
authorization_servers: [deps.gateUrl.replace(/\/+$/, "")],
|
|
22
26
|
bearer_methods_supported: ["header"],
|
|
23
|
-
}
|
|
27
|
+
};
|
|
28
|
+
app.get("/.well-known/oauth-protected-resource/mcp", (context) => context.json(resourceMetadata));
|
|
29
|
+
// The root path stays as a compatibility alias: 0.2.0 published the document only here, and a
|
|
30
|
+
// client that stored that URL from an earlier WWW-Authenticate header keeps working.
|
|
31
|
+
app.get("/.well-known/oauth-protected-resource", (context) => context.json(resourceMetadata));
|
|
24
32
|
const browserAuth = deps.auth;
|
|
25
33
|
if (browserAuth) {
|
|
26
34
|
app.get("/auth/login", async (context) => await browserAuth.login(new URL(context.req.url)));
|
package/package.json
CHANGED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
import { ToolAnnotations, ToolCapability, ToolSource, } from "@anchrd/intel-contract";
|
|
2
|
-
const sourceColumns = `source.id, source.name, source.url, source.connection_handle,
|
|
3
|
-
source.enabled, source.created_by, source.created_at, source.updated_at,
|
|
4
|
-
source.last_discovered_at`;
|
|
5
|
-
const catalogColumns = `${sourceColumns}, tool.name AS tool_name, tool.title AS tool_title,
|
|
6
|
-
tool.description AS tool_description, tool.input_schema_json, tool.output_schema_json,
|
|
7
|
-
tool.annotations_json, tool.fingerprint, tool.discovered_at`;
|
|
8
|
-
// Predicate and bindings are produced together: the placeholder count depends on the roles filter,
|
|
9
|
-
// so returning them separately would let a caller shift every binding without the compiler noticing.
|
|
10
|
-
// An empty roles array means "any role", because `IN ()` is invalid SQL.
|
|
11
|
-
function access(actor, now, roles = []) {
|
|
12
|
-
const role = roles.length ? `AND grant_row.role IN (${roles.map(() => "?").join(", ")})` : "";
|
|
13
|
-
return {
|
|
14
|
-
sql: `(source.created_by = ? OR EXISTS (
|
|
15
|
-
SELECT 1 FROM resource_grants grant_row
|
|
16
|
-
WHERE grant_row.resource_type = 'tool-source' AND grant_row.resource_id = source.id
|
|
17
|
-
AND (
|
|
18
|
-
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
19
|
-
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
20
|
-
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
21
|
-
)
|
|
22
|
-
${role}
|
|
23
|
-
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
24
|
-
))`,
|
|
25
|
-
bindings: [actor.id, actor.id, actor.email, ...roles, now],
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function mapSource(row) {
|
|
29
|
-
return ToolSource.parse({
|
|
30
|
-
id: row.id,
|
|
31
|
-
name: row.name,
|
|
32
|
-
url: row.url,
|
|
33
|
-
connectionHandle: row.connection_handle,
|
|
34
|
-
enabled: row.enabled === 1,
|
|
35
|
-
createdBy: row.created_by,
|
|
36
|
-
createdAt: row.created_at,
|
|
37
|
-
updatedAt: row.updated_at,
|
|
38
|
-
lastDiscoveredAt: row.last_discovered_at,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function mapCapability(row) {
|
|
42
|
-
return ToolCapability.parse({
|
|
43
|
-
sourceId: row.id,
|
|
44
|
-
name: row.tool_name,
|
|
45
|
-
title: row.tool_title,
|
|
46
|
-
description: row.tool_description,
|
|
47
|
-
inputSchema: JSON.parse(row.input_schema_json),
|
|
48
|
-
outputSchema: row.output_schema_json ? JSON.parse(row.output_schema_json) : null,
|
|
49
|
-
annotations: ToolAnnotations.parse(JSON.parse(row.annotations_json)),
|
|
50
|
-
fingerprint: row.fingerprint,
|
|
51
|
-
discoveredAt: row.discovered_at,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
export function createToolRepository(deps) {
|
|
55
|
-
const db = deps.db;
|
|
56
|
-
return {
|
|
57
|
-
async listVisible(actor) {
|
|
58
|
-
const allowed = access(actor, deps.now().toISOString());
|
|
59
|
-
const result = await db
|
|
60
|
-
.prepare(`SELECT ${sourceColumns} FROM tool_sources source
|
|
61
|
-
WHERE ${allowed.sql} ORDER BY lower(source.name), source.id`)
|
|
62
|
-
.bind(...allowed.bindings)
|
|
63
|
-
.all();
|
|
64
|
-
return (result.results ?? []).map(mapSource);
|
|
65
|
-
},
|
|
66
|
-
async getVisible(actor, sourceId) {
|
|
67
|
-
const allowed = access(actor, deps.now().toISOString());
|
|
68
|
-
const row = await db
|
|
69
|
-
.prepare(`SELECT ${sourceColumns} FROM tool_sources source
|
|
70
|
-
WHERE source.id = ? AND ${allowed.sql}`)
|
|
71
|
-
.bind(sourceId, ...allowed.bindings)
|
|
72
|
-
.first();
|
|
73
|
-
return row ? mapSource(row) : null;
|
|
74
|
-
},
|
|
75
|
-
async canManage(actor, sourceId) {
|
|
76
|
-
const permitted = access(actor, deps.now().toISOString(), ["manager"]);
|
|
77
|
-
const row = await db
|
|
78
|
-
.prepare(`SELECT 1 AS allowed FROM tool_sources source
|
|
79
|
-
WHERE source.id = ? AND ${permitted.sql}`)
|
|
80
|
-
.bind(sourceId, ...permitted.bindings)
|
|
81
|
-
.first();
|
|
82
|
-
return row?.allowed === 1;
|
|
83
|
-
},
|
|
84
|
-
async findIdempotent(actorId, operation, key) {
|
|
85
|
-
const row = await db
|
|
86
|
-
.prepare(`SELECT resource_id FROM idempotency_keys
|
|
87
|
-
WHERE actor_id = ? AND operation = ? AND idempotency_key = ?`)
|
|
88
|
-
.bind(actorId, operation, key)
|
|
89
|
-
.first();
|
|
90
|
-
return row?.resource_id ?? null;
|
|
91
|
-
},
|
|
92
|
-
async insertSource(input) {
|
|
93
|
-
const source = input.source;
|
|
94
|
-
try {
|
|
95
|
-
await db.batch([
|
|
96
|
-
db
|
|
97
|
-
.prepare(`INSERT INTO tool_sources (
|
|
98
|
-
id, name, url, connection_handle, enabled, created_by,
|
|
99
|
-
created_at, updated_at, last_discovered_at
|
|
100
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
101
|
-
.bind(source.id, source.name, source.url, source.connectionHandle, source.enabled ? 1 : 0, source.createdBy, source.createdAt, source.updatedAt, source.lastDiscoveredAt),
|
|
102
|
-
db
|
|
103
|
-
.prepare(`INSERT INTO resource_grants (
|
|
104
|
-
id, resource_type, resource_id, principal_type, principal_id, role,
|
|
105
|
-
expires_at, created_by, created_at
|
|
106
|
-
) VALUES (?, 'tool-source', ?, 'organization', '*', 'viewer', NULL, ?, ?)`)
|
|
107
|
-
.bind(input.grantId, source.id, source.createdBy, source.createdAt),
|
|
108
|
-
db
|
|
109
|
-
.prepare(`INSERT INTO idempotency_keys (
|
|
110
|
-
actor_id, operation, idempotency_key, resource_id, created_at
|
|
111
|
-
) VALUES (?, 'tools.create', ?, ?, ?)`)
|
|
112
|
-
.bind(source.createdBy, input.idempotencyKey, source.id, source.createdAt),
|
|
113
|
-
db
|
|
114
|
-
.prepare(`INSERT INTO audit_events (
|
|
115
|
-
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
116
|
-
) VALUES (?, ?, 'tools.create', 'tool-source', ?, ?, ?)`)
|
|
117
|
-
.bind(input.auditId, source.createdBy, source.id, JSON.stringify({ name: source.name, url: source.url }), source.createdAt),
|
|
118
|
-
]);
|
|
119
|
-
return source;
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const replayedId = await this.findIdempotent(source.createdBy, "tools.create", input.idempotencyKey);
|
|
123
|
-
const replayed = replayedId
|
|
124
|
-
? await db
|
|
125
|
-
.prepare(`SELECT ${sourceColumns} FROM tool_sources source WHERE source.id = ?`)
|
|
126
|
-
.bind(replayedId)
|
|
127
|
-
.first()
|
|
128
|
-
: null;
|
|
129
|
-
if (replayed)
|
|
130
|
-
return mapSource(replayed);
|
|
131
|
-
throw error;
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
async replaceCatalog(input) {
|
|
135
|
-
const replayed = await this.findIdempotent(input.actorId, "tools.discover", input.idempotencyKey);
|
|
136
|
-
if (replayed)
|
|
137
|
-
return;
|
|
138
|
-
const statements = [
|
|
139
|
-
db.prepare("DELETE FROM tool_catalog WHERE source_id = ?").bind(input.sourceId),
|
|
140
|
-
...input.capabilities.map((capability) => db
|
|
141
|
-
.prepare(`INSERT INTO tool_catalog (
|
|
142
|
-
source_id, name, title, description, input_schema_json, output_schema_json,
|
|
143
|
-
annotations_json, fingerprint, discovered_at
|
|
144
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
145
|
-
.bind(capability.sourceId, capability.name, capability.title, capability.description, JSON.stringify(capability.inputSchema), capability.outputSchema ? JSON.stringify(capability.outputSchema) : null, JSON.stringify(capability.annotations), capability.fingerprint, capability.discoveredAt)),
|
|
146
|
-
db
|
|
147
|
-
.prepare(`UPDATE tool_sources
|
|
148
|
-
SET last_discovered_at = ?, updated_at = ?
|
|
149
|
-
WHERE id = ?`)
|
|
150
|
-
.bind(input.discoveredAt, input.discoveredAt, input.sourceId),
|
|
151
|
-
db
|
|
152
|
-
.prepare(`INSERT INTO idempotency_keys (
|
|
153
|
-
actor_id, operation, idempotency_key, resource_id, created_at
|
|
154
|
-
) VALUES (?, 'tools.discover', ?, ?, ?)`)
|
|
155
|
-
.bind(input.actorId, input.idempotencyKey, input.sourceId, input.discoveredAt),
|
|
156
|
-
db
|
|
157
|
-
.prepare(`INSERT INTO audit_events (
|
|
158
|
-
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
159
|
-
) VALUES (?, ?, 'tools.discover', 'tool-source', ?, ?, ?)`)
|
|
160
|
-
.bind(input.auditId, input.actorId, input.sourceId, JSON.stringify({ toolCount: input.capabilities.length }), input.discoveredAt),
|
|
161
|
-
];
|
|
162
|
-
try {
|
|
163
|
-
await db.batch(statements);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
const concurrentReplay = await this.findIdempotent(input.actorId, "tools.discover", input.idempotencyKey);
|
|
167
|
-
if (concurrentReplay === input.sourceId)
|
|
168
|
-
return;
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
async listCatalogVisible(actor) {
|
|
173
|
-
const allowed = access(actor, deps.now().toISOString());
|
|
174
|
-
const result = await db
|
|
175
|
-
.prepare(`SELECT ${catalogColumns}
|
|
176
|
-
FROM tool_catalog tool
|
|
177
|
-
JOIN tool_sources source ON source.id = tool.source_id
|
|
178
|
-
WHERE source.enabled = 1 AND ${allowed.sql}
|
|
179
|
-
ORDER BY lower(source.name), lower(coalesce(tool.title, tool.name)), tool.name`)
|
|
180
|
-
.bind(...allowed.bindings)
|
|
181
|
-
.all();
|
|
182
|
-
return (result.results ?? []).map((row) => ({
|
|
183
|
-
source: mapSource(row),
|
|
184
|
-
capability: mapCapability(row),
|
|
185
|
-
}));
|
|
186
|
-
},
|
|
187
|
-
async getCapabilityVisible(actor, sourceId, name) {
|
|
188
|
-
const allowed = access(actor, deps.now().toISOString());
|
|
189
|
-
const row = await db
|
|
190
|
-
.prepare(`SELECT ${catalogColumns}
|
|
191
|
-
FROM tool_catalog tool
|
|
192
|
-
JOIN tool_sources source ON source.id = tool.source_id
|
|
193
|
-
WHERE source.id = ? AND tool.name = ? AND source.enabled = 1
|
|
194
|
-
AND ${allowed.sql}`)
|
|
195
|
-
.bind(sourceId, name, ...allowed.bindings)
|
|
196
|
-
.first();
|
|
197
|
-
return row ? mapCapability(row) : null;
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
}
|