@hua-labs/tap 0.2.3 → 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();
@@ -838,8 +845,11 @@ function resolvePowerShellCommand() {
838
845
  function resolveAuthGatewayScript(repoRoot) {
839
846
  const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
840
847
  const candidates = [
841
- path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
842
- 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
843
853
  path7.join(
844
854
  repoRoot,
845
855
  "packages",
@@ -892,10 +902,8 @@ async function allocateLoopbackPort(hostname) {
892
902
  });
893
903
  });
894
904
  }
895
- function buildProtectedAppServerUrl(publicUrl, token) {
896
- const url = new URL(publicUrl);
897
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
898
- return url.toString().replace(/\/(?=\?|$)/, "");
905
+ function buildProtectedAppServerUrl(publicUrl, _token) {
906
+ return publicUrl;
899
907
  }
900
908
  function readGatewayTokenFromPath(tokenPath) {
901
909
  return fs7.readFileSync(tokenPath, "utf8").trim();
@@ -1010,7 +1018,7 @@ async function createManagedAppServerAuth(options) {
1010
1018
  throw new Error("Failed to spawn app-server auth gateway");
1011
1019
  }
1012
1020
  return {
1013
- mode: "query-token",
1021
+ mode: "subprotocol",
1014
1022
  protectedUrl,
1015
1023
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
1016
1024
  tokenPath,
@@ -1197,7 +1205,7 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
1197
1205
  `Failed to find a free app-server port starting at ${basePort}`
1198
1206
  );
1199
1207
  }
1200
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
1208
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
1201
1209
  const WebSocket = getWebSocketCtor();
1202
1210
  if (!WebSocket) {
1203
1211
  return false;
@@ -1219,7 +1227,8 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1219
1227
  };
1220
1228
  const timer = setTimeout(() => finish(false), timeoutMs);
1221
1229
  try {
1222
- socket = new WebSocket(url);
1230
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
1231
+ socket = new WebSocket(url, protocols);
1223
1232
  socket.addEventListener("open", () => finish(true), { once: true });
1224
1233
  socket.addEventListener("error", () => finish(false), { once: true });
1225
1234
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -1228,10 +1237,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1228
1237
  }
1229
1238
  });
1230
1239
  }
