@premai/pcci-sdk-ts
v1.0.20
Published
End-to-end encrypted OpenAI-compatible client with file upload and tools support, using XWing (ML-KEM768 + X25519) hybrid post-quantum encryption.
Readme
RVENC Client
End-to-end encrypted OpenAI-compatible client with file upload and tools support, using XWing (ML-KEM768 + X25519) hybrid post-quantum encryption.
Installation
npm installQuick Start
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);OpenAI-Compatible API Server
Environment Variables:
- ENCLAVE_URL
- PROXY_URL
- CLIENT_KEK
- HOST (optional)
- PORT (optional)
Run as a standalone server with automatic DEK store management per API key:
# ways to run the termination proxy
## run command from local/remote NPM package
bunx -p @premai/pcci-sdk-ts pcci-proxy
npx -p @premai/pcci-sdk-ts pcci-proxy
## install globally
## make sure to have the "global bin dir" in your PATH
## `npm config get prefix`, for pure node runtime
npm i -g @premai/pcci-sdk-ts
bun i -g @premai/pcci-sdk-ts
# Server runs on http://localhost:3000Use with any OpenAI-compatible client:
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:
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.
Features
- ✅ Chat Completions - OpenAI-compatible API with streaming support
- ✅ Models - List available models
- ✅ 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
Chat Completions
Non-streaming
const response = await client.chat.completions.create({
model: "openai/gpt-oss-120b",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(response.choices[0].message.content);Streaming
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 || "");
}Vision
Use vision models to analyze images:
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);Audio
Transcriptions
Transcribe audio files to text:
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);Translations
Translate audio files to English:
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.
Models
List available models
const client = await createRvencClient({
apiKey: "your-api-key",
});
const models = await client.models.list({
type: "CHAT",
});File Upload
First Time Setup
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!");Subsequent Usage
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,
});Upload Files
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");Upload Files with RAG Indexing
To enable RAG (Retrieval-Augmented Generation) indexing for a file, set ragIndex: true:
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 operations - A shared
ragDEKis created (or reused from dekStore) for all RAG-indexed files - The file DEK is encrypted with the RAG DEK for retrieval operations
- RAG DEK is automatically persisted in
dekStore.ragDEK
Important Notes:
- Each file is encrypted with a unique random DEK
- The DEK is wrapped with your
clientKEKand stored indekStore.fileDEKs - You must save the dekStore after uploading to persist file DEKs
- File DEKs are required for tools to process uploaded files
List Files
Retrieve a paginated list of your encrypted files with optional filtering:
// 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',
});Get File Details
Retrieve detailed information about a specific file:
// 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 downloadDelete File
Delete an encrypted file:
// 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 Files for RAG
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:
// 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}`);
} else {
console.error(`✗ File ${result.file_id} failed: ${result.error}`);
}
});Flexible DEK Management:
You can provide custom DEKs or let the function use DEKs from dekStore:
// 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.ragDEKfirst, thendekStore.ragDEK. Throws error if not found. - fileDEK: For each file, checks
file.fileDEKfirst, thendekStore.fileDEKs. Throws error if not found. - To initialize RAG DEK in dekStore, upload at least one file with
ragIndex: true - Once indexed, files can be searched using
client.tools.searchRag()
Example Workflow:
// 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"Delete Files from RAG Index
Remove files from the RAG index without deleting the actual files:
// 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`);
} else {
console.error(`✗ File ${result.file_id} failed: ${result.error}`);
}
});With Custom RAG DEK:
// 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.ragDEKfirst, thendekStore.ragDEK. Throws error if not found. - Use
client.files.delete()to completely delete files
File Upload Options
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)
}Tools
All tools are encrypted end-to-end. Files are automatically downloaded and decrypted.
File-Producing Tools
Generate files that are automatically decrypted and returned:
// 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);File-Processing Tools
Process uploaded files and get results:
// 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);Simple Tools
Tools that don't require file handling:
// 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);Available Tools
File-Producing Tools
generateImage(params: { prompt: string })- Generate images from textaudioGenerateFromText(params: { text: string })- Text-to-speechcreateFileForUser(params: { fileName: string, fileExtension: string, fileContent: string, mimeType: string })- Create custom files
File-Processing Tools
imageDescribeAndCaption(params: { fileId: string })- Describe imagesimageDescribeAndCaptionFallback(params: { fileId: string })- Alternative image descriptionvideoDescribeAndCaption(params: { fileId: string })- Describe videosgetPDFContent(params: { fileId: string })- Extract PDF textgetTextDocumentContent(params: { fileId: string })- Extract document texttranscribeAudioToText(params: { fileId: string, language?: string })- Audio transcriptiontranscribeAudioWithDiarization(params: { fileId: string, language?: string })- Transcription with speakersaudioDiarization(params: { fileId: string })- Identify speakersgetFileContentOCR(params: { fileId: string })- OCR on imagesgetSpreadsheetContent(params: { fileId: string })- Extract spreadsheet datagetDataFileContent(params: { fileId: string })- Extract data file contentgetPowerPointContent(params: { fileId: string, slideNumbers?: number[] })- Extract presentation content
Simple Tools
getTime(params: { timezone: string })- Get current timewebSearchTool(params: { query: string, country?: string, searchLang?: string })- Web searchwebPageScraperTool(params: { url: string, renderJs?: boolean })- Scrape web pages
RAG Tools
searchRag(params: { query: string })- Search indexed documents with optional file filtering
Configuration 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:
interface DEKStore {
fileDEKs?: Map<string, Uint8Array>; // Per-file DEKs (fileId -> DEK)
ragDEK?: Uint8Array; // RAG operations DEK
ragVersion?: string; // RAG version identifier
}Initialize New DEK Store
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);Serialize for Storage
import { serializeDEKStore } from "@premai/pcci-sdk-ts";
const serialized = serializeDEKStore(client.dekStore);
fs.writeFileSync("dek-store.json", serialized);Deserialize from Storage
import { deserializeDEKStore } from "@premai/pcci-sdk-ts";
const serialized = fs.readFileSync("dek-store.json", "utf-8");
const dekStore = deserializeDEKStore(serialized);Important: Persist After File Operations
// 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
Chat Completions
- Key Exchange: Fetches enclave public key → creates XWing encapsulation → shared secret
- Request Encryption: Encrypts inference params with XChaCha20-Poly1305
- Encrypted Transport: Sends
{ cipherText, encryptedInference, nonce }to proxy - Response Decryption: Decrypts response (streaming SSE or JSON) using shared secret
File Upload
- DEK Generation: Generates random 32-byte DEK for this file
- File Encryption: Encrypts file content with XChaCha20-Poly1305 + managed nonce
- Metadata Encryption: Encrypts filename and MIME type with same DEK
- DEK Wrapping: Wraps DEK with
clientKEKusing AES-KWP - Upload: Sends encrypted data with
wrapped_dekandkid - Storage: Stores file DEK in
dekStore.fileDEKs[fileId]for later use
With RAG Indexing (ragIndex: true)
- RAG DEK: Retrieves existing
ragDEKfrom dekStore or generates new 32-byte key - File DEK Encryption: Encrypts file DEK with RAG DEK (XChaCha20-Poly1305)
- RAG DEK Encryption: Encrypts RAG DEK with
clientKEK(XChaCha20-Poly1305) - Additional Payload: Includes
encrypted_file_dek,encrypted_rag_dek,file_nonce,rag_dek_nonce,cipher_text - RAG Storage: Persists
ragDEKindekStore.ragDEKfor reuse across files
Tools
File-Processing Tools
- Retrieve fileDEK: Gets stored DEK from
dekStore.fileDEKs[fileId] - Encrypt fileDEK: Encrypts fileDEK with shared secret (per-request nonce)
- Send Request: Sends
encryptedFileDEK+fileDEKNonceto enclave - Enclave Decrypts: Enclave decrypts fileDEK, then decrypts file from S3
- Response: Returns encrypted result, SDK decrypts with shared secret
File-Producing Tools
- Generate DEK: Creates random DEK for output file
- Encrypt Request: Encrypts tool parameters with shared secret
- Enclave Executes: Generates file, encrypts with provided DEK
- Download: SDK downloads and decrypts file using the DEK
- Response: Returns
DecryptedFilewith content as Uint8Array
RAG Tools
- Retrieve fileDEKs: Gets DEKs for all specified files
- Encrypt DEKs: Encrypts each fileDEK with shared secret (unique nonces)
- Encrypt ragDEK: Encrypts persistent
ragDEKwith shared secret - Send Request: Sends
encryptedFileDEKs[]+ragDEK+ragVersion - Enclave Processing: Decrypts files, performs RAG search
- Response: Returns encrypted results
Error Handling
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)
Encryption Architecture
clientKEK (32 bytes)
├── wraps fileDEK₁ → file_1.pdf
├── wraps fileDEK₂ → file_2.jpg
├── wraps fileDEK₃ → file_3.mp3
└── wraps fileDEKₙ → file_n.txt
ragDEK (32 bytes) → encrypts RAG index
Each file gets unique DEK, all wrapped by KEKBest Practices
- Initialize Once: Create dekStore once, reuse across sessions
- Persist After Uploads: Save dekStore immediately after each file upload
- Secure Storage: Store
dek-store.jsonin secure, encrypted location - Regular Backups: Backup dekStore to prevent data loss
- Rotation: Generate new dekStore periodically and re-upload files
- Error Handling: Always check if fileDEK exists before using tools
TypeScript Types
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";