wolfronix-sdk 1.0.0 → 1.2.0

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 CHANGED
@@ -7,13 +7,26 @@ Official JavaScript/TypeScript SDK for Wolfronix - Zero-knowledge encryption mad
7
7
 
8
8
  ## Features
9
9
 
10
- - 🔐 **Zero-Knowledge Encryption** - 4-layer enterprise-grade security
10
+ - 🔐 **Zero-Knowledge Encryption** - Keys generated client-side, never leave your device
11
+ - 🏢 **Enterprise Ready** - Seamless integration with your existing storage
12
+
11
13
  - 🚀 **Simple API** - Encrypt files in 2 lines of code
12
14
  - 📦 **TypeScript Native** - Full type definitions included
13
15
  - 🌐 **Universal** - Works in Node.js and browsers
14
16
  - ⚡ **Streaming** - Handle large files with progress tracking
15
17
  - 🔄 **Auto Retry** - Built-in retry logic with exponential backoff
16
18
 
19
+ ## Backend Integration (Enterprise Mode)
20
+
21
+ To use this SDK, your backend API must implement 3 storage endpoints that Wolfronix will call:
22
+
23
+ 1. **POST** `/wolfronix/files/upload` - Store encrypted file + metadata
24
+ 2. **GET** `/wolfronix/files/{id}` - Retrieve metadata
25
+ 3. **GET** `/wolfronix/files/{id}/data` - Retrieve encrypted file blob
26
+
27
+ Wolfronix handles all encryption/decryption keys and logic; you only handle the encrypted blobs.
28
+
29
+
17
30
  ## Installation
18
31
 
19
32
  ```bash
@@ -32,10 +45,13 @@ import Wolfronix from '@wolfronix/sdk';
32
45
  // Initialize client
33
46
  const wfx = new Wolfronix({
34
47
  baseUrl: 'https://your-wolfronix-server:5002',
35
- clientId: 'your-client-id' // Optional for enterprise mode
48
+ clientId: 'your-enterprise-client-id'
36
49
  });
37
50
 
38
- // Login
51
+ // Register (First time only) - Generates keys client-side
52
+ await wfx.register('user@example.com', 'password123');
53
+
54
+ // Login (Subsequent visits)
39
55
  await wfx.login('user@example.com', 'password123');
40
56
 
41
57
  // Encrypt a file
@@ -61,8 +77,9 @@ const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
61
77
  if (!file) return;
62
78
 
63
79
  try {
80
+ // Keys are automatically handled by the SDK
64
81
  const { file_id } = await wfx.encrypt(file);
65
- console.log('File encrypted:', file_id);
82
+ console.log('File encrypted with your private key:', file_id);
66
83
  } catch (error) {
67
84
  console.error('Encryption failed:', error);
68
85
  }
package/dist/index.d.mts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Zero-knowledge encryption made simple
4
4
  *
5
5
  * @package @wolfronix/sdk
6
- * @version 1.0.0
6
+ * @version 1.2.0
7
7
  */
8
8
  interface WolfronixConfig {
9
9
  /** Wolfronix server base URL */
@@ -24,11 +24,10 @@ interface AuthResponse {
24
24
  message: string;
25
25
  }
26
26
  interface EncryptResponse {
27
- success: boolean;
28
- file_id: string;
29
- original_name: string;
30
- encrypted_size: number;
31
- message: string;
27
+ status: string;
28
+ file_id: number;
29
+ file_size: number;
30
+ enc_time_ms: number;
32
31
  }
33
32
  interface FileInfo {
34
33
  file_id: string;
@@ -52,10 +51,6 @@ interface MetricsResponse {
52
51
  total_bytes_encrypted: number;
53
52
  total_bytes_decrypted: number;
54
53
  }
55
- interface StreamToken {
56
- token: string;
57
- expires_at: string;
58
- }
59
54
  declare class WolfronixError extends Error {
60
55
  readonly code: string;
61
56
  readonly statusCode?: number;
@@ -82,6 +77,9 @@ declare class Wolfronix {
82
77
  private token;
83
78
  private userId;
84
79
  private tokenExpiry;
80
+ private publicKey;
81
+ private privateKey;
82
+ private publicKeyPEM;
85
83
  /**
86
84
  * Create a new Wolfronix client
87
85
  *
@@ -152,17 +150,6 @@ declare class Wolfronix {
152
150
  * ```
153
151
  */
154
152
  encrypt(file: File | Blob | ArrayBuffer | Uint8Array, filename?: string): Promise<EncryptResponse>;
155
- /**
156
- * Encrypt a file using streaming (for large files)
157
- *
158
- * @example
159
- * ```typescript
160
- * const result = await wfx.encryptStream(largeFile, (progress) => {
161
- * console.log(`Progress: ${progress}%`);
162
- * });
163
- * ```
164
- */
165
- encryptStream(file: File | Blob, onProgress?: (percent: number) => void): Promise<EncryptResponse>;
166
153
  /**
167
154
  * Decrypt and retrieve a file
168
155
  *
@@ -182,10 +169,6 @@ declare class Wolfronix {
182
169
  * Decrypt and return as ArrayBuffer
183
170
  */
184
171
  decryptToBuffer(fileId: string): Promise<ArrayBuffer>;
185
- /**
186
- * Decrypt using streaming (for large files)
187
- */
188
- decryptStream(fileId: string, onProgress?: (percent: number) => void): Promise<Blob>;
189
172
  /**
190
173
  * List all encrypted files for current user
191
174
  *
@@ -235,4 +218,4 @@ declare class Wolfronix {
235
218
  */