1231
- async function waitForAppServerHealth(url, timeoutMs) {
1240
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
1232
1241
  const deadline = Date.now() + timeoutMs;
1233
1242
  while (Date.now() < deadline) {
1234
- if (await checkAppServerHealth(url)) {
1243
+ if (await checkAppServerHealth(
1244
+ url,
1245
+ APP_SERVER_HEALTH_TIMEOUT_MS,
1246
+ gatewayToken
1247
+ )) {
1235
1248
  return true;
1236
1249
  }
1237
1250
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -1496,8 +1509,9 @@ Or start it manually:
1496
1509
  throw new Error("Tap auth gateway token is missing after startup.");
1497
1510
  }
1498
1511
  const gatewayHealthy = await waitForAppServerHealth(
1499
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
1500
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
1512
+ effectiveUrl,
1513
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
1514
+ gatewayToken
1501
1515
  );
1502
1516
  if (!gatewayHealthy) {
1503
1517
  await terminateProcess(pid, options.platform);
@@ -1854,7 +1868,7 @@ function getBridgeStatus(stateDir, instanceId) {
1854
1868
  }
1855
1869
  return "running";
1856
1870
  }
1857
- 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;
1858
1872
  var init_bridge = __esm({
1859
1873
  "src/engine/bridge.ts"() {
1860
1874
  "use strict";
@@ -1866,7 +1880,7 @@ var init_bridge = __esm({
1866
1880
  APP_SERVER_START_TIMEOUT_MS = 2e4;
1867
1881
  APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
1868
1882
  APP_SERVER_HEALTH_RETRY_MS = 250;
1869
- APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
1883
+ AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
1870
1884
  APP_SERVER_AUTH_FILE_MODE = 384;
1871
1885
  }
1872
1886
  });
@@ -3049,11 +3063,11 @@ function redactProtectedUrl(url) {
3049
3063
  try {
3050
3064
  const parsed = new URL(url);
3051
3065
  if (parsed.searchParams.has("tap_token")) {
3052
- parsed.searchParams.set("tap_token", "***");
3066
+ parsed.searchParams.delete("tap_token");
3053
3067
  }
3054
3068
  return parsed.toString().replace(/\/$/, "");
3055
3069
  } catch {
3056
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
3070
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3057
3071
  }
3058
3072
  }
3059
3073
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
@@ -3136,37 +3150,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3136
3150
  };
3137
3151
  }
3138
3152
  const instanceId = resolved.instanceId;
3139
- let instance = state.instances[instanceId];
3140
- if (!instance?.installed) {
3153
+ let instance2 = state.instances[instanceId];
3154
+ if (!instance2?.installed) {
3141
3155
  return {
3142
3156
  ok: false,
3143
3157
  command: "bridge",
3144
3158
  instanceId,
3145
- runtime: instance?.runtime,
3159
+ runtime: instance2?.runtime,
3146
3160
  code: "TAP_INSTANCE_NOT_FOUND",
3147
- 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}`,
3148
3162
  warnings: [],
3149
3163
  data: {}
3150
3164
  };
3151
3165
  }
3152
- const adapter = getAdapter(instance.runtime);
3166
+ const adapter = getAdapter(instance2.runtime);
3153
3167
  const mode = adapter.bridgeMode();
3154
3168
  if (mode !== "app-server") {
3155
3169
  return {
3156
3170
  ok: true,
3157
3171
  command: "bridge",
3158
3172
  instanceId,
3159
- runtime: instance.runtime,
3173
+ runtime: instance2.runtime,
3160
3174
  code: "TAP_NO_OP",
3161
3175
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3162
3176
  warnings: [],
3163
3177
  data: { bridgeMode: mode }
3164
3178
  };
3165
3179
  }
3166
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3167
- if (agentName && agentName !== instance.agentName) {
3168
- instance = { ...instance, agentName };
3169
- 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);
3170
3184
  saveState(repoRoot, updatedState);
3171
3185
  state = updatedState;
3172
3186
  }
@@ -3177,7 +3191,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3177
3191
  ok: false,
3178
3192
  command: "bridge",
3179
3193
  instanceId,
3180
- runtime: instance.runtime,
3194
+ runtime: instance2.runtime,
3181
3195
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3182
3196
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3183
3197
  warnings: [],
@@ -3186,8 +3200,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3186
3200
  }
3187
3201
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3188
3202
  const runtimeCommand = resolvedConfig.runtimeCommand;
3189
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
3190
- let effectivePort = instance.port;
3203
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
3204
+ let effectivePort = instance2.port;
3191
3205
  if (effectivePort == null && manageAppServer) {
3192
3206
  effectivePort = await findNextAvailableAppServerPort(
3193
3207
  state,
@@ -3195,8 +3209,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3195
3209
  4501,
3196
3210
  instanceId
3197
3211
  );
3198
- instance = { ...instance, port: effectivePort };
3199
- const updatedState = updateInstanceState(state, instanceId, instance);
3212
+ instance2 = { ...instance2, port: effectivePort };
3213
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3200
3214
  saveState(repoRoot, updatedState);
3201
3215
  state = updatedState;
3202
3216
  }
@@ -3212,19 +3226,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3212
3226
  if (effectivePort != null) log(`Port: ${effectivePort}`);
3213
3227
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3214
3228
  const noAuth = flags["no-auth"] === true;
3215
- if (!manageAppServer && instance.runtime === "codex") {
3229
+ if (!manageAppServer && instance2.runtime === "codex") {
3216
3230
  log("Auto server: disabled (--no-server)");
3217
3231
  }
3218
3232
  if (noAuth && manageAppServer) {
3219
3233
  log("Auth gateway: disabled (--no-auth)");
3220
3234
  }
3221
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
3235
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
3222
3236
  if (willBeHeadless) {
3223
- 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";
3224
3238
  log(`Headless: ${role}`);
3225
3239
  }
3226
3240
  try {
3227
- if (!manageAppServer && instance.runtime === "codex") {
3241
+ if (!manageAppServer && instance2.runtime === "codex") {
3228
3242
  log("Checking app-server health...");
3229
3243
  const healthy = await checkAppServerHealth(appServerUrl);
3230
3244
  if (healthy) {
@@ -3235,7 +3249,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3235
3249
  ok: false,
3236
3250
  command: "bridge",
3237
3251
  instanceId,
3238
- runtime: instance.runtime,
3252
+ runtime: instance2.runtime,
3239
3253
  code: "TAP_BRIDGE_START_FAILED",
3240
3254
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
3241
3255
  warnings: [],
@@ -3249,7 +3263,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3249
3263
  ok: false,
3250
3264
  command: "bridge",
3251
3265
  instanceId,
3252
- runtime: instance.runtime,
3266
+ runtime: instance2.runtime,
3253
3267
  code: "TAP_INVALID_ARGUMENT",
3254
3268
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
3255
3269
  warnings: [],
@@ -3257,9 +3271,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3257
3271
  };
3258
3272
  }
3259
3273
  const busyMode = busyModeRaw;
3260
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
3261
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
3262
- 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
+ }
3263
3306
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
3264
3307
  const ephemeral = flags["ephemeral"] === true;
3265
3308
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -3271,7 +3314,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3271
3314
  ok: false,
3272
3315
  command: "bridge",
3273
3316
  instanceId,
3274
- runtime: instance.runtime,
3317
+ runtime: instance2.runtime,
3275
3318
  code: "TAP_INVALID_ARGUMENT",
3276
3319
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
3277
3320
  warnings: [],
@@ -3283,10 +3326,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3283
3326
  role: roleArg ?? "reviewer",
3284
3327
  maxRounds: 5,
3285
3328
  qualitySeverityFloor: "high"
3286
- } : instance.headless;
3329
+ } : instance2.headless;
3287
3330
  const bridge = await startBridge({
3288
3331
  instanceId,
3289
- runtime: instance.runtime,
3332
+ runtime: instance2.runtime,
3290
3333
  stateDir: ctx.stateDir,
3291
3334
  commsDir: ctx.commsDir,
3292
3335
  bridgeScript,
@@ -3327,14 +3370,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3327
3370
  log(`TUI connect: ${bridge.appServer.url}`);
3328
3371
  }
3329
3372
  }
3330
- const updated = { ...instance, bridge, manageAppServer, noAuth };
3373
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
3331
3374
  const newState = updateInstanceState(state, instanceId, updated);
3332
3375
  saveState(repoRoot, newState);
3333
3376
  return {
3334
3377
  ok: true,
3335
3378
  command: "bridge",
3336
3379
  instanceId,
3337
- runtime: instance.runtime,
3380
+ runtime: instance2.runtime,
3338
3381
  code: "TAP_BRIDGE_START_OK",
3339
3382
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
3340
3383
  warnings: [],
@@ -3347,7 +3390,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3347
3390
  ok: false,
3348
3391
  command: "bridge",
3349
3392
  instanceId,
3350
- runtime: instance.runtime,
3393
+ runtime: instance2.runtime,
3351
3394
  code: "TAP_BRIDGE_START_FAILED",
3352
3395
  message: msg,
3353
3396
  warnings: [],
@@ -3449,11 +3492,11 @@ async function bridgeStopOne(identifier) {
3449
3492
  }
3450
3493
  const instanceId = resolved.instanceId;
3451
3494
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3452
- const instance = state.instances[instanceId];
3495
+ const instance2 = state.instances[instanceId];
3453
3496
  const bridgeState = loadCurrentBridgeState(
3454
3497
  ctx.stateDir,
3455
3498
  instanceId,
3456
- instance?.bridge
3499
+ instance2?.bridge
3457
3500
  );
3458
3501
  const appServer = bridgeState?.appServer ?? null;
3459
3502
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -3501,8 +3544,8 @@ async function bridgeStopOne(identifier) {
3501
3544
  }
3502
3545
  }
3503
3546
  }
3504
- if (instance) {
3505
- const updated = { ...instance, bridge: null };
3547
+ if (instance2) {
3548
+ const updated = { ...instance2, bridge: null };
3506
3549
  const newState = updateInstanceState(state, instanceId, updated);
3507
3550
  saveState(repoRoot, newState);
3508
3551
  }
@@ -3574,9 +3617,9 @@ async function bridgeStopAll() {
3574
3617
  logSuccess(`Stopped bridge for ${instanceId}`);
3575
3618
  stopped.push(instanceId);
3576
3619
  }
3577
- const instance = state.instances[instanceId];
3578
- if (instance?.bridge) {
3579
- state.instances[instanceId] = { ...instance, bridge: null };
3620
+ const instance2 = state.instances[instanceId];
3621
+ if (instance2?.bridge) {
3622
+ state.instances[instanceId] = { ...instance2, bridge: null };
3580
3623
  stateChanged = true;
3581
3624
  }
3582
3625
  }
@@ -3872,8 +3915,22 @@ async function bridgeRestart(identifier, flags) {
3872
3915
  };
3873
3916
  }
3874
3917
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3875
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
3876
- 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
+ }
3877
3934
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
3878
3935
  log(`Drain timeout: ${drainTimeout}s`);
3879
3936
  try {
@@ -4039,7 +4096,7 @@ Subcommands:
4039
4096
 
4040
4097
  Options:
4041
4098
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
4042
- Saved to state \u2014 only needed on first start
4099
+ Overrides the stored name from 'tap add' when needed
4043
4100
  --all Start all registered app-server instances
4044
4101
  --busy-mode <steer|wait> How to handle active turns (default: steer)
4045
4102
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -4346,11 +4403,38 @@ function getConfig(options) {
4346
4403
  import {
4347
4404
  createServer as createServer2
4348
4405
  } from "http";
4406
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4349
4407
  var CORS_HEADERS = {
4350
4408
  "Access-Control-Allow-Origin": "http://localhost:3000",
4351
4409
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
4352
- "Access-Control-Allow-Headers": "Content-Type"
4410
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
4353
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
+ }
4354
4438
  function jsonResponse(res, data, status = 200) {
4355
4439
  res.writeHead(status, {
4356
4440
  "Content-Type": "application/json",
@@ -4393,6 +4477,7 @@ function handleHealth(res, apiOptions) {
4393
4477
  async function startHttpServer(options) {
4394
4478
  const port = options?.port ?? 4580;
4395
4479
  const host = "127.0.0.1";
4480
+ const token = options?.token ?? randomBytes2(24).toString("base64url");
4396
4481
  const apiOptions = {
4397
4482
  repoRoot: options?.repoRoot,
4398
4483
  commsDir: options?.commsDir
@@ -4406,21 +4491,32 @@ async function startHttpServer(options) {
4406
4491
  res.end();
4407
4492
  return;
4408
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
+ }
4409
4511
  try {
4410
4512
  if (req.method === "GET") {
4411
4513
  switch (pathname) {
4412
4514
  case "/api/snapshot":
4413
4515
  handleSnapshot(res, apiOptions);
4414
4516
  return;
4415
- case "/api/events":
4416
- await handleEvents(req, res, apiOptions);
4417
- return;
4418
4517
  case "/api/config":
4419
4518
  handleConfig(res, apiOptions);
4420
4519
  return;
4421
- case "/health":
4422
- handleHealth(res, apiOptions);
4423
- return;
4424
4520
  }
4425
4521
  }
4426
4522
  if (req.method === "POST") {
@@ -4458,6 +4554,7 @@ async function startHttpServer(options) {
4458
4554
  });
4459
4555
  return {
4460
4556
  port,
4557
+ token,
4461
4558
  close: () => new Promise((resolve8, reject) => {
4462
4559
  server.close((err) => err ? reject(err) : resolve8());
4463
4560
  })