@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,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { MemoryPeerStore } from "../peer-store.js";
|
|
3
|
+
import type { PeerGateway, LendingRule } from "../types.js";
|
|
4
|
+
|
|
5
|
+
function makePeer(overrides: Partial<PeerGateway> = {}): PeerGateway {
|
|
6
|
+
return {
|
|
7
|
+
id: "peer-1",
|
|
8
|
+
name: "Test Gateway",
|
|
9
|
+
url: "https://peer1.example.com",
|
|
10
|
+
sharedSecret: "secret-abc",
|
|
11
|
+
status: "active",
|
|
12
|
+
advertisedDomains: ["api.example.com"],
|
|
13
|
+
lastSeen: Date.now(),
|
|
14
|
+
createdAt: Date.now(),
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function makeRule(overrides: Partial<LendingRule> = {}): LendingRule {
|
|
20
|
+
return {
|
|
21
|
+
domain: "api.example.com",
|
|
22
|
+
allow: true,
|
|
23
|
+
peers: "*",
|
|
24
|
+
pricing: { mode: "free" },
|
|
25
|
+
createdAt: Date.now(),
|
|
26
|
+
updatedAt: Date.now(),
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("MemoryPeerStore", () => {
|
|
32
|
+
let store: MemoryPeerStore;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
store = new MemoryPeerStore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// --- Peer CRUD ---
|
|
39
|
+
|
|
40
|
+
it("putPeer then getPeer returns the peer", async () => {
|
|
41
|
+
const peer = makePeer();
|
|
42
|
+
await store.putPeer(peer);
|
|
43
|
+
expect(await store.getPeer("peer-1")).toEqual(peer);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("getPeer returns null for unknown id", async () => {
|
|
47
|
+
expect(await store.getPeer("nonexistent")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("deletePeer removes the peer", async () => {
|
|
51
|
+
await store.putPeer(makePeer());
|
|
52
|
+
await store.deletePeer("peer-1");
|
|
53
|
+
expect(await store.getPeer("peer-1")).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("listPeers only returns active peers", async () => {
|
|
57
|
+
await store.putPeer(makePeer({ id: "a", status: "active", name: "A" }));
|
|
58
|
+
await store.putPeer(makePeer({ id: "b", status: "inactive", name: "B" }));
|
|
59
|
+
await store.putPeer(makePeer({ id: "c", status: "active", name: "C" }));
|
|
60
|
+
|
|
61
|
+
const list = await store.listPeers();
|
|
62
|
+
expect(list).toHaveLength(2);
|
|
63
|
+
expect(list.map((p) => p.id).sort()).toEqual(["a", "c"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("updateLastSeen updates the timestamp", async () => {
|
|
67
|
+
await store.putPeer(makePeer({ lastSeen: 1000 }));
|
|
68
|
+
await store.updateLastSeen("peer-1", 9999);
|
|
69
|
+
|
|
70
|
+
const peer = await store.getPeer("peer-1");
|
|
71
|
+
expect(peer!.lastSeen).toBe(9999);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("updateLastSeen is a no-op for unknown peer", async () => {
|
|
75
|
+
// should not throw
|
|
76
|
+
await store.updateLastSeen("nonexistent", Date.now());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- Lending Rule CRUD ---
|
|
80
|
+
|
|
81
|
+
it("putRule then getRule returns the rule", async () => {
|
|
82
|
+
const rule = makeRule();
|
|
83
|
+
await store.putRule(rule);
|
|
84
|
+
expect(await store.getRule("api.example.com")).toEqual(rule);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("getRule returns null for unknown domain", async () => {
|
|
88
|
+
expect(await store.getRule("unknown.example.com")).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("deleteRule removes the rule", async () => {
|
|
92
|
+
await store.putRule(makeRule());
|
|
93
|
+
await store.deleteRule("api.example.com");
|
|
94
|
+
expect(await store.getRule("api.example.com")).toBeNull();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("listRules returns all rules", async () => {
|
|
98
|
+
await store.putRule(makeRule({ domain: "a.com" }));
|
|
99
|
+
await store.putRule(makeRule({ domain: "b.com" }));
|
|
100
|
+
await store.putRule(makeRule({ domain: "c.com" }));
|
|
101
|
+
|
|
102
|
+
const list = await store.listRules();
|
|
103
|
+
expect(list).toHaveLength(3);
|
|
104
|
+
expect(list.map((r) => r.domain).sort()).toEqual(["a.com", "b.com", "c.com"]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("putRule overwrites existing rule for same domain", async () => {
|
|
108
|
+
await store.putRule(makeRule({ domain: "x.com", allow: true }));
|
|
109
|
+
await store.putRule(makeRule({ domain: "x.com", allow: false }));
|
|
110
|
+
|
|
111
|
+
const rule = await store.getRule("x.com");
|
|
112
|
+
expect(rule!.allow).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { D1Database } from "../d1/types.js";
|
|
2
|
+
import type { PeerGateway, LendingRule, PeerStore } from "./types.js";
|
|
3
|
+
|
|
4
|
+
interface PeerRow {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
url: string;
|
|
8
|
+
shared_secret: string;
|
|
9
|
+
status: string;
|
|
10
|
+
advertised_domains: string;
|
|
11
|
+
last_seen: number;
|
|
12
|
+
created_at: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface LendingRuleRow {
|
|
16
|
+
domain: string;
|
|
17
|
+
allow: number;
|
|
18
|
+
peers: string;
|
|
19
|
+
pricing: string;
|
|
20
|
+
rate_limit: string | null;
|
|
21
|
+
created_at: number;
|
|
22
|
+
updated_at: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rowToPeer(row: PeerRow): PeerGateway {
|
|
26
|
+
return {
|
|
27
|
+
id: row.id,
|
|
28
|
+
name: row.name,
|
|
29
|
+
url: row.url,
|
|
30
|
+
sharedSecret: row.shared_secret,
|
|
31
|
+
status: row.status as PeerGateway["status"],
|
|
32
|
+
advertisedDomains: JSON.parse(row.advertised_domains),
|
|
33
|
+
lastSeen: row.last_seen,
|
|
34
|
+
createdAt: row.created_at,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rowToRule(row: LendingRuleRow): LendingRule {
|
|
39
|
+
return {
|
|
40
|
+
domain: row.domain,
|
|
41
|
+
allow: row.allow === 1,
|
|
42
|
+
peers: JSON.parse(row.peers),
|
|
43
|
+
pricing: JSON.parse(row.pricing),
|
|
44
|
+
...(row.rate_limit ? { rateLimit: JSON.parse(row.rate_limit) } : {}),
|
|
45
|
+
createdAt: row.created_at,
|
|
46
|
+
updatedAt: row.updated_at,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class D1PeerStore implements PeerStore {
|
|
51
|
+
constructor(private db: D1Database) {}
|
|
52
|
+
|
|
53
|
+
async initSchema(): Promise<void> {
|
|
54
|
+
await this.db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
56
|
+
id TEXT PRIMARY KEY,
|
|
57
|
+
name TEXT NOT NULL,
|
|
58
|
+
url TEXT NOT NULL,
|
|
59
|
+
shared_secret TEXT NOT NULL,
|
|
60
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
61
|
+
advertised_domains TEXT NOT NULL DEFAULT '[]',
|
|
62
|
+
last_seen INTEGER NOT NULL,
|
|
63
|
+
created_at INTEGER NOT NULL
|
|
64
|
+
)`);
|
|
65
|
+
await this.db.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS lending_rules (
|
|
67
|
+
domain TEXT PRIMARY KEY,
|
|
68
|
+
allow INTEGER NOT NULL DEFAULT 1,
|
|
69
|
+
peers TEXT NOT NULL DEFAULT '"*"',
|
|
70
|
+
pricing TEXT NOT NULL DEFAULT '{"mode":"free"}',
|
|
71
|
+
rate_limit TEXT,
|
|
72
|
+
created_at INTEGER NOT NULL,
|
|
73
|
+
updated_at INTEGER NOT NULL
|
|
74
|
+
)`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getPeer(id: string): Promise<PeerGateway | null> {
|
|
78
|
+
const row = await this.db
|
|
79
|
+
.prepare("SELECT * FROM peers WHERE id = ?")
|
|
80
|
+
.bind(id)
|
|
81
|
+
.first<PeerRow>();
|
|
82
|
+
return row ? rowToPeer(row) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async putPeer(peer: PeerGateway): Promise<void> {
|
|
86
|
+
await this.db
|
|
87
|
+
.prepare(
|
|
88
|
+
`INSERT OR REPLACE INTO peers (id, name, url, shared_secret, status, advertised_domains, last_seen, created_at)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
90
|
+
)
|
|
91
|
+
.bind(
|
|
92
|
+
peer.id,
|
|
93
|
+
peer.name,
|
|
94
|
+
peer.url,
|
|
95
|
+
peer.sharedSecret,
|
|
96
|
+
peer.status,
|
|
97
|
+
JSON.stringify(peer.advertisedDomains),
|
|
98
|
+
peer.lastSeen,
|
|
99
|
+
peer.createdAt,
|
|
100
|
+
)
|
|
101
|
+
.run();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async deletePeer(id: string): Promise<void> {
|
|
105
|
+
await this.db
|
|
106
|
+
.prepare("DELETE FROM peers WHERE id = ?")
|
|
107
|
+
.bind(id)
|
|
108
|
+
.run();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async listPeers(): Promise<PeerGateway[]> {
|
|
112
|
+
const { results } = await this.db
|
|
113
|
+
.prepare("SELECT * FROM peers WHERE status = 'active'")
|
|
114
|
+
.all<PeerRow>();
|
|
115
|
+
return results.map(rowToPeer);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async updateLastSeen(id: string, timestamp: number): Promise<void> {
|
|
119
|
+
await this.db
|
|
120
|
+
.prepare("UPDATE peers SET last_seen = ? WHERE id = ?")
|
|
121
|
+
.bind(timestamp, id)
|
|
122
|
+
.run();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getRule(domain: string): Promise<LendingRule | null> {
|
|
126
|
+
const row = await this.db
|
|
127
|
+
.prepare("SELECT * FROM lending_rules WHERE domain = ?")
|
|
128
|
+
.bind(domain)
|
|
129
|
+
.first<LendingRuleRow>();
|
|
130
|
+
return row ? rowToRule(row) : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async putRule(rule: LendingRule): Promise<void> {
|
|
134
|
+
await this.db
|
|
135
|
+
.prepare(
|
|
136
|
+
`INSERT OR REPLACE INTO lending_rules (domain, allow, peers, pricing, rate_limit, created_at, updated_at)
|
|
137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
138
|
+
)
|
|
139
|
+
.bind(
|
|
140
|
+
rule.domain,
|
|
141
|
+
rule.allow ? 1 : 0,
|
|
142
|
+
JSON.stringify(rule.peers),
|
|
143
|
+
JSON.stringify(rule.pricing),
|
|
144
|
+
rule.rateLimit ? JSON.stringify(rule.rateLimit) : null,
|
|
145
|
+
rule.createdAt,
|
|
146
|
+
rule.updatedAt,
|
|
147
|
+
)
|
|
148
|
+
.run();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async deleteRule(domain: string): Promise<void> {
|
|
152
|
+
await this.db
|
|
153
|
+
.prepare("DELETE FROM lending_rules WHERE domain = ?")
|
|
154
|
+
.bind(domain)
|
|
155
|
+
.run();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async listRules(): Promise<LendingRule[]> {
|
|
159
|
+
const { results } = await this.db
|
|
160
|
+
.prepare("SELECT * FROM lending_rules")
|
|
161
|
+
.all<LendingRuleRow>();
|
|
162
|
+
return results.map(rowToRule);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FsBackend } from "@nkmc/agent-fs";
|
|
2
|
+
import type { PeerClient, PeerExecResult } from "./peer-client.js";
|
|
3
|
+
import type { PeerGateway } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An FsBackend that delegates filesystem operations to a peer gateway.
|
|
7
|
+
* Used when the local gateway doesn't have credentials for a domain
|
|
8
|
+
* but a federated peer does.
|
|
9
|
+
*/
|
|
10
|
+
export class PeerBackend implements FsBackend {
|
|
11
|
+
constructor(
|
|
12
|
+
private client: PeerClient,
|
|
13
|
+
private peer: PeerGateway,
|
|
14
|
+
private agentId: string,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
async list(path: string): Promise<string[]> {
|
|
18
|
+
const result = await this.execOnPeer(`ls ${path}`);
|
|
19
|
+
return (result.data as string[]) ?? [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async read(path: string): Promise<unknown> {
|
|
23
|
+
const result = await this.execOnPeer(`cat ${path}`);
|
|
24
|
+
return result.data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async write(path: string, data: unknown): Promise<{ id: string }> {
|
|
28
|
+
const result = await this.execOnPeer(
|
|
29
|
+
`write ${path} ${JSON.stringify(data)}`,
|
|
30
|
+
);
|
|
31
|
+
return (result.data as { id: string }) ?? { id: "" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async remove(path: string): Promise<void> {
|
|
35
|
+
await this.execOnPeer(`rm ${path}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async search(path: string, pattern: string): Promise<unknown[]> {
|
|
39
|
+
const result = await this.execOnPeer(`grep ${pattern} ${path}`);
|
|
40
|
+
return (result.data as unknown[]) ?? [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async execOnPeer(command: string): Promise<PeerExecResult> {
|
|
44
|
+
const result = await this.client.exec(this.peer, {
|
|
45
|
+
command,
|
|
46
|
+
agentId: this.agentId,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
if (result.paymentRequired) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Payment required: ${result.paymentRequired.price} ${result.paymentRequired.currency}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(result.error ?? "Peer execution failed");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { PeerGateway } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface PeerQueryResult {
|
|
4
|
+
available: boolean;
|
|
5
|
+
pricing?: { mode: string; amount?: number };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PeerExecResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
error?: string;
|
|
12
|
+
paymentRequired?: {
|
|
13
|
+
price: number;
|
|
14
|
+
currency: string;
|
|
15
|
+
payTo: string;
|
|
16
|
+
network: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Client for communicating with peer gateways in the federation.
|
|
22
|
+
* Each method sends authenticated HTTP requests to the peer's URL.
|
|
23
|
+
*/
|
|
24
|
+
export class PeerClient {
|
|
25
|
+
constructor(private selfId: string) {}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Query a peer to check if it has credentials for a domain
|
|
29
|
+
* and its lending rules allow access.
|
|
30
|
+
*/
|
|
31
|
+
async query(peer: PeerGateway, domain: string): Promise<PeerQueryResult> {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${peer.url}/federation/query`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"X-Peer-Id": this.selfId,
|
|
38
|
+
Authorization: `Bearer ${peer.sharedSecret}`,
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ domain }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
return { available: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const body = (await res.json()) as {
|
|
48
|
+
available: boolean;
|
|
49
|
+
pricing?: { mode: string; amount?: number };
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
available: body.available,
|
|
53
|
+
...(body.pricing ? { pricing: body.pricing } : {}),
|
|
54
|
+
};
|
|
55
|
+
} catch {
|
|
56
|
+
return { available: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Execute a command on a peer gateway on behalf of an agent.
|
|
62
|
+
* Handles 402 Payment Required responses with X-402-* headers.
|
|
63
|
+
*/
|
|
64
|
+
async exec(
|
|
65
|
+
peer: PeerGateway,
|
|
66
|
+
request: { command: string; agentId: string },
|
|
67
|
+
): Promise<PeerExecResult> {
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(`${peer.url}/federation/exec`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"X-Peer-Id": this.selfId,
|
|
74
|
+
Authorization: `Bearer ${peer.sharedSecret}`,
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(request),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (res.status === 402) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
paymentRequired: {
|
|
83
|
+
price: Number(res.headers.get("X-402-Price") ?? "0"),
|
|
84
|
+
currency: res.headers.get("X-402-Currency") ?? "USD",
|
|
85
|
+
payTo: res.headers.get("X-402-Pay-To") ?? "",
|
|
86
|
+
network: res.headers.get("X-402-Network") ?? "",
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const body = (await res.json().catch(() => ({}))) as {
|
|
93
|
+
error?: string;
|
|
94
|
+
};
|
|
95
|
+
return { ok: false, error: body.error ?? `HTTP ${res.status}` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const body = (await res.json()) as { data?: unknown };
|
|
99
|
+
return { ok: true, data: body.data };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: err instanceof Error ? err.message : String(err),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Announce our available domains to a peer gateway.
|
|
110
|
+
*/
|
|
111
|
+
async announce(peer: PeerGateway, domains: string[]): Promise<void> {
|
|
112
|
+
await fetch(`${peer.url}/federation/announce`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
"X-Peer-Id": this.selfId,
|
|
117
|
+
Authorization: `Bearer ${peer.sharedSecret}`,
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({ domains }),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PeerGateway, LendingRule, PeerStore } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class MemoryPeerStore implements PeerStore {
|
|
4
|
+
private peers = new Map<string, PeerGateway>();
|
|
5
|
+
private rules = new Map<string, LendingRule>();
|
|
6
|
+
|
|
7
|
+
async getPeer(id: string): Promise<PeerGateway | null> {
|
|
8
|
+
return this.peers.get(id) ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async putPeer(peer: PeerGateway): Promise<void> {
|
|
12
|
+
this.peers.set(peer.id, peer);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async deletePeer(id: string): Promise<void> {
|
|
16
|
+
this.peers.delete(id);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async listPeers(): Promise<PeerGateway[]> {
|
|
20
|
+
return Array.from(this.peers.values()).filter((p) => p.status === "active");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async updateLastSeen(id: string, timestamp: number): Promise<void> {
|
|
24
|
+
const peer = this.peers.get(id);
|
|
25
|
+
if (peer) {
|
|
26
|
+
peer.lastSeen = timestamp;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getRule(domain: string): Promise<LendingRule | null> {
|
|
31
|
+
return this.rules.get(domain) ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async putRule(rule: LendingRule): Promise<void> {
|
|
35
|
+
this.rules.set(rule.domain, rule);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async deleteRule(domain: string): Promise<void> {
|
|
39
|
+
this.rules.delete(domain);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async listRules(): Promise<LendingRule[]> {
|
|
43
|
+
return Array.from(this.rules.values());
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface PeerGateway {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
url: string;
|
|
5
|
+
sharedSecret: string;
|
|
6
|
+
status: "active" | "inactive";
|
|
7
|
+
advertisedDomains: string[];
|
|
8
|
+
lastSeen: number;
|
|
9
|
+
createdAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface LendingRule {
|
|
13
|
+
domain: string;
|
|
14
|
+
allow: boolean;
|
|
15
|
+
peers: string[] | "*";
|
|
16
|
+
pricing: {
|
|
17
|
+
mode: "free" | "per-request" | "per-token";
|
|
18
|
+
amount?: number;
|
|
19
|
+
};
|
|
20
|
+
rateLimit?: {
|
|
21
|
+
requests: number;
|
|
22
|
+
window: "minute" | "hour" | "day";
|
|
23
|
+
};
|
|
24
|
+
createdAt: number;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PeerStore {
|
|
29
|
+
getPeer(id: string): Promise<PeerGateway | null>;
|
|
30
|
+
putPeer(peer: PeerGateway): Promise<void>;
|
|
31
|
+
deletePeer(id: string): Promise<void>;
|
|
32
|
+
listPeers(): Promise<PeerGateway[]>;
|
|
33
|
+
updateLastSeen(id: string, timestamp: number): Promise<void>;
|
|
34
|
+
|
|
35
|
+
getRule(domain: string): Promise<LendingRule | null>;
|
|
36
|
+
putRule(rule: LendingRule): Promise<void>;
|
|
37
|
+
deleteRule(domain: string): Promise<void>;
|
|
38
|
+
listRules(): Promise<LendingRule[]>;
|
|
39
|
+
}
|
package/src/http/app.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { AgentFs } from "@nkmc/agent-fs";
|
|
3
|
+
import type { JWK } from "jose";
|
|
4
|
+
import type { RegistryStore } from "../registry/types.js";
|
|
5
|
+
import type { D1Database } from "../d1/types.js";
|
|
6
|
+
import type { CredentialVault } from "../credential/types.js";
|
|
7
|
+
import { createRegistryResolver } from "../registry/resolver.js";
|
|
8
|
+
import { Context7Backend } from "../registry/context7-backend.js";
|
|
9
|
+
import { adminAuth } from "./middleware/admin-auth.js";
|
|
10
|
+
import { publishOrAdminAuth } from "./middleware/publish-auth.js";
|
|
11
|
+
import { agentAuth } from "./middleware/agent-auth.js";
|
|
12
|
+
import { authRoutes } from "./routes/auth.js";
|
|
13
|
+
import { registryRoutes } from "./routes/registry.js";
|
|
14
|
+
import { domainRoutes } from "./routes/domains.js";
|
|
15
|
+
import { credentialRoutes } from "./routes/credentials.js";
|
|
16
|
+
import { byokRoutes } from "./routes/byok.js";
|
|
17
|
+
import { fsRoutes } from "./routes/fs.js";
|
|
18
|
+
import { proxyRoutes, type ExecResult } from "./routes/proxy.js";
|
|
19
|
+
import { peerRoutes } from "./routes/peers.js";
|
|
20
|
+
import { federationRoutes } from "./routes/federation.js";
|
|
21
|
+
import type { PeerStore } from "../federation/types.js";
|
|
22
|
+
import type { ToolRegistry } from "../proxy/tool-registry.js";
|
|
23
|
+
import type { TunnelStore, TunnelProvider } from "../tunnel/types.js";
|
|
24
|
+
import { tunnelRoutes } from "./routes/tunnels.js";
|
|
25
|
+
|
|
26
|
+
export type Env = {
|
|
27
|
+
Variables: {
|
|
28
|
+
agent: { id: string; roles: string[] };
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface GatewayOptions {
|
|
33
|
+
store: RegistryStore;
|
|
34
|
+
gatewayPrivateKey: JWK;
|
|
35
|
+
gatewayPublicKey: JWK;
|
|
36
|
+
adminToken: string;
|
|
37
|
+
db?: D1Database;
|
|
38
|
+
vault?: CredentialVault;
|
|
39
|
+
context7ApiKey?: string;
|
|
40
|
+
peerStore?: PeerStore;
|
|
41
|
+
proxy?: {
|
|
42
|
+
toolRegistry: ToolRegistry;
|
|
43
|
+
exec: (tool: string, args: string[], env: Record<string, string>) => Promise<ExecResult>;
|
|
44
|
+
};
|
|
45
|
+
tunnel?: {
|
|
46
|
+
store: TunnelStore;
|
|
47
|
+
provider: TunnelProvider;
|
|
48
|
+
domain: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createGateway(options: GatewayOptions): Hono<Env> {
|
|
53
|
+
const { store, gatewayPrivateKey, gatewayPublicKey, adminToken } = options;
|
|
54
|
+
|
|
55
|
+
const app = new Hono<Env>();
|
|
56
|
+
|
|
57
|
+
// Create registry resolver hooks for AgentFs
|
|
58
|
+
const { onMiss, listDomains, searchDomains, searchEndpoints } = createRegistryResolver(
|
|
59
|
+
options.vault
|
|
60
|
+
? { store, vault: options.vault, gatewayPrivateKey }
|
|
61
|
+
: { store, gatewayPrivateKey },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// First-party mounts
|
|
65
|
+
const mounts: { path: string; backend: import("@nkmc/agent-fs").FsBackend }[] = [];
|
|
66
|
+
|
|
67
|
+
// Context7: built-in documentation query service
|
|
68
|
+
if (options.context7ApiKey) {
|
|
69
|
+
mounts.push({ path: "/context7", backend: new Context7Backend({ apiKey: options.context7ApiKey }) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Create AgentFs instance with registry hooks
|
|
73
|
+
const agentFs = new AgentFs({
|
|
74
|
+
mounts,
|
|
75
|
+
onMiss,
|
|
76
|
+
listDomains,
|
|
77
|
+
searchDomains,
|
|
78
|
+
searchEndpoints,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Public: JWKS endpoint for developers to discover gateway public key
|
|
82
|
+
app.get("/.well-known/jwks.json", (c) => {
|
|
83
|
+
return c.json({ keys: [gatewayPublicKey] });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Public: Auth token endpoint
|
|
87
|
+
app.route("/auth", authRoutes({ privateKey: gatewayPrivateKey }));
|
|
88
|
+
|
|
89
|
+
// Public: Domain claim / verify (only when db is provided)
|
|
90
|
+
if (options.db) {
|
|
91
|
+
app.route("/domains", domainRoutes({ db: options.db, gatewayPrivateKey }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Registry management (admin token or publish token)
|
|
95
|
+
app.use("/registry/*", publishOrAdminAuth(adminToken, gatewayPublicKey));
|
|
96
|
+
app.route("/registry", registryRoutes({ store }));
|
|
97
|
+
|
|
98
|
+
// Admin: Credential management (optional — only mounted when vault is provided)
|
|
99
|
+
if (options.vault) {
|
|
100
|
+
app.use("/credentials/*", adminAuth(adminToken));
|
|
101
|
+
app.route("/credentials", credentialRoutes({ vault: options.vault }));
|
|
102
|
+
|
|
103
|
+
// Agent: BYOK credential management (JWT protected)
|
|
104
|
+
app.use("/byok/*", agentAuth(gatewayPublicKey));
|
|
105
|
+
app.route("/byok", byokRoutes({ vault: options.vault }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Agent: FS and execute routes (JWT protected, middleware first)
|
|
109
|
+
app.use("/execute", agentAuth(gatewayPublicKey));
|
|
110
|
+
app.use("/fs/*", agentAuth(gatewayPublicKey));
|
|
111
|
+
app.route("/", fsRoutes({ agentFs }));
|
|
112
|
+
|
|
113
|
+
// Proxy routes (optional — only mounted when proxy config and vault are provided)
|
|
114
|
+
if (options.proxy && options.vault) {
|
|
115
|
+
app.use("/proxy/*", agentAuth(gatewayPublicKey));
|
|
116
|
+
app.route(
|
|
117
|
+
"/proxy",
|
|
118
|
+
proxyRoutes({
|
|
119
|
+
vault: options.vault,
|
|
120
|
+
toolRegistry: options.proxy.toolRegistry,
|
|
121
|
+
exec: options.proxy.exec,
|
|
122
|
+
}),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Federation routes (optional — only mounted when peerStore and vault are provided)
|
|
127
|
+
if (options.peerStore && options.vault) {
|
|
128
|
+
app.use("/admin/federation/*", adminAuth(adminToken));
|
|
129
|
+
app.route("/admin/federation", peerRoutes({ peerStore: options.peerStore }));
|
|
130
|
+
|
|
131
|
+
app.route("/federation", federationRoutes({
|
|
132
|
+
peerStore: options.peerStore,
|
|
133
|
+
vault: options.vault,
|
|
134
|
+
agentFs,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Tunnel routes (optional — only mounted when tunnel config is provided)
|
|
139
|
+
if (options.tunnel) {
|
|
140
|
+
app.use("/tunnels/*", agentAuth(gatewayPublicKey));
|
|
141
|
+
app.route(
|
|
142
|
+
"/tunnels",
|
|
143
|
+
tunnelRoutes({
|
|
144
|
+
tunnelStore: options.tunnel.store,
|
|
145
|
+
tunnelProvider: options.tunnel.provider,
|
|
146
|
+
tunnelDomain: options.tunnel.domain,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return app;
|
|
152
|
+
}
|