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