@wolpertingerlabs/drawlatch 1.0.0-alpha.9.3 → 1.0.0-alpha.9.5
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/bin/drawlatch.js +20 -14
- package/dist/remote/ingestors/manager.d.ts +8 -1
- package/dist/remote/ingestors/manager.js +24 -10
- package/dist/remote/server.js +35 -10
- package/package.json +1 -1
package/bin/drawlatch.js
CHANGED
|
@@ -356,10 +356,16 @@ async function cmdStart() {
|
|
|
356
356
|
}
|
|
357
357
|
console.log(` Logs: drawlatch logs`);
|
|
358
358
|
} else {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
const stillAlive = isProcessAlive(child.pid);
|
|
360
|
+
if (stillAlive) {
|
|
361
|
+
console.log(
|
|
362
|
+
`\nServer started (PID ${child.pid}) but health check did not pass within 5s.`,
|
|
363
|
+
);
|
|
364
|
+
console.log(` The server process is still running — it may need more time.`);
|
|
365
|
+
} else {
|
|
366
|
+
console.log(`\nServer process (PID ${child.pid}) exited before becoming healthy.`);
|
|
367
|
+
cleanPidFile();
|
|
368
|
+
}
|
|
363
369
|
await diagnoseStartFailure();
|
|
364
370
|
}
|
|
365
371
|
}
|
|
@@ -956,18 +962,18 @@ function ensureConfigDir() {
|
|
|
956
962
|
// ── Diagnostic utilities ──────────────────────────────────────────
|
|
957
963
|
|
|
958
964
|
async function diagnoseStartFailure() {
|
|
959
|
-
if (!existsSync(LOG_FILE))
|
|
965
|
+
if (!existsSync(LOG_FILE)) {
|
|
966
|
+
console.log("\n No log file found. The server may not have started at all.");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
960
969
|
try {
|
|
961
970
|
const content = readFileSync(LOG_FILE, "utf-8");
|
|
962
|
-
const lines = content.split("\n").slice(-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
} else if (eacces) {
|
|
969
|
-
console.log("\n Error: Permission denied.");
|
|
970
|
-
console.log(" Try using a port >= 1024.");
|
|
971
|
+
const lines = content.split("\n").filter(Boolean).slice(-15);
|
|
972
|
+
if (lines.length > 0) {
|
|
973
|
+
console.log("\n Recent logs:");
|
|
974
|
+
for (const line of lines) {
|
|
975
|
+
console.log(` ${line}`);
|
|
976
|
+
}
|
|
971
977
|
}
|
|
972
978
|
} catch {
|
|
973
979
|
// Best effort
|
|
@@ -43,7 +43,14 @@ export declare class IngestorManager {
|
|
|
43
43
|
private readonly config;
|
|
44
44
|
/** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
|
|
45
45
|
private ingestors;
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Optional config loader for hot-reload support. When provided, `startOne()`
|
|
48
|
+
* uses it to get fresh config from disk instead of the constructor snapshot.
|
|
49
|
+
*/
|
|
50
|
+
private configLoader;
|
|
51
|
+
constructor(config: RemoteServerConfig, configLoader?: () => RemoteServerConfig);
|
|
52
|
+
/** Return fresh config if a loader is available, otherwise the constructor snapshot. */
|
|
53
|
+
private getConfig;
|
|
47
54
|
/**
|
|
48
55
|
* Start ingestors for all callers whose connections have an `ingestor` config.
|
|
49
56
|
* Called once when the remote server starts listening.
|
|
@@ -48,8 +48,18 @@ export class IngestorManager {
|
|
|
48
48
|
config;
|
|
49
49
|
/** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
|
|
50
50
|
ingestors = new Map();
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Optional config loader for hot-reload support. When provided, `startOne()`
|
|
53
|
+
* uses it to get fresh config from disk instead of the constructor snapshot.
|
|
54
|
+
*/
|
|
55
|
+
configLoader;
|
|
56
|
+
constructor(config, configLoader) {
|
|
52
57
|
this.config = config;
|
|
58
|
+
this.configLoader = configLoader;
|
|
59
|
+
}
|
|
60
|
+
/** Return fresh config if a loader is available, otherwise the constructor snapshot. */
|
|
61
|
+
getConfig() {
|
|
62
|
+
return this.configLoader ? this.configLoader() : this.config;
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
55
65
|
* Start ingestors for all callers whose connections have an `ingestor` config.
|
|
@@ -228,7 +238,11 @@ export class IngestorManager {
|
|
|
228
238
|
* When omitted, starts the default instance (or all instances if listenerInstances is defined).
|
|
229
239
|
*/
|
|
230
240
|
async startOne(callerAlias, connectionAlias, instanceId) {
|
|
231
|
-
|
|
241
|
+
// Get fresh config (from disk in production, or constructor snapshot in tests)
|
|
242
|
+
// so we pick up changes made by tool handlers (e.g. set_connection_enabled,
|
|
243
|
+
// set_listener_params, set_secrets) without requiring a server restart.
|
|
244
|
+
const config = this.getConfig();
|
|
245
|
+
const callerConfig = config.callers[callerAlias];
|
|
232
246
|
if (!callerConfig) {
|
|
233
247
|
return {
|
|
234
248
|
success: false,
|
|
@@ -244,7 +258,7 @@ export class IngestorManager {
|
|
|
244
258
|
error: `Caller does not have connection: ${connectionAlias}`,
|
|
245
259
|
};
|
|
246
260
|
}
|
|
247
|
-
const rawRoutes = resolveCallerRoutes(
|
|
261
|
+
const rawRoutes = resolveCallerRoutes(config, callerAlias);
|
|
248
262
|
const callerEnvResolved = resolveSecrets(callerConfig.env ?? {});
|
|
249
263
|
const resolvedRoutes = resolveRoutes(rawRoutes, callerEnvResolved, callerAlias);
|
|
250
264
|
const rawRoute = rawRoutes[connectionIndex];
|
|
@@ -258,7 +272,7 @@ export class IngestorManager {
|
|
|
258
272
|
}
|
|
259
273
|
// If a specific instanceId is given, start just that one
|
|
260
274
|
if (instanceId) {
|
|
261
|
-
return this.startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute);
|
|
275
|
+
return this.startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute, config);
|
|
262
276
|
}
|
|
263
277
|
// If listenerInstances is defined, start all instances
|
|
264
278
|
const instances = callerConfig.listenerInstances?.[connectionAlias];
|
|
@@ -267,15 +281,15 @@ export class IngestorManager {
|
|
|
267
281
|
for (const [instId, instOverrides] of Object.entries(instances)) {
|
|
268
282
|
if (instOverrides.disabled)
|
|
269
283
|
continue;
|
|
270
|
-
results.push(await this.startOneInstance(callerAlias, connectionAlias, instId, rawRoute, resolvedRoute));
|
|
284
|
+
results.push(await this.startOneInstance(callerAlias, connectionAlias, instId, rawRoute, resolvedRoute, config));
|
|
271
285
|
}
|
|
272
286
|
return results;
|
|
273
287
|
}
|
|
274
288
|
// Single default instance
|
|
275
|
-
return this.startOneInstance(callerAlias, connectionAlias, undefined, rawRoute, resolvedRoute);
|
|
289
|
+
return this.startOneInstance(callerAlias, connectionAlias, undefined, rawRoute, resolvedRoute, config);
|
|
276
290
|
}
|
|
277
291
|
/** Internal: start a single specific instance. */
|
|
278
|
-
async startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute) {
|
|
292
|
+
async startOneInstance(callerAlias, connectionAlias, instanceId, rawRoute, resolvedRoute, config) {
|
|
279
293
|
const key = makeKey(callerAlias, connectionAlias, instanceId ?? DEFAULT_INSTANCE_ID);
|
|
280
294
|
// If already running, return current status
|
|
281
295
|
const existing = this.ingestors.get(key);
|
|
@@ -289,10 +303,10 @@ export class IngestorManager {
|
|
|
289
303
|
// Remove stopped instance to recreate
|
|
290
304
|
this.ingestors.delete(key);
|
|
291
305
|
}
|
|
292
|
-
const
|
|
306
|
+
const callerCfg = config.callers[callerAlias];
|
|
293
307
|
const overrides = instanceId
|
|
294
|
-
?
|
|
295
|
-
:
|
|
308
|
+
? callerCfg.listenerInstances?.[connectionAlias]?.[instanceId]
|
|
309
|
+
: callerCfg.ingestorOverrides?.[connectionAlias];
|
|
296
310
|
const effectiveConfig = IngestorManager.mergeIngestorConfig(rawRoute.ingestor, overrides);
|
|
297
311
|
// Apply instance params
|
|
298
312
|
const instanceSecrets = { ...resolvedRoute.secrets };
|
package/dist/remote/server.js
CHANGED
|
@@ -30,11 +30,11 @@ import { isSecretSetForCaller, setCallerSecrets } from '../shared/env-utils.js';
|
|
|
30
30
|
function loadEnvFile() {
|
|
31
31
|
const configDirEnvPath = getEnvFilePath();
|
|
32
32
|
if (fs.existsSync(configDirEnvPath)) {
|
|
33
|
-
dotenv.config({ path: configDirEnvPath });
|
|
33
|
+
dotenv.config({ path: configDirEnvPath, quiet: true });
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
// Backward compat: fall back to cwd .env
|
|
37
|
-
const result = dotenv.config();
|
|
37
|
+
const result = dotenv.config({ quiet: true });
|
|
38
38
|
if (result.parsed) {
|
|
39
39
|
console.warn(`[remote] Loaded .env from working directory. ` +
|
|
40
40
|
`Move it to ${configDirEnvPath} for portable operation.`);
|
|
@@ -1030,8 +1030,14 @@ export function createApp(options = {}) {
|
|
|
1030
1030
|
const ownKeys = options.ownKeys ?? loadKeyBundle(getServerKeysDir());
|
|
1031
1031
|
const authorizedPeers = options.authorizedPeers ?? loadCallerPeers(config.callers);
|
|
1032
1032
|
rateLimitPerMinute = config.rateLimitPerMinute;
|
|
1033
|
-
// Create or use the provided ingestor manager
|
|
1034
|
-
|
|
1033
|
+
// Create or use the provided ingestor manager.
|
|
1034
|
+
// When config is loaded from disk (production), pass loadRemoteConfig as the
|
|
1035
|
+
// config loader so startOne()/restartOne() read fresh config, picking up
|
|
1036
|
+
// changes made by tool handlers without requiring a server restart.
|
|
1037
|
+
// When config is injected via options (tests), omit the loader so the
|
|
1038
|
+
// IngestorManager uses the injected config snapshot.
|
|
1039
|
+
const configLoader = options.config ? undefined : loadRemoteConfig;
|
|
1040
|
+
const ingestorManager = options.ingestorManager ?? new IngestorManager(config, configLoader);
|
|
1035
1041
|
app.locals.ingestorManager = ingestorManager;
|
|
1036
1042
|
// Log connector and caller summary
|
|
1037
1043
|
const connectorCount = config.connectors?.length ?? 0;
|
|
@@ -1076,9 +1082,12 @@ export function createApp(options = {}) {
|
|
|
1076
1082
|
// Look up the caller alias by matching the returned PublicKeyBundle
|
|
1077
1083
|
const matchedPeer = authorizedPeers.find((p) => p.keys === initiatorPubKey);
|
|
1078
1084
|
const callerAlias = matchedPeer?.alias ?? 'unknown';
|
|
1085
|
+
// Reload config from disk so new sessions pick up changes made by tool
|
|
1086
|
+
// handlers (e.g. set_connection_enabled, set_secrets) without a restart.
|
|
1087
|
+
const freshConfig = options.config ?? loadRemoteConfig();
|
|
1079
1088
|
// Resolve per-caller routes (with optional env overrides)
|
|
1080
|
-
const callerRoutes = resolveCallerRoutes(
|
|
1081
|
-
const caller =
|
|
1089
|
+
const callerRoutes = resolveCallerRoutes(freshConfig, callerAlias);
|
|
1090
|
+
const caller = freshConfig.callers[callerAlias];
|
|
1082
1091
|
const callerEnvResolved = resolveSecrets(caller.env ?? {});
|
|
1083
1092
|
const callerResolvedRoutes = resolveRoutes(callerRoutes, callerEnvResolved, callerAlias);
|
|
1084
1093
|
// Store pending handshake for the finish step
|
|
@@ -1316,19 +1325,21 @@ export function createApp(options = {}) {
|
|
|
1316
1325
|
res.status(400).json({ error: 'INVALID_PAYLOAD', detail: `Invalid public keys: ${msg}` });
|
|
1317
1326
|
return;
|
|
1318
1327
|
}
|
|
1328
|
+
// Reload config from disk so we don't clobber changes made since startup
|
|
1329
|
+
const freshConfig = options.config ?? loadRemoteConfig();
|
|
1319
1330
|
// Register caller in config if not already present
|
|
1320
|
-
if (!(callerAlias in
|
|
1321
|
-
|
|
1331
|
+
if (!(callerAlias in freshConfig.callers)) {
|
|
1332
|
+
freshConfig.callers[callerAlias] = {
|
|
1322
1333
|
connections: [],
|
|
1323
1334
|
};
|
|
1324
|
-
saveRemoteConfig(
|
|
1335
|
+
saveRemoteConfig(freshConfig);
|
|
1325
1336
|
console.log(`[sync] Registered new caller "${callerAlias}" (0 connections — configure manually)`);
|
|
1326
1337
|
}
|
|
1327
1338
|
else {
|
|
1328
1339
|
console.log(`[sync] Caller "${callerAlias}" already exists, updated peer keys`);
|
|
1329
1340
|
}
|
|
1330
1341
|
// Reload authorized peers so the new caller can connect immediately
|
|
1331
|
-
const newPeer = loadCallerPeers({ [callerAlias]:
|
|
1342
|
+
const newPeer = loadCallerPeers({ [callerAlias]: freshConfig.callers[callerAlias] });
|
|
1332
1343
|
for (const p of newPeer) {
|
|
1333
1344
|
if (!authorizedPeers.find((existing) => existing.alias === p.alias)) {
|
|
1334
1345
|
authorizedPeers.push(p);
|
|
@@ -1407,6 +1418,7 @@ export function createApp(options = {}) {
|
|
|
1407
1418
|
}
|
|
1408
1419
|
// ── Start ──────────────────────────────────────────────────────────────────
|
|
1409
1420
|
export function main() {
|
|
1421
|
+
console.log('[remote] Starting drawlatch server...');
|
|
1410
1422
|
// Pre-flight validation: check for common setup issues before starting
|
|
1411
1423
|
const remoteConfigPath = getRemoteConfigPath();
|
|
1412
1424
|
if (!fs.existsSync(remoteConfigPath)) {
|
|
@@ -1443,6 +1455,7 @@ export function main() {
|
|
|
1443
1455
|
let stopTunnel;
|
|
1444
1456
|
const server = app.listen(port, host, () => void (async () => {
|
|
1445
1457
|
console.log(`[remote] Secure remote server listening on ${host}:${port}`);
|
|
1458
|
+
console.log(`[remote] PID: ${process.pid}, Node: ${process.version}`);
|
|
1446
1459
|
// If a tunnel was requested, start it before ingestors so that
|
|
1447
1460
|
// process.env.DRAWLATCH_TUNNEL_URL is available during secret resolution.
|
|
1448
1461
|
if (useTunnel) {
|
|
@@ -1513,6 +1526,18 @@ export function main() {
|
|
|
1513
1526
|
process.exit(1);
|
|
1514
1527
|
}, 10_000).unref();
|
|
1515
1528
|
};
|
|
1529
|
+
server.on('error', (err) => {
|
|
1530
|
+
if (err.code === 'EADDRINUSE') {
|
|
1531
|
+
console.error(`[remote] Error: Port ${port} is already in use.`);
|
|
1532
|
+
}
|
|
1533
|
+
else if (err.code === 'EACCES') {
|
|
1534
|
+
console.error(`[remote] Error: Permission denied for ${host}:${port}. Try a port >= 1024.`);
|
|
1535
|
+
}
|
|
1536
|
+
else {
|
|
1537
|
+
console.error(`[remote] Server error:`, err);
|
|
1538
|
+
}
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
});
|
|
1516
1541
|
process.on('SIGTERM', shutdown);
|
|
1517
1542
|
process.on('SIGINT', shutdown);
|
|
1518
1543
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wolpertingerlabs/drawlatch",
|
|
3
|
-
"version": "1.0.0-alpha.9.
|
|
3
|
+
"version": "1.0.0-alpha.9.5",
|
|
4
4
|
"description": "Encrypted MCP proxy with mutual authentication. Local MCP server forwards requests through an encrypted channel to a remote secrets-holding server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/mcp/server.js",
|