@premai/api-sdk 1.0.46 → 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
  }
@@ -1932,6 +1952,10 @@ function anthropicMessagesCreateToOpenAI(body) {
1932
1952
  messages.push(...systemToOpenAiMessages(body.system));
1933
1953
  }
1934
1954
  for (const m of body.messages) {
1955
+ if (m.role === "system") {
1956
+ messages.push(...systemToOpenAiMessages(m.content));
1957
+ continue;
1958
+ }
1935
1959
  if (m.role !== "user" && m.role !== "assistant") {
1936
1960
  throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
1937
1961
  }
@@ -2356,7 +2380,8 @@ function toAnthropicModel(model) {
2356
2380
  type: "model",
2357
2381
  id: model.model,
2358
2382
  display_name: model.name || model.model,
2359
- created_at: model.created_at
2383
+ created_at: model.created_at,
2384
+ description: model.description
2360
2385
  };
2361
2386
  }
2362
2387
  function filterEnabled(models) {
@@ -2451,8 +2476,8 @@ function registerAnthropicModelsRoute(router, deps) {
2451
2476
 
2452
2477
  // src/server/runtime.ts
2453
2478
  import multer from "multer";
2454
- var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
2455
- 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;
2456
2481
  var CLIENT_CACHE_MAX = (() => {
2457
2482
  let cacheTTL = 256;
2458
2483
  const raw = process.env.CLIENT_CACHE_MAX;
@@ -2482,9 +2507,7 @@ function applyServerOptions(options) {
2482
2507
  if (enclaveUrl) {
2483
2508
  serverEnclaveUrl = enclaveUrl;
2484
2509
  }
2485
- if (kek) {
2486
- serverKek = kek;
2487
- }
2510
+ serverKek = kek || generateNewClientKEK();
2488
2511
  }
2489
2512
  async function getOrCreateRvencClient(apiKey) {
2490
2513
  const existing = clientCache.get(apiKey);
@@ -2703,6 +2726,40 @@ function registerOpenAICompatRoutes(router, deps) {
2703
2726
  });
2704
2727
  }
2705
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
+
2706
2763
  // src/server/route-prefix.ts
2707
2764
  function normalizeRoutePrefix(raw) {
2708
2765
  if (raw == null) {
@@ -2737,6 +2794,9 @@ function prefixedRoute(prefix, path) {
2737
2794
  }
2738
2795
 
2739
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;
2740
2800
  function registerApiDiscoveryRoute(app, mount) {
2741
2801
  const {
2742
2802
  openai: mountOpenAI,
@@ -2766,8 +2826,8 @@ function registerApiDiscoveryRoute(app, mount) {
2766
2826
  labels.push("Anthropic Messages-compatible");
2767
2827
  }
2768
2828
  res.json({
2769
- message: `Rvenc API Server (${labels.join(" + ")})`,
2770
- version: "1.0.0",
2829
+ message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
2830
+ version: SERVER_VERSION,
2771
2831
  compat: resolveCompatLabel(mount),
2772
2832
  route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
2773
2833
  endpoints: endpoints2
@@ -2797,7 +2857,66 @@ function resolveCompatLabel(mount) {
2797
2857
  return "openai";
2798
2858
  }
2799
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
+
2800
2915
  // src/server/create-app.ts
2916
+ var draining = false;
2917
+ function setDraining(value) {
2918
+ draining = value;
2919
+ }
2801
2920
  var rvencDeps = {
2802
2921
  getOrCreateClient: getOrCreateRvencClient
2803
2922
  };
@@ -2819,7 +2938,8 @@ function resolveCreateServerInput(compatOrOptions) {
2819
2938
  compat: compat2,
2820
2939
  openaiPrefix: openaiPrefix2,
2821
2940
  anthropicPrefix: anthropicPrefix2,
2822
- jsonBodyLimit: resolveJsonBodyLimit()
2941
+ jsonBodyLimit: resolveJsonBodyLimit(),
2942
+ shutdown: undefined
2823
2943
  };
2824
2944
  }
2825
2945
  const compat = compatOrOptions.compat ?? "openai";
@@ -2828,7 +2948,8 @@ function resolveCreateServerInput(compatOrOptions) {
2828
2948
  compat,
2829
2949
  openaiPrefix,
2830
2950
  anthropicPrefix,
2831
- jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
2951
+ jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
2952
+ shutdown: compatOrOptions.shutdown
2832
2953
  };
2833
2954
  }
2834
2955
  function httpErrorStatus(err) {
@@ -2845,11 +2966,51 @@ function mountRouter(app, prefix, router) {
2845
2966
  app.use(prefix || "/", router);
2846
2967
  }
2847
2968
  function createServerApp(compatOrOptions = "openai") {
2848
- const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
2969
+ const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
2849
2970
  const mountOpenAI = compat === "openai" || compat === "both";
2850
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
+ };
2851
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
+ });
2852
3012
  app.use(express.json({ limit: jsonBodyLimit }));
3013
+ app.use(requestDebugMiddleware);
2853
3014
  registerApiDiscoveryRoute(app, {
2854
3015
  openai: mountOpenAI,
2855
3016
  anthropic: mountAnthropic,
@@ -2868,18 +3029,18 @@ function createServerApp(compatOrOptions = "openai") {
2868
3029
  registerAnthropicModelsRoute(router, rvencDeps);
2869
3030
  mountRouter(app, anthropicPrefix, router);
2870
3031
  }
2871
- const isAnthropicRequest = (req) => {
2872
- if (!mountAnthropic) {
2873
- return false;
2874
- }
2875
- if (!mountOpenAI) {
2876
- return true;
2877
- }
2878
- return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
2879
- };
3032
+ if (shutdown) {
3033
+ registerShutdownRoute(app, shutdown);
3034
+ }
2880
3035
  app.use((err, req, res, _next) => {
2881
3036
  const status = httpErrorStatus(err);
2882
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
+ });
2883
3044
  if (isAnthropicRequest(req)) {
2884
3045
  const requestId = newAnthropicRequestId();
2885
3046
  res.setHeader("request-id", requestId);
@@ -2902,6 +3063,7 @@ function createServerApp(compatOrOptions = "openai") {
2902
3063
  });
2903
3064
  app.use((req, res) => {
2904
3065
  const message = `Route ${req.method} ${req.path} not found`;
3066
+ logger.debug("route-not-found", { method: req.method, path: req.path });
2905
3067
  if (isAnthropicRequest(req)) {
2906
3068
  const requestId = newAnthropicRequestId();
2907
3069
  res.setHeader("request-id", requestId);
@@ -2921,7 +3083,149 @@ function createServerApp(compatOrOptions = "openai") {
2921
3083
  });
2922
3084
  return app;
2923
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
+
2924
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
+ }
2925
3229
  async function startServer(options = {}) {
2926
3230
  const {
2927
3231
  host,
@@ -2929,31 +3233,161 @@ async function startServer(options = {}) {
2929
3233
  compat: compatOpt,
2930
3234
  openaiRoutePrefix,
2931
3235
  anthropicRoutePrefix,
2932
- jsonBodyLimit
3236
+ jsonBodyLimit,
3237
+ daemon,
3238
+ stateFile,
3239
+ logFile,
3240
+ shutdownTimeoutMs
2933
3241
  } = options;
2934
3242
  const serverHost = host || DEFAULT_HOST;
2935
3243
  const serverPort = port || DEFAULT_PORT;
2936
3244
  const compat = compatOpt ?? "openai";
2937
3245
  applyServerOptions(options);
2938
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;
2939
3286
  const app = createServerApp({
2940
3287
  compat,
2941
3288
  openaiRoutePrefix,
2942
3289
  anthropicRoutePrefix,
2943
- jsonBodyLimit
3290
+ jsonBodyLimit,
3291
+ shutdown: shutdownConfig
2944
3292
  });
2945
- return new Promise((resolve, reject) => {
2946
- const server = app.listen(serverPort, serverHost, () => {
2947
- 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
2948
3308
  });
2949
- server.on("error", (error) => {
2950
- if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
2951
- reject(new Error(`Port ${serverPort} is already in use`));
2952
- } else {
2953
- reject(error);
2954
- }
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");
2955
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
+ }
2956
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);
2957
3391
  }
2958
3392
  // src/server.ts
2959
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>;