@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/README.md +57 -7
- package/dist/anthropic/to-openai.d.ts +1 -1
- package/dist/bare.cjs +20 -5
- package/dist/bare.mjs +20 -5
- package/dist/cli-claude.mjs +419 -3004
- package/dist/cli.mjs +2774 -2206
- package/dist/core.browser.cjs +26 -6
- package/dist/core.browser.mjs +20 -5
- package/dist/core.d.ts +2 -2
- package/dist/files/index.d.ts +3 -3
- package/dist/index.cjs +459 -36
- package/dist/index.mjs +471 -37
- package/dist/launcher/proxy-subprocess.d.ts +4 -2
- package/dist/server/create-app.d.ts +1 -0
- package/dist/server/create-drain-wrapper.d.ts +5 -0
- package/dist/server/discovery.d.ts +2 -0
- package/dist/server/request-debug.d.ts +2 -0
- package/dist/server/runtime.d.ts +2 -2
- package/dist/server/shutdown-route.d.ts +6 -0
- package/dist/server/start.d.ts +7 -4
- package/dist/server.d.ts +2 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/utils/crypto.d.ts +1 -1
- package/dist/utils/debug.d.ts +5 -0
- package/dist/utils/dek-store.d.ts +1 -1
- package/dist/utils/poll-ready.d.ts +2 -0
- package/dist/utils/state-file.d.ts +13 -0
- package/package.json +2 -1
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, {
|
|
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, {
|
|
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, {
|
|
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 = [
|
|
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, {
|
|
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
|
}
|
|
@@ -2017,6 +2032,10 @@ function anthropicMessagesCreateToOpenAI(body) {
|
|
|
2017
2032
|
messages.push(...systemToOpenAiMessages(body.system));
|
|
2018
2033
|
}
|
|
2019
2034
|
for (const m of body.messages) {
|
|
2035
|
+
if (m.role === "system") {
|
|
2036
|
+
messages.push(...systemToOpenAiMessages(m.content));
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2020
2039
|
if (m.role !== "user" && m.role !== "assistant") {
|
|
2021
2040
|
throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
|
|
2022
2041
|
}
|
|
@@ -2441,7 +2460,8 @@ function toAnthropicModel(model) {
|
|
|
2441
2460
|
type: "model",
|
|
2442
2461
|
id: model.model,
|
|
2443
2462
|
display_name: model.name || model.model,
|
|
2444
|
-
created_at: model.created_at
|
|
2463
|
+
created_at: model.created_at,
|
|
2464
|
+
description: model.description
|
|
2445
2465
|
};
|
|
2446
2466
|
}
|
|
2447
2467
|
function filterEnabled(models) {
|
|
@@ -2536,8 +2556,8 @@ function registerAnthropicModelsRoute(router, deps) {
|
|
|
2536
2556
|
|
|
2537
2557
|
// src/server/runtime.ts
|
|
2538
2558
|
var import_multer = __toESM(require("multer"));
|
|
2539
|
-
var DEFAULT_HOST =
|
|
2540
|
-
var DEFAULT_PORT =
|
|
2559
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
2560
|
+
var DEFAULT_PORT = 8787;
|
|
2541
2561
|
var CLIENT_CACHE_MAX = (() => {
|
|
2542
2562
|
let cacheTTL = 256;
|
|
2543
2563
|
const raw = process.env.CLIENT_CACHE_MAX;
|
|
@@ -2567,9 +2587,7 @@ function applyServerOptions(options) {
|
|
|
2567
2587
|
if (enclaveUrl) {
|
|
2568
2588
|
serverEnclaveUrl = enclaveUrl;
|
|
2569
2589
|
}
|
|
2570
|
-
|
|
2571
|
-
serverKek = kek;
|
|
2572
|
-
}
|
|
2590
|
+
serverKek = kek || generateNewClientKEK();
|
|
2573
2591
|
}
|
|
2574
2592
|
async function getOrCreateRvencClient(apiKey) {
|
|
2575
2593
|
const existing = clientCache.get(apiKey);
|
|
@@ -2788,6 +2806,40 @@ function registerOpenAICompatRoutes(router, deps) {
|
|
|
2788
2806
|
});
|
|
2789
2807
|
}
|
|
2790
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
|
+
|
|
2791
2843
|
// src/server/route-prefix.ts
|
|
2792
2844
|
function normalizeRoutePrefix(raw) {
|
|
2793
2845
|
if (raw == null) {
|
|
@@ -2822,6 +2874,9 @@ function prefixedRoute(prefix, path) {
|
|
|
2822
2874
|
}
|
|
2823
2875
|
|
|
2824
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;
|
|
2825
2880
|
function registerApiDiscoveryRoute(app, mount) {
|
|
2826
2881
|
const {
|
|
2827
2882
|
openai: mountOpenAI,
|
|
@@ -2851,8 +2906,8 @@ function registerApiDiscoveryRoute(app, mount) {
|
|
|
2851
2906
|
labels.push("Anthropic Messages-compatible");
|
|
2852
2907
|
}
|
|
2853
2908
|
res.json({
|
|
2854
|
-
message:
|
|
2855
|
-
version:
|
|
2909
|
+
message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
|
|
2910
|
+
version: SERVER_VERSION,
|
|
2856
2911
|
compat: resolveCompatLabel(mount),
|
|
2857
2912
|
route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
|
|
2858
2913
|
endpoints: endpoints2
|
|
@@ -2882,7 +2937,66 @@ function resolveCompatLabel(mount) {
|
|
|
2882
2937
|
return "openai";
|
|
2883
2938
|
}
|
|
2884
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
|
+
|
|
2885
2995
|
// src/server/create-app.ts
|
|
2996
|
+
var draining = false;
|
|
2997
|
+
function setDraining(value) {
|
|
2998
|
+
draining = value;
|
|
2999
|
+
}
|
|
2886
3000
|
var rvencDeps = {
|
|
2887
3001
|
getOrCreateClient: getOrCreateRvencClient
|
|
2888
3002
|
};
|
|
@@ -2904,7 +3018,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2904
3018
|
compat: compat2,
|
|
2905
3019
|
openaiPrefix: openaiPrefix2,
|
|
2906
3020
|
anthropicPrefix: anthropicPrefix2,
|
|
2907
|
-
jsonBodyLimit: resolveJsonBodyLimit()
|
|
3021
|
+
jsonBodyLimit: resolveJsonBodyLimit(),
|
|
3022
|
+
shutdown: undefined
|
|
2908
3023
|
};
|
|
2909
3024
|
}
|
|
2910
3025
|
const compat = compatOrOptions.compat ?? "openai";
|
|
@@ -2913,7 +3028,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2913
3028
|
compat,
|
|
2914
3029
|
openaiPrefix,
|
|
2915
3030
|
anthropicPrefix,
|
|
2916
|
-
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
|
|
3031
|
+
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
|
|
3032
|
+
shutdown: compatOrOptions.shutdown
|
|
2917
3033
|
};
|
|
2918
3034
|
}
|
|
2919
3035
|
function httpErrorStatus(err) {
|
|
@@ -2930,11 +3046,51 @@ function mountRouter(app, prefix, router) {
|
|
|
2930
3046
|
app.use(prefix || "/", router);
|
|
2931
3047
|
}
|
|
2932
3048
|
function createServerApp(compatOrOptions = "openai") {
|
|
2933
|
-
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
|
|
3049
|
+
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
|
|
2934
3050
|
const mountOpenAI = compat === "openai" || compat === "both";
|
|
2935
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
|
+
};
|
|
2936
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
|
+
});
|
|
2937
3092
|
app.use(import_express.default.json({ limit: jsonBodyLimit }));
|
|
3093
|
+
app.use(requestDebugMiddleware);
|
|
2938
3094
|
registerApiDiscoveryRoute(app, {
|
|
2939
3095
|
openai: mountOpenAI,
|
|
2940
3096
|
anthropic: mountAnthropic,
|
|
@@ -2953,18 +3109,18 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2953
3109
|
registerAnthropicModelsRoute(router, rvencDeps);
|
|
2954
3110
|
mountRouter(app, anthropicPrefix, router);
|
|
2955
3111
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
}
|
|
2960
|
-
if (!mountOpenAI) {
|
|
2961
|
-
return true;
|
|
2962
|
-
}
|
|
2963
|
-
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
2964
|
-
};
|
|
3112
|
+
if (shutdown) {
|
|
3113
|
+
registerShutdownRoute(app, shutdown);
|
|
3114
|
+
}
|
|
2965
3115
|
app.use((err, req, res, _next) => {
|
|
2966
3116
|
const status = httpErrorStatus(err);
|
|
2967
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
|
+
});
|
|
2968
3124
|
if (isAnthropicRequest(req)) {
|
|
2969
3125
|
const requestId = newAnthropicRequestId();
|
|
2970
3126
|
res.setHeader("request-id", requestId);
|
|
@@ -2987,6 +3143,7 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2987
3143
|
});
|
|
2988
3144
|
app.use((req, res) => {
|
|
2989
3145
|
const message = `Route ${req.method} ${req.path} not found`;
|
|
3146
|
+
logger.debug("route-not-found", { method: req.method, path: req.path });
|
|
2990
3147
|
if (isAnthropicRequest(req)) {
|
|
2991
3148
|
const requestId = newAnthropicRequestId();
|
|
2992
3149
|
res.setHeader("request-id", requestId);
|
|
@@ -3006,7 +3163,143 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
3006
3163
|
});
|
|
3007
3164
|
return app;
|
|
3008
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
|
+
|
|
3009
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
|
+
}
|
|
3010
3303
|
async function startServer(options = {}) {
|
|
3011
3304
|
const {
|
|
3012
3305
|
host,
|
|
@@ -3014,31 +3307,161 @@ async function startServer(options = {}) {
|
|
|
3014
3307
|
compat: compatOpt,
|
|
3015
3308
|
openaiRoutePrefix,
|
|
3016
3309
|
anthropicRoutePrefix,
|
|
3017
|
-
jsonBodyLimit
|
|
3310
|
+
jsonBodyLimit,
|
|
3311
|
+
daemon,
|
|
3312
|
+
stateFile,
|
|
3313
|
+
logFile,
|
|
3314
|
+
shutdownTimeoutMs
|
|
3018
3315
|
} = options;
|
|
3019
3316
|
const serverHost = host || DEFAULT_HOST;
|
|
3020
3317
|
const serverPort = port || DEFAULT_PORT;
|
|
3021
3318
|
const compat = compatOpt ?? "openai";
|
|
3022
3319
|
applyServerOptions(options);
|
|
3023
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;
|
|
3024
3360
|
const app = createServerApp({
|
|
3025
3361
|
compat,
|
|
3026
3362
|
openaiRoutePrefix,
|
|
3027
3363
|
anthropicRoutePrefix,
|
|
3028
|
-
jsonBodyLimit
|
|
3364
|
+
jsonBodyLimit,
|
|
3365
|
+
shutdown: shutdownConfig
|
|
3029
3366
|
});
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
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
|
|
3033
3382
|
});
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
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");
|
|
3040
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
|
+
}
|
|
3041
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);
|
|
3042
3465
|
}
|
|
3043
3466
|
// src/server.ts
|
|
3044
3467
|
var server_default = createServerApp("both");
|