End-to-end encrypted OpenAI-compatible client with file upload and tools support, using XWing (ML-KEM768 + X25519) hybrid post-quantum encryption.
npm install @premai/pcci-sdk-tsEnd-to-end encrypted OpenAI-compatible client with file upload and tools support, using XWing (ML-KEM768 + X25519) hybrid post-quantum encryption.
``bash`
npm install
`typescript
import createRvencClient from "@premai/pcci-sdk-ts";
// Create client (encryption keys auto-generated)
const client = await createRvencClient({
apiKey: "your-api-key"
});
// Chat completion
const response = await client.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);
`
Run as a standalone server with automatic DEK store management per API key:
`bash`
npm startServer runs on http://localhost:3000
Use with any OpenAI-compatible client:
`bash`
curl http://localhost:3000/v1/chat/completions \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-oss-120b",
"messages": [
{"role": "user", "content": "Hello!"}
],
"stream": false
}'
Or use the OpenAI SDK in Node.js:
`typescript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "your-api-key",
baseURL: "http://localhost:3000/v1",
});
const stream = await client.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: "Count to 10" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || "");
}
`
The server caches clients in memory per API key for better performance.
- ✅ Chat Completions - OpenAI-compatible API with streaming support
- ✅ File Management - Upload, list, get, and delete encrypted files
- ✅ File Upload - Client-side encrypted file uploads with per-file DEKs
- ✅ Tools - Image generation, transcription, RAG search, and more
- ✅ End-to-end Encryption - Post-quantum cryptography (XWing)
- ✅ KEK Architecture - Key Encryption Key wraps all file DEKs
- ✅ RAG Support - Encrypted document retrieval with persistent RAG DEK
- ✅ TypeScript - Full type safety
`typescript
const response = await client.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);
`
`typescript
const stream = await client.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: "Count to 10" }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || "");
}
`
Use vision models to analyze images:
`typescript
const response = await client.chat.completions.create({
model: "OpenGVLab/InternVL3-38B",
messages: [{
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: {
url: "https://fastly.picsum.photos/id/237/200/300.jpg?hmac=TmmQSbShHz9CdQm0NkEjx1Dyh_Y984R9LpNrpvH2D_U",
},
},
],
}],
});
console.log(response.choices[0].message.content);
`
Transcribe audio files to text:
`typescript
import createRvencClient from "@premai/pcci-sdk-ts";
import fs from "fs";
const client = await createRvencClient({
apiKey: "your-api-key"
});
const transcription = await client.audio.transcriptions.create({
file: fs.createReadStream('./audio.wav'),
model: 'openai/whisper-large-v3',
});
console.log(transcription.text);
`
Translate audio files to English:
`typescript
const translation = await client.audio.translations.create({
file: fs.createReadStream('./audio.mp3'),
model: 'openai/whisper-large-v3',
});
console.log(translation.text);
`
> Note: openai/whisper-large-v3 model supports non-streaming only.
`typescript
import createRvencClient, { serializeDEKStore } from "@premai/pcci-sdk-ts";
import fs from "fs";
// Create client (DEK store auto-generated)
const client = await createRvencClient({
apiKey: "your-api-key",
clientKEK: "your-kek" // You can pass clientKEK here or as an env variable CLIENT_KEK
});
// Save DEK store for future use
const serializedStore = serializeDEKStore(client.dekStore);
fs.writeFileSync("./dek-store.json", serializedStore);
console.log("✅ DEK store saved. Keep this file secure!");
`
`typescript
import createRvencClient, { deserializeDEKStore } from "@premai/pcci-sdk-ts";
import fs from "fs";
// Load existing DEK store
const serializedStore = fs.readFileSync("./dek-store.json", "utf-8");
const dekStore = deserializeDEKStore(serializedStore);
// Create client with existing DEK store
const client = await createRvencClient({
apiKey: "your-api-key",
clientKEK: "your-kek" // You can pass clientKEK here or as an env variable CLIENT_KEK
dekStore,
});
`
`typescript
import fs from "fs";
import { serializeDEKStore } from "@premai/pcci-sdk-ts";
const fileContent = fs.readFileSync("./document.pdf");
const result = await client.files.upload({
file: new Uint8Array(fileContent),
fileName: "document.pdf",
mimeType: "application/pdf", // optional - auto-detected
});
console.log("Uploaded:", result.id);
// ⚠️ IMPORTANT: Save dekStore after upload to persist file DEKs
const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("./dek-store.json", serialized);
console.log("✅ DEK store updated with file encryption keys");
`
To enable RAG (Retrieval-Augmented Generation) indexing for a file, set ragIndex: true:
`typescript
const result = await client.files.upload({
file: new Uint8Array(fileContent),
fileName: "document.pdf",
mimeType: "application/pdf",
ragIndex: true, // Enable RAG indexing
});
// Save dekStore to persist both file DEK and RAG DEK
const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("./dek-store.json", serialized);
`
RAG Indexing Notes:
- When ragIndex: true, additional encryption keys are generated for RAG operationsragDEK
- A shared is created (or reused from dekStore) for all RAG-indexed filesdekStore.ragDEK
- The file DEK is encrypted with the RAG DEK for retrieval operations
- RAG DEK is automatically persisted in
Important Notes:
- Each file is encrypted with a unique random DEK
- The DEK is wrapped with your clientKEK and stored in dekStore.fileDEKs
- You must save the dekStore after uploading to persist file DEKs
- File DEKs are required for tools to process uploaded files
Retrieve a paginated list of your encrypted files with optional filtering:
`typescript
// List all files (default: 20 per page)
const result = await client.files.list();
console.log(result.files); // Array of FileMetadata
console.log(result.pagination); // { total, page, limit, pages }
// List with pagination
const page2 = await client.files.list({
limit: 50,
offset: 50,
});
// Filter by type
const images = await client.files.list({
type: 'image', // 'image' | 'document' | 'video' | 'audio' | 'archive' | 'general'
});
// Search by name
const searchResults = await client.files.list({
search: 'report',
});
// Filter by date range (must be ISO8601 format)
const recentFiles = await client.files.list({
from: '2024-01-01T00:00:00Z',
to: '2024-12-31T23:59:59Z',
});
// Or with timezone offset
const specificRange = await client.files.list({
from: '2024-01-01T00:00:00+00:00',
to: '2024-12-31T23:59:59+00:00',
});
// Combine filters
const filtered = await client.files.list({
limit: 100,
offset: 0,
type: 'document',
search: 'quarterly',
from: '2024-01-01',
to: '2024-12-31',
});
`
Retrieve detailed information about a specific file:
`typescript
// Get file metadata
const file = await client.files.get({ id: 'file-id-123' });
console.log(file.original_name);
console.log(file.file_size);
console.log(file.mime_type);
console.log(file.created_at);
// Get file with signed download URL
const fileWithUrl = await client.files.get({
id: 'file-id-123',
url: true // Include signed download URL
});
console.log(fileWithUrl.url); // Temporary signed URL for download
`
Delete an encrypted file:
`typescript
// Delete a file
await client.files.delete({
id: 'file-id-123'
});
// With error handling
try {
await client.files.delete({
id: 'file-id-123'
});
console.log('File deleted successfully');
} catch (error) {
console.error('Delete failed:', error.message);
}
`
Index existing files for RAG (Retrieval-Augmented Generation) search. This allows you to add files to your RAG index that were uploaded without ragIndex: true:
`typescript
// First, list your files to get their IDs and paths
const filesList = await client.files.list();
// Select files to index
const filesToIndex = filesList.files
.filter(f => f.type === 'document')
.map(f => ({
fileId: f.id,
filePath: f.file_path,
}));
// Index the files for RAG (uses DEKs from dekStore)
const indexResult = await client.files.index({
files: filesToIndex
});
// Check results
indexResult.data.results.forEach(result => {
if (result.success) {
console.log(✓ File ${result.file_id}: ${result.rag_status});✗ File ${result.file_id} failed: ${result.error}
} else {
console.error();`
}
});
Flexible DEK Management:
You can provide custom DEKs or let the function use DEKs from dekStore:
`typescript
// Option 1: Use DEKs from dekStore (default)
await client.files.index({
files: [
{ fileId: 'file-id-1', filePath: 's3/path/file1.enc' },
{ fileId: 'file-id-2', filePath: 's3/path/file2.enc' },
]
});
// Option 2: Provide custom ragDEK
await client.files.index({
files: [
{ fileId: 'file-id-1', filePath: 's3/path/file1.enc' },
],
ragDEK: customRagDEK // Override dekStore ragDEK
});
// Option 3: Provide custom fileDEK for specific files
await client.files.index({
files: [
{
fileId: 'file-id-1',
filePath: 's3/path/file1.enc',
fileDEK: customFileDEK // Custom DEK for this file
},
{
fileId: 'file-id-2',
filePath: 's3/path/file2.enc'
// Uses dekStore for this file
},
],
ragDEK: customRagDEK // Single ragDEK for all files
});
`
Important Notes:
- ragDEK: Checks options.ragDEK first, then dekStore.ragDEK. Throws error if not found.file.fileDEK
- fileDEK: For each file, checks first, then dekStore.fileDEKs. Throws error if not found.ragIndex: true
- To initialize RAG DEK in dekStore, upload at least one file with client.tools.searchRag()
- Once indexed, files can be searched using
Example Workflow:
`typescript
// Step 1: Upload first file with RAG to initialize RAG DEK
const firstFile = await client.files.upload({
file: new Uint8Array(fs.readFileSync('./doc1.pdf')),
fileName: 'doc1.pdf',
ragIndex: true, // Creates RAG DEK in dekStore
});
// Save dekStore to persist RAG DEK
fs.writeFileSync('./dek-store.json', serializeDEKStore(client.dekStore));
// Step 2: Upload other files without RAG indexing (faster)
const file2 = await client.files.upload({
file: new Uint8Array(fs.readFileSync('./doc2.pdf')),
fileName: 'doc2.pdf',
ragIndex: false, // Skip RAG indexing during upload
});
// Step 3: Later, index the files for RAG
const result = await client.files.index({
files: [
{ fileId: file2.id, filePath: file2.file_path },
]
});
console.log('Indexed:', result.data.results[0].rag_status); // "running"
`
Remove files from the RAG index without deleting the actual files:
`typescript
// Delete specific files from RAG index
const deleteResult = await client.files.deleteIndex({
fileIds: ['file-id-1', 'file-id-2', 'file-id-3']
});
// Check results
deleteResult.data.results.forEach(result => {
if (result.success) {
console.log(✓ File ${result.file_id} removed from index);✗ File ${result.file_id} failed: ${result.error}
} else {
console.error();`
}
});
With Custom RAG DEK:
`typescript`
// Use custom ragDEK instead of dekStore
await client.files.deleteIndex({
fileIds: ['file-id-1', 'file-id-2'],
ragDEK: customRagDEK // Optional: override dekStore ragDEK
});
Important Notes:
- This removes files from the RAG search index only
- The actual encrypted files remain in storage
- ragDEK: Checks options.ragDEK first, then dekStore.ragDEK. Throws error if not found.client.files.delete()
- Use to completely delete files
`typescript
interface FileUploadOptions {
file: Uint8Array; // File content as bytes
fileName: string; // Name of the file
mimeType?: string; // Optional MIME type (auto-detected from extension)
ragIndex?: boolean; // Optional: Enable RAG indexing for this file
}
interface ListFilesOptions {
limit?: number; // Max files to return (default: 20)
offset?: number; // Skip N files for pagination (default: 0)
search?: string; // Search term to filter by name
from?: string; // Minimum date filter (ISO8601: YYYY-MM-DDTHH:mm:ss+HH:mm or Z)
to?: string; // Maximum date filter (ISO8601: YYYY-MM-DDTHH:mm:ss+HH:mm or Z)
}
interface GetFileOptions {
id: string; // File ID (required)
url?: boolean; // Include signed download URL (default: false)
}
interface DeleteFileOptions {
id: string; // File ID (required)
}
interface IndexFileInput {
fileId: string; // File ID (required)
filePath: string; // S3/R2 path to encrypted file (required)
fileDEK?: Uint8Array; // Optional: custom file DEK (falls back to dekStore)
}
interface IndexFilesOptions {
files: IndexFileInput[]; // Array of files to index (required)
ragDEK?: Uint8Array; // Optional: custom RAG DEK for all files (falls back to dekStore)
}
interface DeleteIndexOptions {
fileIds: string[]; // Array of file IDs to remove from index (required)
ragDEK?: Uint8Array; // Optional: custom RAG DEK (falls back to dekStore)
}
`
All tools are encrypted end-to-end. Files are automatically downloaded and decrypted.
Generate files that are automatically decrypted and returned:
`typescript
// Generate an image
const image = await client.tools.generateImage({prompt: "sunset over mountains"});
console.log(image.fileName); // "generated_image.png"
console.log(image.content); // Uint8Array - save or use directly
fs.writeFileSync(image.fileName, image.content);
// Generate audio from text
const audio = await client.tools.audioGenerateFromText({text: "Hello, world!"});
fs.writeFileSync(audio.fileName, audio.content);
// Create a custom file
const file = await client.tools.createFileForUser(
{
fileName: 'test_file',
fileExtension: 'txt',
fileContent: 'This is the content of the test file.',
mimeType: 'text/plain'
}
);
fs.writeFileSync(file.fileName, file.content);
`
Process uploaded files and get results:
`typescript
// Upload a file first
const upload = await client.files.upload({
file: new Uint8Array(fs.readFileSync("./image.jpg")),
fileName: "image.jpg",
});
// Describe and caption image
const description = await client.tools.imageDescribeAndCaption({fileId: upload.id});
console.log(description);
// Extract PDF content
const pdfUpload = await client.files.upload({
file: new Uint8Array(fs.readFileSync("./doc.pdf")),
fileName: "doc.pdf",
});
const pdfContent = await client.tools.getPDFContent({fileId: pdfUpload.id});
console.log(pdfContent);
// Transcribe audio
const audioUpload = await client.files.upload({
file: new Uint8Array(fs.readFileSync("./audio.mp3")),
fileName: "audio.mp3",
});
const transcript = await client.tools.transcribeAudioToText({fileId: audioUpload.id});
console.log(transcript);
// Video description
const videoUpload = await client.files.upload({
file: new Uint8Array(fs.readFileSync("./video.mp4")),
fileName: "video.mp4",
});
const videoDesc = await client.tools.videoDescribeAndCaption({fileId: videoUpload.id});
console.log(videoDesc);
`
Tools that don't require file handling:
`typescript
// Get current time
const time = await client.tools.getTime({timezone: 'America/New_York'});
console.log(time);
// Web search
const searchResults = await client.tools.webSearchTool({query: 'latest AI news'});
console.log(searchResults);
// Web page scraper
const pageContent = await client.tools.webPageScraperTool({url: 'https://example.com', renderJs: false});
console.log(pageContent);
// RAG search across your uploaded files
const ragResults = await client.tools.searchRag({
query: 'test query',
});
console.log(ragResults);
`
- Generate images from text
- audioGenerateFromText(params: { text: string }) - Text-to-speech
- createFileForUser(params: { fileName: string, fileExtension: string, fileContent: string, mimeType: string }) - Create custom files$3
- imageDescribeAndCaption(params: { fileId: string }) - Describe images
- imageDescribeAndCaptionFallback(params: { fileId: string }) - Alternative image description
- videoDescribeAndCaption(params: { fileId: string }) - Describe videos
- getPDFContent(params: { fileId: string }) - Extract PDF text
- getTextDocumentContent(params: { fileId: string }) - Extract document text
- transcribeAudioToText(params: { fileId: string, language?: string }) - Audio transcription
- transcribeAudioWithDiarization(params: { fileId: string, language?: string }) - Transcription with speakers
- audioDiarization(params: { fileId: string }) - Identify speakers
- getFileContentOCR(params: { fileId: string }) - OCR on images
- getSpreadsheetContent(params: { fileId: string }) - Extract spreadsheet data
- getDataFileContent(params: { fileId: string }) - Extract data file content
- getPowerPointContent(params: { fileId: string, slideNumbers?: number[] }) - Extract presentation content$3
- getTime(params: { timezone: string }) - Get current time
- webSearchTool(params: { query: string, country?: string, searchLang?: string }) - Web search
- webPageScraperTool(params: { url: string, renderJs?: boolean }) - Scrape web pages$3
- searchRag(params: { query: string }) - Search indexed documents with optional file filteringConfiguration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
|
apiKey | string | required | Authorization token |
| encryptionKeys | EncryptionKeys | auto-generated | Pre-generated ML-KEM keys |
| dekStore | DEKStore | auto-generated | DEKs for files and RAG |
| requestTimeoutMs | number | 30000 | Request timeout in milliseconds |
| maxBufferSize | number | 10485760 | Max SSE buffer size (10MB) |DEK Store Management
The DEK store contains encryption keys for all file and RAG operations:
`typescript
interface DEKStore {
fileDEKs?: Map; // Per-file DEKs (fileId -> DEK)
ragDEK?: Uint8Array; // RAG operations DEK
ragVersion?: string; // RAG version identifier
}
`$3
`typescript
import { initializeDEKStore, serializeDEKStore } from "@premai/pcci-sdk-ts";// Create new DEK store with KEK from environment variable
const dekStore = initializeDEKStore();
// Save to secure storage
const serialized = serializeDEKStore(dekStore);
fs.writeFileSync("dek-store.json", serialized);
`$3
`typescript
import { serializeDEKStore } from "@premai/pcci-sdk-ts";const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("dek-store.json", serialized);
`$3
`typescript
import { deserializeDEKStore } from "@premai/pcci-sdk-ts";const serialized = fs.readFileSync("dek-store.json", "utf-8");
const dekStore = deserializeDEKStore(serialized);
`$3
`typescript
// After uploading files
const upload = await client.files.upload({...});// ⚠️ MUST save to persist file DEKs
const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("dek-store.json", serialized);
// After using RAG tools
await client.tools.searchRag({query: "..."});
// ⚠️ Save if ragDEK was auto-generated
const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("dek-store.json", serialized);
`How It Works
$3
1. Key Exchange: Fetches enclave public key → creates XWing encapsulation → shared secret
2. Request Encryption: Encrypts inference params with XChaCha20-Poly1305
3. Encrypted Transport: Sends
{ cipherText, encryptedInference, nonce } to proxy
4. Response Decryption: Decrypts response (streaming SSE or JSON) using shared secret$3
1. DEK Generation: Generates random 32-byte DEK for this file
2. File Encryption: Encrypts file content with XChaCha20-Poly1305 + managed nonce
3. Metadata Encryption: Encrypts filename and MIME type with same DEK
4. DEK Wrapping: Wraps DEK with
clientKEK using AES-KWP
5. Upload: Sends encrypted data with wrapped_dek and kid
6. Storage: Stores file DEK in dekStore.fileDEKs[fileId] for later use#### With RAG Indexing (
ragIndex: true)1. RAG DEK: Retrieves existing
ragDEK from dekStore or generates new 32-byte key
2. File DEK Encryption: Encrypts file DEK with RAG DEK (XChaCha20-Poly1305)
3. RAG DEK Encryption: Encrypts RAG DEK with clientKEK (XChaCha20-Poly1305)
4. Additional Payload: Includes encrypted_file_dek, encrypted_rag_dek, file_nonce, rag_dek_nonce, cipher_text
5. RAG Storage: Persists ragDEK in dekStore.ragDEK for reuse across files$3
#### File-Processing Tools
1. Retrieve fileDEK: Gets stored DEK from
dekStore.fileDEKs[fileId]
2. Encrypt fileDEK: Encrypts fileDEK with shared secret (per-request nonce)
3. Send Request: Sends encryptedFileDEK + fileDEKNonce to enclave
4. Enclave Decrypts: Enclave decrypts fileDEK, then decrypts file from S3
5. Response: Returns encrypted result, SDK decrypts with shared secret#### File-Producing Tools
1. Generate DEK: Creates random DEK for output file
2. Encrypt Request: Encrypts tool parameters with shared secret
3. Enclave Executes: Generates file, encrypts with provided DEK
4. Download: SDK downloads and decrypts file using the DEK
5. Response: Returns
DecryptedFile with content as Uint8Array#### RAG Tools
1. Retrieve fileDEKs: Gets DEKs for all specified files
2. Encrypt DEKs: Encrypts each fileDEK with shared secret (unique nonces)
3. Encrypt ragDEK: Encrypts persistent
ragDEK with shared secret
4. Send Request: Sends encryptedFileDEKs[] + ragDEK + ragVersion
5. Enclave Processing: Decrypts files, performs RAG search
6. Response: Returns encrypted resultsError Handling
`typescript
try {
const client = await createRvencClient({
apiKey: "your-api-key",
clientKEK: "your-kek" // You can pass clientKEK here or as an env variable CLIENT_KEK
});
const image = await client.tools.generateImage("mountain landscape");
fs.writeFileSync(image.fileName, image.content);
} catch (error) {
if (error.message.includes("timeout")) {
console.error("Request timed out");
} else {
console.error("Error:", error.message);
}
}
`Security Notes
- ⚠️ Use environment variables for API keys & client KEK
- ⚠️ Always persist dekStore after file uploads - file DEKs must be saved
- ⚠️ Backup your dekStore - losing it means losing access to uploaded files
- ✅ All encryption happens client-side (zero-knowledge)
- ✅ Files are encrypted with unique DEKs before upload
- ✅ KEK-based architecture - KEK wraps all file DEKs
- ✅ Tool responses are automatically decrypted
- ✅ Post-quantum secure (XWing: ML-KEM768 + X25519)
$3
`
clientKEK (32 bytes)
├── wraps fileDEK₁ → file_1.pdf
├── wraps fileDEK₂ → file_2.jpg
├── wraps fileDEK₃ → file_3.mp3
└── wraps fileDEKₙ → file_n.txtragDEK (32 bytes) → encrypts RAG index
Each file gets unique DEK, all wrapped by KEK
`$3
1. Initialize Once: Create dekStore once, reuse across sessions
2. Persist After Uploads: Save dekStore immediately after each file upload
3. Secure Storage: Store
dek-store.json in secure, encrypted location
4. Regular Backups: Backup dekStore to prevent data loss
5. Rotation: Generate new dekStore periodically and re-upload files
6. Error Handling: Always check if fileDEK exists before using toolsTypeScript Types
`typescript
import type {
RvencClient,
RvencClientOptions,
DEKStore,
EncryptionKeys,
FileUploadOptions,
UploadedFile,
DecryptedFile,
ToolsClient,
ListFilesOptions,
ListFilesResponse,
GetFileOptions,
DeleteFileOptions,
DeleteFileResponse,
IndexFileInput,
IndexFilesOptions,
IndexFilesResponse,
DeleteIndexOptions,
DeleteIndexResponse,
FileMetadata,
PaginationInfo,
} from "@premai/pcci-sdk-ts";
``