devinfra-bridge 0.1.0-alpha.2 → 0.1.0-alpha.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.js CHANGED
@@ -184,6 +184,8 @@ var BytedcliProvider = class {
184
184
  controller = new AbortController();
185
185
  pending;
186
186
  cached;
187
+ latestJwt;
188
+ recovery;
187
189
  timeoutMs;
188
190
  outputBytes;
189
191
  cacheMs;
@@ -191,24 +193,50 @@ var BytedcliProvider = class {
191
193
  if (this.controller.signal.aborted || signal?.aborted) {
192
194
  return Promise.reject(new CredentialError("AUTH_CANCELLED"));
193
195
  }
196
+ if (this.pending) return this.wait(this.pending, signal);
194
197
  if (this.cached && this.cached.until > Date.now()) return Promise.resolve(this.cached.jwt);
195
- if (!this.pending) {
196
- this.pending = this.acquire().finally(() => {
197
- this.pending = void 0;
198
- });
198
+ return this.wait(this.track(this.acquire()), signal);
199
+ }
200
+ refreshJwt(failedJwt, { signal } = {}) {
201
+ if (this.controller.signal.aborted || signal?.aborted) {
202
+ return Promise.reject(new CredentialError("AUTH_CANCELLED"));
203
+ }
204
+ if (this.latestJwt && this.latestJwt !== failedJwt) {
205
+ return this.getJwt({ signal });
199
206
  }
200
- const pending = this.pending;
207
+ if (this.recovery?.failedJwt === failedJwt) return this.wait(this.recovery.promise, signal);
208
+ this.cached = void 0;
209
+ const earlier = this.pending;
210
+ const recovery = (async () => {
211
+ await earlier?.catch(() => {
212
+ });
213
+ if (this.controller.signal.aborted) throw new CredentialError("AUTH_CANCELLED");
214
+ if (this.latestJwt && this.latestJwt !== failedJwt) return this.latestJwt;
215
+ return this.acquire(true);
216
+ })();
217
+ this.recovery = { failedJwt, promise: recovery };
218
+ return this.wait(this.track(recovery), signal);
219
+ }
220
+ track(acquisition) {
221
+ const pending = acquisition.finally(() => {
222
+ if (this.pending === pending) this.pending = void 0;
223
+ });
224
+ this.pending = pending;
225
+ return pending;
226
+ }
227
+ wait(pending, signal) {
201
228
  return new Promise((resolve, reject) => {
202
229
  const onAbort = () => reject(new CredentialError("AUTH_CANCELLED"));
203
230
  signal?.addEventListener("abort", onAbort, { once: true });
204
231
  pending.then(resolve, reject).finally(() => signal?.removeEventListener("abort", onAbort));
232
+ if (signal?.aborted) onAbort();
205
233
  });
206
234
  }
207
- async acquire() {
235
+ async acquire(forceRefresh = false) {
208
236
  const script = [".js", ".mjs", ".cjs"].includes(extname2(this.executable));
209
237
  const stdout = await runCredentialCommand(
210
238
  script ? process.execPath : this.executable,
211
- script ? [this.executable, ...JWT_ARGS] : JWT_ARGS,
239
+ script ? [this.executable, ...JWT_ARGS, ...forceRefresh ? ["--force-refresh"] : []] : [...JWT_ARGS, ...forceRefresh ? ["--force-refresh"] : []],
212
240
  { signal: this.controller.signal, timeoutMs: this.timeoutMs, outputBytes: this.outputBytes }
213
241
  );
214
242
  let result;
@@ -227,7 +255,8 @@ var BytedcliProvider = class {
227
255
  }
228
256
  } catch {
229
257
  }
230
- if (until > Date.now()) this.cached = { jwt, until };
258
+ this.latestJwt = jwt;
259
+ this.cached = until > Date.now() ? { jwt, until } : void 0;
231
260
  return jwt;
232
261
  }
233
262
  async close() {
@@ -235,6 +264,8 @@ var BytedcliProvider = class {
235
264
  this.controller.abort();
236
265
  await this.pending?.catch(() => {
237
266
  });
267
+ this.latestJwt = void 0;
268
+ this.recovery = void 0;
238
269
  }
239
270
  };
240
271
 
@@ -298,36 +329,6 @@ import { randomUUID } from "node:crypto";
298
329
  import { createServer } from "node:http";
299
330
  import { Agent } from "node:https";
300
331
 
301
- // src/proxy/forward-messages.ts
302
- import { request } from "node:https";
303
- import { pipeline } from "node:stream/promises";
304
-
305
- // src/proxy/errors.ts
306
- var ProxyError = class extends Error {
307
- constructor(status, code, message) {
308
- super(message);
309
- this.status = status;
310
- this.code = code;
311
- }
312
- status;
313
- code;
314
- };
315
- function sendError(res, error) {
316
- if (res.destroyed) return;
317
- if (res.headersSent) {
318
- res.destroy();
319
- return;
320
- }
321
- const type = error.status === 429 ? "rate_limit_error" : error.status === 413 ? "request_too_large" : error.status >= 500 ? "api_error" : "invalid_request_error";
322
- const body = JSON.stringify({ type: "error", error: { type, message: error.message } });
323
- res.writeHead(error.status, {
324
- "content-type": "application/json",
325
- "content-length": Buffer.byteLength(body),
326
- connection: "close"
327
- });
328
- res.end(body);
329
- }
330
-
331
332
  // src/agents/identity.ts
332
333
  var AGENT_IDENTITIES = [
333
334
  { id: "cc", client: /^claude-cli(?:\/|\s|$)/i },
@@ -355,6 +356,121 @@ function identifyAgent(headers) {
355
356
  return "x";
356
357
  }
357
358
 
359
+ // src/proxy/forward-model-request.ts
360
+ import { request } from "node:https";
361
+ import { pipeline } from "node:stream/promises";
362
+
363
+ // src/util/logger.ts
364
+ import {
365
+ constants as constants3,
366
+ closeSync,
367
+ existsSync,
368
+ lstatSync,
369
+ openSync,
370
+ renameSync,
371
+ unlinkSync,
372
+ writeSync
373
+ } from "node:fs";
374
+ import { join as join2 } from "node:path";
375
+ var silentLogger = { write() {
376
+ } };
377
+ function safeTraceId(value) {
378
+ return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
379
+ }
380
+ function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
381
+ const path = join2(stateRoot, "daemon.log");
382
+ const safeFile = (file) => {
383
+ const info = lstatSync(file);
384
+ if (!info.isFile() || info.isSymbolicLink() || process.platform !== "win32" && info.uid !== process.getuid?.())
385
+ throw new Error();
386
+ return info;
387
+ };
388
+ return {
389
+ write(record) {
390
+ try {
391
+ const line = `${JSON.stringify({
392
+ time: (/* @__PURE__ */ new Date()).toISOString(),
393
+ event: record.event,
394
+ request_id: record.request_id,
395
+ gateway_logid: safeTraceId(record.gateway_logid),
396
+ gateway_request_id: safeTraceId(record.gateway_request_id),
397
+ gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
398
+ path: record.path,
399
+ protocol: record.protocol,
400
+ agent: record.agent,
401
+ duration_ms: record.duration_ms,
402
+ status: record.status,
403
+ exit_code: record.exit_code,
404
+ code: record.code
405
+ })}
406
+ `;
407
+ if (existsSync(path) && safeFile(path).size + Buffer.byteLength(line) > maxBytes) {
408
+ for (let i = 2; i >= 1; i--) {
409
+ const from = `${path}.${i}`;
410
+ const to = `${path}.${i + 1}`;
411
+ if (existsSync(from)) {
412
+ safeFile(from);
413
+ if (existsSync(to)) {
414
+ safeFile(to);
415
+ unlinkSync(to);
416
+ }
417
+ if (i === 2) unlinkSync(from);
418
+ else renameSync(from, to);
419
+ }
420
+ }
421
+ renameSync(path, `${path}.1`);
422
+ }
423
+ const fd = openSync(
424
+ path,
425
+ constants3.O_WRONLY | constants3.O_APPEND | constants3.O_CREAT | constants3.O_NOFOLLOW,
426
+ 384
427
+ );
428
+ try {
429
+ writeSync(fd, line);
430
+ } finally {
431
+ closeSync(fd);
432
+ }
433
+ } catch {
434
+ }
435
+ }
436
+ };
437
+ }
438
+
439
+ // src/proxy/errors.ts
440
+ var ProxyError = class extends Error {
441
+ constructor(status, code, message) {
442
+ super(message);
443
+ this.status = status;
444
+ this.code = code;
445
+ }
446
+ status;
447
+ code;
448
+ };
449
+ function sendError(res, error, protocol = "messages") {
450
+ if (res.destroyed) return;
451
+ if (res.headersSent) {
452
+ res.destroy();
453
+ return;
454
+ }
455
+ const type = error.status === 429 ? "rate_limit_error" : error.status === 413 ? "request_too_large" : error.status >= 500 ? "api_error" : "invalid_request_error";
456
+ const body = JSON.stringify(
457
+ protocol === "responses" ? {
458
+ error: {
459
+ type: error.status >= 500 ? "server_error" : type,
460
+ message: error.message,
461
+ code: error.code,
462
+ param: null
463
+ }
464
+ } : { type: "error", error: { type, message: error.message } }
465
+ );
466
+ res.writeHead(error.status, {
467
+ "content-type": "application/json",
468
+ "content-length": Buffer.byteLength(body),
469
+ connection: "close"
470
+ });
471
+ res.end(body);
472
+ }
473
+
358
474
  // src/proxy/headers.ts
359
475
  var HOP = /* @__PURE__ */ new Set([
360
476
  "connection",
@@ -368,14 +484,31 @@ var HOP = /* @__PURE__ */ new Set([
368
484
  "proxy-connection"
369
485
  ]);
370
486
  var REQUEST = /* @__PURE__ */ new Set([
371
- "anthropic-version",
372
- "anthropic-beta",
373
487
  "content-type",
374
488
  "accept",
375
489
  "accept-encoding",
376
490
  "x-request-id",
377
- "request-id"
491
+ "request-id",
492
+ "user-agent"
493
+ ]);
494
+ var REQUEST_MESSAGES = /* @__PURE__ */ new Set(["anthropic-version", "anthropic-beta"]);
495
+ var REQUEST_RESPONSES = /* @__PURE__ */ new Set([
496
+ "originator",
497
+ "session-id",
498
+ "thread-id",
499
+ "session_id",
500
+ "conversation_id",
501
+ "x-client-request-id",
502
+ "openai-beta",
503
+ "x-codex-installation-id",
504
+ "x-codex-turn-state",
505
+ "x-codex-turn-metadata",
506
+ "x-codex-parent-thread-id",
507
+ "x-codex-window-id",
508
+ "x-codex-beta-features",
509
+ "x-openai-subagent"
378
510
  ]);
511
+ var RESPONSE_RESPONSES = /* @__PURE__ */ new Set(["x-codex-turn-state", "openai-model", "x-reasoning-included"]);
379
512
  var RESPONSE = /* @__PURE__ */ new Set([
380
513
  "x-tt-logid",
381
514
  "content-type",
@@ -391,11 +524,13 @@ function blocked(headers) {
391
524
  set.add(name.trim().toLowerCase());
392
525
  return set;
393
526
  }
394
- function requestHeaders(incoming, jwt, bytes, agent) {
527
+ function requestHeaders(incoming, jwt, bytes, agent, protocol = "messages") {
395
528
  const result = {};
396
529
  const deny = blocked(incoming);
530
+ const native = protocol === "responses" ? REQUEST_RESPONSES : REQUEST_MESSAGES;
397
531
  for (const [name, value] of Object.entries(incoming)) {
398
- if (REQUEST.has(name) && !deny.has(name) && value !== void 0) result[name] = value;
532
+ if ((REQUEST.has(name) || native.has(name)) && !deny.has(name) && value !== void 0)
533
+ result[name] = value;
399
534
  }
400
535
  result["content-length"] = bytes;
401
536
  result["x-identity-token"] = jwt;
@@ -407,17 +542,81 @@ function requestHeaders(incoming, jwt, bytes, agent) {
407
542
  result["x-tt-env"] = "ppe_devinfra_bridge";
408
543
  return result;
409
544
  }
410
- function responseHeaders(incoming) {
545
+ function responseHeaders(incoming, protocol = "messages") {
411
546
  const result = {};
412
547
  const deny = blocked(incoming);
413
548
  for (const [name, value] of Object.entries(incoming)) {
414
- if (!deny.has(name) && value !== void 0 && (RESPONSE.has(name) || /^anthropic-ratelimit-[a-z0-9-]+$/.test(name)))
549
+ if (!deny.has(name) && value !== void 0 && (RESPONSE.has(name) || (protocol === "messages" ? /^anthropic-ratelimit-[a-z0-9-]+$/.test(name) : RESPONSE_RESPONSES.has(name) || /^x-ratelimit-[a-z0-9-]+$/.test(name) || /^x-codex-(?:primary|secondary)-(?:used-percent|window-minutes|reset-at)$/.test(name))))
415
550
  result[name] = value;
416
551
  }
417
552
  return result;
418
553
  }
419
554
 
420
- // src/proxy/forward-messages.ts
555
+ // src/proxy/jwt-expired.ts
556
+ import { Readable } from "node:stream";
557
+ import { brotliDecompressSync, gunzipSync, inflateSync } from "node:zlib";
558
+ var ERROR_BYTES = 64 * 1024;
559
+ async function probeJwtExpired(response, idleMs) {
560
+ const type = String(response.headers["content-type"] ?? "").split(";")[0]?.trim().toLowerCase();
561
+ const encoding = String(response.headers["content-encoding"] ?? "identity").toLowerCase();
562
+ if (type !== "application/json" && !type?.endsWith("+json") || !["identity", "gzip", "deflate", "br"].includes(encoding)) {
563
+ return { source: response, expired: false };
564
+ }
565
+ const iterator = response.iterator({ destroyOnReturn: false });
566
+ const chunks = [];
567
+ let bytes = 0;
568
+ let timer;
569
+ try {
570
+ while (true) {
571
+ timer = setTimeout(
572
+ () => response.destroy(
573
+ new ProxyError(504, "UPSTREAM_STREAM_TIMEOUT", "Gateway stream timed out.")
574
+ ),
575
+ idleMs
576
+ );
577
+ const next = await iterator.next();
578
+ clearTimeout(timer);
579
+ if (next.done) break;
580
+ const chunk = Buffer.isBuffer(next.value) ? next.value : Buffer.from(next.value);
581
+ chunks.push(chunk);
582
+ bytes += chunk.length;
583
+ if (bytes > ERROR_BYTES) {
584
+ return {
585
+ expired: false,
586
+ source: Readable.from(
587
+ (async function* () {
588
+ try {
589
+ yield* chunks;
590
+ while (true) {
591
+ const next2 = await iterator.next();
592
+ if (next2.done) break;
593
+ yield next2.value;
594
+ }
595
+ } finally {
596
+ await iterator.return?.();
597
+ }
598
+ })()
599
+ )
600
+ };
601
+ }
602
+ }
603
+ } finally {
604
+ clearTimeout(timer);
605
+ }
606
+ const body = Buffer.concat(chunks);
607
+ let expired = false;
608
+ try {
609
+ const options = { maxOutputLength: ERROR_BYTES };
610
+ const decoded = encoding === "gzip" ? gunzipSync(body, options) : encoding === "deflate" ? inflateSync(body, options) : encoding === "br" ? brotliDecompressSync(body, options) : encoding === "identity" ? body : void 0;
611
+ if (!decoded) throw new Error();
612
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
613
+ expired = value?.error?.code === "JWT_EXPIRED";
614
+ } catch {
615
+ }
616
+ return { source: Readable.from([body]), expired };
617
+ }
618
+
619
+ // src/proxy/forward-model-request.ts
421
620
  async function readBody(req, context, signal) {
422
621
  const { bodyBytes, bodyIdleMs } = context.limits;
423
622
  const length = req.headers["content-length"];
@@ -466,16 +665,8 @@ async function readBody(req, context, signal) {
466
665
  if (signal.aborted) onAbort();
467
666
  });
468
667
  }
469
- async function forwardMessages(req, res, context, signal, agent, onUpstreamHeaders) {
668
+ async function forwardModelRequest(req, res, context, signal, agent, protocol, onUpstreamHeaders, requestId) {
470
669
  const body = await readBody(req, context, signal);
471
- try {
472
- const text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(body);
473
- const parsed = JSON.parse(text);
474
- if (!parsed || Array.isArray(parsed) || typeof parsed.model !== "string" || !parsed.model.trim())
475
- throw new Error();
476
- } catch {
477
- throw new ProxyError(400, "INVALID_BODY", "Expected a JSON object with a nonempty model.");
478
- }
479
670
  let jwt;
480
671
  try {
481
672
  jwt = await context.credentialProvider.getJwt({ signal });
@@ -487,6 +678,53 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
487
678
  );
488
679
  }
489
680
  if (signal.aborted) return;
681
+ for (let attempt = 0; attempt < 2; attempt++) {
682
+ let nextJwt;
683
+ const retry = await forwardAttempt(
684
+ req,
685
+ res,
686
+ context,
687
+ signal,
688
+ agent,
689
+ protocol,
690
+ body,
691
+ jwt,
692
+ onUpstreamHeaders,
693
+ attempt === 0 && context.credentialProvider.refreshJwt ? async (headers) => {
694
+ const log = (event, code) => context.logger.write({
695
+ event,
696
+ request_id: requestId,
697
+ path: protocol === "messages" ? "/v1/messages" : "/v1/responses",
698
+ protocol,
699
+ agent: identifyAgent(req.headers),
700
+ code,
701
+ gateway_logid: safeTraceId(headers["x-tt-logid"]),
702
+ gateway_request_id: safeTraceId(headers["request-id"]),
703
+ gateway_x_request_id: safeTraceId(headers["x-request-id"])
704
+ });
705
+ log("jwt_refresh_started");
706
+ try {
707
+ nextJwt = await context.credentialProvider.refreshJwt?.(jwt, { signal });
708
+ if (signal.aborted) return false;
709
+ log("jwt_refresh_succeeded");
710
+ log("query_retry", "JWT_EXPIRED");
711
+ return true;
712
+ } catch (error) {
713
+ if (!signal.aborted)
714
+ log(
715
+ "jwt_refresh_failed",
716
+ error instanceof CredentialError ? error.code : "AUTH_UNAVAILABLE"
717
+ );
718
+ return false;
719
+ }
720
+ } : void 0
721
+ );
722
+ if (!retry || !nextJwt || signal.aborted) return;
723
+ jwt = nextJwt;
724
+ }
725
+ }
726
+ async function forwardAttempt(req, res, context, signal, agent, protocol, body, jwt, onUpstreamHeaders, recover) {
727
+ if (signal.aborted) return false;
490
728
  const transfer = new AbortController();
491
729
  const abort = () => transfer.abort(signal.reason);
492
730
  signal.addEventListener("abort", abort, { once: true });
@@ -494,10 +732,10 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
494
732
  let idleTimer;
495
733
  let cleanupStreamTimer;
496
734
  let upstreamResponse;
497
- const target = new URL(req.url ?? "/v1/messages", context.config.gatewayOrigin);
735
+ const target = new URL(req.url ?? "/", context.config.gatewayOrigin);
498
736
  const upstream = request(target, {
499
737
  method: "POST",
500
- headers: requestHeaders(req.headers, jwt, body.length, agent),
738
+ headers: requestHeaders(req.headers, jwt, body.length, agent, protocol),
501
739
  agent: context.httpsAgent,
502
740
  signal: transfer.signal
503
741
  });
@@ -520,6 +758,14 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
520
758
  response.destroy();
521
759
  throw new ProxyError(502, "UPSTREAM_REDIRECT", "Gateway redirection is not supported.");
522
760
  }
761
+ let source = response;
762
+ if (response.statusCode === 401 && recover) {
763
+ const inspected = await probeJwtExpired(response, context.limits.streamIdleMs);
764
+ if (signal.aborted) return false;
765
+ if (inspected.expired && await recover(response.headers)) return true;
766
+ if (signal.aborted) return false;
767
+ source = inspected.source;
768
+ }
523
769
  let backpressured = false;
524
770
  let streamEnded = false;
525
771
  const resetIdle = () => {
@@ -546,25 +792,26 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
546
792
  streamEnded = true;
547
793
  clearTimeout(idleTimer);
548
794
  };
549
- response.on("pause", onPause);
550
- response.on("resume", onResume);
551
- response.once("end", onEnd);
552
- response.on("data", resetIdle);
795
+ source.on("pause", onPause);
796
+ source.on("resume", onResume);
797
+ source.once("end", onEnd);
798
+ source.on("data", resetIdle);
553
799
  cleanupStreamTimer = () => {
554
800
  clearTimeout(idleTimer);
555
- response.off("pause", onPause);
556
- response.off("resume", onResume);
557
- response.off("end", onEnd);
558
- response.off("data", resetIdle);
801
+ source.off("pause", onPause);
802
+ source.off("resume", onResume);
803
+ source.off("end", onEnd);
804
+ source.off("data", resetIdle);
559
805
  };
560
806
  resetIdle();
561
- res.writeHead(response.statusCode ?? 502, responseHeaders(response.headers));
807
+ res.writeHead(response.statusCode ?? 502, responseHeaders(response.headers, protocol));
562
808
  res.flushHeaders();
563
- await pipeline(response, res, { signal: transfer.signal });
809
+ await pipeline(source, res, { signal: transfer.signal });
810
+ return false;
564
811
  } catch (error) {
565
812
  if (transfer.signal.reason instanceof ProxyError) throw transfer.signal.reason;
566
813
  if (error instanceof ProxyError) throw error;
567
- if (signal.aborted) return;
814
+ if (signal.aborted) return false;
568
815
  throw new ProxyError(502, "UPSTREAM_CONNECTION", "Gateway connection failed.");
569
816
  } finally {
570
817
  clearTimeout(headerTimer);
@@ -578,82 +825,24 @@ async function forwardMessages(req, res, context, signal, agent, onUpstreamHeade
578
825
 
579
826
  // src/agents/registry.ts
580
827
  var AGENT_ROUTES = Object.freeze([
581
- { agent: "claude-code", path: "/v1/messages", method: "POST", forward: forwardMessages }
828
+ {
829
+ agent: "claude-code",
830
+ protocol: "messages",
831
+ path: "/v1/messages",
832
+ method: "POST",
833
+ allowedQueries: ["", "?beta=true"],
834
+ forward: forwardModelRequest
835
+ },
836
+ {
837
+ agent: "codex",
838
+ protocol: "responses",
839
+ path: "/v1/responses",
840
+ method: "POST",
841
+ allowedQueries: [""],
842
+ forward: forwardModelRequest
843
+ }
582
844
  ]);
583
845
 
584
- // src/util/logger.ts
585
- import {
586
- constants as constants3,
587
- closeSync,
588
- existsSync,
589
- lstatSync,
590
- openSync,
591
- renameSync,
592
- unlinkSync,
593
- writeSync
594
- } from "node:fs";
595
- import { join as join2 } from "node:path";
596
- var silentLogger = { write() {
597
- } };
598
- function safeTraceId(value) {
599
- return typeof value === "string" && value.length <= 256 && /^[A-Za-z0-9]/.test(value) && !/[^A-Za-z0-9._:-]/.test(value) ? value : void 0;
600
- }
601
- function createLogger(stateRoot, maxBytes = 5 * 1024 * 1024) {
602
- const path = join2(stateRoot, "daemon.log");
603
- const safeFile = (file) => {
604
- const info = lstatSync(file);
605
- if (!info.isFile() || info.isSymbolicLink() || process.platform !== "win32" && info.uid !== process.getuid?.())
606
- throw new Error();
607
- return info;
608
- };
609
- return {
610
- write(record) {
611
- try {
612
- const line = `${JSON.stringify({
613
- time: (/* @__PURE__ */ new Date()).toISOString(),
614
- event: record.event,
615
- request_id: record.request_id,
616
- gateway_logid: safeTraceId(record.gateway_logid),
617
- gateway_request_id: safeTraceId(record.gateway_request_id),
618
- gateway_x_request_id: safeTraceId(record.gateway_x_request_id),
619
- path: record.path,
620
- duration_ms: record.duration_ms,
621
- status: record.status,
622
- code: record.code
623
- })}
624
- `;
625
- if (existsSync(path) && safeFile(path).size + Buffer.byteLength(line) > maxBytes) {
626
- for (let i = 2; i >= 1; i--) {
627
- const from = `${path}.${i}`;
628
- const to = `${path}.${i + 1}`;
629
- if (existsSync(from)) {
630
- safeFile(from);
631
- if (existsSync(to)) {
632
- safeFile(to);
633
- unlinkSync(to);
634
- }
635
- if (i === 2) unlinkSync(from);
636
- else renameSync(from, to);
637
- }
638
- }
639
- renameSync(path, `${path}.1`);
640
- }
641
- const fd = openSync(
642
- path,
643
- constants3.O_WRONLY | constants3.O_APPEND | constants3.O_CREAT | constants3.O_NOFOLLOW,
644
- 384
645
- );
646
- try {
647
- writeSync(fd, line);
648
- } finally {
649
- closeSync(fd);
650
- }
651
- } catch {
652
- }
653
- }
654
- };
655
- }
656
-
657
846
  // src/proxy/server.ts
658
847
  function createProxyContext(config, credentialProvider, options = {}) {
659
848
  return {
@@ -671,28 +860,19 @@ function createProxyHandler(context) {
671
860
  void (async () => {
672
861
  const raw = req.url ?? "";
673
862
  const route = AGENT_ROUTES.find((item) => item.path === raw.split("?")[0]);
674
- if (!route)
675
- return sendError(res, new ProxyError(404, "UNKNOWN_ROUTE", "Route is not supported."));
863
+ const protocol = route?.protocol ?? (raw.split("?")[0]?.startsWith("/v1/responses") ? "responses" : "messages");
864
+ const fail2 = (error) => sendError(res, error, protocol);
865
+ if (!route) return fail2(new ProxyError(404, "UNKNOWN_ROUTE", "Route is not supported."));
676
866
  if (req.method !== route.method) {
677
867
  res.setHeader("allow", route.method);
678
- return sendError(
679
- res,
680
- new ProxyError(405, "METHOD_NOT_ALLOWED", "Method is not supported.")
681
- );
868
+ return fail2(new ProxyError(405, "METHOD_NOT_ALLOWED", "Method is not supported."));
682
869
  }
683
- if (raw !== route.path && raw !== `${route.path}?beta=true`) {
684
- return sendError(
685
- res,
686
- new ProxyError(400, "INVALID_QUERY", "Query parameters are not supported.")
687
- );
870
+ if (!route.allowedQueries.some((query) => raw === `${route.path}${query}`)) {
871
+ return fail2(new ProxyError(400, "INVALID_QUERY", "Query parameters are not supported."));
688
872
  }
689
- if (context.closing)
690
- return sendError(res, new ProxyError(503, "SHUTTING_DOWN", "Daemon is stopping."));
873
+ if (context.closing) return fail2(new ProxyError(503, "SHUTTING_DOWN", "Daemon is stopping."));
691
874
  if (context.active.size >= context.limits.concurrency) {
692
- return sendError(
693
- res,
694
- new ProxyError(429, "CONCURRENCY_LIMIT", "Too many active requests.")
695
- );
875
+ return fail2(new ProxyError(429, "CONCURRENCY_LIMIT", "Too many active requests."));
696
876
  }
697
877
  const controller = new AbortController();
698
878
  context.active.add(controller);
@@ -707,13 +887,22 @@ function createProxyHandler(context) {
707
887
  req.once("aborted", onAbort);
708
888
  res.once("close", onClose);
709
889
  try {
710
- await route.forward(req, res, context, controller.signal, route.agent, (headers) => {
711
- upstreamHeaders = headers;
712
- });
890
+ await route.forward(
891
+ req,
892
+ res,
893
+ context,
894
+ controller.signal,
895
+ route.agent,
896
+ route.protocol,
897
+ (headers) => {
898
+ upstreamHeaders = headers;
899
+ },
900
+ requestId
901
+ );
713
902
  } catch (error) {
714
903
  const failure = error instanceof ProxyError ? error : new ProxyError(502, "PROXY_FAILURE", "Request forwarding failed.");
715
904
  code = failure.code;
716
- sendError(res, failure);
905
+ fail2(failure);
717
906
  } finally {
718
907
  req.off("aborted", onAbort);
719
908
  res.off("close", onClose);
@@ -724,7 +913,9 @@ function createProxyHandler(context) {
724
913
  gateway_logid: safeTraceId(upstreamHeaders?.["x-tt-logid"]),
725
914
  gateway_request_id: safeTraceId(upstreamHeaders?.["request-id"]),
726
915
  gateway_x_request_id: safeTraceId(upstreamHeaders?.["x-request-id"]),
727
- path: "/v1/messages",
916
+ path: route.path,
917
+ protocol: route.protocol,
918
+ agent: identifyAgent(req.headers),
728
919
  duration_ms: Date.now() - started,
729
920
  status: res.statusCode,
730
921
  code
@@ -771,29 +962,175 @@ async function prepareStateRoot(path) {
771
962
  }
772
963
 
773
964
  // src/version.ts
774
- var VERSION = true ? "0.1.0-alpha.2" : "0.1.0-dev";
965
+ var VERSION = true ? "0.1.0-alpha.4" : "0.1.0-dev";
966
+
967
+ // src/daemon/auto-sync.ts
968
+ import { spawn as spawn2 } from "node:child_process";
969
+ import { lstat as lstat2, readFile as readFile2, stat as stat2 } from "node:fs/promises";
970
+ import { isAbsolute as isAbsolute2, join as join3 } from "node:path";
971
+ var AUTO_SYNC_STARTUP_MS = 3e4;
972
+ var AUTO_SYNC_INTERVAL_MS = 6e5;
973
+ var AUTO_SYNC_TIMEOUT_MS = 12e4;
974
+ var SYNC_ARGS = ["sync", "--yes", "--non-interactive"];
975
+ async function registration(stateRoot) {
976
+ const path = join3(stateRoot, "auto-sync.json");
977
+ try {
978
+ const info = await lstat2(path);
979
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 16 * 1024 || process.platform !== "win32" && (info.uid !== process.getuid?.() || (info.mode & 18) !== 0)) {
980
+ throw new Error("Invalid auto-sync registration");
981
+ }
982
+ const value = JSON.parse(await readFile2(path, "utf8"));
983
+ if (!value || typeof value !== "object" || Array.isArray(value))
984
+ throw new Error("Invalid registration");
985
+ const config = value;
986
+ if (config.enabled !== void 0 && typeof config.enabled !== "boolean")
987
+ throw new Error("Invalid enabled");
988
+ if (config.claude_config_dir !== void 0 && (typeof config.claude_config_dir !== "string" || !isAbsolute2(config.claude_config_dir) || config.claude_config_dir.includes("\0")))
989
+ throw new Error("Invalid directory");
990
+ return config;
991
+ } catch (error) {
992
+ if (error.code === "ENOENT") return {};
993
+ throw error;
994
+ }
995
+ }
996
+ function terminate(child, force) {
997
+ if (!child.pid) return;
998
+ if (process.platform === "win32") {
999
+ const killer = spawn2("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
1000
+ shell: false,
1001
+ windowsHide: true,
1002
+ stdio: "ignore"
1003
+ });
1004
+ killer.on("error", () => child.kill("SIGKILL"));
1005
+ return;
1006
+ }
1007
+ try {
1008
+ process.kill(-child.pid, force ? "SIGKILL" : "SIGTERM");
1009
+ } catch {
1010
+ }
1011
+ }
1012
+ async function runAutoSync(stateRoot, logger, options) {
1013
+ const started = Date.now();
1014
+ let config;
1015
+ try {
1016
+ config = await registration(stateRoot);
1017
+ } catch {
1018
+ logger.write({ event: "auto_sync_failed", code: "AUTO_SYNC_CONFIG_INVALID" });
1019
+ return;
1020
+ }
1021
+ if (config.enabled === false || options.signal.aborted) return;
1022
+ let useEntry = false;
1023
+ if (typeof config.cli_entry === "string" && isAbsolute2(config.cli_entry) && !config.cli_entry.includes("\0")) {
1024
+ useEntry = await stat2(config.cli_entry).then(
1025
+ (info) => info.isFile(),
1026
+ () => false
1027
+ );
1028
+ }
1029
+ if (options.signal.aborted) return;
1030
+ if (!useEntry) logger.write({ event: "auto_sync_fallback", code: "CLI_ENTRY_UNAVAILABLE" });
1031
+ const env = {
1032
+ ...options.env ?? process.env,
1033
+ DEVINFRA_BRIDGE_STATE_ROOT: stateRoot
1034
+ };
1035
+ if (config.claude_config_dir) env.CLAUDE_CONFIG_DIR = config.claude_config_dir;
1036
+ const command = useEntry ? process.execPath : "devinfra-bridge-cli";
1037
+ const args = useEntry ? [config.cli_entry, ...SYNC_ARGS] : SYNC_ARGS;
1038
+ logger.write({ event: "auto_sync_started" });
1039
+ await new Promise((resolve) => {
1040
+ const child = spawn2(command, args, {
1041
+ shell: false,
1042
+ windowsHide: true,
1043
+ detached: process.platform !== "win32",
1044
+ stdio: ["ignore", "pipe", "pipe"],
1045
+ env
1046
+ });
1047
+ child.stdout.resume();
1048
+ child.stderr.resume();
1049
+ let reason;
1050
+ let killTimer;
1051
+ const stop = (code) => {
1052
+ if (reason) return;
1053
+ reason = code;
1054
+ terminate(child, false);
1055
+ killTimer = setTimeout(() => terminate(child, true), options.killGraceMs ?? 1e3);
1056
+ };
1057
+ const onAbort = () => stop("AUTO_SYNC_CANCELLED");
1058
+ options.signal.addEventListener("abort", onAbort, { once: true });
1059
+ const timer = setTimeout(
1060
+ () => stop("AUTO_SYNC_TIMEOUT"),
1061
+ options.timeoutMs ?? AUTO_SYNC_TIMEOUT_MS
1062
+ );
1063
+ child.on("error", () => {
1064
+ reason ??= "CLI_UNAVAILABLE";
1065
+ });
1066
+ child.once("close", (exitCode) => {
1067
+ clearTimeout(timer);
1068
+ if (reason && killTimer) terminate(child, true);
1069
+ clearTimeout(killTimer);
1070
+ options.signal.removeEventListener("abort", onAbort);
1071
+ logger.write({
1072
+ event: reason || exitCode !== 0 ? "auto_sync_failed" : "auto_sync_finished",
1073
+ code: reason ?? (exitCode === 0 ? "AUTO_SYNC_SUCCEEDED" : "CLI_SYNC_FAILED"),
1074
+ duration_ms: Date.now() - started,
1075
+ exit_code: exitCode ?? void 0
1076
+ });
1077
+ resolve();
1078
+ });
1079
+ if (options.signal.aborted) onAbort();
1080
+ });
1081
+ }
1082
+ function startAutoSync(stateRoot, logger, options = {}) {
1083
+ const controller = new AbortController();
1084
+ let timer;
1085
+ let running = Promise.resolve();
1086
+ const schedule = (delay) => {
1087
+ timer = setTimeout(() => {
1088
+ if (controller.signal.aborted) return;
1089
+ running = (async () => {
1090
+ try {
1091
+ if (options.run) await options.run(controller.signal);
1092
+ else await runAutoSync(stateRoot, logger, { signal: controller.signal });
1093
+ } catch {
1094
+ logger.write({ event: "auto_sync_failed", code: "AUTO_SYNC_FAILED" });
1095
+ } finally {
1096
+ if (!controller.signal.aborted) schedule(options.intervalMs ?? AUTO_SYNC_INTERVAL_MS);
1097
+ }
1098
+ })();
1099
+ }, delay);
1100
+ timer.unref();
1101
+ };
1102
+ schedule(options.startupMs ?? AUTO_SYNC_STARTUP_MS);
1103
+ return {
1104
+ close() {
1105
+ clearTimeout(timer);
1106
+ controller.abort();
1107
+ return running;
1108
+ }
1109
+ };
1110
+ }
775
1111
 
776
1112
  // src/daemon/single-instance.ts
777
1113
  import { randomUUID as randomUUID2 } from "node:crypto";
778
- import { readFile as readFile2, rename, unlink, writeFile } from "node:fs/promises";
779
- import { join as join3 } from "node:path";
780
- async function bindSingleInstance(server) {
1114
+ import { readFile as readFile3, rename, unlink, writeFile } from "node:fs/promises";
1115
+ import { join as join4 } from "node:path";
1116
+ async function bindSingleInstance(server, port = PORT) {
781
1117
  await new Promise((resolve, reject) => {
782
- const onError = () => reject(new Error("Cannot bind 127.0.0.1:18765; port is occupied or unavailable."));
1118
+ const onError = () => reject(new Error(`Cannot bind ${HOST}:${port}; port is occupied or unavailable.`));
783
1119
  server.once("error", onError);
784
- server.listen({ host: HOST, port: PORT, exclusive: true }, () => {
1120
+ server.listen({ host: HOST, port, exclusive: true }, () => {
785
1121
  server.off("error", onError);
786
1122
  resolve();
787
1123
  });
788
1124
  });
1125
+ return server.address().port;
789
1126
  }
790
- async function writeInstance(stateRoot, id, version) {
791
- const path = join3(stateRoot, "instance.json");
792
- const temp = join3(stateRoot, `.instance-${randomUUID2()}.tmp`);
1127
+ async function writeInstance(stateRoot, id, version, port = PORT) {
1128
+ const path = join4(stateRoot, "instance.json");
1129
+ const temp = join4(stateRoot, `.instance-${randomUUID2()}.tmp`);
793
1130
  try {
794
1131
  await writeFile(
795
1132
  temp,
796
- `${JSON.stringify({ instance_id: id, pid: process.pid, host: HOST, port: PORT, version })}
1133
+ `${JSON.stringify({ instance_id: id, pid: process.pid, host: HOST, port, version })}
797
1134
  `,
798
1135
  { flag: "wx", mode: 384 }
799
1136
  );
@@ -804,14 +1141,14 @@ async function writeInstance(stateRoot, id, version) {
804
1141
  }
805
1142
  return async () => {
806
1143
  try {
807
- if (JSON.parse(await readFile2(path, "utf8")).instance_id === id) await unlink(path);
1144
+ if (JSON.parse(await readFile3(path, "utf8")).instance_id === id) await unlink(path);
808
1145
  } catch {
809
1146
  }
810
1147
  };
811
1148
  }
812
1149
 
813
1150
  // src/daemon/daemon.ts
814
- async function startDaemon(config) {
1151
+ async function startDaemon(config, { port: requestedPort = PORT } = {}) {
815
1152
  const stateRoot = await prepareStateRoot(config.stateRoot);
816
1153
  const logger = createLogger(stateRoot);
817
1154
  const provider = createCredentialProvider(config.auth);
@@ -824,9 +1161,10 @@ async function startDaemon(config) {
824
1161
  });
825
1162
  const instanceId = randomUUID3();
826
1163
  let cleanup;
1164
+ let port;
827
1165
  try {
828
- await bindSingleInstance(server);
829
- cleanup = await writeInstance(stateRoot, instanceId, VERSION);
1166
+ port = await bindSingleInstance(server, requestedPort);
1167
+ cleanup = await writeInstance(stateRoot, instanceId, VERSION, port);
830
1168
  } catch (error) {
831
1169
  context.closing = true;
832
1170
  for (const controller of context.active) controller.abort();
@@ -836,6 +1174,7 @@ async function startDaemon(config) {
836
1174
  await provider.close();
837
1175
  throw error;
838
1176
  }
1177
+ const autoSync = startAutoSync(stateRoot, logger);
839
1178
  let closing;
840
1179
  const onSignal = () => {
841
1180
  void close().catch(() => logger.write({ event: "shutdown_error" }));
@@ -843,6 +1182,7 @@ async function startDaemon(config) {
843
1182
  const close = ({ drainMs = 1e4 } = {}) => {
844
1183
  if (closing) return closing;
845
1184
  context.closing = true;
1185
+ const syncStopped = autoSync.close();
846
1186
  process.off("SIGINT", onSignal);
847
1187
  process.off("SIGTERM", onSignal);
848
1188
  closing = (async () => {
@@ -862,6 +1202,7 @@ async function startDaemon(config) {
862
1202
  for (const controller of context.active) controller.abort();
863
1203
  context.httpsAgent.destroy();
864
1204
  await provider.close();
1205
+ await syncStopped;
865
1206
  await cleanup?.();
866
1207
  logger.write({ event: "stopped" });
867
1208
  }
@@ -871,7 +1212,7 @@ async function startDaemon(config) {
871
1212
  process.on("SIGINT", onSignal);
872
1213
  process.on("SIGTERM", onSignal);
873
1214
  logger.write({ event: "ready" });
874
- return { instanceId, host: HOST, port: PORT, close };
1215
+ return { instanceId, host: HOST, port, close };
875
1216
  }
876
1217
  export {
877
1218
  CC_PLACEHOLDER_TOKEN,