wolfronix-sdk 1.3.2 → 2.3.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 +51 -16
- package/dist/index.d.mts +246 -7
- package/dist/index.d.ts +246 -7
- package/dist/index.js +422 -18
- package/dist/index.mjs +411 -18
- package/package.json +2 -10
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -27,6 +37,7 @@ __export(index_exports, {
|
|
|
27
37
|
ValidationError: () => ValidationError,
|
|
28
38
|
Wolfronix: () => Wolfronix,
|
|
29
39
|
WolfronixError: () => WolfronixError,
|
|
40
|
+
WolfronixStream: () => WolfronixStream,
|
|
30
41
|
createClient: () => createClient,
|
|
31
42
|
default: () => index_default
|
|
32
43
|
});
|
|
@@ -38,7 +49,7 @@ var getCrypto = () => {
|
|
|
38
49
|
return globalThis.crypto;
|
|
39
50
|
}
|
|
40
51
|
throw new Error(
|
|
41
|
-
"Web Crypto API not available. Requires a modern browser or Node.js
|
|
52
|
+
"Web Crypto API not available. Requires a modern browser or Node.js 18+."
|
|
42
53
|
);
|
|
43
54
|
};
|
|
44
55
|
var RSA_ALG = {
|
|
@@ -213,6 +224,10 @@ async function rsaDecrypt(encryptedBase64, privateKey) {
|
|
|
213
224
|
);
|
|
214
225
|
return decrypted;
|
|
215
226
|
}
|
|
227
|
+
async function rsaDecryptBase64(encryptedBase64, privateKey) {
|
|
228
|
+
const decrypted = await rsaDecrypt(encryptedBase64, privateKey);
|
|
229
|
+
return arrayBufferToBase64(decrypted);
|
|
230
|
+
}
|
|
216
231
|
async function exportSessionKey(key) {
|
|
217
232
|
return await getCrypto().subtle.exportKey("raw", key);
|
|
218
233
|
}
|
|
@@ -325,6 +340,7 @@ var Wolfronix = class {
|
|
|
325
340
|
this.config = {
|
|
326
341
|
baseUrl: config,
|
|
327
342
|
clientId: "",
|
|
343
|
+
wolfronixKey: "",
|
|
328
344
|
timeout: 3e4,
|
|
329
345
|
retries: 3,
|
|
330
346
|
insecure: false
|
|
@@ -333,6 +349,7 @@ var Wolfronix = class {
|
|
|
333
349
|
this.config = {
|
|
334
350
|
baseUrl: config.baseUrl,
|
|
335
351
|
clientId: config.clientId || "",
|
|
352
|
+
wolfronixKey: config.wolfronixKey || "",
|
|
336
353
|
timeout: config.timeout || 3e4,
|
|
337
354
|
retries: config.retries || 3,
|
|
338
355
|
insecure: config.insecure || false
|
|
@@ -340,6 +357,10 @@ var Wolfronix = class {
|
|
|
340
357
|
}
|
|
341
358
|
this.config.baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
342
359
|
}
|
|
360
|
+
/** Expose private key status for testing */
|
|
361
|
+
hasPrivateKey() {
|
|
362
|
+
return this.privateKey !== null;
|
|
363
|
+
}
|
|
343
364
|
// ==========================================================================
|
|
344
365
|
// Private Helpers
|
|
345
366
|
// ==========================================================================
|
|
@@ -350,6 +371,9 @@ var Wolfronix = class {
|
|
|
350
371
|
if (this.config.clientId) {
|
|
351
372
|
headers["X-Client-ID"] = this.config.clientId;
|
|
352
373
|
}
|
|
374
|
+
if (this.config.wolfronixKey) {
|
|
375
|
+
headers["X-Wolfronix-Key"] = this.config.wolfronixKey;
|
|
376
|
+
}
|
|
353
377
|
if (includeAuth && this.token) {
|
|
354
378
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
355
379
|
if (this.userId) {
|
|
@@ -375,6 +399,15 @@ var Wolfronix = class {
|
|
|
375
399
|
headers,
|
|
376
400
|
signal: controller.signal
|
|
377
401
|
};
|
|
402
|
+
if (this.config.insecure && typeof process !== "undefined") {
|
|
403
|
+
try {
|
|
404
|
+
const { Agent } = await import("undici");
|
|
405
|
+
fetchOptions.dispatcher = new Agent({
|
|
406
|
+
connect: { rejectUnauthorized: false }
|
|
407
|
+
});
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
378
411
|
if (formData) {
|
|
379
412
|
fetchOptions.body = formData;
|
|
380
413
|
} else if (body) {
|
|
@@ -462,7 +495,7 @@ var Wolfronix = class {
|
|
|
462
495
|
this.publicKey = keyPair.publicKey;
|
|
463
496
|
this.privateKey = keyPair.privateKey;
|
|
464
497
|
this.publicKeyPEM = publicKeyPEM;
|
|
465
|
-
this.token = "
|
|
498
|
+
this.token = "zk-session";
|
|
466
499
|
}
|
|
467
500
|
return response;
|
|
468
501
|
}
|
|
@@ -497,7 +530,7 @@ var Wolfronix = class {
|
|
|
497
530
|
this.publicKeyPEM = response.public_key_pem;
|
|
498
531
|
this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
|
|
499
532
|
this.userId = email;
|
|
500
|
-
this.token = "
|
|
533
|
+
this.token = "zk-session";
|
|
501
534
|
return {
|
|
502
535
|
success: true,
|
|
503
536
|
user_id: email,
|
|
@@ -594,7 +627,14 @@ var Wolfronix = class {
|
|
|
594
627
|
};
|
|
595
628
|
}
|
|
596
629
|
/**
|
|
597
|
-
* Decrypt and retrieve a file
|
|
630
|
+
* Decrypt and retrieve a file using zero-knowledge flow.
|
|
631
|
+
*
|
|
632
|
+
* Flow:
|
|
633
|
+
* 1. GET /api/v1/files/{id}/key → encrypted key_part_a
|
|
634
|
+
* 2. Decrypt key_part_a client-side with private key (RSA-OAEP)
|
|
635
|
+
* 3. POST /api/v1/files/{id}/decrypt with { decrypted_key_a } in body
|
|
636
|
+
*
|
|
637
|
+
* The private key NEVER leaves the client.
|
|
598
638
|
*
|
|
599
639
|
* @example
|
|
600
640
|
* ```typescript
|
|
@@ -607,7 +647,7 @@ var Wolfronix = class {
|
|
|
607
647
|
* fs.writeFileSync('decrypted.pdf', buffer);
|
|
608
648
|
* ```
|
|
609
649
|
*/
|
|
610
|
-
async decrypt(fileId) {
|
|
650
|
+
async decrypt(fileId, role = "owner") {
|
|
611
651
|
this.ensureAuthenticated();
|
|
612
652
|
if (!fileId) {
|
|
613
653
|
throw new ValidationError("File ID is required");
|
|
@@ -615,19 +655,20 @@ var Wolfronix = class {
|
|
|
615
655
|
if (!this.privateKey) {
|
|
616
656
|
throw new Error("Private key not available. Is user logged in?");
|
|
617
657
|
}
|
|
618
|
-
const
|
|
658
|
+
const keyResponse = await this.getFileKey(fileId);
|
|
659
|
+
const decryptedKeyA = await rsaDecryptBase64(keyResponse.key_part_a, this.privateKey);
|
|
619
660
|
return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
|
|
620
661
|
responseType: "blob",
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
662
|
+
body: {
|
|
663
|
+
decrypted_key_a: decryptedKeyA,
|
|
664
|
+
user_role: role
|
|
624
665
|
}
|
|
625
666
|
});
|
|
626
667
|
}
|
|
627
668
|
/**
|
|
628
|
-
* Decrypt and return as ArrayBuffer
|
|
669
|
+
* Decrypt and return as ArrayBuffer (zero-knowledge flow)
|
|
629
670
|
*/
|
|
630
|
-
async decryptToBuffer(fileId) {
|
|
671
|
+
async decryptToBuffer(fileId, role = "owner") {
|
|
631
672
|
this.ensureAuthenticated();
|
|
632
673
|
if (!fileId) {
|
|
633
674
|
throw new ValidationError("File ID is required");
|
|
@@ -635,15 +676,29 @@ var Wolfronix = class {
|
|
|
635
676
|
if (!this.privateKey) {
|
|
636
677
|
throw new Error("Private key not available. Is user logged in?");
|
|
637
678
|
}
|
|
638
|
-
const
|
|
679
|
+
const keyResponse = await this.getFileKey(fileId);
|
|
680
|
+
const decryptedKeyA = await rsaDecryptBase64(keyResponse.key_part_a, this.privateKey);
|
|
639
681
|
return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
|
|
640
682
|
responseType: "arraybuffer",
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
683
|
+
body: {
|
|
684
|
+
decrypted_key_a: decryptedKeyA,
|
|
685
|
+
user_role: role
|
|
644
686
|
}
|
|
645
687
|
});
|
|
646
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Fetch the encrypted key_part_a for a file (for client-side decryption)
|
|
691
|
+
*
|
|
692
|
+
* @param fileId The file ID to get the key for
|
|
693
|
+
* @returns KeyPartResponse containing the RSA-OAEP encrypted key_part_a
|
|
694
|
+
*/
|
|
695
|
+
async getFileKey(fileId) {
|
|
696
|
+
this.ensureAuthenticated();
|
|
697
|
+
if (!fileId) {
|
|
698
|
+
throw new ValidationError("File ID is required");
|
|
699
|
+
}
|
|
700
|
+
return this.request("GET", `/api/v1/files/${fileId}/key`);
|
|
701
|
+
}
|
|
647
702
|
/**
|
|
648
703
|
* List all encrypted files for current user
|
|
649
704
|
*
|
|
@@ -688,11 +743,19 @@ var Wolfronix = class {
|
|
|
688
743
|
/**
|
|
689
744
|
* Get another user's public key (for E2E encryption)
|
|
690
745
|
* @param userId The ID of the recipient
|
|
746
|
+
* @param clientId Optional: override the configured clientId
|
|
691
747
|
*/
|
|
692
|
-
async getPublicKey(userId) {
|
|
748
|
+
async getPublicKey(userId, clientId) {
|
|
693
749
|
this.ensureAuthenticated();
|
|
694
|
-
const
|
|
695
|
-
|
|
750
|
+
const cid = clientId || this.config.clientId;
|
|
751
|
+
if (!cid) {
|
|
752
|
+
throw new ValidationError("clientId is required for getPublicKey(). Set it in config or pass as second argument.");
|
|
753
|
+
}
|
|
754
|
+
const result = await this.request(
|
|
755
|
+
"GET",
|
|
756
|
+
`/api/v1/keys/public/${encodeURIComponent(cid)}/${encodeURIComponent(userId)}`
|
|
757
|
+
);
|
|
758
|
+
return result.public_key_pem;
|
|
696
759
|
}
|
|
697
760
|
/**
|
|
698
761
|
* Encrypt a short text message for a recipient (Hybrid Encryption: RSA + AES)
|
|
@@ -745,6 +808,166 @@ var Wolfronix = class {
|
|
|
745
808
|
}
|
|
746
809
|
}
|
|
747
810
|
// ==========================================================================
|
|
811
|
+
// Server-Side Message Encryption (Dual-Key Split)
|
|
812
|
+
// ==========================================================================
|
|
813
|
+
/**
|
|
814
|
+
* Encrypt a text message via the Wolfronix server (dual-key split).
|
|
815
|
+
* The server generates an AES key, encrypts the message, and splits the key —
|
|
816
|
+
* you get key_part_a, the server holds key_part_b.
|
|
817
|
+
*
|
|
818
|
+
* Use this for server-managed message encryption (e.g., stored encrypted messages).
|
|
819
|
+
* For true E2E (where the server never sees plaintext), use encryptMessage() instead.
|
|
820
|
+
*
|
|
821
|
+
* @param message The plaintext message to encrypt
|
|
822
|
+
* @param options.layer 3 = AES only (full key returned), 4 = dual-key split (default)
|
|
823
|
+
*
|
|
824
|
+
* @example
|
|
825
|
+
* ```typescript
|
|
826
|
+
* const result = await wfx.serverEncrypt('Hello, World!');
|
|
827
|
+
* // Store result.encrypted_message, result.nonce, result.key_part_a, result.message_tag
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
async serverEncrypt(message, options) {
|
|
831
|
+
this.ensureAuthenticated();
|
|
832
|
+
if (!message) {
|
|
833
|
+
throw new ValidationError("Message is required");
|
|
834
|
+
}
|
|
835
|
+
return this.request("POST", "/api/v1/messages/encrypt", {
|
|
836
|
+
body: {
|
|
837
|
+
message,
|
|
838
|
+
user_id: this.userId,
|
|
839
|
+
layer: options?.layer || 4
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Decrypt a message previously encrypted via serverEncrypt().
|
|
845
|
+
*
|
|
846
|
+
* @param params The encrypted message data (from serverEncrypt result)
|
|
847
|
+
* @returns The decrypted plaintext message
|
|
848
|
+
*
|
|
849
|
+
* @example
|
|
850
|
+
* ```typescript
|
|
851
|
+
* const text = await wfx.serverDecrypt({
|
|
852
|
+
* encryptedMessage: result.encrypted_message,
|
|
853
|
+
* nonce: result.nonce,
|
|
854
|
+
* keyPartA: result.key_part_a,
|
|
855
|
+
* messageTag: result.message_tag,
|
|
856
|
+
* });
|
|
857
|
+
* ```
|
|
858
|
+
*/
|
|
859
|
+
async serverDecrypt(params) {
|
|
860
|
+
this.ensureAuthenticated();
|
|
861
|
+
if (!params.encryptedMessage || !params.nonce || !params.keyPartA) {
|
|
862
|
+
throw new ValidationError("encryptedMessage, nonce, and keyPartA are required");
|
|
863
|
+
}
|
|
864
|
+
const response = await this.request(
|
|
865
|
+
"POST",
|
|
866
|
+
"/api/v1/messages/decrypt",
|
|
867
|
+
{
|
|
868
|
+
body: {
|
|
869
|
+
encrypted_message: params.encryptedMessage,
|
|
870
|
+
nonce: params.nonce,
|
|
871
|
+
key_part_a: params.keyPartA,
|
|
872
|
+
message_tag: params.messageTag || "",
|
|
873
|
+
user_id: this.userId
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
);
|
|
877
|
+
return response.message;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Encrypt multiple messages in a single round-trip (batch).
|
|
881
|
+
* All messages share one AES key (different nonce per message).
|
|
882
|
+
* Efficient for chat history encryption or bulk operations.
|
|
883
|
+
*
|
|
884
|
+
* @param messages Array of { id, message } objects (max 100)
|
|
885
|
+
* @param options.layer 3 or 4 (default: 4)
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```typescript
|
|
889
|
+
* const result = await wfx.serverEncryptBatch([
|
|
890
|
+
* { id: 'msg1', message: 'Hello' },
|
|
891
|
+
* { id: 'msg2', message: 'World' },
|
|
892
|
+
* ]);
|
|
893
|
+
* // result.results[0].encrypted_message, result.key_part_a, result.batch_tag
|
|
894
|
+
* ```
|
|
895
|
+
*/
|
|
896
|
+
async serverEncryptBatch(messages, options) {
|
|
897
|
+
this.ensureAuthenticated();
|
|
898
|
+
if (!messages || messages.length === 0) {
|
|
899
|
+
throw new ValidationError("At least one message is required");
|
|
900
|
+
}
|
|
901
|
+
if (messages.length > 100) {
|
|
902
|
+
throw new ValidationError("Maximum 100 messages per batch");
|
|
903
|
+
}
|
|
904
|
+
return this.request("POST", "/api/v1/messages/batch/encrypt", {
|
|
905
|
+
body: {
|
|
906
|
+
messages,
|
|
907
|
+
user_id: this.userId,
|
|
908
|
+
layer: options?.layer || 4
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Decrypt a single message from a batch result.
|
|
914
|
+
* Uses the shared key_part_a and batch_tag from the batch result.
|
|
915
|
+
*
|
|
916
|
+
* @param batchResult The batch encrypt result
|
|
917
|
+
* @param index The index of the message to decrypt
|
|
918
|
+
*/
|
|
919
|
+
async serverDecryptBatchItem(batchResult, index) {
|
|
920
|
+
if (index < 0 || index >= batchResult.results.length) {
|
|
921
|
+
throw new ValidationError("Invalid batch index");
|
|
922
|
+
}
|
|
923
|
+
const item = batchResult.results[index];
|
|
924
|
+
return this.serverDecrypt({
|
|
925
|
+
encryptedMessage: item.encrypted_message,
|
|
926
|
+
nonce: item.nonce,
|
|
927
|
+
keyPartA: batchResult.key_part_a,
|
|
928
|
+
messageTag: batchResult.batch_tag
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
// ==========================================================================
|
|
932
|
+
// Real-Time Streaming Encryption (WebSocket)
|
|
933
|
+
// ==========================================================================
|
|
934
|
+
/**
|
|
935
|
+
* Create a streaming encryption/decryption session over WebSocket.
|
|
936
|
+
* Data flows in real-time: send chunks, receive encrypted/decrypted chunks back.
|
|
937
|
+
*
|
|
938
|
+
* @param direction 'encrypt' for plaintext→ciphertext, 'decrypt' for reverse
|
|
939
|
+
* @param streamKey Required for decrypt — the key_part_a and stream_tag from the encrypt session
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```typescript
|
|
943
|
+
* // Encrypt stream
|
|
944
|
+
* const stream = await wfx.createStream('encrypt');
|
|
945
|
+
* stream.onData((chunk, seq) => console.log('Encrypted chunk', seq));
|
|
946
|
+
* stream.send('Hello chunk 1');
|
|
947
|
+
* stream.send('Hello chunk 2');
|
|
948
|
+
* const summary = await stream.end();
|
|
949
|
+
* // Save stream.keyPartA and stream.streamTag for decryption
|
|
950
|
+
*
|
|
951
|
+
* // Decrypt stream
|
|
952
|
+
* const dStream = await wfx.createStream('decrypt', {
|
|
953
|
+
* keyPartA: stream.keyPartA!,
|
|
954
|
+
* streamTag: stream.streamTag!,
|
|
955
|
+
* });
|
|
956
|
+
* dStream.onData((chunk, seq) => console.log('Decrypted:', chunk));
|
|
957
|
+
* dStream.send(encryptedChunk1);
|
|
958
|
+
* await dStream.end();
|
|
959
|
+
* ```
|
|
960
|
+
*/
|
|
961
|
+
async createStream(direction, streamKey) {
|
|
962
|
+
this.ensureAuthenticated();
|
|
963
|
+
if (direction === "decrypt" && !streamKey) {
|
|
964
|
+
throw new ValidationError("streamKey (keyPartA + streamTag) is required for decrypt streams");
|
|
965
|
+
}
|
|
966
|
+
const stream = new WolfronixStream(this.config, this.userId);
|
|
967
|
+
await stream.connect(direction, streamKey);
|
|
968
|
+
return stream;
|
|
969
|
+
}
|
|
970
|
+
// ==========================================================================
|
|
748
971
|
// Metrics & Status
|
|
749
972
|
// ==========================================================================
|
|
750
973
|
/**
|
|
@@ -774,6 +997,186 @@ var Wolfronix = class {
|
|
|
774
997
|
}
|
|
775
998
|
}
|
|
776
999
|
};
|
|
1000
|
+
var WolfronixStream = class {
|
|
1001
|
+
/** @internal */
|
|
1002
|
+
constructor(config, userId) {
|
|
1003
|
+
this.config = config;
|
|
1004
|
+
this.userId = userId;
|
|
1005
|
+
this.ws = null;
|
|
1006
|
+
this.dataCallbacks = [];
|
|
1007
|
+
this.errorCallbacks = [];
|
|
1008
|
+
this.pendingChunks = /* @__PURE__ */ new Map();
|
|
1009
|
+
this.seqCounter = 0;
|
|
1010
|
+
/** Client's key half (available after encrypt stream init) */
|
|
1011
|
+
this.keyPartA = null;
|
|
1012
|
+
/** Stream tag (available after encrypt stream init) */
|
|
1013
|
+
this.streamTag = null;
|
|
1014
|
+
}
|
|
1015
|
+
/** @internal Connect and initialize the stream session */
|
|
1016
|
+
async connect(direction, streamKey) {
|
|
1017
|
+
return new Promise((resolve, reject) => {
|
|
1018
|
+
const wsBase = this.config.baseUrl.replace(/^http/, "ws");
|
|
1019
|
+
const params = new URLSearchParams();
|
|
1020
|
+
if (this.config.wolfronixKey) {
|
|
1021
|
+
params.set("wolfronix_key", this.config.wolfronixKey);
|
|
1022
|
+
}
|
|
1023
|
+
if (this.config.clientId) {
|
|
1024
|
+
params.set("client_id", this.config.clientId);
|
|
1025
|
+
}
|
|
1026
|
+
const wsUrl = `${wsBase}/api/v1/stream?${params.toString()}`;
|
|
1027
|
+
this.ws = new WebSocket(wsUrl);
|
|
1028
|
+
this.ws.onopen = () => {
|
|
1029
|
+
const initMsg = { type: "init", direction };
|
|
1030
|
+
if (direction === "decrypt" && streamKey) {
|
|
1031
|
+
initMsg.key_part_a = streamKey.keyPartA;
|
|
1032
|
+
initMsg.stream_tag = streamKey.streamTag;
|
|
1033
|
+
}
|
|
1034
|
+
this.ws.send(JSON.stringify(initMsg));
|
|
1035
|
+
};
|
|
1036
|
+
let initResolved = false;
|
|
1037
|
+
this.ws.onmessage = (event) => {
|
|
1038
|
+
try {
|
|
1039
|
+
const msg = JSON.parse(event.data);
|
|
1040
|
+
if (msg.type === "error") {
|
|
1041
|
+
const err = new Error(msg.error);
|
|
1042
|
+
if (!initResolved) {
|
|
1043
|
+
initResolved = true;
|
|
1044
|
+
reject(err);
|
|
1045
|
+
}
|
|
1046
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (msg.type === "init_ack" && !initResolved) {
|
|
1050
|
+
initResolved = true;
|
|
1051
|
+
if (msg.key_part_a) this.keyPartA = msg.key_part_a;
|
|
1052
|
+
if (msg.stream_tag) this.streamTag = msg.stream_tag;
|
|
1053
|
+
resolve();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (msg.type === "data") {
|
|
1057
|
+
this.dataCallbacks.forEach((cb) => cb(msg.data, msg.seq));
|
|
1058
|
+
const pending = this.pendingChunks.get(msg.seq);
|
|
1059
|
+
if (pending) {
|
|
1060
|
+
pending.resolve(msg.data);
|
|
1061
|
+
this.pendingChunks.delete(msg.seq);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (msg.type === "end_ack") {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
const err = new Error("Failed to parse stream message");
|
|
1070
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
this.ws.onerror = (event) => {
|
|
1074
|
+
const err = new Error("WebSocket error");
|
|
1075
|
+
if (!initResolved) {
|
|
1076
|
+
initResolved = true;
|
|
1077
|
+
reject(err);
|
|
1078
|
+
}
|
|
1079
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1080
|
+
};
|
|
1081
|
+
this.ws.onclose = () => {
|
|
1082
|
+
this.pendingChunks.forEach((p) => p.reject(new Error("Stream closed")));
|
|
1083
|
+
this.pendingChunks.clear();
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Send a data chunk for encryption/decryption.
|
|
1089
|
+
* Returns a promise that resolves with the processed (encrypted/decrypted) chunk.
|
|
1090
|
+
*
|
|
1091
|
+
* @param data String or base64-encoded binary data
|
|
1092
|
+
* @returns The processed chunk (base64-encoded)
|
|
1093
|
+
*/
|
|
1094
|
+
async send(data) {
|
|
1095
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1096
|
+
throw new Error("Stream not connected");
|
|
1097
|
+
}
|
|
1098
|
+
const b64Data = this.isBase64(data) ? data : btoa(data);
|
|
1099
|
+
const seq = this.seqCounter++;
|
|
1100
|
+
return new Promise((resolve, reject) => {
|
|
1101
|
+
this.pendingChunks.set(seq, { resolve, reject });
|
|
1102
|
+
this.ws.send(JSON.stringify({ type: "data", data: b64Data }));
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Send raw binary data for encryption/decryption.
|
|
1107
|
+
*
|
|
1108
|
+
* @param buffer ArrayBuffer or Uint8Array
|
|
1109
|
+
* @returns The processed chunk (base64-encoded)
|
|
1110
|
+
*/
|
|
1111
|
+
async sendBinary(buffer) {
|
|
1112
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1113
|
+
let binary = "";
|
|
1114
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1115
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1116
|
+
}
|
|
1117
|
+
const b64 = btoa(binary);
|
|
1118
|
+
return this.send(b64);
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Register a callback for incoming data chunks.
|
|
1122
|
+
*
|
|
1123
|
+
* @param callback Called with (base64Data, sequenceNumber) for each chunk
|
|
1124
|
+
*/
|
|
1125
|
+
onData(callback) {
|
|
1126
|
+
this.dataCallbacks.push(callback);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Register a callback for stream errors.
|
|
1130
|
+
*/
|
|
1131
|
+
onError(callback) {
|
|
1132
|
+
this.errorCallbacks.push(callback);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* End the stream session. Returns the total number of chunks processed.
|
|
1136
|
+
*/
|
|
1137
|
+
async end() {
|
|
1138
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1139
|
+
return { chunksProcessed: this.seqCounter };
|
|
1140
|
+
}
|
|
1141
|
+
return new Promise((resolve) => {
|
|
1142
|
+
const originalHandler = this.ws.onmessage;
|
|
1143
|
+
this.ws.onmessage = (event) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const msg = JSON.parse(event.data);
|
|
1146
|
+
if (msg.type === "end_ack") {
|
|
1147
|
+
resolve({ chunksProcessed: msg.chunks_processed || this.seqCounter });
|
|
1148
|
+
this.ws.close();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
if (originalHandler && this.ws) originalHandler.call(this.ws, event);
|
|
1154
|
+
};
|
|
1155
|
+
this.ws.send(JSON.stringify({ type: "end" }));
|
|
1156
|
+
setTimeout(() => {
|
|
1157
|
+
resolve({ chunksProcessed: this.seqCounter });
|
|
1158
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1159
|
+
this.ws.close();
|
|
1160
|
+
}
|
|
1161
|
+
}, 5e3);
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Close the stream immediately without sending an end message.
|
|
1166
|
+
*/
|
|
1167
|
+
close() {
|
|
1168
|
+
if (this.ws) {
|
|
1169
|
+
this.ws.close();
|
|
1170
|
+
this.ws = null;
|
|
1171
|
+
}
|
|
1172
|
+
this.pendingChunks.forEach((p) => p.reject(new Error("Stream closed")));
|
|
1173
|
+
this.pendingChunks.clear();
|
|
1174
|
+
}
|
|
1175
|
+
isBase64(str) {
|
|
1176
|
+
if (str.length % 4 !== 0) return false;
|
|
1177
|
+
return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
777
1180
|
function createClient(config) {
|
|
778
1181
|
return new Wolfronix(config);
|
|
779
1182
|
}
|
|
@@ -787,5 +1190,6 @@ var index_default = Wolfronix;
|
|
|
787
1190
|
ValidationError,
|
|
788
1191
|
Wolfronix,
|
|
789
1192
|
WolfronixError,
|
|
1193
|
+
WolfronixStream,
|
|
790
1194
|
createClient
|
|
791
1195
|
});
|