@nkmc/server 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/config.js +31 -0
- package/dist/index.js +338 -0
- package/dist/server.js +300 -0
- package/package.json +38 -0
- package/src/config.ts +44 -0
- package/src/exec.ts +44 -0
- package/src/index.ts +12 -0
- package/src/migrations/0001_init.sql +76 -0
- package/src/migrations/0002_auth_mode.sql +2 -0
- package/src/migrations.ts +120 -0
- package/src/server.ts +183 -0
- package/test/config.test.ts +123 -0
- package/test/e2e.test.ts +237 -0
- package/test/exec.test.ts +58 -0
- package/test/server.test.ts +120 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +19 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var DEFAULT_DATA_DIR = join(homedir(), ".nkmc", "server");
|
|
6
|
+
function loadConfig() {
|
|
7
|
+
const dataDir = process.env.NKMC_DATA_DIR ?? DEFAULT_DATA_DIR;
|
|
8
|
+
const configPath = join(dataDir, "config.json");
|
|
9
|
+
let fileConfig = {};
|
|
10
|
+
if (existsSync(configPath)) {
|
|
11
|
+
try {
|
|
12
|
+
fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function get(envKey, fileKey) {
|
|
17
|
+
return process.env[envKey] ?? fileConfig[fileKey];
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
port: parseInt(process.env.NKMC_PORT ?? fileConfig.port ?? "9090", 10),
|
|
21
|
+
host: process.env.NKMC_HOST ?? fileConfig.host ?? "0.0.0.0",
|
|
22
|
+
dataDir,
|
|
23
|
+
adminToken: get("NKMC_ADMIN_TOKEN", "adminToken"),
|
|
24
|
+
encryptionKey: get("NKMC_ENCRYPTION_KEY", "encryptionKey"),
|
|
25
|
+
gatewayPrivateKey: get("NKMC_GATEWAY_PRIVATE_KEY", "gatewayPrivateKey"),
|
|
26
|
+
gatewayPublicKey: get("NKMC_GATEWAY_PUBLIC_KEY", "gatewayPublicKey")
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
loadConfig
|
|
31
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var DEFAULT_DATA_DIR = join(homedir(), ".nkmc", "server");
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
const dataDir = process.env.NKMC_DATA_DIR ?? DEFAULT_DATA_DIR;
|
|
10
|
+
const configPath = join(dataDir, "config.json");
|
|
11
|
+
let fileConfig = {};
|
|
12
|
+
if (existsSync(configPath)) {
|
|
13
|
+
try {
|
|
14
|
+
fileConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function get(envKey, fileKey) {
|
|
19
|
+
return process.env[envKey] ?? fileConfig[fileKey];
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
port: parseInt(process.env.NKMC_PORT ?? fileConfig.port ?? "9090", 10),
|
|
23
|
+
host: process.env.NKMC_HOST ?? fileConfig.host ?? "0.0.0.0",
|
|
24
|
+
dataDir,
|
|
25
|
+
adminToken: get("NKMC_ADMIN_TOKEN", "adminToken"),
|
|
26
|
+
encryptionKey: get("NKMC_ENCRYPTION_KEY", "encryptionKey"),
|
|
27
|
+
gatewayPrivateKey: get("NKMC_GATEWAY_PRIVATE_KEY", "gatewayPrivateKey"),
|
|
28
|
+
gatewayPublicKey: get("NKMC_GATEWAY_PUBLIC_KEY", "gatewayPublicKey")
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/server.ts
|
|
33
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, chmodSync } from "fs";
|
|
34
|
+
import { join as join2 } from "path";
|
|
35
|
+
import { randomUUID, randomBytes } from "crypto";
|
|
36
|
+
import Database from "better-sqlite3";
|
|
37
|
+
import { serve } from "@hono/node-server";
|
|
38
|
+
import { generateKeyPair, exportJWK } from "jose";
|
|
39
|
+
import { createSqliteD1, D1RegistryStore, D1CredentialVault, D1PeerStore } from "@nkmc/gateway";
|
|
40
|
+
import { createGateway } from "@nkmc/gateway/http";
|
|
41
|
+
import { createDefaultToolRegistry } from "@nkmc/gateway/proxy";
|
|
42
|
+
import { nanoid } from "nanoid";
|
|
43
|
+
|
|
44
|
+
// src/exec.ts
|
|
45
|
+
import { spawn } from "child_process";
|
|
46
|
+
function createExec(opts) {
|
|
47
|
+
const timeout = opts?.timeout ?? 3e4;
|
|
48
|
+
return (tool, args, env) => {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
const child = spawn(tool, args, {
|
|
51
|
+
env: { ...process.env, ...env },
|
|
52
|
+
timeout,
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
54
|
+
});
|
|
55
|
+
let stdout = "";
|
|
56
|
+
let stderr = "";
|
|
57
|
+
child.stdout.on("data", (data) => {
|
|
58
|
+
stdout += data;
|
|
59
|
+
});
|
|
60
|
+
child.stderr.on("data", (data) => {
|
|
61
|
+
stderr += data;
|
|
62
|
+
});
|
|
63
|
+
child.on("close", (code) => {
|
|
64
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
65
|
+
});
|
|
66
|
+
child.on("error", (err) => {
|
|
67
|
+
resolve({ stdout: "", stderr: err.message, exitCode: 1 });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/migrations.ts
|
|
74
|
+
var migrations = [
|
|
75
|
+
{
|
|
76
|
+
name: "0001_init",
|
|
77
|
+
sql: `
|
|
78
|
+
-- Services registry
|
|
79
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
80
|
+
domain TEXT NOT NULL,
|
|
81
|
+
version TEXT NOT NULL,
|
|
82
|
+
name TEXT NOT NULL,
|
|
83
|
+
description TEXT,
|
|
84
|
+
roles TEXT,
|
|
85
|
+
skill_md TEXT NOT NULL,
|
|
86
|
+
endpoints TEXT,
|
|
87
|
+
is_first_party INTEGER DEFAULT 0,
|
|
88
|
+
status TEXT DEFAULT 'active',
|
|
89
|
+
is_default INTEGER DEFAULT 1,
|
|
90
|
+
source TEXT,
|
|
91
|
+
sunset_date INTEGER,
|
|
92
|
+
created_at INTEGER NOT NULL,
|
|
93
|
+
updated_at INTEGER NOT NULL,
|
|
94
|
+
PRIMARY KEY (domain, version)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_services_default ON services(domain, is_default);
|
|
98
|
+
|
|
99
|
+
-- Credentials vault
|
|
100
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
101
|
+
domain TEXT NOT NULL,
|
|
102
|
+
scope TEXT NOT NULL DEFAULT 'pool',
|
|
103
|
+
developer_id TEXT NOT NULL DEFAULT '',
|
|
104
|
+
auth_encrypted TEXT NOT NULL,
|
|
105
|
+
created_at INTEGER NOT NULL,
|
|
106
|
+
updated_at INTEGER NOT NULL,
|
|
107
|
+
PRIMARY KEY (domain, scope, developer_id)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
-- Metering records
|
|
111
|
+
CREATE TABLE IF NOT EXISTS meter_records (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
timestamp INTEGER NOT NULL,
|
|
114
|
+
domain TEXT NOT NULL,
|
|
115
|
+
version TEXT NOT NULL,
|
|
116
|
+
endpoint TEXT NOT NULL,
|
|
117
|
+
agent_id TEXT NOT NULL,
|
|
118
|
+
developer_id TEXT,
|
|
119
|
+
cost REAL NOT NULL,
|
|
120
|
+
currency TEXT NOT NULL DEFAULT 'USDC'
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_meter_domain ON meter_records(domain, timestamp);
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_meter_agent ON meter_records(agent_id, timestamp);
|
|
125
|
+
|
|
126
|
+
-- Domain verification challenges
|
|
127
|
+
CREATE TABLE IF NOT EXISTS domain_challenges (
|
|
128
|
+
domain TEXT PRIMARY KEY,
|
|
129
|
+
challenge_code TEXT NOT NULL,
|
|
130
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
131
|
+
created_at INTEGER NOT NULL,
|
|
132
|
+
verified_at INTEGER,
|
|
133
|
+
expires_at INTEGER NOT NULL
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
-- Developer-Agent binding
|
|
137
|
+
CREATE TABLE IF NOT EXISTS developer_agents (
|
|
138
|
+
user_id TEXT NOT NULL,
|
|
139
|
+
agent_id TEXT NOT NULL,
|
|
140
|
+
label TEXT,
|
|
141
|
+
created_at INTEGER NOT NULL,
|
|
142
|
+
PRIMARY KEY (user_id, agent_id)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_da_agent ON developer_agents(agent_id);
|
|
146
|
+
|
|
147
|
+
-- Claim tokens (for agent-first onboarding)
|
|
148
|
+
CREATE TABLE IF NOT EXISTS claim_tokens (
|
|
149
|
+
token TEXT PRIMARY KEY,
|
|
150
|
+
agent_id TEXT NOT NULL,
|
|
151
|
+
expires_at INTEGER NOT NULL,
|
|
152
|
+
created_at INTEGER NOT NULL
|
|
153
|
+
);
|
|
154
|
+
`
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "0002_auth_mode",
|
|
158
|
+
sql: `ALTER TABLE services ADD COLUMN auth_mode TEXT`
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: "0003_federation",
|
|
162
|
+
sql: `
|
|
163
|
+
-- Federation: peer gateways and lending rules
|
|
164
|
+
|
|
165
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
166
|
+
id TEXT PRIMARY KEY,
|
|
167
|
+
name TEXT NOT NULL,
|
|
168
|
+
url TEXT NOT NULL,
|
|
169
|
+
shared_secret TEXT NOT NULL,
|
|
170
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
171
|
+
advertised_domains TEXT NOT NULL DEFAULT '[]',
|
|
172
|
+
last_seen INTEGER NOT NULL,
|
|
173
|
+
created_at INTEGER NOT NULL
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE TABLE IF NOT EXISTS lending_rules (
|
|
177
|
+
domain TEXT PRIMARY KEY,
|
|
178
|
+
allow INTEGER NOT NULL DEFAULT 1,
|
|
179
|
+
peers TEXT NOT NULL DEFAULT '"*"',
|
|
180
|
+
pricing TEXT NOT NULL DEFAULT '{"mode":"free"}',
|
|
181
|
+
rate_limit TEXT,
|
|
182
|
+
created_at INTEGER NOT NULL,
|
|
183
|
+
updated_at INTEGER NOT NULL
|
|
184
|
+
);
|
|
185
|
+
`
|
|
186
|
+
}
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
// src/server.ts
|
|
190
|
+
async function startServer(options) {
|
|
191
|
+
const { config, silent } = options;
|
|
192
|
+
const log = silent ? () => {
|
|
193
|
+
} : console.log.bind(console);
|
|
194
|
+
mkdirSync(config.dataDir, { recursive: true });
|
|
195
|
+
const dbPath = join2(config.dataDir, "nkmc.db");
|
|
196
|
+
const sqlite = new Database(dbPath);
|
|
197
|
+
sqlite.pragma("journal_mode = WAL");
|
|
198
|
+
sqlite.pragma("foreign_keys = ON");
|
|
199
|
+
const db = createSqliteD1(sqlite);
|
|
200
|
+
sqlite.exec(`CREATE TABLE IF NOT EXISTS _nkmc_migrations (name TEXT PRIMARY KEY, applied_at INTEGER NOT NULL DEFAULT (unixepoch()))`);
|
|
201
|
+
for (const m of migrations) {
|
|
202
|
+
const applied = sqlite.prepare("SELECT 1 FROM _nkmc_migrations WHERE name = ?").get(m.name);
|
|
203
|
+
if (applied) continue;
|
|
204
|
+
try {
|
|
205
|
+
sqlite.exec(m.sql);
|
|
206
|
+
sqlite.prepare("INSERT OR IGNORE INTO _nkmc_migrations (name) VALUES (?)").run(m.name);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
if (msg.includes("duplicate column")) {
|
|
210
|
+
sqlite.prepare("INSERT OR IGNORE INTO _nkmc_migrations (name) VALUES (?)").run(m.name);
|
|
211
|
+
} else {
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
log("[nkmc] Migrations applied");
|
|
217
|
+
let privateKey;
|
|
218
|
+
let publicKey;
|
|
219
|
+
const keysPath = join2(config.dataDir, "keys.json");
|
|
220
|
+
if (config.gatewayPrivateKey && config.gatewayPublicKey) {
|
|
221
|
+
privateKey = JSON.parse(config.gatewayPrivateKey);
|
|
222
|
+
publicKey = JSON.parse(config.gatewayPublicKey);
|
|
223
|
+
log("[nkmc] Loaded gateway keys from config/env");
|
|
224
|
+
} else if (existsSync2(keysPath)) {
|
|
225
|
+
const keys = JSON.parse(readFileSync2(keysPath, "utf-8"));
|
|
226
|
+
privateKey = keys.privateKey;
|
|
227
|
+
publicKey = keys.publicKey;
|
|
228
|
+
log("[nkmc] Loaded gateway keys from", keysPath);
|
|
229
|
+
} else {
|
|
230
|
+
const pair = await generateKeyPair("EdDSA", { crv: "Ed25519", extractable: true });
|
|
231
|
+
privateKey = { ...await exportJWK(pair.privateKey), kty: "OKP", crv: "Ed25519" };
|
|
232
|
+
publicKey = { ...await exportJWK(pair.publicKey), kty: "OKP", crv: "Ed25519" };
|
|
233
|
+
const kid = nanoid(12);
|
|
234
|
+
privateKey.kid = kid;
|
|
235
|
+
publicKey.kid = kid;
|
|
236
|
+
writeFileSync(keysPath, JSON.stringify({ privateKey, publicKey }, null, 2), "utf-8");
|
|
237
|
+
chmodSync(keysPath, 384);
|
|
238
|
+
log("[nkmc] Generated new gateway key pair ->", keysPath);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
chmodSync(keysPath, 384);
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
const encKeyPath = join2(config.dataDir, "encryption.key");
|
|
245
|
+
let rawKeyB64;
|
|
246
|
+
if (config.encryptionKey) {
|
|
247
|
+
rawKeyB64 = config.encryptionKey;
|
|
248
|
+
log("[nkmc] Using encryption key from config/env");
|
|
249
|
+
} else if (existsSync2(encKeyPath)) {
|
|
250
|
+
rawKeyB64 = readFileSync2(encKeyPath, "utf-8").trim();
|
|
251
|
+
log("[nkmc] Loaded encryption key from", encKeyPath);
|
|
252
|
+
} else {
|
|
253
|
+
const buf = randomBytes(32);
|
|
254
|
+
rawKeyB64 = buf.toString("base64");
|
|
255
|
+
writeFileSync(encKeyPath, rawKeyB64, "utf-8");
|
|
256
|
+
chmodSync(encKeyPath, 384);
|
|
257
|
+
log("[nkmc] Generated new encryption key ->", encKeyPath);
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
chmodSync(encKeyPath, 384);
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
const rawKey = Uint8Array.from(atob(rawKeyB64), (c) => c.charCodeAt(0));
|
|
264
|
+
const encryptionKey = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, [
|
|
265
|
+
"encrypt",
|
|
266
|
+
"decrypt"
|
|
267
|
+
]);
|
|
268
|
+
const adminTokenPath = join2(config.dataDir, "admin-token");
|
|
269
|
+
let adminToken = config.adminToken;
|
|
270
|
+
if (!adminToken) {
|
|
271
|
+
if (existsSync2(adminTokenPath)) {
|
|
272
|
+
adminToken = readFileSync2(adminTokenPath, "utf-8").trim();
|
|
273
|
+
log("[nkmc] Loaded admin token from", adminTokenPath);
|
|
274
|
+
} else {
|
|
275
|
+
adminToken = randomUUID();
|
|
276
|
+
writeFileSync(adminTokenPath, adminToken, "utf-8");
|
|
277
|
+
chmodSync(adminTokenPath, 384);
|
|
278
|
+
log("[nkmc] Generated admin token ->", adminTokenPath);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
chmodSync(adminTokenPath, 384);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
const store = new D1RegistryStore(db);
|
|
286
|
+
const vault = new D1CredentialVault(db, encryptionKey);
|
|
287
|
+
const peerStore = new D1PeerStore(db);
|
|
288
|
+
const toolRegistry = createDefaultToolRegistry();
|
|
289
|
+
const exec = createExec();
|
|
290
|
+
const gateway = createGateway({
|
|
291
|
+
store,
|
|
292
|
+
vault,
|
|
293
|
+
db,
|
|
294
|
+
gatewayPrivateKey: privateKey,
|
|
295
|
+
gatewayPublicKey: publicKey,
|
|
296
|
+
adminToken,
|
|
297
|
+
peerStore,
|
|
298
|
+
proxy: { toolRegistry, exec }
|
|
299
|
+
});
|
|
300
|
+
return new Promise((resolve) => {
|
|
301
|
+
const server = serve(
|
|
302
|
+
{
|
|
303
|
+
fetch: gateway.fetch,
|
|
304
|
+
port: config.port,
|
|
305
|
+
hostname: config.host
|
|
306
|
+
},
|
|
307
|
+
(info) => {
|
|
308
|
+
log();
|
|
309
|
+
log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
310
|
+
log(" \u2502 nakamichi gateway (standalone) \u2502");
|
|
311
|
+
log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
312
|
+
log();
|
|
313
|
+
log(` Port: ${info.port}`);
|
|
314
|
+
log(` Host: ${config.host}`);
|
|
315
|
+
log(` Data dir: ${config.dataDir}`);
|
|
316
|
+
log(` Database: ${dbPath}`);
|
|
317
|
+
log();
|
|
318
|
+
resolve({
|
|
319
|
+
port: info.port,
|
|
320
|
+
close: () => {
|
|
321
|
+
server.close();
|
|
322
|
+
sqlite.close();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/index.ts
|
|
331
|
+
async function main() {
|
|
332
|
+
const config = loadConfig();
|
|
333
|
+
await startServer({ config });
|
|
334
|
+
}
|
|
335
|
+
main().catch((err) => {
|
|
336
|
+
console.error("[nkmc] Fatal error:", err);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
});
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { randomUUID, randomBytes } from "crypto";
|
|
5
|
+
import Database from "better-sqlite3";
|
|
6
|
+
import { serve } from "@hono/node-server";
|
|
7
|
+
import { generateKeyPair, exportJWK } from "jose";
|
|
8
|
+
import { createSqliteD1, D1RegistryStore, D1CredentialVault, D1PeerStore } from "@nkmc/gateway";
|
|
9
|
+
import { createGateway } from "@nkmc/gateway/http";
|
|
10
|
+
import { createDefaultToolRegistry } from "@nkmc/gateway/proxy";
|
|
11
|
+
import { nanoid } from "nanoid";
|
|
12
|
+
|
|
13
|
+
// src/exec.ts
|
|
14
|
+
import { spawn } from "child_process";
|
|
15
|
+
function createExec(opts) {
|
|
16
|
+
const timeout = opts?.timeout ?? 3e4;
|
|
17
|
+
return (tool, args, env) => {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn(tool, args, {
|
|
20
|
+
env: { ...process.env, ...env },
|
|
21
|
+
timeout,
|
|
22
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
23
|
+
});
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
child.stdout.on("data", (data) => {
|
|
27
|
+
stdout += data;
|
|
28
|
+
});
|
|
29
|
+
child.stderr.on("data", (data) => {
|
|
30
|
+
stderr += data;
|
|
31
|
+
});
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
resolve({ stdout, stderr, exitCode: code ?? 1 });
|
|
34
|
+
});
|
|
35
|
+
child.on("error", (err) => {
|
|
36
|
+
resolve({ stdout: "", stderr: err.message, exitCode: 1 });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/migrations.ts
|
|
43
|
+
var migrations = [
|
|
44
|
+
{
|
|
45
|
+
name: "0001_init",
|
|
46
|
+
sql: `
|
|
47
|
+
-- Services registry
|
|
48
|
+
CREATE TABLE IF NOT EXISTS services (
|
|
49
|
+
domain TEXT NOT NULL,
|
|
50
|
+
version TEXT NOT NULL,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
description TEXT,
|
|
53
|
+
roles TEXT,
|
|
54
|
+
skill_md TEXT NOT NULL,
|
|
55
|
+
endpoints TEXT,
|
|
56
|
+
is_first_party INTEGER DEFAULT 0,
|
|
57
|
+
status TEXT DEFAULT 'active',
|
|
58
|
+
is_default INTEGER DEFAULT 1,
|
|
59
|
+
source TEXT,
|
|
60
|
+
sunset_date INTEGER,
|
|
61
|
+
created_at INTEGER NOT NULL,
|
|
62
|
+
updated_at INTEGER NOT NULL,
|
|
63
|
+
PRIMARY KEY (domain, version)
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_services_default ON services(domain, is_default);
|
|
67
|
+
|
|
68
|
+
-- Credentials vault
|
|
69
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
70
|
+
domain TEXT NOT NULL,
|
|
71
|
+
scope TEXT NOT NULL DEFAULT 'pool',
|
|
72
|
+
developer_id TEXT NOT NULL DEFAULT '',
|
|
73
|
+
auth_encrypted TEXT NOT NULL,
|
|
74
|
+
created_at INTEGER NOT NULL,
|
|
75
|
+
updated_at INTEGER NOT NULL,
|
|
76
|
+
PRIMARY KEY (domain, scope, developer_id)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
-- Metering records
|
|
80
|
+
CREATE TABLE IF NOT EXISTS meter_records (
|
|
81
|
+
id TEXT PRIMARY KEY,
|
|
82
|
+
timestamp INTEGER NOT NULL,
|
|
83
|
+
domain TEXT NOT NULL,
|
|
84
|
+
version TEXT NOT NULL,
|
|
85
|
+
endpoint TEXT NOT NULL,
|
|
86
|
+
agent_id TEXT NOT NULL,
|
|
87
|
+
developer_id TEXT,
|
|
88
|
+
cost REAL NOT NULL,
|
|
89
|
+
currency TEXT NOT NULL DEFAULT 'USDC'
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_meter_domain ON meter_records(domain, timestamp);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_meter_agent ON meter_records(agent_id, timestamp);
|
|
94
|
+
|
|
95
|
+
-- Domain verification challenges
|
|
96
|
+
CREATE TABLE IF NOT EXISTS domain_challenges (
|
|
97
|
+
domain TEXT PRIMARY KEY,
|
|
98
|
+
challenge_code TEXT NOT NULL,
|
|
99
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
100
|
+
created_at INTEGER NOT NULL,
|
|
101
|
+
verified_at INTEGER,
|
|
102
|
+
expires_at INTEGER NOT NULL
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
-- Developer-Agent binding
|
|
106
|
+
CREATE TABLE IF NOT EXISTS developer_agents (
|
|
107
|
+
user_id TEXT NOT NULL,
|
|
108
|
+
agent_id TEXT NOT NULL,
|
|
109
|
+
label TEXT,
|
|
110
|
+
created_at INTEGER NOT NULL,
|
|
111
|
+
PRIMARY KEY (user_id, agent_id)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_da_agent ON developer_agents(agent_id);
|
|
115
|
+
|
|
116
|
+
-- Claim tokens (for agent-first onboarding)
|
|
117
|
+
CREATE TABLE IF NOT EXISTS claim_tokens (
|
|
118
|
+
token TEXT PRIMARY KEY,
|
|
119
|
+
agent_id TEXT NOT NULL,
|
|
120
|
+
expires_at INTEGER NOT NULL,
|
|
121
|
+
created_at INTEGER NOT NULL
|
|
122
|
+
);
|
|
123
|
+
`
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "0002_auth_mode",
|
|
127
|
+
sql: `ALTER TABLE services ADD COLUMN auth_mode TEXT`
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "0003_federation",
|
|
131
|
+
sql: `
|
|
132
|
+
-- Federation: peer gateways and lending rules
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
name TEXT NOT NULL,
|
|
137
|
+
url TEXT NOT NULL,
|
|
138
|
+
shared_secret TEXT NOT NULL,
|
|
139
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
140
|
+
advertised_domains TEXT NOT NULL DEFAULT '[]',
|
|
141
|
+
last_seen INTEGER NOT NULL,
|
|
142
|
+
created_at INTEGER NOT NULL
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE TABLE IF NOT EXISTS lending_rules (
|
|
146
|
+
domain TEXT PRIMARY KEY,
|
|
147
|
+
allow INTEGER NOT NULL DEFAULT 1,
|
|
148
|
+
peers TEXT NOT NULL DEFAULT '"*"',
|
|
149
|
+
pricing TEXT NOT NULL DEFAULT '{"mode":"free"}',
|
|
150
|
+
rate_limit TEXT,
|
|
151
|
+
created_at INTEGER NOT NULL,
|
|
152
|
+
updated_at INTEGER NOT NULL
|
|
153
|
+
);
|
|
154
|
+
`
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
// src/server.ts
|
|
159
|
+
async function startServer(options) {
|
|
160
|
+
const { config, silent } = options;
|
|
161
|
+
const log = silent ? () => {
|
|
162
|
+
} : console.log.bind(console);
|
|
163
|
+
mkdirSync(config.dataDir, { recursive: true });
|
|
164
|
+
const dbPath = join(config.dataDir, "nkmc.db");
|
|
165
|
+
const sqlite = new Database(dbPath);
|
|
166
|
+
sqlite.pragma("journal_mode = WAL");
|
|
167
|
+
sqlite.pragma("foreign_keys = ON");
|
|
168
|
+
const db = createSqliteD1(sqlite);
|
|
169
|
+
sqlite.exec(`CREATE TABLE IF NOT EXISTS _nkmc_migrations (name TEXT PRIMARY KEY, applied_at INTEGER NOT NULL DEFAULT (unixepoch()))`);
|
|
170
|
+
for (const m of migrations) {
|
|
171
|
+
const applied = sqlite.prepare("SELECT 1 FROM _nkmc_migrations WHERE name = ?").get(m.name);
|
|
172
|
+
if (applied) continue;
|
|
173
|
+
try {
|
|
174
|
+
sqlite.exec(m.sql);
|
|
175
|
+
sqlite.prepare("INSERT OR IGNORE INTO _nkmc_migrations (name) VALUES (?)").run(m.name);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
178
|
+
if (msg.includes("duplicate column")) {
|
|
179
|
+
sqlite.prepare("INSERT OR IGNORE INTO _nkmc_migrations (name) VALUES (?)").run(m.name);
|
|
180
|
+
} else {
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
log("[nkmc] Migrations applied");
|
|
186
|
+
let privateKey;
|
|
187
|
+
let publicKey;
|
|
188
|
+
const keysPath = join(config.dataDir, "keys.json");
|
|
189
|
+
if (config.gatewayPrivateKey && config.gatewayPublicKey) {
|
|
190
|
+
privateKey = JSON.parse(config.gatewayPrivateKey);
|
|
191
|
+
publicKey = JSON.parse(config.gatewayPublicKey);
|
|
192
|
+
log("[nkmc] Loaded gateway keys from config/env");
|
|
193
|
+
} else if (existsSync(keysPath)) {
|
|
194
|
+
const keys = JSON.parse(readFileSync(keysPath, "utf-8"));
|
|
195
|
+
privateKey = keys.privateKey;
|
|
196
|
+
publicKey = keys.publicKey;
|
|
197
|
+
log("[nkmc] Loaded gateway keys from", keysPath);
|
|
198
|
+
} else {
|
|
199
|
+
const pair = await generateKeyPair("EdDSA", { crv: "Ed25519", extractable: true });
|
|
200
|
+
privateKey = { ...await exportJWK(pair.privateKey), kty: "OKP", crv: "Ed25519" };
|
|
201
|
+
publicKey = { ...await exportJWK(pair.publicKey), kty: "OKP", crv: "Ed25519" };
|
|
202
|
+
const kid = nanoid(12);
|
|
203
|
+
privateKey.kid = kid;
|
|
204
|
+
publicKey.kid = kid;
|
|
205
|
+
writeFileSync(keysPath, JSON.stringify({ privateKey, publicKey }, null, 2), "utf-8");
|
|
206
|
+
chmodSync(keysPath, 384);
|
|
207
|
+
log("[nkmc] Generated new gateway key pair ->", keysPath);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
chmodSync(keysPath, 384);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
const encKeyPath = join(config.dataDir, "encryption.key");
|
|
214
|
+
let rawKeyB64;
|
|
215
|
+
if (config.encryptionKey) {
|
|
216
|
+
rawKeyB64 = config.encryptionKey;
|
|
217
|
+
log("[nkmc] Using encryption key from config/env");
|
|
218
|
+
} else if (existsSync(encKeyPath)) {
|
|
219
|
+
rawKeyB64 = readFileSync(encKeyPath, "utf-8").trim();
|
|
220
|
+
log("[nkmc] Loaded encryption key from", encKeyPath);
|
|
221
|
+
} else {
|
|
222
|
+
const buf = randomBytes(32);
|
|
223
|
+
rawKeyB64 = buf.toString("base64");
|
|
224
|
+
writeFileSync(encKeyPath, rawKeyB64, "utf-8");
|
|
225
|
+
chmodSync(encKeyPath, 384);
|
|
226
|
+
log("[nkmc] Generated new encryption key ->", encKeyPath);
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
chmodSync(encKeyPath, 384);
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
const rawKey = Uint8Array.from(atob(rawKeyB64), (c) => c.charCodeAt(0));
|
|
233
|
+
const encryptionKey = await crypto.subtle.importKey("raw", rawKey, "AES-GCM", false, [
|
|
234
|
+
"encrypt",
|
|
235
|
+
"decrypt"
|
|
236
|
+
]);
|
|
237
|
+
const adminTokenPath = join(config.dataDir, "admin-token");
|
|
238
|
+
let adminToken = config.adminToken;
|
|
239
|
+
if (!adminToken) {
|
|
240
|
+
if (existsSync(adminTokenPath)) {
|
|
241
|
+
adminToken = readFileSync(adminTokenPath, "utf-8").trim();
|
|
242
|
+
log("[nkmc] Loaded admin token from", adminTokenPath);
|
|
243
|
+
} else {
|
|
244
|
+
adminToken = randomUUID();
|
|
245
|
+
writeFileSync(adminTokenPath, adminToken, "utf-8");
|
|
246
|
+
chmodSync(adminTokenPath, 384);
|
|
247
|
+
log("[nkmc] Generated admin token ->", adminTokenPath);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
chmodSync(adminTokenPath, 384);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
const store = new D1RegistryStore(db);
|
|
255
|
+
const vault = new D1CredentialVault(db, encryptionKey);
|
|
256
|
+
const peerStore = new D1PeerStore(db);
|
|
257
|
+
const toolRegistry = createDefaultToolRegistry();
|
|
258
|
+
const exec = createExec();
|
|
259
|
+
const gateway = createGateway({
|
|
260
|
+
store,
|
|
261
|
+
vault,
|
|
262
|
+
db,
|
|
263
|
+
gatewayPrivateKey: privateKey,
|
|
264
|
+
gatewayPublicKey: publicKey,
|
|
265
|
+
adminToken,
|
|
266
|
+
peerStore,
|
|
267
|
+
proxy: { toolRegistry, exec }
|
|
268
|
+
});
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
const server = serve(
|
|
271
|
+
{
|
|
272
|
+
fetch: gateway.fetch,
|
|
273
|
+
port: config.port,
|
|
274
|
+
hostname: config.host
|
|
275
|
+
},
|
|
276
|
+
(info) => {
|
|
277
|
+
log();
|
|
278
|
+
log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
279
|
+
log(" \u2502 nakamichi gateway (standalone) \u2502");
|
|
280
|
+
log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
281
|
+
log();
|
|
282
|
+
log(` Port: ${info.port}`);
|
|
283
|
+
log(` Host: ${config.host}`);
|
|
284
|
+
log(` Data dir: ${config.dataDir}`);
|
|
285
|
+
log(` Database: ${dbPath}`);
|
|
286
|
+
log();
|
|
287
|
+
resolve({
|
|
288
|
+
port: info.port,
|
|
289
|
+
close: () => {
|
|
290
|
+
server.close();
|
|
291
|
+
sqlite.close();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
export {
|
|
299
|
+
startServer
|
|
300
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nkmc/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nkmc-server": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/server.d.ts",
|
|
11
|
+
"import": "./dist/server.js"
|
|
12
|
+
},
|
|
13
|
+
"./config": {
|
|
14
|
+
"types": "./dist/config.d.ts",
|
|
15
|
+
"import": "./dist/config.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"start": "node dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@nkmc/gateway": "workspace:*",
|
|
25
|
+
"@nkmc/agent-fs": "workspace:*",
|
|
26
|
+
"@nkmc/core": "^0.1.1",
|
|
27
|
+
"@hono/node-server": "^1.14.1",
|
|
28
|
+
"better-sqlite3": "^12.6.2",
|
|
29
|
+
"hono": "^4.11.9",
|
|
30
|
+
"jose": "^6.1.3",
|
|
31
|
+
"nanoid": "^5.1.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
35
|
+
"tsup": "^8.4.0",
|
|
36
|
+
"typescript": "^5.8.2"
|
|
37
|
+
}
|
|
38
|
+
}
|