@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.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
  }
@@ -2360,7 +2380,8 @@ function toAnthropicModel(model) {
2360
2380
  type: "model",
2361
2381
  id: model.model,
2362
2382
  display_name: model.name || model.model,
2363
- created_at: model.created_at
2383
+ created_at: model.created_at,
2384
+ description: model.description
2364
2385
  };
2365
2386
  }
2366
2387
  function filterEnabled(models) {
@@ -2455,8 +2476,8 @@ function registerAnthropicModelsRoute(router, deps) {
2455
2476
 
2456
2477
  // src/server/runtime.ts
2457
2478
  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;
2479
+ var DEFAULT_HOST = "127.0.0.1";
2480
+ var DEFAULT_PORT = 8787;
2460
2481
  var CLIENT_CACHE_MAX = (() => {
2461
2482
  let cacheTTL = 256;
2462
2483
  const raw = process.env.CLIENT_CACHE_MAX;
@@ -2486,9 +2507,7 @@ function applyServerOptions(options) {
2486
2507
  if (enclaveUrl) {
2487
2508
  serverEnclaveUrl = enclaveUrl;
2488
2509
  }
2489
- if (kek) {
2490
- serverKek = kek;
2491
- }
2510
+ serverKek = kek || generateNewClientKEK();
2492
2511
  }
2493
2512
  async function getOrCreateRvencClient(apiKey) {
2494
2513
  const existing = clientCache.get(apiKey);
@@ -2707,6 +2726,40 @@ function registerOpenAICompatRoutes(router, deps) {
2707
2726
  });
2708
2727
  }
2709
2728
 
2729
+ // src/utils/debug.ts
2730
+ import { existsSync, mkdirSync } from "node:fs";
2731
+ import { dirname } from "node:path";
2732
+ import envPaths from "env-paths";
2733
+ import winston from "winston";
2734
+ var defaultLogFile = `${envPaths("confidential-proxy").data}/confidential-proxy.log`;
2735
+ var dir = dirname(defaultLogFile);
2736
+ try {
2737
+ if (!existsSync(dir))
2738
+ mkdirSync(dir, { recursive: true });
2739
+ } catch {}
2740
+ var level = process.env.CONFIDENTIAL_PROXY_LOG_LEVEL ?? "info";
2741
+ var fileTransport = new winston.transports.File({
2742
+ filename: defaultLogFile,
2743
+ level,
2744
+ maxsize: 10 * 1024 * 1024,
2745
+ maxFiles: 3,
2746
+ format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.json())
2747
+ });
2748
+ var consoleTransport = new winston.transports.Console({
2749
+ level,
2750
+ format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.printf(({ timestamp, level: level2, message, ...rest }) => {
2751
+ const meta = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : "";
2752
+ return `[${timestamp}] [${level2}] ${message}${meta}`;
2753
+ }))
2754
+ });
2755
+ var logger = winston.createLogger({
2756
+ level,
2757
+ transports: [fileTransport, consoleTransport]
2758
+ });
2759
+
2760
+ // src/server/discovery.ts
2761
+ import { readFileSync } from "node:fs";
2762
+
2710
2763
  // src/server/route-prefix.ts
2711
2764
  function normalizeRoutePrefix(raw) {
2712
2765
  if (raw == null) {
@@ -2741,6 +2794,9 @@ function prefixedRoute(prefix, path) {
2741
2794
  }
2742
2795
 
2743
2796
  // src/server/discovery.ts
2797
+ var pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
2798
+ var SERVER_MESSAGE = "Rvenc API Server";
2799
+ var SERVER_VERSION = pkg.version;
2744
2800
  function registerApiDiscoveryRoute(app, mount) {
2745
2801
  const {
2746
2802
  openai: mountOpenAI,
@@ -2770,8 +2826,8 @@ function registerApiDiscoveryRoute(app, mount) {
2770
2826
  labels.push("Anthropic Messages-compatible");
2771
2827
  }
2772
2828
  res.json({
2773
- message: `Rvenc API Server (${labels.join(" + ")})`,
2774
- version: "1.0.0",
2829
+ message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
2830
+ version: SERVER_VERSION,
2775
2831
  compat: resolveCompatLabel(mount),
2776
2832
  route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
2777
2833
  endpoints: endpoints2
@@ -2801,7 +2857,66 @@ function resolveCompatLabel(mount) {
2801
2857
  return "openai";
2802
2858
  }
2803
2859
 
2860
+ // src/server/request-debug.ts
2861
+ function requestDebugMiddleware(req, res, next) {
2862
+ const start = Date.now();
2863
+ const method = req.method;
2864
+ const path = req.path;
2865
+ logger.debug("request", { method, path });
2866
+ res.on("finish", () => {
2867
+ const elapsed = Date.now() - start;
2868
+ const status = res.statusCode;
2869
+ const contentLength = res.getHeader("content-length") ?? res.get("content-length");
2870
+ logger.debug("response", {
2871
+ method,
2872
+ path,
2873
+ status,
2874
+ elapsedMs: elapsed,
2875
+ contentLength
2876
+ });
2877
+ });
2878
+ next();
2879
+ }
2880
+
2881
+ // src/server/shutdown-route.ts
2882
+ import { timingSafeEqual } from "node:crypto";
2883
+ var LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
2884
+ function isLoopback(addr) {
2885
+ return !!addr && LOOPBACK.has(addr);
2886
+ }
2887
+ function hexBuf(s) {
2888
+ if (!s || s.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(s))
2889
+ return null;
2890
+ return Buffer.from(s, "hex");
2891
+ }
2892
+ function registerShutdownRoute(app, hooks) {
2893
+ const expected = Buffer.from(hooks.token, "hex");
2894
+ app.post("/__shutdown", (req, res) => {
2895
+ if (!isLoopback(req.socket.remoteAddress)) {
2896
+ logger.debug("shutdown rejected: non-loopback", {
2897
+ remote: req.socket.remoteAddress
2898
+ });
2899
+ res.status(403).end();
2900
+ return;
2901
+ }
2902
+ const provided = hexBuf(req.header("x-shutdown-token") ?? "");
2903
+ if (!provided || provided.length !== expected.length || !timingSafeEqual(provided, expected)) {
2904
+ res.status(401).end();
2905
+ return;
2906
+ }
2907
+ logger.debug("shutdown accepted");
2908
+ res.status(202).end();
2909
+ setImmediate(() => {
2910
+ hooks.onShutdown().catch((err) => logger.debug("shutdown error", { error: String(err) })).finally(() => process.exit(0));
2911
+ });
2912
+ });
2913
+ }
2914
+
2804
2915
  // src/server/create-app.ts
2916
+ var draining = false;
2917
+ function setDraining(value) {
2918
+ draining = value;
2919
+ }
2805
2920
  var rvencDeps = {
2806
2921
  getOrCreateClient: getOrCreateRvencClient
2807
2922
  };
@@ -2823,7 +2938,8 @@ function resolveCreateServerInput(compatOrOptions) {
2823
2938
  compat: compat2,
2824
2939
  openaiPrefix: openaiPrefix2,
2825
2940
  anthropicPrefix: anthropicPrefix2,
2826
- jsonBodyLimit: resolveJsonBodyLimit()
2941
+ jsonBodyLimit: resolveJsonBodyLimit(),
2942
+ shutdown: undefined
2827
2943
  };
2828
2944
  }
2829
2945
  const compat = compatOrOptions.compat ?? "openai";
@@ -2832,7 +2948,8 @@ function resolveCreateServerInput(compatOrOptions) {
2832
2948
  compat,
2833
2949
  openaiPrefix,
2834
2950
  anthropicPrefix,
2835
- jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
2951
+ jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
2952
+ shutdown: compatOrOptions.shutdown
2836
2953
  };
2837
2954
  }
2838
2955
  function httpErrorStatus(err) {
@@ -2849,11 +2966,51 @@ function mountRouter(app, prefix, router) {
2849
2966
  app.use(prefix || "/", router);
2850
2967
  }
2851
2968
  function createServerApp(compatOrOptions = "openai") {
2852
- const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
2969
+ const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
2853
2970
  const mountOpenAI = compat === "openai" || compat === "both";
2854
2971
  const mountAnthropic = compat === "anthropic" || compat === "both";
2972
+ const isAnthropicRequest = (req) => {
2973
+ if (!mountAnthropic) {
2974
+ return false;
2975
+ }
2976
+ if (!mountOpenAI) {
2977
+ return true;
2978
+ }
2979
+ return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
2980
+ };
2855
2981
  const app = express();
2982
+ app.use((req, res, next) => {
2983
+ if (draining) {
2984
+ logger.debug("drain-reject", { method: req.method, path: req.path });
2985
+ if (!res.headersSent) {
2986
+ res.setHeader("Connection", "close");
2987
+ res.setHeader("Retry-After", "5");
2988
+ if (isAnthropicRequest(req)) {
2989
+ const requestId = newAnthropicRequestId();
2990
+ res.setHeader("request-id", requestId);
2991
+ res.status(503).json({
2992
+ type: "error",
2993
+ error: {
2994
+ type: httpStatusToAnthropicErrorType(503),
2995
+ message: "Server is shutting down"
2996
+ },
2997
+ request_id: requestId
2998
+ });
2999
+ } else {
3000
+ res.status(503).json({
3001
+ error: {
3002
+ message: "Server is shutting down",
3003
+ type: "server_error"
3004
+ }
3005
+ });
3006
+ }
3007
+ }
3008
+ return;
3009
+ }
3010
+ next();
3011
+ });
2856
3012
  app.use(express.json({ limit: jsonBodyLimit }));
3013
+ app.use(requestDebugMiddleware);
2857
3014
  registerApiDiscoveryRoute(app, {
2858
3015
  openai: mountOpenAI,
2859
3016
  anthropic: mountAnthropic,
@@ -2872,18 +3029,18 @@ function createServerApp(compatOrOptions = "openai") {
2872
3029
  registerAnthropicModelsRoute(router, rvencDeps);
2873
3030
  mountRouter(app, anthropicPrefix, router);
2874
3031
  }
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
- };
3032
+ if (shutdown) {
3033
+ registerShutdownRoute(app, shutdown);
3034
+ }
2884
3035
  app.use((err, req, res, _next) => {
2885
3036
  const status = httpErrorStatus(err);
2886
3037
  const message = err instanceof Error ? err.message : "Internal server error";
3038
+ logger.debug("request-error", {
3039
+ method: req.method,
3040
+ path: req.path,
3041
+ status,
3042
+ message
3043
+ });
2887
3044
  if (isAnthropicRequest(req)) {
2888
3045
  const requestId = newAnthropicRequestId();
2889
3046
  res.setHeader("request-id", requestId);
@@ -2906,6 +3063,7 @@ function createServerApp(compatOrOptions = "openai") {
2906
3063
  });
2907
3064
  app.use((req, res) => {
2908
3065
  const message = `Route ${req.method} ${req.path} not found`;
3066
+ logger.debug("route-not-found", { method: req.method, path: req.path });
2909
3067
  if (isAnthropicRequest(req)) {
2910
3068
  const requestId = newAnthropicRequestId();
2911
3069
  res.setHeader("request-id", requestId);
@@ -2925,7 +3083,149 @@ function createServerApp(compatOrOptions = "openai") {
2925
3083
  });
2926
3084
  return app;
2927
3085
  }
3086
+ // src/server/create-drain-wrapper.ts
3087
+ var DEFAULT_SHUTDOWN_TIMEOUT_MS = 30000;
3088
+ function createDrainingServer(app, port, host, opts = {}) {
3089
+ const timeoutMs = opts.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
3090
+ const activeSockets = new Set;
3091
+ return new Promise((resolve, reject) => {
3092
+ const server = app.listen(port, host, () => {
3093
+ server.on("connection", (socket) => {
3094
+ activeSockets.add(socket);
3095
+ socket.once("close", () => activeSockets.delete(socket));
3096
+ });
3097
+ const { port: boundPort } = server.address();
3098
+ resolve({
3099
+ port: boundPort,
3100
+ close: () => {
3101
+ setDraining(true);
3102
+ server.close();
3103
+ },
3104
+ shutdown: async (customTimeout) => {
3105
+ const deadline = customTimeout ?? timeoutMs;
3106
+ setDraining(true);
3107
+ server.close();
3108
+ const deadlineTime = Date.now() + deadline;
3109
+ while (activeSockets.size > 0 && Date.now() < deadlineTime) {
3110
+ await new Promise((r) => setTimeout(r, 100));
3111
+ }
3112
+ for (const socket of activeSockets) {
3113
+ socket.destroy();
3114
+ }
3115
+ }
3116
+ });
3117
+ });
3118
+ server.on("error", (err) => {
3119
+ if (err.code === "EADDRINUSE") {
3120
+ const e = new Error(`Port ${port} is already in use`);
3121
+ e.code = "EADDRINUSE";
3122
+ reject(e);
3123
+ } else {
3124
+ reject(err);
3125
+ }
3126
+ });
3127
+ });
3128
+ }
3129
+ // src/utils/poll-ready.ts
3130
+ async function isProxyRoot(baseUrl) {
3131
+ try {
3132
+ const res = await fetch(`${baseUrl}/`, {
3133
+ signal: AbortSignal.timeout(2000)
3134
+ });
3135
+ if (!res.ok)
3136
+ return false;
3137
+ const body = await res.json();
3138
+ return typeof body === "object" && body !== null && typeof body.message === "string" && body.message.startsWith(SERVER_MESSAGE);
3139
+ } catch {
3140
+ return false;
3141
+ }
3142
+ }
3143
+ async function pollForReadiness(baseUrl, timeoutMs = 30000) {
3144
+ const deadline = Date.now() + timeoutMs;
3145
+ let backoff = 200;
3146
+ while (Date.now() < deadline) {
3147
+ if (await isProxyRoot(baseUrl))
3148
+ return;
3149
+ await new Promise((r) => setTimeout(r, backoff));
3150
+ backoff = Math.min(backoff * 1.5, 2000);
3151
+ }
3152
+ throw new Error(`Proxy did not become reachable within ${timeoutMs}ms`);
3153
+ }
3154
+
3155
+ // src/utils/state-file.ts
3156
+ import {
3157
+ existsSync as existsSync2,
3158
+ mkdirSync as mkdirSync2,
3159
+ readFileSync as readFileSync2,
3160
+ unlinkSync,
3161
+ writeFileSync
3162
+ } from "node:fs";
3163
+ import { dirname as dirname2 } from "node:path";
3164
+ import { bytesToHex as bytesToHex8, randomBytes as randomBytes6 } from "@noble/ciphers/utils.js";
3165
+ import envPaths2 from "env-paths";
3166
+ var appData = envPaths2("confidential-proxy");
3167
+ function defaultStateFile() {
3168
+ return `${appData.data}/proxy.state.json`;
3169
+ }
3170
+ function writeStateFile(path, state) {
3171
+ const dir2 = dirname2(path);
3172
+ if (!existsSync2(dir2))
3173
+ mkdirSync2(dir2, { recursive: true });
3174
+ writeFileSync(path, JSON.stringify(state), { mode: 384 });
3175
+ }
3176
+ function removeStateFile(path) {
3177
+ try {
3178
+ if (existsSync2(path))
3179
+ unlinkSync(path);
3180
+ } catch {}
3181
+ }
3182
+ function generateShutdownToken() {
3183
+ return bytesToHex8(randomBytes6(32));
3184
+ }
3185
+ function isProcessAlive(pid) {
3186
+ try {
3187
+ process.kill(pid, 0);
3188
+ return true;
3189
+ } catch {
3190
+ return false;
3191
+ }
3192
+ }
3193
+ function acquireDaemonLock(stateFile) {
3194
+ const lockFile = `${stateFile}.lock`;
3195
+ mkdirSync2(dirname2(lockFile), { recursive: true });
3196
+ try {
3197
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3198
+ } catch (err) {
3199
+ if (err.code !== "EEXIST")
3200
+ throw err;
3201
+ const lockPid = parseInt(readFileSync2(lockFile, "utf-8").trim(), 10);
3202
+ if (Number.isFinite(lockPid) && lockPid > 0 && isProcessAlive(lockPid)) {
3203
+ throw new Error(`Daemon lock is held by PID ${lockPid}. Another instance may already be running.`);
3204
+ }
3205
+ unlinkSync(lockFile);
3206
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
3207
+ }
3208
+ return () => {
3209
+ try {
3210
+ unlinkSync(lockFile);
3211
+ } catch {}
3212
+ };
3213
+ }
3214
+
2928
3215
  // src/server/start.ts
3216
+ var SHUTDOWN_TOKEN_ENV = "CONFIDENTIAL_PROXY_SHUTDOWN_TOKEN";
3217
+ var DAEMON_CHILD_ENV = "CONFIDENTIAL_PROXY_DAEMON_CHILD";
3218
+ async function bindServer(app, host, port, allowFallback, shutdownTimeoutMs) {
3219
+ try {
3220
+ return await createDrainingServer(app, port, host, { shutdownTimeoutMs });
3221
+ } catch (err) {
3222
+ if (!allowFallback || err.code !== "EADDRINUSE") {
3223
+ throw err;
3224
+ }
3225
+ logger.debug("port in use, falling back to OS-assigned port", { port });
3226
+ return await createDrainingServer(app, 0, host, { shutdownTimeoutMs });
3227
+ }
3228
+ }
2929
3229
  async function startServer(options = {}) {
2930
3230
  const {
2931
3231
  host,
@@ -2933,31 +3233,161 @@ async function startServer(options = {}) {
2933
3233
  compat: compatOpt,
2934
3234
  openaiRoutePrefix,
2935
3235
  anthropicRoutePrefix,
2936
- jsonBodyLimit
3236
+ jsonBodyLimit,
3237
+ daemon,
3238
+ stateFile,
3239
+ logFile,
3240
+ shutdownTimeoutMs
2937
3241
  } = options;
2938
3242
  const serverHost = host || DEFAULT_HOST;
2939
3243
  const serverPort = port || DEFAULT_PORT;
2940
3244
  const compat = compatOpt ?? "openai";
2941
3245
  applyServerOptions(options);
2942
3246
  resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
3247
+ const isDaemonChild = !!process.env[DAEMON_CHILD_ENV];
3248
+ logger.debug("startServer", {
3249
+ daemon,
3250
+ daemonChild: isDaemonChild,
3251
+ serverHost,
3252
+ serverPort,
3253
+ compat
3254
+ });
3255
+ if (daemon && !isDaemonChild) {
3256
+ return runDaemonParent({
3257
+ serverHost,
3258
+ serverPort,
3259
+ stateFile,
3260
+ logFile
3261
+ });
3262
+ }
3263
+ const allowFallback = !(daemon || isDaemonChild);
3264
+ const stateFilePath = stateFile || (isDaemonChild ? defaultStateFile() : undefined);
3265
+ let token;
3266
+ if (isDaemonChild) {
3267
+ token = process.env[SHUTDOWN_TOKEN_ENV];
3268
+ process.env[SHUTDOWN_TOKEN_ENV] = undefined;
3269
+ if (!token) {
3270
+ throw new Error(`Daemon child is missing ${SHUTDOWN_TOKEN_ENV}. Refusing to start.`);
3271
+ }
3272
+ } else if (stateFilePath) {
3273
+ token = generateShutdownToken();
3274
+ }
3275
+ const handleRef = { current: null };
3276
+ const shutdownConfig = token ? {
3277
+ token,
3278
+ onShutdown: async () => {
3279
+ if (handleRef.current) {
3280
+ await handleRef.current.shutdown();
3281
+ }
3282
+ if (stateFilePath)
3283
+ removeStateFile(stateFilePath);
3284
+ }
3285
+ } : undefined;
2943
3286
  const app = createServerApp({
2944
3287
  compat,
2945
3288
  openaiRoutePrefix,
2946
3289
  anthropicRoutePrefix,
2947
- jsonBodyLimit
3290
+ jsonBodyLimit,
3291
+ shutdown: shutdownConfig
2948
3292
  });
2949
- return new Promise((resolve, reject) => {
2950
- const server = app.listen(serverPort, serverHost, () => {
2951
- resolve({ close: () => server.close() });
3293
+ logger.debug("starting server", {
3294
+ serverHost,
3295
+ serverPort,
3296
+ allowFallback,
3297
+ isDaemonChild,
3298
+ hasStateFile: !!stateFilePath
3299
+ });
3300
+ const handle = await bindServer(app, serverHost, serverPort, allowFallback, shutdownTimeoutMs);
3301
+ handleRef.current = handle;
3302
+ if (stateFilePath && token) {
3303
+ writeStateFile(stateFilePath, {
3304
+ pid: process.pid,
3305
+ host: serverHost,
3306
+ port: handle.port,
3307
+ token
2952
3308
  });
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
- }
3309
+ }
3310
+ if (allowFallback) {
3311
+ console.log(`Proxy listening on http://${serverHost}:${handle.port}`);
3312
+ }
3313
+ logger.debug("server listening", { serverHost, port: handle.port });
3314
+ const onShutdown = async (signal) => {
3315
+ logger.debug("shutdown signal", { signal });
3316
+ console.log(`
3317
+ Received ${signal}. Shutting down gracefully...`);
3318
+ try {
3319
+ await handle.shutdown();
3320
+ } finally {
3321
+ if (stateFilePath)
3322
+ removeStateFile(stateFilePath);
3323
+ process.exit(signal === "SIGINT" ? 130 : 143);
3324
+ }
3325
+ };
3326
+ process.once("SIGINT", () => {
3327
+ onShutdown("SIGINT");
3328
+ });
3329
+ process.once("SIGTERM", () => {
3330
+ onShutdown("SIGTERM");
3331
+ });
3332
+ if (process.platform !== "win32") {
3333
+ process.once("SIGHUP", () => {
3334
+ onShutdown("SIGHUP");
2959
3335
  });
3336
+ }
3337
+ process.on("uncaughtException", (err) => {
3338
+ logger.debug("uncaughtException", { error: String(err) });
3339
+ console.error("Uncaught exception:", err);
3340
+ handle.shutdown().finally(() => process.exit(1));
3341
+ });
3342
+ return handle;
3343
+ }
3344
+ async function runDaemonParent(opts) {
3345
+ const { serverHost, serverPort, stateFile, logFile } = opts;
3346
+ const stateFilePath = stateFile || defaultStateFile();
3347
+ let releaseLock;
3348
+ try {
3349
+ releaseLock = acquireDaemonLock(stateFilePath);
3350
+ } catch (err) {
3351
+ console.error(err instanceof Error ? err.message : String(err));
3352
+ process.exit(1);
3353
+ }
3354
+ const token = generateShutdownToken();
3355
+ const scriptPath = process.argv[1];
3356
+ const args = [scriptPath, ...process.argv.slice(2)];
3357
+ logger.debug("daemon re-exec", { execPath: process.execPath, args });
3358
+ const child = Bun.spawn([process.execPath, ...args], {
3359
+ stdin: "ignore",
3360
+ stdout: logFile ? Bun.file(logFile) : "ignore",
3361
+ stderr: logFile ? Bun.file(logFile) : "ignore",
3362
+ env: {
3363
+ ...process.env,
3364
+ [DAEMON_CHILD_ENV]: "1",
3365
+ [SHUTDOWN_TOKEN_ENV]: token
3366
+ }
2960
3367
  });
3368
+ logger.debug("daemon child spawned", { pid: child.pid, stateFilePath });
3369
+ const baseUrl = `http://${serverHost}:${serverPort}`;
3370
+ const startupCrash = child.exited.then((code) => {
3371
+ throw new Error(`Proxy exited during startup with code ${code}. Run with CONFIDENTIAL_PROXY_LOG_LEVEL=debug to capture logs.`);
3372
+ });
3373
+ let started = false;
3374
+ try {
3375
+ await Promise.race([pollForReadiness(baseUrl), startupCrash]);
3376
+ started = true;
3377
+ } catch (err) {
3378
+ console.error(err instanceof Error ? err.message : String(err));
3379
+ } finally {
3380
+ child.unref();
3381
+ startupCrash.catch(() => {});
3382
+ if (!started) {
3383
+ releaseLock?.();
3384
+ removeStateFile(stateFilePath);
3385
+ process.exit(1);
3386
+ }
3387
+ releaseLock?.();
3388
+ }
3389
+ console.log(`Proxy started. PID: ${child.pid}. State file: ${stateFilePath}`);
3390
+ process.exit(0);
2961
3391
  }
2962
3392
  // src/server.ts
2963
3393
  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>;
@@ -1,6 +1,7 @@
1
1
  import express from "express";
2
2
  import type { RvencClient } from "../core";
3
3
  import type { CreateServerAppOptions, ServerCompatMode } from "../types";
4
+ export declare function setDraining(value: boolean): void;
4
5
  export type RvencServerDeps = {
5
6
  getOrCreateClient: (apiKey: string) => Promise<RvencClient>;
6
7
  };
@@ -0,0 +1,5 @@
1
+ import type { Application } from "express";
2
+ import type { ProxyServerHandle } from "../types";
3
+ export declare function createDrainingServer(app: Application, port: number, host: string, opts?: {
4
+ shutdownTimeoutMs?: number;
5
+ }): Promise<ProxyServerHandle>;
@@ -1,4 +1,6 @@
1
1
  import type { Application } from "express";
2
+ export declare const SERVER_MESSAGE = "Rvenc API Server";
3
+ export declare const SERVER_VERSION: any;
2
4
  export declare function registerApiDiscoveryRoute(app: Application, mount: {
3
5
  openai: boolean;
4
6
  anthropic: boolean;