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 +21 -4
- package/dist/index.d.mts +9 -26
- package/dist/index.d.ts +9 -26
- package/dist/index.js +230 -103
- package/dist/index.mjs +228 -103
- package/package.json +2 -2
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** -
|
|
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'
|
|
48
|
+
clientId: 'your-enterprise-client-id'
|
|
36
49
|
});
|
|
37
50
|
|
|
38
|
-
//
|
|
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.
|
|
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
|
-
|
|
28
|
-
file_id:
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
28
|
-
file_id:
|
|
29
|
-
|
|
30
|
-
|
|
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,
|
|
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
|
|
209
|
-
|
|
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.
|
|
214
|
-
this.
|
|
215
|
-
this.
|
|
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: {
|
|
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.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
396
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
177
|
-
|
|
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.
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
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: {
|
|
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.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
352
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|