@wolpertingerlabs/drawlatch 1.0.0-alpha.2 → 1.0.0-alpha.29
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 +395 -465
- package/bin/drawlatch.js +1018 -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 +37 -0
- package/dist/remote/admin-mutations.js +251 -0
- package/dist/remote/admin-types.d.ts +149 -0
- package/dist/remote/admin-types.js +11 -0
- package/dist/remote/admin.d.ts +37 -0
- package/dist/remote/admin.js +316 -0
- package/dist/remote/caller-bootstrap.d.ts +71 -0
- package/dist/remote/caller-bootstrap.js +141 -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 +681 -195
- 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 +53 -0
- package/dist/remote/tunnel.js +149 -0
- package/dist/shared/config.d.ts +97 -25
- package/dist/shared/config.js +48 -35
- 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 +67 -0
- package/dist/shared/crypto/key-manager.js +152 -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/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-D4k5QKBP.css +1 -0
- package/frontend/dist/assets/index-IzwnSOmu.js +267 -0
- package/frontend/dist/index.html +15 -0
- package/package.json +66 -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(
|
|
@@ -57,11 +74,15 @@ try {
|
|
|
57
74
|
help: { type: "boolean", short: "h", default: false },
|
|
58
75
|
version: { type: "boolean", short: "v", default: false },
|
|
59
76
|
foreground: { type: "boolean", short: "f", default: false },
|
|
77
|
+
tunnel: { type: "boolean", short: "t", default: false },
|
|
60
78
|
port: { type: "string" },
|
|
61
79
|
host: { type: "string" },
|
|
62
80
|
lines: { type: "string", short: "n", default: "50" },
|
|
63
|
-
follow: { type: "boolean", default:
|
|
81
|
+
follow: { type: "boolean", default: false },
|
|
64
82
|
path: { type: "boolean", default: false },
|
|
83
|
+
full: { type: "boolean", default: false },
|
|
84
|
+
requests: { type: "boolean", default: false },
|
|
85
|
+
ttl: { type: "string", default: "300" },
|
|
65
86
|
},
|
|
66
87
|
strict: false,
|
|
67
88
|
allowPositionals: true,
|
|
@@ -71,6 +92,11 @@ try {
|
|
|
71
92
|
process.exit(1);
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
// ── Kick off version check early (non-blocking) ──────────────────
|
|
96
|
+
const updateCheckPromise = (subcommand === null || subcommand === "help" || (values.help && !subcommand))
|
|
97
|
+
? checkForUpdate()
|
|
98
|
+
: Promise.resolve(null);
|
|
99
|
+
|
|
74
100
|
// ── Dispatch ──────────────────────────────────────────────────────
|
|
75
101
|
if (values.version) {
|
|
76
102
|
console.log(VERSION);
|
|
@@ -78,6 +104,11 @@ if (values.version) {
|
|
|
78
104
|
}
|
|
79
105
|
if (values.help && !subcommand) {
|
|
80
106
|
printHelp();
|
|
107
|
+
const latestVersion = await Promise.race([
|
|
108
|
+
updateCheckPromise,
|
|
109
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
110
|
+
]);
|
|
111
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
81
112
|
process.exit(0);
|
|
82
113
|
}
|
|
83
114
|
|
|
@@ -85,6 +116,13 @@ switch (subcommand) {
|
|
|
85
116
|
case null:
|
|
86
117
|
await cmdDefault();
|
|
87
118
|
break;
|
|
119
|
+
case "init":
|
|
120
|
+
if (values.help) {
|
|
121
|
+
printInitHelp();
|
|
122
|
+
} else {
|
|
123
|
+
await cmdInit();
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
88
126
|
case "start":
|
|
89
127
|
if (values.help) {
|
|
90
128
|
printStartHelp();
|
|
@@ -134,8 +172,44 @@ switch (subcommand) {
|
|
|
134
172
|
await cmdGenerateKeys();
|
|
135
173
|
}
|
|
136
174
|
break;
|
|
175
|
+
case "doctor":
|
|
176
|
+
if (values.help) {
|
|
177
|
+
printDoctorHelp();
|
|
178
|
+
} else {
|
|
179
|
+
await cmdDoctor();
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "sync":
|
|
183
|
+
if (values.help) {
|
|
184
|
+
printSyncHelp();
|
|
185
|
+
} else {
|
|
186
|
+
await cmdSync();
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case "set-password":
|
|
190
|
+
case "change-password":
|
|
191
|
+
if (values.help) {
|
|
192
|
+
printSetPasswordHelp(subcommand);
|
|
193
|
+
} else {
|
|
194
|
+
await cmdSetPassword();
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
case "watch":
|
|
198
|
+
if (values.help) {
|
|
199
|
+
printWatchHelp();
|
|
200
|
+
} else {
|
|
201
|
+
await cmdWatch();
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
137
204
|
case "help":
|
|
138
205
|
printHelp();
|
|
206
|
+
{
|
|
207
|
+
const latestVersion = await Promise.race([
|
|
208
|
+
updateCheckPromise,
|
|
209
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
210
|
+
]);
|
|
211
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
212
|
+
}
|
|
139
213
|
break;
|
|
140
214
|
default:
|
|
141
215
|
console.error(`Unknown command: ${subcommand}\n`);
|
|
@@ -153,8 +227,96 @@ async function cmdDefault() {
|
|
|
153
227
|
console.log("Drawlatch remote server is not running.\n");
|
|
154
228
|
printHelp();
|
|
155
229
|
}
|
|
230
|
+
// Show update notice only if the check already resolved (don't block on network)
|
|
231
|
+
const latestVersion = await Promise.race([
|
|
232
|
+
updateCheckPromise,
|
|
233
|
+
new Promise((r) => setTimeout(() => r(null), 100)),
|
|
234
|
+
]);
|
|
235
|
+
if (latestVersion) console.log(formatUpdateNotice(latestVersion));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function cmdInit() {
|
|
239
|
+
console.log(`\nDrawlatch Setup`);
|
|
240
|
+
console.log(`===============\n`);
|
|
241
|
+
|
|
242
|
+
const steps = [];
|
|
243
|
+
|
|
244
|
+
// Step 1: Ensure config directory
|
|
245
|
+
ensureConfigDir();
|
|
246
|
+
steps.push(`Config directory: ${CONFIG_DIR}`);
|
|
247
|
+
|
|
248
|
+
// Step 2: Generate server keypair
|
|
249
|
+
const serverKeysDir = getServerKeysDir();
|
|
250
|
+
if (existsSync(join(serverKeysDir, "signing.key.pem"))) {
|
|
251
|
+
const existing = loadKeyBundle(serverKeysDir);
|
|
252
|
+
const fp = fingerprint(extractPublicKeys(existing));
|
|
253
|
+
steps.push(`Server keys: already exist (${fp})`);
|
|
254
|
+
} else {
|
|
255
|
+
const bundle = generateKeyBundle();
|
|
256
|
+
saveKeyBundle(bundle, serverKeysDir);
|
|
257
|
+
const fp = fingerprint(extractPublicKeys(bundle));
|
|
258
|
+
steps.push(`Server keys: CREATED (${fp})`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Step 3: Scaffold proxy.config.json
|
|
262
|
+
const proxyConfigPath = getProxyConfigPath();
|
|
263
|
+
if (existsSync(proxyConfigPath)) {
|
|
264
|
+
steps.push(`Proxy config: already exists`);
|
|
265
|
+
} else {
|
|
266
|
+
const proxyConfig = {
|
|
267
|
+
remoteUrl: "http://127.0.0.1:9999",
|
|
268
|
+
connectTimeout: 10000,
|
|
269
|
+
requestTimeout: 300000,
|
|
270
|
+
};
|
|
271
|
+
writeFileSync(proxyConfigPath, JSON.stringify(proxyConfig, null, 2) + "\n", { mode: 0o600 });
|
|
272
|
+
steps.push(`Proxy config: CREATED`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Step 4: Scaffold remote.config.json
|
|
276
|
+
const remoteConfigPath = getRemoteConfigPath();
|
|
277
|
+
if (existsSync(remoteConfigPath)) {
|
|
278
|
+
steps.push(`Remote config: already exists`);
|
|
279
|
+
} else {
|
|
280
|
+
const remoteConfig = {
|
|
281
|
+
host: "0.0.0.0",
|
|
282
|
+
port: 9999,
|
|
283
|
+
rateLimitPerMinute: 60,
|
|
284
|
+
callers: {},
|
|
285
|
+
};
|
|
286
|
+
writeFileSync(remoteConfigPath, JSON.stringify(remoteConfig, null, 2) + "\n", { mode: 0o600 });
|
|
287
|
+
steps.push(`Remote config: CREATED`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Step 5: Scaffold .env file
|
|
291
|
+
if (existsSync(ENV_FILE)) {
|
|
292
|
+
steps.push(`.env file: already exists`);
|
|
293
|
+
} else {
|
|
294
|
+
const envLines = [
|
|
295
|
+
"# Drawlatch environment secrets",
|
|
296
|
+
"# Set tokens for your enabled connections",
|
|
297
|
+
"# Secrets are prefixed per caller (e.g., DEFAULT_GITHUB_TOKEN)",
|
|
298
|
+
"",
|
|
299
|
+
];
|
|
300
|
+
writeFileSync(ENV_FILE, envLines.join("\n") + "\n", { mode: 0o600 });
|
|
301
|
+
steps.push(`.env file: CREATED`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Print summary
|
|
305
|
+
for (const step of steps) {
|
|
306
|
+
console.log(` ${step}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(`\nSetup complete! Next steps:\n`);
|
|
310
|
+
console.log(` 1. Start the remote server:`);
|
|
311
|
+
console.log(` drawlatch start\n`);
|
|
312
|
+
console.log(` 2. Add callers via key sync:`);
|
|
313
|
+
console.log(` drawlatch sync\n`);
|
|
314
|
+
console.log(` 3. Verify your setup:`);
|
|
315
|
+
console.log(` drawlatch doctor\n`);
|
|
156
316
|
}
|
|
157
317
|
|
|
318
|
+
|
|
319
|
+
|
|
158
320
|
async function cmdStart() {
|
|
159
321
|
if (values.foreground) return cmdStartForeground();
|
|
160
322
|
|
|
@@ -182,6 +344,7 @@ async function cmdStart() {
|
|
|
182
344
|
NODE_ENV: "production",
|
|
183
345
|
...(values.port ? { DRAWLATCH_PORT: String(port) } : {}),
|
|
184
346
|
...(values.host ? { DRAWLATCH_HOST: host } : {}),
|
|
347
|
+
...(values.tunnel ? { DRAWLATCH_TUNNEL: "1" } : {}),
|
|
185
348
|
},
|
|
186
349
|
cwd: PKG_ROOT,
|
|
187
350
|
});
|
|
@@ -195,12 +358,30 @@ async function cmdStart() {
|
|
|
195
358
|
if (healthy) {
|
|
196
359
|
console.log(`\nRemote server is running (PID ${child.pid}).`);
|
|
197
360
|
console.log(` Listening: ${host}:${port}`);
|
|
361
|
+
if (values.tunnel) {
|
|
362
|
+
// The tunnel starts asynchronously after the server is healthy —
|
|
363
|
+
// poll the health endpoint until the tunnel URL appears (up to 20s).
|
|
364
|
+
console.log(` Tunnel: waiting for cloudflared...`);
|
|
365
|
+
const tunnelUrl = await waitForTunnelUrl(host, port, 20000);
|
|
366
|
+
if (tunnelUrl) {
|
|
367
|
+
console.log(` Tunnel: ${tunnelUrl}`);
|
|
368
|
+
console.log(` Webhooks: ${tunnelUrl}/webhooks/<path>`);
|
|
369
|
+
} else {
|
|
370
|
+
console.log(` Tunnel: not available (check logs: drawlatch logs)`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
198
373
|
console.log(` Logs: drawlatch logs`);
|
|
199
374
|
} else {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
375
|
+
const stillAlive = isProcessAlive(child.pid);
|
|
376
|
+
if (stillAlive) {
|
|
377
|
+
console.log(
|
|
378
|
+
`\nServer started (PID ${child.pid}) but health check did not pass within 5s.`,
|
|
379
|
+
);
|
|
380
|
+
console.log(` The server process is still running — it may need more time.`);
|
|
381
|
+
} else {
|
|
382
|
+
console.log(`\nServer process (PID ${child.pid}) exited before becoming healthy.`);
|
|
383
|
+
cleanPidFile();
|
|
384
|
+
}
|
|
204
385
|
await diagnoseStartFailure();
|
|
205
386
|
}
|
|
206
387
|
}
|
|
@@ -209,6 +390,7 @@ async function cmdStartForeground() {
|
|
|
209
390
|
process.env.NODE_ENV = process.env.NODE_ENV || "production";
|
|
210
391
|
if (values.port) process.env.DRAWLATCH_PORT = values.port;
|
|
211
392
|
if (values.host) process.env.DRAWLATCH_HOST = values.host;
|
|
393
|
+
if (values.tunnel) process.env.DRAWLATCH_TUNNEL = "1";
|
|
212
394
|
|
|
213
395
|
ensureConfigDir();
|
|
214
396
|
|
|
@@ -250,6 +432,17 @@ async function cmdStop() {
|
|
|
250
432
|
async function cmdRestart() {
|
|
251
433
|
const pid = readPid();
|
|
252
434
|
if (pid) {
|
|
435
|
+
// If the previous server had an active tunnel, carry the flag forward
|
|
436
|
+
// so the restarted server also starts a tunnel (unless --tunnel is
|
|
437
|
+
// already set or the user explicitly omitted it).
|
|
438
|
+
if (!values.tunnel) {
|
|
439
|
+
const config = loadRemoteConfig();
|
|
440
|
+
const prevHealth = await healthCheckFull(config.host, config.port);
|
|
441
|
+
if (prevHealth?.tunnelUrl) {
|
|
442
|
+
console.log("Previous server had an active tunnel — re-enabling --tunnel.");
|
|
443
|
+
values.tunnel = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
253
446
|
await cmdStop();
|
|
254
447
|
}
|
|
255
448
|
await cmdStart();
|
|
@@ -279,12 +472,19 @@ async function cmdStatus() {
|
|
|
279
472
|
console.log("Drawlatch remote server is running.");
|
|
280
473
|
console.log(` PID: ${pid}`);
|
|
281
474
|
console.log(` Listening: ${host}:${port}`);
|
|
475
|
+
console.log(` Dashboard: http://${connectHost(host)}:${port}/`);
|
|
476
|
+
console.log(
|
|
477
|
+
` Password: ${isPasswordConfigured() ? "configured" : "not set (run: drawlatch set-password)"}`,
|
|
478
|
+
);
|
|
282
479
|
console.log(` Uptime: ${uptime}`);
|
|
283
480
|
console.log(
|
|
284
481
|
` Health: ${healthData ? "healthy" : "unhealthy (not responding)"}`,
|
|
285
482
|
);
|
|
286
483
|
if (healthData) {
|
|
287
484
|
console.log(` Active sessions: ${healthData.activeSessions}`);
|
|
485
|
+
if (healthData.tunnelUrl) {
|
|
486
|
+
console.log(` Tunnel: ${healthData.tunnelUrl}`);
|
|
487
|
+
}
|
|
288
488
|
}
|
|
289
489
|
}
|
|
290
490
|
|
|
@@ -297,39 +497,154 @@ async function cmdLogs() {
|
|
|
297
497
|
|
|
298
498
|
const lines = parseInt(values.lines, 10) || 50;
|
|
299
499
|
const follow = values.follow;
|
|
500
|
+
const showRequests = values.requests;
|
|
300
501
|
|
|
301
502
|
const tailArgs = follow
|
|
302
503
|
? ["-n", String(lines), "-f", LOG_FILE]
|
|
303
504
|
: ["-n", String(lines), LOG_FILE];
|
|
304
505
|
|
|
305
|
-
|
|
506
|
+
if (showRequests) {
|
|
507
|
+
// Show everything — pipe directly to stdout
|
|
508
|
+
const tail = spawn("tail", tailArgs, { stdio: "inherit" });
|
|
306
509
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
510
|
+
tail.on("error", () => logsFallback(lines, follow, null));
|
|
511
|
+
|
|
512
|
+
process.on("SIGINT", () => {
|
|
513
|
+
tail.kill();
|
|
514
|
+
process.exit(0);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await new Promise((res) => tail.on("close", res));
|
|
518
|
+
} else {
|
|
519
|
+
// Filter out [audit] lines (request/response noise from poll_events etc.)
|
|
520
|
+
const tail = spawn("tail", tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
|
|
521
|
+
const grepProc = spawn("grep", ["-v", "^\\[audit\\]"], {
|
|
522
|
+
stdio: [tail.stdout, "inherit", "inherit"],
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
tail.on("error", () => logsFallback(lines, follow, "[audit]"));
|
|
526
|
+
|
|
527
|
+
process.on("SIGINT", () => {
|
|
528
|
+
tail.kill();
|
|
529
|
+
grepProc.kill();
|
|
530
|
+
process.exit(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
await new Promise((res) => grepProc.on("close", res));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function logsFallback(lines, follow, filterPrefix) {
|
|
538
|
+
try {
|
|
539
|
+
const content = readFileSync(LOG_FILE, "utf-8");
|
|
540
|
+
let allLines = content.split("\n");
|
|
541
|
+
if (filterPrefix) {
|
|
542
|
+
allLines = allLines.filter((l) => !l.startsWith(filterPrefix));
|
|
322
543
|
}
|
|
323
|
-
|
|
544
|
+
const lastLines = allLines.slice(-lines).join("\n");
|
|
545
|
+
console.log(lastLines);
|
|
546
|
+
if (follow) {
|
|
547
|
+
console.log(
|
|
548
|
+
"\n(Live following not available \u2014 'tail' command not found)",
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
console.error(`Error reading log file: ${err.message}`);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function cmdWatch() {
|
|
558
|
+
const config = loadRemoteConfig();
|
|
559
|
+
const port = config.port;
|
|
560
|
+
const host = config.host;
|
|
561
|
+
|
|
562
|
+
// Verify the server is running
|
|
563
|
+
const healthy = await healthCheck(host, port);
|
|
564
|
+
if (!healthy) {
|
|
565
|
+
console.error("Remote server is not running. Start it first:");
|
|
566
|
+
console.error(" drawlatch start");
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const showFull = values.full;
|
|
571
|
+
const sourceFilter = positionals[0] || null;
|
|
324
572
|
|
|
325
|
-
|
|
573
|
+
console.log("Watching for events" + (sourceFilter ? ` from "${sourceFilter}"` : "") + "...");
|
|
574
|
+
console.log("Press Ctrl+C to stop.\n");
|
|
575
|
+
|
|
576
|
+
const controller = new AbortController();
|
|
326
577
|
process.on("SIGINT", () => {
|
|
327
|
-
|
|
578
|
+
controller.abort();
|
|
328
579
|
process.exit(0);
|
|
329
580
|
});
|
|
330
581
|
|
|
331
|
-
|
|
332
|
-
|
|
582
|
+
try {
|
|
583
|
+
const res = await fetch(
|
|
584
|
+
`http://${connectHost(host)}:${port}/events/stream`,
|
|
585
|
+
{ signal: controller.signal },
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
if (!res.ok) {
|
|
589
|
+
console.error(`Failed to connect to event stream: HTTP ${res.status}`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const decoder = new TextDecoder();
|
|
594
|
+
let buffer = "";
|
|
595
|
+
|
|
596
|
+
for await (const chunk of res.body) {
|
|
597
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
598
|
+
|
|
599
|
+
// Parse SSE lines
|
|
600
|
+
let newlineIdx;
|
|
601
|
+
while ((newlineIdx = buffer.indexOf("\n\n")) !== -1) {
|
|
602
|
+
const message = buffer.slice(0, newlineIdx);
|
|
603
|
+
buffer = buffer.slice(newlineIdx + 2);
|
|
604
|
+
|
|
605
|
+
for (const line of message.split("\n")) {
|
|
606
|
+
if (!line.startsWith("data: ")) continue;
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
const event = JSON.parse(line.slice(6));
|
|
610
|
+
|
|
611
|
+
// Apply source filter if set
|
|
612
|
+
if (sourceFilter && event.source !== sourceFilter) continue;
|
|
613
|
+
|
|
614
|
+
const time = new Date(event.receivedAt).toLocaleTimeString();
|
|
615
|
+
const source = event.source;
|
|
616
|
+
const instance = event.instanceId ? `:${event.instanceId}` : "";
|
|
617
|
+
const caller = event.callerAlias || "?";
|
|
618
|
+
const eventType = event.eventType;
|
|
619
|
+
|
|
620
|
+
const dataStr = JSON.stringify(event.data);
|
|
621
|
+
|
|
622
|
+
if (showFull) {
|
|
623
|
+
console.log(
|
|
624
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m`
|
|
625
|
+
);
|
|
626
|
+
console.log(dataStr);
|
|
627
|
+
console.log("");
|
|
628
|
+
} else {
|
|
629
|
+
const preview =
|
|
630
|
+
dataStr.length > 100
|
|
631
|
+
? dataStr.slice(0, 100) + "…"
|
|
632
|
+
: dataStr;
|
|
633
|
+
console.log(
|
|
634
|
+
`\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m ${preview}`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
} catch {
|
|
638
|
+
// Skip malformed SSE data
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
if (err.name === "AbortError") return;
|
|
645
|
+
console.error(`Event stream error: ${err.message}`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
333
648
|
}
|
|
334
649
|
|
|
335
650
|
function cmdConfig() {
|
|
@@ -349,14 +664,51 @@ function cmdConfig() {
|
|
|
349
664
|
console.log(` Host: ${config.host}`);
|
|
350
665
|
console.log(` Port: ${config.port}`);
|
|
351
666
|
console.log(` Rate limit: ${config.rateLimitPerMinute} req/min`);
|
|
352
|
-
console.log(`
|
|
667
|
+
console.log(` Server keys dir: ${getServerKeysDir()}`);
|
|
668
|
+
|
|
669
|
+
// Show key fingerprints if keys exist
|
|
670
|
+
const serverKeysPath = getServerKeysDir();
|
|
671
|
+
if (existsSync(join(serverKeysPath, "signing.key.pem"))) {
|
|
672
|
+
try {
|
|
673
|
+
const serverKeys = loadKeyBundle(serverKeysPath);
|
|
674
|
+
console.log(` Server key fp: ${fingerprint(extractPublicKeys(serverKeys))}`);
|
|
675
|
+
} catch { /* skip */ }
|
|
676
|
+
}
|
|
353
677
|
|
|
354
678
|
const callerEntries = Object.entries(config.callers || {});
|
|
355
679
|
console.log(` Callers: ${callerEntries.length}`);
|
|
680
|
+
|
|
681
|
+
// Build template map for secret status checking
|
|
682
|
+
const templates = listConnectionTemplates();
|
|
683
|
+
const templateMap = new Map(templates.map((t) => [t.alias, t]));
|
|
684
|
+
|
|
685
|
+
// Load .env vars into process.env so isSecretSetForCaller works.
|
|
686
|
+
// Force-set to override empty shell env vars.
|
|
687
|
+
const envVars = loadEnvFileVars();
|
|
688
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
689
|
+
process.env[k] = v;
|
|
690
|
+
}
|
|
691
|
+
|
|
356
692
|
for (const [alias, caller] of callerEntries) {
|
|
357
693
|
console.log(
|
|
358
694
|
` ${alias}: ${caller.connections ? caller.connections.length : 0} connection(s)`,
|
|
359
695
|
);
|
|
696
|
+
for (const connName of caller.connections || []) {
|
|
697
|
+
const tpl = templateMap.get(connName);
|
|
698
|
+
if (!tpl) {
|
|
699
|
+
console.log(` ${connName} (custom connector)`);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const secretStatuses = tpl.requiredSecrets.map((s) => {
|
|
703
|
+
const set = isSecretSetForCaller(s, alias, caller.env);
|
|
704
|
+
return `${s}: ${set ? "SET" : "NOT SET"}`;
|
|
705
|
+
});
|
|
706
|
+
if (secretStatuses.length > 0) {
|
|
707
|
+
console.log(` ${connName}: ${secretStatuses.join(", ")}`);
|
|
708
|
+
} else {
|
|
709
|
+
console.log(` ${connName}: (no secrets required)`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
360
712
|
}
|
|
361
713
|
|
|
362
714
|
console.log(
|
|
@@ -384,6 +736,385 @@ async function cmdGenerateKeys() {
|
|
|
384
736
|
process.exit(child.exitCode ?? 0);
|
|
385
737
|
}
|
|
386
738
|
|
|
739
|
+
|
|
740
|
+
async function cmdDoctor() {
|
|
741
|
+
console.log(`\nDrawlatch Setup Check`);
|
|
742
|
+
console.log(`=====================\n`);
|
|
743
|
+
|
|
744
|
+
let passed = 0;
|
|
745
|
+
let failed = 0;
|
|
746
|
+
|
|
747
|
+
function check(label, ok, fix) {
|
|
748
|
+
const padded = label.padEnd(50);
|
|
749
|
+
if (ok) {
|
|
750
|
+
console.log(` ${padded} PASS`);
|
|
751
|
+
passed++;
|
|
752
|
+
} else {
|
|
753
|
+
console.log(` ${padded} FAIL`);
|
|
754
|
+
if (fix) console.log(` -> ${fix}`);
|
|
755
|
+
failed++;
|
|
756
|
+
}
|
|
757
|
+
return ok;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Load .env for secret checks
|
|
761
|
+
const envVars = loadEnvFileVars();
|
|
762
|
+
for (const [k, v] of Object.entries(envVars)) {
|
|
763
|
+
process.env[k] = v;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// 1. Config directory
|
|
767
|
+
check("Config directory", existsSync(CONFIG_DIR), "Run: drawlatch init");
|
|
768
|
+
|
|
769
|
+
// 2. Remote config
|
|
770
|
+
const remoteConfigPath = getRemoteConfigPath();
|
|
771
|
+
const hasRemoteConfig = existsSync(remoteConfigPath);
|
|
772
|
+
check("Remote config (remote.config.json)", hasRemoteConfig, "Run: drawlatch init");
|
|
773
|
+
|
|
774
|
+
// 3. Proxy config
|
|
775
|
+
const proxyConfigPath = getProxyConfigPath();
|
|
776
|
+
const hasProxyConfig = existsSync(proxyConfigPath);
|
|
777
|
+
check("Proxy config (proxy.config.json)", hasProxyConfig, "Run: drawlatch init");
|
|
778
|
+
|
|
779
|
+
// 4. Server keys
|
|
780
|
+
const serverKeysPath = getServerKeysDir();
|
|
781
|
+
const hasServerKeys = existsSync(join(serverKeysPath, "signing.key.pem")) &&
|
|
782
|
+
existsSync(join(serverKeysPath, "exchange.key.pem")) &&
|
|
783
|
+
existsSync(join(serverKeysPath, "signing.pub.pem")) &&
|
|
784
|
+
existsSync(join(serverKeysPath, "exchange.pub.pem"));
|
|
785
|
+
check("Server keys (keys/server/)", hasServerKeys, "Run: drawlatch generate-keys server");
|
|
786
|
+
|
|
787
|
+
if (hasServerKeys) {
|
|
788
|
+
try {
|
|
789
|
+
const sk = loadKeyBundle(serverKeysPath);
|
|
790
|
+
console.log(` Fingerprint: ${fingerprint(extractPublicKeys(sk))}`);
|
|
791
|
+
} catch { /* skip */ }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// 5. Caller keys — check each caller from config
|
|
795
|
+
if (hasRemoteConfig) {
|
|
796
|
+
try {
|
|
797
|
+
const config = loadRemoteConfig();
|
|
798
|
+
const callersDir = getCallerKeysDir();
|
|
799
|
+
for (const [alias] of Object.entries(config.callers)) {
|
|
800
|
+
const callerDir = join(callersDir, alias);
|
|
801
|
+
const hasCallerKeys = existsSync(join(callerDir, "signing.pub.pem")) &&
|
|
802
|
+
existsSync(join(callerDir, "exchange.pub.pem"));
|
|
803
|
+
check(`Caller keys for "${alias}" (keys/callers/${alias}/)`, hasCallerKeys, `Run: drawlatch generate-keys caller ${alias} (or sync)`);
|
|
804
|
+
|
|
805
|
+
if (hasCallerKeys) {
|
|
806
|
+
try {
|
|
807
|
+
const pk = loadPublicKeys(callerDir);
|
|
808
|
+
console.log(` Fingerprint: ${fingerprint(pk)}`);
|
|
809
|
+
} catch { /* skip */ }
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
} catch { /* skip */ }
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// 8. .env file
|
|
816
|
+
check("Environment file (.env)", existsSync(ENV_FILE), "Run: drawlatch init");
|
|
817
|
+
|
|
818
|
+
// 9. Required secrets for each caller's connections
|
|
819
|
+
if (hasRemoteConfig) {
|
|
820
|
+
try {
|
|
821
|
+
const config = loadRemoteConfig();
|
|
822
|
+
const templates = listConnectionTemplates();
|
|
823
|
+
const templateMap = new Map(templates.map((t) => [t.alias, t]));
|
|
824
|
+
|
|
825
|
+
for (const [callerAlias, caller] of Object.entries(config.callers)) {
|
|
826
|
+
for (const connName of caller.connections) {
|
|
827
|
+
const tpl = templateMap.get(connName);
|
|
828
|
+
if (!tpl) continue;
|
|
829
|
+
for (const secret of tpl.requiredSecrets) {
|
|
830
|
+
const isSet = isSecretSetForCaller(secret, callerAlias, caller.env);
|
|
831
|
+
check(` ${callerAlias}/${connName}: ${secret}`, isSet, `Set ${secret} in ${ENV_FILE}`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
} catch { /* skip */ }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// 10. Remote server health check
|
|
839
|
+
if (hasRemoteConfig) {
|
|
840
|
+
try {
|
|
841
|
+
const config = loadRemoteConfig();
|
|
842
|
+
const healthy = await healthCheck(config.host, config.port);
|
|
843
|
+
check(`Remote server health (${config.host}:${config.port})`, healthy, "Run: drawlatch start");
|
|
844
|
+
} catch {
|
|
845
|
+
check("Remote server health", false, "Run: drawlatch start");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Summary
|
|
850
|
+
const total = passed + failed;
|
|
851
|
+
console.log(`\nResult: ${passed}/${total} checks passed`);
|
|
852
|
+
if (failed > 0) {
|
|
853
|
+
console.log(`Fix the ${failed} failing check(s) above and re-run: drawlatch doctor`);
|
|
854
|
+
} else {
|
|
855
|
+
console.log(`All checks passed! Your setup is ready.`);
|
|
856
|
+
}
|
|
857
|
+
console.log();
|
|
858
|
+
|
|
859
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function cmdSync() {
|
|
863
|
+
const config = loadRemoteConfig();
|
|
864
|
+
const port = config.port;
|
|
865
|
+
const host = config.host;
|
|
866
|
+
const ttlSeconds = parseInt(values.ttl, 10) || 300;
|
|
867
|
+
const ttlMs = ttlSeconds * 1000;
|
|
868
|
+
|
|
869
|
+
// Check that the server is running
|
|
870
|
+
const healthy = await healthCheck(host, port);
|
|
871
|
+
if (!healthy) {
|
|
872
|
+
console.error(
|
|
873
|
+
"Error: Drawlatch remote server is not running.\n" +
|
|
874
|
+
" Start it first: drawlatch start\n",
|
|
875
|
+
);
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Import sync helpers from compiled drawlatch code
|
|
880
|
+
const { generateSyncCode, generateSyncEncryptionKey } = await import(
|
|
881
|
+
join(PKG_ROOT, "dist/shared/protocol/sync.js")
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
const inviteCode = generateSyncCode();
|
|
885
|
+
const encryptionKey = generateSyncEncryptionKey();
|
|
886
|
+
|
|
887
|
+
console.log("\nStarting key exchange...\n");
|
|
888
|
+
console.log(` Invite code: ${inviteCode}`);
|
|
889
|
+
console.log(` Encryption key: ${encryptionKey}`);
|
|
890
|
+
console.log(
|
|
891
|
+
"\nGive both values to the callboard operator.",
|
|
892
|
+
);
|
|
893
|
+
console.log("They will provide a confirm code.\n");
|
|
894
|
+
|
|
895
|
+
// Prompt for confirm code
|
|
896
|
+
const confirmCode = await promptInput("Enter confirm code: ");
|
|
897
|
+
if (!confirmCode.trim()) {
|
|
898
|
+
console.error("No confirm code entered. Aborting.");
|
|
899
|
+
process.exit(1);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Open the sync session on the running server
|
|
903
|
+
try {
|
|
904
|
+
const controller = new AbortController();
|
|
905
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
906
|
+
const res = await fetch(`http://${host}:${port}/sync/listen`, {
|
|
907
|
+
method: "POST",
|
|
908
|
+
headers: { "Content-Type": "application/json" },
|
|
909
|
+
body: JSON.stringify({
|
|
910
|
+
inviteCode,
|
|
911
|
+
confirmCode: confirmCode.trim(),
|
|
912
|
+
encryptionKey,
|
|
913
|
+
ttlMs,
|
|
914
|
+
}),
|
|
915
|
+
signal: controller.signal,
|
|
916
|
+
});
|
|
917
|
+
clearTimeout(timeout);
|
|
918
|
+
|
|
919
|
+
if (!res.ok) {
|
|
920
|
+
const body = await res.json().catch(() => ({}));
|
|
921
|
+
console.error(`Failed to open sync session: ${body.error || res.statusText}`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
} catch (err) {
|
|
925
|
+
console.error(`Failed to reach server: ${err.message}`);
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
console.log(`\nWaiting for sync from callboard (timeout: ${ttlSeconds}s)...`);
|
|
930
|
+
|
|
931
|
+
// Poll for completion
|
|
932
|
+
const start = Date.now();
|
|
933
|
+
while (Date.now() - start < ttlMs) {
|
|
934
|
+
await sleep(1000);
|
|
935
|
+
|
|
936
|
+
try {
|
|
937
|
+
const controller = new AbortController();
|
|
938
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
939
|
+
const res = await fetch(`http://${host}:${port}/sync/status`, {
|
|
940
|
+
signal: controller.signal,
|
|
941
|
+
});
|
|
942
|
+
clearTimeout(timeout);
|
|
943
|
+
|
|
944
|
+
if (!res.ok) continue;
|
|
945
|
+
const status = await res.json();
|
|
946
|
+
|
|
947
|
+
if (status.completed) {
|
|
948
|
+
console.log(`\nSync complete!`);
|
|
949
|
+
console.log(` Caller alias: ${status.callerAlias}`);
|
|
950
|
+
console.log(` Fingerprint: ${status.fingerprint}`);
|
|
951
|
+
console.log(
|
|
952
|
+
` Keys saved to: ${join(CONFIG_DIR, "keys", "callers", status.callerAlias)}/`,
|
|
953
|
+
);
|
|
954
|
+
console.log(`\nThe caller can now connect (no server restart needed).`);
|
|
955
|
+
console.log(`\nTo grant API access, add connections in ${join(CONFIG_DIR, "remote.config.json")}:`);
|
|
956
|
+
console.log(` "callers": {`);
|
|
957
|
+
console.log(` "${status.callerAlias}": {`);
|
|
958
|
+
console.log(` "connections": ["github", "slack", ...]`);
|
|
959
|
+
console.log(` }`);
|
|
960
|
+
console.log(` }`);
|
|
961
|
+
console.log(`\nThen set the required secrets in ${ENV_FILE}`);
|
|
962
|
+
console.log();
|
|
963
|
+
process.exit(0);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (!status.active) {
|
|
967
|
+
console.error("\nSync session expired or was cancelled.");
|
|
968
|
+
process.exit(1);
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
// Transient fetch error, keep polling
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
console.error("\nSync session timed out.");
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function promptInput(prompt) {
|
|
980
|
+
const { createInterface } = await import("node:readline");
|
|
981
|
+
return new Promise((resolve) => {
|
|
982
|
+
const rl = createInterface({
|
|
983
|
+
input: process.stdin,
|
|
984
|
+
output: process.stdout,
|
|
985
|
+
});
|
|
986
|
+
rl.question(prompt, (answer) => {
|
|
987
|
+
rl.close();
|
|
988
|
+
resolve(answer);
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ── set-password / change-password ────────────────────────────────
|
|
994
|
+
//
|
|
995
|
+
// Sets the dashboard/admin password. The scrypt hash + salt are written to
|
|
996
|
+
// ~/.drawlatch/.env (AUTH_PASSWORD_HASH / AUTH_PASSWORD_SALT) using the same
|
|
997
|
+
// env-writer the daemon's change-password handler uses, so both stay in sync.
|
|
998
|
+
async function cmdSetPassword() {
|
|
999
|
+
ensureConfigDir();
|
|
1000
|
+
|
|
1001
|
+
const { hashPassword, generateSalt } = await import(
|
|
1002
|
+
join(PKG_ROOT, "dist/auth/password.js")
|
|
1003
|
+
);
|
|
1004
|
+
const { updateEnvFile } = await import(
|
|
1005
|
+
join(PKG_ROOT, "dist/auth/env-writer.js")
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
let password, confirm;
|
|
1009
|
+
if (process.stdin.isTTY) {
|
|
1010
|
+
password = await promptPasswordTTY("Enter new password: ");
|
|
1011
|
+
if (!password) {
|
|
1012
|
+
console.error("Error: password cannot be empty.");
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
if (password.length < 8) {
|
|
1016
|
+
console.error("Error: password must be at least 8 characters.");
|
|
1017
|
+
process.exit(1);
|
|
1018
|
+
}
|
|
1019
|
+
confirm = await promptPasswordTTY("Confirm new password: ");
|
|
1020
|
+
} else {
|
|
1021
|
+
// Piped input — read both lines from a single readline interface so
|
|
1022
|
+
// closing it doesn't break the second prompt.
|
|
1023
|
+
const { createInterface } = await import("node:readline");
|
|
1024
|
+
const rl = createInterface({ input: process.stdin });
|
|
1025
|
+
[password, confirm] = await readTwoLines(rl);
|
|
1026
|
+
rl.close();
|
|
1027
|
+
if (!password) {
|
|
1028
|
+
console.error("Error: password cannot be empty.");
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
if (password.length < 8) {
|
|
1032
|
+
console.error("Error: password must be at least 8 characters.");
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (password !== confirm) {
|
|
1038
|
+
console.error("Error: passwords do not match.");
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const salt = generateSalt();
|
|
1043
|
+
const hash = await hashPassword(password, salt);
|
|
1044
|
+
|
|
1045
|
+
updateEnvFile(
|
|
1046
|
+
{ AUTH_PASSWORD_HASH: hash, AUTH_PASSWORD_SALT: salt },
|
|
1047
|
+
["AUTH_PASSWORD"], // strip any plaintext leftover, just in case
|
|
1048
|
+
);
|
|
1049
|
+
|
|
1050
|
+
console.log("\nPassword set successfully.");
|
|
1051
|
+
console.log(` Stored in: ${ENV_FILE}`);
|
|
1052
|
+
|
|
1053
|
+
const pid = readPid();
|
|
1054
|
+
if (pid) {
|
|
1055
|
+
console.log(" Restart the server to apply: drawlatch restart");
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function readTwoLines(rl) {
|
|
1060
|
+
return new Promise((resolveP) => {
|
|
1061
|
+
const lines = [];
|
|
1062
|
+
const onLine = (line) => {
|
|
1063
|
+
lines.push(line);
|
|
1064
|
+
if (lines.length >= 2) {
|
|
1065
|
+
rl.removeListener("line", onLine);
|
|
1066
|
+
resolveP(lines);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
rl.on("line", onLine);
|
|
1070
|
+
rl.once("close", () => {
|
|
1071
|
+
while (lines.length < 2) lines.push("");
|
|
1072
|
+
resolveP(lines);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function promptPasswordTTY(prompt) {
|
|
1078
|
+
return new Promise((resolveP) => {
|
|
1079
|
+
process.stdout.write(prompt);
|
|
1080
|
+
const stdin = process.stdin;
|
|
1081
|
+
stdin.setRawMode(true);
|
|
1082
|
+
stdin.resume();
|
|
1083
|
+
stdin.setEncoding("utf8");
|
|
1084
|
+
let password = "";
|
|
1085
|
+
const onData = (ch) => {
|
|
1086
|
+
ch = ch.toString();
|
|
1087
|
+
if (ch === "\n" || ch === "\r" || ch === "\u0004") {
|
|
1088
|
+
stdin.setRawMode(false);
|
|
1089
|
+
stdin.pause();
|
|
1090
|
+
stdin.removeListener("data", onData);
|
|
1091
|
+
process.stdout.write("\n");
|
|
1092
|
+
resolveP(password);
|
|
1093
|
+
} else if (ch === "\u0003") {
|
|
1094
|
+
// Ctrl+C
|
|
1095
|
+
stdin.setRawMode(false);
|
|
1096
|
+
process.stdout.write("\n");
|
|
1097
|
+
process.exit(0);
|
|
1098
|
+
} else if (ch === "\u007F" || ch === "\b") {
|
|
1099
|
+
if (password.length > 0) password = password.slice(0, -1);
|
|
1100
|
+
} else {
|
|
1101
|
+
password += ch;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
stdin.on("data", onData);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/** Read ~/.drawlatch/.env and report whether a dashboard password hash is set. */
|
|
1109
|
+
function isPasswordConfigured() {
|
|
1110
|
+
try {
|
|
1111
|
+
const envVars = loadEnvFileVars();
|
|
1112
|
+
return !!envVars.AUTH_PASSWORD_HASH;
|
|
1113
|
+
} catch {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
387
1118
|
// ── PID utilities ─────────────────────────────────────────────────
|
|
388
1119
|
|
|
389
1120
|
function isProcessAlive(pid) {
|
|
@@ -420,11 +1151,16 @@ function cleanPidFile() {
|
|
|
420
1151
|
|
|
421
1152
|
// ── Health check utilities ────────────────────────────────────────
|
|
422
1153
|
|
|
1154
|
+
/** Resolve the host for client connections — 0.0.0.0 is a bind address, not connectable. */
|
|
1155
|
+
function connectHost(host) {
|
|
1156
|
+
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
423
1159
|
async function healthCheck(host, port) {
|
|
424
1160
|
try {
|
|
425
1161
|
const controller = new AbortController();
|
|
426
1162
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
427
|
-
const res = await fetch(`http://${host}:${port}/health`, {
|
|
1163
|
+
const res = await fetch(`http://${connectHost(host)}:${port}/health`, {
|
|
428
1164
|
signal: controller.signal,
|
|
429
1165
|
});
|
|
430
1166
|
clearTimeout(timeout);
|
|
@@ -438,7 +1174,7 @@ async function healthCheckFull(host, port) {
|
|
|
438
1174
|
try {
|
|
439
1175
|
const controller = new AbortController();
|
|
440
1176
|
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
441
|
-
const res = await fetch(`http://${host}:${port}/health`, {
|
|
1177
|
+
const res = await fetch(`http://${connectHost(host)}:${port}/health`, {
|
|
442
1178
|
signal: controller.signal,
|
|
443
1179
|
});
|
|
444
1180
|
clearTimeout(timeout);
|
|
@@ -449,6 +1185,16 @@ async function healthCheckFull(host, port) {
|
|
|
449
1185
|
}
|
|
450
1186
|
}
|
|
451
1187
|
|
|
1188
|
+
async function waitForTunnelUrl(host, port, timeoutMs) {
|
|
1189
|
+
const start = Date.now();
|
|
1190
|
+
while (Date.now() - start < timeoutMs) {
|
|
1191
|
+
const data = await healthCheckFull(host, port);
|
|
1192
|
+
if (data?.tunnelUrl) return data.tunnelUrl;
|
|
1193
|
+
await sleep(500);
|
|
1194
|
+
}
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
452
1198
|
async function waitForHealth(host, port, timeoutMs) {
|
|
453
1199
|
const start = Date.now();
|
|
454
1200
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -476,18 +1222,18 @@ function ensureConfigDir() {
|
|
|
476
1222
|
// ── Diagnostic utilities ──────────────────────────────────────────
|
|
477
1223
|
|
|
478
1224
|
async function diagnoseStartFailure() {
|
|
479
|
-
if (!existsSync(LOG_FILE))
|
|
1225
|
+
if (!existsSync(LOG_FILE)) {
|
|
1226
|
+
console.log("\n No log file found. The server may not have started at all.");
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
480
1229
|
try {
|
|
481
1230
|
const content = readFileSync(LOG_FILE, "utf-8");
|
|
482
|
-
const lines = content.split("\n").slice(-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
} else if (eacces) {
|
|
489
|
-
console.log("\n Error: Permission denied.");
|
|
490
|
-
console.log(" Try using a port >= 1024.");
|
|
1231
|
+
const lines = content.split("\n").filter(Boolean).slice(-15);
|
|
1232
|
+
if (lines.length > 0) {
|
|
1233
|
+
console.log("\n Recent logs:");
|
|
1234
|
+
for (const line of lines) {
|
|
1235
|
+
console.log(` ${line}`);
|
|
1236
|
+
}
|
|
491
1237
|
}
|
|
492
1238
|
} catch {
|
|
493
1239
|
// Best effort
|
|
@@ -510,6 +1256,120 @@ function sleep(ms) {
|
|
|
510
1256
|
return new Promise((r) => setTimeout(r, ms));
|
|
511
1257
|
}
|
|
512
1258
|
|
|
1259
|
+
// ── Version check utilities ───────────────────────────────────────
|
|
1260
|
+
|
|
1261
|
+
const NPM_PACKAGE_NAME = pkgJson.name;
|
|
1262
|
+
|
|
1263
|
+
function readVersionCache() {
|
|
1264
|
+
try {
|
|
1265
|
+
if (!existsSync(VERSION_CACHE_FILE)) return null;
|
|
1266
|
+
const data = JSON.parse(readFileSync(VERSION_CACHE_FILE, "utf-8"));
|
|
1267
|
+
if (data.checkedAt && Date.now() - data.checkedAt < VERSION_CHECK_TTL_MS) {
|
|
1268
|
+
return data.latestVersion;
|
|
1269
|
+
}
|
|
1270
|
+
return null; // stale
|
|
1271
|
+
} catch {
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function writeVersionCache(latestVersion) {
|
|
1277
|
+
try {
|
|
1278
|
+
ensureConfigDir();
|
|
1279
|
+
writeFileSync(
|
|
1280
|
+
VERSION_CACHE_FILE,
|
|
1281
|
+
JSON.stringify({ latestVersion, checkedAt: Date.now() }) + "\n",
|
|
1282
|
+
{ mode: 0o600 },
|
|
1283
|
+
);
|
|
1284
|
+
} catch {
|
|
1285
|
+
// Best effort
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function fetchLatestVersion() {
|
|
1290
|
+
try {
|
|
1291
|
+
const controller = new AbortController();
|
|
1292
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1293
|
+
const res = await fetch(
|
|
1294
|
+
`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`,
|
|
1295
|
+
{ signal: controller.signal },
|
|
1296
|
+
);
|
|
1297
|
+
clearTimeout(timeout);
|
|
1298
|
+
if (!res.ok) return null;
|
|
1299
|
+
const data = await res.json();
|
|
1300
|
+
return data.version || null;
|
|
1301
|
+
} catch {
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function compareVersions(a, b) {
|
|
1307
|
+
// Returns > 0 if a > b, < 0 if a < b, 0 if equal
|
|
1308
|
+
// Handles pre-release: 1.0.0 > 1.0.0-alpha.1
|
|
1309
|
+
const parseVer = (v) => {
|
|
1310
|
+
const [core, pre] = v.split("-", 2);
|
|
1311
|
+
const parts = core.split(".").map(Number);
|
|
1312
|
+
return { parts, pre: pre || null };
|
|
1313
|
+
};
|
|
1314
|
+
const va = parseVer(a);
|
|
1315
|
+
const vb = parseVer(b);
|
|
1316
|
+
|
|
1317
|
+
// Compare core version parts
|
|
1318
|
+
const maxLen = Math.max(va.parts.length, vb.parts.length);
|
|
1319
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1320
|
+
const pa = va.parts[i] || 0;
|
|
1321
|
+
const pb = vb.parts[i] || 0;
|
|
1322
|
+
if (pa !== pb) return pa - pb;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Same core: no pre-release > pre-release
|
|
1326
|
+
if (!va.pre && vb.pre) return 1;
|
|
1327
|
+
if (va.pre && !vb.pre) return -1;
|
|
1328
|
+
if (!va.pre && !vb.pre) return 0;
|
|
1329
|
+
|
|
1330
|
+
// Both have pre-release: compare segments
|
|
1331
|
+
const aParts = va.pre.split(".");
|
|
1332
|
+
const bParts = vb.pre.split(".");
|
|
1333
|
+
const preLen = Math.max(aParts.length, bParts.length);
|
|
1334
|
+
for (let i = 0; i < preLen; i++) {
|
|
1335
|
+
const sa = aParts[i];
|
|
1336
|
+
const sb = bParts[i];
|
|
1337
|
+
if (sa === undefined) return -1;
|
|
1338
|
+
if (sb === undefined) return 1;
|
|
1339
|
+
const na = Number(sa);
|
|
1340
|
+
const nb = Number(sb);
|
|
1341
|
+
const aIsNum = !isNaN(na);
|
|
1342
|
+
const bIsNum = !isNaN(nb);
|
|
1343
|
+
if (aIsNum && bIsNum) {
|
|
1344
|
+
if (na !== nb) return na - nb;
|
|
1345
|
+
} else if (aIsNum) {
|
|
1346
|
+
return -1; // numbers sort before strings
|
|
1347
|
+
} else if (bIsNum) {
|
|
1348
|
+
return 1;
|
|
1349
|
+
} else {
|
|
1350
|
+
if (sa < sb) return -1;
|
|
1351
|
+
if (sa > sb) return 1;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return 0;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async function checkForUpdate() {
|
|
1358
|
+
// Try cache first
|
|
1359
|
+
const cached = readVersionCache();
|
|
1360
|
+
if (cached) return cached !== VERSION && compareVersions(cached, VERSION) > 0 ? cached : null;
|
|
1361
|
+
|
|
1362
|
+
// Fetch in background-ish (awaited but with short timeout)
|
|
1363
|
+
const latest = await fetchLatestVersion();
|
|
1364
|
+
if (latest) writeVersionCache(latest);
|
|
1365
|
+
if (latest && latest !== VERSION && compareVersions(latest, VERSION) > 0) return latest;
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function formatUpdateNotice(latestVersion) {
|
|
1370
|
+
return `\n Update available: ${VERSION} → ${latestVersion}\n Run: npm install -g ${NPM_PACKAGE_NAME}\n`;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
513
1373
|
// ── Help text ─────────────────────────────────────────────────────
|
|
514
1374
|
|
|
515
1375
|
function printHelp() {
|
|
@@ -519,13 +1379,18 @@ drawlatch v${VERSION}
|
|
|
519
1379
|
Usage: drawlatch [command] [options]
|
|
520
1380
|
|
|
521
1381
|
Commands:
|
|
1382
|
+
init Set up drawlatch server (keys, config, .env)
|
|
522
1383
|
start Start the remote server (background by default)
|
|
523
1384
|
stop Stop the background remote server
|
|
524
1385
|
restart Restart the background remote server
|
|
525
1386
|
status Show server status (PID, port, uptime, health, sessions)
|
|
526
1387
|
logs View and follow remote server logs
|
|
527
1388
|
config Show effective configuration
|
|
1389
|
+
watch Watch ingestor events in real time
|
|
1390
|
+
doctor Validate setup and diagnose issues
|
|
528
1391
|
generate-keys Generate Ed25519 + X25519 keypairs
|
|
1392
|
+
sync Exchange keys with a callboard instance
|
|
1393
|
+
set-password Set/change the dashboard password (alias: change-password)
|
|
529
1394
|
|
|
530
1395
|
Options:
|
|
531
1396
|
-h, --help Show this help message
|
|
@@ -534,14 +1399,13 @@ Options:
|
|
|
534
1399
|
Running 'drawlatch' with no arguments shows status (if running) or this help.
|
|
535
1400
|
|
|
536
1401
|
Examples:
|
|
537
|
-
drawlatch
|
|
1402
|
+
drawlatch init Set up the remote server
|
|
538
1403
|
drawlatch start Start remote server in background
|
|
539
1404
|
drawlatch start -f Start remote server in foreground
|
|
540
|
-
drawlatch start --
|
|
1405
|
+
drawlatch start -f --tunnel Start with a public tunnel for webhooks
|
|
1406
|
+
drawlatch doctor Validate full setup
|
|
541
1407
|
drawlatch status Check if server is running
|
|
542
1408
|
drawlatch logs -n 100 View last 100 log lines
|
|
543
|
-
drawlatch generate-keys remote Generate remote server keypair
|
|
544
|
-
drawlatch generate-keys local mybot Generate local keypair for alias "mybot"
|
|
545
1409
|
`);
|
|
546
1410
|
}
|
|
547
1411
|
|
|
@@ -555,6 +1419,7 @@ Usage: drawlatch start [options]
|
|
|
555
1419
|
|
|
556
1420
|
Options:
|
|
557
1421
|
-f, --foreground Run in foreground (default when no command given)
|
|
1422
|
+
-t, --tunnel Start a Cloudflare tunnel for webhook ingestion (requires cloudflared)
|
|
558
1423
|
--port <number> Override the configured port
|
|
559
1424
|
--host <address> Override the configured host
|
|
560
1425
|
-h, --help Show this help message
|
|
@@ -617,19 +1482,42 @@ function printLogsHelp() {
|
|
|
617
1482
|
console.log(`
|
|
618
1483
|
drawlatch logs
|
|
619
1484
|
|
|
620
|
-
View server logs.
|
|
1485
|
+
View server logs (request/response audit lines are hidden by default).
|
|
621
1486
|
|
|
622
1487
|
Usage: drawlatch logs [options]
|
|
623
1488
|
|
|
624
1489
|
Options:
|
|
625
1490
|
-n, --lines <number> Number of lines to show (default: 50)
|
|
626
|
-
--
|
|
1491
|
+
--follow Follow/tail the log output (default: print and exit)
|
|
1492
|
+
--requests Include request/response audit lines (noisy with polling)
|
|
627
1493
|
-h, --help Show this help message
|
|
628
1494
|
|
|
629
1495
|
Log file: ~/.drawlatch/logs/drawlatch.log
|
|
630
1496
|
`);
|
|
631
1497
|
}
|
|
632
1498
|
|
|
1499
|
+
function printWatchHelp() {
|
|
1500
|
+
console.log(`
|
|
1501
|
+
drawlatch watch
|
|
1502
|
+
|
|
1503
|
+
Watch ingestor events in real time.
|
|
1504
|
+
|
|
1505
|
+
Usage: drawlatch watch [source] [options]
|
|
1506
|
+
|
|
1507
|
+
Arguments:
|
|
1508
|
+
source Filter to a specific connection (e.g., "discord-bot", "github")
|
|
1509
|
+
|
|
1510
|
+
Options:
|
|
1511
|
+
--full Show full event payload (default: truncate to 100 chars)
|
|
1512
|
+
-h, --help Show this help message
|
|
1513
|
+
|
|
1514
|
+
Examples:
|
|
1515
|
+
drawlatch watch Watch all events
|
|
1516
|
+
drawlatch watch github Watch only GitHub events
|
|
1517
|
+
drawlatch watch discord-bot --full Watch Discord events with full payloads
|
|
1518
|
+
`);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
633
1521
|
function printConfigHelp() {
|
|
634
1522
|
console.log(`
|
|
635
1523
|
drawlatch config
|
|
@@ -647,6 +1535,86 @@ server configuration including callers and connections.
|
|
|
647
1535
|
`);
|
|
648
1536
|
}
|
|
649
1537
|
|
|
1538
|
+
function printInitHelp() {
|
|
1539
|
+
console.log(`
|
|
1540
|
+
drawlatch init
|
|
1541
|
+
|
|
1542
|
+
Set up the drawlatch remote server. Generates server keys, creates
|
|
1543
|
+
config files, and scaffolds a .env template.
|
|
1544
|
+
|
|
1545
|
+
Callers are added separately via 'drawlatch sync' after the server
|
|
1546
|
+
is running.
|
|
1547
|
+
|
|
1548
|
+
Usage: drawlatch init [options]
|
|
1549
|
+
|
|
1550
|
+
Options:
|
|
1551
|
+
-h, --help Show this help message
|
|
1552
|
+
|
|
1553
|
+
All steps are idempotent — safe to re-run without overwriting existing files.
|
|
1554
|
+
`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
function printDoctorHelp() {
|
|
1558
|
+
console.log(`
|
|
1559
|
+
drawlatch doctor
|
|
1560
|
+
|
|
1561
|
+
Validate your drawlatch setup and diagnose common issues.
|
|
1562
|
+
|
|
1563
|
+
Usage: drawlatch doctor [options]
|
|
1564
|
+
|
|
1565
|
+
Options:
|
|
1566
|
+
--full Include a live handshake test (requires server running)
|
|
1567
|
+
-h, --help Show this help message
|
|
1568
|
+
|
|
1569
|
+
Checks config files, keys, peer directories, secrets, and server health.
|
|
1570
|
+
Each failure includes the exact command to fix it.
|
|
1571
|
+
`);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function printSyncHelp() {
|
|
1575
|
+
console.log(`
|
|
1576
|
+
drawlatch sync
|
|
1577
|
+
|
|
1578
|
+
Exchange keys with a callboard instance using a double-code approval flow.
|
|
1579
|
+
|
|
1580
|
+
Usage: drawlatch sync [options]
|
|
1581
|
+
|
|
1582
|
+
Options:
|
|
1583
|
+
--ttl <seconds> Sync session timeout (default: 300)
|
|
1584
|
+
-h, --help Show this help message
|
|
1585
|
+
|
|
1586
|
+
Flow:
|
|
1587
|
+
1. Run 'drawlatch sync' — displays an invite code and encryption key
|
|
1588
|
+
2. Give both values to the callboard operator
|
|
1589
|
+
3. They enter them into callboard, which generates a confirm code
|
|
1590
|
+
4. Enter the confirm code when prompted
|
|
1591
|
+
5. Callboard sends the encrypted sync request
|
|
1592
|
+
6. Keys are exchanged automatically
|
|
1593
|
+
|
|
1594
|
+
The server must be running (drawlatch start) before using this command.
|
|
1595
|
+
`);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function printSetPasswordHelp(cmd = "set-password") {
|
|
1599
|
+
console.log(`
|
|
1600
|
+
drawlatch ${cmd}
|
|
1601
|
+
|
|
1602
|
+
Set or change the password that gates the admin dashboard and /api/admin/*.
|
|
1603
|
+
|
|
1604
|
+
Usage: drawlatch ${cmd} [options]
|
|
1605
|
+
|
|
1606
|
+
Options:
|
|
1607
|
+
-h, --help Show this help message
|
|
1608
|
+
|
|
1609
|
+
Prompts for a new password (hidden in a TTY; reads two lines when piped),
|
|
1610
|
+
hashes it with scrypt, and stores AUTH_PASSWORD_HASH / AUTH_PASSWORD_SALT in
|
|
1611
|
+
${ENV_FILE}.
|
|
1612
|
+
|
|
1613
|
+
The password must be at least 8 characters. 'change-password' is an alias.
|
|
1614
|
+
Restart the server afterwards (drawlatch restart) to apply the new password.
|
|
1615
|
+
`);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
650
1618
|
function printGenerateKeysHelp() {
|
|
651
1619
|
console.log(`
|
|
652
1620
|
drawlatch generate-keys
|
|
@@ -656,10 +1624,11 @@ Generate Ed25519 + X25519 keypairs for authentication and encryption.
|
|
|
656
1624
|
Usage: drawlatch generate-keys <subcommand> [options]
|
|
657
1625
|
|
|
658
1626
|
Subcommands:
|
|
659
|
-
|
|
1627
|
+
caller [alias] Generate caller keypair
|
|
660
1628
|
Alias defaults to "default" if omitted.
|
|
661
|
-
Keys are stored in keys/
|
|
662
|
-
|
|
1629
|
+
Keys are stored in keys/callers/<alias>/
|
|
1630
|
+
server Generate server keypair
|
|
1631
|
+
Keys are stored in keys/server/
|
|
663
1632
|
--dir <path> Generate keypair in a custom directory
|
|
664
1633
|
show <path> Show fingerprint of an existing keypair
|
|
665
1634
|
|