@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,174 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import type { JWK } from "jose";
|
|
4
|
+
import { signPublishToken } from "@nkmc/core";
|
|
5
|
+
import type { D1Database } from "../../d1/types.js";
|
|
6
|
+
import { queryDnsTxt } from "../lib/dns.js";
|
|
7
|
+
|
|
8
|
+
export interface DomainRouteOptions {
|
|
9
|
+
db: D1Database;
|
|
10
|
+
gatewayPrivateKey: JWK;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ChallengeRow {
|
|
14
|
+
domain: string;
|
|
15
|
+
challenge_code: string;
|
|
16
|
+
status: string;
|
|
17
|
+
created_at: number;
|
|
18
|
+
verified_at: number | null;
|
|
19
|
+
expires_at: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
function isValidDomain(domain: string): boolean {
|
|
26
|
+
return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)+$/.test(domain);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function domainRoutes(options: DomainRouteOptions) {
|
|
30
|
+
const { db, gatewayPrivateKey } = options;
|
|
31
|
+
const app = new Hono();
|
|
32
|
+
|
|
33
|
+
// Request a DNS challenge for domain ownership
|
|
34
|
+
app.post("/challenge", async (c) => {
|
|
35
|
+
const body = await c.req.json<{ domain: string }>().catch(() => null);
|
|
36
|
+
const domain = body?.domain;
|
|
37
|
+
|
|
38
|
+
if (!domain || !isValidDomain(domain)) {
|
|
39
|
+
return c.json({ error: "Invalid or missing domain" }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
|
|
44
|
+
// Already verified and not expired → no new challenge needed
|
|
45
|
+
const verified = await db
|
|
46
|
+
.prepare(
|
|
47
|
+
"SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?",
|
|
48
|
+
)
|
|
49
|
+
.bind(domain, now)
|
|
50
|
+
.first<ChallengeRow>();
|
|
51
|
+
|
|
52
|
+
if (verified) {
|
|
53
|
+
return c.json(
|
|
54
|
+
{
|
|
55
|
+
error: "Domain already verified. Use `nkmc claim <domain> --verify` to renew your token.",
|
|
56
|
+
expiresAt: verified.expires_at,
|
|
57
|
+
},
|
|
58
|
+
409,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for existing unexpired pending challenge
|
|
63
|
+
const existing = await db
|
|
64
|
+
.prepare(
|
|
65
|
+
"SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?",
|
|
66
|
+
)
|
|
67
|
+
.bind(domain, now)
|
|
68
|
+
.first<ChallengeRow>();
|
|
69
|
+
|
|
70
|
+
if (existing) {
|
|
71
|
+
return c.json({
|
|
72
|
+
domain,
|
|
73
|
+
txtRecord: `_nkmc.${domain}`,
|
|
74
|
+
txtValue: `nkmc-verify=${existing.challenge_code}`,
|
|
75
|
+
expiresAt: existing.expires_at,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Generate new challenge (also covers expired verified domains)
|
|
80
|
+
const challengeCode = nanoid(32);
|
|
81
|
+
const expiresAt = now + SEVEN_DAYS_MS;
|
|
82
|
+
|
|
83
|
+
await db
|
|
84
|
+
.prepare(
|
|
85
|
+
`INSERT OR REPLACE INTO domain_challenges (domain, challenge_code, status, created_at, expires_at)
|
|
86
|
+
VALUES (?, ?, 'pending', ?, ?)`,
|
|
87
|
+
)
|
|
88
|
+
.bind(domain, challengeCode, now, expiresAt)
|
|
89
|
+
.run();
|
|
90
|
+
|
|
91
|
+
return c.json({
|
|
92
|
+
domain,
|
|
93
|
+
txtRecord: `_nkmc.${domain}`,
|
|
94
|
+
txtValue: `nkmc-verify=${challengeCode}`,
|
|
95
|
+
expiresAt,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Verify DNS record and issue publish token
|
|
100
|
+
app.post("/verify", async (c) => {
|
|
101
|
+
const body = await c.req.json<{ domain: string }>().catch(() => null);
|
|
102
|
+
const domain = body?.domain;
|
|
103
|
+
|
|
104
|
+
if (!domain || !isValidDomain(domain)) {
|
|
105
|
+
return c.json({ error: "Invalid or missing domain" }, 400);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
|
|
110
|
+
// 1. Already verified and not expired → issue new token without DNS check
|
|
111
|
+
const verified = await db
|
|
112
|
+
.prepare(
|
|
113
|
+
"SELECT * FROM domain_challenges WHERE domain = ? AND status = 'verified' AND expires_at > ?",
|
|
114
|
+
)
|
|
115
|
+
.bind(domain, now)
|
|
116
|
+
.first<ChallengeRow>();
|
|
117
|
+
|
|
118
|
+
if (verified) {
|
|
119
|
+
const publishToken = await signPublishToken(gatewayPrivateKey, domain);
|
|
120
|
+
return c.json({ ok: true, domain, publishToken });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2. Pending challenge → verify DNS
|
|
124
|
+
const challenge = await db
|
|
125
|
+
.prepare(
|
|
126
|
+
"SELECT * FROM domain_challenges WHERE domain = ? AND status = 'pending' AND expires_at > ?",
|
|
127
|
+
)
|
|
128
|
+
.bind(domain, now)
|
|
129
|
+
.first<ChallengeRow>();
|
|
130
|
+
|
|
131
|
+
if (!challenge) {
|
|
132
|
+
return c.json(
|
|
133
|
+
{ error: "No pending challenge found. Run `nkmc claim <domain>` first." },
|
|
134
|
+
404,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Query DNS TXT records
|
|
139
|
+
const expectedValue = `nkmc-verify=${challenge.challenge_code}`;
|
|
140
|
+
let txtRecords: string[];
|
|
141
|
+
try {
|
|
142
|
+
txtRecords = await queryDnsTxt(`_nkmc.${domain}`);
|
|
143
|
+
} catch {
|
|
144
|
+
return c.json(
|
|
145
|
+
{ error: "Failed to query DNS. Please try again later." },
|
|
146
|
+
502,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!txtRecords.includes(expectedValue)) {
|
|
151
|
+
return c.json(
|
|
152
|
+
{
|
|
153
|
+
error: `DNS TXT record not found. Expected TXT record on _nkmc.${domain} with value "${expectedValue}". DNS propagation can take up to 5 minutes.`,
|
|
154
|
+
},
|
|
155
|
+
422,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Mark as verified (ownership valid for 1 year)
|
|
160
|
+
await db
|
|
161
|
+
.prepare(
|
|
162
|
+
"UPDATE domain_challenges SET status = 'verified', verified_at = ?, expires_at = ? WHERE domain = ?",
|
|
163
|
+
)
|
|
164
|
+
.bind(now, now + ONE_YEAR_MS, domain)
|
|
165
|
+
.run();
|
|
166
|
+
|
|
167
|
+
// Sign publish token (24h expiry, scoped to domain)
|
|
168
|
+
const publishToken = await signPublishToken(gatewayPrivateKey, domain);
|
|
169
|
+
|
|
170
|
+
return c.json({ ok: true, domain, publishToken });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return app;
|
|
174
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AgentFs } from "@nkmc/agent-fs";
|
|
3
|
+
import type { PeerStore } from "../../federation/types.js";
|
|
4
|
+
import type { CredentialVault } from "../../credential/types.js";
|
|
5
|
+
import type { Env } from "../app.js";
|
|
6
|
+
|
|
7
|
+
export interface FederationRouteOptions {
|
|
8
|
+
peerStore: PeerStore;
|
|
9
|
+
vault: CredentialVault;
|
|
10
|
+
agentFs: AgentFs;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verify that the request comes from a known peer.
|
|
15
|
+
* Returns the peer ID or null if auth fails.
|
|
16
|
+
*/
|
|
17
|
+
async function authenticatePeer(
|
|
18
|
+
peerStore: PeerStore,
|
|
19
|
+
peerId: string | undefined,
|
|
20
|
+
authHeader: string | undefined,
|
|
21
|
+
): ReturnType<typeof peerStore.getPeer> {
|
|
22
|
+
if (!peerId || !authHeader) return null;
|
|
23
|
+
|
|
24
|
+
const peer = await peerStore.getPeer(peerId);
|
|
25
|
+
if (!peer || peer.status !== "active") return null;
|
|
26
|
+
|
|
27
|
+
const token = authHeader.replace(/^Bearer\s+/i, "");
|
|
28
|
+
if (token !== peer.sharedSecret) return null;
|
|
29
|
+
|
|
30
|
+
return peer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract the domain from a command string.
|
|
35
|
+
* e.g. "ls /api.example.com/data" => "api.example.com"
|
|
36
|
+
* "cat /github.com/repos" => "github.com"
|
|
37
|
+
*/
|
|
38
|
+
function extractDomainFromCommand(command: string): string | null {
|
|
39
|
+
const parts = command.trim().split(/\s+/);
|
|
40
|
+
if (parts.length < 2) return null;
|
|
41
|
+
const path = parts[1];
|
|
42
|
+
if (!path.startsWith("/")) return null;
|
|
43
|
+
const segments = path.slice(1).split("/");
|
|
44
|
+
return segments[0] || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function federationRoutes(options: FederationRouteOptions) {
|
|
48
|
+
const { peerStore, vault, agentFs } = options;
|
|
49
|
+
const app = new Hono<Env>();
|
|
50
|
+
|
|
51
|
+
// POST /federation/query — Check if we have credentials for a domain
|
|
52
|
+
app.post("/query", async (c) => {
|
|
53
|
+
const peerId = c.req.header("X-Peer-Id");
|
|
54
|
+
const authHeader = c.req.header("Authorization");
|
|
55
|
+
|
|
56
|
+
const peer = await authenticatePeer(peerStore, peerId, authHeader);
|
|
57
|
+
if (!peer) {
|
|
58
|
+
return c.json({ error: "Unauthorized peer" }, 403);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const body = await c.req.json<{ domain: string }>();
|
|
62
|
+
if (!body.domain) {
|
|
63
|
+
return c.json({ error: "Missing 'domain' field" }, 400);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Update last seen
|
|
67
|
+
await peerStore.updateLastSeen(peer.id, Date.now());
|
|
68
|
+
|
|
69
|
+
// Check if we have a credential for this domain
|
|
70
|
+
const credential = await vault.get(body.domain);
|
|
71
|
+
if (!credential) {
|
|
72
|
+
return c.json({ available: false });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check lending rule
|
|
76
|
+
const rule = await peerStore.getRule(body.domain);
|
|
77
|
+
if (!rule || !rule.allow) {
|
|
78
|
+
return c.json({ available: false });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if this peer is in the allowed list
|
|
82
|
+
if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
|
|
83
|
+
return c.json({ available: false });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return c.json({
|
|
87
|
+
available: true,
|
|
88
|
+
pricing: rule.pricing,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// POST /federation/exec — Execute a command on behalf of a peer
|
|
93
|
+
app.post("/exec", async (c) => {
|
|
94
|
+
const peerId = c.req.header("X-Peer-Id");
|
|
95
|
+
const authHeader = c.req.header("Authorization");
|
|
96
|
+
|
|
97
|
+
const peer = await authenticatePeer(peerStore, peerId, authHeader);
|
|
98
|
+
if (!peer) {
|
|
99
|
+
return c.json({ error: "Unauthorized peer" }, 403);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const body = await c.req.json<{ command: string; agentId: string }>();
|
|
103
|
+
if (!body.command) {
|
|
104
|
+
return c.json({ error: "Missing 'command' field" }, 400);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Update last seen
|
|
108
|
+
await peerStore.updateLastSeen(peer.id, Date.now());
|
|
109
|
+
|
|
110
|
+
// Extract domain from command and check lending rule
|
|
111
|
+
const domain = extractDomainFromCommand(body.command);
|
|
112
|
+
if (domain) {
|
|
113
|
+
const rule = await peerStore.getRule(domain);
|
|
114
|
+
|
|
115
|
+
if (!rule || !rule.allow) {
|
|
116
|
+
return c.json({ error: "Domain not available for lending" }, 403);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (rule.peers !== "*" && !rule.peers.includes(peer.id)) {
|
|
120
|
+
return c.json({ error: "Peer not in allowed list" }, 403);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check if payment is required
|
|
124
|
+
if (rule.pricing.mode !== "free") {
|
|
125
|
+
const paymentHeader = c.req.header("X-402-Payment");
|
|
126
|
+
if (!paymentHeader) {
|
|
127
|
+
return c.json({ error: "Payment required" }, 402);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Execute with synthetic agent context: peer:{peerId}:{agentId}
|
|
133
|
+
const syntheticAgentId = `peer:${peer.id}:${body.agentId}`;
|
|
134
|
+
const result = await agentFs.execute(body.command, ["agent"], {
|
|
135
|
+
id: syntheticAgentId,
|
|
136
|
+
roles: ["agent"],
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (!result.ok) {
|
|
140
|
+
return c.json({ ok: false, error: result.error.message }, 500);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return c.json({ ok: true, data: result.data });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// POST /federation/announce — Peer announces available domains
|
|
147
|
+
app.post("/announce", async (c) => {
|
|
148
|
+
const peerId = c.req.header("X-Peer-Id");
|
|
149
|
+
const authHeader = c.req.header("Authorization");
|
|
150
|
+
|
|
151
|
+
const peer = await authenticatePeer(peerStore, peerId, authHeader);
|
|
152
|
+
if (!peer) {
|
|
153
|
+
return c.json({ error: "Unauthorized peer" }, 403);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const body = await c.req.json<{ domains: string[] }>();
|
|
157
|
+
if (!Array.isArray(body.domains)) {
|
|
158
|
+
return c.json({ error: "Missing 'domains' field" }, 400);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Update peer's advertised domains and last seen
|
|
162
|
+
peer.advertisedDomains = body.domains;
|
|
163
|
+
await peerStore.putPeer(peer);
|
|
164
|
+
await peerStore.updateLastSeen(peer.id, Date.now());
|
|
165
|
+
|
|
166
|
+
return c.json({ ok: true });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return app;
|
|
170
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { AgentFs, FsOp } from "@nkmc/agent-fs";
|
|
3
|
+
import type { Env } from "../app.js";
|
|
4
|
+
|
|
5
|
+
export interface FsRouteOptions {
|
|
6
|
+
agentFs: AgentFs;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorToStatus(code: string): number {
|
|
10
|
+
switch (code) {
|
|
11
|
+
case "PARSE_ERROR":
|
|
12
|
+
case "INVALID_PATH":
|
|
13
|
+
return 400;
|
|
14
|
+
case "PERMISSION_DENIED":
|
|
15
|
+
return 403;
|
|
16
|
+
case "NOT_FOUND":
|
|
17
|
+
case "NO_MOUNT":
|
|
18
|
+
return 404;
|
|
19
|
+
default:
|
|
20
|
+
return 500;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fsRoutes(options: FsRouteOptions) {
|
|
25
|
+
const { agentFs } = options;
|
|
26
|
+
const app = new Hono<Env>();
|
|
27
|
+
|
|
28
|
+
// POST /execute — raw command execution
|
|
29
|
+
app.post("/execute", async (c) => {
|
|
30
|
+
const body = await c.req.json<{ command: string; roles?: string[] }>();
|
|
31
|
+
if (!body.command || typeof body.command !== "string") {
|
|
32
|
+
return c.json({ error: "Missing 'command' field" }, 400);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const agent = c.get("agent");
|
|
36
|
+
const roles = body.roles ?? agent?.roles;
|
|
37
|
+
const result = await agentFs.execute(body.command, roles, agent);
|
|
38
|
+
const status = result.ok ? 200 : errorToStatus(result.error.code);
|
|
39
|
+
|
|
40
|
+
return c.json(result, status as 200);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// /fs/* — REST-style access
|
|
44
|
+
app.all("/fs/*", async (c) => {
|
|
45
|
+
const fullPath = c.req.path;
|
|
46
|
+
const virtualPath = fullPath.slice(fullPath.indexOf("/fs") + 3) || "/";
|
|
47
|
+
const query = c.req.query("q");
|
|
48
|
+
const agent = c.get("agent");
|
|
49
|
+
const roles = agent?.roles;
|
|
50
|
+
|
|
51
|
+
let op: FsOp;
|
|
52
|
+
let data: unknown | undefined;
|
|
53
|
+
let pattern: string | undefined;
|
|
54
|
+
|
|
55
|
+
switch (c.req.method) {
|
|
56
|
+
case "GET":
|
|
57
|
+
if (query) {
|
|
58
|
+
op = "grep";
|
|
59
|
+
pattern = query;
|
|
60
|
+
} else if (virtualPath.endsWith("/")) {
|
|
61
|
+
op = "ls";
|
|
62
|
+
} else {
|
|
63
|
+
op = "cat";
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
case "POST":
|
|
67
|
+
case "PUT":
|
|
68
|
+
op = "write";
|
|
69
|
+
data = await c.req.json();
|
|
70
|
+
break;
|
|
71
|
+
case "DELETE":
|
|
72
|
+
op = "rm";
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
return c.json({ error: "Method not allowed" }, 405);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await agentFs.executeCommand(
|
|
79
|
+
{ op, path: virtualPath, data, pattern },
|
|
80
|
+
roles,
|
|
81
|
+
agent,
|
|
82
|
+
);
|
|
83
|
+
const status = result.ok ? 200 : errorToStatus(result.error.code);
|
|
84
|
+
|
|
85
|
+
return c.json(result, status as 200);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return app;
|
|
89
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../app.js";
|
|
3
|
+
import type { PeerStore, PeerGateway, LendingRule } from "../../federation/types.js";
|
|
4
|
+
|
|
5
|
+
export interface PeerRouteOptions {
|
|
6
|
+
peerStore: PeerStore;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function peerRoutes(options: PeerRouteOptions) {
|
|
10
|
+
const { peerStore } = options;
|
|
11
|
+
const app = new Hono<Env>();
|
|
12
|
+
|
|
13
|
+
// GET /peers — list all peers (don't expose sharedSecret)
|
|
14
|
+
app.get("/peers", async (c) => {
|
|
15
|
+
const peers = await peerStore.listPeers();
|
|
16
|
+
const safe = peers.map(({ sharedSecret: _, ...rest }) => rest);
|
|
17
|
+
return c.json({ peers: safe });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// PUT /peers/:id — create or update peer
|
|
21
|
+
app.put("/peers/:id", async (c) => {
|
|
22
|
+
const id = c.req.param("id");
|
|
23
|
+
const body = await c.req.json<{
|
|
24
|
+
name?: string;
|
|
25
|
+
url?: string;
|
|
26
|
+
sharedSecret?: string;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
if (!body.name || !body.url || !body.sharedSecret) {
|
|
30
|
+
return c.json({ error: "Missing required fields: name, url, sharedSecret" }, 400);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const existing = await peerStore.getPeer(id);
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
|
|
36
|
+
const peer: PeerGateway = {
|
|
37
|
+
id,
|
|
38
|
+
name: body.name,
|
|
39
|
+
url: body.url,
|
|
40
|
+
sharedSecret: body.sharedSecret,
|
|
41
|
+
status: "active",
|
|
42
|
+
advertisedDomains: existing?.advertisedDomains ?? [],
|
|
43
|
+
lastSeen: existing?.lastSeen ?? 0,
|
|
44
|
+
createdAt: existing?.createdAt ?? now,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await peerStore.putPeer(peer);
|
|
48
|
+
return c.json({ ok: true, id });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// DELETE /peers/:id — remove peer
|
|
52
|
+
app.delete("/peers/:id", async (c) => {
|
|
53
|
+
const id = c.req.param("id");
|
|
54
|
+
await peerStore.deletePeer(id);
|
|
55
|
+
return c.json({ ok: true, id });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// GET /rules — list all lending rules
|
|
59
|
+
app.get("/rules", async (c) => {
|
|
60
|
+
const rules = await peerStore.listRules();
|
|
61
|
+
return c.json({ rules });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// PUT /rules/:domain — create or update lending rule
|
|
65
|
+
app.put("/rules/:domain", async (c) => {
|
|
66
|
+
const domain = c.req.param("domain");
|
|
67
|
+
const body = await c.req.json<{
|
|
68
|
+
allow?: boolean;
|
|
69
|
+
peers?: string[] | "*";
|
|
70
|
+
pricing?: { mode: string; amount?: number };
|
|
71
|
+
rateLimit?: { requests: number; window: string };
|
|
72
|
+
}>();
|
|
73
|
+
|
|
74
|
+
if (body.allow === undefined) {
|
|
75
|
+
return c.json({ error: "Missing required field: allow" }, 400);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const existing = await peerStore.getRule(domain);
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
|
|
81
|
+
const rule: LendingRule = {
|
|
82
|
+
domain,
|
|
83
|
+
allow: body.allow,
|
|
84
|
+
peers: body.peers ?? existing?.peers ?? "*",
|
|
85
|
+
pricing: (body.pricing as LendingRule["pricing"]) ?? existing?.pricing ?? { mode: "free" },
|
|
86
|
+
rateLimit: body.rateLimit as LendingRule["rateLimit"],
|
|
87
|
+
createdAt: existing?.createdAt ?? now,
|
|
88
|
+
updatedAt: now,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
await peerStore.putRule(rule);
|
|
92
|
+
return c.json({ ok: true, domain });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// DELETE /rules/:domain — remove lending rule
|
|
96
|
+
app.delete("/rules/:domain", async (c) => {
|
|
97
|
+
const domain = c.req.param("domain");
|
|
98
|
+
await peerStore.deleteRule(domain);
|
|
99
|
+
return c.json({ ok: true, domain });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return app;
|
|
103
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { Env } from "../app.js";
|
|
3
|
+
import type { CredentialVault } from "../../credential/types.js";
|
|
4
|
+
import type { ToolRegistry } from "../../proxy/tool-registry.js";
|
|
5
|
+
|
|
6
|
+
export interface ExecResult {
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ProxyRouteOptions {
|
|
13
|
+
vault: CredentialVault;
|
|
14
|
+
toolRegistry: ToolRegistry;
|
|
15
|
+
exec: (tool: string, args: string[], env: Record<string, string>) => Promise<ExecResult>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function proxyRoutes(options: ProxyRouteOptions) {
|
|
19
|
+
const { vault, toolRegistry, exec } = options;
|
|
20
|
+
const app = new Hono<Env>();
|
|
21
|
+
|
|
22
|
+
// POST /exec — Execute a CLI tool with injected credentials
|
|
23
|
+
app.post("/exec", async (c) => {
|
|
24
|
+
const body = await c.req.json<{ tool: string; args?: string[] }>();
|
|
25
|
+
if (!body.tool || typeof body.tool !== "string") {
|
|
26
|
+
return c.json({ error: "Missing 'tool' field" }, 400);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toolDef = toolRegistry.get(body.tool);
|
|
30
|
+
if (!toolDef) {
|
|
31
|
+
return c.json({ error: `Unknown tool: ${body.tool}` }, 404);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const agent = c.get("agent");
|
|
35
|
+
const credential = await vault.get(toolDef.credentialDomain, agent.id);
|
|
36
|
+
if (!credential) {
|
|
37
|
+
return c.json({ error: `No credential for domain: ${toolDef.credentialDomain}` }, 401);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const env = toolRegistry.buildEnv(toolDef, credential.auth);
|
|
41
|
+
const args = body.args ?? [];
|
|
42
|
+
const result = await exec(body.tool, args, env);
|
|
43
|
+
|
|
44
|
+
return c.json(result);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// GET /tools — List available tools
|
|
48
|
+
app.get("/tools", (c) => {
|
|
49
|
+
const tools = toolRegistry.list().map((t) => ({
|
|
50
|
+
name: t.name,
|
|
51
|
+
credentialDomain: t.credentialDomain,
|
|
52
|
+
}));
|
|
53
|
+
return c.json({ tools });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return app;
|
|
57
|
+
}
|