@wolpertingerlabs/drawlatch 1.0.0-alpha.3 → 1.0.0-alpha.35
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/CONNECTIONS.md +3 -0
- package/README.md +393 -465
- package/bin/drawlatch.js +1151 -49
- package/dist/auth/auth.d.ts +10 -0
- package/dist/auth/auth.js +146 -0
- package/dist/auth/env-writer.d.ts +6 -0
- package/dist/auth/env-writer.js +52 -0
- package/dist/auth/password.d.ts +8 -0
- package/dist/auth/password.js +28 -0
- package/dist/auth/sessions.d.ts +13 -0
- package/dist/auth/sessions.js +85 -0
- package/dist/cli/generate-keys.d.ts +4 -4
- package/dist/cli/generate-keys.js +30 -25
- package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
- package/dist/connections/{devin.json → ai/devin.json} +7 -1
- package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
- package/dist/connections/{openai.json → ai/openai.json} +7 -1
- package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
- package/dist/connections/developer-tools/github.json +138 -0
- package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
- package/dist/connections/developer-tools/linear.json +75 -0
- package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
- package/dist/connections/messaging/agentmail.json +21 -0
- package/dist/connections/messaging/discord-bot.json +114 -0
- package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
- package/dist/connections/messaging/slack.json +75 -0
- package/dist/connections/messaging/telegram.json +66 -0
- package/dist/connections/{google.json → productivity/google.json} +7 -1
- package/dist/connections/productivity/notion.json +75 -0
- package/dist/connections/productivity/stripe.json +74 -0
- package/dist/connections/productivity/trello.json +113 -0
- package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
- package/dist/connections/social-media/mastodon.json +65 -0
- package/dist/connections/social-media/reddit.json +80 -0
- package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
- package/dist/connections/social-media/x.json +67 -0
- package/dist/mcp/server.js +544 -31
- package/dist/remote/admin-mutations.d.ts +43 -0
- package/dist/remote/admin-mutations.js +321 -0
- package/dist/remote/admin-types.d.ts +153 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +317 -0
- package/dist/remote/caller-bootstrap.d.ts +121 -0
- package/dist/remote/caller-bootstrap.js +270 -0
- package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
- package/dist/remote/ingestors/base-ingestor.js +27 -6
- package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
- package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
- package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
- package/dist/remote/ingestors/e2e/setup.js +147 -0
- package/dist/remote/ingestors/manager.d.ts +110 -10
- package/dist/remote/ingestors/manager.js +449 -42
- package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
- package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
- package/dist/remote/ingestors/registry.d.ts +2 -2
- package/dist/remote/ingestors/registry.js +2 -2
- package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
- package/dist/remote/ingestors/slack/socket-mode.js +28 -6
- package/dist/remote/ingestors/types.d.ts +25 -0
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
- package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
- package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
- package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
- package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
- package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
- package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
- package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
- package/dist/remote/server.d.ts +25 -33
- package/dist/remote/server.js +596 -187
- package/dist/remote/tool-dispatch.d.ts +84 -0
- package/dist/remote/tool-dispatch.js +910 -0
- package/dist/remote/triggers/rule-engine.d.ts +40 -0
- package/dist/remote/triggers/rule-engine.js +228 -0
- package/dist/remote/triggers/types.d.ts +69 -0
- package/dist/remote/triggers/types.js +10 -0
- package/dist/remote/tunnel-state.d.ts +14 -0
- package/dist/remote/tunnel-state.js +20 -0
- package/dist/remote/tunnel.d.ts +13 -0
- package/dist/remote/tunnel.js +33 -0
- package/dist/shared/config.d.ts +104 -25
- package/dist/shared/config.js +49 -36
- package/dist/shared/connections.d.ts +21 -5
- package/dist/shared/connections.js +56 -14
- package/dist/shared/crypto/index.d.ts +1 -0
- package/dist/shared/crypto/index.js +1 -0
- package/dist/shared/crypto/key-manager.d.ts +81 -0
- package/dist/shared/crypto/key-manager.js +174 -0
- package/dist/shared/env-utils.d.ts +42 -0
- package/dist/shared/env-utils.js +150 -0
- package/dist/shared/listener-config.d.ts +157 -0
- package/dist/shared/listener-config.js +10 -0
- package/dist/shared/migrations.d.ts +40 -0
- package/dist/shared/migrations.js +122 -0
- package/dist/shared/protocol/caller-bundle-crypto.d.ts +37 -0
- package/dist/shared/protocol/caller-bundle-crypto.js +62 -0
- package/dist/shared/protocol/caller-bundle.d.ts +75 -0
- package/dist/shared/protocol/caller-bundle.js +24 -0
- package/dist/shared/protocol/handshake.js +14 -1
- package/dist/shared/protocol/index.d.ts +2 -0
- package/dist/shared/protocol/index.js +2 -0
- package/dist/shared/protocol/sync-client.d.ts +52 -0
- package/dist/shared/protocol/sync-client.js +99 -0
- package/dist/shared/protocol/sync.d.ts +71 -0
- package/dist/shared/protocol/sync.js +176 -0
- package/frontend/dist/assets/index-BdCSPSZK.js +282 -0
- package/frontend/dist/assets/index-BmK26bY2.css +1 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +65 -13
- package/dist/connections/discord-bot.json +0 -24
- package/dist/connections/github.json +0 -25
- package/dist/connections/linear.json +0 -29
- package/dist/connections/mastodon.json +0 -25
- package/dist/connections/notion.json +0 -33
- package/dist/connections/reddit.json +0 -28
- package/dist/connections/slack.json +0 -23
- package/dist/connections/stripe.json +0 -25
- package/dist/connections/telegram.json +0 -26
- package/dist/connections/trello.json +0 -25
- package/dist/connections/x.json +0 -27
package/bin/drawlatch.js
CHANGED
|
@@ -28,15 +28,32 @@ const SERVER_ENTRY = join(PKG_ROOT, "dist/remote/server.js");
|
|
|
28
28
|
const GENERATE_KEYS_ENTRY = join(PKG_ROOT, "dist/cli/generate-keys.js");
|
|
29
29
|
|
|
30
30
|
// Import config helpers from compiled drawlatch code
|
|
31
|
-
const { getConfigDir, getEnvFilePath, loadRemoteConfig } = await import(
|
|
31
|
+
const { getConfigDir, getEnvFilePath, getKeysDir, getCallerKeysDir, getServerKeysDir, getProxyConfigPath, getRemoteConfigPath, loadRemoteConfig } = await import(
|
|
32
32
|
join(PKG_ROOT, "dist/shared/config.js")
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
// Import crypto helpers for init command
|
|
36
|
+
const { generateKeyBundle, saveKeyBundle, extractPublicKeys, fingerprint, loadKeyBundle, loadPublicKeys } = await import(
|
|
37
|
+
join(PKG_ROOT, "dist/shared/crypto/index.js")
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Import connection template helpers
|
|
41
|
+
const { listConnectionTemplates } = await import(
|
|
42
|
+
join(PKG_ROOT, "dist/shared/connections.js")
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Import env utils
|
|
46
|
+
const { isSecretSetForCaller, loadEnvFile: loadEnvFileVars } = await import(
|
|
47
|
+
join(PKG_ROOT, "dist/shared/env-utils.js")
|
|
48
|
+
);
|
|
49
|
+
|
|
35
50
|
const CONFIG_DIR = getConfigDir();
|
|
36
51
|
const ENV_FILE = getEnvFilePath();
|
|
37
52
|
const PID_FILE = join(CONFIG_DIR, "drawlatch.pid");
|
|
38
53
|
const LOG_DIR = join(CONFIG_DIR, "logs");
|
|
39
54
|
const LOG_FILE = join(LOG_DIR, "drawlatch.log");
|
|
55
|
+
const VERSION_CACHE_FILE = join(CONFIG_DIR, "latest-version.json");
|
|
56
|
+
const VERSION_CHECK_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
40
57
|
|
|
41
58
|
// Read version from package.json
|
|
42
59
|
const pkgJson = JSON.parse(
|
|
@@ -61,8 +78,17 @@ try {
|
|
|
61
78
|
port: { type: "string" },
|
|
62
79
|
host: { type: "string" },
|
|
63
80
|
lines: { type: "string", short: "n", default: "50" },
|
|
64
|
-
follow: { type: "boolean", default:
|
|
81
|
+
follow: { type: "boolean", default: false },
|
|
65
82
|
path: { type: "boolean", default: false },
|
|
83
|
+
full: { type: "boolean", default: false },
|
|
84
|
+
requests: { type: "boolean", default: false },
|
|
85
|
+
ttl: { type: "string", default: "300" },
|
|
86
|
+
name: { type: "string" },
|
|
87
|
+
connections: { type: "string" },
|
|
88
|
+
endpoint: { type: "string" },
|
|
89
|
+
passphrase: { type: "boolean", default: false },
|
|
90
|
+
output: { type: "string", short: "o" },
|
|
91
|
+
into: { type: "string" },
|
|
66
92
|
},
|
|
67
93
|
strict: false,
|
|
68
94
|
allowPositionals: true,
|
|
@@ -72,6 +98,11 @@ try {
|
|
|
72
98
|
process.exit(1);
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
// ── Kick off version check early (non-blocking) ──────────────────
|
|
102
|
+
const updateCheckPromise = (subcommand === null || subcommand === "help" || (values.help && !subcommand))
|
|
103
|
+
? checkForUpdate()
|
|
104
|
+
: Promise.resolve(null);
|
|
105
|
+
|
|
75
106
|
// ── Dispatch ──────────────────────────────────────────────────────
|
|
76
107
|
if (values.version) {
|
|
77
108
|
console.log(VERSION);
|
|
@@ -79,6 +110,11 @@ if (values.version) {
|
|
|
79
110
|
}
|
|
80
111
|
if (values.help && !subcommand) {
|
|
81
112
|
printHelp();
|
|
113
|
+
const latestVersion = await Promise.race([
|
|
114
|
+
updateCheckPromise,
|
|
115
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
116
|
+
]);
|
|
117
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
82
118
|
process.exit(0);
|
|
83
119
|
}
|
|
84
120
|
|
|
@@ -86,6 +122,13 @@ switch (subcommand) {
|
|
|
86
122
|
case null:
|
|
87
123
|
await cmdDefault();
|
|
88
124
|
break;
|
|
125
|
+
case "init":
|
|
126
|
+
if (values.help) {
|
|
127
|
+
printInitHelp();
|
|
128
|
+
} else {
|
|
129
|
+
await cmdInit();
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
89
132
|
case "start":
|
|
90
133
|
if (values.help) {
|
|
91
134
|
printStartHelp();
|
|
@@ -135,8 +178,53 @@ switch (subcommand) {
|
|
|
135
178
|
await cmdGenerateKeys();
|
|
136
179
|
}
|
|
137
180
|
break;
|
|
181
|
+
case "doctor":
|
|
182
|
+
if (values.help) {
|
|
183
|
+
printDoctorHelp();
|
|
184
|
+
} else {
|
|
185
|
+
await cmdDoctor();
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case "issue-caller":
|
|
189
|
+
if (values.help) {
|
|
190
|
+
printIssueCallerHelp();
|
|
191
|
+
} else {
|
|
192
|
+
await cmdIssueCaller();
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case "sync":
|
|
196
|
+
// Parked: kept in-tree as a high-assurance escape hatch, undocumented in
|
|
197
|
+
// the main help (superseded by `issue-caller`).
|
|
198
|
+
if (values.help) {
|
|
199
|
+
printSyncHelp();
|
|
200
|
+
} else {
|
|
201
|
+
await cmdSync();
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
case "set-password":
|
|
205
|
+
case "change-password":
|
|
206
|
+
if (values.help) {
|
|
207
|
+
printSetPasswordHelp(subcommand);
|
|
208
|
+
} else {
|
|
209
|
+
await cmdSetPassword();
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case "watch":
|
|
213
|
+
if (values.help) {
|
|
214
|
+
printWatchHelp();
|
|
215
|
+
} else {
|
|
216
|
+
await cmdWatch();
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
138
219
|
case "help":
|
|
139
220
|
printHelp();
|
|
221
|
+
{
|
|
222
|
+
const latestVersion = await Promise.race([
|
|
223
|
+
updateCheckPromise,
|
|
224
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
225
|
+
]);
|
|
226
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
227
|
+
}
|
|
140
228
|
break;
|
|
141
229
|
default:
|
|
142
230
|
console.error(`Unknown command: ${subcommand}\n`);
|
|
@@ -154,8 +242,98 @@ async function cmdDefault() {
|
|
|
154
242
|
console.log("Drawlatch remote server is not running.\n");
|
|
155
243
|
printHelp();
|
|
156
244
|
}
|
|
245
|
+
// Show update notice only if the check already resolved (don't block on network)
|
|
246
|
+
const latestVersion = await Promise.race([
|
|
247
|
+
updateCheckPromise,
|
|
248
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
249
|
+
]);
|
|
250
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
157
251
|
}
|
|
158
252
|
|
|
253
|
+
async function cmdInit() {
|
|
254
|
+
console.log(`\nDrawlatch Setup`);
|
|
255
|
+
console.log(`===============\n`);
|
|
256
|
+
|
|
257
|
+
const steps = [];
|
|
258
|
+
|
|
259
|
+
// Step 1: Ensure config directory
|
|
260
|
+
ensureConfigDir();
|
|
261
|
+
steps.push(`Config directory: ${CONFIG_DIR}`);
|
|
262
|
+
|
|
263
|
+
// Step 2: Generate server keypair
|
|
264
|
+
const serverKeysDir = getServerKeysDir();
|
|
265
|
+
if (existsSync(join(serverKeysDir, "signing.key.pem"))) {
|
|
266
|
+
const existing = loadKeyBundle(serverKeysDir);
|
|
267
|
+
const fp = fingerprint(extractPublicKeys(existing));
|
|
268
|
+
steps.push(`Server keys: already exist (${fp})`);
|
|
269
|
+
} else {
|
|
270
|
+
const bundle = generateKeyBundle();
|
|
271
|
+
saveKeyBundle(bundle, serverKeysDir);
|
|
272
|
+
const fp = fingerprint(extractPublicKeys(bundle));
|
|
273
|
+
steps.push(`Server keys: CREATED (${fp})`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 3: Scaffold proxy.config.json
|
|
277
|
+
const proxyConfigPath = getProxyConfigPath();
|
|
278
|
+
if (existsSync(proxyConfigPath)) {
|
|
279
|
+
steps.push(`Proxy config: already exists`);
|
|
280
|
+
} else {
|
|
281
|
+
const proxyConfig = {
|
|
282
|
+
remoteUrl: "http://127.0.0.1:9999",
|
|
283
|
+
connectTimeout: 10000,
|
|
284
|
+
requestTimeout: 300000,
|
|
285
|
+
};
|
|
286
|
+
writeFileSync(proxyConfigPath, JSON.stringify(proxyConfig, null, 2) + "\n", { mode: 0o600 });
|
|
287
|
+
steps.push(`Proxy config: CREATED`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Step 4: Scaffold remote.config.json
|
|
291
|
+
const remoteConfigPath = getRemoteConfigPath();
|
|
292
|
+
if (existsSync(remoteConfigPath)) {
|
|
293
|
+
steps.push(`Remote config: already exists`);
|
|
294
|
+
} else {
|
|
295
|
+
const remoteConfig = {
|
|
296
|
+
host: "0.0.0.0",
|
|
297
|
+
port: 9999,
|
|
298
|
+
rateLimitPerMinute: 60,
|
|
299
|
+
callers: {},
|
|
300
|
+
};
|
|
301
|
+
writeFileSync(remoteConfigPath, JSON.stringify(remoteConfig, null, 2) + "\n", { mode: 0o600 });
|
|
302
|
+
steps.push(`Remote config: CREATED`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Step 5: Scaffold .env file
|
|
306
|
+
if (existsSync(ENV_FILE)) {
|
|
307
|
+
steps.push(`.env file: already exists`);
|
|
308
|
+
} else {
|
|
309
|
+
const envLines = [
|
|
310
|
+
"# Drawlatch environment secrets",
|
|
311
|
+
"# Set tokens for your enabled connections",
|
|
312
|
+
"# Secrets are prefixed per caller (e.g., DEFAULT_GITHUB_TOKEN)",
|
|
313
|
+
"",
|
|
314
|
+
];
|
|
315
|
+
writeFileSync(ENV_FILE, envLines.join("\n") + "\n", { mode: 0o600 });
|
|
316
|
+
steps.push(`.env file: CREATED`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Print summary
|
|
320
|
+
for (const step of steps) {
|
|
321
|
+
console.log(` ${step}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log(`\nSetup complete! Next steps:\n`);
|
|
325
|
+
console.log(` 1. Set the dashboard password:`);
|
|
326
|
+
console.log(` drawlatch set-password\n`);
|
|
327
|
+
console.log(` 2. Start the remote server:`);
|
|
328
|
+
console.log(` drawlatch start\n`);
|
|
329
|
+
console.log(` 3. Issue a caller credential bundle (or use the dashboard Callers page):`);
|
|
330
|
+
console.log(` drawlatch issue-caller callboard-prod -o callboard-prod.drawlatch-caller.json\n`);
|
|
331
|
+
console.log(` 4. Verify your setup:`);
|
|
332
|
+
console.log(` drawlatch doctor\n`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
159
337
|
async function cmdStart() {
|
|
160
338
|
if (values.foreground) return cmdStartForeground();
|
|
161
339
|
|
|
@@ -211,10 +389,16 @@ async function cmdStart() {
|
|
|
211
389
|
}
|
|
212
390
|
console.log(` Logs: drawlatch logs`);
|
|
213
391
|
} else {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
392
|
+
const stillAlive = isProcessAlive(child.pid);
|
|
393
|
+
if (stillAlive) {
|
|
394
|
+
console.log(
|
|
395
|
+
`\nServer started (PID ${child.pid}) but health check did not pass within 5s.`,
|
|
396
|
+
);
|
|
397
|
+
console.log(` The server process is still running — it may need more time.`);
|
|
398
|
+
} else {
|
|
399
|
+
console.log(`\nServer process (PID ${child.pid}) exited before becoming healthy.`);
|
|
400
|
+
cleanPidFile();
|
|
401
|
+
}
|
|
218
402
|
await diagnoseStartFailure();
|
|
219
403
|
}
|
|
220
404
|
}
|
|
@@ -305,6 +489,10 @@ async function cmdStatus() {
|
|
|
305
489
|
console.log("Drawlatch remote server is running.");
|
|
306
490
|
console.log(` PID: ${pid}`);
|
|
307
491
|
console.log(` Listening: ${host}:${port}`);
|
|
492
|
+
console.log(` Dashboard: http://${connectHost(host)}:${port}/`);
|
|
493
|
+
console.log(
|
|
494
|
+
` Password: ${isPasswordConfigured() ? "configured" : "not set (run: drawlatch set-password)"}`,
|
|
495
|
+
);
|
|
308
496
|
console.log(` Uptime: ${uptime}`);
|
|
309
497
|
console.log(
|
|
310
498
|
` Health: ${healthData ? "healthy" : "unhealthy (not responding)"}`,
|
|
@@ -326,39 +514,154 @@ async function cmdLogs() {
|
|
|
326
514
|
|
|
327
515
|
const lines = parseInt(values.lines, 10) || 50;
|
|
328
516
|
const follow = values.follow;
|
|
517
|
+
const showRequests = values.requests;
|
|
329
518
|
|
|
330
519
|
const tailArgs = follow
|
|
331
520
|
? ["-n", String(lines), "-f", LOG_FILE]
|
|
332
521
|
: ["-n", String(lines), LOG_FILE];
|
|
333
522
|
|
|
334
|
-
|
|
523
|
+
if (showRequests) {
|
|
524
|
+
// Show everything — pipe directly to stdout
|
|
525
|
+
const tail = spawn("tail", tailArgs, { stdio: "inherit" });
|
|
335
526
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
527
|
+
tail.on("error", () => logsFallback(lines, follow, null));
|
|
528
|
+
|
|
529
|
+
process.on("SIGINT", () => {
|
|
530
|
+
tail.kill();
|
|
531
|
+
process.exit(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await new Promise((res) => tail.on("close", res));
|
|
535
|
+
} else {
|
|
536
|
+
// Filter out [audit] lines (request/response noise from poll_events etc.)
|
|
537
|
+
const tail = spawn("tail", tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
|
|
538
|
+
const grepProc = spawn("grep", ["-v", "^\\[audit\\]"], {
|
|
539
|
+
stdio: [tail.stdout, "inherit", "inherit"],
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
tail.on("error", () => logsFallback(lines, follow, "[audit]"));
|
|
543
|
+
|
|
544
|
+
process.on("SIGINT", () => {
|
|
545
|
+
tail.kill();
|
|
546
|
+
grepProc.kill();
|
|
547
|
+
process.exit(0);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await new Promise((res) => grepProc.on("close", res));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function logsFallback(lines, follow, filterPrefix) {
|
|
555
|
+
try {
|
|
556
|
+
const content = readFileSync(LOG_FILE, "utf-8");
|
|
557
|
+
let allLines = content.split("\n");
|
|
558
|
+
if (filterPrefix) {
|
|
559
|
+
allLines = allLines.filter((l) => !l.startsWith(filterPrefix));
|
|
351
560
|
}
|
|
352
|
-
|
|
561
|
+
const lastLines = allLines.slice(-lines).join("\n");
|
|
562
|
+
console.log(lastLines);
|
|
563
|
+
if (follow) {
|
|
564
|
+
console.log(
|
|
565
|
+
"\n(Live following not available \u2014 'tail' command not found)",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.error(`Error reading log file: ${err.message}`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function cmdWatch() {
|
|
575
|
+
const config = loadRemoteConfig();
|
|
576
|
+
const port = config.port;
|
|
577
|
+
const host = config.host;
|
|
578
|
+
|
|
579
|
+
// Verify the server is running
|
|
580
|
+
const healthy = await healthCheck(host, port);
|
|
581
|
+
if (!healthy) {
|
|
582
|
+
console.error("Remote server is not running. Start it first:");
|
|
583
|
+
console.error(" drawlatch start");
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const showFull = values.full;
|
|
588
|
+
const sourceFilter = positionals[0] || null;
|
|
353
589
|
|
|
354
|
-
|
|
590
|
+
console.log("Watching for events" + (sourceFilter ? ` from "${sourceFilter}"` : "") + "...");
|
|
591
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
592
|
+
|
|
593
|
+
const controller = new AbortController();
|
|
355
594
|
process.on("SIGINT", () => {
|
|
356
|
-
|
|
595
|
+
controller.abort();
|
|
357
596
|
process.exit(0);
|
|
358
597
|
});
|
|
359
598
|
|
|
360
|
-
|
|
361
|
-
|
|
599
|
+
try {
|
|
600
|
+
const res = await fetch(
|
|
601
|
+
`http://${connectHost(host)}:${port}/events/stream`,
|
|
602
|
+
{ signal: controller.signal },
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
if (!res.ok) {
|
|
606
|
+
console.error(`Failed to connect to event stream: HTTP ${res.status}`);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const decoder = new TextDecoder();
|
|
611
|
+
let buffer = "";
|
|
612
|
+
|
|
613
|
+
for await (const chunk of res.body) {
|
|
614
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
615
|
+
|
|
616
|
+
// Parse SSE lines
|
|
617
|
+
let newlineIdx;
|
|
618
|
+
while ((newlineIdx = buffer.indexOf("\n\n")) !== -1) {
|
|
619
|
+
const message = buffer.slice(0, newlineIdx);
|
|
620
|
+
buffer = buffer.slice(newlineIdx + 2);
|
|
621
|
+
|
|
622
|
+
for (const line of message.split("\n")) {
|
|
623
|
+
if (!line.startsWith("data: ")) continue;
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
const event = JSON.parse(line.slice(6));
|
|
627
|
+
|
|
628
|
+
// Apply source filter if set
|
|
629
|
+
if (sourceFilter && event.source !== sourceFilter) continue;
|
|
630
|
+
|
|
631
|
+
const time = new Date(event.receivedAt).toLocaleTimeString();
|
|
632
|
+
const source = event.source;
|
|
633
|
+
const instance = event.instanceId ? `:${event.instanceId}` : "";
|
|
634
|
+
const caller = event.callerAlias || "?";
|
|
635
|
+
const eventType = event.eventType;
|
|
636
|
+
|
|
637
|
+
const dataStr = JSON.stringify(event.data);
|
|
638
|
+
|
|
639
|
+
if (showFull) {
|
|
640
|
+
console.log(
|
|
641
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m`
|
|
642
|
+
);
|
|
643
|
+
console.log(dataStr);
|
|
644
|
+
console.log("");
|
|
645
|
+
} else {
|
|
646
|
+
const preview =
|
|
647
|
+
dataStr.length > 100
|
|
648
|
+
? dataStr.slice(0, 100) + "…"
|
|
649
|
+
: dataStr;
|
|
650
|
+
console.log(
|
|
651
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m ${preview}`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
// Skip malformed SSE data
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
if (err.name === "AbortError") return;
|
|
662
|
+
console.error(`Event stream error: ${err.message}`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
362
665
|
}
|
|
363
666
|
|
|
364
667
|
function cmdConfig() {
|
|
@@ -378,14 +681,51 @@ function cmdConfig() {
|
|
|
378
681
|
console.log(` Host: ${config.host}`);
|
|
379
682
|
console.log(` Port: ${config.port}`);
|
|
380
683
|
console.log(` Rate limit: ${config.rateLimitPerMinute} req/min`);
|
|
381
|
-
console.log(`
|
|
684
|
+
console.log(` Server keys dir: ${getServerKeysDir()}`);
|
|
685
|
+
|
|
686
|
+
// Show key fingerprints if keys exist
|
|
687
|
+
const serverKeysPath = getServerKeysDir();
|
|
688
|
+
if (existsSync(join(serverKeysPath, "signing.key.pem"))) {
|
|
689
|
+
try {
|
|
690
|
+
const serverKeys = loadKeyBundle(serverKeysPath);
|
|
691
|
+
console.log(` Server key fp: ${fingerprint(extractPublicKeys(serverKeys))}`);
|
|
692
|
+
} catch { /* skip */ }
|
|
693
|
+
}
|
|
382
694
|
|
|
383
695
|
const callerEntries = Object.entries(config.callers || {});
|
|
384
696
|
console.log(` Callers: ${callerEntries.length}`);
|
|
697
|
+
|
|
698
|
+
// Build template map for secret status checking
|
|
699
|
+
const templates = listConnectionTemplates();
|
|
700
|
+
const templateMap = new Map(templates.map((t) => [t.alias, t]));
|
|
701
|
+
|
|
702
|
+
// Load .env vars into process.env so isSecretSetForCaller works.
|
|
703
|
+
// Force-set to override empty shell env vars.
|
|
704
|
+
const envVars = loadEnvFileVars();
|
|
705
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
706
|
+
process.env[k] = v;
|
|
707
|
+
}
|
|
708
|
+
|
|
385
709
|
for (const [alias, caller] of callerEntries) {
|
|
386
710
|
console.log(
|
|
387
711
|
` ${alias}: ${caller.connections ? caller.connections.length : 0} connection(s)`,
|
|
388
712
|
);
|
|
713
|
+
for (const connName of caller.connections || []) {
|
|
714
|
+
const tpl = templateMap.get(connName);
|
|
715
|
+
if (!tpl) {
|
|
716
|
+
console.log(` ${connName} (custom connector)`);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const secretStatuses = tpl.requiredSecrets.map((s) => {
|
|
720
|
+
const set = isSecretSetForCaller(s, alias, caller.env);
|
|
721
|
+
return `${s}: ${set ? "SET" : "NOT SET"}`;
|
|
722
|
+
});
|
|
723
|
+
if (secretStatuses.length > 0) {
|
|
724
|
+
console.log(` ${connName}: ${secretStatuses.join(", ")}`);
|
|
725
|
+
} else {
|
|
726
|
+
console.log(` ${connName}: (no secrets required)`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
389
729
|
}
|
|
390
730
|
|
|
391
731
|
console.log(
|
|
@@ -413,6 +753,512 @@ async function cmdGenerateKeys() {
|
|
|
413
753
|
process.exit(child.exitCode ?? 0);
|
|
414
754
|
}
|
|
415
755
|
|
|
756
|
+
|
|
757
|
+
async function cmdDoctor() {
|
|
758
|
+
console.log(`\nDrawlatch Setup Check`);
|
|
759
|
+
console.log(`=====================\n`);
|
|
760
|
+
|
|
761
|
+
let passed = 0;
|
|
762
|
+
let failed = 0;
|
|
763
|
+
|
|
764
|
+
function check(label, ok, fix) {
|
|
765
|
+
const padded = label.padEnd(50);
|
|
766
|
+
if (ok) {
|
|
767
|
+
console.log(` ${padded} PASS`);
|
|
768
|
+
passed++;
|
|
769
|
+
} else {
|
|
770
|
+
console.log(` ${padded} FAIL`);
|
|
771
|
+
if (fix) console.log(` -> ${fix}`);
|
|
772
|
+
failed++;
|
|
773
|
+
}
|
|
774
|
+
return ok;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Load .env for secret checks
|
|
778
|
+
const envVars = loadEnvFileVars();
|
|
779
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
780
|
+
process.env[k] = v;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 1. Config directory
|
|
784
|
+
check("Config directory", existsSync(CONFIG_DIR), "Run: drawlatch init");
|
|
785
|
+
|
|
786
|
+
// 2. Remote config
|
|
787
|
+
const remoteConfigPath = getRemoteConfigPath();
|
|
788
|
+
const hasRemoteConfig = existsSync(remoteConfigPath);
|
|
789
|
+
check("Remote config (remote.config.json)", hasRemoteConfig, "Run: drawlatch init");
|
|
790
|
+
|
|
791
|
+
// 3. Proxy config
|
|
792
|
+
const proxyConfigPath = getProxyConfigPath();
|
|
793
|
+
const hasProxyConfig = existsSync(proxyConfigPath);
|
|
794
|
+
check("Proxy config (proxy.config.json)", hasProxyConfig, "Run: drawlatch init");
|
|
795
|
+
|
|
796
|
+
// 4. Server keys
|
|
797
|
+
const serverKeysPath = getServerKeysDir();
|
|
798
|
+
const hasServerKeys = existsSync(join(serverKeysPath, "signing.key.pem")) &&
|
|
799
|
+
existsSync(join(serverKeysPath, "exchange.key.pem")) &&
|
|
800
|
+
existsSync(join(serverKeysPath, "signing.pub.pem")) &&
|
|
801
|
+
existsSync(join(serverKeysPath, "exchange.pub.pem"));
|
|
802
|
+
check("Server keys (keys/server/)", hasServerKeys, "Run: drawlatch generate-keys server");
|
|
803
|
+
|
|
804
|
+
if (hasServerKeys) {
|
|
805
|
+
try {
|
|
806
|
+
const sk = loadKeyBundle(serverKeysPath);
|
|
807
|
+
console.log(` Fingerprint: ${fingerprint(extractPublicKeys(sk))}`);
|
|
808
|
+
} catch { /* skip */ }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 5. Caller keys — check each caller from config
|
|
812
|
+
if (hasRemoteConfig) {
|
|
813
|
+
try {
|
|
814
|
+
const config = loadRemoteConfig();
|
|
815
|
+
const callersDir = getCallerKeysDir();
|
|
816
|
+
for (const [alias] of Object.entries(config.callers)) {
|
|
817
|
+
const callerDir = join(callersDir, alias);
|
|
818
|
+
const hasCallerKeys = existsSync(join(callerDir, "signing.pub.pem")) &&
|
|
819
|
+
existsSync(join(callerDir, "exchange.pub.pem"));
|
|
820
|
+
check(`Caller keys for "${alias}" (keys/callers/${alias}/)`, hasCallerKeys, `Run: drawlatch generate-keys caller ${alias} (or sync)`);
|
|
821
|
+
|
|
822
|
+
if (hasCallerKeys) {
|
|
823
|
+
try {
|
|
824
|
+
const pk = loadPublicKeys(callerDir);
|
|
825
|
+
console.log(` Fingerprint: ${fingerprint(pk)}`);
|
|
826
|
+
} catch { /* skip */ }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} catch { /* skip */ }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// 8. .env file
|
|
833
|
+
check("Environment file (.env)", existsSync(ENV_FILE), "Run: drawlatch init");
|
|
834
|
+
|
|
835
|
+
// 9. Required secrets for each caller's connections
|
|
836
|
+
if (hasRemoteConfig) {
|
|
837
|
+
try {
|
|
838
|
+
const config = loadRemoteConfig();
|
|
839
|
+
const templates = listConnectionTemplates();
|
|
840
|
+
const templateMap = new Map(templates.map((t) => [t.alias, t]));
|
|
841
|
+
|
|
842
|
+
for (const [callerAlias, caller] of Object.entries(config.callers)) {
|
|
843
|
+
for (const connName of caller.connections) {
|
|
844
|
+
const tpl = templateMap.get(connName);
|
|
845
|
+
if (!tpl) continue;
|
|
846
|
+
for (const secret of tpl.requiredSecrets) {
|
|
847
|
+
const isSet = isSecretSetForCaller(secret, callerAlias, caller.env);
|
|
848
|
+
check(` ${callerAlias}/${connName}: ${secret}`, isSet, `Set ${secret} in ${ENV_FILE}`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} catch { /* skip */ }
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// 10. Remote server health check
|
|
856
|
+
if (hasRemoteConfig) {
|
|
857
|
+
try {
|
|
858
|
+
const config = loadRemoteConfig();
|
|
859
|
+
const healthy = await healthCheck(config.host, config.port);
|
|
860
|
+
check(`Remote server health (${config.host}:${config.port})`, healthy, "Run: drawlatch start");
|
|
861
|
+
} catch {
|
|
862
|
+
check("Remote server health", false, "Run: drawlatch start");
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Summary
|
|
867
|
+
const total = passed + failed;
|
|
868
|
+
console.log(`\nResult: ${passed}/${total} checks passed`);
|
|
869
|
+
if (failed > 0) {
|
|
870
|
+
console.log(`Fix the ${failed} failing check(s) above and re-run: drawlatch doctor`);
|
|
871
|
+
} else {
|
|
872
|
+
console.log(`All checks passed! Your setup is ready.`);
|
|
873
|
+
}
|
|
874
|
+
console.log();
|
|
875
|
+
|
|
876
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function cmdSync() {
|
|
880
|
+
const config = loadRemoteConfig();
|
|
881
|
+
const port = config.port;
|
|
882
|
+
const host = config.host;
|
|
883
|
+
const ttlSeconds = parseInt(values.ttl, 10) || 300;
|
|
884
|
+
const ttlMs = ttlSeconds * 1000;
|
|
885
|
+
|
|
886
|
+
// Check that the server is running
|
|
887
|
+
const healthy = await healthCheck(host, port);
|
|
888
|
+
if (!healthy) {
|
|
889
|
+
console.error(
|
|
890
|
+
"Error: Drawlatch remote server is not running.\n" +
|
|
891
|
+
" Start it first: drawlatch start\n",
|
|
892
|
+
);
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Import sync helpers from compiled drawlatch code
|
|
897
|
+
const { generateSyncCode, generateSyncEncryptionKey } = await import(
|
|
898
|
+
join(PKG_ROOT, "dist/shared/protocol/sync.js")
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
const inviteCode = generateSyncCode();
|
|
902
|
+
const encryptionKey = generateSyncEncryptionKey();
|
|
903
|
+
|
|
904
|
+
console.log("\nStarting key exchange...\n");
|
|
905
|
+
console.log(` Invite code: ${inviteCode}`);
|
|
906
|
+
console.log(` Encryption key: ${encryptionKey}`);
|
|
907
|
+
console.log(
|
|
908
|
+
"\nGive both values to the callboard operator.",
|
|
909
|
+
);
|
|
910
|
+
console.log("They will provide a confirm code.\n");
|
|
911
|
+
|
|
912
|
+
// Prompt for confirm code
|
|
913
|
+
const confirmCode = await promptInput("Enter confirm code: ");
|
|
914
|
+
if (!confirmCode.trim()) {
|
|
915
|
+
console.error("No confirm code entered. Aborting.");
|
|
916
|
+
process.exit(1);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Open the sync session on the running server
|
|
920
|
+
try {
|
|
921
|
+
const controller = new AbortController();
|
|
922
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
923
|
+
const res = await fetch(`http://${host}:${port}/sync/listen`, {
|
|
924
|
+
method: "POST",
|
|
925
|
+
headers: { "Content-Type": "application/json" },
|
|
926
|
+
body: JSON.stringify({
|
|
927
|
+
inviteCode,
|
|
928
|
+
confirmCode: confirmCode.trim(),
|
|
929
|
+
encryptionKey,
|
|
930
|
+
ttlMs,
|
|
931
|
+
}),
|
|
932
|
+
signal: controller.signal,
|
|
933
|
+
});
|
|
934
|
+
clearTimeout(timeout);
|
|
935
|
+
|
|
936
|
+
if (!res.ok) {
|
|
937
|
+
const body = await res.json().catch(() => ({}));
|
|
938
|
+
console.error(`Failed to open sync session: ${body.error || res.statusText}`);
|
|
939
|
+
process.exit(1);
|
|
940
|
+
}
|
|
941
|
+
} catch (err) {
|
|
942
|
+
console.error(`Failed to reach server: ${err.message}`);
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
console.log(`\nWaiting for sync from callboard (timeout: ${ttlSeconds}s)...`);
|
|
947
|
+
|
|
948
|
+
// Poll for completion
|
|
949
|
+
const start = Date.now();
|
|
950
|
+
while (Date.now() - start < ttlMs) {
|
|
951
|
+
await sleep(1000);
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
const controller = new AbortController();
|
|
955
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
956
|
+
const res = await fetch(`http://${host}:${port}/sync/status`, {
|
|
957
|
+
signal: controller.signal,
|
|
958
|
+
});
|
|
959
|
+
clearTimeout(timeout);
|
|
960
|
+
|
|
961
|
+
if (!res.ok) continue;
|
|
962
|
+
const status = await res.json();
|
|
963
|
+
|
|
964
|
+
if (status.completed) {
|
|
965
|
+
console.log(`\nSync complete!`);
|
|
966
|
+
console.log(` Caller alias: ${status.callerAlias}`);
|
|
967
|
+
console.log(` Fingerprint: ${status.fingerprint}`);
|
|
968
|
+
console.log(
|
|
969
|
+
` Keys saved to: ${join(CONFIG_DIR, "keys", "callers", status.callerAlias)}/`,
|
|
970
|
+
);
|
|
971
|
+
console.log(`\nThe caller can now connect (no server restart needed).`);
|
|
972
|
+
console.log(`\nTo grant API access, add connections in ${join(CONFIG_DIR, "remote.config.json")}:`);
|
|
973
|
+
console.log(` "callers": {`);
|
|
974
|
+
console.log(` "${status.callerAlias}": {`);
|
|
975
|
+
console.log(` "connections": ["github", "slack", ...]`);
|
|
976
|
+
console.log(` }`);
|
|
977
|
+
console.log(` }`);
|
|
978
|
+
console.log(`\nThen set the required secrets in ${ENV_FILE}`);
|
|
979
|
+
console.log();
|
|
980
|
+
process.exit(0);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (!status.active) {
|
|
984
|
+
console.error("\nSync session expired or was cancelled.");
|
|
985
|
+
process.exit(1);
|
|
986
|
+
}
|
|
987
|
+
} catch {
|
|
988
|
+
// Transient fetch error, keep polling
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
console.error("\nSync session timed out.");
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ── issue-caller ──────────────────────────────────────────────────────────
|
|
997
|
+
//
|
|
998
|
+
// Mint a caller credential bundle using the same primitive as the dashboard
|
|
999
|
+
// "Issue credentials" action. Two delivery modes:
|
|
1000
|
+
// - default / -o file : emit the {alias}.drawlatch-caller.json bundle (the
|
|
1001
|
+
// caller PRIVATE key is in the file; drawlatch keeps only the public key).
|
|
1002
|
+
// - --into <keysDir> : write the UNPACKED key files straight into a
|
|
1003
|
+
// co-located callboard's keys dir (same-host convenience).
|
|
1004
|
+
async function cmdIssueCaller() {
|
|
1005
|
+
const alias = positionals[0];
|
|
1006
|
+
if (!alias) {
|
|
1007
|
+
console.error("Error: caller alias is required.\n Usage: drawlatch issue-caller <alias> [options]");
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
ensureConfigDir();
|
|
1012
|
+
|
|
1013
|
+
const { issueCallerBundle, issueLocalCaller, CALLER_ALIAS_REGEX } = await import(
|
|
1014
|
+
join(PKG_ROOT, "dist/remote/caller-bootstrap.js")
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
if (!CALLER_ALIAS_REGEX.test(alias)) {
|
|
1018
|
+
console.error(
|
|
1019
|
+
"Error: invalid alias. Use letters, numbers, dashes or underscores (must start alphanumeric).",
|
|
1020
|
+
);
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const connections = values.connections
|
|
1025
|
+
? values.connections.split(",").map((s) => s.trim()).filter(Boolean)
|
|
1026
|
+
: undefined;
|
|
1027
|
+
const name = values.name;
|
|
1028
|
+
|
|
1029
|
+
try {
|
|
1030
|
+
// ── Mode 2: write unpacked key files directly into callboard's keys dir ──
|
|
1031
|
+
if (values.into) {
|
|
1032
|
+
const keysDir = resolve(values.into);
|
|
1033
|
+
const res = issueLocalCaller({
|
|
1034
|
+
alias,
|
|
1035
|
+
keysDir,
|
|
1036
|
+
...(connections ? { connections } : {}),
|
|
1037
|
+
...(name ? { name } : {}),
|
|
1038
|
+
});
|
|
1039
|
+
console.log(`\nIssued local caller "${res.alias}" (write-to-path).`);
|
|
1040
|
+
console.log(` Key files: ${res.callerKeysDir}/`);
|
|
1041
|
+
console.log(` Server keys: ${join(keysDir, "server")}/`);
|
|
1042
|
+
console.log(` Fingerprint: ${res.fingerprint}`);
|
|
1043
|
+
console.log(` Connections: ${res.connections.join(", ") || "(none)"}`);
|
|
1044
|
+
notifyDaemonReloadNeeded();
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ── Mode 1: emit a bundle (file or stdout) ──────────────────────────────
|
|
1049
|
+
const config = loadRemoteConfig();
|
|
1050
|
+
const endpoint = values.endpoint || `http://${connectHost(config.host)}:${config.port}`;
|
|
1051
|
+
|
|
1052
|
+
let passphrase;
|
|
1053
|
+
if (values.passphrase) {
|
|
1054
|
+
let confirm;
|
|
1055
|
+
if (process.stdin.isTTY) {
|
|
1056
|
+
passphrase = await promptPasswordTTY("Passphrase to protect private keys: ");
|
|
1057
|
+
confirm = await promptPasswordTTY("Confirm passphrase: ");
|
|
1058
|
+
} else {
|
|
1059
|
+
// Piped input — read both lines from one readline interface.
|
|
1060
|
+
const { createInterface } = await import("node:readline");
|
|
1061
|
+
const rl = createInterface({ input: process.stdin });
|
|
1062
|
+
[passphrase, confirm] = await readTwoLines(rl);
|
|
1063
|
+
rl.close();
|
|
1064
|
+
}
|
|
1065
|
+
if (!passphrase) {
|
|
1066
|
+
console.error("Error: passphrase cannot be empty.");
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
if (passphrase !== confirm) {
|
|
1070
|
+
console.error("Error: passphrases do not match.");
|
|
1071
|
+
process.exit(1);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const bundle = issueCallerBundle({
|
|
1076
|
+
alias,
|
|
1077
|
+
endpointUrl: endpoint,
|
|
1078
|
+
...(connections ? { connections } : {}),
|
|
1079
|
+
...(name ? { name } : {}),
|
|
1080
|
+
...(passphrase ? { passphrase } : {}),
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const json = JSON.stringify(bundle, null, 2) + "\n";
|
|
1084
|
+
|
|
1085
|
+
if (values.output) {
|
|
1086
|
+
const outPath = resolve(values.output);
|
|
1087
|
+
writeFileSync(outPath, json, { mode: 0o600 });
|
|
1088
|
+
console.error(`\nWrote credential bundle to ${outPath}`);
|
|
1089
|
+
console.error(` Caller: ${bundle.callerAlias}`);
|
|
1090
|
+
console.error(` Fingerprint: ${bundle.fingerprint}`);
|
|
1091
|
+
console.error(` Endpoint: ${bundle.endpointUrl}`);
|
|
1092
|
+
console.error(
|
|
1093
|
+
bundle.encryption
|
|
1094
|
+
? ` Private keys: passphrase-protected (share the passphrase out-of-band).`
|
|
1095
|
+
: ` Private keys: PLAINTEXT in the file — protect it (or use --passphrase).`,
|
|
1096
|
+
);
|
|
1097
|
+
console.error(` This file contains the caller PRIVATE key and won't be re-issued.`);
|
|
1098
|
+
notifyDaemonReloadNeeded();
|
|
1099
|
+
} else {
|
|
1100
|
+
// Bundle (with private keys) to stdout; notes to stderr.
|
|
1101
|
+
process.stdout.write(json);
|
|
1102
|
+
console.error(`\n# Caller "${bundle.callerAlias}" issued (fingerprint ${bundle.fingerprint}).`);
|
|
1103
|
+
if (!bundle.encryption) {
|
|
1104
|
+
console.error(`# WARNING: private keys are PLAINTEXT above. Use --passphrase or -o to a 0600 file.`);
|
|
1105
|
+
}
|
|
1106
|
+
notifyDaemonReloadNeeded();
|
|
1107
|
+
}
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
console.error(`Error: ${err.message}`);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/** If the daemon is running, it won't authorize a freshly-issued caller until
|
|
1115
|
+
* it reloads its peer set — tell the user to restart it. (The dashboard issue
|
|
1116
|
+
* flow live-reloads peers; the offline CLI path cannot.) */
|
|
1117
|
+
function notifyDaemonReloadNeeded() {
|
|
1118
|
+
if (readPid()) {
|
|
1119
|
+
console.error(`\nNote: restart the daemon so it authorizes the new caller — drawlatch restart`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async function promptInput(prompt) {
|
|
1124
|
+
const { createInterface } = await import("node:readline");
|
|
1125
|
+
return new Promise((resolve) => {
|
|
1126
|
+
const rl = createInterface({
|
|
1127
|
+
input: process.stdin,
|
|
1128
|
+
output: process.stdout,
|
|
1129
|
+
});
|
|
1130
|
+
rl.question(prompt, (answer) => {
|
|
1131
|
+
rl.close();
|
|
1132
|
+
resolve(answer);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// ── set-password / change-password ────────────────────────────────
|
|
1138
|
+
//
|
|
1139
|
+
// Sets the dashboard/admin password. The scrypt hash + salt are written to
|
|
1140
|
+
// ~/.drawlatch/.env (AUTH_PASSWORD_HASH / AUTH_PASSWORD_SALT) using the same
|
|
1141
|
+
// env-writer the daemon's change-password handler uses, so both stay in sync.
|
|
1142
|
+
async function cmdSetPassword() {
|
|
1143
|
+
ensureConfigDir();
|
|
1144
|
+
|
|
1145
|
+
const { hashPassword, generateSalt } = await import(
|
|
1146
|
+
join(PKG_ROOT, "dist/auth/password.js")
|
|
1147
|
+
);
|
|
1148
|
+
const { updateEnvFile } = await import(
|
|
1149
|
+
join(PKG_ROOT, "dist/auth/env-writer.js")
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
let password, confirm;
|
|
1153
|
+
if (process.stdin.isTTY) {
|
|
1154
|
+
password = await promptPasswordTTY("Enter new password: ");
|
|
1155
|
+
if (!password) {
|
|
1156
|
+
console.error("Error: password cannot be empty.");
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
if (password.length < 8) {
|
|
1160
|
+
console.error("Error: password must be at least 8 characters.");
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
confirm = await promptPasswordTTY("Confirm new password: ");
|
|
1164
|
+
} else {
|
|
1165
|
+
// Piped input — read both lines from a single readline interface so
|
|
1166
|
+
// closing it doesn't break the second prompt.
|
|
1167
|
+
const { createInterface } = await import("node:readline");
|
|
1168
|
+
const rl = createInterface({ input: process.stdin });
|
|
1169
|
+
[password, confirm] = await readTwoLines(rl);
|
|
1170
|
+
rl.close();
|
|
1171
|
+
if (!password) {
|
|
1172
|
+
console.error("Error: password cannot be empty.");
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
if (password.length < 8) {
|
|
1176
|
+
console.error("Error: password must be at least 8 characters.");
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (password !== confirm) {
|
|
1182
|
+
console.error("Error: passwords do not match.");
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const salt = generateSalt();
|
|
1187
|
+
const hash = await hashPassword(password, salt);
|
|
1188
|
+
|
|
1189
|
+
updateEnvFile(
|
|
1190
|
+
{ AUTH_PASSWORD_HASH: hash, AUTH_PASSWORD_SALT: salt },
|
|
1191
|
+
["AUTH_PASSWORD"], // strip any plaintext leftover, just in case
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
console.log("\nPassword set successfully.");
|
|
1195
|
+
console.log(` Stored in: ${ENV_FILE}`);
|
|
1196
|
+
|
|
1197
|
+
const pid = readPid();
|
|
1198
|
+
if (pid) {
|
|
1199
|
+
console.log(" Restart the server to apply: drawlatch restart");
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function readTwoLines(rl) {
|
|
1204
|
+
return new Promise((resolveP) => {
|
|
1205
|
+
const lines = [];
|
|
1206
|
+
const onLine = (line) => {
|
|
1207
|
+
lines.push(line);
|
|
1208
|
+
if (lines.length >= 2) {
|
|
1209
|
+
rl.removeListener("line", onLine);
|
|
1210
|
+
resolveP(lines);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
rl.on("line", onLine);
|
|
1214
|
+
rl.once("close", () => {
|
|
1215
|
+
while (lines.length < 2) lines.push("");
|
|
1216
|
+
resolveP(lines);
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function promptPasswordTTY(prompt) {
|
|
1222
|
+
return new Promise((resolveP) => {
|
|
1223
|
+
process.stdout.write(prompt);
|
|
1224
|
+
const stdin = process.stdin;
|
|
1225
|
+
stdin.setRawMode(true);
|
|
1226
|
+
stdin.resume();
|
|
1227
|
+
stdin.setEncoding("utf8");
|
|
1228
|
+
let password = "";
|
|
1229
|
+
const onData = (ch) => {
|
|
1230
|
+
ch = ch.toString();
|
|
1231
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
1232
|
+
stdin.setRawMode(false);
|
|
1233
|
+
stdin.pause();
|
|
1234
|
+
stdin.removeListener("data", onData);
|
|
1235
|
+
process.stdout.write("\n");
|
|
1236
|
+
resolveP(password);
|
|
1237
|
+
} else if (ch === "\u0003") {
|
|
1238
|
+
// Ctrl+C
|
|
1239
|
+
stdin.setRawMode(false);
|
|
1240
|
+
process.stdout.write("\n");
|
|
1241
|
+
process.exit(0);
|
|
1242
|
+
} else if (ch === "\u007F" || ch === "\b") {
|
|
1243
|
+
if (password.length > 0) password = password.slice(0, -1);
|
|
1244
|
+
} else {
|
|
1245
|
+
password += ch;
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
stdin.on("data", onData);
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/** Read ~/.drawlatch/.env and report whether a dashboard password hash is set. */
|
|
1253
|
+
function isPasswordConfigured() {
|
|
1254
|
+
try {
|
|
1255
|
+
const envVars = loadEnvFileVars();
|
|
1256
|
+
return !!envVars.AUTH_PASSWORD_HASH;
|
|
1257
|
+
} catch {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
416
1262
|
// ── PID utilities ─────────────────────────────────────────────────
|
|
417
1263
|
|
|
418
1264
|
function isProcessAlive(pid) {
|
|
@@ -449,11 +1295,16 @@ function cleanPidFile() {
|
|
|
449
1295
|
|
|
450
1296
|
// ── Health check utilities ────────────────────────────────────────
|
|
451
1297
|
|
|
1298
|
+
/** Resolve the host for client connections — 0.0.0.0 is a bind address, not connectable. */
|
|
1299
|
+
function connectHost(host) {
|
|
1300
|
+
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
452
1303
|
async function healthCheck(host, port) {
|
|
453
1304
|
try {
|
|
454
1305
|
const controller = new AbortController();
|
|
455
1306
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
456
|
-
const res = await fetch(`http://${host}:${port}/health`, {
|
|
1307
|
+
const res = await fetch(`http://${connectHost(host)}:${port}/health`, {
|
|
457
1308
|
signal: controller.signal,
|
|
458
1309
|
});
|
|
459
1310
|
clearTimeout(timeout);
|
|
@@ -467,7 +1318,7 @@ async function healthCheckFull(host, port) {
|
|
|
467
1318
|
try {
|
|
468
1319
|
const controller = new AbortController();
|
|
469
1320
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
470
|
-
const res = await fetch(`http://${host}:${port}/health`, {
|
|
1321
|
+
const res = await fetch(`http://${connectHost(host)}:${port}/health`, {
|
|
471
1322
|
signal: controller.signal,
|
|
472
1323
|
});
|
|
473
1324
|
clearTimeout(timeout);
|
|
@@ -515,18 +1366,18 @@ function ensureConfigDir() {
|
|
|
515
1366
|
// ── Diagnostic utilities ──────────────────────────────────────────
|
|
516
1367
|
|
|
517
1368
|
async function diagnoseStartFailure() {
|
|
518
|
-
if (!existsSync(LOG_FILE))
|
|
1369
|
+
if (!existsSync(LOG_FILE)) {
|
|
1370
|
+
console.log("\n No log file found. The server may not have started at all.");
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
519
1373
|
try {
|
|
520
1374
|
const content = readFileSync(LOG_FILE, "utf-8");
|
|
521
|
-
const lines = content.split("\n").slice(-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
} else if (eacces) {
|
|
528
|
-
console.log("\n Error: Permission denied.");
|
|
529
|
-
console.log(" Try using a port >= 1024.");
|
|
1375
|
+
const lines = content.split("\n").filter(Boolean).slice(-15);
|
|
1376
|
+
if (lines.length > 0) {
|
|
1377
|
+
console.log("\n Recent logs:");
|
|
1378
|
+
for (const line of lines) {
|
|
1379
|
+
console.log(` ${line}`);
|
|
1380
|
+
}
|
|
530
1381
|
}
|
|
531
1382
|
} catch {
|
|
532
1383
|
// Best effort
|
|
@@ -549,6 +1400,120 @@ function sleep(ms) {
|
|
|
549
1400
|
return new Promise((r) => setTimeout(r, ms));
|
|
550
1401
|
}
|
|
551
1402
|
|
|
1403
|
+
// ── Version check utilities ───────────────────────────────────────
|
|
1404
|
+
|
|
1405
|
+
const NPM_PACKAGE_NAME = pkgJson.name;
|
|
1406
|
+
|
|
1407
|
+
function readVersionCache() {
|
|
1408
|
+
try {
|
|
1409
|
+
if (!existsSync(VERSION_CACHE_FILE)) return null;
|
|
1410
|
+
const data = JSON.parse(readFileSync(VERSION_CACHE_FILE, "utf-8"));
|
|
1411
|
+
if (data.checkedAt && Date.now() - data.checkedAt < VERSION_CHECK_TTL_MS) {
|
|
1412
|
+
return data.latestVersion;
|
|
1413
|
+
}
|
|
1414
|
+
return null; // stale
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function writeVersionCache(latestVersion) {
|
|
1421
|
+
try {
|
|
1422
|
+
ensureConfigDir();
|
|
1423
|
+
writeFileSync(
|
|
1424
|
+
VERSION_CACHE_FILE,
|
|
1425
|
+
JSON.stringify({ latestVersion, checkedAt: Date.now() }) + "\n",
|
|
1426
|
+
{ mode: 0o600 },
|
|
1427
|
+
);
|
|
1428
|
+
} catch {
|
|
1429
|
+
// Best effort
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function fetchLatestVersion() {
|
|
1434
|
+
try {
|
|
1435
|
+
const controller = new AbortController();
|
|
1436
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1437
|
+
const res = await fetch(
|
|
1438
|
+
`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`,
|
|
1439
|
+
{ signal: controller.signal },
|
|
1440
|
+
);
|
|
1441
|
+
clearTimeout(timeout);
|
|
1442
|
+
if (!res.ok) return null;
|
|
1443
|
+
const data = await res.json();
|
|
1444
|
+
return data.version || null;
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function compareVersions(a, b) {
|
|
1451
|
+
// Returns > 0 if a > b, < 0 if a < b, 0 if equal
|
|
1452
|
+
// Handles pre-release: 1.0.0 > 1.0.0-alpha.1
|
|
1453
|
+
const parseVer = (v) => {
|
|
1454
|
+
const [core, pre] = v.split("-", 2);
|
|
1455
|
+
const parts = core.split(".").map(Number);
|
|
1456
|
+
return { parts, pre: pre || null };
|
|
1457
|
+
};
|
|
1458
|
+
const va = parseVer(a);
|
|
1459
|
+
const vb = parseVer(b);
|
|
1460
|
+
|
|
1461
|
+
// Compare core version parts
|
|
1462
|
+
const maxLen = Math.max(va.parts.length, vb.parts.length);
|
|
1463
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1464
|
+
const pa = va.parts[i] || 0;
|
|
1465
|
+
const pb = vb.parts[i] || 0;
|
|
1466
|
+
if (pa !== pb) return pa - pb;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// Same core: no pre-release > pre-release
|
|
1470
|
+
if (!va.pre && vb.pre) return 1;
|
|
1471
|
+
if (va.pre && !vb.pre) return -1;
|
|
1472
|
+
if (!va.pre && !vb.pre) return 0;
|
|
1473
|
+
|
|
1474
|
+
// Both have pre-release: compare segments
|
|
1475
|
+
const aParts = va.pre.split(".");
|
|
1476
|
+
const bParts = vb.pre.split(".");
|
|
1477
|
+
const preLen = Math.max(aParts.length, bParts.length);
|
|
1478
|
+
for (let i = 0; i < preLen; i++) {
|
|
1479
|
+
const sa = aParts[i];
|
|
1480
|
+
const sb = bParts[i];
|
|
1481
|
+
if (sa === undefined) return -1;
|
|
1482
|
+
if (sb === undefined) return 1;
|
|
1483
|
+
const na = Number(sa);
|
|
1484
|
+
const nb = Number(sb);
|
|
1485
|
+
const aIsNum = !isNaN(na);
|
|
1486
|
+
const bIsNum = !isNaN(nb);
|
|
1487
|
+
if (aIsNum && bIsNum) {
|
|
1488
|
+
if (na !== nb) return na - nb;
|
|
1489
|
+
} else if (aIsNum) {
|
|
1490
|
+
return -1; // numbers sort before strings
|
|
1491
|
+
} else if (bIsNum) {
|
|
1492
|
+
return 1;
|
|
1493
|
+
} else {
|
|
1494
|
+
if (sa < sb) return -1;
|
|
1495
|
+
if (sa > sb) return 1;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return 0;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async function checkForUpdate() {
|
|
1502
|
+
// Try cache first
|
|
1503
|
+
const cached = readVersionCache();
|
|
1504
|
+
if (cached) return cached !== VERSION && compareVersions(cached, VERSION) > 0 ? cached : null;
|
|
1505
|
+
|
|
1506
|
+
// Fetch in background-ish (awaited but with short timeout)
|
|
1507
|
+
const latest = await fetchLatestVersion();
|
|
1508
|
+
if (latest) writeVersionCache(latest);
|
|
1509
|
+
if (latest && latest !== VERSION && compareVersions(latest, VERSION) > 0) return latest;
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function formatUpdateNotice(latestVersion) {
|
|
1514
|
+
return `\n Update available: ${VERSION} → ${latestVersion}\n Run: npm install -g ${NPM_PACKAGE_NAME}\n`;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
552
1517
|
// ── Help text ─────────────────────────────────────────────────────
|
|
553
1518
|
|
|
554
1519
|
function printHelp() {
|
|
@@ -558,13 +1523,18 @@ drawlatch v${VERSION}
|
|
|
558
1523
|
Usage: drawlatch [command] [options]
|
|
559
1524
|
|
|
560
1525
|
Commands:
|
|
1526
|
+
init Set up drawlatch server (keys, config, .env)
|
|
561
1527
|
start Start the remote server (background by default)
|
|
562
1528
|
stop Stop the background remote server
|
|
563
1529
|
restart Restart the background remote server
|
|
564
1530
|
status Show server status (PID, port, uptime, health, sessions)
|
|
565
1531
|
logs View and follow remote server logs
|
|
566
1532
|
config Show effective configuration
|
|
1533
|
+
watch Watch ingestor events in real time
|
|
1534
|
+
doctor Validate setup and diagnose issues
|
|
567
1535
|
generate-keys Generate Ed25519 + X25519 keypairs
|
|
1536
|
+
issue-caller Issue a caller credential bundle (for a callboard instance)
|
|
1537
|
+
set-password Set/change the dashboard password (alias: change-password)
|
|
568
1538
|
|
|
569
1539
|
Options:
|
|
570
1540
|
-h, --help Show this help message
|
|
@@ -573,15 +1543,13 @@ Options:
|
|
|
573
1543
|
Running 'drawlatch' with no arguments shows status (if running) or this help.
|
|
574
1544
|
|
|
575
1545
|
Examples:
|
|
576
|
-
drawlatch
|
|
1546
|
+
drawlatch init Set up the remote server
|
|
577
1547
|
drawlatch start Start remote server in background
|
|
578
1548
|
drawlatch start -f Start remote server in foreground
|
|
579
1549
|
drawlatch start -f --tunnel Start with a public tunnel for webhooks
|
|
580
|
-
drawlatch
|
|
1550
|
+
drawlatch doctor Validate full setup
|
|
581
1551
|
drawlatch status Check if server is running
|
|
582
1552
|
drawlatch logs -n 100 View last 100 log lines
|
|
583
|
-
drawlatch generate-keys remote Generate remote server keypair
|
|
584
|
-
drawlatch generate-keys local mybot Generate local keypair for alias "mybot"
|
|
585
1553
|
`);
|
|
586
1554
|
}
|
|
587
1555
|
|
|
@@ -658,19 +1626,42 @@ function printLogsHelp() {
|
|
|
658
1626
|
console.log(`
|
|
659
1627
|
drawlatch logs
|
|
660
1628
|
|
|
661
|
-
View server logs.
|
|
1629
|
+
View server logs (request/response audit lines are hidden by default).
|
|
662
1630
|
|
|
663
1631
|
Usage: drawlatch logs [options]
|
|
664
1632
|
|
|
665
1633
|
Options:
|
|
666
1634
|
-n, --lines <number> Number of lines to show (default: 50)
|
|
667
|
-
--
|
|
1635
|
+
--follow Follow/tail the log output (default: print and exit)
|
|
1636
|
+
--requests Include request/response audit lines (noisy with polling)
|
|
668
1637
|
-h, --help Show this help message
|
|
669
1638
|
|
|
670
1639
|
Log file: ~/.drawlatch/logs/drawlatch.log
|
|
671
1640
|
`);
|
|
672
1641
|
}
|
|
673
1642
|
|
|
1643
|
+
function printWatchHelp() {
|
|
1644
|
+
console.log(`
|
|
1645
|
+
drawlatch watch
|
|
1646
|
+
|
|
1647
|
+
Watch ingestor events in real time.
|
|
1648
|
+
|
|
1649
|
+
Usage: drawlatch watch [source] [options]
|
|
1650
|
+
|
|
1651
|
+
Arguments:
|
|
1652
|
+
source Filter to a specific connection (e.g., "discord-bot", "github")
|
|
1653
|
+
|
|
1654
|
+
Options:
|
|
1655
|
+
--full Show full event payload (default: truncate to 100 chars)
|
|
1656
|
+
-h, --help Show this help message
|
|
1657
|
+
|
|
1658
|
+
Examples:
|
|
1659
|
+
drawlatch watch Watch all events
|
|
1660
|
+
drawlatch watch github Watch only GitHub events
|
|
1661
|
+
drawlatch watch discord-bot --full Watch Discord events with full payloads
|
|
1662
|
+
`);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
674
1665
|
function printConfigHelp() {
|
|
675
1666
|
console.log(`
|
|
676
1667
|
drawlatch config
|
|
@@ -688,6 +1679,116 @@ server configuration including callers and connections.
|
|
|
688
1679
|
`);
|
|
689
1680
|
}
|
|
690
1681
|
|
|
1682
|
+
function printInitHelp() {
|
|
1683
|
+
console.log(`
|
|
1684
|
+
drawlatch init
|
|
1685
|
+
|
|
1686
|
+
Set up the drawlatch remote server. Generates server keys, creates
|
|
1687
|
+
config files, and scaffolds a .env template.
|
|
1688
|
+
|
|
1689
|
+
Callers are issued separately via 'drawlatch issue-caller' (or the dashboard
|
|
1690
|
+
Callers page) after the server is running.
|
|
1691
|
+
|
|
1692
|
+
Usage: drawlatch init [options]
|
|
1693
|
+
|
|
1694
|
+
Options:
|
|
1695
|
+
-h, --help Show this help message
|
|
1696
|
+
|
|
1697
|
+
All steps are idempotent — safe to re-run without overwriting existing files.
|
|
1698
|
+
`);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function printDoctorHelp() {
|
|
1702
|
+
console.log(`
|
|
1703
|
+
drawlatch doctor
|
|
1704
|
+
|
|
1705
|
+
Validate your drawlatch setup and diagnose common issues.
|
|
1706
|
+
|
|
1707
|
+
Usage: drawlatch doctor [options]
|
|
1708
|
+
|
|
1709
|
+
Options:
|
|
1710
|
+
--full Include a live handshake test (requires server running)
|
|
1711
|
+
-h, --help Show this help message
|
|
1712
|
+
|
|
1713
|
+
Checks config files, keys, peer directories, secrets, and server health.
|
|
1714
|
+
Each failure includes the exact command to fix it.
|
|
1715
|
+
`);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function printSyncHelp() {
|
|
1719
|
+
console.log(`
|
|
1720
|
+
drawlatch sync
|
|
1721
|
+
|
|
1722
|
+
Exchange keys with a callboard instance using a double-code approval flow.
|
|
1723
|
+
|
|
1724
|
+
Usage: drawlatch sync [options]
|
|
1725
|
+
|
|
1726
|
+
Options:
|
|
1727
|
+
--ttl <seconds> Sync session timeout (default: 300)
|
|
1728
|
+
-h, --help Show this help message
|
|
1729
|
+
|
|
1730
|
+
Flow:
|
|
1731
|
+
1. Run 'drawlatch sync' — displays an invite code and encryption key
|
|
1732
|
+
2. Give both values to the callboard operator
|
|
1733
|
+
3. They enter them into callboard, which generates a confirm code
|
|
1734
|
+
4. Enter the confirm code when prompted
|
|
1735
|
+
5. Callboard sends the encrypted sync request
|
|
1736
|
+
6. Keys are exchanged automatically
|
|
1737
|
+
|
|
1738
|
+
The server must be running (drawlatch start) before using this command.
|
|
1739
|
+
`);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function printIssueCallerHelp() {
|
|
1743
|
+
console.log(`
|
|
1744
|
+
drawlatch issue-caller
|
|
1745
|
+
|
|
1746
|
+
Issue a caller credential bundle that a callboard instance imports to gain a
|
|
1747
|
+
caller identity. drawlatch mints the keypair, keeps only the PUBLIC key, and
|
|
1748
|
+
hands the private key out in the bundle (shown once — re-issue to rotate).
|
|
1749
|
+
|
|
1750
|
+
Usage: drawlatch issue-caller <alias> [options]
|
|
1751
|
+
|
|
1752
|
+
Options:
|
|
1753
|
+
--name <name> Human-readable display name (defaults to the alias)
|
|
1754
|
+
--connections <a,b,c> Comma-separated connections to authorize
|
|
1755
|
+
(defaults to cloning the "default" caller)
|
|
1756
|
+
--endpoint <url> Endpoint URL to pin in the bundle
|
|
1757
|
+
(defaults to this server's host:port)
|
|
1758
|
+
--passphrase Prompt for a passphrase and scrypt+AES-256-GCM wrap
|
|
1759
|
+
the private keys in the bundle (for untrusted transport)
|
|
1760
|
+
-o, --output <file> Write the bundle to a file (0600) instead of stdout
|
|
1761
|
+
--into <keysDir> Same-host: write the UNPACKED key files directly into a
|
|
1762
|
+
co-located callboard's keys dir (no bundle file)
|
|
1763
|
+
-h, --help Show this help message
|
|
1764
|
+
|
|
1765
|
+
Examples:
|
|
1766
|
+
drawlatch issue-caller callboard-prod -o callboard-prod.drawlatch-caller.json
|
|
1767
|
+
drawlatch issue-caller callboard-prod --passphrase -o bundle.json
|
|
1768
|
+
drawlatch issue-caller callboard-local --into ~/.callboard/keys
|
|
1769
|
+
`);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function printSetPasswordHelp(cmd = "set-password") {
|
|
1773
|
+
console.log(`
|
|
1774
|
+
drawlatch ${cmd}
|
|
1775
|
+
|
|
1776
|
+
Set or change the password that gates the admin dashboard and /api/admin/*.
|
|
1777
|
+
|
|
1778
|
+
Usage: drawlatch ${cmd} [options]
|
|
1779
|
+
|
|
1780
|
+
Options:
|
|
1781
|
+
-h, --help Show this help message
|
|
1782
|
+
|
|
1783
|
+
Prompts for a new password (hidden in a TTY; reads two lines when piped),
|
|
1784
|
+
hashes it with scrypt, and stores AUTH_PASSWORD_HASH / AUTH_PASSWORD_SALT in
|
|
1785
|
+
${ENV_FILE}.
|
|
1786
|
+
|
|
1787
|
+
The password must be at least 8 characters. 'change-password' is an alias.
|
|
1788
|
+
Restart the server afterwards (drawlatch restart) to apply the new password.
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
691
1792
|
function printGenerateKeysHelp() {
|
|
692
1793
|
console.log(`
|
|
693
1794
|
drawlatch generate-keys
|
|
@@ -697,10 +1798,11 @@ Generate Ed25519 + X25519 keypairs for authentication and encryption.
|
|
|
697
1798
|
Usage: drawlatch generate-keys <subcommand> [options]
|
|
698
1799
|
|
|
699
1800
|
Subcommands:
|
|
700
|
-
|
|
1801
|
+
caller [alias] Generate caller keypair
|
|
701
1802
|
Alias defaults to "default" if omitted.
|
|
702
|
-
Keys are stored in keys/
|
|
703
|
-
|
|
1803
|
+
Keys are stored in keys/callers/<alias>/
|
|
1804
|
+
server Generate server keypair
|
|
1805
|
+
Keys are stored in keys/server/
|
|
704
1806
|
--dir <path> Generate keypair in a custom directory
|
|
705
1807
|
show <path> Show fingerprint of an existing keypair
|
|
706
1808
|
|