@premai/api-sdk 1.0.40 → 1.0.42
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 +165 -39
- package/dist/anthropic/count-tokens-route.d.ts +3 -0
- package/dist/anthropic/from-openai.d.ts +33 -0
- package/dist/anthropic/http.d.ts +19 -0
- package/dist/anthropic/messages-route.d.ts +3 -0
- package/dist/anthropic/models-route.d.ts +3 -0
- package/dist/anthropic/to-openai.d.ts +35 -0
- package/dist/audio/index.d.ts +1 -0
- package/dist/cli-claude.d.ts +2 -0
- package/dist/cli-claude.mjs +3304 -0
- package/dist/cli.mjs +2959 -0
- package/dist/core.browser.cjs +161 -21
- package/dist/core.browser.mjs +161 -21
- package/dist/index.cjs +1508 -238
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +1508 -240
- package/dist/launcher/claude-code.d.ts +1 -0
- package/dist/launcher/model-picker.d.ts +7 -0
- package/dist/launcher/proxy-subprocess.d.ts +13 -0
- package/dist/launcher/text-input.d.ts +3 -0
- package/dist/openai/routes.d.ts +3 -0
- package/dist/server/create-app.d.ts +8 -0
- package/dist/server/discovery.d.ts +7 -0
- package/dist/server/route-prefix.d.ts +9 -0
- package/dist/server/runtime.d.ts +8 -0
- package/dist/server/start.d.ts +4 -0
- package/dist/server.d.ts +7 -5
- package/dist/types.d.ts +9 -3
- package/package.json +8 -3
- package/dist/cli.cjs +0 -1698
package/dist/cli.cjs
DELETED
|
@@ -1,1698 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
4
|
-
var __defProp = Object.defineProperty;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
function __accessProp(key) {
|
|
8
|
-
return this[key];
|
|
9
|
-
}
|
|
10
|
-
var __toESMCache_node;
|
|
11
|
-
var __toESMCache_esm;
|
|
12
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
-
var canCache = mod != null && typeof mod === "object";
|
|
14
|
-
if (canCache) {
|
|
15
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
-
var cached = cache.get(mod);
|
|
17
|
-
if (cached)
|
|
18
|
-
return cached;
|
|
19
|
-
}
|
|
20
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
-
for (let key of __getOwnPropNames(mod))
|
|
23
|
-
if (!__hasOwnProp.call(to, key))
|
|
24
|
-
__defProp(to, key, {
|
|
25
|
-
get: __accessProp.bind(mod, key),
|
|
26
|
-
enumerable: true
|
|
27
|
-
});
|
|
28
|
-
if (canCache)
|
|
29
|
-
cache.set(mod, to);
|
|
30
|
-
return to;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// src/cli.ts
|
|
34
|
-
var import_node_util = require("node:util");
|
|
35
|
-
|
|
36
|
-
// src/server.ts
|
|
37
|
-
var import_dotenv = __toESM(require("dotenv"));
|
|
38
|
-
var import_express = __toESM(require("express"));
|
|
39
|
-
var import_multer = __toESM(require("multer"));
|
|
40
|
-
|
|
41
|
-
// src/audio/index.ts
|
|
42
|
-
var import_utils2 = require("@noble/ciphers/utils.js");
|
|
43
|
-
|
|
44
|
-
// src/config.ts
|
|
45
|
-
var endpoints = {
|
|
46
|
-
enclave: process.env.ENCLAVE_URL,
|
|
47
|
-
proxy: process.env.PROXY_URL
|
|
48
|
-
};
|
|
49
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 600000;
|
|
50
|
-
var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
51
|
-
|
|
52
|
-
// src/utils/attestation.ts
|
|
53
|
-
var cachedPrem;
|
|
54
|
-
async function loadPrem() {
|
|
55
|
-
if (cachedPrem)
|
|
56
|
-
return cachedPrem;
|
|
57
|
-
const isBare = typeof globalThis.Bare !== "undefined";
|
|
58
|
-
if (isBare) {
|
|
59
|
-
cachedPrem = await (async (s, y) => await import(s, y))("@premai/reticle", { with: { type: "script" } });
|
|
60
|
-
return cachedPrem;
|
|
61
|
-
}
|
|
62
|
-
cachedPrem = await import("@premai/reticle");
|
|
63
|
-
return cachedPrem;
|
|
64
|
-
}
|
|
65
|
-
async function attest(apiKey, options = { enabled: true }) {
|
|
66
|
-
if (!options.enabled)
|
|
67
|
-
return null;
|
|
68
|
-
const prem = await loadPrem();
|
|
69
|
-
const client = await new prem.ClientBuilder(endpoints.proxy ?? "").with_authorization(apiKey).build();
|
|
70
|
-
let query = new prem.QueryParams;
|
|
71
|
-
if (options.model)
|
|
72
|
-
query = query.with("model", options.model);
|
|
73
|
-
try {
|
|
74
|
-
client.set_query(query);
|
|
75
|
-
const attested = await client.attest();
|
|
76
|
-
const headers = attested.headers();
|
|
77
|
-
const sessionId = attested.headers().gpu()?.get("x-session-id") ?? null;
|
|
78
|
-
headers.free();
|
|
79
|
-
attested.free();
|
|
80
|
-
return sessionId;
|
|
81
|
-
} finally {
|
|
82
|
-
client.free();
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// src/utils/crypto.ts
|
|
87
|
-
var import_aes = require("@noble/ciphers/aes.js");
|
|
88
|
-
var import_chacha = require("@noble/ciphers/chacha.js");
|
|
89
|
-
var import_utils = require("@noble/ciphers/utils.js");
|
|
90
|
-
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
91
|
-
var import_sha3 = require("@noble/hashes/sha3.js");
|
|
92
|
-
var import_hybrid = require("@noble/post-quantum/hybrid.js");
|
|
93
|
-
function createMLKEMEncapsulation(publicKeyHex) {
|
|
94
|
-
return import_hybrid.XWing.encapsulate(import_utils.hexToBytes(publicKeyHex));
|
|
95
|
-
}
|
|
96
|
-
function encryptPayload(sharedSecret, data) {
|
|
97
|
-
const nonce = import_utils.randomBytes(24);
|
|
98
|
-
const chacha = import_chacha.xchacha20poly1305(sharedSecret, nonce);
|
|
99
|
-
let encodedData;
|
|
100
|
-
if (data instanceof Uint8Array) {
|
|
101
|
-
encodedData = data;
|
|
102
|
-
} else if (typeof data === "string") {
|
|
103
|
-
encodedData = new TextEncoder().encode(data);
|
|
104
|
-
} else {
|
|
105
|
-
encodedData = new TextEncoder().encode(JSON.stringify(data));
|
|
106
|
-
}
|
|
107
|
-
const encrypted = chacha.encrypt(encodedData);
|
|
108
|
-
return { encrypted, nonce };
|
|
109
|
-
}
|
|
110
|
-
function decryptPayload(encryptedData, sharedSecret, nonce) {
|
|
111
|
-
const chacha = import_chacha.xchacha20poly1305(sharedSecret, nonce);
|
|
112
|
-
const encrypted = import_utils.hexToBytes(encryptedData);
|
|
113
|
-
const decrypted = chacha.decrypt(encrypted);
|
|
114
|
-
const str = new TextDecoder().decode(decrypted);
|
|
115
|
-
try {
|
|
116
|
-
return JSON.parse(str);
|
|
117
|
-
} catch {
|
|
118
|
-
return str;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
async function getEnclavePublicKey(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
122
|
-
const controller = new AbortController;
|
|
123
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
124
|
-
try {
|
|
125
|
-
const response = await fetch(`${endpoints.enclave}/publicKey`, {
|
|
126
|
-
signal: controller.signal
|
|
127
|
-
});
|
|
128
|
-
if (!response.ok) {
|
|
129
|
-
throw new Error(`Failed to fetch enclave public key: ${response.status} ${response.statusText}`);
|
|
130
|
-
}
|
|
131
|
-
const data = await response.json();
|
|
132
|
-
if (!data.publicKey || typeof data.publicKey !== "string") {
|
|
133
|
-
throw new Error("Invalid public key response from enclave");
|
|
134
|
-
}
|
|
135
|
-
return data.publicKey;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
138
|
-
throw new Error(`Enclave public key request timed out after ${timeoutMs}ms`);
|
|
139
|
-
}
|
|
140
|
-
throw new Error(`Failed to get enclave public key: ${error instanceof Error ? error.message : error}`);
|
|
141
|
-
} finally {
|
|
142
|
-
clearTimeout(timeoutId);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function generateEncryptionKeys(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
146
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
147
|
-
return createMLKEMEncapsulation(enclavePublicKey);
|
|
148
|
-
}
|
|
149
|
-
function keyIdFromKEK(kek, context = "kek:v1", length = 16) {
|
|
150
|
-
const ctx = new TextEncoder().encode(context);
|
|
151
|
-
const input = new Uint8Array(kek.length + ctx.length);
|
|
152
|
-
input.set(kek, 0);
|
|
153
|
-
input.set(ctx, kek.length);
|
|
154
|
-
const digest = import_sha2.sha256(input);
|
|
155
|
-
return digest.slice(0, length);
|
|
156
|
-
}
|
|
157
|
-
function encryptWithDEK(dek, plaintext) {
|
|
158
|
-
const aead = import_utils.managedNonce(import_chacha.xchacha20poly1305)(dek);
|
|
159
|
-
return aead.encrypt(plaintext);
|
|
160
|
-
}
|
|
161
|
-
function encryptMetadataWithDEK(dek, metadata) {
|
|
162
|
-
const encoded = new TextEncoder().encode(metadata);
|
|
163
|
-
const encrypted = encryptWithDEK(dek, encoded);
|
|
164
|
-
return import_utils.bytesToHex(encrypted);
|
|
165
|
-
}
|
|
166
|
-
function wrapDEK(kek, dek) {
|
|
167
|
-
const kw = import_aes.aeskwp(kek);
|
|
168
|
-
return kw.encrypt(dek);
|
|
169
|
-
}
|
|
170
|
-
function unwrapDEK(kek, wrappedDEK) {
|
|
171
|
-
const kw = import_aes.aeskwp(kek);
|
|
172
|
-
return kw.decrypt(wrappedDEK);
|
|
173
|
-
}
|
|
174
|
-
function decryptWithDEK(dek, encryptedContent) {
|
|
175
|
-
const aead = import_utils.managedNonce(import_chacha.xchacha20poly1305)(dek);
|
|
176
|
-
return aead.decrypt(encryptedContent);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/utils/error.ts
|
|
180
|
-
async function throwIfErrorResponse(response) {
|
|
181
|
-
let raw;
|
|
182
|
-
try {
|
|
183
|
-
raw = await response.json();
|
|
184
|
-
if (!raw.status)
|
|
185
|
-
raw = { ...raw, status: response.status };
|
|
186
|
-
} catch {
|
|
187
|
-
raw = {
|
|
188
|
-
status: response.status,
|
|
189
|
-
data: null,
|
|
190
|
-
error: response.statusText || `HTTP ${response.status}`,
|
|
191
|
-
message: null
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
throw raw;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// src/utils/files.ts
|
|
198
|
-
var getFileName = (file) => {
|
|
199
|
-
if (file instanceof File) {
|
|
200
|
-
return file.name;
|
|
201
|
-
}
|
|
202
|
-
if (file instanceof Blob) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const fileAny = file;
|
|
206
|
-
if (fileAny.path) {
|
|
207
|
-
const path = typeof fileAny.path === "string" ? fileAny.path : fileAny.path.toString();
|
|
208
|
-
return path.split("/").pop() || path.split("\\").pop() || path;
|
|
209
|
-
}
|
|
210
|
-
if (file instanceof Uint8Array || file instanceof ArrayBuffer) {
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
return;
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
// src/audio/index.ts
|
|
217
|
-
async function readUploadableToUint8Array(file) {
|
|
218
|
-
if (file instanceof Uint8Array) {
|
|
219
|
-
return file;
|
|
220
|
-
}
|
|
221
|
-
if (file instanceof ArrayBuffer) {
|
|
222
|
-
return new Uint8Array(file);
|
|
223
|
-
}
|
|
224
|
-
if (typeof file.arrayBuffer === "function") {
|
|
225
|
-
const blob = file;
|
|
226
|
-
const buffer = await blob.arrayBuffer();
|
|
227
|
-
return new Uint8Array(buffer);
|
|
228
|
-
}
|
|
229
|
-
const fileAny = file;
|
|
230
|
-
if (typeof fileAny.on === "function" && (typeof fileAny.read === "function" || typeof fileAny.pipe === "function")) {
|
|
231
|
-
const chunks = [];
|
|
232
|
-
return new Promise((resolve, reject) => {
|
|
233
|
-
fileAny.on("data", (chunk) => {
|
|
234
|
-
if (Buffer.isBuffer(chunk)) {
|
|
235
|
-
chunks.push(new Uint8Array(chunk));
|
|
236
|
-
} else if (chunk instanceof Uint8Array) {
|
|
237
|
-
chunks.push(chunk);
|
|
238
|
-
} else if (typeof chunk === "object" && chunk !== null) {
|
|
239
|
-
chunks.push(new Uint8Array(Buffer.from(chunk)));
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
fileAny.on("end", () => {
|
|
243
|
-
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
244
|
-
const result = new Uint8Array(totalLength);
|
|
245
|
-
let offset = 0;
|
|
246
|
-
for (const chunk of chunks) {
|
|
247
|
-
result.set(chunk, offset);
|
|
248
|
-
offset += chunk.length;
|
|
249
|
-
}
|
|
250
|
-
resolve(result);
|
|
251
|
-
});
|
|
252
|
-
fileAny.on("error", (err) => reject(err));
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
throw new Error("Unsupported file type for audio transcription");
|
|
256
|
-
}
|
|
257
|
-
async function preprocessAudioRequest(body, encryptionKeys) {
|
|
258
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
259
|
-
const audioData = await readUploadableToUint8Array(body.file);
|
|
260
|
-
const isDeepgram = body.model.startsWith("deepgram/");
|
|
261
|
-
const requestBody = isDeepgram ? {
|
|
262
|
-
model: body.model,
|
|
263
|
-
diarize: body.diarize
|
|
264
|
-
} : {
|
|
265
|
-
model: body.model,
|
|
266
|
-
language: body.language,
|
|
267
|
-
prompt: body.prompt,
|
|
268
|
-
response_format: body.response_format,
|
|
269
|
-
temperature: body.temperature,
|
|
270
|
-
timestamp_granularities: body.timestamp_granularities
|
|
271
|
-
};
|
|
272
|
-
const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
|
|
273
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
|
|
274
|
-
const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
|
|
275
|
-
const fileName = getFileName(body.file) || "audio.mp3";
|
|
276
|
-
const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
|
|
277
|
-
return {
|
|
278
|
-
body: {
|
|
279
|
-
cipherText: import_utils2.bytesToHex(cipherText),
|
|
280
|
-
encryptedInference: import_utils2.bytesToHex(encrypted),
|
|
281
|
-
nonce: import_utils2.bytesToHex(nonce),
|
|
282
|
-
fileNameNonce: import_utils2.bytesToHex(fileNameNonce),
|
|
283
|
-
encryptedFileName: import_utils2.bytesToHex(encryptedFileName),
|
|
284
|
-
fileNonce: import_utils2.bytesToHex(fileNonce),
|
|
285
|
-
encryptedFile: import_utils2.bytesToHex(encryptedFile),
|
|
286
|
-
model: body.model
|
|
287
|
-
},
|
|
288
|
-
sharedSecret
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
async function postprocessTranscriptionResponse(response, sharedSecret) {
|
|
292
|
-
const responseData = await response.json();
|
|
293
|
-
const data = responseData.data;
|
|
294
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
295
|
-
throw new Error("Invalid transcription response: missing encryptedResponse or nonce");
|
|
296
|
-
}
|
|
297
|
-
const responseNonce = import_utils2.hexToBytes(data.nonce);
|
|
298
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
299
|
-
}
|
|
300
|
-
async function postprocessTranslationResponse(response, sharedSecret) {
|
|
301
|
-
const responseData = await response.json();
|
|
302
|
-
const data = responseData.data;
|
|
303
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
304
|
-
throw new Error("Invalid translation response: missing encryptedResponse or nonce");
|
|
305
|
-
}
|
|
306
|
-
const responseNonce = import_utils2.hexToBytes(data.nonce);
|
|
307
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
308
|
-
}
|
|
309
|
-
async function preprocessAudioTranslationRequest(body, encryptionKeys) {
|
|
310
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
311
|
-
const audioData = await readUploadableToUint8Array(body.file);
|
|
312
|
-
const requestBody = {
|
|
313
|
-
model: body.model,
|
|
314
|
-
prompt: body.prompt,
|
|
315
|
-
response_format: body.response_format,
|
|
316
|
-
temperature: body.temperature
|
|
317
|
-
};
|
|
318
|
-
const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
|
|
319
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
|
|
320
|
-
const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
|
|
321
|
-
const fileName = getFileName(body.file) || "audio.mp3";
|
|
322
|
-
const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
|
|
323
|
-
return {
|
|
324
|
-
body: {
|
|
325
|
-
cipherText: import_utils2.bytesToHex(cipherText),
|
|
326
|
-
encryptedInference: import_utils2.bytesToHex(encrypted),
|
|
327
|
-
nonce: import_utils2.bytesToHex(nonce),
|
|
328
|
-
fileNameNonce: import_utils2.bytesToHex(fileNameNonce),
|
|
329
|
-
encryptedFileName: import_utils2.bytesToHex(encryptedFileName),
|
|
330
|
-
fileNonce: import_utils2.bytesToHex(fileNonce),
|
|
331
|
-
encryptedFile: import_utils2.bytesToHex(encryptedFile),
|
|
332
|
-
model: body.model
|
|
333
|
-
},
|
|
334
|
-
sharedSecret
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
338
|
-
async function createTranscription(body) {
|
|
339
|
-
const controller = new AbortController;
|
|
340
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
341
|
-
try {
|
|
342
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
343
|
-
const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
|
|
344
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
|
|
345
|
-
method: "POST",
|
|
346
|
-
headers: {
|
|
347
|
-
"Content-Type": "application/json",
|
|
348
|
-
Authorization: apiKey,
|
|
349
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
350
|
-
},
|
|
351
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
352
|
-
signal: controller.signal
|
|
353
|
-
});
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
await throwIfErrorResponse(response);
|
|
356
|
-
}
|
|
357
|
-
clearTimeout(timeoutId);
|
|
358
|
-
return await postprocessTranscriptionResponse(response, encryptedRequest.sharedSecret);
|
|
359
|
-
} catch (error) {
|
|
360
|
-
clearTimeout(timeoutId);
|
|
361
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
362
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
363
|
-
}
|
|
364
|
-
throw error;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const transcriptionsClient = {
|
|
368
|
-
create: createTranscription
|
|
369
|
-
};
|
|
370
|
-
async function createTranslation(body) {
|
|
371
|
-
const controller = new AbortController;
|
|
372
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
373
|
-
try {
|
|
374
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
375
|
-
const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
|
|
376
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
|
|
377
|
-
method: "POST",
|
|
378
|
-
headers: {
|
|
379
|
-
"Content-Type": "application/json",
|
|
380
|
-
Authorization: apiKey,
|
|
381
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
382
|
-
},
|
|
383
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
384
|
-
signal: controller.signal
|
|
385
|
-
});
|
|
386
|
-
if (!response.ok) {
|
|
387
|
-
await throwIfErrorResponse(response);
|
|
388
|
-
}
|
|
389
|
-
clearTimeout(timeoutId);
|
|
390
|
-
return await postprocessTranslationResponse(response, encryptedRequest.sharedSecret);
|
|
391
|
-
} catch (error) {
|
|
392
|
-
clearTimeout(timeoutId);
|
|
393
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
394
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
395
|
-
}
|
|
396
|
-
throw error;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
const translationsClient = {
|
|
400
|
-
create: createTranslation
|
|
401
|
-
};
|
|
402
|
-
return {
|
|
403
|
-
transcriptions: transcriptionsClient,
|
|
404
|
-
translations: translationsClient
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// src/files/index.ts
|
|
409
|
-
var import_utils4 = require("@noble/ciphers/utils.js");
|
|
410
|
-
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
411
|
-
var import_date_fns = require("date-fns");
|
|
412
|
-
var import_zod = require("zod");
|
|
413
|
-
|
|
414
|
-
// src/utils/dek-store.ts
|
|
415
|
-
var import_utils3 = require("@noble/ciphers/utils.js");
|
|
416
|
-
function initializeDEKStore(clientKEK) {
|
|
417
|
-
const ragDEK = import_utils3.randomBytes(32);
|
|
418
|
-
const _clientKEK = clientKEK ? import_utils3.hexToBytes(clientKEK) : getClientKEK();
|
|
419
|
-
const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
|
|
420
|
-
return {
|
|
421
|
-
fileDEKs: new Map,
|
|
422
|
-
ragDEK: wrappedRagDEK,
|
|
423
|
-
ragVersion: "2"
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function getClientKEK() {
|
|
427
|
-
if (!process.env.CLIENT_KEK) {
|
|
428
|
-
throw new Error("CLIENT_KEK environment variable is not set.");
|
|
429
|
-
}
|
|
430
|
-
return import_utils3.hexToBytes(process.env.CLIENT_KEK);
|
|
431
|
-
}
|
|
432
|
-
function getClientKID(clientKEK) {
|
|
433
|
-
if (clientKEK) {
|
|
434
|
-
return import_utils3.bytesToHex(keyIdFromKEK(import_utils3.hexToBytes(clientKEK)));
|
|
435
|
-
}
|
|
436
|
-
const _clientKEK = getClientKEK();
|
|
437
|
-
return import_utils3.bytesToHex(keyIdFromKEK(_clientKEK));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/files/index.ts
|
|
441
|
-
var MAX_FILENAME_LENGTH = 255;
|
|
442
|
-
var MIN_FILENAME_LENGTH = 1;
|
|
443
|
-
var ALLOWED_MIME_TYPES = new Set([
|
|
444
|
-
"image/jpeg",
|
|
445
|
-
"image/png",
|
|
446
|
-
"image/gif",
|
|
447
|
-
"image/webp",
|
|
448
|
-
"application/pdf",
|
|
449
|
-
"application/msword",
|
|
450
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
451
|
-
"application/vnd.ms-excel",
|
|
452
|
-
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
453
|
-
"text/plain",
|
|
454
|
-
"text/csv",
|
|
455
|
-
"text/markdown",
|
|
456
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
457
|
-
"video/mp4",
|
|
458
|
-
"video/webm",
|
|
459
|
-
"video/quicktime",
|
|
460
|
-
"audio/mpeg",
|
|
461
|
-
"audio/wav",
|
|
462
|
-
"audio/ogg",
|
|
463
|
-
"application/zip",
|
|
464
|
-
"application/x-rar-compressed",
|
|
465
|
-
"application/x-7z-compressed",
|
|
466
|
-
"application/octet-stream"
|
|
467
|
-
]);
|
|
468
|
-
var ApiKeySchema = import_zod.z.string().trim().min(1, "API key cannot be empty");
|
|
469
|
-
var TimeoutSchema = import_zod.z.number().int().min(1, "Timeout must be at least 1ms").max(600000, "Timeout must not exceed 600000ms (10 minutes)");
|
|
470
|
-
var DEKStoreSchema = import_zod.z.object({
|
|
471
|
-
ragDEK: import_zod.z.instanceof(Uint8Array).optional(),
|
|
472
|
-
fileDEKs: import_zod.z.instanceof(Map).optional()
|
|
473
|
-
});
|
|
474
|
-
var ISO8601DateSchema = import_zod.z.string().refine((val) => {
|
|
475
|
-
const date = import_date_fns.parseISO(val);
|
|
476
|
-
return import_date_fns.isValid(date);
|
|
477
|
-
}, { message: "Must be a valid ISO8601 date" });
|
|
478
|
-
var MimeTypeSchema = import_zod.z.string().refine((val) => ALLOWED_MIME_TYPES.has(val), {
|
|
479
|
-
message: "MIME type is not allowed"
|
|
480
|
-
});
|
|
481
|
-
var FileUploadOptionsSchema = import_zod.z.object({
|
|
482
|
-
file: import_zod.z.instanceof(Uint8Array),
|
|
483
|
-
fileName: import_zod.z.string().min(MIN_FILENAME_LENGTH, "File name cannot be empty").max(MAX_FILENAME_LENGTH, `File name cannot exceed ${MAX_FILENAME_LENGTH} characters`).refine((name) => !/[<>:"|?*\x00-\x1F]/.test(name), "Invalid characters").refine((name) => !name.includes("..") && !name.includes("/") && !name.includes("\\"), "No path separators allowed"),
|
|
484
|
-
mimeType: import_zod.z.string().optional(),
|
|
485
|
-
ragIndex: import_zod.z.boolean().optional()
|
|
486
|
-
});
|
|
487
|
-
var ListFilesOptionsSchema = import_zod.z.object({
|
|
488
|
-
limit: import_zod.z.number().int().positive("Limit must be a positive integer").optional(),
|
|
489
|
-
offset: import_zod.z.number().int().nonnegative("Offset must be a non-negative integer").optional(),
|
|
490
|
-
search: import_zod.z.string().optional(),
|
|
491
|
-
from: ISO8601DateSchema.optional(),
|
|
492
|
-
to: ISO8601DateSchema.optional()
|
|
493
|
-
}).optional();
|
|
494
|
-
var GetFileOptionsSchema = import_zod.z.object({
|
|
495
|
-
id: import_zod.z.string().trim().min(1, "File ID cannot be empty"),
|
|
496
|
-
url: import_zod.z.boolean().optional()
|
|
497
|
-
});
|
|
498
|
-
var DeleteFileOptionsSchema = import_zod.z.object({
|
|
499
|
-
id: import_zod.z.string().min(1, "File ID is required")
|
|
500
|
-
});
|
|
501
|
-
var IndexFileInputSchema = import_zod.z.object({
|
|
502
|
-
fileId: import_zod.z.string().min(1, "File ID is required"),
|
|
503
|
-
filePath: import_zod.z.string().min(1, "File path is required"),
|
|
504
|
-
fileDEK: import_zod.z.instanceof(Uint8Array).optional()
|
|
505
|
-
});
|
|
506
|
-
var IndexFilesOptionsSchema = import_zod.z.object({
|
|
507
|
-
files: import_zod.z.array(IndexFileInputSchema).min(1, "Files array must not be empty"),
|
|
508
|
-
ragDEK: import_zod.z.instanceof(Uint8Array).optional()
|
|
509
|
-
});
|
|
510
|
-
var DeleteIndexOptionsSchema = import_zod.z.object({
|
|
511
|
-
fileIds: import_zod.z.array(import_zod.z.string().min(1)).min(1, "File IDs array must not be empty"),
|
|
512
|
-
ragDEK: import_zod.z.instanceof(Uint8Array).optional()
|
|
513
|
-
});
|
|
514
|
-
function validateAPIKey(apiKey) {
|
|
515
|
-
ApiKeySchema.parse(apiKey);
|
|
516
|
-
}
|
|
517
|
-
function validateDEKStore(dekStore) {
|
|
518
|
-
DEKStoreSchema.parse(dekStore);
|
|
519
|
-
}
|
|
520
|
-
function validateMimeType(mimeType) {
|
|
521
|
-
MimeTypeSchema.parse(mimeType);
|
|
522
|
-
}
|
|
523
|
-
function validateFileUploadOptions(options) {
|
|
524
|
-
FileUploadOptionsSchema.parse(options);
|
|
525
|
-
}
|
|
526
|
-
function validateListFilesOptions(options) {
|
|
527
|
-
ListFilesOptionsSchema.parse(options);
|
|
528
|
-
}
|
|
529
|
-
function validateGetFileOptions(options) {
|
|
530
|
-
GetFileOptionsSchema.parse(options);
|
|
531
|
-
}
|
|
532
|
-
function guessMimeType(fileName) {
|
|
533
|
-
const ext = fileName.toLowerCase().split(".").pop() || "";
|
|
534
|
-
const mimeTypeMap = {
|
|
535
|
-
jpg: "image/jpeg",
|
|
536
|
-
jpeg: "image/jpeg",
|
|
537
|
-
png: "image/png",
|
|
538
|
-
gif: "image/gif",
|
|
539
|
-
webp: "image/webp",
|
|
540
|
-
pdf: "application/pdf",
|
|
541
|
-
doc: "application/msword",
|
|
542
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
543
|
-
xls: "application/vnd.ms-excel",
|
|
544
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
545
|
-
txt: "text/plain",
|
|
546
|
-
csv: "text/csv",
|
|
547
|
-
md: "text/markdown",
|
|
548
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
549
|
-
mp4: "video/mp4",
|
|
550
|
-
webm: "video/webm",
|
|
551
|
-
mov: "video/quicktime",
|
|
552
|
-
mp3: "audio/mpeg",
|
|
553
|
-
wav: "audio/wav",
|
|
554
|
-
ogg: "audio/ogg",
|
|
555
|
-
zip: "application/zip",
|
|
556
|
-
rar: "application/x-rar-compressed",
|
|
557
|
-
"7z": "application/x-7z-compressed"
|
|
558
|
-
};
|
|
559
|
-
return mimeTypeMap[ext] || "application/octet-stream";
|
|
560
|
-
}
|
|
561
|
-
async function saveRagDEKToBackend(apiKey, wrappedRagDEK, timeoutMs) {
|
|
562
|
-
const controller = new AbortController;
|
|
563
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
564
|
-
try {
|
|
565
|
-
const response = await fetch(`${endpoints.proxy}/users/save_rag_dek`, {
|
|
566
|
-
method: "POST",
|
|
567
|
-
headers: {
|
|
568
|
-
Authorization: apiKey,
|
|
569
|
-
"Content-Type": "application/json"
|
|
570
|
-
},
|
|
571
|
-
body: JSON.stringify({
|
|
572
|
-
data: {
|
|
573
|
-
wrappedRagDEK,
|
|
574
|
-
confirmReplaceRagDEK: true
|
|
575
|
-
}
|
|
576
|
-
}),
|
|
577
|
-
signal: controller.signal
|
|
578
|
-
});
|
|
579
|
-
if (!response.ok) {
|
|
580
|
-
throw new Error(`Failed to save RAG DEK: HTTP ${response.status}`);
|
|
581
|
-
}
|
|
582
|
-
const result = await response.json();
|
|
583
|
-
if (result.error) {
|
|
584
|
-
throw new Error(result.error);
|
|
585
|
-
}
|
|
586
|
-
} catch (error) {
|
|
587
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
588
|
-
throw new Error(`Save RAG DEK request timed out after ${timeoutMs}ms`);
|
|
589
|
-
}
|
|
590
|
-
throw error;
|
|
591
|
-
} finally {
|
|
592
|
-
clearTimeout(timeoutId);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
async function prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
596
|
-
const fileBytes = options.file;
|
|
597
|
-
const mimeType = options.mimeType || guessMimeType(options.fileName);
|
|
598
|
-
validateMimeType(mimeType);
|
|
599
|
-
const dek = import_utils4.randomBytes(32);
|
|
600
|
-
const encryptedFile = encryptWithDEK(dek, fileBytes);
|
|
601
|
-
const encryptedName = encryptMetadataWithDEK(dek, options.fileName);
|
|
602
|
-
const encryptedMimeType = encryptMetadataWithDEK(dek, mimeType);
|
|
603
|
-
const _clientKEK = clientKEK ? import_utils4.hexToBytes(clientKEK) : getClientKEK();
|
|
604
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
605
|
-
const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
|
|
606
|
-
const filePayload = {
|
|
607
|
-
client_hash: import_utils4.bytesToHex(import_sha22.sha256(fileBytes)),
|
|
608
|
-
encrypted_content: import_utils4.bytesToHex(encryptedFile),
|
|
609
|
-
encrypted_name: encryptedName,
|
|
610
|
-
kid: clientKID,
|
|
611
|
-
mime_type: encryptedMimeType,
|
|
612
|
-
version: "2",
|
|
613
|
-
wrapped_dek: import_utils4.bytesToHex(wrappedDEK)
|
|
614
|
-
};
|
|
615
|
-
if (options.ragIndex) {
|
|
616
|
-
await addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs);
|
|
617
|
-
}
|
|
618
|
-
return { dek, filePayload };
|
|
619
|
-
}
|
|
620
|
-
async function addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
621
|
-
let ragDEK = dekStore.ragDEK;
|
|
622
|
-
const _clientKEK = clientKEK ? import_utils4.hexToBytes(clientKEK) : getClientKEK();
|
|
623
|
-
if (!ragDEK) {
|
|
624
|
-
ragDEK = import_utils4.randomBytes(32);
|
|
625
|
-
const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
|
|
626
|
-
dekStore.ragDEK = wrappedRagDEK;
|
|
627
|
-
try {
|
|
628
|
-
await saveRagDEKToBackend(apiKey, import_utils4.bytesToHex(wrappedRagDEK), timeoutMs);
|
|
629
|
-
} catch (error) {
|
|
630
|
-
console.error("Warning: Failed to save RAG DEK to backend:", error);
|
|
631
|
-
}
|
|
632
|
-
} else {
|
|
633
|
-
ragDEK = unwrapDEK(_clientKEK, ragDEK);
|
|
634
|
-
}
|
|
635
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
636
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
637
|
-
const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, dek);
|
|
638
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
639
|
-
filePayload.encrypted_file_dek = import_utils4.bytesToHex(encryptedFileDEK);
|
|
640
|
-
filePayload.encrypted_rag_dek = import_utils4.bytesToHex(encryptedRagDEK);
|
|
641
|
-
filePayload.file_nonce = import_utils4.bytesToHex(fileNonce);
|
|
642
|
-
filePayload.rag_dek_nonce = import_utils4.bytesToHex(ragDEKNonce);
|
|
643
|
-
filePayload.cipher_text = import_utils4.bytesToHex(cipherText);
|
|
644
|
-
}
|
|
645
|
-
async function performUpload(apiKey, filePayload, controller) {
|
|
646
|
-
const uploadResponse = await fetch(`${endpoints.proxy}/files/encrypted/upload`, {
|
|
647
|
-
method: "POST",
|
|
648
|
-
headers: {
|
|
649
|
-
Authorization: apiKey,
|
|
650
|
-
"Content-Type": "application/json"
|
|
651
|
-
},
|
|
652
|
-
body: JSON.stringify(filePayload),
|
|
653
|
-
signal: controller.signal
|
|
654
|
-
});
|
|
655
|
-
if (!uploadResponse.ok) {
|
|
656
|
-
let errorMessage = `Upload request failed with status ${uploadResponse.status}`;
|
|
657
|
-
try {
|
|
658
|
-
const body = await uploadResponse.json();
|
|
659
|
-
if (body.error) {
|
|
660
|
-
errorMessage = body.error;
|
|
661
|
-
}
|
|
662
|
-
} catch {}
|
|
663
|
-
throw new Error(errorMessage);
|
|
664
|
-
}
|
|
665
|
-
const uploadResult = await uploadResponse.json();
|
|
666
|
-
if (uploadResult.status !== 200) {
|
|
667
|
-
throw new Error(uploadResult.error || "Upload failed");
|
|
668
|
-
}
|
|
669
|
-
if (!uploadResult.data) {
|
|
670
|
-
throw new Error("Upload response missing data");
|
|
671
|
-
}
|
|
672
|
-
return uploadResult.data;
|
|
673
|
-
}
|
|
674
|
-
function storeDEKForFile(dekStore, fileId, dek, clientKEK) {
|
|
675
|
-
if (!dekStore.fileDEKs) {
|
|
676
|
-
dekStore.fileDEKs = new Map;
|
|
677
|
-
}
|
|
678
|
-
const _clientKEK = clientKEK ? import_utils4.hexToBytes(clientKEK) : getClientKEK();
|
|
679
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
680
|
-
dekStore.fileDEKs.set(fileId, wrappedDEK);
|
|
681
|
-
}
|
|
682
|
-
async function uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
683
|
-
validateAPIKey(apiKey);
|
|
684
|
-
validateDEKStore(dekStore);
|
|
685
|
-
validateFileUploadOptions(options);
|
|
686
|
-
TimeoutSchema.parse(timeoutMs);
|
|
687
|
-
const controller = new AbortController;
|
|
688
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
689
|
-
try {
|
|
690
|
-
const { dek, filePayload } = await prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs);
|
|
691
|
-
const uploadedFile = await performUpload(apiKey, filePayload, controller);
|
|
692
|
-
storeDEKForFile(dekStore, uploadedFile.id, dek, clientKEK);
|
|
693
|
-
return uploadedFile;
|
|
694
|
-
} catch (error) {
|
|
695
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
696
|
-
throw new Error(`File upload timed out after ${timeoutMs}ms`);
|
|
697
|
-
}
|
|
698
|
-
throw error;
|
|
699
|
-
} finally {
|
|
700
|
-
clearTimeout(timeoutId);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
async function listFiles(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
704
|
-
validateAPIKey(apiKey);
|
|
705
|
-
validateListFilesOptions(options);
|
|
706
|
-
TimeoutSchema.parse(timeoutMs);
|
|
707
|
-
const controller = new AbortController;
|
|
708
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
709
|
-
const queryParams = new URLSearchParams;
|
|
710
|
-
if (options?.limit !== undefined) {
|
|
711
|
-
queryParams.append("limit", options.limit.toString());
|
|
712
|
-
}
|
|
713
|
-
if (options?.offset !== undefined) {
|
|
714
|
-
queryParams.append("offset", options.offset.toString());
|
|
715
|
-
}
|
|
716
|
-
if (options?.search) {
|
|
717
|
-
queryParams.append("search", options.search);
|
|
718
|
-
}
|
|
719
|
-
if (options?.from) {
|
|
720
|
-
queryParams.append("from", options.from);
|
|
721
|
-
}
|
|
722
|
-
if (options?.to) {
|
|
723
|
-
queryParams.append("to", options.to);
|
|
724
|
-
}
|
|
725
|
-
const queryString = queryParams.toString();
|
|
726
|
-
const url = `${endpoints.proxy}/files/encrypted${queryString ? `?${queryString}` : ""}`;
|
|
727
|
-
try {
|
|
728
|
-
const response = await fetch(url, {
|
|
729
|
-
method: "GET",
|
|
730
|
-
headers: {
|
|
731
|
-
Authorization: apiKey,
|
|
732
|
-
"Content-Type": "application/json"
|
|
733
|
-
},
|
|
734
|
-
signal: controller.signal
|
|
735
|
-
});
|
|
736
|
-
if (!response.ok) {
|
|
737
|
-
throw new Error(`List files request failed with status ${response.status}`);
|
|
738
|
-
}
|
|
739
|
-
const result = await response.json();
|
|
740
|
-
if (result.status !== 200) {
|
|
741
|
-
throw new Error(result.error || "List files failed");
|
|
742
|
-
}
|
|
743
|
-
if (!result.data) {
|
|
744
|
-
throw new Error("List files response missing data");
|
|
745
|
-
}
|
|
746
|
-
return result.data;
|
|
747
|
-
} catch (error) {
|
|
748
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
749
|
-
throw new Error(`List files request timed out after ${timeoutMs}ms`);
|
|
750
|
-
}
|
|
751
|
-
throw error;
|
|
752
|
-
} finally {
|
|
753
|
-
clearTimeout(timeoutId);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
async function getFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
757
|
-
validateAPIKey(apiKey);
|
|
758
|
-
validateGetFileOptions(options);
|
|
759
|
-
TimeoutSchema.parse(timeoutMs);
|
|
760
|
-
const controller = new AbortController;
|
|
761
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
762
|
-
const queryParams = new URLSearchParams;
|
|
763
|
-
if (options.url !== undefined) {
|
|
764
|
-
queryParams.append("url", options.url ? "true" : "false");
|
|
765
|
-
}
|
|
766
|
-
const queryString = queryParams.toString();
|
|
767
|
-
const url = `${endpoints.proxy}/files/encrypted/${options.id}${queryString ? `?${queryString}` : ""}`;
|
|
768
|
-
try {
|
|
769
|
-
const response = await fetch(url, {
|
|
770
|
-
method: "GET",
|
|
771
|
-
headers: {
|
|
772
|
-
Authorization: apiKey,
|
|
773
|
-
"Content-Type": "application/json"
|
|
774
|
-
},
|
|
775
|
-
signal: controller.signal
|
|
776
|
-
});
|
|
777
|
-
if (!response.ok) {
|
|
778
|
-
if (response.status === 404) {
|
|
779
|
-
throw new Error(`File not found: ${options.id}`);
|
|
780
|
-
}
|
|
781
|
-
throw new Error(`Get file request failed with status ${response.status}`);
|
|
782
|
-
}
|
|
783
|
-
const result = await response.json();
|
|
784
|
-
if (result.status !== 200) {
|
|
785
|
-
throw new Error(result.error || "Get file failed");
|
|
786
|
-
}
|
|
787
|
-
if (!result.data) {
|
|
788
|
-
throw new Error("Get file response missing data");
|
|
789
|
-
}
|
|
790
|
-
return result.data;
|
|
791
|
-
} catch (error) {
|
|
792
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
793
|
-
throw new Error(`Get file request timed out after ${timeoutMs}ms`);
|
|
794
|
-
}
|
|
795
|
-
throw error;
|
|
796
|
-
} finally {
|
|
797
|
-
clearTimeout(timeoutId);
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
async function deleteFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
801
|
-
validateAPIKey(apiKey);
|
|
802
|
-
DeleteFileOptionsSchema.parse(options);
|
|
803
|
-
TimeoutSchema.parse(timeoutMs);
|
|
804
|
-
const controller = new AbortController;
|
|
805
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
806
|
-
try {
|
|
807
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/${options.id}`, {
|
|
808
|
-
method: "DELETE",
|
|
809
|
-
headers: {
|
|
810
|
-
Authorization: apiKey,
|
|
811
|
-
"Content-Type": "application/json"
|
|
812
|
-
},
|
|
813
|
-
signal: controller.signal
|
|
814
|
-
});
|
|
815
|
-
if (!response.ok) {
|
|
816
|
-
if (response.status === 404) {
|
|
817
|
-
throw new Error(`File not found: ${options.id}`);
|
|
818
|
-
}
|
|
819
|
-
throw new Error(`Delete file request failed with status ${response.status}`);
|
|
820
|
-
}
|
|
821
|
-
await response.json();
|
|
822
|
-
} catch (error) {
|
|
823
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
824
|
-
throw new Error(`Delete file request timed out after ${timeoutMs}ms`);
|
|
825
|
-
}
|
|
826
|
-
throw error;
|
|
827
|
-
} finally {
|
|
828
|
-
clearTimeout(timeoutId);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
async function indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
832
|
-
validateAPIKey(apiKey);
|
|
833
|
-
validateDEKStore(dekStore);
|
|
834
|
-
IndexFilesOptionsSchema.parse(options);
|
|
835
|
-
TimeoutSchema.parse(timeoutMs);
|
|
836
|
-
const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
|
|
837
|
-
if (!wrappedRagDEK) {
|
|
838
|
-
throw new Error("RAG DEK not found. Provide ragDEK in options or upload at least one file with ragIndex: true.");
|
|
839
|
-
}
|
|
840
|
-
const _clientKEK = clientKEK ? import_utils4.hexToBytes(clientKEK) : getClientKEK();
|
|
841
|
-
const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
|
|
842
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
843
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
844
|
-
const encryptedFiles = options.files.map((file) => {
|
|
845
|
-
const wrappedFileDEK = file.fileDEK || dekStore.fileDEKs?.get(file.fileId);
|
|
846
|
-
if (!wrappedFileDEK) {
|
|
847
|
-
throw new Error(`File DEK not found for file: ${file.fileId}. Provide fileDEK or ensure file was uploaded with this DEK store.`);
|
|
848
|
-
}
|
|
849
|
-
const fileDEK = unwrapDEK(_clientKEK, wrappedFileDEK);
|
|
850
|
-
const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, fileDEK);
|
|
851
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
852
|
-
return {
|
|
853
|
-
file_id: file.fileId,
|
|
854
|
-
encrypted_file_dek: import_utils4.bytesToHex(encryptedFileDEK),
|
|
855
|
-
encrypted_rag_dek: import_utils4.bytesToHex(encryptedRagDEK),
|
|
856
|
-
file_nonce: import_utils4.bytesToHex(fileNonce),
|
|
857
|
-
rag_dek_nonce: import_utils4.bytesToHex(ragDEKNonce),
|
|
858
|
-
s3_r2_path: file.filePath,
|
|
859
|
-
cipher_text: import_utils4.bytesToHex(cipherText)
|
|
860
|
-
};
|
|
861
|
-
});
|
|
862
|
-
const controller = new AbortController;
|
|
863
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
864
|
-
try {
|
|
865
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/index`, {
|
|
866
|
-
method: "POST",
|
|
867
|
-
headers: {
|
|
868
|
-
Authorization: apiKey,
|
|
869
|
-
"Content-Type": "application/json"
|
|
870
|
-
},
|
|
871
|
-
body: JSON.stringify({ files: encryptedFiles }),
|
|
872
|
-
signal: controller.signal
|
|
873
|
-
});
|
|
874
|
-
if (!response.ok) {
|
|
875
|
-
throw new Error(`Index files request failed with status ${response.status}`);
|
|
876
|
-
}
|
|
877
|
-
const result = await response.json();
|
|
878
|
-
return result;
|
|
879
|
-
} catch (error) {
|
|
880
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
881
|
-
throw new Error(`Index files request timed out after ${timeoutMs}ms`);
|
|
882
|
-
}
|
|
883
|
-
throw error;
|
|
884
|
-
} finally {
|
|
885
|
-
clearTimeout(timeoutId);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
async function deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
889
|
-
validateAPIKey(apiKey);
|
|
890
|
-
validateDEKStore(dekStore);
|
|
891
|
-
DeleteIndexOptionsSchema.parse(options);
|
|
892
|
-
TimeoutSchema.parse(timeoutMs);
|
|
893
|
-
const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
|
|
894
|
-
if (!wrappedRagDEK) {
|
|
895
|
-
throw new Error("RAG DEK not found. Provide ragDEK in options or ensure dekStore has a ragDEK.");
|
|
896
|
-
}
|
|
897
|
-
const _clientKEK = clientKEK ? import_utils4.hexToBytes(clientKEK) : getClientKEK();
|
|
898
|
-
const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
|
|
899
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
900
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
901
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
902
|
-
const controller = new AbortController;
|
|
903
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
904
|
-
try {
|
|
905
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/delete-index`, {
|
|
906
|
-
method: "POST",
|
|
907
|
-
headers: {
|
|
908
|
-
Authorization: apiKey,
|
|
909
|
-
"Content-Type": "application/json"
|
|
910
|
-
},
|
|
911
|
-
body: JSON.stringify({
|
|
912
|
-
cipher_text: import_utils4.bytesToHex(cipherText),
|
|
913
|
-
encrypted_rag_dek: import_utils4.bytesToHex(encryptedRagDEK),
|
|
914
|
-
rag_dek_nonce: import_utils4.bytesToHex(ragDEKNonce),
|
|
915
|
-
fileIds: options.fileIds
|
|
916
|
-
}),
|
|
917
|
-
signal: controller.signal
|
|
918
|
-
});
|
|
919
|
-
if (!response.ok) {
|
|
920
|
-
throw new Error(`Delete index request failed with status ${response.status}`);
|
|
921
|
-
}
|
|
922
|
-
const result = await response.json();
|
|
923
|
-
return result;
|
|
924
|
-
} catch (error) {
|
|
925
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
926
|
-
throw new Error(`Delete index request timed out after ${timeoutMs}ms`);
|
|
927
|
-
}
|
|
928
|
-
throw error;
|
|
929
|
-
} finally {
|
|
930
|
-
clearTimeout(timeoutId);
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
function createFilesClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
934
|
-
return {
|
|
935
|
-
upload: (options) => uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs),
|
|
936
|
-
list: (options) => listFiles(apiKey, options, timeoutMs),
|
|
937
|
-
get: (options) => getFile(apiKey, options, timeoutMs),
|
|
938
|
-
delete: (options) => deleteFile(apiKey, options, timeoutMs),
|
|
939
|
-
index: (options) => indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs),
|
|
940
|
-
deleteIndex: (options) => deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs)
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// src/models/index.ts
|
|
945
|
-
async function listModels(params, apiKey, timeoutMs) {
|
|
946
|
-
const controller = new AbortController;
|
|
947
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
948
|
-
const queryParams = new URLSearchParams;
|
|
949
|
-
if (params?.type !== undefined) {
|
|
950
|
-
queryParams.append("type", params.type);
|
|
951
|
-
}
|
|
952
|
-
const queryString = queryParams.toString();
|
|
953
|
-
const url = `${endpoints.proxy}/models${queryString ? `?${queryString}` : ""}`;
|
|
954
|
-
try {
|
|
955
|
-
const response = await fetch(url, {
|
|
956
|
-
method: "GET",
|
|
957
|
-
headers: {
|
|
958
|
-
Authorization: apiKey,
|
|
959
|
-
"Content-Type": "application/json"
|
|
960
|
-
},
|
|
961
|
-
signal: controller.signal
|
|
962
|
-
});
|
|
963
|
-
if (!response.ok) {
|
|
964
|
-
throw new Error(`List models request failed with status ${response.status}`);
|
|
965
|
-
}
|
|
966
|
-
const result = await response.json();
|
|
967
|
-
if (result.status !== 200) {
|
|
968
|
-
throw new Error(result.error || "List models failed");
|
|
969
|
-
}
|
|
970
|
-
if (!result.data) {
|
|
971
|
-
throw new Error("List models response missing data");
|
|
972
|
-
}
|
|
973
|
-
return result.data;
|
|
974
|
-
} catch (error) {
|
|
975
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
976
|
-
throw new Error(`List models request timed out after ${timeoutMs}ms`);
|
|
977
|
-
}
|
|
978
|
-
throw error;
|
|
979
|
-
} finally {
|
|
980
|
-
clearTimeout(timeoutId);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
function createModelsClient(apiKey, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
984
|
-
return {
|
|
985
|
-
list: (params) => listModels(params, apiKey, timeoutMs)
|
|
986
|
-
};
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// src/rvenc/index.ts
|
|
990
|
-
var import_utils5 = require("@noble/ciphers/utils.js");
|
|
991
|
-
var import_openai = __toESM(require("openai"));
|
|
992
|
-
function preprocessRequest(body, encryptionKeys) {
|
|
993
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
994
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, body);
|
|
995
|
-
return {
|
|
996
|
-
body: {
|
|
997
|
-
cipherText: import_utils5.bytesToHex(cipherText),
|
|
998
|
-
encryptedInference: import_utils5.bytesToHex(encrypted),
|
|
999
|
-
nonce: import_utils5.bytesToHex(nonce),
|
|
1000
|
-
model: body.model,
|
|
1001
|
-
stream: body.stream === true
|
|
1002
|
-
},
|
|
1003
|
-
sharedSecret,
|
|
1004
|
-
nonce
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
async function postprocessStreamingResponse(response, sharedSecret, nonce, maxBufferSize) {
|
|
1008
|
-
if (!response.body) {
|
|
1009
|
-
throw new Error("Response body is null");
|
|
1010
|
-
}
|
|
1011
|
-
const reader = response.body.getReader();
|
|
1012
|
-
const generator = createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize);
|
|
1013
|
-
return {
|
|
1014
|
-
[Symbol.asyncIterator]() {
|
|
1015
|
-
return generator;
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
async function postprocessNonStreamingResponse(response, sharedSecret) {
|
|
1020
|
-
const data = await response.json();
|
|
1021
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
1022
|
-
throw new Error("Invalid non-streaming response: missing encryptedResponse or nonce");
|
|
1023
|
-
}
|
|
1024
|
-
const responseNonce = import_utils5.hexToBytes(data.nonce);
|
|
1025
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
1026
|
-
}
|
|
1027
|
-
function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE, attest2 = true, OpenAIClientParams) {
|
|
1028
|
-
const client = new import_openai.default({ apiKey: "not-used", ...OpenAIClientParams });
|
|
1029
|
-
const originalChatCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
1030
|
-
client.chat.completions.create = async (body) => {
|
|
1031
|
-
const isStreaming = body.stream === true;
|
|
1032
|
-
const controller = new AbortController;
|
|
1033
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1034
|
-
try {
|
|
1035
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
1036
|
-
const encryptedRequest = preprocessRequest(body, encryptionKeys);
|
|
1037
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
|
|
1038
|
-
method: "POST",
|
|
1039
|
-
headers: {
|
|
1040
|
-
"Content-Type": "application/json",
|
|
1041
|
-
Accept: isStreaming ? "text/event-stream" : "application/json",
|
|
1042
|
-
Authorization: apiKey,
|
|
1043
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
1044
|
-
},
|
|
1045
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
1046
|
-
signal: controller.signal
|
|
1047
|
-
});
|
|
1048
|
-
if (!response.ok) {
|
|
1049
|
-
await throwIfErrorResponse(response);
|
|
1050
|
-
}
|
|
1051
|
-
clearTimeout(timeoutId);
|
|
1052
|
-
if (isStreaming) {
|
|
1053
|
-
return await postprocessStreamingResponse(response, encryptedRequest.sharedSecret, encryptedRequest.nonce, maxBufferSize);
|
|
1054
|
-
} else {
|
|
1055
|
-
return await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
|
|
1056
|
-
}
|
|
1057
|
-
} catch (error) {
|
|
1058
|
-
clearTimeout(timeoutId);
|
|
1059
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1060
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
1061
|
-
}
|
|
1062
|
-
throw error;
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
return client;
|
|
1066
|
-
}
|
|
1067
|
-
async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
|
|
1068
|
-
const decoder = new TextDecoder;
|
|
1069
|
-
let buffer = "";
|
|
1070
|
-
try {
|
|
1071
|
-
while (true) {
|
|
1072
|
-
const { value, done } = await reader.read();
|
|
1073
|
-
if (done)
|
|
1074
|
-
break;
|
|
1075
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1076
|
-
if (buffer.length > maxBufferSize) {
|
|
1077
|
-
throw new Error(`Stream buffer exceeded maximum size of ${maxBufferSize} bytes`);
|
|
1078
|
-
}
|
|
1079
|
-
const parts = buffer.split(`
|
|
1080
|
-
|
|
1081
|
-
`);
|
|
1082
|
-
for (let i = 0;i < parts.length - 1; i++) {
|
|
1083
|
-
const part = parts[i];
|
|
1084
|
-
const lines = part.split(`
|
|
1085
|
-
`);
|
|
1086
|
-
let event;
|
|
1087
|
-
let data;
|
|
1088
|
-
if (lines[0]) {
|
|
1089
|
-
const eventSplit = lines[0].split(": ");
|
|
1090
|
-
event = eventSplit[1];
|
|
1091
|
-
}
|
|
1092
|
-
if (lines[1]) {
|
|
1093
|
-
const dataSplit = lines[1].split(": ");
|
|
1094
|
-
data = dataSplit.slice(1).join(": ");
|
|
1095
|
-
}
|
|
1096
|
-
if (event === "done" && data === "[DONE]") {
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
if (event === "error") {
|
|
1100
|
-
const errorObj = JSON.parse(data || "{}");
|
|
1101
|
-
throw new Error(errorObj.error?.message || data || "Stream error");
|
|
1102
|
-
}
|
|
1103
|
-
if (event === "data" && data && data !== "[DONE]") {
|
|
1104
|
-
const chunk = decryptPayload(data, sharedSecret, nonce);
|
|
1105
|
-
if (chunk.error) {
|
|
1106
|
-
throw new Error(chunk.error.message || "Stream error");
|
|
1107
|
-
}
|
|
1108
|
-
yield chunk;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
buffer = parts[parts.length - 1];
|
|
1112
|
-
}
|
|
1113
|
-
} finally {
|
|
1114
|
-
reader.releaseLock();
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// src/tools/index.ts
|
|
1119
|
-
var import_utils6 = require("@noble/ciphers/utils.js");
|
|
1120
|
-
var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
|
|
1121
|
-
var FILE_INPUT_TOOLS = [
|
|
1122
|
-
"imageDescribeAndCaption",
|
|
1123
|
-
"imageDescribeAndCaptionFallback",
|
|
1124
|
-
"videoDescribeAndCaption",
|
|
1125
|
-
"getPDFContent",
|
|
1126
|
-
"getTextDocumentContent",
|
|
1127
|
-
"transcribeAudioToText",
|
|
1128
|
-
"transcribeAudioWithDiarization",
|
|
1129
|
-
"audioDiarization",
|
|
1130
|
-
"getSpreadsheetContent",
|
|
1131
|
-
"getPowerPointContent",
|
|
1132
|
-
"getDataFileContent",
|
|
1133
|
-
"getFileContentOCR"
|
|
1134
|
-
];
|
|
1135
|
-
var RAG_TOOLS = ["searchRag"];
|
|
1136
|
-
async function callToolRequest(toolName, body, apiKey, timeoutMs, attest2) {
|
|
1137
|
-
const controller = new AbortController;
|
|
1138
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1139
|
-
try {
|
|
1140
|
-
const response = await fetch(`${endpoints.proxy}/tools/${toolName}`, {
|
|
1141
|
-
method: "POST",
|
|
1142
|
-
headers: {
|
|
1143
|
-
"Content-Type": "application/json",
|
|
1144
|
-
Authorization: apiKey
|
|
1145
|
-
},
|
|
1146
|
-
body: JSON.stringify(body),
|
|
1147
|
-
signal: controller.signal
|
|
1148
|
-
});
|
|
1149
|
-
clearTimeout(timeoutId);
|
|
1150
|
-
if (!response.ok) {
|
|
1151
|
-
await throwIfErrorResponse(response);
|
|
1152
|
-
}
|
|
1153
|
-
const data = await response.json();
|
|
1154
|
-
return data.data;
|
|
1155
|
-
} catch (error) {
|
|
1156
|
-
clearTimeout(timeoutId);
|
|
1157
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1158
|
-
throw new Error(`Tool request timed out after ${timeoutMs}ms`);
|
|
1159
|
-
}
|
|
1160
|
-
throw new Error(`Tool request failed: ${error instanceof Error ? error.message : error}`);
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
|
|
1164
|
-
const controller = new AbortController;
|
|
1165
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1166
|
-
try {
|
|
1167
|
-
const metadataResponse = await fetch(`${endpoints.proxy}/files/encrypted/${fileId}?url=true`, {
|
|
1168
|
-
headers: { Authorization: apiKey },
|
|
1169
|
-
signal: controller.signal
|
|
1170
|
-
});
|
|
1171
|
-
if (!metadataResponse.ok) {
|
|
1172
|
-
throw new Error(`Failed to get file metadata: ${metadataResponse.status}`);
|
|
1173
|
-
}
|
|
1174
|
-
const metadata = await metadataResponse.json();
|
|
1175
|
-
const downloadUrl = metadata.data?.url;
|
|
1176
|
-
if (!downloadUrl) {
|
|
1177
|
-
throw new Error("No download URL in response");
|
|
1178
|
-
}
|
|
1179
|
-
const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
|
|
1180
|
-
if (!fileResponse.ok) {
|
|
1181
|
-
throw new Error(`Failed to download file: ${fileResponse.status}`);
|
|
1182
|
-
}
|
|
1183
|
-
clearTimeout(timeoutId);
|
|
1184
|
-
const arrayBuffer = await fileResponse.arrayBuffer();
|
|
1185
|
-
return new Uint8Array(arrayBuffer);
|
|
1186
|
-
} catch (error) {
|
|
1187
|
-
clearTimeout(timeoutId);
|
|
1188
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1189
|
-
throw new Error(`File download timed out after ${timeoutMs}ms`);
|
|
1190
|
-
}
|
|
1191
|
-
throw error;
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
async function downloadAndDecryptFile(response, dek, apiKey, timeoutMs) {
|
|
1195
|
-
if (!response.success || !response.fileId) {
|
|
1196
|
-
return null;
|
|
1197
|
-
}
|
|
1198
|
-
const decryptFileName = (encryptedHex) => {
|
|
1199
|
-
const encrypted = import_utils6.hexToBytes(encryptedHex);
|
|
1200
|
-
const decrypted = decryptWithDEK(dek, encrypted);
|
|
1201
|
-
return new TextDecoder().decode(decrypted);
|
|
1202
|
-
};
|
|
1203
|
-
const fileName = decryptFileName(response.fileName);
|
|
1204
|
-
const mimeType = decryptFileName(response.mimeType);
|
|
1205
|
-
const encryptedFile = await downloadEncryptedFile(response.fileId, apiKey, timeoutMs);
|
|
1206
|
-
const decryptedFile = decryptWithDEK(dek, encryptedFile);
|
|
1207
|
-
return {
|
|
1208
|
-
fileId: response.fileId,
|
|
1209
|
-
fileName,
|
|
1210
|
-
mimeType,
|
|
1211
|
-
content: decryptedFile,
|
|
1212
|
-
fileSize: decryptedFile.length
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
async function callSimpleTool(toolName, params, apiKey, timeoutMs, attest2) {
|
|
1216
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1217
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1218
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
1219
|
-
const body = {
|
|
1220
|
-
cipherText: import_utils6.bytesToHex(cipherText),
|
|
1221
|
-
encryptedParams: import_utils6.bytesToHex(encrypted),
|
|
1222
|
-
nonce: import_utils6.bytesToHex(nonce)
|
|
1223
|
-
};
|
|
1224
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
1225
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, import_utils6.hexToBytes(response.nonce));
|
|
1226
|
-
}
|
|
1227
|
-
async function callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1228
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1229
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1230
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
1231
|
-
const dek = import_utils6.randomBytes(32);
|
|
1232
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
1233
|
-
const _clientKEK = clientKEK ? import_utils6.hexToBytes(clientKEK) : getClientKEK();
|
|
1234
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
1235
|
-
const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
|
|
1236
|
-
const body = {
|
|
1237
|
-
cipherText: import_utils6.bytesToHex(cipherText),
|
|
1238
|
-
encryptedParams: import_utils6.bytesToHex(encrypted),
|
|
1239
|
-
nonce: import_utils6.bytesToHex(nonce),
|
|
1240
|
-
encryptedDEK: import_utils6.bytesToHex(encryptedDEK),
|
|
1241
|
-
dekNonce: import_utils6.bytesToHex(dekNonce),
|
|
1242
|
-
kid: clientKID,
|
|
1243
|
-
wrappedDEK: import_utils6.bytesToHex(wrappedDEK)
|
|
1244
|
-
};
|
|
1245
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
1246
|
-
const result = await downloadAndDecryptFile(response, dek, apiKey, timeoutMs);
|
|
1247
|
-
if (result && result.fileId) {
|
|
1248
|
-
if (!dekStore.fileDEKs) {
|
|
1249
|
-
dekStore.fileDEKs = new Map;
|
|
1250
|
-
}
|
|
1251
|
-
dekStore.fileDEKs.set(result.fileId, wrappedDEK);
|
|
1252
|
-
}
|
|
1253
|
-
return result;
|
|
1254
|
-
}
|
|
1255
|
-
async function callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1256
|
-
if (!params.fileId) {
|
|
1257
|
-
throw new Error(`Tool ${toolName} requires fileId parameter`);
|
|
1258
|
-
}
|
|
1259
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1260
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1261
|
-
const dek = import_utils6.randomBytes(32);
|
|
1262
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
1263
|
-
const nonce = import_utils6.randomBytes(24);
|
|
1264
|
-
if (!dekStore.fileDEKs) {
|
|
1265
|
-
dekStore.fileDEKs = new Map;
|
|
1266
|
-
}
|
|
1267
|
-
const _clientKEK = clientKEK ? import_utils6.hexToBytes(clientKEK) : getClientKEK();
|
|
1268
|
-
let fileDEK = dekStore.fileDEKs.get(params.fileId);
|
|
1269
|
-
if (!fileDEK) {
|
|
1270
|
-
fileDEK = import_utils6.randomBytes(32);
|
|
1271
|
-
const wrappedFileDEK = wrapDEK(_clientKEK, fileDEK);
|
|
1272
|
-
dekStore.fileDEKs.set(params.fileId, wrappedFileDEK);
|
|
1273
|
-
} else {
|
|
1274
|
-
fileDEK = unwrapDEK(_clientKEK, fileDEK);
|
|
1275
|
-
}
|
|
1276
|
-
const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, fileDEK);
|
|
1277
|
-
const body = {
|
|
1278
|
-
cipherText: import_utils6.bytesToHex(cipherText),
|
|
1279
|
-
nonce: import_utils6.bytesToHex(nonce),
|
|
1280
|
-
fileId: params.fileId,
|
|
1281
|
-
encryptedDEK: import_utils6.bytesToHex(encryptedDEK),
|
|
1282
|
-
dekNonce: import_utils6.bytesToHex(dekNonce),
|
|
1283
|
-
encryptedFileDEK: import_utils6.bytesToHex(encryptedFileDEK),
|
|
1284
|
-
fileDEKNonce: import_utils6.bytesToHex(fileDEKNonce)
|
|
1285
|
-
};
|
|
1286
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
1287
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, import_utils6.hexToBytes(response.nonce));
|
|
1288
|
-
}
|
|
1289
|
-
async function callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1290
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1291
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1292
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
1293
|
-
const dek = import_utils6.randomBytes(32);
|
|
1294
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
1295
|
-
if (!dekStore.fileDEKs) {
|
|
1296
|
-
dekStore.fileDEKs = new Map;
|
|
1297
|
-
}
|
|
1298
|
-
let fileIds = [];
|
|
1299
|
-
if (dekStore.fileDEKs.size > 0) {
|
|
1300
|
-
fileIds = Array.from(dekStore.fileDEKs.keys());
|
|
1301
|
-
}
|
|
1302
|
-
const _clientKEK = clientKEK ? import_utils6.hexToBytes(clientKEK) : getClientKEK();
|
|
1303
|
-
const encryptedFileDEKs = fileIds.reduce((acc, fileId) => {
|
|
1304
|
-
const fileDEK = dekStore.fileDEKs.get(fileId);
|
|
1305
|
-
if (!fileDEK) {
|
|
1306
|
-
return acc;
|
|
1307
|
-
}
|
|
1308
|
-
const unwrappedFileDEK = unwrapDEK(_clientKEK, fileDEK);
|
|
1309
|
-
const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, unwrappedFileDEK);
|
|
1310
|
-
acc.push({
|
|
1311
|
-
fileId,
|
|
1312
|
-
encryptedDEK: import_utils6.bytesToHex(encryptedFileDEK),
|
|
1313
|
-
nonce: import_utils6.bytesToHex(fileDEKNonce)
|
|
1314
|
-
});
|
|
1315
|
-
return acc;
|
|
1316
|
-
}, []);
|
|
1317
|
-
if (!dekStore.ragDEK) {
|
|
1318
|
-
throw new Error("RAG DEK not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
|
|
1319
|
-
}
|
|
1320
|
-
if (!dekStore.ragVersion) {
|
|
1321
|
-
throw new Error("RAG Version not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
|
|
1322
|
-
}
|
|
1323
|
-
const ragDEK = unwrapDEK(_clientKEK, dekStore.ragDEK);
|
|
1324
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
1325
|
-
const { encrypted: encryptedRagVersion, nonce: ragVersionNonce } = encryptPayload(sharedSecret, dekStore.ragVersion);
|
|
1326
|
-
const body = {
|
|
1327
|
-
cipherText: import_utils6.bytesToHex(cipherText),
|
|
1328
|
-
encryptedParams: import_utils6.bytesToHex(encrypted),
|
|
1329
|
-
nonce: import_utils6.bytesToHex(nonce),
|
|
1330
|
-
encryptedDEK: import_utils6.bytesToHex(encryptedDEK),
|
|
1331
|
-
dekNonce: import_utils6.bytesToHex(dekNonce),
|
|
1332
|
-
encryptedFileDEKs,
|
|
1333
|
-
encryptedRagDEK: import_utils6.bytesToHex(encryptedRagDEK),
|
|
1334
|
-
ragDEKNonce: import_utils6.bytesToHex(ragDEKNonce),
|
|
1335
|
-
encryptedRagVersion: import_utils6.bytesToHex(encryptedRagVersion),
|
|
1336
|
-
ragVersionNonce: import_utils6.bytesToHex(ragVersionNonce)
|
|
1337
|
-
};
|
|
1338
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
1339
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, import_utils6.hexToBytes(response.nonce));
|
|
1340
|
-
}
|
|
1341
|
-
async function callTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1342
|
-
if (FILE_OUTPUT_TOOLS.includes(toolName)) {
|
|
1343
|
-
return callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
1344
|
-
} else if (FILE_INPUT_TOOLS.includes(toolName)) {
|
|
1345
|
-
return callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
1346
|
-
} else if (RAG_TOOLS.includes(toolName)) {
|
|
1347
|
-
return callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
1348
|
-
} else {
|
|
1349
|
-
return callSimpleTool(toolName, params, apiKey, timeoutMs, attest2);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
function createToolsClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1353
|
-
return {
|
|
1354
|
-
generateImage: (params) => callTool("generateImage", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1355
|
-
audioGenerateFromText: (params) => callTool("audioGenerateFromText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1356
|
-
createFileForUser: (params) => callTool("createFileForUser", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1357
|
-
imageDescribeAndCaption: (params) => callTool("imageDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1358
|
-
imageDescribeAndCaptionFallback: (params) => callTool("imageDescribeAndCaptionFallback", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1359
|
-
videoDescribeAndCaption: (params) => callTool("videoDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1360
|
-
getPDFContent: (params) => callTool("getPDFContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1361
|
-
getTextDocumentContent: (params) => callTool("getTextDocumentContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1362
|
-
transcribeAudioToText: (params) => callTool("transcribeAudioToText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1363
|
-
transcribeAudioWithDiarization: (params) => callTool("transcribeAudioWithDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1364
|
-
audioDiarization: (params) => callTool("audioDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1365
|
-
getFileContentOCR: (params) => callTool("getFileContentOCR", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1366
|
-
getSpreadsheetContent: (params) => callTool("getSpreadsheetContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1367
|
-
getDataFileContent: (params) => callTool("getDataFileContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1368
|
-
getPowerPointContent: (params) => callTool("getPowerPointContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1369
|
-
getTime: (params) => callTool("getTime", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1370
|
-
webSearchTool: (params) => callTool("webSearchTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1371
|
-
webPageScraperTool: (params) => callTool("webPageScraperTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
1372
|
-
searchRag: (params) => callTool("searchRag", params, apiKey, dekStore, clientKEK, timeoutMs, attest2)
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
// src/core.ts
|
|
1377
|
-
async function createRvencClient(options) {
|
|
1378
|
-
const {
|
|
1379
|
-
apiKey,
|
|
1380
|
-
clientKEK,
|
|
1381
|
-
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
1382
|
-
maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
|
|
1383
|
-
attest: attest2 = true
|
|
1384
|
-
} = options;
|
|
1385
|
-
if (options.config?.endpoints !== undefined) {
|
|
1386
|
-
Object.assign(endpoints, options.config.endpoints);
|
|
1387
|
-
}
|
|
1388
|
-
let encryptionKeys;
|
|
1389
|
-
try {
|
|
1390
|
-
encryptionKeys = options.encryptionKeys ?? await generateEncryptionKeys(requestTimeoutMs);
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
throw new Error(`Failed to initialize encryption keys: ${error instanceof Error ? error.message : error}`);
|
|
1393
|
-
}
|
|
1394
|
-
const dekStore = options.dekStore ?? initializeDEKStore(clientKEK);
|
|
1395
|
-
const client = createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs, maxBufferSize, attest2, options.config?.openAIClientOptions ?? {});
|
|
1396
|
-
client.files = createFilesClient(apiKey, dekStore, clientKEK, requestTimeoutMs);
|
|
1397
|
-
client.tools = createToolsClient(apiKey, dekStore, clientKEK, requestTimeoutMs, attest2);
|
|
1398
|
-
client.audio = createAudioClient(apiKey, encryptionKeys, requestTimeoutMs, attest2);
|
|
1399
|
-
client.models = createModelsClient(apiKey, requestTimeoutMs);
|
|
1400
|
-
client.dekStore = dekStore;
|
|
1401
|
-
return client;
|
|
1402
|
-
}
|
|
1403
|
-
var core_default = createRvencClient;
|
|
1404
|
-
|
|
1405
|
-
// src/server.ts
|
|
1406
|
-
import_dotenv.default.config();
|
|
1407
|
-
var app = import_express.default();
|
|
1408
|
-
var HOST = process.env.HOST ?? "127.0.0.1";
|
|
1409
|
-
var PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 3000;
|
|
1410
|
-
var serverProxyUrl = process.env.PROXY_URL;
|
|
1411
|
-
var serverEnclaveUrl = process.env.ENCLAVE_URL;
|
|
1412
|
-
var serverKek = process.env.CLIENT_KEK;
|
|
1413
|
-
var serverAttest = true;
|
|
1414
|
-
app.use(import_express.default.json());
|
|
1415
|
-
var storage = import_multer.default.memoryStorage();
|
|
1416
|
-
var upload = import_multer.default({
|
|
1417
|
-
storage,
|
|
1418
|
-
limits: {
|
|
1419
|
-
fileSize: 25 * 1024 * 1024
|
|
1420
|
-
}
|
|
1421
|
-
});
|
|
1422
|
-
var clientCache = new Map;
|
|
1423
|
-
function extractApiKey(req) {
|
|
1424
|
-
const authHeader = req.headers.authorization;
|
|
1425
|
-
if (!authHeader) {
|
|
1426
|
-
return null;
|
|
1427
|
-
}
|
|
1428
|
-
if (authHeader.startsWith("Bearer ")) {
|
|
1429
|
-
return authHeader.substring(7);
|
|
1430
|
-
}
|
|
1431
|
-
return authHeader;
|
|
1432
|
-
}
|
|
1433
|
-
async function getOrCreateClient(apiKey) {
|
|
1434
|
-
if (clientCache.has(apiKey)) {
|
|
1435
|
-
return clientCache.get(apiKey);
|
|
1436
|
-
}
|
|
1437
|
-
const client = await core_default({
|
|
1438
|
-
apiKey,
|
|
1439
|
-
clientKEK: serverKek,
|
|
1440
|
-
attest: serverAttest,
|
|
1441
|
-
config: {
|
|
1442
|
-
endpoints: {
|
|
1443
|
-
enclave: serverEnclaveUrl,
|
|
1444
|
-
proxy: serverProxyUrl
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
});
|
|
1448
|
-
clientCache.set(apiKey, client);
|
|
1449
|
-
return client;
|
|
1450
|
-
}
|
|
1451
|
-
app.get("/", (_, res) => {
|
|
1452
|
-
res.json({
|
|
1453
|
-
message: "Rvenc OpenAI-compatible API Server",
|
|
1454
|
-
version: "1.0.0",
|
|
1455
|
-
endpoints: {
|
|
1456
|
-
chat_completions: "POST /v1/chat/completions"
|
|
1457
|
-
}
|
|
1458
|
-
});
|
|
1459
|
-
});
|
|
1460
|
-
app.post("/v1/chat/completions", async (req, res) => {
|
|
1461
|
-
try {
|
|
1462
|
-
const apiKey = extractApiKey(req);
|
|
1463
|
-
if (!apiKey) {
|
|
1464
|
-
return res.status(401).json({
|
|
1465
|
-
error: {
|
|
1466
|
-
message: 'Missing Authorization header. Expected format: "Bearer <api-key>" or "<api-key>"',
|
|
1467
|
-
type: "invalid_request_error",
|
|
1468
|
-
code: "invalid_api_key"
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
const client = await getOrCreateClient(apiKey);
|
|
1473
|
-
const params = req.body;
|
|
1474
|
-
const completion = await client.chat.completions.create(params);
|
|
1475
|
-
if (params.stream) {
|
|
1476
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
1477
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1478
|
-
res.setHeader("Connection", "keep-alive");
|
|
1479
|
-
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
1480
|
-
try {
|
|
1481
|
-
for await (const chunk of completion) {
|
|
1482
|
-
const data = `data: ${JSON.stringify(chunk)}
|
|
1483
|
-
|
|
1484
|
-
`;
|
|
1485
|
-
res.write(data);
|
|
1486
|
-
}
|
|
1487
|
-
res.write(`data: [DONE]
|
|
1488
|
-
|
|
1489
|
-
`);
|
|
1490
|
-
res.end();
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
console.error("Streaming error:", error);
|
|
1493
|
-
if (!res.headersSent) {
|
|
1494
|
-
res.status(500).json({
|
|
1495
|
-
error: {
|
|
1496
|
-
message: error.message || "Streaming error",
|
|
1497
|
-
type: "server_error"
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
} else {
|
|
1501
|
-
res.end();
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
} else {
|
|
1505
|
-
res.write(`data: ${JSON.stringify(completion)}
|
|
1506
|
-
|
|
1507
|
-
`);
|
|
1508
|
-
res.write(`data: [DONE]
|
|
1509
|
-
|
|
1510
|
-
`);
|
|
1511
|
-
res.end();
|
|
1512
|
-
}
|
|
1513
|
-
} else {
|
|
1514
|
-
res.json(completion);
|
|
1515
|
-
}
|
|
1516
|
-
} catch (error) {
|
|
1517
|
-
console.error("Chat completions error:", error);
|
|
1518
|
-
const statusCode = error.status || 500;
|
|
1519
|
-
res.status(statusCode).json({
|
|
1520
|
-
error: {
|
|
1521
|
-
message: error.message || "Internal server error",
|
|
1522
|
-
type: error.type || "server_error",
|
|
1523
|
-
code: error.code
|
|
1524
|
-
}
|
|
1525
|
-
});
|
|
1526
|
-
}
|
|
1527
|
-
});
|
|
1528
|
-
app.post("/v1/audio/transcriptions", upload.single("file"), async (req, res) => {
|
|
1529
|
-
try {
|
|
1530
|
-
const apiKey = extractApiKey(req);
|
|
1531
|
-
if (!apiKey) {
|
|
1532
|
-
return res.status(401).json({
|
|
1533
|
-
error: {
|
|
1534
|
-
message: 'Missing Authorization header. Expected format: "Bearer <api-key>" or "<api-key>"',
|
|
1535
|
-
type: "invalid_request_error",
|
|
1536
|
-
code: "invalid_api_key"
|
|
1537
|
-
}
|
|
1538
|
-
});
|
|
1539
|
-
}
|
|
1540
|
-
if (!req.file) {
|
|
1541
|
-
return res.status(400).json({
|
|
1542
|
-
error: {
|
|
1543
|
-
message: "Missing required file parameter",
|
|
1544
|
-
type: "invalid_request_error"
|
|
1545
|
-
}
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
const client = await getOrCreateClient(apiKey);
|
|
1549
|
-
const file = new File([req.file.buffer], req.file.originalname, {
|
|
1550
|
-
type: req.file.mimetype
|
|
1551
|
-
});
|
|
1552
|
-
const params = {
|
|
1553
|
-
file,
|
|
1554
|
-
model: req.body.model
|
|
1555
|
-
};
|
|
1556
|
-
if (req.body.language)
|
|
1557
|
-
params.language = req.body.language;
|
|
1558
|
-
if (req.body.prompt)
|
|
1559
|
-
params.prompt = req.body.prompt;
|
|
1560
|
-
if (req.body.response_format)
|
|
1561
|
-
params.response_format = req.body.response_format;
|
|
1562
|
-
if (req.body.temperature)
|
|
1563
|
-
params.temperature = parseFloat(req.body.temperature);
|
|
1564
|
-
if (req.body.timestamp_granularities) {
|
|
1565
|
-
params.timestamp_granularities = Array.isArray(req.body.timestamp_granularities) ? req.body.timestamp_granularities : JSON.parse(req.body.timestamp_granularities);
|
|
1566
|
-
}
|
|
1567
|
-
const transcription = await client.audio.transcriptions.create(params);
|
|
1568
|
-
res.json(transcription);
|
|
1569
|
-
} catch (error) {
|
|
1570
|
-
console.error("Audio transcription error:", error);
|
|
1571
|
-
const statusCode = error.status || 500;
|
|
1572
|
-
res.status(statusCode).json({
|
|
1573
|
-
error: {
|
|
1574
|
-
message: error.message || "Internal server error",
|
|
1575
|
-
type: error.type || "server_error",
|
|
1576
|
-
code: error.code
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
app.post("/v1/audio/translations", upload.single("file"), async (req, res) => {
|
|
1582
|
-
try {
|
|
1583
|
-
const apiKey = extractApiKey(req);
|
|
1584
|
-
if (!apiKey) {
|
|
1585
|
-
return res.status(401).json({
|
|
1586
|
-
error: {
|
|
1587
|
-
message: 'Missing Authorization header. Expected format: "Bearer <api-key>" or "<api-key>"',
|
|
1588
|
-
type: "invalid_request_error",
|
|
1589
|
-
code: "invalid_api_key"
|
|
1590
|
-
}
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
if (!req.file) {
|
|
1594
|
-
return res.status(400).json({
|
|
1595
|
-
error: {
|
|
1596
|
-
message: "Missing required file parameter",
|
|
1597
|
-
type: "invalid_request_error"
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
}
|
|
1601
|
-
const client = await getOrCreateClient(apiKey);
|
|
1602
|
-
const file = new File([req.file.buffer], req.file.originalname, {
|
|
1603
|
-
type: req.file.mimetype
|
|
1604
|
-
});
|
|
1605
|
-
const params = {
|
|
1606
|
-
file,
|
|
1607
|
-
model: req.body.model
|
|
1608
|
-
};
|
|
1609
|
-
if (req.body.prompt)
|
|
1610
|
-
params.prompt = req.body.prompt;
|
|
1611
|
-
if (req.body.response_format)
|
|
1612
|
-
params.response_format = req.body.response_format;
|
|
1613
|
-
if (req.body.temperature)
|
|
1614
|
-
params.temperature = parseFloat(req.body.temperature);
|
|
1615
|
-
const translation = await client.audio.translations.create(params);
|
|
1616
|
-
res.json(translation);
|
|
1617
|
-
} catch (error) {
|
|
1618
|
-
console.error("Audio translation error:", error);
|
|
1619
|
-
const statusCode = error.status || 500;
|
|
1620
|
-
res.status(statusCode).json({
|
|
1621
|
-
error: {
|
|
1622
|
-
message: error.message || "Internal server error",
|
|
1623
|
-
type: error.type || "server_error",
|
|
1624
|
-
code: error.code
|
|
1625
|
-
}
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
});
|
|
1629
|
-
app.use((err, _req, res, _next) => {
|
|
1630
|
-
console.error(`Unhandled error: ${err}`);
|
|
1631
|
-
res.status(500).json({
|
|
1632
|
-
error: {
|
|
1633
|
-
message: err.message || "Internal server error",
|
|
1634
|
-
type: "server_error"
|
|
1635
|
-
}
|
|
1636
|
-
});
|
|
1637
|
-
});
|
|
1638
|
-
app.use((req, res) => {
|
|
1639
|
-
res.status(404).json({
|
|
1640
|
-
error: {
|
|
1641
|
-
message: `Route ${req.method} ${req.path} not found`,
|
|
1642
|
-
type: "invalid_request_error"
|
|
1643
|
-
}
|
|
1644
|
-
});
|
|
1645
|
-
});
|
|
1646
|
-
async function startServer(options = {}) {
|
|
1647
|
-
const { host, port, proxyUrl, enclaveUrl, kek, attest: attest2 } = options;
|
|
1648
|
-
const serverHost = host || HOST;
|
|
1649
|
-
const serverPort = port || PORT;
|
|
1650
|
-
serverAttest = attest2 !== false;
|
|
1651
|
-
if (proxyUrl) {
|
|
1652
|
-
serverProxyUrl = proxyUrl;
|
|
1653
|
-
}
|
|
1654
|
-
if (enclaveUrl) {
|
|
1655
|
-
serverEnclaveUrl = enclaveUrl;
|
|
1656
|
-
}
|
|
1657
|
-
if (kek) {
|
|
1658
|
-
serverKek = kek;
|
|
1659
|
-
}
|
|
1660
|
-
return new Promise((resolve, reject) => {
|
|
1661
|
-
const server = app.listen(serverPort, serverHost, () => {
|
|
1662
|
-
console.log(`
|
|
1663
|
-
Rvenc Server running on http://${serverHost}:${serverPort}`);
|
|
1664
|
-
resolve();
|
|
1665
|
-
});
|
|
1666
|
-
server.on("error", (error) => {
|
|
1667
|
-
if (error.code === "EADDRINUSE") {
|
|
1668
|
-
console.error(`❌ Port ${serverPort} is already in use`);
|
|
1669
|
-
} else {
|
|
1670
|
-
console.error(`❌ Failed to start server: ${error}`);
|
|
1671
|
-
}
|
|
1672
|
-
reject(error);
|
|
1673
|
-
});
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
if (false) {}
|
|
1677
|
-
|
|
1678
|
-
// src/cli.ts
|
|
1679
|
-
var { values } = import_node_util.parseArgs({
|
|
1680
|
-
args: process.argv.slice(2),
|
|
1681
|
-
options: {
|
|
1682
|
-
host: { type: "string", default: "127.0.0.1" },
|
|
1683
|
-
"proxy-url": { type: "string" },
|
|
1684
|
-
"enclave-url": { type: "string" },
|
|
1685
|
-
kek: { type: "string" },
|
|
1686
|
-
port: { type: "string", default: "8000" },
|
|
1687
|
-
"no-attest": { type: "boolean", default: false }
|
|
1688
|
-
},
|
|
1689
|
-
strict: true
|
|
1690
|
-
});
|
|
1691
|
-
startServer({
|
|
1692
|
-
host: values["host"],
|
|
1693
|
-
proxyUrl: values["proxy-url"],
|
|
1694
|
-
enclaveUrl: values["enclave-url"],
|
|
1695
|
-
kek: values["kek"],
|
|
1696
|
-
port: Number(values["port"]),
|
|
1697
|
-
attest: !values["no-attest"]
|
|
1698
|
-
});
|