@nkmc/gateway 0.1.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/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- package/tsup.config.ts +8 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { HttpAuth } from "@nkmc/agent-fs";
|
|
2
|
+
|
|
3
|
+
export interface RpcManifestDef {
|
|
4
|
+
url: string;
|
|
5
|
+
convention?: "crud" | "evm" | "raw";
|
|
6
|
+
methods: Array<{
|
|
7
|
+
rpcMethod: string;
|
|
8
|
+
description: string;
|
|
9
|
+
resource?: string;
|
|
10
|
+
fsOp?: "list" | "read" | "write" | "create" | "remove" | "search";
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A single service to onboard */
|
|
15
|
+
export interface ManifestEntry {
|
|
16
|
+
domain: string;
|
|
17
|
+
/** OpenAPI spec URL — triggers compilation */
|
|
18
|
+
specUrl?: string;
|
|
19
|
+
/** skill.md URL — fetched and registered directly */
|
|
20
|
+
skillMdUrl?: string;
|
|
21
|
+
/** Inline skill.md content */
|
|
22
|
+
skillMd?: string;
|
|
23
|
+
/** JSON-RPC definition — triggers RPC compilation */
|
|
24
|
+
rpcDef?: RpcManifestDef;
|
|
25
|
+
/** Pool credential — values can be "${ENV_VAR}" references */
|
|
26
|
+
auth?: ManifestAuth;
|
|
27
|
+
/** Tags for categorization */
|
|
28
|
+
tags?: string[];
|
|
29
|
+
/** Skip this entry (default false) */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ManifestAuth {
|
|
34
|
+
type: "bearer" | "api-key" | "basic" | "oauth2";
|
|
35
|
+
token?: string;
|
|
36
|
+
prefix?: string;
|
|
37
|
+
header?: string;
|
|
38
|
+
key?: string;
|
|
39
|
+
username?: string;
|
|
40
|
+
password?: string;
|
|
41
|
+
tokenUrl?: string;
|
|
42
|
+
clientId?: string;
|
|
43
|
+
clientSecret?: string;
|
|
44
|
+
scope?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Result of onboarding one service */
|
|
48
|
+
export interface OnboardResult {
|
|
49
|
+
domain: string;
|
|
50
|
+
status: "ok" | "failed" | "skipped";
|
|
51
|
+
error?: string;
|
|
52
|
+
source: "openapi" | "skillmd" | "wellknown" | "jsonrpc" | "none";
|
|
53
|
+
endpoints: number;
|
|
54
|
+
resources: number;
|
|
55
|
+
hasCredentials: boolean;
|
|
56
|
+
smokeTest?: {
|
|
57
|
+
ls: boolean;
|
|
58
|
+
cat: boolean;
|
|
59
|
+
catEndpoint?: string;
|
|
60
|
+
};
|
|
61
|
+
durationMs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Summary of a batch onboard run */
|
|
65
|
+
export interface OnboardReport {
|
|
66
|
+
total: number;
|
|
67
|
+
ok: number;
|
|
68
|
+
failed: number;
|
|
69
|
+
skipped: number;
|
|
70
|
+
results: OnboardResult[];
|
|
71
|
+
durationMs: number;
|
|
72
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type { HttpAuth } from "@nkmc/agent-fs";
|
|
3
|
+
import {
|
|
4
|
+
ToolRegistry,
|
|
5
|
+
createDefaultToolRegistry,
|
|
6
|
+
} from "../tool-registry.js";
|
|
7
|
+
|
|
8
|
+
describe("ToolRegistry", () => {
|
|
9
|
+
it("resolves a known tool", () => {
|
|
10
|
+
const registry = createDefaultToolRegistry();
|
|
11
|
+
const gh = registry.get("gh");
|
|
12
|
+
|
|
13
|
+
expect(gh).not.toBeNull();
|
|
14
|
+
expect(gh!.name).toBe("gh");
|
|
15
|
+
expect(gh!.credentialDomain).toBe("github.com");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns null for an unknown tool", () => {
|
|
19
|
+
const registry = createDefaultToolRegistry();
|
|
20
|
+
expect(registry.get("nonexistent")).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("lists all registered tools", () => {
|
|
24
|
+
const registry = createDefaultToolRegistry();
|
|
25
|
+
const tools = registry.list();
|
|
26
|
+
|
|
27
|
+
expect(tools.length).toBe(5);
|
|
28
|
+
|
|
29
|
+
const names = tools.map((t) => t.name).sort();
|
|
30
|
+
expect(names).toEqual(["anthropic", "aws", "gh", "openai", "stripe"]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("builds env vars from bearer credential (gh → GH_TOKEN)", () => {
|
|
34
|
+
const registry = createDefaultToolRegistry();
|
|
35
|
+
const gh = registry.get("gh")!;
|
|
36
|
+
|
|
37
|
+
const auth: HttpAuth = { type: "bearer", token: "ghp_abc123" };
|
|
38
|
+
const env = registry.buildEnv(gh, auth);
|
|
39
|
+
|
|
40
|
+
expect(env).toEqual({ GH_TOKEN: "ghp_abc123" });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("builds env vars for api-key auth type (stripe → STRIPE_API_KEY)", () => {
|
|
44
|
+
const registry = createDefaultToolRegistry();
|
|
45
|
+
const stripe = registry.get("stripe")!;
|
|
46
|
+
|
|
47
|
+
const auth: HttpAuth = {
|
|
48
|
+
type: "api-key",
|
|
49
|
+
header: "Authorization",
|
|
50
|
+
key: "sk_test_xyz",
|
|
51
|
+
};
|
|
52
|
+
const env = registry.buildEnv(stripe, auth);
|
|
53
|
+
|
|
54
|
+
expect(env).toEqual({ STRIPE_API_KEY: "sk_test_xyz" });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("builds env vars for basic auth (aws → ACCESS_KEY_ID + SECRET)", () => {
|
|
58
|
+
const registry = createDefaultToolRegistry();
|
|
59
|
+
const aws = registry.get("aws")!;
|
|
60
|
+
|
|
61
|
+
const auth: HttpAuth = {
|
|
62
|
+
type: "basic",
|
|
63
|
+
username: "AKIAIOSFODNN7EXAMPLE",
|
|
64
|
+
password: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
65
|
+
};
|
|
66
|
+
const env = registry.buildEnv(aws, auth);
|
|
67
|
+
|
|
68
|
+
expect(env).toEqual({
|
|
69
|
+
AWS_ACCESS_KEY_ID: "AKIAIOSFODNN7EXAMPLE",
|
|
70
|
+
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("omits env vars when auth type does not match requested field", () => {
|
|
75
|
+
const registry = new ToolRegistry();
|
|
76
|
+
registry.register({
|
|
77
|
+
name: "test",
|
|
78
|
+
credentialDomain: "example.com",
|
|
79
|
+
envMapping: { MY_TOKEN: "token" },
|
|
80
|
+
});
|
|
81
|
+
const tool = registry.get("test")!;
|
|
82
|
+
|
|
83
|
+
// Pass basic auth but tool wants "token" → should get empty env
|
|
84
|
+
const auth: HttpAuth = {
|
|
85
|
+
type: "basic",
|
|
86
|
+
username: "user",
|
|
87
|
+
password: "pass",
|
|
88
|
+
};
|
|
89
|
+
const env = registry.buildEnv(tool, auth);
|
|
90
|
+
|
|
91
|
+
expect(env).toEqual({});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { HttpAuth } from "@nkmc/agent-fs";
|
|
2
|
+
|
|
3
|
+
/** Field names that can be extracted from an HttpAuth credential. */
|
|
4
|
+
export type AuthField = "token" | "key" | "username" | "password";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Defines how a CLI tool maps to a credential domain and which
|
|
8
|
+
* environment variables should be injected at runtime.
|
|
9
|
+
*/
|
|
10
|
+
export interface ToolDefinition {
|
|
11
|
+
/** CLI tool name, e.g. "gh", "stripe" */
|
|
12
|
+
name: string;
|
|
13
|
+
/** Domain used to look up credentials in the vault */
|
|
14
|
+
credentialDomain: string;
|
|
15
|
+
/** Maps env var name → field to extract from HttpAuth */
|
|
16
|
+
envMapping: Record<string, AuthField>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Registry of CLI tools that can be proxied through the gateway.
|
|
21
|
+
* Each tool declares the credential domain it needs and how to
|
|
22
|
+
* translate stored credentials into environment variables.
|
|
23
|
+
*/
|
|
24
|
+
export class ToolRegistry {
|
|
25
|
+
private tools = new Map<string, ToolDefinition>();
|
|
26
|
+
|
|
27
|
+
/** Register a tool definition. Overwrites any existing entry with the same name. */
|
|
28
|
+
register(tool: ToolDefinition): void {
|
|
29
|
+
this.tools.set(tool.name, tool);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Look up a tool by name. Returns null if not found. */
|
|
33
|
+
get(name: string): ToolDefinition | null {
|
|
34
|
+
return this.tools.get(name) ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Return all registered tool definitions. */
|
|
38
|
+
list(): ToolDefinition[] {
|
|
39
|
+
return [...this.tools.values()];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a record of environment variables for the given tool by
|
|
44
|
+
* extracting the requested fields from the HttpAuth credential.
|
|
45
|
+
*
|
|
46
|
+
* If the auth type does not contain the requested field (e.g.
|
|
47
|
+
* requesting "token" from a basic-auth credential), that env var
|
|
48
|
+
* is silently omitted.
|
|
49
|
+
*/
|
|
50
|
+
buildEnv(tool: ToolDefinition, auth: HttpAuth): Record<string, string> {
|
|
51
|
+
const env: Record<string, string> = {};
|
|
52
|
+
|
|
53
|
+
for (const [envVar, field] of Object.entries(tool.envMapping)) {
|
|
54
|
+
const value = extractField(auth, field);
|
|
55
|
+
if (value !== undefined) {
|
|
56
|
+
env[envVar] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return env;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Extract a named field from an HttpAuth credential. */
|
|
65
|
+
function extractField(auth: HttpAuth, field: AuthField): string | undefined {
|
|
66
|
+
switch (field) {
|
|
67
|
+
case "token":
|
|
68
|
+
return auth.type === "bearer" ? auth.token : undefined;
|
|
69
|
+
case "key":
|
|
70
|
+
return auth.type === "api-key" ? auth.key : undefined;
|
|
71
|
+
case "username":
|
|
72
|
+
return auth.type === "basic" ? auth.username : undefined;
|
|
73
|
+
case "password":
|
|
74
|
+
return auth.type === "basic" ? auth.password : undefined;
|
|
75
|
+
default:
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Default tools
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** Create a ToolRegistry pre-populated with common CLI tools. */
|
|
85
|
+
export function createDefaultToolRegistry(): ToolRegistry {
|
|
86
|
+
const registry = new ToolRegistry();
|
|
87
|
+
|
|
88
|
+
registry.register({
|
|
89
|
+
name: "gh",
|
|
90
|
+
credentialDomain: "github.com",
|
|
91
|
+
envMapping: { GH_TOKEN: "token" },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
registry.register({
|
|
95
|
+
name: "stripe",
|
|
96
|
+
credentialDomain: "api.stripe.com",
|
|
97
|
+
envMapping: { STRIPE_API_KEY: "key" },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
registry.register({
|
|
101
|
+
name: "openai",
|
|
102
|
+
credentialDomain: "api.openai.com",
|
|
103
|
+
envMapping: { OPENAI_API_KEY: "key" },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
registry.register({
|
|
107
|
+
name: "anthropic",
|
|
108
|
+
credentialDomain: "api.anthropic.com",
|
|
109
|
+
envMapping: { ANTHROPIC_API_KEY: "key" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
registry.register({
|
|
113
|
+
name: "aws",
|
|
114
|
+
credentialDomain: "aws.amazon.com",
|
|
115
|
+
envMapping: {
|
|
116
|
+
AWS_ACCESS_KEY_ID: "username",
|
|
117
|
+
AWS_SECRET_ACCESS_KEY: "password",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return registry;
|
|
122
|
+
}
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { FsBackend } from "@nkmc/agent-fs";
|
|
2
|
+
import { Context7Client, type Context7Options, type LibrarySearchResult } from "./context7.js";
|
|
3
|
+
|
|
4
|
+
export interface Context7BackendOptions extends Context7Options {}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* FsBackend that maps filesystem operations to Context7 documentation queries.
|
|
8
|
+
*
|
|
9
|
+
* Filesystem mapping:
|
|
10
|
+
* grep "react" / → searchLibraries("react") — search for libraries
|
|
11
|
+
* cat /{owner}/{repo} → queryDocs("/{owner}/{repo}", repo) — library overview
|
|
12
|
+
* grep "hooks" /{o}/{r} → queryDocs("/{o}/{r}", "hooks") — query specific docs
|
|
13
|
+
* ls / → usage instructions
|
|
14
|
+
*/
|
|
15
|
+
export class Context7Backend implements FsBackend {
|
|
16
|
+
private client: Context7Client;
|
|
17
|
+
|
|
18
|
+
constructor(options?: Context7BackendOptions) {
|
|
19
|
+
this.client = new Context7Client(options);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async list(path: string): Promise<string[]> {
|
|
23
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
24
|
+
|
|
25
|
+
if (!cleaned) {
|
|
26
|
+
return [
|
|
27
|
+
'grep "<关键词>" /context7/ — 搜索库',
|
|
28
|
+
'grep "<问题>" /context7/{id} — 查询文档',
|
|
29
|
+
'cat /context7/{owner}/{repo} — 库概览',
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ls /{owner}/{repo}/ — not much to list, return hint
|
|
34
|
+
return ['grep "<问题>" /context7/' + cleaned + " — 查询此库文档"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async read(path: string): Promise<unknown> {
|
|
38
|
+
const libraryId = parseLibraryId(path);
|
|
39
|
+
if (!libraryId) {
|
|
40
|
+
return { usage: 'grep "<关键词>" /context7/ — 搜索库' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// cat /{owner}/{repo} → query overview
|
|
44
|
+
const name = libraryId.split("/").pop() ?? libraryId;
|
|
45
|
+
const docs = await this.client.queryDocs(libraryId, `${name} overview getting started`);
|
|
46
|
+
return { libraryId, docs };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async write(_path: string, _data: unknown): Promise<{ id: string }> {
|
|
50
|
+
throw new Error("context7 is read-only");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async remove(_path: string): Promise<void> {
|
|
54
|
+
throw new Error("context7 is read-only");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async search(path: string, pattern: string): Promise<unknown[]> {
|
|
58
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
59
|
+
|
|
60
|
+
if (!cleaned) {
|
|
61
|
+
// grep at root → search libraries
|
|
62
|
+
const results = await this.client.searchLibraries(pattern);
|
|
63
|
+
return results.map(formatSearchResult);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// grep at /{owner}/{repo} → query docs
|
|
67
|
+
const libraryId = parseLibraryId(path);
|
|
68
|
+
if (!libraryId) return [];
|
|
69
|
+
|
|
70
|
+
const docs = await this.client.queryDocs(libraryId, pattern);
|
|
71
|
+
if (!docs) return [];
|
|
72
|
+
return [{ libraryId, query: pattern, docs }];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseLibraryId(path: string): string | null {
|
|
77
|
+
const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
78
|
+
if (!cleaned) return null;
|
|
79
|
+
|
|
80
|
+
// Expect owner/repo format
|
|
81
|
+
const parts = cleaned.split("/");
|
|
82
|
+
if (parts.length < 2) return null;
|
|
83
|
+
return "/" + parts.slice(0, 2).join("/");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatSearchResult(r: LibrarySearchResult): Record<string, unknown> {
|
|
87
|
+
return {
|
|
88
|
+
id: r.id,
|
|
89
|
+
name: r.name,
|
|
90
|
+
description: r.description ?? "",
|
|
91
|
+
snippets: r.totalSnippets ?? 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface Context7Options {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
fetchFn?: typeof globalThis.fetch;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface LibrarySearchResult {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
totalSnippets?: number;
|
|
12
|
+
trustScore?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Context7Client {
|
|
16
|
+
private apiKey?: string;
|
|
17
|
+
private baseUrl: string;
|
|
18
|
+
private fetchFn: typeof globalThis.fetch;
|
|
19
|
+
|
|
20
|
+
constructor(options?: Context7Options) {
|
|
21
|
+
this.apiKey = options?.apiKey;
|
|
22
|
+
this.baseUrl = options?.baseUrl ?? "https://context7.com/api/v2";
|
|
23
|
+
this.fetchFn = options?.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Search for a library by name. Returns matching library entries. */
|
|
27
|
+
async searchLibraries(libraryName: string, query?: string): Promise<LibrarySearchResult[]> {
|
|
28
|
+
const params = new URLSearchParams({ libraryName });
|
|
29
|
+
if (query) params.set("query", query);
|
|
30
|
+
|
|
31
|
+
const resp = await this.fetchFn(`${this.baseUrl}/libs/search?${params}`, {
|
|
32
|
+
headers: this.headers(),
|
|
33
|
+
});
|
|
34
|
+
if (!resp.ok) throw new Error(`Context7 search failed: ${resp.status}`);
|
|
35
|
+
return resp.json() as Promise<LibrarySearchResult[]>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Query documentation for a specific library. Returns documentation text. */
|
|
39
|
+
async queryDocs(libraryId: string, query: string): Promise<string> {
|
|
40
|
+
const params = new URLSearchParams({ libraryId, query, type: "txt" });
|
|
41
|
+
|
|
42
|
+
const resp = await this.fetchFn(`${this.baseUrl}/context?${params}`, {
|
|
43
|
+
headers: this.headers(),
|
|
44
|
+
});
|
|
45
|
+
if (!resp.ok) throw new Error(`Context7 query failed: ${resp.status}`);
|
|
46
|
+
return resp.text();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private headers(): Record<string, string> {
|
|
50
|
+
const h: Record<string, string> = {};
|
|
51
|
+
if (this.apiKey) h["Authorization"] = `Bearer ${this.apiKey}`;
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { D1Database } from "../d1/types.js";
|
|
2
|
+
import type {
|
|
3
|
+
RegistryStore,
|
|
4
|
+
RegistryStats,
|
|
5
|
+
SearchResult,
|
|
6
|
+
ServiceRecord,
|
|
7
|
+
ServiceSummary,
|
|
8
|
+
VersionSummary,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
const CREATE_SERVICES = `
|
|
12
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
13
|
+
domain TEXT NOT NULL,
|
|
14
|
+
version TEXT NOT NULL,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
description TEXT,
|
|
17
|
+
roles TEXT,
|
|
18
|
+
skill_md TEXT NOT NULL,
|
|
19
|
+
endpoints TEXT,
|
|
20
|
+
is_first_party INTEGER DEFAULT 0,
|
|
21
|
+
status TEXT DEFAULT 'active',
|
|
22
|
+
is_default INTEGER DEFAULT 1,
|
|
23
|
+
source TEXT,
|
|
24
|
+
sunset_date INTEGER,
|
|
25
|
+
auth_mode TEXT,
|
|
26
|
+
created_at INTEGER NOT NULL,
|
|
27
|
+
updated_at INTEGER NOT NULL,
|
|
28
|
+
PRIMARY KEY (domain, version)
|
|
29
|
+
)`;
|
|
30
|
+
|
|
31
|
+
const CREATE_SERVICES_DEFAULT_INDEX = `
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_services_default ON services(domain, is_default)`;
|
|
33
|
+
|
|
34
|
+
const CREATE_DOMAIN_CHALLENGES = `
|
|
35
|
+
CREATE TABLE IF NOT EXISTS domain_challenges (
|
|
36
|
+
domain TEXT PRIMARY KEY,
|
|
37
|
+
challenge_code TEXT NOT NULL,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
39
|
+
created_at INTEGER NOT NULL,
|
|
40
|
+
verified_at INTEGER,
|
|
41
|
+
expires_at INTEGER NOT NULL
|
|
42
|
+
)`;
|
|
43
|
+
|
|
44
|
+
interface ServiceRow {
|
|
45
|
+
domain: string;
|
|
46
|
+
version: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
roles: string;
|
|
50
|
+
skill_md: string;
|
|
51
|
+
endpoints: string;
|
|
52
|
+
is_first_party: number;
|
|
53
|
+
status: string;
|
|
54
|
+
is_default: number;
|
|
55
|
+
source: string | null;
|
|
56
|
+
sunset_date: number | null;
|
|
57
|
+
auth_mode: string | null;
|
|
58
|
+
created_at: number;
|
|
59
|
+
updated_at: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rowToRecord(row: ServiceRow): ServiceRecord {
|
|
63
|
+
return {
|
|
64
|
+
domain: row.domain,
|
|
65
|
+
name: row.name,
|
|
66
|
+
description: row.description,
|
|
67
|
+
version: row.version,
|
|
68
|
+
roles: JSON.parse(row.roles),
|
|
69
|
+
skillMd: row.skill_md,
|
|
70
|
+
endpoints: JSON.parse(row.endpoints),
|
|
71
|
+
isFirstParty: row.is_first_party === 1,
|
|
72
|
+
createdAt: row.created_at,
|
|
73
|
+
updatedAt: row.updated_at,
|
|
74
|
+
status: row.status as ServiceRecord["status"],
|
|
75
|
+
isDefault: row.is_default === 1,
|
|
76
|
+
...(row.source ? { source: JSON.parse(row.source) } : {}),
|
|
77
|
+
...(row.sunset_date ? { sunsetDate: row.sunset_date } : {}),
|
|
78
|
+
...(row.auth_mode ? { authMode: row.auth_mode as ServiceRecord["authMode"] } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function toSummary(row: ServiceRow): ServiceSummary {
|
|
83
|
+
return {
|
|
84
|
+
domain: row.domain,
|
|
85
|
+
name: row.name,
|
|
86
|
+
description: row.description,
|
|
87
|
+
isFirstParty: row.is_first_party === 1,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class D1RegistryStore implements RegistryStore {
|
|
92
|
+
constructor(private db: D1Database) {}
|
|
93
|
+
|
|
94
|
+
async initSchema(): Promise<void> {
|
|
95
|
+
await this.db.exec(CREATE_SERVICES);
|
|
96
|
+
await this.db.exec(CREATE_SERVICES_DEFAULT_INDEX);
|
|
97
|
+
await this.db.exec(CREATE_DOMAIN_CHALLENGES);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async get(domain: string): Promise<ServiceRecord | null> {
|
|
101
|
+
const row = await this.db
|
|
102
|
+
.prepare("SELECT * FROM services WHERE domain = ? AND is_default = 1")
|
|
103
|
+
.bind(domain)
|
|
104
|
+
.first<ServiceRow>();
|
|
105
|
+
|
|
106
|
+
return row ? rowToRecord(row) : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getVersion(domain: string, version: string): Promise<ServiceRecord | null> {
|
|
110
|
+
const row = await this.db
|
|
111
|
+
.prepare("SELECT * FROM services WHERE domain = ? AND version = ?")
|
|
112
|
+
.bind(domain, version)
|
|
113
|
+
.first<ServiceRow>();
|
|
114
|
+
|
|
115
|
+
return row ? rowToRecord(row) : null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async listVersions(domain: string): Promise<VersionSummary[]> {
|
|
119
|
+
const { results } = await this.db
|
|
120
|
+
.prepare(
|
|
121
|
+
"SELECT version, status, is_default, created_at, updated_at FROM services WHERE domain = ? ORDER BY created_at DESC",
|
|
122
|
+
)
|
|
123
|
+
.bind(domain)
|
|
124
|
+
.all<ServiceRow>();
|
|
125
|
+
|
|
126
|
+
return results.map((row) => ({
|
|
127
|
+
version: row.version,
|
|
128
|
+
status: row.status as ServiceRecord["status"],
|
|
129
|
+
isDefault: row.is_default === 1,
|
|
130
|
+
createdAt: row.created_at,
|
|
131
|
+
updatedAt: row.updated_at,
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Max endpoints JSON size before stripping verbose fields (parameters, requestBody, responses). */
|
|
136
|
+
static readonly ENDPOINTS_SIZE_LIMIT = 800_000; // ~800 KB, well under D1's ~1 MB per-value limit
|
|
137
|
+
|
|
138
|
+
async put(domain: string, record: ServiceRecord): Promise<void> {
|
|
139
|
+
let endpointsJson = JSON.stringify(record.endpoints);
|
|
140
|
+
|
|
141
|
+
// Only strip verbose fields when the full JSON exceeds the size limit.
|
|
142
|
+
// Small APIs keep parameters/requestBody/responses for the explore detail page.
|
|
143
|
+
if (endpointsJson.length > D1RegistryStore.ENDPOINTS_SIZE_LIMIT) {
|
|
144
|
+
const slim = record.endpoints.map(({ method, path, description, price, pricing }) => ({
|
|
145
|
+
method,
|
|
146
|
+
path,
|
|
147
|
+
description,
|
|
148
|
+
...(price ? { price } : {}),
|
|
149
|
+
...(pricing ? { pricing } : {}),
|
|
150
|
+
}));
|
|
151
|
+
endpointsJson = JSON.stringify(slim);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await this.db
|
|
155
|
+
.prepare(
|
|
156
|
+
`INSERT OR REPLACE INTO services
|
|
157
|
+
(domain, version, name, description, roles, skill_md, endpoints, is_first_party, status, is_default, source, sunset_date, auth_mode, created_at, updated_at)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
159
|
+
)
|
|
160
|
+
.bind(
|
|
161
|
+
domain,
|
|
162
|
+
record.version,
|
|
163
|
+
record.name,
|
|
164
|
+
record.description,
|
|
165
|
+
JSON.stringify(record.roles),
|
|
166
|
+
record.skillMd,
|
|
167
|
+
endpointsJson,
|
|
168
|
+
record.isFirstParty ? 1 : 0,
|
|
169
|
+
record.status,
|
|
170
|
+
record.isDefault ? 1 : 0,
|
|
171
|
+
record.source ? JSON.stringify(record.source) : null,
|
|
172
|
+
record.sunsetDate ?? null,
|
|
173
|
+
record.authMode ?? null,
|
|
174
|
+
record.createdAt,
|
|
175
|
+
record.updatedAt,
|
|
176
|
+
)
|
|
177
|
+
.run();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async delete(domain: string): Promise<void> {
|
|
181
|
+
await this.db
|
|
182
|
+
.prepare("DELETE FROM services WHERE domain = ?")
|
|
183
|
+
.bind(domain)
|
|
184
|
+
.run();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async list(): Promise<ServiceSummary[]> {
|
|
188
|
+
const { results } = await this.db
|
|
189
|
+
.prepare("SELECT domain, name, description, is_first_party FROM services WHERE is_default = 1")
|
|
190
|
+
.all<ServiceRow>();
|
|
191
|
+
|
|
192
|
+
return results.map(toSummary);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async search(query: string): Promise<SearchResult[]> {
|
|
196
|
+
const pattern = `%${query}%`;
|
|
197
|
+
const { results: rows } = await this.db
|
|
198
|
+
.prepare(
|
|
199
|
+
`SELECT * FROM services
|
|
200
|
+
WHERE is_default = 1 AND (name LIKE ? OR description LIKE ? OR endpoints LIKE ?)`,
|
|
201
|
+
)
|
|
202
|
+
.bind(pattern, pattern, pattern)
|
|
203
|
+
.all<ServiceRow>();
|
|
204
|
+
|
|
205
|
+
const q = query.toLowerCase();
|
|
206
|
+
return rows.map((row) => {
|
|
207
|
+
const endpoints = JSON.parse(row.endpoints) as Array<{
|
|
208
|
+
method: string;
|
|
209
|
+
path: string;
|
|
210
|
+
description: string;
|
|
211
|
+
}>;
|
|
212
|
+
const matched = endpoints.filter(
|
|
213
|
+
(e) =>
|
|
214
|
+
e.description.toLowerCase().includes(q) ||
|
|
215
|
+
e.method.toLowerCase().includes(q) ||
|
|
216
|
+
e.path.toLowerCase().includes(q),
|
|
217
|
+
);
|
|
218
|
+
return {
|
|
219
|
+
...toSummary(row),
|
|
220
|
+
matchedEndpoints: matched.map((e) => ({
|
|
221
|
+
method: e.method,
|
|
222
|
+
path: e.path,
|
|
223
|
+
description: e.description,
|
|
224
|
+
})),
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async stats(): Promise<RegistryStats> {
|
|
230
|
+
const row = await this.db
|
|
231
|
+
.prepare(
|
|
232
|
+
`SELECT COUNT(*) as service_count, COALESCE(SUM(json_array_length(endpoints)), 0) as endpoint_count
|
|
233
|
+
FROM services WHERE is_default = 1`,
|
|
234
|
+
)
|
|
235
|
+
.first<{ service_count: number; endpoint_count: number }>();
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
serviceCount: row?.service_count ?? 0,
|
|
239
|
+
endpointCount: row?.endpoint_count ?? 0,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|