@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/README.md +57 -7
- package/dist/anthropic/from-openai.d.ts +8 -1
- package/dist/bare.cjs +20 -5
- package/dist/bare.mjs +20 -5
- package/dist/cli-claude.mjs +419 -3008
- package/dist/cli.mjs +2776 -2202
- 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 +465 -36
- package/dist/index.mjs +477 -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
|
}
|
|
@@ -1916,6 +1931,9 @@ function anthropicAssistantContentToOpenAI(content) {
|
|
|
1916
1931
|
textParts.push(part.text);
|
|
1917
1932
|
continue;
|
|
1918
1933
|
}
|
|
1934
|
+
if (part.type === "thinking" || part.type === "redacted_thinking") {
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1919
1937
|
if (part.type === "tool_use") {
|
|
1920
1938
|
const p = part;
|
|
1921
1939
|
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
@@ -2089,6 +2107,13 @@ function extractTextCharCount(body) {
|
|
|
2089
2107
|
continue;
|
|
2090
2108
|
if (part.type === "text" && typeof part.text === "string") {
|
|
2091
2109
|
len += part.text.length;
|
|
2110
|
+
} else if (part.type === "thinking" && typeof part.thinking === "string") {
|
|
2111
|
+
len += part.thinking.length;
|
|
2112
|
+
} else if (part.type === "redacted_thinking") {
|
|
2113
|
+
const d = part.data;
|
|
2114
|
+
if (typeof d === "string") {
|
|
2115
|
+
len += d.length;
|
|
2116
|
+
}
|
|
2092
2117
|
} else if (part.type === "tool_result") {
|
|
2093
2118
|
const c = part.content;
|
|
2094
2119
|
if (typeof c === "string") {
|
|
@@ -2445,7 +2470,8 @@ function toAnthropicModel(model) {
|
|
|
2445
2470
|
type: "model",
|
|
2446
2471
|
id: model.model,
|
|
2447
2472
|
display_name: model.name || model.model,
|
|
2448
|
-
created_at: model.created_at
|
|
2473
|
+
created_at: model.created_at,
|
|
2474
|
+
description: model.description
|
|
2449
2475
|
};
|
|
2450
2476
|
}
|
|
2451
2477
|
function filterEnabled(models) {
|
|
@@ -2540,8 +2566,8 @@ function registerAnthropicModelsRoute(router, deps) {
|
|
|
2540
2566
|
|
|
2541
2567
|
// src/server/runtime.ts
|
|
2542
2568
|
var import_multer = __toESM(require("multer"));
|
|
2543
|
-
var DEFAULT_HOST =
|
|
2544
|
-
var DEFAULT_PORT =
|
|
2569
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
2570
|
+
var DEFAULT_PORT = 8787;
|
|
2545
2571
|
var CLIENT_CACHE_MAX = (() => {
|
|
2546
2572
|
let cacheTTL = 256;
|
|
2547
2573
|
const raw = process.env.CLIENT_CACHE_MAX;
|
|
@@ -2571,9 +2597,7 @@ function applyServerOptions(options) {
|
|
|
2571
2597
|
if (enclaveUrl) {
|
|
2572
2598
|
serverEnclaveUrl = enclaveUrl;
|
|
2573
2599
|
}
|
|
2574
|
-
|
|
2575
|
-
serverKek = kek;
|
|
2576
|
-
}
|
|
2600
|
+
serverKek = kek || generateNewClientKEK();
|
|
2577
2601
|
}
|
|
2578
2602
|
async function getOrCreateRvencClient(apiKey) {
|
|
2579
2603
|
const existing = clientCache.get(apiKey);
|
|
@@ -2792,6 +2816,40 @@ function registerOpenAICompatRoutes(router, deps) {
|
|
|
2792
2816
|
});
|
|
2793
2817
|
}
|
|
2794
2818
|
|
|
2819
|
+
// src/utils/debug.ts
|
|
2820
|
+
var import_node_fs = require("node:fs");
|
|
2821
|
+
var import_node_path = require("node:path");
|
|
2822
|
+
var import_env_paths = __toESM(require("env-paths"));
|
|
2823
|
+
var import_winston = __toESM(require("winston"));
|
|
2824
|
+
var defaultLogFile = `${import_env_paths.default("confidential-proxy").data}/confidential-proxy.log`;
|
|
2825
|
+
var dir = import_node_path.dirname(defaultLogFile);
|
|
2826
|
+
try {
|
|
2827
|
+
if (!import_node_fs.existsSync(dir))
|
|
2828
|
+
import_node_fs.mkdirSync(dir, { recursive: true });
|
|
2829
|
+
} catch {}
|
|
2830
|
+
var level = process.env.CONFIDENTIAL_PROXY_LOG_LEVEL ?? "info";
|
|
2831
|
+
var fileTransport = new import_winston.default.transports.File({
|
|
2832
|
+
filename: defaultLogFile,
|
|
2833
|
+
level,
|
|
2834
|
+
maxsize: 10 * 1024 * 1024,
|
|
2835
|
+
maxFiles: 3,
|
|
2836
|
+
format: import_winston.default.format.combine(import_winston.default.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), import_winston.default.format.json())
|
|
2837
|
+
});
|
|
2838
|
+
var consoleTransport = new import_winston.default.transports.Console({
|
|
2839
|
+
level,
|
|
2840
|
+
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 }) => {
|
|
2841
|
+
const meta = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : "";
|
|
2842
|
+
return `[${timestamp}] [${level2}] ${message}${meta}`;
|
|
2843
|
+
}))
|
|
2844
|
+
});
|
|
2845
|
+
var logger = import_winston.default.createLogger({
|
|
2846
|
+
level,
|
|
2847
|
+
transports: [fileTransport, consoleTransport]
|
|
2848
|
+
});
|
|
2849
|
+
|
|
2850
|
+
// src/server/discovery.ts
|
|
2851
|
+
var import_node_fs2 = require("node:fs");
|
|
2852
|
+
|
|
2795
2853
|
// src/server/route-prefix.ts
|
|
2796
2854
|
function normalizeRoutePrefix(raw) {
|
|
2797
2855
|
if (raw == null) {
|
|
@@ -2826,6 +2884,9 @@ function prefixedRoute(prefix, path) {
|
|
|
2826
2884
|
}
|
|
2827
2885
|
|
|
2828
2886
|
// src/server/discovery.ts
|
|
2887
|
+
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"));
|
|
2888
|
+
var SERVER_MESSAGE = "Rvenc API Server";
|
|
2889
|
+
var SERVER_VERSION = pkg.version;
|
|
2829
2890
|
function registerApiDiscoveryRoute(app, mount) {
|
|
2830
2891
|
const {
|
|
2831
2892
|
openai: mountOpenAI,
|
|
@@ -2855,8 +2916,8 @@ function registerApiDiscoveryRoute(app, mount) {
|
|
|
2855
2916
|
labels.push("Anthropic Messages-compatible");
|
|
2856
2917
|
}
|
|
2857
2918
|
res.json({
|
|
2858
|
-
message:
|
|
2859
|
-
version:
|
|
2919
|
+
message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
|
|
2920
|
+
version: SERVER_VERSION,
|
|
2860
2921
|
compat: resolveCompatLabel(mount),
|
|
2861
2922
|
route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
|
|
2862
2923
|
endpoints: endpoints2
|
|
@@ -2886,7 +2947,66 @@ function resolveCompatLabel(mount) {
|
|
|
2886
2947
|
return "openai";
|
|
2887
2948
|
}
|
|
2888
2949
|
|
|
2950
|
+
// src/server/request-debug.ts
|
|
2951
|
+
function requestDebugMiddleware(req, res, next) {
|
|
2952
|
+
const start = Date.now();
|
|
2953
|
+
const method = req.method;
|
|
2954
|
+
const path = req.path;
|
|
2955
|
+
logger.debug("request", { method, path });
|
|
2956
|
+
res.on("finish", () => {
|
|
2957
|
+
const elapsed = Date.now() - start;
|
|
2958
|
+
const status = res.statusCode;
|
|
2959
|
+
const contentLength = res.getHeader("content-length") ?? res.get("content-length");
|
|
2960
|
+
logger.debug("response", {
|
|
2961
|
+
method,
|
|
2962
|
+
path,
|
|
2963
|
+
status,
|
|
2964
|
+
elapsedMs: elapsed,
|
|
2965
|
+
contentLength
|
|
2966
|
+
});
|
|
2967
|
+
});
|
|
2968
|
+
next();
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/server/shutdown-route.ts
|
|
2972
|
+
var import_node_crypto = require("node:crypto");
|
|
2973
|
+
var LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
2974
|
+
function isLoopback(addr) {
|
|
2975
|
+
return !!addr && LOOPBACK.has(addr);
|
|
2976
|
+
}
|
|
2977
|
+
function hexBuf(s) {
|
|
2978
|
+
if (!s || s.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(s))
|
|
2979
|
+
return null;
|
|
2980
|
+
return Buffer.from(s, "hex");
|
|
2981
|
+
}
|
|
2982
|
+
function registerShutdownRoute(app, hooks) {
|
|
2983
|
+
const expected = Buffer.from(hooks.token, "hex");
|
|
2984
|
+
app.post("/__shutdown", (req, res) => {
|
|
2985
|
+
if (!isLoopback(req.socket.remoteAddress)) {
|
|
2986
|
+
logger.debug("shutdown rejected: non-loopback", {
|
|
2987
|
+
remote: req.socket.remoteAddress
|
|
2988
|
+
});
|
|
2989
|
+
res.status(403).end();
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const provided = hexBuf(req.header("x-shutdown-token") ?? "");
|
|
2993
|
+
if (!provided || provided.length !== expected.length || !import_node_crypto.timingSafeEqual(provided, expected)) {
|
|
2994
|
+
res.status(401).end();
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
logger.debug("shutdown accepted");
|
|
2998
|
+
res.status(202).end();
|
|
2999
|
+
setImmediate(() => {
|
|
3000
|
+
hooks.onShutdown().catch((err) => logger.debug("shutdown error", { error: String(err) })).finally(() => process.exit(0));
|
|
3001
|
+
});
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
|
|
2889
3005
|
// src/server/create-app.ts
|
|
3006
|
+
var draining = false;
|
|
3007
|
+
function setDraining(value) {
|
|
3008
|
+
draining = value;
|
|
3009
|
+
}
|
|
2890
3010
|
var rvencDeps = {
|
|
2891
3011
|
getOrCreateClient: getOrCreateRvencClient
|
|
2892
3012
|
};
|
|
@@ -2908,7 +3028,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2908
3028
|
compat: compat2,
|
|
2909
3029
|
openaiPrefix: openaiPrefix2,
|
|
2910
3030
|
anthropicPrefix: anthropicPrefix2,
|
|
2911
|
-
jsonBodyLimit: resolveJsonBodyLimit()
|
|
3031
|
+
jsonBodyLimit: resolveJsonBodyLimit(),
|
|
3032
|
+
shutdown: undefined
|
|
2912
3033
|
};
|
|
2913
3034
|
}
|
|
2914
3035
|
const compat = compatOrOptions.compat ?? "openai";
|
|
@@ -2917,7 +3038,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2917
3038
|
compat,
|
|
2918
3039
|
openaiPrefix,
|
|
2919
3040
|
anthropicPrefix,
|
|
2920
|
-
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
|
|
3041
|
+
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
|
|
3042
|
+
shutdown: compatOrOptions.shutdown
|
|
2921
3043
|
};
|
|
2922
3044
|
}
|
|
2923
3045
|
function httpErrorStatus(err) {
|
|
@@ -2934,11 +3056,51 @@ function mountRouter(app, prefix, router) {
|
|
|
2934
3056
|
app.use(prefix || "/", router);
|
|
2935
3057
|
}
|
|
2936
3058
|
function createServerApp(compatOrOptions = "openai") {
|
|
2937
|
-
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
|
|
3059
|
+
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
|
|
2938
3060
|
const mountOpenAI = compat === "openai" || compat === "both";
|
|
2939
3061
|
const mountAnthropic = compat === "anthropic" || compat === "both";
|
|
3062
|
+
const isAnthropicRequest = (req) => {
|
|
3063
|
+
if (!mountAnthropic) {
|
|
3064
|
+
return false;
|
|
3065
|
+
}
|
|
3066
|
+
if (!mountOpenAI) {
|
|
3067
|
+
return true;
|
|
3068
|
+
}
|
|
3069
|
+
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
3070
|
+
};
|
|
2940
3071
|
const app = import_express.default();
|
|
3072
|
+
app.use((req, res, next) => {
|
|
3073
|
+
if (draining) {
|
|
3074
|
+
logger.debug("drain-reject", { method: req.method, path: req.path });
|
|
3075
|
+
if (!res.headersSent) {
|
|
3076
|
+
res.setHeader("Connection", "close");
|
|
3077
|
+
res.setHeader("Retry-After", "5");
|
|
3078
|
+
if (isAnthropicRequest(req)) {
|
|
3079
|
+
const requestId = newAnthropicRequestId();
|
|
3080
|
+
res.setHeader("request-id", requestId);
|
|
3081
|
+
res.status(503).json({
|
|
3082
|
+
type: "error",
|
|
3083
|
+
error: {
|
|
3084
|
+
type: httpStatusToAnthropicErrorType(503),
|
|
3085
|
+
message: "Server is shutting down"
|
|
3086
|
+
},
|
|
3087
|
+
request_id: requestId
|
|
3088
|
+
});
|
|
3089
|
+
} else {
|
|
3090
|
+
res.status(503).json({
|
|
3091
|
+
error: {
|
|
3092
|
+
message: "Server is shutting down",
|
|
3093
|
+
type: "server_error"
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
next();
|
|
3101
|
+
});
|
|
2941
3102
|
app.use(import_express.default.json({ limit: jsonBodyLimit }));
|
|
3103
|
+
app.use(requestDebugMiddleware);
|
|
2942
3104
|
registerApiDiscoveryRoute(app, {
|
|
2943
3105
|
openai: mountOpenAI,
|
|
2944
3106
|
anthropic: mountAnthropic,
|
|
@@ -2957,18 +3119,18 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2957
3119
|
registerAnthropicModelsRoute(router, rvencDeps);
|
|
2958
3120
|
mountRouter(app, anthropicPrefix, router);
|
|
2959
3121
|
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
}
|
|
2964
|
-
if (!mountOpenAI) {
|
|
2965
|
-
return true;
|
|
2966
|
-
}
|
|
2967
|
-
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
2968
|
-
};
|
|
3122
|
+
if (shutdown) {
|
|
3123
|
+
registerShutdownRoute(app, shutdown);
|
|
3124
|
+
}
|
|
2969
3125
|
app.use((err, req, res, _next) => {
|
|
2970
3126
|
const status = httpErrorStatus(err);
|
|
2971
3127
|
const message = err instanceof Error ? err.message : "Internal server error";
|
|
3128
|
+
logger.debug("request-error", {
|
|
3129
|
+
method: req.method,
|
|
3130
|
+
path: req.path,
|
|
3131
|
+
status,
|
|
3132
|
+
message
|
|
3133
|
+
});
|
|
2972
3134
|
if (isAnthropicRequest(req)) {
|
|
2973
3135
|
const requestId = newAnthropicRequestId();
|
|
2974
3136
|
res.setHeader("request-id", requestId);
|
|
@@ -2991,6 +3153,7 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2991
3153
|
});
|
|
2992
3154
|
app.use((req, res) => {
|
|
2993
3155
|
const message = `Route ${req.method} ${req.path} not found`;
|
|
3156
|
+
logger.debug("route-not-found", { method: req.method, path: req.path });
|
|
2994
3157
|
if (isAnthropicRequest(req)) {
|
|
2995
3158
|
const requestId = newAnthropicRequestId();
|
|
2996
3159
|
res.setHeader("request-id", requestId);
|
|
@@ -3010,7 +3173,143 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
3010
3173
|
});
|
|
3011
3174
|
return app;
|
|
3012
3175
|
}
|
|
3176
|
+
// src/server/create-drain-wrapper.ts
|
|
3177
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 30000;
|
|
3178
|
+
function createDrainingServer(app, port, host, opts = {}) {
|
|
3179
|
+
const timeoutMs = opts.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
3180
|
+
const activeSockets = new Set;
|
|
3181
|
+
return new Promise((resolve, reject) => {
|
|
3182
|
+
const server = app.listen(port, host, () => {
|
|
3183
|
+
server.on("connection", (socket) => {
|
|
3184
|
+
activeSockets.add(socket);
|
|
3185
|
+
socket.once("close", () => activeSockets.delete(socket));
|
|
3186
|
+
});
|
|
3187
|
+
const { port: boundPort } = server.address();
|
|
3188
|
+
resolve({
|
|
3189
|
+
port: boundPort,
|
|
3190
|
+
close: () => {
|
|
3191
|
+
setDraining(true);
|
|
3192
|
+
server.close();
|
|
3193
|
+
},
|
|
3194
|
+
shutdown: async (customTimeout) => {
|
|
3195
|
+
const deadline = customTimeout ?? timeoutMs;
|
|
3196
|
+
setDraining(true);
|
|
3197
|
+
server.close();
|
|
3198
|
+
const deadlineTime = Date.now() + deadline;
|
|
3199
|
+
while (activeSockets.size > 0 && Date.now() < deadlineTime) {
|
|
3200
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
3201
|
+
}
|
|
3202
|
+
for (const socket of activeSockets) {
|
|
3203
|
+
socket.destroy();
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
});
|
|
3207
|
+
});
|
|
3208
|
+
server.on("error", (err) => {
|
|
3209
|
+
if (err.code === "EADDRINUSE") {
|
|
3210
|
+
const e = new Error(`Port ${port} is already in use`);
|
|
3211
|
+
e.code = "EADDRINUSE";
|
|
3212
|
+
reject(e);
|
|
3213
|
+
} else {
|
|
3214
|
+
reject(err);
|
|
3215
|
+
}
|
|
3216
|
+
});
|
|
3217
|
+
});
|
|
3218
|
+
}
|
|
3219
|
+
// src/utils/poll-ready.ts
|
|
3220
|
+
async function isProxyRoot(baseUrl) {
|
|
3221
|
+
try {
|
|
3222
|
+
const res = await fetch(`${baseUrl}/`, {
|
|
3223
|
+
signal: AbortSignal.timeout(2000)
|
|
3224
|
+
});
|
|
3225
|
+
if (!res.ok)
|
|
3226
|
+
return false;
|
|
3227
|
+
const body = await res.json();
|
|
3228
|
+
return typeof body === "object" && body !== null && typeof body.message === "string" && body.message.startsWith(SERVER_MESSAGE);
|
|
3229
|
+
} catch {
|
|
3230
|
+
return false;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
async function pollForReadiness(baseUrl, timeoutMs = 30000) {
|
|
3234
|
+
const deadline = Date.now() + timeoutMs;
|
|
3235
|
+
let backoff = 200;
|
|
3236
|
+
while (Date.now() < deadline) {
|
|
3237
|
+
if (await isProxyRoot(baseUrl))
|
|
3238
|
+
return;
|
|
3239
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
3240
|
+
backoff = Math.min(backoff * 1.5, 2000);
|
|
3241
|
+
}
|
|
3242
|
+
throw new Error(`Proxy did not become reachable within ${timeoutMs}ms`);
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// src/utils/state-file.ts
|
|
3246
|
+
var import_node_fs3 = require("node:fs");
|
|
3247
|
+
var import_node_path2 = require("node:path");
|
|
3248
|
+
var import_utils8 = require("@noble/ciphers/utils.js");
|
|
3249
|
+
var import_env_paths2 = __toESM(require("env-paths"));
|
|
3250
|
+
var appData = import_env_paths2.default("confidential-proxy");
|
|
3251
|
+
function defaultStateFile() {
|
|
3252
|
+
return `${appData.data}/proxy.state.json`;
|
|
3253
|
+
}
|
|
3254
|
+
function writeStateFile(path, state) {
|
|
3255
|
+
const dir2 = import_node_path2.dirname(path);
|
|
3256
|
+
if (!import_node_fs3.existsSync(dir2))
|
|
3257
|
+
import_node_fs3.mkdirSync(dir2, { recursive: true });
|
|
3258
|
+
import_node_fs3.writeFileSync(path, JSON.stringify(state), { mode: 384 });
|
|
3259
|
+
}
|
|
3260
|
+
function removeStateFile(path) {
|
|
3261
|
+
try {
|
|
3262
|
+
if (import_node_fs3.existsSync(path))
|
|
3263
|
+
import_node_fs3.unlinkSync(path);
|
|
3264
|
+
} catch {}
|
|
3265
|
+
}
|
|
3266
|
+
function generateShutdownToken() {
|
|
3267
|
+
return import_utils8.bytesToHex(import_utils8.randomBytes(32));
|
|
3268
|
+
}
|
|
3269
|
+
function isProcessAlive(pid) {
|
|
3270
|
+
try {
|
|
3271
|
+
process.kill(pid, 0);
|
|
3272
|
+
return true;
|
|
3273
|
+
} catch {
|
|
3274
|
+
return false;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
function acquireDaemonLock(stateFile) {
|
|
3278
|
+
const lockFile = `${stateFile}.lock`;
|
|
3279
|
+
import_node_fs3.mkdirSync(import_node_path2.dirname(lockFile), { recursive: true });
|
|
3280
|
+
try {
|
|
3281
|
+
import_node_fs3.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
|
|
3282
|
+
} catch (err) {
|
|
3283
|
+
if (err.code !== "EEXIST")
|
|
3284
|
+
throw err;
|
|
3285
|
+
const lockPid = parseInt(import_node_fs3.readFileSync(lockFile, "utf-8").trim(), 10);
|
|
3286
|
+
if (Number.isFinite(lockPid) && lockPid > 0 && isProcessAlive(lockPid)) {
|
|
3287
|
+
throw new Error(`Daemon lock is held by PID ${lockPid}. Another instance may already be running.`);
|
|
3288
|
+
}
|
|
3289
|
+
import_node_fs3.unlinkSync(lockFile);
|
|
3290
|
+
import_node_fs3.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
|
|
3291
|
+
}
|
|
3292
|
+
return () => {
|
|
3293
|
+
try {
|
|
3294
|
+
import_node_fs3.unlinkSync(lockFile);
|
|
3295
|
+
} catch {}
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3013
3299
|
// src/server/start.ts
|
|
3300
|
+
var SHUTDOWN_TOKEN_ENV = "CONFIDENTIAL_PROXY_SHUTDOWN_TOKEN";
|
|
3301
|
+
var DAEMON_CHILD_ENV = "CONFIDENTIAL_PROXY_DAEMON_CHILD";
|
|
3302
|
+
async function bindServer(app, host, port, allowFallback, shutdownTimeoutMs) {
|
|
3303
|
+
try {
|
|
3304
|
+
return await createDrainingServer(app, port, host, { shutdownTimeoutMs });
|
|
3305
|
+
} catch (err) {
|
|
3306
|
+
if (!allowFallback || err.code !== "EADDRINUSE") {
|
|
3307
|
+
throw err;
|
|
3308
|
+
}
|
|
3309
|
+
logger.debug("port in use, falling back to OS-assigned port", { port });
|
|
3310
|
+
return await createDrainingServer(app, 0, host, { shutdownTimeoutMs });
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3014
3313
|
async function startServer(options = {}) {
|
|
3015
3314
|
const {
|
|
3016
3315
|
host,
|
|
@@ -3018,31 +3317,161 @@ async function startServer(options = {}) {
|
|
|
3018
3317
|
compat: compatOpt,
|
|
3019
3318
|
openaiRoutePrefix,
|
|
3020
3319
|
anthropicRoutePrefix,
|
|
3021
|
-
jsonBodyLimit
|
|
3320
|
+
jsonBodyLimit,
|
|
3321
|
+
daemon,
|
|
3322
|
+
stateFile,
|
|
3323
|
+
logFile,
|
|
3324
|
+
shutdownTimeoutMs
|
|
3022
3325
|
} = options;
|
|
3023
3326
|
const serverHost = host || DEFAULT_HOST;
|
|
3024
3327
|
const serverPort = port || DEFAULT_PORT;
|
|
3025
3328
|
const compat = compatOpt ?? "openai";
|
|
3026
3329
|
applyServerOptions(options);
|
|
3027
3330
|
resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
|
|
3331
|
+
const isDaemonChild = !!process.env[DAEMON_CHILD_ENV];
|
|
3332
|
+
logger.debug("startServer", {
|
|
3333
|
+
daemon,
|
|
3334
|
+
daemonChild: isDaemonChild,
|
|
3335
|
+
serverHost,
|
|
3336
|
+
serverPort,
|
|
3337
|
+
compat
|
|
3338
|
+
});
|
|
3339
|
+
if (daemon && !isDaemonChild) {
|
|
3340
|
+
return runDaemonParent({
|
|
3341
|
+
serverHost,
|
|
3342
|
+
serverPort,
|
|
3343
|
+
stateFile,
|
|
3344
|
+
logFile
|
|
3345
|
+
});
|
|
3346
|
+
}
|
|
3347
|
+
const allowFallback = !(daemon || isDaemonChild);
|
|
3348
|
+
const stateFilePath = stateFile || (isDaemonChild ? defaultStateFile() : undefined);
|
|
3349
|
+
let token;
|
|
3350
|
+
if (isDaemonChild) {
|
|
3351
|
+
token = process.env[SHUTDOWN_TOKEN_ENV];
|
|
3352
|
+
process.env[SHUTDOWN_TOKEN_ENV] = undefined;
|
|
3353
|
+
if (!token) {
|
|
3354
|
+
throw new Error(`Daemon child is missing ${SHUTDOWN_TOKEN_ENV}. Refusing to start.`);
|
|
3355
|
+
}
|
|
3356
|
+
} else if (stateFilePath) {
|
|
3357
|
+
token = generateShutdownToken();
|
|
3358
|
+
}
|
|
3359
|
+
const handleRef = { current: null };
|
|
3360
|
+
const shutdownConfig = token ? {
|
|
3361
|
+
token,
|
|
3362
|
+
onShutdown: async () => {
|
|
3363
|
+
if (handleRef.current) {
|
|
3364
|
+
await handleRef.current.shutdown();
|
|
3365
|
+
}
|
|
3366
|
+
if (stateFilePath)
|
|
3367
|
+
removeStateFile(stateFilePath);
|
|
3368
|
+
}
|
|
3369
|
+
} : undefined;
|
|
3028
3370
|
const app = createServerApp({
|
|
3029
3371
|
compat,
|
|
3030
3372
|
openaiRoutePrefix,
|
|
3031
3373
|
anthropicRoutePrefix,
|
|
3032
|
-
jsonBodyLimit
|
|
3374
|
+
jsonBodyLimit,
|
|
3375
|
+
shutdown: shutdownConfig
|
|
3033
3376
|
});
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3377
|
+
logger.debug("starting server", {
|
|
3378
|
+
serverHost,
|
|
3379
|
+
serverPort,
|
|
3380
|
+
allowFallback,
|
|
3381
|
+
isDaemonChild,
|
|
3382
|
+
hasStateFile: !!stateFilePath
|
|
3383
|
+
});
|
|
3384
|
+
const handle = await bindServer(app, serverHost, serverPort, allowFallback, shutdownTimeoutMs);
|
|
3385
|
+
handleRef.current = handle;
|
|
3386
|
+
if (stateFilePath && token) {
|
|
3387
|
+
writeStateFile(stateFilePath, {
|
|
3388
|
+
pid: process.pid,
|
|
3389
|
+
host: serverHost,
|
|
3390
|
+
port: handle.port,
|
|
3391
|
+
token
|
|
3037
3392
|
});
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3393
|
+
}
|
|
3394
|
+
if (allowFallback) {
|
|
3395
|
+
console.log(`Proxy listening on http://${serverHost}:${handle.port}`);
|
|
3396
|
+
}
|
|
3397
|
+
logger.debug("server listening", { serverHost, port: handle.port });
|
|
3398
|
+
const onShutdown = async (signal) => {
|
|
3399
|
+
logger.debug("shutdown signal", { signal });
|
|
3400
|
+
console.log(`
|
|
3401
|
+
Received ${signal}. Shutting down gracefully...`);
|
|
3402
|
+
try {
|
|
3403
|
+
await handle.shutdown();
|
|
3404
|
+
} finally {
|
|
3405
|
+
if (stateFilePath)
|
|
3406
|
+
removeStateFile(stateFilePath);
|
|
3407
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3410
|
+
process.once("SIGINT", () => {
|
|
3411
|
+
onShutdown("SIGINT");
|
|
3412
|
+
});
|
|
3413
|
+
process.once("SIGTERM", () => {
|
|
3414
|
+
onShutdown("SIGTERM");
|
|
3415
|
+
});
|
|
3416
|
+
if (process.platform !== "win32") {
|
|
3417
|
+
process.once("SIGHUP", () => {
|
|
3418
|
+
onShutdown("SIGHUP");
|
|
3044
3419
|
});
|
|
3420
|
+
}
|
|
3421
|
+
process.on("uncaughtException", (err) => {
|
|
3422
|
+
logger.debug("uncaughtException", { error: String(err) });
|
|
3423
|
+
console.error("Uncaught exception:", err);
|
|
3424
|
+
handle.shutdown().finally(() => process.exit(1));
|
|
3045
3425
|
});
|
|
3426
|
+
return handle;
|
|
3427
|
+
}
|
|
3428
|
+
async function runDaemonParent(opts) {
|
|
3429
|
+
const { serverHost, serverPort, stateFile, logFile } = opts;
|
|
3430
|
+
const stateFilePath = stateFile || defaultStateFile();
|
|
3431
|
+
let releaseLock;
|
|
3432
|
+
try {
|
|
3433
|
+
releaseLock = acquireDaemonLock(stateFilePath);
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3436
|
+
process.exit(1);
|
|
3437
|
+
}
|
|
3438
|
+
const token = generateShutdownToken();
|
|
3439
|
+
const scriptPath = process.argv[1];
|
|
3440
|
+
const args = [scriptPath, ...process.argv.slice(2)];
|
|
3441
|
+
logger.debug("daemon re-exec", { execPath: process.execPath, args });
|
|
3442
|
+
const child = Bun.spawn([process.execPath, ...args], {
|
|
3443
|
+
stdin: "ignore",
|
|
3444
|
+
stdout: logFile ? Bun.file(logFile) : "ignore",
|
|
3445
|
+
stderr: logFile ? Bun.file(logFile) : "ignore",
|
|
3446
|
+
env: {
|
|
3447
|
+
...process.env,
|
|
3448
|
+
[DAEMON_CHILD_ENV]: "1",
|
|
3449
|
+
[SHUTDOWN_TOKEN_ENV]: token
|
|
3450
|
+
}
|
|
3451
|
+
});
|
|
3452
|
+
logger.debug("daemon child spawned", { pid: child.pid, stateFilePath });
|
|
3453
|
+
const baseUrl = `http://${serverHost}:${serverPort}`;
|
|
3454
|
+
const startupCrash = child.exited.then((code) => {
|
|
3455
|
+
throw new Error(`Proxy exited during startup with code ${code}. Run with CONFIDENTIAL_PROXY_LOG_LEVEL=debug to capture logs.`);
|
|
3456
|
+
});
|
|
3457
|
+
let started = false;
|
|
3458
|
+
try {
|
|
3459
|
+
await Promise.race([pollForReadiness(baseUrl), startupCrash]);
|
|
3460
|
+
started = true;
|
|
3461
|
+
} catch (err) {
|
|
3462
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
3463
|
+
} finally {
|
|
3464
|
+
child.unref();
|
|
3465
|
+
startupCrash.catch(() => {});
|
|
3466
|
+
if (!started) {
|
|
3467
|
+
releaseLock?.();
|
|
3468
|
+
removeStateFile(stateFilePath);
|
|
3469
|
+
process.exit(1);
|
|
3470
|
+
}
|
|
3471
|
+
releaseLock?.();
|
|
3472
|
+
}
|
|
3473
|
+
console.log(`Proxy started. PID: ${child.pid}. State file: ${stateFilePath}`);
|
|
3474
|
+
process.exit(0);
|
|
3046
3475
|
}
|
|
3047
3476
|
// src/server.ts
|
|
3048
3477
|
var server_default = createServerApp("both");
|