@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,16 @@
|
|
|
1
|
+
export interface D1Result<T = unknown> {
|
|
2
|
+
results?: T[];
|
|
3
|
+
meta?: {
|
|
4
|
+
changes?: number;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export interface D1Statement {
|
|
8
|
+
bind(...values: unknown[]): D1Statement;
|
|
9
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
10
|
+
all<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
11
|
+
run<T = Record<string, unknown>>(): Promise<D1Result<T>>;
|
|
12
|
+
}
|
|
13
|
+
export interface D1Database {
|
|
14
|
+
prepare(query: string): D1Statement;
|
|
15
|
+
batch<T = unknown>(statements: D1Statement[]): Promise<D1Result<T>[]>;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function converted(result) {
|
|
2
|
+
const item = Array.isArray(result) ? result[0] : result;
|
|
3
|
+
if (typeof item !== "object" ||
|
|
4
|
+
item === null ||
|
|
5
|
+
!("format" in item) ||
|
|
6
|
+
(item.format !== "markdown" && item.format !== "text") ||
|
|
7
|
+
!("data" in item) ||
|
|
8
|
+
typeof item.data !== "string") {
|
|
9
|
+
const detail = typeof item === "object" && item !== null && "error" in item && typeof item.error === "string"
|
|
10
|
+
? item.error
|
|
11
|
+
: "Markdown conversion returned an invalid response";
|
|
12
|
+
throw new Error(detail);
|
|
13
|
+
}
|
|
14
|
+
return item.data;
|
|
15
|
+
}
|
|
16
|
+
export function createDocumentConverter(ai) {
|
|
17
|
+
return {
|
|
18
|
+
async convert(name, mediaType, content) {
|
|
19
|
+
return converted(await ai.toMarkdown({
|
|
20
|
+
name,
|
|
21
|
+
blob: new Blob([content], { type: mediaType }),
|
|
22
|
+
}));
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function createFlowRuntime(binding) {
|
|
2
|
+
return {
|
|
3
|
+
async start(runId) {
|
|
4
|
+
try {
|
|
5
|
+
await binding.create({ id: runId, params: { runId } });
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
try {
|
|
9
|
+
const existing = await binding.get(runId);
|
|
10
|
+
await existing.status();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
async signal(runId) {
|
|
18
|
+
const instance = await binding.get(runId);
|
|
19
|
+
await instance.sendEvent({ type: "advance", payload: { runId } });
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface WorkflowBinding {
|
|
2
|
+
create(options: {
|
|
3
|
+
id: string;
|
|
4
|
+
params: {
|
|
5
|
+
runId: string;
|
|
6
|
+
};
|
|
7
|
+
}): Promise<{
|
|
8
|
+
id: string;
|
|
9
|
+
}>;
|
|
10
|
+
get(id: string): Promise<{
|
|
11
|
+
status(): Promise<{
|
|
12
|
+
status: string;
|
|
13
|
+
}>;
|
|
14
|
+
sendEvent(event: {
|
|
15
|
+
type: string;
|
|
16
|
+
payload: unknown;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { QueueProducer } from "./index-queue.types.js";
|
|
3
|
+
export declare const IndexMessage: z.ZodObject<{
|
|
4
|
+
type: z.ZodLiteral<"knowledge.version">;
|
|
5
|
+
versionId: z.ZodString;
|
|
6
|
+
}, z.core.$strict>;
|
|
7
|
+
export type IndexMessage = z.infer<typeof IndexMessage>;
|
|
8
|
+
export declare function createIndexQueue(queue: QueueProducer<IndexMessage>): {
|
|
9
|
+
enqueue(versionId: string): Promise<void>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const IndexMessage = z.strictObject({
|
|
3
|
+
type: z.literal("knowledge.version"),
|
|
4
|
+
versionId: z.string().min(1),
|
|
5
|
+
});
|
|
6
|
+
export function createIndexQueue(queue) {
|
|
7
|
+
return {
|
|
8
|
+
async enqueue(versionId) {
|
|
9
|
+
await queue.send({ type: "knowledge.version", versionId });
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Ajv, {} from "ajv";
|
|
2
|
+
// Tool schemas come from remote catalogs and are therefore untrusted: allErrors lets a crafted
|
|
3
|
+
// schema amplify validation work, and Ajv keeps every compiled schema for the life of the isolate.
|
|
4
|
+
const MaxCompiledSchemas = 200;
|
|
5
|
+
export function createJsonSchemaValidator() {
|
|
6
|
+
const ajv = new Ajv({ allErrors: false, strict: false });
|
|
7
|
+
const compiled = new Map();
|
|
8
|
+
return (schema, value) => {
|
|
9
|
+
try {
|
|
10
|
+
const key = JSON.stringify(schema);
|
|
11
|
+
let validate = compiled.get(key);
|
|
12
|
+
if (!validate) {
|
|
13
|
+
if (compiled.size >= MaxCompiledSchemas) {
|
|
14
|
+
compiled.clear();
|
|
15
|
+
ajv.removeSchema();
|
|
16
|
+
}
|
|
17
|
+
validate = ajv.compile(schema);
|
|
18
|
+
compiled.set(key, validate);
|
|
19
|
+
}
|
|
20
|
+
if (validate(value))
|
|
21
|
+
return { valid: true };
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
detail: ajv.errorsText(validate.errors, { separator: "; " }).slice(0, 2_000),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { valid: false, detail: "The cached tool schema is invalid" };
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { ToolSourceUrl } from "@anchrd/intel-contract";
|
|
2
|
+
import * as client from "openid-client";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const ProtectedResource = z.looseObject({
|
|
5
|
+
resource: ToolSourceUrl,
|
|
6
|
+
authorization_servers: z.array(ToolSourceUrl).min(1),
|
|
7
|
+
});
|
|
8
|
+
function metadataCandidates(resourceUrl, challenge) {
|
|
9
|
+
const resource = new URL(resourceUrl);
|
|
10
|
+
const path = resource.pathname === "/" ? "" : resource.pathname.replace(/\/+$/, "");
|
|
11
|
+
let challengeUrl = null;
|
|
12
|
+
try {
|
|
13
|
+
challengeUrl = challenge ? new URL(challenge, resource) : null;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
challengeUrl = null;
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
...(challengeUrl?.origin === resource.origin ? [challengeUrl.href] : []),
|
|
20
|
+
`${resource.origin}/.well-known/oauth-protected-resource${path}`,
|
|
21
|
+
`${resource.origin}/.well-known/oauth-protected-resource`,
|
|
22
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
23
|
+
}
|
|
24
|
+
function resourceMetadataChallenge(header) {
|
|
25
|
+
return header?.match(/resource_metadata="([^"]+)"/i)?.[1] ?? null;
|
|
26
|
+
}
|
|
27
|
+
function isCloudflareAccess(issuer) {
|
|
28
|
+
return new URL(issuer).hostname.endsWith(".cloudflareaccess.com");
|
|
29
|
+
}
|
|
30
|
+
function tokenResponse(tokens) {
|
|
31
|
+
return {
|
|
32
|
+
accessToken: tokens.access_token,
|
|
33
|
+
expiresIn: tokens.expires_in ?? 3_600,
|
|
34
|
+
...(tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Discovery, token and probe requests run inside the login and connect paths. Without a deadline a
|
|
38
|
+
// hanging issuer would hold the request open until the platform kills it.
|
|
39
|
+
const RequestTimeoutMs = 10_000;
|
|
40
|
+
export function createOpenId(deps) {
|
|
41
|
+
const customFetch = async (url, options) => {
|
|
42
|
+
const init = options;
|
|
43
|
+
const deadline = AbortSignal.timeout(RequestTimeoutMs);
|
|
44
|
+
// The library may pass its own signal. Spreading init last would drop the deadline, spreading it
|
|
45
|
+
// first would drop the caller's cancellation, so both have to abort the request.
|
|
46
|
+
return await deps.fetch(url, {
|
|
47
|
+
...init,
|
|
48
|
+
signal: init.signal ? AbortSignal.any([init.signal, deadline]) : deadline,
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const configurations = new Map();
|
|
52
|
+
function options(issuer) {
|
|
53
|
+
const insecure = new URL(issuer).protocol === "http:";
|
|
54
|
+
if (insecure && !deps.allowInsecure) {
|
|
55
|
+
throw new Error("Insecure OAuth issuers require ALLOW_INSECURE_OAUTH=true");
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
[client.customFetch]: customFetch,
|
|
59
|
+
...(insecure ? { execute: [client.allowInsecureRequests] } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function discover(issuer, clientId) {
|
|
63
|
+
const key = `${issuer}#${clientId}`;
|
|
64
|
+
let configuration = configurations.get(key);
|
|
65
|
+
if (!configuration) {
|
|
66
|
+
configuration = client.discovery(new URL(issuer), clientId, { token_endpoint_auth_method: "none" }, client.None(), options(issuer));
|
|
67
|
+
configurations.set(key, configuration);
|
|
68
|
+
void configuration.catch(() => configurations.delete(key));
|
|
69
|
+
}
|
|
70
|
+
return await configuration;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
async discoverResource(resourceUrl) {
|
|
74
|
+
const probe = await deps
|
|
75
|
+
.fetch(resourceUrl, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
signal: AbortSignal.timeout(RequestTimeoutMs),
|
|
78
|
+
headers: {
|
|
79
|
+
accept: "application/json, text/event-stream",
|
|
80
|
+
"content-type": "application/json",
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
jsonrpc: "2.0",
|
|
84
|
+
id: "intel-oauth-discovery",
|
|
85
|
+
method: "initialize",
|
|
86
|
+
params: {
|
|
87
|
+
protocolVersion: "2025-11-25",
|
|
88
|
+
capabilities: {},
|
|
89
|
+
clientInfo: { name: "intel", version: "0.1.0" },
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
.catch(() => null);
|
|
94
|
+
const candidates = metadataCandidates(resourceUrl, resourceMetadataChallenge(probe?.headers.get("www-authenticate") ?? null));
|
|
95
|
+
for (const candidate of candidates) {
|
|
96
|
+
const response = await deps
|
|
97
|
+
.fetch(candidate, {
|
|
98
|
+
signal: AbortSignal.timeout(RequestTimeoutMs),
|
|
99
|
+
headers: { accept: "application/json" },
|
|
100
|
+
})
|
|
101
|
+
.catch(() => null);
|
|
102
|
+
if (!response?.ok)
|
|
103
|
+
continue;
|
|
104
|
+
const metadata = ProtectedResource.safeParse(await response.json().catch(() => null));
|
|
105
|
+
if (!metadata.success)
|
|
106
|
+
continue;
|
|
107
|
+
if (new URL(metadata.data.resource).href !== new URL(resourceUrl).href)
|
|
108
|
+
continue;
|
|
109
|
+
const issuer = metadata.data.authorization_servers[0];
|
|
110
|
+
if (issuer) {
|
|
111
|
+
return {
|
|
112
|
+
resource: metadata.data.resource,
|
|
113
|
+
issuer,
|
|
114
|
+
...(isCloudflareAccess(issuer) ? {} : { scope: "openid profile email offline_access" }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
throw new Error("MCP resource did not publish valid OAuth metadata");
|
|
119
|
+
},
|
|
120
|
+
async register(input) {
|
|
121
|
+
const configuration = await client.dynamicClientRegistration(new URL(input.issuer), {
|
|
122
|
+
client_name: input.clientName,
|
|
123
|
+
redirect_uris: [input.redirectUri],
|
|
124
|
+
response_types: ["code"],
|
|
125
|
+
grant_types: ["authorization_code"],
|
|
126
|
+
...(isCloudflareAccess(input.issuer)
|
|
127
|
+
? {}
|
|
128
|
+
: { scope: "openid profile email offline_access" }),
|
|
129
|
+
token_endpoint_auth_method: "none",
|
|
130
|
+
...(isCloudflareAccess(input.issuer) ? { resource: input.resource } : {}),
|
|
131
|
+
}, client.None(), options(input.issuer));
|
|
132
|
+
return configuration.clientMetadata().client_id;
|
|
133
|
+
},
|
|
134
|
+
async authorizationUrl(input) {
|
|
135
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
136
|
+
return client.buildAuthorizationUrl(configuration, {
|
|
137
|
+
response_type: "code",
|
|
138
|
+
redirect_uri: input.redirectUri,
|
|
139
|
+
...(input.scope ? { scope: input.scope } : {}),
|
|
140
|
+
resource: input.resource,
|
|
141
|
+
code_challenge: input.codeChallenge,
|
|
142
|
+
code_challenge_method: "S256",
|
|
143
|
+
state: input.state,
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
async exchange(input) {
|
|
147
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
148
|
+
const tokens = await client.authorizationCodeGrant(configuration, input.callbackUrl, { pkceCodeVerifier: input.codeVerifier, expectedState: input.state }, { resource: input.resource });
|
|
149
|
+
return tokenResponse(tokens);
|
|
150
|
+
},
|
|
151
|
+
async refresh(input) {
|
|
152
|
+
const configuration = await discover(input.issuer, input.clientId);
|
|
153
|
+
const tokens = await client.refreshTokenGrant(configuration, input.refreshToken, {
|
|
154
|
+
resource: input.resource,
|
|
155
|
+
});
|
|
156
|
+
return tokenResponse(tokens);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PortalTokenStore } from "../../tools/tools.types.js";
|
|
2
|
+
import type { D1Database } from "../db/db.types.js";
|
|
3
|
+
export declare function createPortalTokenStore(deps: {
|
|
4
|
+
db: D1Database;
|
|
5
|
+
secret: string;
|
|
6
|
+
crypto: Crypto;
|
|
7
|
+
now(): Date;
|
|
8
|
+
}): PortalTokenStore;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const StoredToken = z.strictObject({
|
|
3
|
+
version: z.literal(1),
|
|
4
|
+
accessToken: z.string().min(1),
|
|
5
|
+
refreshToken: z.string().min(1).nullable(),
|
|
6
|
+
expiresAt: z.number().int().positive(),
|
|
7
|
+
issuer: z.url(),
|
|
8
|
+
clientId: z.string().min(1),
|
|
9
|
+
resource: z.url(),
|
|
10
|
+
});
|
|
11
|
+
// Bound to this purpose so a sealed value cannot be replayed as a session cookie and vice versa.
|
|
12
|
+
const additionalData = new TextEncoder().encode("anchrd-intel-portal-token-v1");
|
|
13
|
+
function encode(bytes) {
|
|
14
|
+
let value = "";
|
|
15
|
+
for (const byte of bytes)
|
|
16
|
+
value += String.fromCharCode(byte);
|
|
17
|
+
return btoa(value);
|
|
18
|
+
}
|
|
19
|
+
function decode(value) {
|
|
20
|
+
return Uint8Array.from(atob(value), (character) => character.charCodeAt(0));
|
|
21
|
+
}
|
|
22
|
+
export function createPortalTokenStore(deps) {
|
|
23
|
+
if (new TextEncoder().encode(deps.secret).byteLength < 32) {
|
|
24
|
+
throw new Error("INTEL_SESSION_SECRET must contain at least 32 bytes");
|
|
25
|
+
}
|
|
26
|
+
const key = deps.crypto.subtle
|
|
27
|
+
.digest("SHA-256", new TextEncoder().encode(deps.secret))
|
|
28
|
+
.then((hash) => deps.crypto.subtle.importKey("raw", hash, "AES-GCM", false, ["encrypt", "decrypt"]));
|
|
29
|
+
return {
|
|
30
|
+
async read(userId) {
|
|
31
|
+
const row = await deps.db
|
|
32
|
+
.prepare("SELECT sealed FROM portal_tokens WHERE user_id = ?")
|
|
33
|
+
.bind(userId)
|
|
34
|
+
.first();
|
|
35
|
+
if (!row)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const raw = decode(row.sealed);
|
|
39
|
+
const plaintext = await deps.crypto.subtle.decrypt({ name: "AES-GCM", iv: raw.subarray(0, 12), additionalData }, await key, raw.subarray(12));
|
|
40
|
+
const parsed = StoredToken.parse(JSON.parse(new TextDecoder().decode(plaintext)));
|
|
41
|
+
const { version: _version, ...token } = parsed;
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// A row that no longer decrypts (rotated secret, tampering) is treated as absent so the
|
|
46
|
+
// user is asked to reconnect instead of hitting an opaque failure on every call.
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async write(userId, token) {
|
|
51
|
+
const iv = deps.crypto.getRandomValues(new Uint8Array(12));
|
|
52
|
+
const encrypted = new Uint8Array(await deps.crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, await key, new TextEncoder().encode(JSON.stringify({ version: 1, ...token }))));
|
|
53
|
+
const sealed = new Uint8Array(iv.byteLength + encrypted.byteLength);
|
|
54
|
+
sealed.set(iv);
|
|
55
|
+
sealed.set(encrypted, iv.byteLength);
|
|
56
|
+
const timestamp = deps.now().toISOString();
|
|
57
|
+
await deps.db
|
|
58
|
+
.prepare(`INSERT INTO portal_tokens (user_id, sealed, expires_at, updated_at)
|
|
59
|
+
VALUES (?, ?, ?, ?)
|
|
60
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
61
|
+
sealed = excluded.sealed,
|
|
62
|
+
expires_at = excluded.expires_at,
|
|
63
|
+
updated_at = excluded.updated_at`)
|
|
64
|
+
.bind(userId, encode(sealed), new Date(token.expiresAt).toISOString(), timestamp)
|
|
65
|
+
.run();
|
|
66
|
+
},
|
|
67
|
+
async clear(userId) {
|
|
68
|
+
await deps.db.prepare("DELETE FROM portal_tokens WHERE user_id = ?").bind(userId).run();
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ToolTestResult } from "@anchrd/intel-contract";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
function responseWithLimit(response, maxBytes) {
|
|
5
|
+
const length = Number(response.headers.get("content-length"));
|
|
6
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
7
|
+
void response.body?.cancel();
|
|
8
|
+
throw new Error(`Remote MCP response exceeded ${maxBytes} bytes`);
|
|
9
|
+
}
|
|
10
|
+
if (!response.body)
|
|
11
|
+
return response;
|
|
12
|
+
let received = 0;
|
|
13
|
+
const bounded = response.body.pipeThrough(new TransformStream({
|
|
14
|
+
transform(chunk, controller) {
|
|
15
|
+
received += chunk.byteLength;
|
|
16
|
+
if (received > maxBytes) {
|
|
17
|
+
controller.error(new Error(`Remote MCP response exceeded ${maxBytes} bytes`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
controller.enqueue(chunk);
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
return new Response(bounded, {
|
|
24
|
+
status: response.status,
|
|
25
|
+
statusText: response.statusText,
|
|
26
|
+
headers: response.headers,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function createRemoteTools(deps) {
|
|
30
|
+
async function connected(url, accessToken, signal, maxBytes, work) {
|
|
31
|
+
const client = new Client({ name: "intel-tools", version: "0.1.0" });
|
|
32
|
+
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
33
|
+
fetch: async (input, init) => responseWithLimit(await deps.fetch(input, {
|
|
34
|
+
...init,
|
|
35
|
+
signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal,
|
|
36
|
+
}), maxBytes),
|
|
37
|
+
requestInit: { headers: { authorization: `Bearer ${accessToken}` }, signal },
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
// The SDK's transport and client currently disagree under exactOptionalPropertyTypes.
|
|
41
|
+
await client.connect(transport);
|
|
42
|
+
return await work(client);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await client.close().catch(() => undefined);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
async list(url, accessToken, signal) {
|
|
50
|
+
return await connected(url, accessToken, signal, 5_000_000, async (client) => {
|
|
51
|
+
const tools = [];
|
|
52
|
+
let cursor;
|
|
53
|
+
do {
|
|
54
|
+
const page = await client.listTools(cursor ? { cursor } : undefined);
|
|
55
|
+
for (const tool of page.tools) {
|
|
56
|
+
tools.push({
|
|
57
|
+
name: tool.name,
|
|
58
|
+
title: tool.title ?? tool.annotations?.title ?? null,
|
|
59
|
+
description: tool.description ?? null,
|
|
60
|
+
inputSchema: tool.inputSchema,
|
|
61
|
+
outputSchema: tool.outputSchema ?? null,
|
|
62
|
+
annotations: {
|
|
63
|
+
...(tool.annotations?.title ? { title: tool.annotations.title } : {}),
|
|
64
|
+
...(tool.annotations?.readOnlyHint !== undefined
|
|
65
|
+
? { readOnlyHint: tool.annotations.readOnlyHint }
|
|
66
|
+
: {}),
|
|
67
|
+
...(tool.annotations?.destructiveHint !== undefined
|
|
68
|
+
? { destructiveHint: tool.annotations.destructiveHint }
|
|
69
|
+
: {}),
|
|
70
|
+
...(tool.annotations?.idempotentHint !== undefined
|
|
71
|
+
? { idempotentHint: tool.annotations.idempotentHint }
|
|
72
|
+
: {}),
|
|
73
|
+
...(tool.annotations?.openWorldHint !== undefined
|
|
74
|
+
? { openWorldHint: tool.annotations.openWorldHint }
|
|
75
|
+
: {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
if (tools.length > 1_000)
|
|
79
|
+
return tools;
|
|
80
|
+
}
|
|
81
|
+
cursor = page.nextCursor;
|
|
82
|
+
} while (cursor);
|
|
83
|
+
return tools;
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
async call(input) {
|
|
87
|
+
return await connected(input.url, input.accessToken, input.signal, 1_100_000, async (client) => {
|
|
88
|
+
const result = await client.callTool({
|
|
89
|
+
name: input.name,
|
|
90
|
+
arguments: input.arguments,
|
|
91
|
+
});
|
|
92
|
+
if (!("content" in result)) {
|
|
93
|
+
return ToolTestResult.parse({
|
|
94
|
+
isError: false,
|
|
95
|
+
content: [],
|
|
96
|
+
structuredContent: result.toolResult,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return ToolTestResult.parse({
|
|
100
|
+
isError: result.isError ?? false,
|
|
101
|
+
content: result.content,
|
|
102
|
+
...(result.structuredContent !== undefined
|
|
103
|
+
? { structuredContent: result.structuredContent }
|
|
104
|
+
: {}),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const defaultModel = "@cf/baai/bge-m3";
|
|
2
|
+
const maxEmbeddingCharacters = 24_000;
|
|
3
|
+
function embedding(result) {
|
|
4
|
+
if (typeof result !== "object" ||
|
|
5
|
+
result === null ||
|
|
6
|
+
!("data" in result) ||
|
|
7
|
+
!Array.isArray(result.data) ||
|
|
8
|
+
!Array.isArray(result.data[0]) ||
|
|
9
|
+
!result.data[0].every((value) => typeof value === "number")) {
|
|
10
|
+
throw new Error("The embedding provider returned an invalid response");
|
|
11
|
+
}
|
|
12
|
+
return result.data[0];
|
|
13
|
+
}
|
|
14
|
+
function matches(result) {
|
|
15
|
+
if (typeof result !== "object" ||
|
|
16
|
+
result === null ||
|
|
17
|
+
!("matches" in result) ||
|
|
18
|
+
!Array.isArray(result.matches)) {
|
|
19
|
+
throw new Error("The semantic index returned an invalid response");
|
|
20
|
+
}
|
|
21
|
+
return result.matches.flatMap((match) => {
|
|
22
|
+
if (typeof match !== "object" ||
|
|
23
|
+
match === null ||
|
|
24
|
+
!("id" in match) ||
|
|
25
|
+
typeof match.id !== "string" ||
|
|
26
|
+
!("score" in match) ||
|
|
27
|
+
typeof match.score !== "number") {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [{ nodeId: match.id, score: Math.max(0, Math.min(1, match.score)) }];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function createSemanticIndex(deps) {
|
|
34
|
+
const model = deps.model ?? defaultModel;
|
|
35
|
+
async function embed(text) {
|
|
36
|
+
return embedding(await deps.ai.run(model, { text: [text] }));
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
async replace(target, content) {
|
|
40
|
+
const text = `${target.title}\n\n${content}`.slice(0, maxEmbeddingCharacters);
|
|
41
|
+
await deps.index.upsert([
|
|
42
|
+
{
|
|
43
|
+
id: target.nodeId,
|
|
44
|
+
values: await embed(text),
|
|
45
|
+
metadata: { versionId: target.versionId },
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
},
|
|
49
|
+
async search(query, limit) {
|
|
50
|
+
const result = await deps.index.query(await embed(query), {
|
|
51
|
+
topK: Math.max(1, Math.min(100, limit)),
|
|
52
|
+
returnMetadata: "none",
|
|
53
|
+
});
|
|
54
|
+
return matches(result);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { KnowledgeIndexTarget } from "../../knowledge/knowledge.types.js";
|
|
2
|
+
export interface WorkersAiBinding {
|
|
3
|
+
run(model: string, input: {
|
|
4
|
+
text: string[];
|
|
5
|
+
}): Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
export interface VectorizeBinding {
|
|
8
|
+
upsert(vectors: Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
values: number[];
|
|
11
|
+
metadata: {
|
|
12
|
+
versionId: string;
|
|
13
|
+
};
|
|
14
|
+
}>): Promise<unknown>;
|
|
15
|
+
query(vector: number[], options: {
|
|
16
|
+
topK: number;
|
|
17
|
+
returnMetadata: "none";
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface SemanticHit {
|
|
21
|
+
nodeId: string;
|
|
22
|
+
score: number;
|
|
23
|
+
}
|
|
24
|
+
export interface SemanticIndex {
|
|
25
|
+
replace(target: KnowledgeIndexTarget, content: string): Promise<void>;
|
|
26
|
+
search(query: string, limit: number): Promise<SemanticHit[]>;
|
|
27
|
+
}
|
|
28
|
+
export interface SemanticIndexDeps {
|
|
29
|
+
ai: WorkersAiBinding;
|
|
30
|
+
index: VectorizeBinding;
|
|
31
|
+
model?: string;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|