@premai/api-sdk 1.0.47 → 1.0.49

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.mjs CHANGED
@@ -166,7 +166,12 @@ async function attest(apiKey, options = { enabled: true }) {
166
166
  // src/utils/crypto.ts
167
167
  import { aeskwp } from "@noble/ciphers/aes.js";
168
168
  import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
169
- import { bytesToHex, hexToBytes, managedNonce, randomBytes } from "@noble/ciphers/utils.js";
169
+ import {
170
+ bytesToHex,
171
+ hexToBytes,
172
+ managedNonce,
173
+ randomBytes
174
+ } from "@noble/ciphers/utils.js";
170
175
  import { sha256 } from "@noble/hashes/sha2.js";
171
176
  import { sha3_256 } from "@noble/hashes/sha3.js";
172
177
  import { XWing } from "@noble/post-quantum/hybrid.js";
@@ -420,7 +425,10 @@ function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_RE
420
425
  const controller = new AbortController;
421
426
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
422
427
  try {
423
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
428
+ const sessionId = await attest(apiKey, {
429
+ model: body.model,
430
+ enabled: attest2
431
+ });
424
432
  const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
425
433
  const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
426
434
  method: "POST",
@@ -452,7 +460,10 @@ function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_RE
452
460
  const controller = new AbortController;
453
461
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
454
462
  try {
455
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
463
+ const sessionId = await attest(apiKey, {
464
+ model: body.model,
465
+ enabled: attest2
466
+ });
456
467
  const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
457
468
  const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
458
469
  method: "POST",
@@ -1143,7 +1154,10 @@ function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAUL
1143
1154
  const controller = new AbortController;
1144
1155
  const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
1145
1156
  try {
1146
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
1157
+ const sessionId = await attest(apiKey, {
1158
+ model: body.model,
1159
+ enabled: attest2
1160
+ });
1147
1161
  const encryptedRequest = preprocessRequest(body, encryptionKeys);
1148
1162
  const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
1149
1163
  method: "POST",
@@ -1265,7 +1279,11 @@ async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxB
1265
1279
 
1266
1280
  // src/tools/index.ts
1267
1281
  import { bytesToHex as bytesToHex6, hexToBytes as hexToBytes6, randomBytes as randomBytes4 } from "@noble/ciphers/utils.js";
1268
- var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
1282
+ var FILE_OUTPUT_TOOLS = [
1283
+ "generateImage",
1284
+ "audioGenerateFromText",
1285
+ "createFileForUser"
1286
+ ];
1269
1287
  var FILE_INPUT_TOOLS = [
1270
1288
  "imageDescribeAndCaption",
1271
1289
  "imageDescribeAndCaptionFallback",
@@ -1324,7 +1342,9 @@ async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
1324
1342
  if (!downloadUrl) {
1325
1343
  throw new Error("No download URL in response");
1326
1344
  }
1327
- const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
1345
+ const fileResponse = await fetch(downloadUrl, {
1346
+ signal: controller.signal
1347
+ });
1328
1348
  if (!fileResponse.ok) {
1329
1349
  throw new Error(`Failed to download file: ${fileResponse.status}`);
1330
1350
  }
@@ -1831,6 +1851,9 @@ function anthropicAssistantContentToOpenAI(content) {
1831
1851
  textParts.push(part.text);
1832
1852
  continue;
1833
1853
  }
1854
+ if (part.type === "thinking" || part.type === "redacted_thinking") {
1855
+ continue;
1856
+ }
1834
1857
  if (part.type === "tool_use") {
1835
1858
  const p = part;
1836
1859
  if (typeof p.id !== "string" || p.id.length === 0) {
@@ -2004,6 +2027,13 @@ function extractTextCharCount(body) {
2004
2027
  continue;
2005
2028
  if (part.type === "text" && typeof part.text === "string") {
2006
2029
  len += part.text.length;
2030
+ } else if (part.type === "thinking" && typeof part.thinking === "string") {
2031
+ len += part.thinking.length;
2032
+ } else if (part.type === "redacted_thinking") {
2033
+ const d = part.data;
2034
+ if (typeof d === "string") {
2035
+ len += d.length;
2036
+ }
2007
2037
  } else if (part.type === "tool_result") {
2008
2038
  const c = part.content;
2009
2039
  if (typeof c === "string") {
@@ -2360,7 +2390,8 @@ function toAnthropicModel(model) {
2360
2390
  type: "model",
2361
2391
  id: model.model,
2362
2392
  display_name: model.name || model.model,
2363
- created_at: model.created_at
2393
+ created_at: model.created_at,
2394
+ description: model.description
2364
2395
  };
2365
2396
  }
2366
2397
  function filterEnabled(models) {
@@ -2455,8 +2486,8 @@ function registerAnthropicModelsRoute(router, deps) {
2455
2486
 
2456
2487
  // src/server/runtime.ts
2457
2488
  import multer from "multer";
2458
- var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
2459
- var DEFAULT_PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
2489
+ var DEFAULT_HOST = "127.0.0.1";
2490
+ var DEFAULT_PORT = 8787;
2460
2491
  var CLIENT_CACHE_MAX = (() => {
2461
2492
  let cacheTTL = 256;
2462
2493
  const raw = process.env.CLIENT_CACHE_MAX;
@@ -2486,9 +2517,7 @@ function applyServerOptions(options) {
2486
2517
  if (enclaveUrl) {
2487
2518
  serverEnclaveUrl = enclaveUrl;
2488
2519
  }
2489
- if (kek) {
2490
- serverKek = kek;
2491
- }
2520
+ serverKek = kek || generateNewClientKEK();
2492
2521
  }
2493
2522
  async function getOrCreateRvencClient(apiKey) {
2494
2523
  const existing = clientCache.get(apiKey);
@@ -2707,6 +2736,40 @@ function registerOpenAICompatRoutes(router, deps) {
2707
2736
  });
2708
2737
  }
2709
2738
 
2739
+ // src/utils/debug.ts
2740
+ import { existsSync, mkdirSync } from "node:fs";
2741
+ import { dirname } from "node:path";
2742
+ import envPaths from "env-paths";
2743
+ import winston from "winston";
2744
+ var defaultLogFile = `${envPaths("confidential-proxy").data}/confidential-proxy.log`;
2745
+ var dir = dirname(defaultLogFile);
2746
+ try {
2747
+ if (!existsSync(dir))
2748
+ mkdirSync(dir, { recursive: true });
2749
+ } catch {}
2750
+ var level = process.env.CONFIDENTIAL_PROXY_LOG_LEVEL ?? "info";
2751
+ var fileTransport = new winston.transports.File({
2752
+ filename: defaultLogFile,
2753
+ level,
2754
+ maxsize: 10 * 1024 * 1024,
2755
+ maxFiles: 3,
2756
+ format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.json())
2757
+ });
2758
+ var consoleTransport = new winston.transports.Console({
2759
+ level,
2760
+ format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.printf(({ timestamp, level: level2, message, ...rest }) => {
2761
+ const meta = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : "";
2762
+ return `[${timestamp}] [${level2}] ${message}${meta}`;
2763
+ }))
2764
+ });
2765
+ var logger = winston.createLogger({
2766
+ level,
2767
+ transports: [fileTransport, consoleTransport]
2768
+ });
2769
+
2770
+ // src/server/discovery.ts
2771
+ import { readFileSync } from "node:fs";
2772
+
2710
2773
  // src/server/route-prefix.ts
2711
2774
  function normalizeRoutePrefix(raw) {
2712
2775
  if (raw == null) {
@@ -2741,6 +2804,9 @@ function prefixedRoute(prefix, path) {
2741
2804
  }
2742
2805
 
2743
2806
  // src/server/discovery.ts
2807
+ var pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
2808
+ var SERVER_MESSAGE = "Rvenc API Server";
2809
+ var SERVER_VERSION = pkg.version;
2744
2810
  function registerApiDiscoveryRoute(app, mount) {
2745
2811
  const {
2746
2812
  openai: mountOpenAI,
@@ -2770,8 +2836,8 @@ function registerApiDiscoveryRoute(app, mount) {
2770
2836
  labels.push("Anthropic Messages-compatible");
2771
2837
  }
2772
2838
  res.json({
2773
- message: `Rvenc API Server (${labels.join(" + ")})`,
2774
- version: "1.0.0",
2839
+ message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
2840
+ version: SERVER_VERSION,
2775
2841
  compat: resolveCompatLabel(mount),
2776
2842
  route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
2777
2843
  endpoints: endpoints2
@@ -2801,7 +2867,66 @@ function resolveCompatLabel(mount) {
2801
2867
  return "openai";
2802
2868
  }
2803
2869
 
2870
+ // src/server/request-debug.ts
2871
+ function requestDebugMiddleware(req, res, next) {
2872
+ const start = Date.now();
2873
+ const method = req.method;
2874
+ const path = req.path;
2875
+ logger.debug("request", { method, path });
2876
+ res.on("finish", () => {
2877
+ const elapsed = Date.now() - start;
2878
+ const status = res.statusCode;
2879
+ const contentLength = res.getHeader("content-length") ?? res.get("content-length");
2880
+ logger.debug("response", {
2881
+ method,
2882
+ path,
2883
+ status,
2884
+ elapsedMs: elapsed,
2885
+ contentLength
2886
+ });
2887
+ });
2888
+ next();
2889
+ }
2890
+
2891
+ // src/server/shutdown-route.ts
2892
+ import { timingSafeEqual } from "node:crypto";
2893
+ var LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
2894
+ function isLoopback(addr) {
2895
+ return !!addr && LOOPBACK.has(addr);
2896
+ }
2897
+ function hexBuf(s) {
2898
+ if (!s || s.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(s))
2899
+ return null;
2900
+ return Buffer.from(s, "hex");
2901
+ }
2902
+ function registerShutdownRoute(app, hooks) {
2903
+ const expected = Buffer.from(hooks.token, "hex");
2904
+ app.post("/__shutdown", (req, res) => {
2905
+ if (!isLoopback(req.socket.remoteAddress)) {
2906
+ logger.debug("shutdown rejected: non-loopback", {
2907
+ remote: req.socket.remoteAddress
2908
+ });
2909
+ res.status(403).end();
2910
+ return;
2911
+ }
2912
+ const provided = hexBuf(req.header("x-shutdown-token") ?? "");
2913
+ if (!provided || provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
2914
+ res.status(401).end();
2915
+ return;
2916
+ }
2917
+ logger.debug("shutdown accepted");
2918
+ res.status(202).end();
2919
+ setImmediate(() => {
2920
+ hooks.onShutdown().catch((err) => logger.debug("shutdown error", { error: String(err) })).finally(() => process.exit(0));
2921
+ });
2922
+ });
2923
+ }
2924
+
2804
2925
  // src/server/create-app.ts
2926
+ var draining = false;
2927
+ function setDraining(value) {
2928
+ draining = value;
2929
+ }
2805
2930
  var rvencDeps = {
2806
2931
  getOrCreateClient: getOrCreateRvencClient
2807
2932
  };
@@ -2823,7 +2948,8 @@ function resolveCreateServerInput(compatOrOptions) {
2823
2948
  compat: compat2,
2824
2949
  openaiPrefix: openaiPrefix2,
2825
2950
  anthropicPrefix: anthropicPrefix2,
2826
- jsonBodyLimit: resolveJsonBodyLimit()
2951
+ jsonBodyLimit: resolveJsonBodyLimit(),
2952
+ shutdown: undefined
2827
2953
  };
2828
2954
  }
2829
2955
  const compat = compatOrOptions.compat ?? "openai";
@@ -2832,7 +2958,8 @@ function resolveCreateServerInput(compatOrOptions) {
2832
2958
  compat,
2833
2959
  openaiPrefix,
2834
2960
  anthropicPrefix,
2835
- jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
2961
+ jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
2962
+ shutdown: compatOrOptions.shutdown
2836
2963
  };
2837
2964
  }
2838
2965
  function httpErrorStatus(err) {
@@ -2849,11 +2976,51 @@ function mountRouter(app, prefix, router) {
2849
2976
  app.use(prefix || "/", router);
2850
2977
  }
2851
2978
  function createServerApp(compatOrOptions = "openai") {
2852
- const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
2979
+ const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
2853
2980
  const mountOpenAI = compat === "openai" || compat === "both";
2854
2981
  const mountAnthropic = compat === "anthropic" || compat === "both";
2982
+ const isAnthropicRequest = (req) => {
2983
+ if (!mountAnthropic) {
2984
+ return false;
2985
+ }
2986
+ if (!mountOpenAI) {
2987
+ return true;
2988
+ }
2989
+ return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
2990
+ };
2855
2991
  const app = express();
2992
+ app.use((req, res, next) => {
2993
+ if (draining) {
2994
+ logger.debug("drain-reject", { method: req.method, path: req.path });
2995
+ if (!res.headersSent) {
2996
+ res.setHeader("Connection", "close");
2997
+ res.setHeader("Retry-After", "5");
2998
+ if (isAnthropicRequest(req)) {
2999
+ const requestId = newAnthropicRequestId();
3000
+ res.setHeader("request-id", requestId);
3001
+ res.status(503).json({
3002
+ type: "error",
3003
+ error: {
3004
+ type: httpStatusToAnthropicErrorType(503),
3005
+ message: "Server is shutting down"
3006
+ },
3007
+ request_id: requestId
3008
+ });
3009
+ } else {
3010
+ res.status(503).json({
3011
+ error: {
3012
+ message: "Server is shutting down",
3013
+ type: "server_error"
3014
+ }
3015
+ });
3016
+ }
3017
+ }
3018
+ return;
3019
+ }
3020
+ next();
3021
+ });
2856
3022
  app.use(express.json({ limit: jsonBodyLimit }));
3023
+ app.use(requestDebugMiddleware);
2857
3024
  registerApiDiscoveryRoute(app, {
2858
3025
  openai: mountOpenAI,
2859
3026
  anthropic: mountAnthropic,
@@ -2872,18 +3039,18 @@ function createServerApp(compatOrOptions = "openai") {
2872
3039
  registerAnthropicModelsRoute(router, rvencDeps);
2873
3040
  mountRouter(app, anthropicPrefix, router);
2874
3041
  }
2875
- const isAnthropicRequest = (req) => {
2876
- if (!mountAnthropic) {
2877
- return false;
2878
- }
2879
- if (!mountOpenAI) {
2880
- return true;
2881
- }
2882
- return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
2883
- };
3042
+ if (shutdown) {
3043
+ registerShutdownRoute(app, shutdown);
3044
+ }
2884
3045
  app.use((err, req, res, _next) => {
2885
3046
  const status = httpErrorStatus(err);
2886
3047
  const message = err instanceof Error ? err.message : "Internal server error";
3048
+ logger.debug("request-error", {
3049
+ method: req.method,
3050
+ path: req.path,
3051
+ status,
3052
+ message
3053
+ });
2887
3054
  if (isAnthropicRequest(req)) {
2888
3055
  const requestId = newAnthropicRequestId();
2889
3056
  res.setHeader("request-id", requestId);
@@ -2906,6 +3073,7 @@ function createServerApp(compatOrOptions = "openai") {
2906
3073
  });
2907
3074
  app.use((req, res) => {
2908
3075
  const message = `Route ${req.method} ${req.path} not found`;
3076
+ logger.debug("route-not-found", { method: req.method, path: req.path });
2909
3077
  if (isAnthropicRequest(req)) {
2910
3078
  const requestId = newAnthropicRequestId();
2911
3079
  res.setHeader("request-id", requestId);
@@ -2925,7 +3093,149 @@ function createServerApp(compatOrOptions = "openai") {
2925
3093
  });
2926
3094
  return app;
2927
3095
  }
3096
+ // src/server/create-drain-wrapper.ts
3097
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 30000;
3098
+ function createDrainingServer(app, port, host, opts = {}) {
3099
+ const timeoutMs = opts.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
3100
+ const activeSockets = new Set;
3101
+ return new Promise((resolve, reject) => {
3102
+ const server = app.listen(port, host, () => {
3103
+ server.on("connection", (socket) => {
3104
+ activeSockets.add(socket);
3105
+ socket.once("close", () => activeSockets.delete(socket));
3106
+ });
3107
+ const { port: boundPort } = server.address();
3108
+ resolve({
3109
+ port: boundPort,
3110
+ close: () => {
3111
+ setDraining(true);
3112
+ server.close();
3113
+ },
3114
+ shutdown: async (customTimeout) => {
3115
+ const deadline = customTimeout ?? timeoutMs;
3116
+ setDraining(true);
3117
+ server.close();
3118
+ const deadlineTime = Date.now() + deadline;
3119
+ while (activeSockets.size > 0 && Date.now() < deadlineTime) {
3120
+ await new Promise((r) => setTimeout(r, 100));
3121
+ }
3122
+ for (const socket of activeSockets) {
3123
+ socket.destroy();
3124
+ }
3125
+ }
3126
+ });
3127
+ });
3128
+ server.on("error", (err) => {
3129
+ if (err.code === "EADDRINUSE") {
3130
+ const e = new Error(`Port ${port} is already in use`);
3131
+ e.code = "EADDRINUSE";
3132
+ reject(e);
3133
+ } else {
3134
+ reject(err);
3135
+ }
3136
+ });
3137
+ });
3138
+ }
3139
+ // src/utils/poll-ready.ts
3140
+ async function isProxyRoot(baseUrl) {
3141
+ try {
3142
+ const res = await fetch(`${baseUrl}/`, {
3143
+ signal: AbortSignal.timeout(2000)
3144
+ });
3145
+ if (!res.ok)
3146
+ return false;
3147
+ const body = await res.json();
3148
+ return typeof body === "object" && body !== null && typeof body.message === "string" && body.message.startsWith(SERVER_MESSAGE);
3149
+ } catch {
3150
+ return false;
3151
+ }
3152
+ }
3153
+ async function pollForReadiness(baseUrl, timeoutMs = 30000) {
3154
+ const deadline = Date.now() + timeoutMs;
3155
+ let backoff = 200;
3156
+ while (Date.now() < deadline) {
3157
+ if (await isProxyRoot(baseUrl))
3158
+ return;
3159
+ await new Promise((r) => setTimeout(r, backoff));
3160
+ backoff = Math.min(backoff * 1.5, 2000);
3161
+ }
3162
+ throw new Error(`Proxy did not become reachable within ${timeoutMs}ms`);
3163
+ }
3164
+
3165
+ // src/utils/state-file.ts
3166
+ import {
3167
+ existsSync as existsSync2,
3168
+ mkdirSync as mkdirSync2,
3169
+ readFileSync as readFileSync2,
3170
+ unlinkSync,
3171
+ writeFileSync
3172
+ } from "node:fs";
3173
+ import { dirname as dirname2 } from "node:path";
3174
+ import { bytesToHex as bytesToHex8, randomBytes as randomBytes6 } from "@noble/ciphers/utils.js";
3175
+ import envPaths2 from "env-paths";
3176
+ var appData = envPaths2("confidential-proxy");
3177
+ function defaultStateFile() {
3178
+ return `${appData.data}/proxy.state.json`;
3179
+ }
3180
+ function writeStateFile(path, state) {
3181
+ const dir2 = dirname2(path);
3182
+ if (!existsSync2(dir2))
3183
+ mkdirSync2(dir2, { recursive: true });
3184
+ writeFileSync(path, JSON.stringify(state), { mode: 384 });
3185
+ }
3186
+ function removeStateFile(path) {
3187
+ try {
3188
+ if (existsSync2(path))
3189
+ unlinkSync(path);
3190
+ } catch {}
3191
+ }
3192
+ function generateShutdownToken() {
3193
+ return bytesToHex8(randomBytes6(32));
3194
+ }
3195
+ function isProcessAlive(pid) {
3196
+ try {
3197
+ process.kill(pid, 0);
3198
+ return true;
3199
+ } catch {
3200
+ return false;
3201
+ }
3202
+ }
3203
+ function acquireDaemonLock(stateFile) {
3204
+ const lockFile = `${stateFile}.lock`;
3205
+ mkdirSync2(dirname2(lockFile), { recursive: true });
3206
+ try {
3207
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3208
+ } catch (err) {
3209
+ if (err.code !== "EEXIST")
3210
+ throw err;
3211
+ const lockPid = parseInt(readFileSync2(lockFile, "utf-8").trim(), 10);
3212
+ if (Number.isFinite(lockPid) && lockPid > 0 && isProcessAlive(lockPid)) {
3213
+ throw new Error(`Daemon lock is held by PID ${lockPid}. Another instance may already be running.`);
3214
+ }
3215
+ unlinkSync(lockFile);
3216
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3217
+ }
3218
+ return () => {
3219
+ try {
3220
+ unlinkSync(lockFile);
3221
+ } catch {}
3222
+ };
3223
+ }
3224
+
2928
3225
  // src/server/start.ts
3226
+ var SHUTDOWN_TOKEN_ENV = "CONFIDENTIAL_PROXY_SHUTDOWN_TOKEN";
3227
+ var DAEMON_CHILD_ENV = "CONFIDENTIAL_PROXY_DAEMON_CHILD";
3228
+ async function bindServer(app, host, port, allowFallback, shutdownTimeoutMs) {
3229
+ try {
3230
+ return await createDrainingServer(app, port, host, { shutdownTimeoutMs });
3231
+ } catch (err) {
3232
+ if (!allowFallback || err.code !== "EADDRINUSE") {
3233
+ throw err;
3234
+ }
3235
+ logger.debug("port in use, falling back to OS-assigned port", { port });
3236
+ return await createDrainingServer(app, 0, host, { shutdownTimeoutMs });
3237
+ }
3238
+ }
2929
3239
  async function startServer(options = {}) {
2930
3240
  const {
2931
3241
  host,
@@ -2933,31 +3243,161 @@ async function startServer(options = {}) {
2933
3243
  compat: compatOpt,
2934
3244
  openaiRoutePrefix,
2935
3245
  anthropicRoutePrefix,
2936
- jsonBodyLimit
3246
+ jsonBodyLimit,
3247
+ daemon,
3248
+ stateFile,
3249
+ logFile,
3250
+ shutdownTimeoutMs
2937
3251
  } = options;
2938
3252
  const serverHost = host || DEFAULT_HOST;
2939
3253
  const serverPort = port || DEFAULT_PORT;
2940
3254
  const compat = compatOpt ?? "openai";
2941
3255
  applyServerOptions(options);
2942
3256
  resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
3257
+ const isDaemonChild = !!process.env[DAEMON_CHILD_ENV];
3258
+ logger.debug("startServer", {
3259
+ daemon,
3260
+ daemonChild: isDaemonChild,
3261
+ serverHost,
3262
+ serverPort,
3263
+ compat
3264
+ });
3265
+ if (daemon && !isDaemonChild) {
3266
+ return runDaemonParent({
3267
+ serverHost,
3268
+ serverPort,
3269
+ stateFile,
3270
+ logFile
3271
+ });
3272
+ }
3273
+ const allowFallback = !(daemon || isDaemonChild);
3274
+ const stateFilePath = stateFile || (isDaemonChild ? defaultStateFile() : undefined);
3275
+ let token;
3276
+ if (isDaemonChild) {
3277
+ token = process.env[SHUTDOWN_TOKEN_ENV];
3278
+ process.env[SHUTDOWN_TOKEN_ENV] = undefined;
3279
+ if (!token) {
3280
+ throw new Error(`Daemon child is missing ${SHUTDOWN_TOKEN_ENV}. Refusing to start.`);
3281
+ }
3282
+ } else if (stateFilePath) {
3283
+ token = generateShutdownToken();
3284
+ }
3285
+ const handleRef = { current: null };
3286
+ const shutdownConfig = token ? {
3287
+ token,
3288
+ onShutdown: async () => {
3289
+ if (handleRef.current) {
3290
+ await handleRef.current.shutdown();
3291
+ }
3292
+ if (stateFilePath)
3293
+ removeStateFile(stateFilePath);
3294
+ }
3295
+ } : undefined;
2943
3296
  const app = createServerApp({
2944
3297
  compat,
2945
3298
  openaiRoutePrefix,
2946
3299
  anthropicRoutePrefix,
2947
- jsonBodyLimit
3300
+ jsonBodyLimit,
3301
+ shutdown: shutdownConfig
2948
3302
  });
2949
- return new Promise((resolve, reject) => {
2950
- const server = app.listen(serverPort, serverHost, () => {
2951
- resolve({ close: () => server.close() });
3303
+ logger.debug("starting server", {
3304
+ serverHost,
3305
+ serverPort,
3306
+ allowFallback,
3307
+ isDaemonChild,
3308
+ hasStateFile: !!stateFilePath
3309
+ });
3310
+ const handle = await bindServer(app, serverHost, serverPort, allowFallback, shutdownTimeoutMs);
3311
+ handleRef.current = handle;
3312
+ if (stateFilePath && token) {
3313
+ writeStateFile(stateFilePath, {
3314
+ pid: process.pid,
3315
+ host: serverHost,
3316
+ port: handle.port,
3317
+ token
2952
3318
  });
2953
- server.on("error", (error) => {
2954
- if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
2955
- reject(new Error(`Port ${serverPort} is already in use`));
2956
- } else {
2957
- reject(error);
2958
- }
3319
+ }
3320
+ if (allowFallback) {
3321
+ console.log(`Proxy listening on http://${serverHost}:${handle.port}`);
3322
+ }
3323
+ logger.debug("server listening", { serverHost, port: handle.port });
3324
+ const onShutdown = async (signal) => {
3325
+ logger.debug("shutdown signal", { signal });
3326
+ console.log(`
3327
+ Received ${signal}. Shutting down gracefully...`);
3328
+ try {
3329
+ await handle.shutdown();
3330
+ } finally {
3331
+ if (stateFilePath)
3332
+ removeStateFile(stateFilePath);
3333
+ process.exit(signal === "SIGINT" ? 130 : 143);
3334
+ }
3335
+ };
3336
+ process.once("SIGINT", () => {
3337
+ onShutdown("SIGINT");
3338
+ });
3339
+ process.once("SIGTERM", () => {
3340
+ onShutdown("SIGTERM");
3341
+ });
3342
+ if (process.platform !== "win32") {
3343
+ process.once("SIGHUP", () => {
3344
+ onShutdown("SIGHUP");
2959
3345
  });
3346
+ }
3347
+ process.on("uncaughtException", (err) => {
3348
+ logger.debug("uncaughtException", { error: String(err) });
3349
+ console.error("Uncaught exception:", err);
3350
+ handle.shutdown().finally(() => process.exit(1));
2960
3351
  });
3352
+ return handle;
3353
+ }
3354
+ async function runDaemonParent(opts) {
3355
+ const { serverHost, serverPort, stateFile, logFile } = opts;
3356
+ const stateFilePath = stateFile || defaultStateFile();
3357
+ let releaseLock;
3358
+ try {
3359
+ releaseLock = acquireDaemonLock(stateFilePath);
3360
+ } catch (err) {
3361
+ console.error(err instanceof Error ? err.message : String(err));
3362
+ process.exit(1);
3363
+ }
3364
+ const token = generateShutdownToken();
3365
+ const scriptPath = process.argv[1];
3366
+ const args = [scriptPath, ...process.argv.slice(2)];
3367
+ logger.debug("daemon re-exec", { execPath: process.execPath, args });
3368
+ const child = Bun.spawn([process.execPath, ...args], {
3369
+ stdin: "ignore",
3370
+ stdout: logFile ? Bun.file(logFile) : "ignore",
3371
+ stderr: logFile ? Bun.file(logFile) : "ignore",
3372
+ env: {
3373
+ ...process.env,
3374
+ [DAEMON_CHILD_ENV]: "1",
3375
+ [SHUTDOWN_TOKEN_ENV]: token
3376
+ }
3377
+ });
3378
+ logger.debug("daemon child spawned", { pid: child.pid, stateFilePath });
3379
+ const baseUrl = `http://${serverHost}:${serverPort}`;
3380
+ const startupCrash = child.exited.then((code) => {
3381
+ throw new Error(`Proxy exited during startup with code ${code}. Run with CONFIDENTIAL_PROXY_LOG_LEVEL=debug to capture logs.`);
3382
+ });
3383
+ let started = false;
3384
+ try {
3385
+ await Promise.race([pollForReadiness(baseUrl), startupCrash]);
3386
+ started = true;
3387
+ } catch (err) {
3388
+ console.error(err instanceof Error ? err.message : String(err));
3389
+ } finally {
3390
+ child.unref();
3391
+ startupCrash.catch(() => {});
3392
+ if (!started) {
3393
+ releaseLock?.();
3394
+ removeStateFile(stateFilePath);
3395
+ process.exit(1);
3396
+ }
3397
+ releaseLock?.();
3398
+ }
3399
+ console.log(`Proxy started. PID: ${child.pid}. State file: ${stateFilePath}`);
3400
+ process.exit(0);
2961
3401
  }
2962
3402
  // src/server.ts
2963
3403
  var server_default = createServerApp("both");
@@ -4,10 +4,12 @@ export type ProxyConfig = {
4
4
  proxyUrl: string;
5
5
  enclaveUrl: string;
6
6
  kek: string;
7
+ attest?: boolean;
7
8
  };
8
9
  export type ProxyHandle = {
9
- stop: () => void;
10
+ stop: () => Promise<void>;
10
11
  ready: Promise<void>;
11
12
  whenCrashed: Promise<never>;
13
+ pid: number;
12
14
  };
13
- export declare function startProxySubprocess(config: ProxyConfig): ProxyHandle;
15
+ export declare function ensureProxyRunning(config: ProxyConfig): Promise<ProxyHandle>;