@premai/api-sdk 1.0.47 → 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/bare.cjs +20 -5
- package/dist/bare.mjs +20 -5
- package/dist/cli-claude.mjs +419 -3008
- package/dist/cli.mjs +2767 -2203
- 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 +455 -36
- package/dist/index.mjs +467 -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
|
}
|
|
@@ -2360,7 +2380,8 @@ function toAnthropicModel(model) {
|
|
|
2360
2380
|
type: "model",
|
|
2361
2381
|
id: model.model,
|
|
2362
2382
|
display_name: model.name || model.model,
|
|
2363
|
-
created_at: model.created_at
|
|
2383
|
+
created_at: model.created_at,
|
|
2384
|
+
description: model.description
|
|
2364
2385
|
};
|
|
2365
2386
|
}
|
|
2366
2387
|
function filterEnabled(models) {
|
|
@@ -2455,8 +2476,8 @@ function registerAnthropicModelsRoute(router, deps) {
|
|
|
2455
2476
|
|
|
2456
2477
|
// src/server/runtime.ts
|
|
2457
2478
|
import multer from "multer";
|
|
2458
|
-
var DEFAULT_HOST =
|
|
2459
|
-
var DEFAULT_PORT =
|
|
2479
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
2480
|
+
var DEFAULT_PORT = 8787;
|
|
2460
2481
|
var CLIENT_CACHE_MAX = (() => {
|
|
2461
2482
|
let cacheTTL = 256;
|
|
2462
2483
|
const raw = process.env.CLIENT_CACHE_MAX;
|
|
@@ -2486,9 +2507,7 @@ function applyServerOptions(options) {
|
|
|
2486
2507
|
if (enclaveUrl) {
|
|
2487
2508
|
serverEnclaveUrl = enclaveUrl;
|
|
2488
2509
|
}
|
|
2489
|
-
|
|
2490
|
-
serverKek = kek;
|
|
2491
|
-
}
|
|
2510
|
+
serverKek = kek || generateNewClientKEK();
|
|
2492
2511
|
}
|
|
2493
2512
|
async function getOrCreateRvencClient(apiKey) {
|
|
2494
2513
|
const existing = clientCache.get(apiKey);
|
|
@@ -2707,6 +2726,40 @@ function registerOpenAICompatRoutes(router, deps) {
|
|
|
2707
2726
|
});
|
|
2708
2727
|
}
|
|
2709
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
|
+
|
|
2710
2763
|
// src/server/route-prefix.ts
|
|
2711
2764
|
function normalizeRoutePrefix(raw) {
|
|
2712
2765
|
if (raw == null) {
|
|
@@ -2741,6 +2794,9 @@ function prefixedRoute(prefix, path) {
|
|
|
2741
2794
|
}
|
|
2742
2795
|
|
|
2743
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;
|
|
2744
2800
|
function registerApiDiscoveryRoute(app, mount) {
|
|
2745
2801
|
const {
|
|
2746
2802
|
openai: mountOpenAI,
|
|
@@ -2770,8 +2826,8 @@ function registerApiDiscoveryRoute(app, mount) {
|
|
|
2770
2826
|
labels.push("Anthropic Messages-compatible");
|
|
2771
2827
|
}
|
|
2772
2828
|
res.json({
|
|
2773
|
-
message:
|
|
2774
|
-
version:
|
|
2829
|
+
message: `${SERVER_MESSAGE} (${labels.join(" + ")})`,
|
|
2830
|
+
version: SERVER_VERSION,
|
|
2775
2831
|
compat: resolveCompatLabel(mount),
|
|
2776
2832
|
route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
|
|
2777
2833
|
endpoints: endpoints2
|
|
@@ -2801,7 +2857,66 @@ function resolveCompatLabel(mount) {
|
|
|
2801
2857
|
return "openai";
|
|
2802
2858
|
}
|
|
2803
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
|
+
|
|
2804
2915
|
// src/server/create-app.ts
|
|
2916
|
+
var draining = false;
|
|
2917
|
+
function setDraining(value) {
|
|
2918
|
+
draining = value;
|
|
2919
|
+
}
|
|
2805
2920
|
var rvencDeps = {
|
|
2806
2921
|
getOrCreateClient: getOrCreateRvencClient
|
|
2807
2922
|
};
|
|
@@ -2823,7 +2938,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2823
2938
|
compat: compat2,
|
|
2824
2939
|
openaiPrefix: openaiPrefix2,
|
|
2825
2940
|
anthropicPrefix: anthropicPrefix2,
|
|
2826
|
-
jsonBodyLimit: resolveJsonBodyLimit()
|
|
2941
|
+
jsonBodyLimit: resolveJsonBodyLimit(),
|
|
2942
|
+
shutdown: undefined
|
|
2827
2943
|
};
|
|
2828
2944
|
}
|
|
2829
2945
|
const compat = compatOrOptions.compat ?? "openai";
|
|
@@ -2832,7 +2948,8 @@ function resolveCreateServerInput(compatOrOptions) {
|
|
|
2832
2948
|
compat,
|
|
2833
2949
|
openaiPrefix,
|
|
2834
2950
|
anthropicPrefix,
|
|
2835
|
-
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
|
|
2951
|
+
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit),
|
|
2952
|
+
shutdown: compatOrOptions.shutdown
|
|
2836
2953
|
};
|
|
2837
2954
|
}
|
|
2838
2955
|
function httpErrorStatus(err) {
|
|
@@ -2849,11 +2966,51 @@ function mountRouter(app, prefix, router) {
|
|
|
2849
2966
|
app.use(prefix || "/", router);
|
|
2850
2967
|
}
|
|
2851
2968
|
function createServerApp(compatOrOptions = "openai") {
|
|
2852
|
-
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
|
|
2969
|
+
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit, shutdown } = resolveCreateServerInput(compatOrOptions);
|
|
2853
2970
|
const mountOpenAI = compat === "openai" || compat === "both";
|
|
2854
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
|
+
};
|
|
2855
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
|
+
});
|
|
2856
3012
|
app.use(express.json({ limit: jsonBodyLimit }));
|
|
3013
|
+
app.use(requestDebugMiddleware);
|
|
2857
3014
|
registerApiDiscoveryRoute(app, {
|
|
2858
3015
|
openai: mountOpenAI,
|
|
2859
3016
|
anthropic: mountAnthropic,
|
|
@@ -2872,18 +3029,18 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2872
3029
|
registerAnthropicModelsRoute(router, rvencDeps);
|
|
2873
3030
|
mountRouter(app, anthropicPrefix, router);
|
|
2874
3031
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
}
|
|
2879
|
-
if (!mountOpenAI) {
|
|
2880
|
-
return true;
|
|
2881
|
-
}
|
|
2882
|
-
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
2883
|
-
};
|
|
3032
|
+
if (shutdown) {
|
|
3033
|
+
registerShutdownRoute(app, shutdown);
|
|
3034
|
+
}
|
|
2884
3035
|
app.use((err, req, res, _next) => {
|
|
2885
3036
|
const status = httpErrorStatus(err);
|
|
2886
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
|
+
});
|
|
2887
3044
|
if (isAnthropicRequest(req)) {
|
|
2888
3045
|
const requestId = newAnthropicRequestId();
|
|
2889
3046
|
res.setHeader("request-id", requestId);
|
|
@@ -2906,6 +3063,7 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2906
3063
|
});
|
|
2907
3064
|
app.use((req, res) => {
|
|
2908
3065
|
const message = `Route ${req.method} ${req.path} not found`;
|
|
3066
|
+
logger.debug("route-not-found", { method: req.method, path: req.path });
|
|
2909
3067
|
if (isAnthropicRequest(req)) {
|
|
2910
3068
|
const requestId = newAnthropicRequestId();
|
|
2911
3069
|
res.setHeader("request-id", requestId);
|
|
@@ -2925,7 +3083,149 @@ function createServerApp(compatOrOptions = "openai") {
|
|
|
2925
3083
|
});
|
|
2926
3084
|
return app;
|
|
2927
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
|
+
|
|
2928
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
|
+
}
|
|
2929
3229
|
async function startServer(options = {}) {
|
|
2930
3230
|
const {
|
|
2931
3231
|
host,
|
|
@@ -2933,31 +3233,161 @@ async function startServer(options = {}) {
|
|
|
2933
3233
|
compat: compatOpt,
|
|
2934
3234
|
openaiRoutePrefix,
|
|
2935
3235
|
anthropicRoutePrefix,
|
|
2936
|
-
jsonBodyLimit
|
|
3236
|
+
jsonBodyLimit,
|
|
3237
|
+
daemon,
|
|
3238
|
+
stateFile,
|
|
3239
|
+
logFile,
|
|
3240
|
+
shutdownTimeoutMs
|
|
2937
3241
|
} = options;
|
|
2938
3242
|
const serverHost = host || DEFAULT_HOST;
|
|
2939
3243
|
const serverPort = port || DEFAULT_PORT;
|
|
2940
3244
|
const compat = compatOpt ?? "openai";
|
|
2941
3245
|
applyServerOptions(options);
|
|
2942
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;
|
|
2943
3286
|
const app = createServerApp({
|
|
2944
3287
|
compat,
|
|
2945
3288
|
openaiRoutePrefix,
|
|
2946
3289
|
anthropicRoutePrefix,
|
|
2947
|
-
jsonBodyLimit
|
|
3290
|
+
jsonBodyLimit,
|
|
3291
|
+
shutdown: shutdownConfig
|
|
2948
3292
|
});
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
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
|
|
2952
3308
|
});
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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");
|
|
2959
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
|
+
}
|
|
2960
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);
|
|
2961
3391
|
}
|
|
2962
3392
|
// src/server.ts
|
|
2963
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
|
};
|