@hua-labs/tap 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -88,7 +88,7 @@ interface HeadlessConfig {
88
88
  qualitySeverityFloor: "critical" | "high" | "medium";
89
89
  }
90
90
  interface AppServerAuthState {
91
- mode: "query-token";
91
+ mode: "subprotocol" | "query-token";
92
92
  protectedUrl: string;
93
93
  upstreamUrl: string;
94
94
  tokenPath: string;
@@ -442,6 +442,8 @@ declare function getConfig(options?: StateApiOptions): {
442
442
  interface HttpServerOptions extends StateApiOptions {
443
443
  /** Port to listen on (default: 4580) */
444
444
  port?: number;
445
+ /** Pre-set API token (default: auto-generated) */
446
+ token?: string;
445
447
  }
446
448
  /**
447
449
  * Start a localhost-only HTTP server for the tap State API.
@@ -449,6 +451,7 @@ interface HttpServerOptions extends StateApiOptions {
449
451
  */
450
452
  declare function startHttpServer(options?: HttpServerOptions): Promise<{
451
453
  port: number;
454
+ token: string;
452
455
  close: () => Promise<void>;
453
456
  }>;
454
457
 
package/dist/index.mjs CHANGED
@@ -27,8 +27,8 @@ function findRepoRoot(startDir = process.cwd()) {
27
27
  if (fs.existsSync(path.join(dir, "package.json"))) {
28
28
  if (!_noGitWarned) {
29
29
  _setNoGitWarned();
30
- logWarn(
31
- "No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
30
+ log(
31
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
32
32
  );
33
33
  }
34
34
  return dir;
@@ -39,8 +39,8 @@ function findRepoRoot(startDir = process.cwd()) {
39
39
  }
40
40
  if (!_noGitWarned) {
41
41
  _setNoGitWarned();
42
- logWarn(
43
- "No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
42
+ log(
43
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
44
44
  );
45
45
  }
46
46
  return process.cwd();
@@ -82,9 +82,6 @@ function log(message) {
82
82
  function logSuccess(message) {
83
83
  if (!_jsonMode) console.log(` + ${message}`);
84
84
  }
85
- function logWarn(message) {
86
- if (!_jsonMode) console.log(` ! ${message}`);
87
- }
88
85
  function logError(message) {
89
86
  if (!_jsonMode) console.error(` x ${message}`);
90
87
  }
@@ -93,6 +90,16 @@ function logHeader(message) {
93
90
  ${message}
94
91
  `);
95
92
  }
93
+ function parseIntFlag(value, name, min, max) {
94
+ if (value === void 0) return void 0;
95
+ const parsed = Number(value);
96
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
97
+ throw new RangeError(
98
+ `Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
99
+ );
100
+ }
101
+ return parsed;
102
+ }
96
103
  function resolveInstanceId(identifier, state) {
97
104
  if (state.instances[identifier]) {
98
105
  return { ok: true, instanceId: identifier };
@@ -140,8 +147,8 @@ function findRepoRoot2(startDir = process.cwd()) {
140
147
  if (fs2.existsSync(path2.join(dir, "package.json"))) {
141
148
  if (!_noGitWarned) {
142
149
  _setNoGitWarned();
143
- console.error(
144
- "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
150
+ log(
151
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
145
152
  );
146
153
  }
147
154
  return dir;
@@ -152,8 +159,8 @@ function findRepoRoot2(startDir = process.cwd()) {
152
159
  }
153
160
  if (!_noGitWarned) {
154
161
  _setNoGitWarned();
155
- console.error(
156
- "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
162
+ log(
163
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
157
164
  );
158
165
  }
159
166
  return process.cwd();
@@ -556,18 +563,18 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
556
563
  return { command: null, args: [], env, sourcePath, warnings, issues };
557
564
  }
558
565
  const isBundled = sourcePath.endsWith(".mjs");
566
+ const isEphemeralSource = isEphemeralPath(sourcePath);
559
567
  let command = bunCommand;
560
568
  let args = [toForwardSlashPath(sourcePath)];
561
- if (!command && isBundled) {
562
- const isEphemeralSource = isEphemeralPath(sourcePath);
569
+ if (isEphemeralSource && isBundled) {
570
+ command = "npx";
571
+ args = ["@hua-labs/tap", "serve"];
572
+ warnings.push(
573
+ "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
574
+ );
575
+ } else if (!command && isBundled) {
563
576
  const isEphemeralNode = isEphemeralPath(process.execPath);
564
- if (isEphemeralSource) {
565
- command = "npx";
566
- args = ["@hua-labs/tap", "serve"];
567
- warnings.push(
568
- "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
569
- );
570
- } else if (isEphemeralNode) {
577
+ if (isEphemeralNode) {
571
578
  command = "node";
572
579
  warnings.push(
573
580
  "Detected ephemeral node path. Using `node` from PATH for MCP config stability."
@@ -575,11 +582,9 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
575
582
  } else {
576
583
  command = toForwardSlashPath(process.execPath);
577
584
  }
578
- if (!isEphemeralSource) {
579
- warnings.push(
580
- "bun not found; using node to run the compiled MCP server. Install bun for better performance."
581
- );
582
- }
585
+ warnings.push(
586
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
587
+ );
583
588
  }
584
589
  if (!command) {
585
590
  issues.push(
@@ -840,8 +845,11 @@ function resolvePowerShellCommand() {
840
845
  function resolveAuthGatewayScript(repoRoot) {
841
846
  const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
842
847
  const candidates = [
843
- path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
844
- path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
848
+ // Bundled: dist/bridges/ sibling (npm install / built package)
849
+ path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
850
+ // Source: src/bridges/ sibling (monorepo dev with ts runner)
851
+ path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
852
+ // Monorepo dist fallback
845
853
  path7.join(
846
854
  repoRoot,
847
855
  "packages",
@@ -894,10 +902,8 @@ async function allocateLoopbackPort(hostname) {
894
902
  });
895
903
  });
896
904
  }
897
- function buildProtectedAppServerUrl(publicUrl, token) {
898
- const url = new URL(publicUrl);
899
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
900
- return url.toString().replace(/\/(?=\?|$)/, "");
905
+ function buildProtectedAppServerUrl(publicUrl, _token) {
906
+ return publicUrl;
901
907
  }
902
908
  function readGatewayTokenFromPath(tokenPath) {
903
909
  return fs7.readFileSync(tokenPath, "utf8").trim();
@@ -1012,7 +1018,7 @@ async function createManagedAppServerAuth(options) {
1012
1018
  throw new Error("Failed to spawn app-server auth gateway");
1013
1019
  }
1014
1020
  return {
1015
- mode: "query-token",
1021
+ mode: "subprotocol",
1016
1022
  protectedUrl,
1017
1023
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
1018
1024
  tokenPath,
@@ -1199,7 +1205,7 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
1199
1205
  `Failed to find a free app-server port starting at ${basePort}`
1200
1206
  );
1201
1207
  }
1202
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
1208
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
1203
1209
  const WebSocket = getWebSocketCtor();
1204
1210
  if (!WebSocket) {
1205
1211
  return false;
@@ -1221,7 +1227,8 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1221
1227
  };
1222
1228
  const timer = setTimeout(() => finish(false), timeoutMs);
1223
1229
  try {
1224
- socket = new WebSocket(url);
1230
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
1231
+ socket = new WebSocket(url, protocols);
1225
1232
  socket.addEventListener("open", () => finish(true), { once: true });
1226
1233
  socket.addEventListener("error", () => finish(false), { once: true });
1227
1234
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -1230,10 +1237,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1230
1237
  }
1231
1238
  });
1232
1239
  }
1233
- async function waitForAppServerHealth(url, timeoutMs) {
1240
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
1234
1241
  const deadline = Date.now() + timeoutMs;
1235
1242
  while (Date.now() < deadline) {
1236
- if (await checkAppServerHealth(url)) {
1243
+ if (await checkAppServerHealth(
1244
+ url,
1245
+ APP_SERVER_HEALTH_TIMEOUT_MS,
1246
+ gatewayToken
1247
+ )) {
1237
1248
  return true;
1238
1249
  }
1239
1250
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -1498,8 +1509,9 @@ Or start it manually:
1498
1509
  throw new Error("Tap auth gateway token is missing after startup.");
1499
1510
  }
1500
1511
  const gatewayHealthy = await waitForAppServerHealth(
1501
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
1502
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
1512
+ effectiveUrl,
1513
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
1514
+ gatewayToken
1503
1515
  );
1504
1516
  if (!gatewayHealthy) {
1505
1517
  await terminateProcess(pid, options.platform);
@@ -1856,7 +1868,7 @@ function getBridgeStatus(stateDir, instanceId) {
1856
1868
  }
1857
1869
  return "running";
1858
1870
  }
1859
- var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, APP_SERVER_AUTH_QUERY_PARAM, APP_SERVER_AUTH_FILE_MODE;
1871
+ var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, AUTH_SUBPROTOCOL_PREFIX, APP_SERVER_AUTH_FILE_MODE;
1860
1872
  var init_bridge = __esm({
1861
1873
  "src/engine/bridge.ts"() {
1862
1874
  "use strict";
@@ -1868,7 +1880,7 @@ var init_bridge = __esm({
1868
1880
  APP_SERVER_START_TIMEOUT_MS = 2e4;
1869
1881
  APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
1870
1882
  APP_SERVER_HEALTH_RETRY_MS = 250;
1871
- APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
1883
+ AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
1872
1884
  APP_SERVER_AUTH_FILE_MODE = 384;
1873
1885
  }
1874
1886
  });
@@ -2496,13 +2508,12 @@ function verifyManagedToml(content, ctx, configPath) {
2496
2508
  });
2497
2509
  }
2498
2510
  if (mainTable && managed.command) {
2511
+ const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
2499
2512
  checks.push({
2500
2513
  name: "Managed command configured",
2501
2514
  passed: mainTable.includes(
2502
2515
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
2503
- ) && mainTable.includes(
2504
- `args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
2505
- ),
2516
+ ) && mainTable.includes(`args = [${expectedArgs}]`),
2506
2517
  message: "Managed tap-comms command/args do not match expected values"
2507
2518
  });
2508
2519
  }
@@ -3052,11 +3063,11 @@ function redactProtectedUrl(url) {
3052
3063
  try {
3053
3064
  const parsed = new URL(url);
3054
3065
  if (parsed.searchParams.has("tap_token")) {
3055
- parsed.searchParams.set("tap_token", "***");
3066
+ parsed.searchParams.delete("tap_token");
3056
3067
  }
3057
3068
  return parsed.toString().replace(/\/$/, "");
3058
3069
  } catch {
3059
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
3070
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3060
3071
  }
3061
3072
  }
3062
3073
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
@@ -3139,37 +3150,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3139
3150
  };
3140
3151
  }
3141
3152
  const instanceId = resolved.instanceId;
3142
- let instance = state.instances[instanceId];
3143
- if (!instance?.installed) {
3153
+ let instance2 = state.instances[instanceId];
3154
+ if (!instance2?.installed) {
3144
3155
  return {
3145
3156
  ok: false,
3146
3157
  command: "bridge",
3147
3158
  instanceId,
3148
- runtime: instance?.runtime,
3159
+ runtime: instance2?.runtime,
3149
3160
  code: "TAP_INSTANCE_NOT_FOUND",
3150
- message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
3161
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
3151
3162
  warnings: [],
3152
3163
  data: {}
3153
3164
  };
3154
3165
  }
3155
- const adapter = getAdapter(instance.runtime);
3166
+ const adapter = getAdapter(instance2.runtime);
3156
3167
  const mode = adapter.bridgeMode();
3157
3168
  if (mode !== "app-server") {
3158
3169
  return {
3159
3170
  ok: true,
3160
3171
  command: "bridge",
3161
3172
  instanceId,
3162
- runtime: instance.runtime,
3173
+ runtime: instance2.runtime,
3163
3174
  code: "TAP_NO_OP",
3164
3175
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3165
3176
  warnings: [],
3166
3177
  data: { bridgeMode: mode }
3167
3178
  };
3168
3179
  }
3169
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3170
- if (agentName && agentName !== instance.agentName) {
3171
- instance = { ...instance, agentName };
3172
- const updatedState = updateInstanceState(state, instanceId, instance);
3180
+ const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
3181
+ if (agentName && agentName !== instance2.agentName) {
3182
+ instance2 = { ...instance2, agentName };
3183
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3173
3184
  saveState(repoRoot, updatedState);
3174
3185
  state = updatedState;
3175
3186
  }
@@ -3180,7 +3191,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3180
3191
  ok: false,
3181
3192
  command: "bridge",
3182
3193
  instanceId,
3183
- runtime: instance.runtime,
3194
+ runtime: instance2.runtime,
3184
3195
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3185
3196
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3186
3197
  warnings: [],
@@ -3189,8 +3200,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3189
3200
  }
3190
3201
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3191
3202
  const runtimeCommand = resolvedConfig.runtimeCommand;
3192
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
3193
- let effectivePort = instance.port;
3203
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
3204
+ let effectivePort = instance2.port;
3194
3205
  if (effectivePort == null && manageAppServer) {
3195
3206
  effectivePort = await findNextAvailableAppServerPort(
3196
3207
  state,
@@ -3198,8 +3209,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3198
3209
  4501,
3199
3210
  instanceId
3200
3211
  );
3201
- instance = { ...instance, port: effectivePort };
3202
- const updatedState = updateInstanceState(state, instanceId, instance);
3212
+ instance2 = { ...instance2, port: effectivePort };
3213
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3203
3214
  saveState(repoRoot, updatedState);
3204
3215
  state = updatedState;
3205
3216
  }
@@ -3215,19 +3226,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3215
3226
  if (effectivePort != null) log(`Port: ${effectivePort}`);
3216
3227
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3217
3228
  const noAuth = flags["no-auth"] === true;
3218
- if (!manageAppServer && instance.runtime === "codex") {
3229
+ if (!manageAppServer && instance2.runtime === "codex") {
3219
3230
  log("Auto server: disabled (--no-server)");
3220
3231
  }
3221
3232
  if (noAuth && manageAppServer) {
3222
3233
  log("Auth gateway: disabled (--no-auth)");
3223
3234
  }
3224
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
3235
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
3225
3236
  if (willBeHeadless) {
3226
- const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
3237
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
3227
3238
  log(`Headless: ${role}`);
3228
3239
  }
3229
3240
  try {
3230
- if (!manageAppServer && instance.runtime === "codex") {
3241
+ if (!manageAppServer && instance2.runtime === "codex") {
3231
3242
  log("Checking app-server health...");
3232
3243
  const healthy = await checkAppServerHealth(appServerUrl);
3233
3244
  if (healthy) {
@@ -3238,7 +3249,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3238
3249
  ok: false,
3239
3250
  command: "bridge",
3240
3251
  instanceId,
3241
- runtime: instance.runtime,
3252
+ runtime: instance2.runtime,
3242
3253
  code: "TAP_BRIDGE_START_FAILED",
3243
3254
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
3244
3255
  warnings: [],
@@ -3252,7 +3263,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3252
3263
  ok: false,
3253
3264
  command: "bridge",
3254
3265
  instanceId,
3255
- runtime: instance.runtime,
3266
+ runtime: instance2.runtime,
3256
3267
  code: "TAP_INVALID_ARGUMENT",
3257
3268
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
3258
3269
  warnings: [],
@@ -3260,9 +3271,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3260
3271
  };
3261
3272
  }
3262
3273
  const busyMode = busyModeRaw;
3263
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
3264
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
3265
- const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
3274
+ const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
3275
+ const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
3276
+ const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
3277
+ let pollSeconds;
3278
+ let reconnectSeconds;
3279
+ let messageLookbackMinutes;
3280
+ try {
3281
+ pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
3282
+ reconnectSeconds = parseIntFlag(
3283
+ reconnectSecondsRaw,
3284
+ "--reconnect-seconds",
3285
+ 1,
3286
+ 3600
3287
+ );
3288
+ messageLookbackMinutes = parseIntFlag(
3289
+ lookbackRaw,
3290
+ "--message-lookback-minutes",
3291
+ 1,
3292
+ 10080
3293
+ );
3294
+ } catch (err) {
3295
+ return {
3296
+ ok: false,
3297
+ command: "bridge",
3298
+ instanceId,
3299
+ runtime: instance2.runtime,
3300
+ code: "TAP_INVALID_ARGUMENT",
3301
+ message: err instanceof Error ? err.message : String(err),
3302
+ warnings: [],
3303
+ data: {}
3304
+ };
3305
+ }
3266
3306
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
3267
3307
  const ephemeral = flags["ephemeral"] === true;
3268
3308
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -3274,7 +3314,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3274
3314
  ok: false,
3275
3315
  command: "bridge",
3276
3316
  instanceId,
3277
- runtime: instance.runtime,
3317
+ runtime: instance2.runtime,
3278
3318
  code: "TAP_INVALID_ARGUMENT",
3279
3319
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
3280
3320
  warnings: [],
@@ -3286,10 +3326,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3286
3326
  role: roleArg ?? "reviewer",
3287
3327
  maxRounds: 5,
3288
3328
  qualitySeverityFloor: "high"
3289
- } : instance.headless;
3329
+ } : instance2.headless;
3290
3330
  const bridge = await startBridge({
3291
3331
  instanceId,
3292
- runtime: instance.runtime,
3332
+ runtime: instance2.runtime,
3293
3333
  stateDir: ctx.stateDir,
3294
3334
  commsDir: ctx.commsDir,
3295
3335
  bridgeScript,
@@ -3330,14 +3370,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3330
3370
  log(`TUI connect: ${bridge.appServer.url}`);
3331
3371
  }
3332
3372
  }
3333
- const updated = { ...instance, bridge, manageAppServer, noAuth };
3373
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
3334
3374
  const newState = updateInstanceState(state, instanceId, updated);
3335
3375
  saveState(repoRoot, newState);
3336
3376
  return {
3337
3377
  ok: true,
3338
3378
  command: "bridge",
3339
3379
  instanceId,
3340
- runtime: instance.runtime,
3380
+ runtime: instance2.runtime,
3341
3381
  code: "TAP_BRIDGE_START_OK",
3342
3382
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
3343
3383
  warnings: [],
@@ -3350,7 +3390,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3350
3390
  ok: false,
3351
3391
  command: "bridge",
3352
3392
  instanceId,
3353
- runtime: instance.runtime,
3393
+ runtime: instance2.runtime,
3354
3394
  code: "TAP_BRIDGE_START_FAILED",
3355
3395
  message: msg,
3356
3396
  warnings: [],
@@ -3452,11 +3492,11 @@ async function bridgeStopOne(identifier) {
3452
3492
  }
3453
3493
  const instanceId = resolved.instanceId;
3454
3494
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3455
- const instance = state.instances[instanceId];
3495
+ const instance2 = state.instances[instanceId];
3456
3496
  const bridgeState = loadCurrentBridgeState(
3457
3497
  ctx.stateDir,
3458
3498
  instanceId,
3459
- instance?.bridge
3499
+ instance2?.bridge
3460
3500
  );
3461
3501
  const appServer = bridgeState?.appServer ?? null;
3462
3502
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -3504,8 +3544,8 @@ async function bridgeStopOne(identifier) {
3504
3544
  }
3505
3545
  }
3506
3546
  }
3507
- if (instance) {
3508
- const updated = { ...instance, bridge: null };
3547
+ if (instance2) {
3548
+ const updated = { ...instance2, bridge: null };
3509
3549
  const newState = updateInstanceState(state, instanceId, updated);
3510
3550
  saveState(repoRoot, newState);
3511
3551
  }
@@ -3577,9 +3617,9 @@ async function bridgeStopAll() {
3577
3617
  logSuccess(`Stopped bridge for ${instanceId}`);
3578
3618
  stopped.push(instanceId);
3579
3619
  }
3580
- const instance = state.instances[instanceId];
3581
- if (instance?.bridge) {
3582
- state.instances[instanceId] = { ...instance, bridge: null };
3620
+ const instance2 = state.instances[instanceId];
3621
+ if (instance2?.bridge) {
3622
+ state.instances[instanceId] = { ...instance2, bridge: null };
3583
3623
  stateChanged = true;
3584
3624
  }
3585
3625
  }
@@ -3875,8 +3915,22 @@ async function bridgeRestart(identifier, flags) {
3875
3915
  };
3876
3916
  }
3877
3917
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3878
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
3879
- const drainTimeout = parseInt(drainStr, 10) || 30;
3918
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
3919
+ let drainTimeout;
3920
+ try {
3921
+ drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
3922
+ } catch (err) {
3923
+ return {
3924
+ ok: false,
3925
+ command: "bridge",
3926
+ instanceId,
3927
+ runtime: instance.runtime,
3928
+ code: "TAP_INVALID_ARGUMENT",
3929
+ message: err instanceof Error ? err.message : String(err),
3930
+ warnings: [],
3931
+ data: {}
3932
+ };
3933
+ }
3880
3934
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
3881
3935
  log(`Drain timeout: ${drainTimeout}s`);
3882
3936
  try {
@@ -4042,7 +4096,7 @@ Subcommands:
4042
4096
 
4043
4097
  Options:
4044
4098
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
4045
- Saved to state \u2014 only needed on first start
4099
+ Overrides the stored name from 'tap add' when needed
4046
4100
  --all Start all registered app-server instances
4047
4101
  --busy-mode <steer|wait> How to handle active turns (default: steer)
4048
4102
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -4349,11 +4403,38 @@ function getConfig(options) {
4349
4403
  import {
4350
4404
  createServer as createServer2
4351
4405
  } from "http";
4406
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4352
4407
  var CORS_HEADERS = {
4353
4408
  "Access-Control-Allow-Origin": "http://localhost:3000",
4354
4409
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
4355
- "Access-Control-Allow-Headers": "Content-Type"
4410
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
4356
4411
  };
4412
+ function tokensMatch(presentedToken, expectedToken) {
4413
+ if (!presentedToken) {
4414
+ return false;
4415
+ }
4416
+ const presented = Buffer.from(presentedToken, "utf8");
4417
+ const expected = Buffer.from(expectedToken, "utf8");
4418
+ if (presented.length !== expected.length) {
4419
+ return false;
4420
+ }
4421
+ return timingSafeEqual(presented, expected);
4422
+ }
4423
+ function verifyBearerToken(req, expectedToken) {
4424
+ const header = req.headers.authorization;
4425
+ if (!header?.startsWith("Bearer ")) {
4426
+ return false;
4427
+ }
4428
+ return tokensMatch(header.slice(7), expectedToken);
4429
+ }
4430
+ function verifySseToken(req, expectedToken, serverUrl) {
4431
+ if (verifyBearerToken(req, expectedToken)) {
4432
+ return true;
4433
+ }
4434
+ const url = new URL(req.url ?? "/", serverUrl);
4435
+ const queryToken = url.searchParams.get("token");
4436
+ return tokensMatch(queryToken, expectedToken);
4437
+ }
4357
4438
  function jsonResponse(res, data, status = 200) {
4358
4439
  res.writeHead(status, {
4359
4440
  "Content-Type": "application/json",
@@ -4396,6 +4477,7 @@ function handleHealth(res, apiOptions) {
4396
4477
  async function startHttpServer(options) {
4397
4478
  const port = options?.port ?? 4580;
4398
4479
  const host = "127.0.0.1";
4480
+ const token = options?.token ?? randomBytes2(24).toString("base64url");
4399
4481
  const apiOptions = {
4400
4482
  repoRoot: options?.repoRoot,
4401
4483
  commsDir: options?.commsDir
@@ -4409,21 +4491,32 @@ async function startHttpServer(options) {
4409
4491
  res.end();
4410
4492
  return;
4411
4493
  }
4494
+ if (req.method === "GET" && pathname === "/health") {
4495
+ handleHealth(res, apiOptions);
4496
+ return;
4497
+ }
4498
+ if (req.method === "GET" && pathname === "/api/events") {
4499
+ const serverUrl = `http://${host}:${port}`;
4500
+ if (!verifySseToken(req, token, serverUrl)) {
4501
+ jsonResponse(res, { error: "Unauthorized" }, 401);
4502
+ return;
4503
+ }
4504
+ await handleEvents(req, res, apiOptions);
4505
+ return;
4506
+ }
4507
+ if (!verifyBearerToken(req, token)) {
4508
+ jsonResponse(res, { error: "Unauthorized" }, 401);
4509
+ return;
4510
+ }
4412
4511
  try {
4413
4512
  if (req.method === "GET") {
4414
4513
  switch (pathname) {
4415
4514
  case "/api/snapshot":
4416
4515
  handleSnapshot(res, apiOptions);
4417
4516
  return;
4418
- case "/api/events":
4419
- await handleEvents(req, res, apiOptions);
4420
- return;
4421
4517
  case "/api/config":
4422
4518
  handleConfig(res, apiOptions);
4423
4519
  return;
4424
- case "/health":
4425
- handleHealth(res, apiOptions);
4426
- return;
4427
4520
  }
4428
4521
  }
4429
4522
  if (req.method === "POST") {
@@ -4461,6 +4554,7 @@ async function startHttpServer(options) {
4461
4554
  });
4462
4555
  return {
4463
4556
  port,
4557
+ token,
4464
4558
  close: () => new Promise((resolve8, reject) => {
4465
4559
  server.close((err) => err ? reject(err) : resolve8());
4466
4560
  })