236
219
  declare function createClient(config: WolfronixConfig | string): Wolfronix;
237
220
 
238
- export { type AuthResponse, AuthenticationError, type DeleteResponse, type EncryptResponse, type FileInfo, FileNotFoundError, type ListFilesResponse, type MetricsResponse, NetworkError, PermissionDeniedError, type StreamToken, ValidationError, Wolfronix, type WolfronixConfig, WolfronixError, createClient, Wolfronix as default };
221
+ export { type AuthResponse, AuthenticationError, type DeleteResponse, type EncryptResponse, type FileInfo, FileNotFoundError, type ListFilesResponse, type MetricsResponse, NetworkError, PermissionDeniedError, ValidationError, Wolfronix, type WolfronixConfig, WolfronixError, createClient, Wolfronix as default };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * Zero-knowledge encryption made simple
4
4
  *
5
5
  * @package @wolfronix/sdk
6
- * @version 1.0.0
6
+ * @version 1.2.0
7
7
  */
8
8
  interface WolfronixConfig {
9
9
  /** Wolfronix server base URL */
@@ -24,11 +24,10 @@ interface AuthResponse {
24
24
  message: string;
25
25
  }
26
26
  interface EncryptResponse {
27
- success: boolean;
28
- file_id: string;
29
- original_name: string;
30
- encrypted_size: number;
31
- message: string;
27
+ status: string;
28
+ file_id: number;
29
+ file_size: number;
30
+ enc_time_ms: number;
32
31
  }
33
32
  interface FileInfo {
34
33
  file_id: string;
@@ -52,10 +51,6 @@ interface MetricsResponse {
52
51
  total_bytes_encrypted: number;
53
52
  total_bytes_decrypted: number;
54
53
  }
55
- interface StreamToken {
56
- token: string;
57
- expires_at: string;
58
- }
59
54
  declare class WolfronixError extends Error {
60
55
  readonly code: string;
61
56
  readonly statusCode?: number;
@@ -82,6 +77,9 @@ declare class Wolfronix {
82
77
  private token;
83
78
  private userId;
84
79
  private tokenExpiry;
80
+ private publicKey;
81
+ private privateKey;
82
+ private publicKeyPEM;
85
83
  /**
86
84
  * Create a new Wolfronix client
87
85
  *
@@ -152,17 +150,6 @@ declare class Wolfronix {
152
150
  * ```
153
151
  */
154
152
  encrypt(file: File | Blob | ArrayBuffer | Uint8Array, filename?: string): Promise<EncryptResponse>;
155
- /**
156
- * Encrypt a file using streaming (for large files)
157
- *
158
- * @example
159
- * ```typescript
160
- * const result = await wfx.encryptStream(largeFile, (progress) => {
161
- * console.log(`Progress: ${progress}%`);
162
- * });
163
- * ```
164
- */
165
- encryptStream(file: File | Blob, onProgress?: (percent: number) => void): Promise<EncryptResponse>;
166
153
  /**
167
154
  * Decrypt and retrieve a file
168
155
  *
@@ -182,10 +169,6 @@ declare class Wolfronix {
182
169
  * Decrypt and return as ArrayBuffer
183
170
  */
184
171
  decryptToBuffer(fileId: string): Promise<ArrayBuffer>;
185
- /**
186
- * Decrypt using streaming (for large files)
187
- */
188
- decryptStream(fileId: string, onProgress?: (percent: number) => void): Promise<Blob>;
189
172
  /**
190
173
  * List all encrypted files for current user
191
174
  *
@@ -235,4 +218,4 @@ declare class Wolfronix {
235
218
  */
236
219
  declare function createClient(config: WolfronixConfig | string): Wolfronix;
237
220
 
238
- export { type AuthResponse, AuthenticationError, type DeleteResponse, type EncryptResponse, type FileInfo, FileNotFoundError, type ListFilesResponse, type MetricsResponse, NetworkError, PermissionDeniedError, type StreamToken, ValidationError, Wolfronix, type WolfronixConfig, WolfronixError, createClient, Wolfronix as default };
221
+ export { type AuthResponse, AuthenticationError, type DeleteResponse, type EncryptResponse, type FileInfo, FileNotFoundError, type ListFilesResponse, type MetricsResponse, NetworkError, PermissionDeniedError, ValidationError, Wolfronix, type WolfronixConfig, WolfronixError, createClient, Wolfronix as default };
package/dist/index.js CHANGED
@@ -31,6 +31,146 @@ __export(index_exports, {
31
31
  default: () => index_default
32
32
  });
33
33
  module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/crypto.ts
36
+ var RSA_ALG = {
37
+ name: "RSA-OAEP",
38
+ modulusLength: 2048,
39
+ publicExponent: new Uint8Array([1, 0, 1]),
40
+ hash: "SHA-256"
41
+ };
42
+ var WRAP_ALG = "AES-GCM";
43
+ var PBKDF2_ITERATIONS = 1e5;
44
+ async function generateKeyPair() {
45
+ return await window.crypto.subtle.generateKey(
46
+ RSA_ALG,
47
+ true,
48
+ // extractable
49
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
50
+ );
51
+ }
52
+ async function exportKeyToPEM(key, type) {
53
+ const format = type === "public" ? "spki" : "pkcs8";
54
+ const exported = await window.crypto.subtle.exportKey(format, key);
55
+ const exportedAsBase64 = arrayBufferToBase64(exported);
56
+ const pemHeader = type === "public" ? "-----BEGIN PUBLIC KEY-----" : "-----BEGIN PRIVATE KEY-----";
57
+ const pemFooter = type === "public" ? "-----END PUBLIC KEY-----" : "-----END PRIVATE KEY-----";
58
+ return `${pemHeader}
59
+ ${exportedAsBase64}
60
+ ${pemFooter}`;
61
+ }
62
+ async function importKeyFromPEM(pem, type) {
63
+ const pemHeader = type === "public" ? "-----BEGIN PUBLIC KEY-----" : "-----BEGIN PRIVATE KEY-----";
64
+ const pemFooter = type === "public" ? "-----END PUBLIC KEY-----" : "-----END PRIVATE KEY-----";
65
+ const pemContents = pem.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
66
+ const binaryDer = base64ToArrayBuffer(pemContents);
67
+ const format = type === "public" ? "spki" : "pkcs8";
68
+ const usage = type === "public" ? ["encrypt", "wrapKey"] : ["decrypt", "unwrapKey"];
69
+ return await window.crypto.subtle.importKey(
70
+ format,
71
+ binaryDer,
72
+ RSA_ALG,
73
+ true,
74
+ usage
75
+ );
76
+ }
77
+ async function deriveWrappingKey(password, saltHex) {
78
+ const enc = new TextEncoder();
79
+ const passwordKey = await window.crypto.subtle.importKey(
80
+ "raw",
81
+ enc.encode(password),
82
+ "PBKDF2",
83
+ false,
84
+ ["deriveKey"]
85
+ );
86
+ const salt = hexToArrayBuffer(saltHex);
87
+ return await window.crypto.subtle.deriveKey(
88
+ {
89
+ name: "PBKDF2",
90
+ salt,
91
+ iterations: PBKDF2_ITERATIONS,
92
+ hash: "SHA-256"
93
+ },
94
+ passwordKey,
95
+ { name: WRAP_ALG, length: 256 },
96
+ false,
97
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
98
+ );
99
+ }
100
+ async function wrapPrivateKey(privateKey, password) {
101
+ const salt = window.crypto.getRandomValues(new Uint8Array(16));
102
+ const saltHex = arrayBufferToHex(salt.buffer);
103
+ const wrappingKey = await deriveWrappingKey(password, saltHex);
104
+ const exportedKey = await window.crypto.subtle.exportKey("pkcs8", privateKey);
105
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
106
+ const encryptedContent = await window.crypto.subtle.encrypt(
107
+ {
108
+ name: WRAP_ALG,
109
+ iv
110
+ },
111
+ wrappingKey,
112
+ exportedKey
113
+ );
114
+ const combined = new Uint8Array(iv.length + encryptedContent.byteLength);
115
+ combined.set(iv);
116
+ combined.set(new Uint8Array(encryptedContent), iv.length);
117
+ return {
118
+ encryptedKey: arrayBufferToBase64(combined.buffer),
119
+ salt: saltHex
120
+ };
121
+ }
122
+ async function unwrapPrivateKey(encryptedKeyBase64, password, saltHex) {
123
+ const combined = base64ToArrayBuffer(encryptedKeyBase64);
124
+ const combinedArray = new Uint8Array(combined);
125
+ const iv = combinedArray.slice(0, 12);
126
+ const data = combinedArray.slice(12);
127
+ const wrappingKey = await deriveWrappingKey(password, saltHex);
128
+ const decryptedKeyData = await window.crypto.subtle.decrypt(
129
+ {
130
+ name: WRAP_ALG,
131
+ iv
132
+ },
133
+ wrappingKey,
134
+ data
135
+ );
136
+ return await window.crypto.subtle.importKey(
137
+ "pkcs8",
138
+ decryptedKeyData,
139
+ RSA_ALG,
140
+ true,
141
+ ["decrypt", "unwrapKey"]
142
+ );
143
+ }
144
+ function arrayBufferToBase64(buffer) {
145
+ let binary = "";
146
+ const bytes = new Uint8Array(buffer);
147
+ const len = bytes.byteLength;
148
+ for (let i = 0; i < len; i++) {
149
+ binary += String.fromCharCode(bytes[i]);
150
+ }
151
+ return window.btoa(binary);
152
+ }
153
+ function base64ToArrayBuffer(base64) {
154
+ const binary_string = window.atob(base64);
155
+ const len = binary_string.length;
156
+ const bytes = new Uint8Array(len);
157
+ for (let i = 0; i < len; i++) {
158
+ bytes[i] = binary_string.charCodeAt(i);
159
+ }
160
+ return bytes.buffer;
161
+ }
162
+ function arrayBufferToHex(buffer) {
163
+ return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("");
164
+ }
165
+ function hexToArrayBuffer(hex) {
166
+ const bytes = new Uint8Array(hex.length / 2);
167
+ for (let i = 0; i < hex.length; i += 2) {
168
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
169
+ }
170
+ return bytes.buffer;
171
+ }
172
+
173
+ // src/index.ts
34
174
  var WolfronixError = class extends Error {
35
175
  constructor(message, code, statusCode, details) {
36
176
  super(message);
@@ -86,6 +226,10 @@ var Wolfronix = class {
86
226
  this.token = null;
87
227
  this.userId = null;
88
228
  this.tokenExpiry = null;
229
+ // Client-side keys (never stored on server in raw form)
230
+ this.publicKey = null;
231
+ this.privateKey = null;
232
+ this.publicKeyPEM = null;
89
233
  if (typeof config === "string") {
90
234
  this.config = {
91
235
  baseUrl: config,
@@ -117,13 +261,16 @@ var Wolfronix = class {
117
261
  }
118
262
  if (includeAuth && this.token) {
119
263
  headers["Authorization"] = `Bearer ${this.token}`;
264
+ if (this.userId) {
265
+ headers["X-User-ID"] = this.userId;
266
+ }
120
267
  }
121
268
  return headers;
122
269
  }
123
270
  async request(method, endpoint, options = {}) {
124
- const { body, formData, includeAuth = true, responseType = "json" } = options;
271
+ const { body, formData, includeAuth = true, responseType = "json", headers: extraHeaders } = options;
125
272
  const url = `${this.config.baseUrl}${endpoint}`;
126
- const headers = this.getHeaders(includeAuth);
273
+ const headers = { ...this.getHeaders(includeAuth), ...extraHeaders };
127
274
  if (body && !formData) {
128
275
  headers["Content-Type"] = "application/json";
129
276
  }
@@ -205,14 +352,26 @@ var Wolfronix = class {
205
352
  if (!email || !password) {
206
353
  throw new ValidationError("Email and password are required");
207
354
  }
208
- const response = await this.request("POST", "/api/v1/register", {
209
- body: { email, password },
355
+ const keyPair = await generateKeyPair();
356
+ const publicKeyPEM = await exportKeyToPEM(keyPair.publicKey, "public");
357
+ const { encryptedKey, salt } = await wrapPrivateKey(keyPair.privateKey, password);
358
+ const response = await this.request("POST", "/api/v1/keys/register", {
359
+ body: {
360
+ client_id: this.config.clientId,
361
+ user_id: email,
362
+ // Using email as user_id for simplicity
363
+ public_key_pem: publicKeyPEM,
364
+ encrypted_private_key: encryptedKey,
365
+ salt
366
+ },
210
367
  includeAuth: false
211
368
  });
212
369
  if (response.success) {
213
- this.token = response.token;
214
- this.userId = response.user_id;
215
- this.tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1e3);
370
+ this.userId = email;
371
+ this.publicKey = keyPair.publicKey;
372
+ this.privateKey = keyPair.privateKey;
373
+ this.publicKeyPEM = publicKeyPEM;
374
+ this.token = "session_" + Date.now();
216
375
  }
217
376
  return response;
218
377
  }
@@ -228,16 +387,35 @@ var Wolfronix = class {
228
387
  if (!email || !password) {
229
388
  throw new ValidationError("Email and password are required");
230
389
  }
231
- const response = await this.request("POST", "/api/v1/login", {
232
- body: { email, password },
390
+ const response = await this.request("POST", "/api/v1/keys/login", {
391
+ body: {
392
+ client_id: this.config.clientId,
393
+ user_id: email
394
+ },
233
395
  includeAuth: false
234
396
  });
235
- if (response.success) {
236
- this.token = response.token;
237
- this.userId = response.user_id;
238
- this.tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1e3);
397
+ if (!response.encrypted_private_key || !response.salt) {
398
+ throw new AuthenticationError("Invalid credentials or keys not found");
399
+ }
400
+ try {
401
+ this.privateKey = await unwrapPrivateKey(
402
+ response.encrypted_private_key,
403
+ password,
404
+ response.salt
405
+ );
406
+ this.publicKeyPEM = response.public_key_pem;
407
+ this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
408
+ this.userId = email;
409
+ this.token = "session_" + Date.now();
410
+ return {
411
+ success: true,
412
+ user_id: email,
413
+ token: this.token,
414
+ message: "Logged in successfully"
415
+ };
416
+ } catch (err) {
417
+ throw new AuthenticationError("Invalid password (decryption failed)");
239
418
  }
240
- return response;
241
419
  }
242
420
  /**
243
421
  * Set authentication token directly (useful for server-side apps)
@@ -259,6 +437,9 @@ var Wolfronix = class {
259
437
  this.token = null;
260
438
  this.userId = null;
261
439
  this.tokenExpiry = null;
440
+ this.publicKey = null;
441
+ this.privateKey = null;
442
+ this.publicKeyPEM = null;
262
443
  }
263
444
  /**
264
445
  * Check if client is authenticated
@@ -307,57 +488,11 @@ var Wolfronix = class {
307
488
  throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
308
489
  }
309
490
  formData.append("user_id", this.userId || "");
310
- return this.request("POST", "/api/v1/encrypt", {
311
- formData
312
- });
313
- }
314
- /**
315
- * Encrypt a file using streaming (for large files)
316
- *
317
- * @example
318
- * ```typescript
319
- * const result = await wfx.encryptStream(largeFile, (progress) => {
320
- * console.log(`Progress: ${progress}%`);
321
- * });
322
- * ```
323
- */
324
- async encryptStream(file, onProgress) {
325
- this.ensureAuthenticated();
326
- const tokenResponse = await this.request("POST", "/api/v1/stream/token", {
327
- body: {
328
- user_id: this.userId,
329
- client_id: this.config.clientId
330
- }
331
- });
332
- const formData = new FormData();
333
- formData.append("file", file);
334
- formData.append("user_id", this.userId || "");
335
- formData.append("stream_token", tokenResponse.token);
336
- if (onProgress && typeof XMLHttpRequest !== "undefined") {
337
- return new Promise((resolve, reject) => {
338
- const xhr = new XMLHttpRequest();
339
- xhr.upload.onprogress = (event) => {
340
- if (event.lengthComputable) {
341
- onProgress(Math.round(event.loaded / event.total * 100));
342
- }
343
- };
344
- xhr.onload = () => {
345
- if (xhr.status >= 200 && xhr.status < 300) {
346
- resolve(JSON.parse(xhr.responseText));
347
- } else {
348
- reject(new WolfronixError("Upload failed", "UPLOAD_ERROR", xhr.status));
349
- }
350
- };
351
- xhr.onerror = () => reject(new NetworkError("Upload failed"));
352
- xhr.open("POST", `${this.config.baseUrl}/api/v1/stream/encrypt`);
353
- const headers = this.getHeaders();
354
- Object.entries(headers).forEach(([key, value]) => {
355
- xhr.setRequestHeader(key, value);
356
- });
357
- xhr.send(formData);
358
- });
491
+ if (!this.publicKeyPEM) {
492
+ throw new Error("Public key not available. Is user logged in?");
359
493
  }
360
- return this.request("POST", "/api/v1/stream/encrypt", {
494
+ formData.append("client_public_key", this.publicKeyPEM);
495
+ return this.request("POST", "/api/v1/encrypt", {
361
496
  formData
362
497
  });
363
498
  }
@@ -380,8 +515,16 @@ var Wolfronix = class {
380
515
  if (!fileId) {
381
516
  throw new ValidationError("File ID is required");
382
517
  }
383
- return this.request("GET", `/api/v1/decrypt/${fileId}`, {
384
- responseType: "blob"
518
+ if (!this.privateKey) {
519
+ throw new Error("Private key not available. Is user logged in?");
520
+ }
521
+ const privateKeyPEM = await exportKeyToPEM(this.privateKey, "private");
522
+ return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
523
+ responseType: "blob",
524
+ headers: {
525
+ "X-Private-Key": privateKeyPEM,
526
+ "X-User-Role": "owner"
527
+ }
385
528
  });
386
529
  }
387
530
  /**
@@ -392,42 +535,16 @@ var Wolfronix = class {
392
535
  if (!fileId) {
393
536
  throw new ValidationError("File ID is required");
394
537
  }
395
- return this.request("GET", `/api/v1/decrypt/${fileId}`, {
396
- responseType: "arraybuffer"
397
- });
398
- }
399
- /**
400
- * Decrypt using streaming (for large files)
401
- */
402
- async decryptStream(fileId, onProgress) {
403
- this.ensureAuthenticated();
404
- if (onProgress && typeof XMLHttpRequest !== "undefined") {
405
- return new Promise((resolve, reject) => {
406
- const xhr = new XMLHttpRequest();
407
- xhr.responseType = "blob";
408
- xhr.onprogress = (event) => {
409
- if (event.lengthComputable) {
410
- onProgress(Math.round(event.loaded / event.total * 100));
411
- }
412
- };
413
- xhr.onload = () => {
414
- if (xhr.status >= 200 && xhr.status < 300) {
415
- resolve(xhr.response);
416
- } else {
417
- reject(new WolfronixError("Download failed", "DOWNLOAD_ERROR", xhr.status));
418
- }
419
- };
420
- xhr.onerror = () => reject(new NetworkError("Download failed"));
421
- xhr.open("GET", `${this.config.baseUrl}/api/v1/stream/decrypt/${fileId}`);
422
- const headers = this.getHeaders();
423
- Object.entries(headers).forEach(([key, value]) => {
424
- xhr.setRequestHeader(key, value);
425
- });
426
- xhr.send();
427
- });
538
+ if (!this.privateKey) {
539
+ throw new Error("Private key not available. Is user logged in?");
428
540
  }
429
- return this.request("GET", `/api/v1/stream/decrypt/${fileId}`, {
430
- responseType: "blob"
541
+ const privateKeyPEM = await exportKeyToPEM(this.privateKey, "private");
542
+ return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
543
+ responseType: "arraybuffer",
544
+ headers: {
545
+ "X-Private-Key": privateKeyPEM,
546
+ "X-User-Role": "owner"
547
+ }
431
548
  });
432
549
  }
433
550
  /**
@@ -441,7 +558,17 @@ var Wolfronix = class {
441
558
  */
442
559
  async listFiles() {
443
560
  this.ensureAuthenticated();
444
- return this.request("GET", "/api/v1/files");
561
+ const files = await this.request("GET", "/api/v1/files");
562
+ return {
563
+ success: true,
564
+ files: (files || []).map((f) => ({
565
+ file_id: f.id,
566
+ original_name: f.name,
567
+ encrypted_size: f.size_bytes,
568
+ created_at: f.date
569
+ })),
570
+ total: (files || []).length
571
+ };
445
572
  }
446
573
  /**
447
574
  * Delete an encrypted file
@@ -472,7 +599,7 @@ var Wolfronix = class {
472
599
  */
473
600
  async getMetrics() {
474
601
  this.ensureAuthenticated();
475
- return this.request("GET", "/api/v1/metrics");
602
+ return this.request("GET", "/api/v1/metrics/summary");
476
603
  }
477
604
  /**
478
605
  * Check if server is healthy
package/dist/index.mjs CHANGED
@@ -1,3 +1,141 @@
1
+ // src/crypto.ts
2
+ var RSA_ALG = {
3
+ name: "RSA-OAEP",
4
+ modulusLength: 2048,
5
+ publicExponent: new Uint8Array([1, 0, 1]),
6
+ hash: "SHA-256"
7
+ };
8
+ var WRAP_ALG = "AES-GCM";
9
+ var PBKDF2_ITERATIONS = 1e5;
10
+ async function generateKeyPair() {
11
+ return await window.crypto.subtle.generateKey(
12
+ RSA_ALG,
13
+ true,
14
+ // extractable
15
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
16
+ );
17
+ }
18
+ async function exportKeyToPEM(key, type) {
19
+ const format = type === "public" ? "spki" : "pkcs8";
20
+ const exported = await window.crypto.subtle.exportKey(format, key);
21
+ const exportedAsBase64 = arrayBufferToBase64(exported);
22
+ const pemHeader = type === "public" ? "-----BEGIN PUBLIC KEY-----" : "-----BEGIN PRIVATE KEY-----";
23
+ const pemFooter = type === "public" ? "-----END PUBLIC KEY-----" : "-----END PRIVATE KEY-----";
24
+ return `${pemHeader}
25
+ ${exportedAsBase64}
26
+ ${pemFooter}`;
27
+ }
28
+ async function importKeyFromPEM(pem, type) {
29
+ const pemHeader = type === "public" ? "-----BEGIN PUBLIC KEY-----" : "-----BEGIN PRIVATE KEY-----";
30
+ const pemFooter = type === "public" ? "-----END PUBLIC KEY-----" : "-----END PRIVATE KEY-----";
31
+ const pemContents = pem.replace(pemHeader, "").replace(pemFooter, "").replace(/\s/g, "");
32
+ const binaryDer = base64ToArrayBuffer(pemContents);
33
+ const format = type === "public" ? "spki" : "pkcs8";
34
+ const usage = type === "public" ? ["encrypt", "wrapKey"] : ["decrypt", "unwrapKey"];
35
+ return await window.crypto.subtle.importKey(
36
+ format,
37
+ binaryDer,
38
+ RSA_ALG,
39
+ true,
40
+ usage
41
+ );
42
+ }
43
+ async function deriveWrappingKey(password, saltHex) {
44
+ const enc = new TextEncoder();
45
+ const passwordKey = await window.crypto.subtle.importKey(
46
+ "raw",
47
+ enc.encode(password),
48
+ "PBKDF2",
49
+ false,
50
+ ["deriveKey"]
51
+ );
52
+ const salt = hexToArrayBuffer(saltHex);
53
+ return await window.crypto.subtle.deriveKey(
54
+ {
55
+ name: "PBKDF2",
56
+ salt,
57
+ iterations: PBKDF2_ITERATIONS,
58
+ hash: "SHA-256"
59
+ },
60
+ passwordKey,
61
+ { name: WRAP_ALG, length: 256 },
62
+ false,
63
+ ["encrypt", "decrypt", "wrapKey", "unwrapKey"]
64
+ );
65
+ }
66
+ async function wrapPrivateKey(privateKey, password) {
67
+ const salt = window.crypto.getRandomValues(new Uint8Array(16));
68
+ const saltHex = arrayBufferToHex(salt.buffer);
69
+ const wrappingKey = await deriveWrappingKey(password, saltHex);
70
+ const exportedKey = await window.crypto.subtle.exportKey("pkcs8", privateKey);
71
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
72
+ const encryptedContent = await window.crypto.subtle.encrypt(
73
+ {
74
+ name: WRAP_ALG,
75
+ iv
76
+ },
77
+ wrappingKey,
78
+ exportedKey
79
+ );
80
+ const combined = new Uint8Array(iv.length + encryptedContent.byteLength);
81
+ combined.set(iv);
82
+ combined.set(new Uint8Array(encryptedContent), iv.length);
83
+ return {
84
+ encryptedKey: arrayBufferToBase64(combined.buffer),
85
+ salt: saltHex
86
+ };
87
+ }
88
+ async function unwrapPrivateKey(encryptedKeyBase64, password, saltHex) {
89
+ const combined = base64ToArrayBuffer(encryptedKeyBase64);
90
+ const combinedArray = new Uint8Array(combined);
91
+ const iv = combinedArray.slice(0, 12);
92
+ const data = combinedArray.slice(12);
93
+ const wrappingKey = await deriveWrappingKey(password, saltHex);
94
+ const decryptedKeyData = await window.crypto.subtle.decrypt(
95
+ {
96
+ name: WRAP_ALG,
97
+ iv
98
+ },
99
+ wrappingKey,
100
+ data
101
+ );
102
+ return await window.crypto.subtle.importKey(
103
+ "pkcs8",
104
+ decryptedKeyData,
105
+ RSA_ALG,
106
+ true,
107
+ ["decrypt", "unwrapKey"]
108
+ );
109
+ }
110
+ function arrayBufferToBase64(buffer) {
111
+ let binary = "";
112
+ const bytes = new Uint8Array(buffer);
113
+ const len = bytes.byteLength;
114
+ for (let i = 0; i < len; i++) {
115
+ binary += String.fromCharCode(bytes[i]);
116
+ }
117
+ return window.btoa(binary);
118
+ }
119
+ function base64ToArrayBuffer(base64) {
120
+ const binary_string = window.atob(base64);
121
+ const len = binary_string.length;
122
+ const bytes = new Uint8Array(len);
123
+ for (let i = 0; i < len; i++) {
124
+ bytes[i] = binary_string.charCodeAt(i);
125
+ }
126
+ return bytes.buffer;
127
+ }
128
+ function arrayBufferToHex(buffer) {
129
+ return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, "0")).join("");
130
+ }
131
+ function hexToArrayBuffer(hex) {
132
+ const bytes = new Uint8Array(hex.length / 2);
133
+ for (let i = 0; i < hex.length; i += 2) {
134
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
135
+ }
136
+ return bytes.buffer;
137
+ }
138
+
1
139
  // src/index.ts
2
140
  var WolfronixError = class extends Error {
3
141
  constructor(message, code, statusCode, details) {
@@ -54,6 +192,10 @@ var Wolfronix = class {
54
192
  this.token = null;
55
193
  this.userId = null;
56
194
  this.tokenExpiry = null;
195
+ // Client-side keys (never stored on server in raw form)
196
+ this.publicKey = null;
197
+ this.privateKey = null;
198
+ this.publicKeyPEM = null;
57
199
  if (typeof config === "string") {
58
200
  this.config = {
59
201
  baseUrl: config,
@@ -85,13 +227,16 @@ var Wolfronix = class {
85
227
  }
86
228
  if (includeAuth && this.token) {
87
229
  headers["Authorization"] = `Bearer ${this.token}`;
230
+ if (this.userId) {
231
+ headers["X-User-ID"] = this.userId;
232
+ }
88
233
  }
89
234
  return headers;
90
235
  }
91
236
  async request(method, endpoint, options = {}) {
92
- const { body, formData, includeAuth = true, responseType = "json" } = options;
237
+ const { body, formData, includeAuth = true, responseType = "json", headers: extraHeaders } = options;
93
238
  const url = `${this.config.baseUrl}${endpoint}`;
94
- const headers = this.getHeaders(includeAuth);
239
+ const headers = { ...this.getHeaders(includeAuth), ...extraHeaders };
95
240
  if (body && !formData) {
96
241
  headers["Content-Type"] = "application/json";
97
242
  }
@@ -173,14 +318,26 @@ var Wolfronix = class {
173
318
  if (!email || !password) {
174
319
  throw new ValidationError("Email and password are required");
175
320
  }
176
- const response = await this.request("POST", "/api/v1/register", {
177
- body: { email, password },
321
+ const keyPair = await generateKeyPair();
322
+ const publicKeyPEM = await exportKeyToPEM(keyPair.publicKey, "public");
323
+ const { encryptedKey, salt } = await wrapPrivateKey(keyPair.privateKey, password);
324
+ const response = await this.request("POST", "/api/v1/keys/register", {
325
+ body: {
326
+ client_id: this.config.clientId,
327
+ user_id: email,
328
+ // Using email as user_id for simplicity
329
+ public_key_pem: publicKeyPEM,
330
+ encrypted_private_key: encryptedKey,
331
+ salt
332
+ },
178
333
  includeAuth: false
179
334
  });
180
335
  if (response.success) {
181
- this.token = response.token;
182
- this.userId = response.user_id;
183
- this.tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1e3);
336
+ this.userId = email;
337
+ this.publicKey = keyPair.publicKey;
338
+ this.privateKey = keyPair.privateKey;
339
+ this.publicKeyPEM = publicKeyPEM;
340
+ this.token = "session_" + Date.now();
184
341
  }
185
342
  return response;
186
343
  }
@@ -196,16 +353,35 @@ var Wolfronix = class {
196
353
  if (!email || !password) {
197
354
  throw new ValidationError("Email and password are required");
198
355
  }
199
- const response = await this.request("POST", "/api/v1/login", {
200
- body: { email, password },
356
+ const response = await this.request("POST", "/api/v1/keys/login", {
357
+ body: {
358
+ client_id: this.config.clientId,
359
+ user_id: email
360
+ },
201
361
  includeAuth: false
202
362
  });
203
- if (response.success) {
204
- this.token = response.token;
205
- this.userId = response.user_id;
206
- this.tokenExpiry = new Date(Date.now() + 24 * 60 * 60 * 1e3);
363
+ if (!response.encrypted_private_key || !response.salt) {
364
+ throw new AuthenticationError("Invalid credentials or keys not found");
365
+ }
366
+ try {
367
+ this.privateKey = await unwrapPrivateKey(
368
+ response.encrypted_private_key,
369
+ password,
370
+ response.salt
371
+ );
372
+ this.publicKeyPEM = response.public_key_pem;
373
+ this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
374
+ this.userId = email;
375
+ this.token = "session_" + Date.now();
376
+ return {
377
+ success: true,
378
+ user_id: email,
379
+ token: this.token,
380
+ message: "Logged in successfully"
381
+ };
382
+ } catch (err) {
383
+ throw new AuthenticationError("Invalid password (decryption failed)");
207
384
  }
208
- return response;
209
385
  }
210
386
  /**
211
387
  * Set authentication token directly (useful for server-side apps)
@@ -227,6 +403,9 @@ var Wolfronix = class {
227
403
  this.token = null;
228
404
  this.userId = null;
229
405
  this.tokenExpiry = null;
406
+ this.publicKey = null;
407
+ this.privateKey = null;
408
+ this.publicKeyPEM = null;
230
409
  }
231
410
  /**
232
411
  * Check if client is authenticated
@@ -275,57 +454,11 @@ var Wolfronix = class {
275
454
  throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
276
455
  }
277
456
  formData.append("user_id", this.userId || "");
278
- return this.request("POST", "/api/v1/encrypt", {
279
- formData
280
- });
281
- }
282
- /**
283
- * Encrypt a file using streaming (for large files)
284
- *
285
- * @example
286
- * ```typescript
287
- * const result = await wfx.encryptStream(largeFile, (progress) => {
288
- * console.log(`Progress: ${progress}%`);
289
- * });
290
- * ```
291
- */
292
- async encryptStream(file, onProgress) {
293
- this.ensureAuthenticated();
294
- const tokenResponse = await this.request("POST", "/api/v1/stream/token", {
295
- body: {
296
- user_id: this.userId,
297
- client_id: this.config.clientId
298
- }
299
- });
300
- const formData = new FormData();
301
- formData.append("file", file);
302
- formData.append("user_id", this.userId || "");
303
- formData.append("stream_token", tokenResponse.token);
304
- if (onProgress && typeof XMLHttpRequest !== "undefined") {
305
- return new Promise((resolve, reject) => {
306
- const xhr = new XMLHttpRequest();
307
- xhr.upload.onprogress = (event) => {
308
- if (event.lengthComputable) {
309
- onProgress(Math.round(event.loaded / event.total * 100));
310
- }
311
- };
312
- xhr.onload = () => {
313
- if (xhr.status >= 200 && xhr.status < 300) {
314
- resolve(JSON.parse(xhr.responseText));
315
- } else {
316
- reject(new WolfronixError("Upload failed", "UPLOAD_ERROR", xhr.status));
317
- }
318
- };
319
- xhr.onerror = () => reject(new NetworkError("Upload failed"));
320
- xhr.open("POST", `${this.config.baseUrl}/api/v1/stream/encrypt`);
321
- const headers = this.getHeaders();
322
- Object.entries(headers).forEach(([key, value]) => {
323
- xhr.setRequestHeader(key, value);
324
- });
325
- xhr.send(formData);
326
- });
457
+ if (!this.publicKeyPEM) {
458
+ throw new Error("Public key not available. Is user logged in?");
327
459
  }
328
- return this.request("POST", "/api/v1/stream/encrypt", {
460
+ formData.append("client_public_key", this.publicKeyPEM);
461
+ return this.request("POST", "/api/v1/encrypt", {
329
462
  formData
330
463
  });
331
464
  }
@@ -348,8 +481,16 @@ var Wolfronix = class {
348
481
  if (!fileId) {
349
482
  throw new ValidationError("File ID is required");
350
483
  }
351
- return this.request("GET", `/api/v1/decrypt/${fileId}`, {
352
- responseType: "blob"
484
+ if (!this.privateKey) {
485
+ throw new Error("Private key not available. Is user logged in?");
486
+ }
487
+ const privateKeyPEM = await exportKeyToPEM(this.privateKey, "private");
488
+ return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
489
+ responseType: "blob",
490
+ headers: {
491
+ "X-Private-Key": privateKeyPEM,
492
+ "X-User-Role": "owner"
493
+ }
353
494
  });
354
495
  }
355
496
  /**
@@ -360,42 +501,16 @@ var Wolfronix = class {
360
501
  if (!fileId) {
361
502
  throw new ValidationError("File ID is required");
362
503
  }
363
- return this.request("GET", `/api/v1/decrypt/${fileId}`, {
364
- responseType: "arraybuffer"
365
- });
366
- }
367
- /**
368
- * Decrypt using streaming (for large files)
369
- */
370
- async decryptStream(fileId, onProgress) {
371
- this.ensureAuthenticated();
372
- if (onProgress && typeof XMLHttpRequest !== "undefined") {
373
- return new Promise((resolve, reject) => {
374
- const xhr = new XMLHttpRequest();
375
- xhr.responseType = "blob";
376
- xhr.onprogress = (event) => {
377
- if (event.lengthComputable) {
378
- onProgress(Math.round(event.loaded / event.total * 100));
379
- }
380
- };
381
- xhr.onload = () => {
382
- if (xhr.status >= 200 && xhr.status < 300) {
383
- resolve(xhr.response);
384
- } else {
385
- reject(new WolfronixError("Download failed", "DOWNLOAD_ERROR", xhr.status));
386
- }
387
- };
388
- xhr.onerror = () => reject(new NetworkError("Download failed"));
389
- xhr.open("GET", `${this.config.baseUrl}/api/v1/stream/decrypt/${fileId}`);
390
- const headers = this.getHeaders();
391
- Object.entries(headers).forEach(([key, value]) => {
392
- xhr.setRequestHeader(key, value);
393
- });
394
- xhr.send();
395
- });
504
+ if (!this.privateKey) {
505
+ throw new Error("Private key not available. Is user logged in?");
396
506
  }
397
- return this.request("GET", `/api/v1/stream/decrypt/${fileId}`, {
398
- responseType: "blob"
507
+ const privateKeyPEM = await exportKeyToPEM(this.privateKey, "private");
508
+ return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
509
+ responseType: "arraybuffer",
510
+ headers: {
511
+ "X-Private-Key": privateKeyPEM,
512
+ "X-User-Role": "owner"
513
+ }
399
514
  });
400
515
  }
401
516
  /**
@@ -409,7 +524,17 @@ var Wolfronix = class {
409
524
  */
410
525
  async listFiles() {
411
526
  this.ensureAuthenticated();
412
- return this.request("GET", "/api/v1/files");
527
+ const files = await this.request("GET", "/api/v1/files");
528
+ return {
529
+ success: true,
530
+ files: (files || []).map((f) => ({
531
+ file_id: f.id,
532
+ original_name: f.name,
533
+ encrypted_size: f.size_bytes,
534
+ created_at: f.date
535
+ })),
536
+ total: (files || []).length
537
+ };
413
538
  }
414
539
  /**
415
540
  * Delete an encrypted file
@@ -440,7 +565,7 @@ var Wolfronix = class {
440
565
  */
441
566
  async getMetrics() {
442
567
  this.ensureAuthenticated();
443
- return this.request("GET", "/api/v1/metrics");
568
+ return this.request("GET", "/api/v1/metrics/summary");
444
569
  }
445
570
  /**
446
571
  * Check if server is healthy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolfronix-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official Wolfronix SDK for JavaScript/TypeScript - Zero-knowledge encryption made simple",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -62,4 +62,4 @@
62
62
  "optional": true
63
63
  }
64
64
  }
65
- }
65
+ }