@premai/api-sdk 1.0.47 → 1.0.48

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.cjs CHANGED
@@ -505,7 +505,10 @@ function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_RE
505
505
  const controller = new AbortController;
506
506
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
507
507
  try {
508
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
508
+ const sessionId = await attest(apiKey, {
509
+ model: body.model,
510
+ enabled: attest2
511
+ });
509
512
  const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
510
513
  const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
511
514
  method: "POST",
@@ -537,7 +540,10 @@ function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_RE
537
540
  const controller = new AbortController;
538
541
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
539
542
  try {
540
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
543
+ const sessionId = await attest(apiKey, {
544
+ model: body.model,
545
+ enabled: attest2
546
+ });
541
547
  const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
542
548
  const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
543
549
  method: "POST",
@@ -1228,7 +1234,10 @@ function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAUL
1228
1234
  const controller = new AbortController;
1229
1235
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
1230
1236
  try {
1231
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
1237
+ const sessionId = await attest(apiKey, {
1238
+ model: body.model,
1239
+ enabled: attest2
1240
+ });
1232
1241
  const encryptedRequest = preprocessRequest(body, encryptionKeys);
1233
1242
  const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
1234
1243
  method: "POST",
@@ -1350,7 +1359,11 @@ async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxB
1350
1359
 
1351
1360
  // src/tools/index.ts
1352
1361
  var import_utils6 = require("@noble/ciphers/utils.js");
1353
- var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
1362
+ var FILE_OUTPUT_TOOLS = [
1363
+ "generateImage",
1364
+ "audioGenerateFromText",
1365
+ "createFileForUser"
1366
+ ];
1354
1367
  var FILE_INPUT_TOOLS = [
1355
1368
  "imageDescribeAndCaption",
1356
1369
  "imageDescribeAndCaptionFallback",
@@ -1409,7 +1422,9 @@ async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
1409
1422
  if (!downloadUrl) {
1410
1423
  throw new Error("No download URL in response");
1411
1424
  }
1412
- const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
1425
+ const fileResponse = await fetch(downloadUrl, {
1426
+ signal: controller.signal
1427
+ });
1413
1428
  if (!fileResponse.ok) {
1414
1429
  throw new Error(`Failed to download file: ${fileResponse.status}`);
1415
1430
  }
@@ -2445,7 +2460,8 @@ function toAnthropicModel(model) {
2445
2460
  type: "model",
2446
2461
  id: model.model,
2447
2462
  display_name: model.name || model.model,
2448
- created_at: model.created_at
2463
+ created_at: model.created_at,
2464
+ description: model.description
2449
2465
  };
2450
2466
  }
2451
2467
  function filterEnabled(models) {
@@ -2540,8 +2556,8 @@ function registerAnthropicModelsRoute(router, deps) {
2540
2556
 
2541
2557
  // src/server/runtime.ts
2542
2558
  var import_multer = __toESM(require("multer"));
2543
- var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
2544
- var DEFAULT_PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
2559
+ var DEFAULT_HOST = "127.0.0.1";
2560
+ var DEFAULT_PORT = 8787;
2545
2561
  var CLIENT_CACHE_MAX = (() => {
2546
2562
  let cacheTTL = 256;
2547
2563
  const raw = process.env.CLIENT_CACHE_MAX;
@@ -2571,9 +2587,7 @@ function applyServerOptions(options) {
2571
2587
  if (enclaveUrl) {
2572
2588
  serverEnclaveUrl = enclaveUrl;
2573
2589
  }
2574
- if (kek) {
2575
- serverKek = kek;
2576
- }
2590
+ serverKek = kek || generateNewClientKEK();
2577
2591
  }
2578
2592
  async function getOrCreateRvencClient(apiKey) {
2579
2593
  const existing = clientCache.get(apiKey);
@@ -2792,6 +2806,40 @@ function registerOpenAICompatRoutes(router, deps) {
2792
2806
  });
2793
2807
  }
2794
2808
 
2809
+ // src/utils/debug.ts
2810
+ var import_node_fs = require("node:fs");
2811
+ var import_node_path = require("node:path");
2812
+ var import_env_paths = __toESM(require("env-paths"));
2813
+ var import_winston = __toESM(require("winston"));
2814
+ var defaultLogFile = `${import_env_paths.default("confidential-proxy").data}/confidential-proxy.log`;
2815
+ var dir = import_node_path.dirname(defaultLogFile);
2816
+ try {
2817
+ if (!import_node_fs.existsSync(dir))
2818
+ import_node_fs.mkdirSync(dir, { recursive: true });
2819
+ } catch {}
2820
+ var level = process.env.CONFIDENTIAL_PROXY_LOG_LEVEL ?? "info";
2821
+ var fileTransport = new import_winston.default.transports.File({
2822
+ filename: defaultLogFile,
2823
+ level,
2824
+ maxsize: 10 * 1024 * 1024,
2825
+ maxFiles: 3,
2826
+ format: import_winston.default.format.combine(import_winston.default.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), import_winston.default.format.json())
2827
+ });
2828
+ var consoleTransport = new import_winston.default.transports.Console({
2829
+ level,
2830
+ format: import_winston.default.format.combine(import_winston.default.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), import_winston.default.format.printf(({ timestamp, level: level2, message, ...rest }) => {
2831
+ const meta = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : "";
2832
+ return `[${timestamp}] [${level2}] ${message}${meta}`;
2833
+ }))
2834
+ });
2835
+ var logger = import_winston.default.createLogger({
2836
+ level,
2837
+ transports: [fileTransport, consoleTransport]
2838
+ });
2839
+
2840
+ // src/server/discovery.ts
2841
+ var import_node_fs2 = require("node:fs");
2842
+
2795
2843
  // src/server/route-prefix.ts
2796
2844
  function normalizeRoutePrefix(raw) {
2797
2845
  if (raw == null) {
@@ -2826,6 +2874,9 @@ function prefixedRoute(prefix, path) {
2826
2874
  }
2827
2875
 
2828
2876
  // src/server/discovery.ts
2877
+ var pkg = JSON.parse(import_node_fs2.readFileSync(new URL("../../package.json", "file:///home/runner/_work/api-sdk-ts/api-sdk-ts/src/server/discovery.ts"), "utf8"));
2878
+ var SERVER_MESSAGE = "Rvenc API Server";
2879
+ var SERVER_VERSION = pkg.version;
2829
2880
  function registerApiDiscoveryRoute(app, mount) {
2830
2881
  const {
2831
2882
  openai: mountOpenAI,
@@ -2855,8 +2906,8 @@ function registerApiDiscoveryRoute(app, mount) {
2855
2906
  labels.push("Anthropic Messages-compatible");
2856
2907
  }
2857
2908
  res.json({
2858
- message: `Rvenc API Server (${labels.join(" + ")})`,
2859
- version: "1.0.0",
2909
+ message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
2910
+ version: SERVER_VERSION,
2860
2911
  compat: resolveCompatLabel(mount),
2861
2912
  route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
2862
2913
  endpoints: endpoints2
@@ -2886,7 +2937,66 @@ function resolveCompatLabel(mount) {
2886
2937
  return "openai";
2887
2938
  }
2888
2939
 
2940
+ // src/server/request-debug.ts
2941
+ function requestDebugMiddleware(req, res, next) {
2942
+ const start = Date.now();
2943
+ const method = req.method;
2944
+ const path = req.path;
2945
+ logger.debug("request", { method, path });
2946
+ res.on("finish", () => {
2947
+ const elapsed = Date.now() - start;
2948
+ const status = res.statusCode;
2949
+ const contentLength = res.getHeader("content-length") ?? res.get("content-length");
2950
+ logger.debug("response", {
2951
+ method,
2952
+ path,
2953
+ status,
2954
+ elapsedMs: elapsed,
2955
+ contentLength
2956
+ });
2957
+ });
2958
+ next();
2959
+ }
2960
+
2961
+ // src/server/shutdown-route.ts
2962
+ var import_node_crypto = require("node:crypto");
2963
+ var LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
2964
+ function isLoopback(addr) {
2965
+ return !!addr && LOOPBACK.has(addr);
2966
+ }
2967
+ function hexBuf(s) {
2968
+ if (!s || s.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(s))
2969
+ return null;
2970
+ return Buffer.from(s, "hex");
2971
+ }
2972
+ function registerShutdownRoute(app, hooks) {
2973
+ const expected = Buffer.from(hooks.token, "hex");
2974
+ app.post("/__shutdown", (req, res) => {
2975
+ if (!isLoopback(req.socket.remoteAddress)) {
2976
+ logger.debug("shutdown rejected: non-loopback", {
2977
+ remote: req.socket.remoteAddress
2978
+ });
2979
+ res.status(403).end();
2980
+ return;
2981
+ }
2982
+ const provided = hexBuf(req.header("x-shutdown-token") ?? "");
2983
+ if (!provided || provided.length !== expected.length || !import_node_crypto.timingSafeEqual(provided, expected)) {
2984
+ res.status(401).end();
2985
+ return;
2986
+ }
2987
+ logger.debug("shutdown accepted");
2988
+ res.status(202).end();
2989
+ setImmediate(() => {
2990
+ hooks.onShutdown().catch((err) => logger.debug("shutdown error", { error: String(err) })).finally(() => process.exit(0));
2991
+ });
2992
+ });
2993
+ }
2994
+
2889
2995
  // src/server/create-app.ts
2996
+ var draining = false;
2997
+ function setDraining(value) {
2998
+ draining = value;
2999
+ }
2890
3000
  var rvencDeps = {
2891
3001
  getOrCreateClient: getOrCreateRvencClient
2892
3002
  };
@@ -2908,7 +3018,8 @@ function resolveCreateServerInput(compatOrOptions) {
2908
3018
  compat: compat2,
2909
3019
  openaiPrefix: openaiPrefix2,
2910
3020
  anthropicPrefix: anthropicPrefix2,
2911
- jsonBodyLimit: resolveJsonBodyLimit()
3021
+ jsonBodyLimit: resolveJsonBodyLimit(),
3022
+ shutdown: undefined
2912
3023
  };
2913
3024
  }
2914
3025
  const compat = compatOrOptions.compat ?? "openai";
@@ -2917,7 +3028,8 @@ function resolveCreateServerInput(compatOrOptions) {
2917
3028
  compat,
2918
3029
  openaiPrefix,
2919
3030
  anthropicPrefix,
2920
- jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
3031
+ jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
3032
+ shutdown: compatOrOptions.shutdown
2921
3033
  };
2922
3034
  }
2923
3035
  function httpErrorStatus(err) {
@@ -2934,11 +3046,51 @@ function mountRouter(app, prefix, router) {
2934
3046
  app.use(prefix || "/", router);
2935
3047
  }
2936
3048
  function createServerApp(compatOrOptions = "openai") {
2937
- const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
3049
+ const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
2938
3050
  const mountOpenAI = compat === "openai" || compat === "both";
2939
3051
  const mountAnthropic = compat === "anthropic" || compat === "both";
3052
+ const isAnthropicRequest = (req) => {
3053
+ if (!mountAnthropic) {
3054
+ return false;
3055
+ }
3056
+ if (!mountOpenAI) {
3057
+ return true;
3058
+ }
3059
+ return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
3060
+ };
2940
3061
  const app = import_express.default();
3062
+ app.use((req, res, next) => {
3063
+ if (draining) {
3064
+ logger.debug("drain-reject", { method: req.method, path: req.path });
3065
+ if (!res.headersSent) {
3066
+ res.setHeader("Connection", "close");
3067
+ res.setHeader("Retry-After", "5");
3068
+ if (isAnthropicRequest(req)) {
3069
+ const requestId = newAnthropicRequestId();
3070
+ res.setHeader("request-id", requestId);
3071
+ res.status(503).json({
3072
+ type: "error",
3073
+ error: {
3074
+ type: httpStatusToAnthropicErrorType(503),
3075
+ message: "Server is shutting down"
3076
+ },
3077
+ request_id: requestId
3078
+ });
3079
+ } else {
3080
+ res.status(503).json({
3081
+ error: {
3082
+ message: "Server is shutting down",
3083
+ type: "server_error"
3084
+ }
3085
+ });
3086
+ }
3087
+ }
3088
+ return;
3089
+ }
3090
+ next();
3091
+ });
2941
3092
  app.use(import_express.default.json({ limit: jsonBodyLimit }));
3093
+ app.use(requestDebugMiddleware);
2942
3094
  registerApiDiscoveryRoute(app, {
2943
3095
  openai: mountOpenAI,
2944
3096
  anthropic: mountAnthropic,
@@ -2957,18 +3109,18 @@ function createServerApp(compatOrOptions = "openai") {
2957
3109
  registerAnthropicModelsRoute(router, rvencDeps);
2958
3110
  mountRouter(app, anthropicPrefix, router);
2959
3111
  }
2960
- const isAnthropicRequest = (req) => {
2961
- if (!mountAnthropic) {
2962
- return false;
2963
- }
2964
- if (!mountOpenAI) {
2965
- return true;
2966
- }
2967
- return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
2968
- };
3112
+ if (shutdown) {
3113
+ registerShutdownRoute(app, shutdown);
3114
+ }
2969
3115
  app.use((err, req, res, _next) => {
2970
3116
  const status = httpErrorStatus(err);
2971
3117
  const message = err instanceof Error ? err.message : "Internal server error";
3118
+ logger.debug("request-error", {
3119
+ method: req.method,
3120
+ path: req.path,
3121
+ status,
3122
+ message
3123
+ });
2972
3124
  if (isAnthropicRequest(req)) {
2973
3125
  const requestId = newAnthropicRequestId();
2974
3126
  res.setHeader("request-id", requestId);
@@ -2991,6 +3143,7 @@ function createServerApp(compatOrOptions = "openai") {
2991
3143
  });
2992
3144
  app.use((req, res) => {
2993
3145
  const message = `Route ${req.method} ${req.path} not found`;
3146
+ logger.debug("route-not-found", { method: req.method, path: req.path });
2994
3147
  if (isAnthropicRequest(req)) {
2995
3148
  const requestId = newAnthropicRequestId();
2996
3149
  res.setHeader("request-id", requestId);
@@ -3010,7 +3163,143 @@ function createServerApp(compatOrOptions = "openai") {
3010
3163
  });
3011
3164
  return app;
3012
3165
  }
3166
+ // src/server/create-drain-wrapper.ts
3167
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 30000;
3168
+ function createDrainingServer(app, port, host, opts = {}) {
3169
+ const timeoutMs = opts.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
3170
+ const activeSockets = new Set;
3171
+ return new Promise((resolve, reject) => {
3172
+ const server = app.listen(port, host, () => {
3173
+ server.on("connection", (socket) => {
3174
+ activeSockets.add(socket);
3175
+ socket.once("close", () => activeSockets.delete(socket));
3176
+ });
3177
+ const { port: boundPort } = server.address();
3178
+ resolve({
3179
+ port: boundPort,
3180
+ close: () => {
3181
+ setDraining(true);
3182
+ server.close();
3183
+ },
3184
+ shutdown: async (customTimeout) => {
3185
+ const deadline = customTimeout ?? timeoutMs;
3186
+ setDraining(true);
3187
+ server.close();
3188
+ const deadlineTime = Date.now() + deadline;
3189
+ while (activeSockets.size > 0 && Date.now() < deadlineTime) {
3190
+ await new Promise((r) => setTimeout(r, 100));
3191
+ }
3192
+ for (const socket of activeSockets) {
3193
+ socket.destroy();
3194
+ }
3195
+ }
3196
+ });
3197
+ });
3198
+ server.on("error", (err) => {
3199
+ if (err.code === "EADDRINUSE") {
3200
+ const e = new Error(`Port ${port} is already in use`);
3201
+ e.code = "EADDRINUSE";
3202
+ reject(e);
3203
+ } else {
3204
+ reject(err);
3205
+ }
3206
+ });
3207
+ });
3208
+ }
3209
+ // src/utils/poll-ready.ts
3210
+ async function isProxyRoot(baseUrl) {
3211
+ try {
3212
+ const res = await fetch(`${baseUrl}/`, {
3213
+ signal: AbortSignal.timeout(2000)
3214
+ });
3215
+ if (!res.ok)
3216
+ return false;
3217
+ const body = await res.json();
3218
+ return typeof body === "object" && body !== null && typeof body.message === "string" && body.message.startsWith(SERVER_MESSAGE);
3219
+ } catch {
3220
+ return false;
3221
+ }
3222
+ }
3223
+ async function pollForReadiness(baseUrl, timeoutMs = 30000) {
3224
+ const deadline = Date.now() + timeoutMs;
3225
+ let backoff = 200;
3226
+ while (Date.now() < deadline) {
3227
+ if (await isProxyRoot(baseUrl))
3228
+ return;
3229
+ await new Promise((r) => setTimeout(r, backoff));
3230
+ backoff = Math.min(backoff * 1.5, 2000);
3231
+ }
3232
+ throw new Error(`Proxy did not become reachable within ${timeoutMs}ms`);
3233
+ }
3234
+
3235
+ // src/utils/state-file.ts
3236
+ var import_node_fs3 = require("node:fs");
3237
+ var import_node_path2 = require("node:path");
3238
+ var import_utils8 = require("@noble/ciphers/utils.js");
3239
+ var import_env_paths2 = __toESM(require("env-paths"));
3240
+ var appData = import_env_paths2.default("confidential-proxy");
3241
+ function defaultStateFile() {
3242
+ return `${appData.data}/proxy.state.json`;
3243
+ }
3244
+ function writeStateFile(path, state) {
3245
+ const dir2 = import_node_path2.dirname(path);
3246
+ if (!import_node_fs3.existsSync(dir2))
3247
+ import_node_fs3.mkdirSync(dir2, { recursive: true });
3248
+ import_node_fs3.writeFileSync(path, JSON.stringify(state), { mode: 384 });
3249
+ }
3250
+ function removeStateFile(path) {
3251
+ try {
3252
+ if (import_node_fs3.existsSync(path))
3253
+ import_node_fs3.unlinkSync(path);
3254
+ } catch {}
3255
+ }
3256
+ function generateShutdownToken() {
3257
+ return import_utils8.bytesToHex(import_utils8.randomBytes(32));
3258
+ }
3259
+ function isProcessAlive(pid) {
3260
+ try {
3261
+ process.kill(pid, 0);
3262
+ return true;
3263
+ } catch {
3264
+ return false;
3265
+ }
3266
+ }
3267
+ function acquireDaemonLock(stateFile) {
3268
+ const lockFile = `${stateFile}.lock`;
3269
+ import_node_fs3.mkdirSync(import_node_path2.dirname(lockFile), { recursive: true });
3270
+ try {
3271
+ import_node_fs3.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3272
+ } catch (err) {
3273
+ if (err.code !== "EEXIST")
3274
+ throw err;
3275
+ const lockPid = parseInt(import_node_fs3.readFileSync(lockFile, "utf-8").trim(), 10);
3276
+ if (Number.isFinite(lockPid) && lockPid > 0 && isProcessAlive(lockPid)) {
3277
+ throw new Error(`Daemon lock is held by PID ${lockPid}. Another instance may already be running.`);
3278
+ }
3279
+ import_node_fs3.unlinkSync(lockFile);
3280
+ import_node_fs3.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3281
+ }
3282
+ return () => {
3283
+ try {
3284
+ import_node_fs3.unlinkSync(lockFile);
3285
+ } catch {}
3286
+ };
3287
+ }
3288
+
3013
3289
  // src/server/start.ts
3290
+ var SHUTDOWN_TOKEN_ENV = "CONFIDENTIAL_PROXY_SHUTDOWN_TOKEN";
3291
+ var DAEMON_CHILD_ENV = "CONFIDENTIAL_PROXY_DAEMON_CHILD";
3292
+ async function bindServer(app, host, port, allowFallback, shutdownTimeoutMs) {
3293
+ try {
3294
+ return await createDrainingServer(app, port, host, { shutdownTimeoutMs });
3295
+ } catch (err) {
3296
+ if (!allowFallback || err.code !== "EADDRINUSE") {
3297
+ throw err;
3298
+ }
3299
+ logger.debug("port in use, falling back to OS-assigned port", { port });
3300
+ return await createDrainingServer(app, 0, host, { shutdownTimeoutMs });
3301
+ }
3302
+ }
3014
3303
  async function startServer(options = {}) {
3015
3304
  const {
3016
3305
  host,
@@ -3018,31 +3307,161 @@ async function startServer(options = {}) {
3018
3307
  compat: compatOpt,
3019
3308
  openaiRoutePrefix,
3020
3309
  anthropicRoutePrefix,
3021
- jsonBodyLimit
3310
+ jsonBodyLimit,
3311
+ daemon,
3312
+ stateFile,
3313
+ logFile,
3314
+ shutdownTimeoutMs
3022
3315
  } = options;
3023
3316
  const serverHost = host || DEFAULT_HOST;
3024
3317
  const serverPort = port || DEFAULT_PORT;
3025
3318
  const compat = compatOpt ?? "openai";
3026
3319
  applyServerOptions(options);
3027
3320
  resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
3321
+ const isDaemonChild = !!process.env[DAEMON_CHILD_ENV];
3322
+ logger.debug("startServer", {
3323
+ daemon,
3324
+ daemonChild: isDaemonChild,
3325
+ serverHost,
3326
+ serverPort,
3327
+ compat
3328
+ });
3329
+ if (daemon && !isDaemonChild) {
3330
+ return runDaemonParent({
3331
+ serverHost,
3332
+ serverPort,
3333
+ stateFile,
3334
+ logFile
3335
+ });
3336
+ }
3337
+ const allowFallback = !(daemon || isDaemonChild);
3338
+ const stateFilePath = stateFile || (isDaemonChild ? defaultStateFile() : undefined);
3339
+ let token;
3340
+ if (isDaemonChild) {
3341
+ token = process.env[SHUTDOWN_TOKEN_ENV];
3342
+ process.env[SHUTDOWN_TOKEN_ENV] = undefined;
3343
+ if (!token) {
3344
+ throw new Error(`Daemon child is missing ${SHUTDOWN_TOKEN_ENV}. Refusing to start.`);
3345
+ }
3346
+ } else if (stateFilePath) {
3347
+ token = generateShutdownToken();
3348
+ }
3349
+ const handleRef = { current: null };
3350
+ const shutdownConfig = token ? {
3351
+ token,
3352
+ onShutdown: async () => {
3353
+ if (handleRef.current) {
3354
+ await handleRef.current.shutdown();
3355
+ }
3356
+ if (stateFilePath)
3357
+ removeStateFile(stateFilePath);
3358
+ }
3359
+ } : undefined;
3028
3360
  const app = createServerApp({
3029
3361
  compat,
3030
3362
  openaiRoutePrefix,
3031
3363
  anthropicRoutePrefix,
3032
- jsonBodyLimit
3364
+ jsonBodyLimit,
3365
+ shutdown: shutdownConfig
3033
3366
  });
3034
- return new Promise((resolve, reject) => {
3035
- const server = app.listen(serverPort, serverHost, () => {
3036
- resolve({ close: () => server.close() });
3367
+ logger.debug("starting server", {
3368
+ serverHost,
3369
+ serverPort,
3370
+ allowFallback,
3371
+ isDaemonChild,
3372
+ hasStateFile: !!stateFilePath
3373
+ });
3374
+ const handle = await bindServer(app, serverHost, serverPort, allowFallback, shutdownTimeoutMs);
3375
+ handleRef.current = handle;
3376
+ if (stateFilePath && token) {
3377
+ writeStateFile(stateFilePath, {
3378
+ pid: process.pid,
3379
+ host: serverHost,
3380
+ port: handle.port,
3381
+ token
3037
3382
  });
3038
- server.on("error", (error) => {
3039
- if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
3040
- reject(new Error(`Port ${serverPort} is already in use`));
3041
- } else {
3042
- reject(error);
3043
- }
3383
+ }
3384
+ if (allowFallback) {
3385
+ console.log(`Proxy listening on http://${serverHost}:${handle.port}`);
3386
+ }
3387
+ logger.debug("server listening", { serverHost, port: handle.port });
3388
+ const onShutdown = async (signal) => {
3389
+ logger.debug("shutdown signal", { signal });
3390
+ console.log(`
3391
+ Received ${signal}. Shutting down gracefully...`);
3392
+ try {
3393
+ await handle.shutdown();
3394
+ } finally {
3395
+ if (stateFilePath)
3396
+ removeStateFile(stateFilePath);
3397
+ process.exit(signal === "SIGINT" ? 130 : 143);
3398
+ }
3399
+ };
3400
+ process.once("SIGINT", () => {
3401
+ onShutdown("SIGINT");
3402
+ });
3403
+ process.once("SIGTERM", () => {
3404
+ onShutdown("SIGTERM");
3405
+ });
3406
+ if (process.platform !== "win32") {
3407
+ process.once("SIGHUP", () => {
3408
+ onShutdown("SIGHUP");
3044
3409
  });
3410
+ }
3411
+ process.on("uncaughtException", (err) => {
3412
+ logger.debug("uncaughtException", { error: String(err) });
3413
+ console.error("Uncaught exception:", err);
3414
+ handle.shutdown().finally(() => process.exit(1));
3415
+ });
3416
+ return handle;
3417
+ }
3418
+ async function runDaemonParent(opts) {
3419
+ const { serverHost, serverPort, stateFile, logFile } = opts;
3420
+ const stateFilePath = stateFile || defaultStateFile();
3421
+ let releaseLock;
3422
+ try {
3423
+ releaseLock = acquireDaemonLock(stateFilePath);
3424
+ } catch (err) {
3425
+ console.error(err instanceof Error ? err.message : String(err));
3426
+ process.exit(1);
3427
+ }
3428
+ const token = generateShutdownToken();
3429
+ const scriptPath = process.argv[1];
3430
+ const args = [scriptPath, ...process.argv.slice(2)];
3431
+ logger.debug("daemon re-exec", { execPath: process.execPath, args });
3432
+ const child = Bun.spawn([process.execPath, ...args], {
3433
+ stdin: "ignore",
3434
+ stdout: logFile ? Bun.file(logFile) : "ignore",
3435
+ stderr: logFile ? Bun.file(logFile) : "ignore",
3436
+ env: {
3437
+ ...process.env,
3438
+ [DAEMON_CHILD_ENV]: "1",
3439
+ [SHUTDOWN_TOKEN_ENV]: token
3440
+ }
3045
3441
  });
3442
+ logger.debug("daemon child spawned", { pid: child.pid, stateFilePath });
3443
+ const baseUrl = `http://${serverHost}:${serverPort}`;
3444
+ const startupCrash = child.exited.then((code) => {
3445
+ throw new Error(`Proxy exited during startup with code ${code}. Run with CONFIDENTIAL_PROXY_LOG_LEVEL=debug to capture logs.`);
3446
+ });
3447
+ let started = false;
3448
+ try {
3449
+ await Promise.race([pollForReadiness(baseUrl), startupCrash]);
3450
+ started = true;
3451
+ } catch (err) {
3452
+ console.error(err instanceof Error ? err.message : String(err));
3453
+ } finally {
3454
+ child.unref();
3455
+ startupCrash.catch(() => {});
3456
+ if (!started) {
3457
+ releaseLock?.();
3458
+ removeStateFile(stateFilePath);
3459
+ process.exit(1);
3460
+ }
3461
+ releaseLock?.();
3462
+ }
3463
+ console.log(`Proxy started. PID: ${child.pid}. State file: ${stateFilePath}`);
3464
+ process.exit(0);
3046
3465
  }
3047
3466
  // src/server.ts
3048
3467
  var server_default = createServerApp("both");