wolfronix-sdk 1.3.1 → 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 +3 -11
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ var getCrypto = () => {
|
|
|
4
4
|
return globalThis.crypto;
|
|
5
5
|
}
|
|
6
6
|
throw new Error(
|
|
7
|
-
"Web Crypto API not available. Requires a modern browser or Node.js
|
|
7
|
+
"Web Crypto API not available. Requires a modern browser or Node.js 18+."
|
|
8
8
|
);
|
|
9
9
|
};
|
|
10
10
|
var RSA_ALG = {
|
|
@@ -179,6 +179,10 @@ async function rsaDecrypt(encryptedBase64, privateKey) {
|
|
|
179
179
|
);
|
|
180
180
|
return decrypted;
|
|
181
181
|
}
|
|
182
|
+
async function rsaDecryptBase64(encryptedBase64, privateKey) {
|
|
183
|
+
const decrypted = await rsaDecrypt(encryptedBase64, privateKey);
|
|
184
|
+
return arrayBufferToBase64(decrypted);
|
|
185
|
+
}
|
|
182
186
|
async function exportSessionKey(key) {
|
|
183
187
|
return await getCrypto().subtle.exportKey("raw", key);
|
|
184
188
|
}
|
|
@@ -291,6 +295,7 @@ var Wolfronix = class {
|
|
|
291
295
|
this.config = {
|
|
292
296
|
baseUrl: config,
|
|
293
297
|
clientId: "",
|
|
298
|
+
wolfronixKey: "",
|
|
294
299
|
timeout: 3e4,
|
|
295
300
|
retries: 3,
|
|
296
301
|
insecure: false
|
|
@@ -299,6 +304,7 @@ var Wolfronix = class {
|
|
|
299
304
|
this.config = {
|
|
300
305
|
baseUrl: config.baseUrl,
|
|
301
306
|
clientId: config.clientId || "",
|
|
307
|
+
wolfronixKey: config.wolfronixKey || "",
|
|
302
308
|
timeout: config.timeout || 3e4,
|
|
303
309
|
retries: config.retries || 3,
|
|
304
310
|
insecure: config.insecure || false
|
|
@@ -306,6 +312,10 @@ var Wolfronix = class {
|
|
|
306
312
|
}
|
|
307
313
|
this.config.baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
308
314
|
}
|
|
315
|
+
/** Expose private key status for testing */
|
|
316
|
+
hasPrivateKey() {
|
|
317
|
+
return this.privateKey !== null;
|
|
318
|
+
}
|
|
309
319
|
// ==========================================================================
|
|
310
320
|
// Private Helpers
|
|
311
321
|
// ==========================================================================
|
|
@@ -316,6 +326,9 @@ var Wolfronix = class {
|
|
|
316
326
|
if (this.config.clientId) {
|
|
317
327
|
headers["X-Client-ID"] = this.config.clientId;
|
|
318
328
|
}
|
|
329
|
+
if (this.config.wolfronixKey) {
|
|
330
|
+
headers["X-Wolfronix-Key"] = this.config.wolfronixKey;
|
|
331
|
+
}
|
|
319
332
|
if (includeAuth && this.token) {
|
|
320
333
|
headers["Authorization"] = `Bearer ${this.token}`;
|
|
321
334
|
if (this.userId) {
|
|
@@ -341,6 +354,15 @@ var Wolfronix = class {
|
|
|
341
354
|
headers,
|
|
342
355
|
signal: controller.signal
|
|
343
356
|
};
|
|
357
|
+
if (this.config.insecure && typeof process !== "undefined") {
|
|
358
|
+
try {
|
|
359
|
+
const { Agent } = await import("undici");
|
|
360
|
+
fetchOptions.dispatcher = new Agent({
|
|
361
|
+
connect: { rejectUnauthorized: false }
|
|
362
|
+
});
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
}
|
|
344
366
|
if (formData) {
|
|
345
367
|
fetchOptions.body = formData;
|
|
346
368
|
} else if (body) {
|
|
@@ -428,7 +450,7 @@ var Wolfronix = class {
|
|
|
428
450
|
this.publicKey = keyPair.publicKey;
|
|
429
451
|
this.privateKey = keyPair.privateKey;
|
|
430
452
|
this.publicKeyPEM = publicKeyPEM;
|
|
431
|
-
this.token = "
|
|
453
|
+
this.token = "zk-session";
|
|
432
454
|
}
|
|
433
455
|
return response;
|
|
434
456
|
}
|
|
@@ -463,7 +485,7 @@ var Wolfronix = class {
|
|
|
463
485
|
this.publicKeyPEM = response.public_key_pem;
|
|
464
486
|
this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
|
|
465
487
|
this.userId = email;
|
|
466
|
-
this.token = "
|
|
488
|
+
this.token = "zk-session";
|
|
467
489
|
return {
|
|
468
490
|
success: true,
|
|
469
491
|
user_id: email,
|
|
@@ -560,7 +582,14 @@ var Wolfronix = class {
|
|
|
560
582
|
};
|
|
561
583
|
}
|
|
562
584
|
/**
|
|
563
|
-
* Decrypt and retrieve a file
|
|
585
|
+
* Decrypt and retrieve a file using zero-knowledge flow.
|
|
586
|
+
*
|
|
587
|
+
* Flow:
|
|
588
|
+
* 1. GET /api/v1/files/{id}/key → encrypted key_part_a
|
|
589
|
+
* 2. Decrypt key_part_a client-side with private key (RSA-OAEP)
|
|
590
|
+
* 3. POST /api/v1/files/{id}/decrypt with { decrypted_key_a } in body
|
|
591
|
+
*
|
|
592
|
+
* The private key NEVER leaves the client.
|
|
564
593
|
*
|
|
565
594
|
* @example
|
|
566
595
|
* ```typescript
|
|
@@ -573,7 +602,7 @@ var Wolfronix = class {
|
|
|
573
602
|
* fs.writeFileSync('decrypted.pdf', buffer);
|
|
574
603
|
* ```
|
|
575
604
|
*/
|
|
576
|
-
async decrypt(fileId) {
|
|
605
|
+
async decrypt(fileId, role = "owner") {
|
|
577
606
|
this.ensureAuthenticated();
|
|
578
607
|
if (!fileId) {
|
|
579
608
|
throw new ValidationError("File ID is required");
|
|
@@ -581,19 +610,20 @@ var Wolfronix = class {
|
|
|
581
610
|
if (!this.privateKey) {
|
|
582
611
|
throw new Error("Private key not available. Is user logged in?");
|
|
583
612
|
}
|
|
584
|
-
const
|
|
613
|
+
const keyResponse = await this.getFileKey(fileId);
|
|
614
|
+
const decryptedKeyA = await rsaDecryptBase64(keyResponse.key_part_a, this.privateKey);
|
|
585
615
|
return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
|
|
586
616
|
responseType: "blob",
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
617
|
+
body: {
|
|
618
|
+
decrypted_key_a: decryptedKeyA,
|
|
619
|
+
user_role: role
|
|
590
620
|
}
|
|
591
621
|
});
|
|
592
622
|
}
|
|
593
623
|
/**
|
|
594
|
-
* Decrypt and return as ArrayBuffer
|
|
624
|
+
* Decrypt and return as ArrayBuffer (zero-knowledge flow)
|
|
595
625
|
*/
|
|
596
|
-
async decryptToBuffer(fileId) {
|
|
626
|
+
async decryptToBuffer(fileId, role = "owner") {
|
|
597
627
|
this.ensureAuthenticated();
|
|
598
628
|
if (!fileId) {
|
|
599
629
|
throw new ValidationError("File ID is required");
|
|
@@ -601,15 +631,29 @@ var Wolfronix = class {
|
|
|
601
631
|
if (!this.privateKey) {
|
|
602
632
|
throw new Error("Private key not available. Is user logged in?");
|
|
603
633
|
}
|
|
604
|
-
const
|
|
634
|
+
const keyResponse = await this.getFileKey(fileId);
|
|
635
|
+
const decryptedKeyA = await rsaDecryptBase64(keyResponse.key_part_a, this.privateKey);
|
|
605
636
|
return this.request("POST", `/api/v1/files/${fileId}/decrypt`, {
|
|
606
637
|
responseType: "arraybuffer",
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
638
|
+
body: {
|
|
639
|
+
decrypted_key_a: decryptedKeyA,
|
|
640
|
+
user_role: role
|
|
610
641
|
}
|
|
611
642
|
});
|
|
612
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Fetch the encrypted key_part_a for a file (for client-side decryption)
|
|
646
|
+
*
|
|
647
|
+
* @param fileId The file ID to get the key for
|
|
648
|
+
* @returns KeyPartResponse containing the RSA-OAEP encrypted key_part_a
|
|
649
|
+
*/
|
|
650
|
+
async getFileKey(fileId) {
|
|
651
|
+
this.ensureAuthenticated();
|
|
652
|
+
if (!fileId) {
|
|
653
|
+
throw new ValidationError("File ID is required");
|
|
654
|
+
}
|
|
655
|
+
return this.request("GET", `/api/v1/files/${fileId}/key`);
|
|
656
|
+
}
|
|
613
657
|
/**
|
|
614
658
|
* List all encrypted files for current user
|
|
615
659
|
*
|
|
@@ -654,11 +698,19 @@ var Wolfronix = class {
|
|
|
654
698
|
/**
|
|
655
699
|
* Get another user's public key (for E2E encryption)
|
|
656
700
|
* @param userId The ID of the recipient
|
|
701
|
+
* @param clientId Optional: override the configured clientId
|
|
657
702
|
*/
|
|
658
|
-
async getPublicKey(userId) {
|
|
703
|
+
async getPublicKey(userId, clientId) {
|
|
659
704
|
this.ensureAuthenticated();
|
|
660
|
-
const
|
|
661
|
-
|
|
705
|
+
const cid = clientId || this.config.clientId;
|
|
706
|
+
if (!cid) {
|
|
707
|
+
throw new ValidationError("clientId is required for getPublicKey(). Set it in config or pass as second argument.");
|
|
708
|
+
}
|
|
709
|
+
const result = await this.request(
|
|
710
|
+
"GET",
|
|
711
|
+
`/api/v1/keys/public/${encodeURIComponent(cid)}/${encodeURIComponent(userId)}`
|
|
712
|
+
);
|
|
713
|
+
return result.public_key_pem;
|
|
662
714
|
}
|
|
663
715
|
/**
|
|
664
716
|
* Encrypt a short text message for a recipient (Hybrid Encryption: RSA + AES)
|
|
@@ -711,6 +763,166 @@ var Wolfronix = class {
|
|
|
711
763
|
}
|
|
712
764
|
}
|
|
713
765
|
// ==========================================================================
|
|
766
|
+
// Server-Side Message Encryption (Dual-Key Split)
|
|
767
|
+
// ==========================================================================
|
|
768
|
+
/**
|
|
769
|
+
* Encrypt a text message via the Wolfronix server (dual-key split).
|
|
770
|
+
* The server generates an AES key, encrypts the message, and splits the key —
|
|
771
|
+
* you get key_part_a, the server holds key_part_b.
|
|
772
|
+
*
|
|
773
|
+
* Use this for server-managed message encryption (e.g., stored encrypted messages).
|
|
774
|
+
* For true E2E (where the server never sees plaintext), use encryptMessage() instead.
|
|
775
|
+
*
|
|
776
|
+
* @param message The plaintext message to encrypt
|
|
777
|
+
* @param options.layer 3 = AES only (full key returned), 4 = dual-key split (default)
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* ```typescript
|
|
781
|
+
* const result = await wfx.serverEncrypt('Hello, World!');
|
|
782
|
+
* // Store result.encrypted_message, result.nonce, result.key_part_a, result.message_tag
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
async serverEncrypt(message, options) {
|
|
786
|
+
this.ensureAuthenticated();
|
|
787
|
+
if (!message) {
|
|
788
|
+
throw new ValidationError("Message is required");
|
|
789
|
+
}
|
|
790
|
+
return this.request("POST", "/api/v1/messages/encrypt", {
|
|
791
|
+
body: {
|
|
792
|
+
message,
|
|
793
|
+
user_id: this.userId,
|
|
794
|
+
layer: options?.layer || 4
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Decrypt a message previously encrypted via serverEncrypt().
|
|
800
|
+
*
|
|
801
|
+
* @param params The encrypted message data (from serverEncrypt result)
|
|
802
|
+
* @returns The decrypted plaintext message
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```typescript
|
|
806
|
+
* const text = await wfx.serverDecrypt({
|
|
807
|
+
* encryptedMessage: result.encrypted_message,
|
|
808
|
+
* nonce: result.nonce,
|
|
809
|
+
* keyPartA: result.key_part_a,
|
|
810
|
+
* messageTag: result.message_tag,
|
|
811
|
+
* });
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
async serverDecrypt(params) {
|
|
815
|
+
this.ensureAuthenticated();
|
|
816
|
+
if (!params.encryptedMessage || !params.nonce || !params.keyPartA) {
|
|
817
|
+
throw new ValidationError("encryptedMessage, nonce, and keyPartA are required");
|
|
818
|
+
}
|
|
819
|
+
const response = await this.request(
|
|
820
|
+
"POST",
|
|
821
|
+
"/api/v1/messages/decrypt",
|
|
822
|
+
{
|
|
823
|
+
body: {
|
|
824
|
+
encrypted_message: params.encryptedMessage,
|
|
825
|
+
nonce: params.nonce,
|
|
826
|
+
key_part_a: params.keyPartA,
|
|
827
|
+
message_tag: params.messageTag || "",
|
|
828
|
+
user_id: this.userId
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
return response.message;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Encrypt multiple messages in a single round-trip (batch).
|
|
836
|
+
* All messages share one AES key (different nonce per message).
|
|
837
|
+
* Efficient for chat history encryption or bulk operations.
|
|
838
|
+
*
|
|
839
|
+
* @param messages Array of { id, message } objects (max 100)
|
|
840
|
+
* @param options.layer 3 or 4 (default: 4)
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* ```typescript
|
|
844
|
+
* const result = await wfx.serverEncryptBatch([
|
|
845
|
+
* { id: 'msg1', message: 'Hello' },
|
|
846
|
+
* { id: 'msg2', message: 'World' },
|
|
847
|
+
* ]);
|
|
848
|
+
* // result.results[0].encrypted_message, result.key_part_a, result.batch_tag
|
|
849
|
+
* ```
|
|
850
|
+
*/
|
|
851
|
+
async serverEncryptBatch(messages, options) {
|
|
852
|
+
this.ensureAuthenticated();
|
|
853
|
+
if (!messages || messages.length === 0) {
|
|
854
|
+
throw new ValidationError("At least one message is required");
|
|
855
|
+
}
|
|
856
|
+
if (messages.length > 100) {
|
|
857
|
+
throw new ValidationError("Maximum 100 messages per batch");
|
|
858
|
+
}
|
|
859
|
+
return this.request("POST", "/api/v1/messages/batch/encrypt", {
|
|
860
|
+
body: {
|
|
861
|
+
messages,
|
|
862
|
+
user_id: this.userId,
|
|
863
|
+
layer: options?.layer || 4
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Decrypt a single message from a batch result.
|
|
869
|
+
* Uses the shared key_part_a and batch_tag from the batch result.
|
|
870
|
+
*
|
|
871
|
+
* @param batchResult The batch encrypt result
|
|
872
|
+
* @param index The index of the message to decrypt
|
|
873
|
+
*/
|
|
874
|
+
async serverDecryptBatchItem(batchResult, index) {
|
|
875
|
+
if (index < 0 || index >= batchResult.results.length) {
|
|
876
|
+
throw new ValidationError("Invalid batch index");
|
|
877
|
+
}
|
|
878
|
+
const item = batchResult.results[index];
|
|
879
|
+
return this.serverDecrypt({
|
|
880
|
+
encryptedMessage: item.encrypted_message,
|
|
881
|
+
nonce: item.nonce,
|
|
882
|
+
keyPartA: batchResult.key_part_a,
|
|
883
|
+
messageTag: batchResult.batch_tag
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
// ==========================================================================
|
|
887
|
+
// Real-Time Streaming Encryption (WebSocket)
|
|
888
|
+
// ==========================================================================
|
|
889
|
+
/**
|
|
890
|
+
* Create a streaming encryption/decryption session over WebSocket.
|
|
891
|
+
* Data flows in real-time: send chunks, receive encrypted/decrypted chunks back.
|
|
892
|
+
*
|
|
893
|
+
* @param direction 'encrypt' for plaintext→ciphertext, 'decrypt' for reverse
|
|
894
|
+
* @param streamKey Required for decrypt — the key_part_a and stream_tag from the encrypt session
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```typescript
|
|
898
|
+
* // Encrypt stream
|
|
899
|
+
* const stream = await wfx.createStream('encrypt');
|
|
900
|
+
* stream.onData((chunk, seq) => console.log('Encrypted chunk', seq));
|
|
901
|
+
* stream.send('Hello chunk 1');
|
|
902
|
+
* stream.send('Hello chunk 2');
|
|
903
|
+
* const summary = await stream.end();
|
|
904
|
+
* // Save stream.keyPartA and stream.streamTag for decryption
|
|
905
|
+
*
|
|
906
|
+
* // Decrypt stream
|
|
907
|
+
* const dStream = await wfx.createStream('decrypt', {
|
|
908
|
+
* keyPartA: stream.keyPartA!,
|
|
909
|
+
* streamTag: stream.streamTag!,
|
|
910
|
+
* });
|
|
911
|
+
* dStream.onData((chunk, seq) => console.log('Decrypted:', chunk));
|
|
912
|
+
* dStream.send(encryptedChunk1);
|
|
913
|
+
* await dStream.end();
|
|
914
|
+
* ```
|
|
915
|
+
*/
|
|
916
|
+
async createStream(direction, streamKey) {
|
|
917
|
+
this.ensureAuthenticated();
|
|
918
|
+
if (direction === "decrypt" && !streamKey) {
|
|
919
|
+
throw new ValidationError("streamKey (keyPartA + streamTag) is required for decrypt streams");
|
|
920
|
+
}
|
|
921
|
+
const stream = new WolfronixStream(this.config, this.userId);
|
|
922
|
+
await stream.connect(direction, streamKey);
|
|
923
|
+
return stream;
|
|
924
|
+
}
|
|
925
|
+
// ==========================================================================
|
|
714
926
|
// Metrics & Status
|
|
715
927
|
// ==========================================================================
|
|
716
928
|
/**
|
|
@@ -740,6 +952,186 @@ var Wolfronix = class {
|
|
|
740
952
|
}
|
|
741
953
|
}
|
|
742
954
|
};
|
|
955
|
+
var WolfronixStream = class {
|
|
956
|
+
/** @internal */
|
|
957
|
+
constructor(config, userId) {
|
|
958
|
+
this.config = config;
|
|
959
|
+
this.userId = userId;
|
|
960
|
+
this.ws = null;
|
|
961
|
+
this.dataCallbacks = [];
|
|
962
|
+
this.errorCallbacks = [];
|
|
963
|
+
this.pendingChunks = /* @__PURE__ */ new Map();
|
|
964
|
+
this.seqCounter = 0;
|
|
965
|
+
/** Client's key half (available after encrypt stream init) */
|
|
966
|
+
this.keyPartA = null;
|
|
967
|
+
/** Stream tag (available after encrypt stream init) */
|
|
968
|
+
this.streamTag = null;
|
|
969
|
+
}
|
|
970
|
+
/** @internal Connect and initialize the stream session */
|
|
971
|
+
async connect(direction, streamKey) {
|
|
972
|
+
return new Promise((resolve, reject) => {
|
|
973
|
+
const wsBase = this.config.baseUrl.replace(/^http/, "ws");
|
|
974
|
+
const params = new URLSearchParams();
|
|
975
|
+
if (this.config.wolfronixKey) {
|
|
976
|
+
params.set("wolfronix_key", this.config.wolfronixKey);
|
|
977
|
+
}
|
|
978
|
+
if (this.config.clientId) {
|
|
979
|
+
params.set("client_id", this.config.clientId);
|
|
980
|
+
}
|
|
981
|
+
const wsUrl = `${wsBase}/api/v1/stream?${params.toString()}`;
|
|
982
|
+
this.ws = new WebSocket(wsUrl);
|
|
983
|
+
this.ws.onopen = () => {
|
|
984
|
+
const initMsg = { type: "init", direction };
|
|
985
|
+
if (direction === "decrypt" && streamKey) {
|
|
986
|
+
initMsg.key_part_a = streamKey.keyPartA;
|
|
987
|
+
initMsg.stream_tag = streamKey.streamTag;
|
|
988
|
+
}
|
|
989
|
+
this.ws.send(JSON.stringify(initMsg));
|
|
990
|
+
};
|
|
991
|
+
let initResolved = false;
|
|
992
|
+
this.ws.onmessage = (event) => {
|
|
993
|
+
try {
|
|
994
|
+
const msg = JSON.parse(event.data);
|
|
995
|
+
if (msg.type === "error") {
|
|
996
|
+
const err = new Error(msg.error);
|
|
997
|
+
if (!initResolved) {
|
|
998
|
+
initResolved = true;
|
|
999
|
+
reject(err);
|
|
1000
|
+
}
|
|
1001
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (msg.type === "init_ack" && !initResolved) {
|
|
1005
|
+
initResolved = true;
|
|
1006
|
+
if (msg.key_part_a) this.keyPartA = msg.key_part_a;
|
|
1007
|
+
if (msg.stream_tag) this.streamTag = msg.stream_tag;
|
|
1008
|
+
resolve();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (msg.type === "data") {
|
|
1012
|
+
this.dataCallbacks.forEach((cb) => cb(msg.data, msg.seq));
|
|
1013
|
+
const pending = this.pendingChunks.get(msg.seq);
|
|
1014
|
+
if (pending) {
|
|
1015
|
+
pending.resolve(msg.data);
|
|
1016
|
+
this.pendingChunks.delete(msg.seq);
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (msg.type === "end_ack") {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
const err = new Error("Failed to parse stream message");
|
|
1025
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
this.ws.onerror = (event) => {
|
|
1029
|
+
const err = new Error("WebSocket error");
|
|
1030
|
+
if (!initResolved) {
|
|
1031
|
+
initResolved = true;
|
|
1032
|
+
reject(err);
|
|
1033
|
+
}
|
|
1034
|
+
this.errorCallbacks.forEach((cb) => cb(err));
|
|
1035
|
+
};
|
|
1036
|
+
this.ws.onclose = () => {
|
|
1037
|
+
this.pendingChunks.forEach((p) => p.reject(new Error("Stream closed")));
|
|
1038
|
+
this.pendingChunks.clear();
|
|
1039
|
+
};
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Send a data chunk for encryption/decryption.
|
|
1044
|
+
* Returns a promise that resolves with the processed (encrypted/decrypted) chunk.
|
|
1045
|
+
*
|
|
1046
|
+
* @param data String or base64-encoded binary data
|
|
1047
|
+
* @returns The processed chunk (base64-encoded)
|
|
1048
|
+
*/
|
|
1049
|
+
async send(data) {
|
|
1050
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1051
|
+
throw new Error("Stream not connected");
|
|
1052
|
+
}
|
|
1053
|
+
const b64Data = this.isBase64(data) ? data : btoa(data);
|
|
1054
|
+
const seq = this.seqCounter++;
|
|
1055
|
+
return new Promise((resolve, reject) => {
|
|
1056
|
+
this.pendingChunks.set(seq, { resolve, reject });
|
|
1057
|
+
this.ws.send(JSON.stringify({ type: "data", data: b64Data }));
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Send raw binary data for encryption/decryption.
|
|
1062
|
+
*
|
|
1063
|
+
* @param buffer ArrayBuffer or Uint8Array
|
|
1064
|
+
* @returns The processed chunk (base64-encoded)
|
|
1065
|
+
*/
|
|
1066
|
+
async sendBinary(buffer) {
|
|
1067
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1068
|
+
let binary = "";
|
|
1069
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
1070
|
+
binary += String.fromCharCode(bytes[i]);
|
|
1071
|
+
}
|
|
1072
|
+
const b64 = btoa(binary);
|
|
1073
|
+
return this.send(b64);
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Register a callback for incoming data chunks.
|
|
1077
|
+
*
|
|
1078
|
+
* @param callback Called with (base64Data, sequenceNumber) for each chunk
|
|
1079
|
+
*/
|
|
1080
|
+
onData(callback) {
|
|
1081
|
+
this.dataCallbacks.push(callback);
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1084
|
+
* Register a callback for stream errors.
|
|
1085
|
+
*/
|
|
1086
|
+
onError(callback) {
|
|
1087
|
+
this.errorCallbacks.push(callback);
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* End the stream session. Returns the total number of chunks processed.
|
|
1091
|
+
*/
|
|
1092
|
+
async end() {
|
|
1093
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1094
|
+
return { chunksProcessed: this.seqCounter };
|
|
1095
|
+
}
|
|
1096
|
+
return new Promise((resolve) => {
|
|
1097
|
+
const originalHandler = this.ws.onmessage;
|
|
1098
|
+
this.ws.onmessage = (event) => {
|
|
1099
|
+
try {
|
|
1100
|
+
const msg = JSON.parse(event.data);
|
|
1101
|
+
if (msg.type === "end_ack") {
|
|
1102
|
+
resolve({ chunksProcessed: msg.chunks_processed || this.seqCounter });
|
|
1103
|
+
this.ws.close();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
if (originalHandler && this.ws) originalHandler.call(this.ws, event);
|
|
1109
|
+
};
|
|
1110
|
+
this.ws.send(JSON.stringify({ type: "end" }));
|
|
1111
|
+
setTimeout(() => {
|
|
1112
|
+
resolve({ chunksProcessed: this.seqCounter });
|
|
1113
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
1114
|
+
this.ws.close();
|
|
1115
|
+
}
|
|
1116
|
+
}, 5e3);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Close the stream immediately without sending an end message.
|
|
1121
|
+
*/
|
|
1122
|
+
close() {
|
|
1123
|
+
if (this.ws) {
|
|
1124
|
+
this.ws.close();
|
|
1125
|
+
this.ws = null;
|
|
1126
|
+
}
|
|
1127
|
+
this.pendingChunks.forEach((p) => p.reject(new Error("Stream closed")));
|
|
1128
|
+
this.pendingChunks.clear();
|
|
1129
|
+
}
|
|
1130
|
+
isBase64(str) {
|
|
1131
|
+
if (str.length % 4 !== 0) return false;
|
|
1132
|
+
return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
743
1135
|
function createClient(config) {
|
|
744
1136
|
return new Wolfronix(config);
|
|
745
1137
|
}
|
|
@@ -752,6 +1144,7 @@ export {
|
|
|
752
1144
|
ValidationError,
|
|
753
1145
|
Wolfronix,
|
|
754
1146
|
WolfronixError,
|
|
1147
|
+
WolfronixStream,
|
|
755
1148
|
createClient,
|
|
756
1149
|
index_default as default
|
|
757
1150
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolfronix-sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.3.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",
|
|
@@ -46,20 +46,12 @@
|
|
|
46
46
|
},
|
|
47
47
|
"homepage": "https://wolfronix.com/docs/sdk/javascript",
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^20.10.0",
|
|
53
53
|
"tsup": "^8.0.0",
|
|
54
54
|
"typescript": "^5.3.0",
|
|
55
55
|
"vitest": "^1.0.0"
|
|
56
|
-
},
|
|
57
|
-
"peerDependencies": {
|
|
58
|
-
"node-fetch": ">=3.0.0"
|
|
59
|
-
},
|
|
60
|
-
"peerDependenciesMeta": {
|
|
61
|
-
"node-fetch": {
|
|
62
|
-
"optional": true
|
|
63
|
-
}
|
|
64
56
|
}
|
|
65
|
-
}
|
|
57
|
+
}
|