@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.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
|
}
|
|
@@ -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 =
|
|
2455
|
-
var DEFAULT_PORT =
|
|
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
|
-
|
|
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:
|
|
2770
|
-
version:
|
|
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
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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
|
|
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
|
};
|