@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 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
+ }