wolfronix-sdk 2.4.3 → 2.4.4

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/dist/index.mjs CHANGED
@@ -271,6 +271,641 @@ var ValidationError = class extends WolfronixError {
271
271
  this.name = "ValidationError";
272
272
  }
273
273
  };
274
+ var RECOVERY_WORDS = [
275
+ "able",
276
+ "about",
277
+ "absorb",
278
+ "access",
279
+ "acid",
280
+ "across",
281
+ "action",
282
+ "adapt",
283
+ "admit",
284
+ "adult",
285
+ "agent",
286
+ "agree",
287
+ "ahead",
288
+ "air",
289
+ "alert",
290
+ "alpha",
291
+ "anchor",
292
+ "angle",
293
+ "apple",
294
+ "arch",
295
+ "arena",
296
+ "argue",
297
+ "armed",
298
+ "arrow",
299
+ "asset",
300
+ "atlas",
301
+ "attack",
302
+ "audio",
303
+ "august",
304
+ "auto",
305
+ "avoid",
306
+ "awake",
307
+ "aware",
308
+ "badge",
309
+ "balance",
310
+ "banana",
311
+ "basic",
312
+ "beach",
313
+ "beauty",
314
+ "before",
315
+ "begin",
316
+ "below",
317
+ "benefit",
318
+ "best",
319
+ "beyond",
320
+ "bicycle",
321
+ "bird",
322
+ "black",
323
+ "bless",
324
+ "board",
325
+ "bold",
326
+ "bonus",
327
+ "border",
328
+ "borrow",
329
+ "bottle",
330
+ "bottom",
331
+ "brain",
332
+ "brand",
333
+ "brave",
334
+ "breeze",
335
+ "brick",
336
+ "brief",
337
+ "bring",
338
+ "brother",
339
+ "budget",
340
+ "build",
341
+ "camera",
342
+ "camp",
343
+ "canal",
344
+ "carbon",
345
+ "carry",
346
+ "casual",
347
+ "center",
348
+ "chain",
349
+ "change",
350
+ "charge",
351
+ "chase",
352
+ "cheap",
353
+ "check",
354
+ "chief",
355
+ "choice",
356
+ "circle",
357
+ "city",
358
+ "claim",
359
+ "class",
360
+ "clean",
361
+ "clear",
362
+ "client",
363
+ "clock",
364
+ "cloud",
365
+ "coach",
366
+ "coast",
367
+ "color",
368
+ "column",
369
+ "combo",
370
+ "common",
371
+ "concept",
372
+ "confirm",
373
+ "connect",
374
+ "copy",
375
+ "core",
376
+ "corner",
377
+ "correct",
378
+ "cost",
379
+ "cover",
380
+ "craft",
381
+ "create",
382
+ "credit",
383
+ "cross",
384
+ "crowd",
385
+ "crystal",
386
+ "current",
387
+ "custom",
388
+ "cycle",
389
+ "daily",
390
+ "danger",
391
+ "data",
392
+ "dealer",
393
+ "debate",
394
+ "decide",
395
+ "deep",
396
+ "define",
397
+ "degree",
398
+ "delay",
399
+ "demand",
400
+ "denial",
401
+ "design",
402
+ "detail",
403
+ "device",
404
+ "dialog",
405
+ "digital",
406
+ "direct",
407
+ "doctor",
408
+ "domain",
409
+ "double",
410
+ "draft",
411
+ "dragon",
412
+ "drama",
413
+ "dream",
414
+ "drive",
415
+ "early",
416
+ "earth",
417
+ "easy",
418
+ "echo",
419
+ "edge",
420
+ "edit",
421
+ "effect",
422
+ "either",
423
+ "elder",
424
+ "element",
425
+ "elite",
426
+ "email",
427
+ "energy",
428
+ "engine",
429
+ "enough",
430
+ "enter",
431
+ "equal",
432
+ "error",
433
+ "escape",
434
+ "estate",
435
+ "event",
436
+ "exact",
437
+ "example",
438
+ "exchange",
439
+ "exist",
440
+ "expand",
441
+ "expect",
442
+ "expert",
443
+ "extra",
444
+ "fabric",
445
+ "factor",
446
+ "family",
447
+ "famous",
448
+ "feature",
449
+ "fence",
450
+ "field",
451
+ "figure",
452
+ "filter",
453
+ "final",
454
+ "finger",
455
+ "finish",
456
+ "first",
457
+ "focus",
458
+ "follow",
459
+ "force",
460
+ "forest",
461
+ "format",
462
+ "forward",
463
+ "frame",
464
+ "fresh",
465
+ "front",
466
+ "future",
467
+ "gallery",
468
+ "general",
469
+ "giant",
470
+ "global",
471
+ "gold",
472
+ "good",
473
+ "grace",
474
+ "grant",
475
+ "green",
476
+ "group",
477
+ "guard",
478
+ "habit",
479
+ "half",
480
+ "hammer",
481
+ "handle",
482
+ "happy",
483
+ "harbor",
484
+ "health",
485
+ "height",
486
+ "hidden",
487
+ "history",
488
+ "honest",
489
+ "host",
490
+ "hotel",
491
+ "human",
492
+ "hybrid",
493
+ "idea",
494
+ "image",
495
+ "impact",
496
+ "income",
497
+ "index",
498
+ "input",
499
+ "inside",
500
+ "insight",
501
+ "island",
502
+ "item",
503
+ "jacket",
504
+ "jazz",
505
+ "join",
506
+ "jungle",
507
+ "keep",
508
+ "keyboard",
509
+ "kind",
510
+ "king",
511
+ "kitchen",
512
+ "label",
513
+ "ladder",
514
+ "language",
515
+ "large",
516
+ "laser",
517
+ "later",
518
+ "launch",
519
+ "layer",
520
+ "leader",
521
+ "learn",
522
+ "level",
523
+ "light",
524
+ "limit",
525
+ "linear",
526
+ "link",
527
+ "listen",
528
+ "local",
529
+ "logic",
530
+ "lucky",
531
+ "machine",
532
+ "magic",
533
+ "major",
534
+ "manage",
535
+ "manual",
536
+ "market",
537
+ "master",
538
+ "matrix",
539
+ "matter",
540
+ "member",
541
+ "memory",
542
+ "message",
543
+ "method",
544
+ "middle",
545
+ "million",
546
+ "mind",
547
+ "mirror",
548
+ "mobile",
549
+ "model",
550
+ "module",
551
+ "moment",
552
+ "monitor",
553
+ "moral",
554
+ "motion",
555
+ "mountain",
556
+ "music",
557
+ "native",
558
+ "nature",
559
+ "network",
560
+ "never",
561
+ "normal",
562
+ "notice",
563
+ "number",
564
+ "object",
565
+ "ocean",
566
+ "offer",
567
+ "office",
568
+ "online",
569
+ "option",
570
+ "orange",
571
+ "order",
572
+ "origin",
573
+ "output",
574
+ "owner",
575
+ "packet",
576
+ "panel",
577
+ "paper",
578
+ "parent",
579
+ "partner",
580
+ "pattern",
581
+ "pause",
582
+ "payment",
583
+ "people",
584
+ "perfect",
585
+ "phone",
586
+ "phrase",
587
+ "pilot",
588
+ "pixel",
589
+ "planet",
590
+ "platform",
591
+ "please",
592
+ "plus",
593
+ "policy",
594
+ "portal",
595
+ "position",
596
+ "power",
597
+ "predict",
598
+ "premium",
599
+ "prepare",
600
+ "present",
601
+ "pretty",
602
+ "price",
603
+ "prime",
604
+ "private",
605
+ "process",
606
+ "profile",
607
+ "project",
608
+ "protect",
609
+ "public",
610
+ "quality",
611
+ "quick",
612
+ "quiet",
613
+ "radio",
614
+ "random",
615
+ "rapid",
616
+ "rate",
617
+ "ready",
618
+ "reason",
619
+ "record",
620
+ "recover",
621
+ "region",
622
+ "release",
623
+ "remote",
624
+ "repair",
625
+ "repeat",
626
+ "report",
627
+ "request",
628
+ "result",
629
+ "return",
630
+ "review",
631
+ "right",
632
+ "rival",
633
+ "river",
634
+ "robot",
635
+ "route",
636
+ "royal",
637
+ "safe",
638
+ "sample",
639
+ "scale",
640
+ "scene",
641
+ "school",
642
+ "science",
643
+ "screen",
644
+ "search",
645
+ "secure",
646
+ "select",
647
+ "seller",
648
+ "senior",
649
+ "series",
650
+ "server",
651
+ "session",
652
+ "shadow",
653
+ "shape",
654
+ "share",
655
+ "shield",
656
+ "shift",
657
+ "ship",
658
+ "short",
659
+ "signal",
660
+ "silver",
661
+ "simple",
662
+ "single",
663
+ "skill",
664
+ "smart",
665
+ "smooth",
666
+ "social",
667
+ "solid",
668
+ "source",
669
+ "space",
670
+ "special",
671
+ "speed",
672
+ "spirit",
673
+ "split",
674
+ "square",
675
+ "stable",
676
+ "stack",
677
+ "stage",
678
+ "start",
679
+ "state",
680
+ "status",
681
+ "steel",
682
+ "step",
683
+ "stock",
684
+ "store",
685
+ "storm",
686
+ "story",
687
+ "stream",
688
+ "strike",
689
+ "strong",
690
+ "studio",
691
+ "style",
692
+ "subject",
693
+ "submit",
694
+ "success",
695
+ "sudden",
696
+ "sugar",
697
+ "supply",
698
+ "support",
699
+ "surface",
700
+ "switch",
701
+ "system",
702
+ "table",
703
+ "target",
704
+ "task",
705
+ "team",
706
+ "temple",
707
+ "tempo",
708
+ "tenant",
709
+ "term",
710
+ "test",
711
+ "theme",
712
+ "theory",
713
+ "thing",
714
+ "thread",
715
+ "time",
716
+ "title",
717
+ "token",
718
+ "tool",
719
+ "topic",
720
+ "total",
721
+ "tower",
722
+ "track",
723
+ "trade",
724
+ "traffic",
725
+ "train",
726
+ "travel",
727
+ "trust",
728
+ "tunnel",
729
+ "type",
730
+ "unable",
731
+ "update",
732
+ "upload",
733
+ "usage",
734
+ "useful",
735
+ "user",
736
+ "valid",
737
+ "value",
738
+ "vector",
739
+ "verify",
740
+ "version",
741
+ "video",
742
+ "view",
743
+ "virtual",
744
+ "vision",
745
+ "voice",
746
+ "volume",
747
+ "wait",
748
+ "wallet",
749
+ "watch",
750
+ "water",
751
+ "wealth",
752
+ "web",
753
+ "welcome",
754
+ "window",
755
+ "winner",
756
+ "wire",
757
+ "wise",
758
+ "wonder",
759
+ "work",
760
+ "world",
761
+ "write",
762
+ "xenon",
763
+ "year",
764
+ "yield",
765
+ "zone"
766
+ ];
767
+ function randomInt(maxExclusive) {
768
+ const values = new Uint32Array(1);
769
+ globalThis.crypto.getRandomValues(values);
770
+ return values[0] % maxExclusive;
771
+ }
772
+ function generateRecoveryWords(count = 24) {
773
+ const words = [];
774
+ for (let i = 0; i < count; i++) {
775
+ words.push(RECOVERY_WORDS[randomInt(RECOVERY_WORDS.length)]);
776
+ }
777
+ return words;
778
+ }
779
+ var PFS_PROTOCOL = "wfx-dr-v1";
780
+ var ZERO_32 = new Uint8Array(32);
781
+ function toBase64(buf) {
782
+ if (typeof Buffer !== "undefined") {
783
+ return Buffer.from(buf).toString("base64");
784
+ }
785
+ const bytes = new Uint8Array(buf);
786
+ let binary = "";
787
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
788
+ return btoa(binary);
789
+ }
790
+ function fromBase64(b64) {
791
+ if (typeof Buffer !== "undefined") {
792
+ const buf = Buffer.from(b64, "base64");
793
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
794
+ }
795
+ const binary = atob(b64);
796
+ const bytes = new Uint8Array(binary.length);
797
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
798
+ return bytes.buffer;
799
+ }
800
+ function normalizeJwk(jwk) {
801
+ return JSON.stringify({
802
+ kty: jwk.kty || "",
803
+ crv: jwk.crv || "",
804
+ x: jwk.x || "",
805
+ y: jwk.y || ""
806
+ });
807
+ }
808
+ function ratchetKeyId(jwk, n) {
809
+ const j = normalizeJwk(jwk);
810
+ if (typeof Buffer !== "undefined") {
811
+ return `${Buffer.from(j).toString("base64")}:${n}`;
812
+ }
813
+ return `${btoa(j)}:${n}`;
814
+ }
815
+ async function generatePfsRatchetKeyPair() {
816
+ return globalThis.crypto.subtle.generateKey(
817
+ { name: "ECDH", namedCurve: "P-256" },
818
+ true,
819
+ ["deriveBits"]
820
+ );
821
+ }
822
+ async function exportPublicJwk(key) {
823
+ return globalThis.crypto.subtle.exportKey("jwk", key);
824
+ }
825
+ async function exportPrivateJwk(key) {
826
+ return globalThis.crypto.subtle.exportKey("jwk", key);
827
+ }
828
+ async function importPfsPublicJwk(jwk) {
829
+ return globalThis.crypto.subtle.importKey(
830
+ "jwk",
831
+ jwk,
832
+ { name: "ECDH", namedCurve: "P-256" },
833
+ false,
834
+ []
835
+ );
836
+ }
837
+ async function importPfsPrivateJwk(jwk) {
838
+ return globalThis.crypto.subtle.importKey(
839
+ "jwk",
840
+ jwk,
841
+ { name: "ECDH", namedCurve: "P-256" },
842
+ false,
843
+ ["deriveBits"]
844
+ );
845
+ }
846
+ async function deriveEcdhSecret(privateJwk, publicJwk) {
847
+ const priv = await importPfsPrivateJwk(privateJwk);
848
+ const pub = await importPfsPublicJwk(publicJwk);
849
+ return globalThis.crypto.subtle.deriveBits({ name: "ECDH", public: pub }, priv, 256);
850
+ }
851
+ async function hkdfExpand(ikm, salt, info, outBits) {
852
+ const ikmKey = await globalThis.crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
853
+ return globalThis.crypto.subtle.deriveBits(
854
+ {
855
+ name: "HKDF",
856
+ hash: "SHA-256",
857
+ salt,
858
+ info: new TextEncoder().encode(info)
859
+ },
860
+ ikmKey,
861
+ outBits
862
+ );
863
+ }
864
+ async function hmacSha256(keyRaw, input) {
865
+ const key = await globalThis.crypto.subtle.importKey(
866
+ "raw",
867
+ keyRaw,
868
+ { name: "HMAC", hash: "SHA-256" },
869
+ false,
870
+ ["sign"]
871
+ );
872
+ return globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(input));
873
+ }
874
+ async function deriveRootAndChains(rootKeyB64, dhSecret) {
875
+ const rootKeyRaw = rootKeyB64 ? fromBase64(rootKeyB64) : ZERO_32.buffer;
876
+ const mixed = await hkdfExpand(dhSecret, rootKeyRaw, `${PFS_PROTOCOL}:root`, 96 * 8);
877
+ const bytes = new Uint8Array(mixed);
878
+ return {
879
+ rootKey: toBase64(bytes.slice(0, 32).buffer),
880
+ chainA: toBase64(bytes.slice(32, 64).buffer),
881
+ chainB: toBase64(bytes.slice(64, 96).buffer)
882
+ };
883
+ }
884
+ async function deriveMessageKey(chainKeyB64, n) {
885
+ return hmacSha256(fromBase64(chainKeyB64), `msg:${n}`);
886
+ }
887
+ async function deriveNextChainKey(chainKeyB64) {
888
+ const next = await hmacSha256(fromBase64(chainKeyB64), "chain");
889
+ return toBase64(next);
890
+ }
891
+ async function encryptWithRawKey(rawKey, plaintext) {
892
+ const key = await globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt"]);
893
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
894
+ const out = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
895
+ return {
896
+ ciphertext: toBase64(out),
897
+ iv: toBase64(iv.buffer)
898
+ };
899
+ }
900
+ async function decryptWithRawKey(rawKey, ciphertextB64, ivB64) {
901
+ const key = await globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
902
+ const out = await globalThis.crypto.subtle.decrypt(
903
+ { name: "AES-GCM", iv: new Uint8Array(fromBase64(ivB64)) },
904
+ key,
905
+ fromBase64(ciphertextB64)
906
+ );
907
+ return new TextDecoder().decode(out);
908
+ }
274
909
  var Wolfronix = class {
275
910
  /**
276
911
  * Create a new Wolfronix client
@@ -291,6 +926,9 @@ var Wolfronix = class {
291
926
  this.publicKey = null;
292
927
  this.privateKey = null;
293
928
  this.publicKeyPEM = null;
929
+ this.pfsIdentityPrivateJwk = null;
930
+ this.pfsIdentityPublicJwk = null;
931
+ this.pfsSessions = /* @__PURE__ */ new Map();
294
932
  if (typeof config === "string") {
295
933
  this.config = {
296
934
  baseUrl: config,
@@ -410,6 +1048,59 @@ var Wolfronix = class {
410
1048
  throw new AuthenticationError("Not authenticated. Call login() or register() first.");
411
1049
  }
412
1050
  }
1051
+ toBlob(file) {
1052
+ if (file instanceof File || file instanceof Blob) {
1053
+ return file;
1054
+ }
1055
+ if (file instanceof ArrayBuffer) {
1056
+ return new Blob([new Uint8Array(file)]);
1057
+ }
1058
+ if (file instanceof Uint8Array) {
1059
+ const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
1060
+ return new Blob([arrayBuffer]);
1061
+ }
1062
+ throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
1063
+ }
1064
+ async ensurePfsIdentity() {
1065
+ if (this.pfsIdentityPrivateJwk && this.pfsIdentityPublicJwk) {
1066
+ return;
1067
+ }
1068
+ const kp = await generatePfsRatchetKeyPair();
1069
+ this.pfsIdentityPrivateJwk = await exportPrivateJwk(kp.privateKey);
1070
+ this.pfsIdentityPublicJwk = await exportPublicJwk(kp.publicKey);
1071
+ }
1072
+ getPfsSession(sessionId) {
1073
+ const session = this.pfsSessions.get(sessionId);
1074
+ if (!session) {
1075
+ throw new ValidationError(`PFS session not found: ${sessionId}`);
1076
+ }
1077
+ return session;
1078
+ }
1079
+ async ratchetForSend(session) {
1080
+ const nextRatchet = await generatePfsRatchetKeyPair();
1081
+ const nextPriv = await exportPrivateJwk(nextRatchet.privateKey);
1082
+ const nextPub = await exportPublicJwk(nextRatchet.publicKey);
1083
+ const dh = await deriveEcdhSecret(nextPriv, session.their_ratchet_public_jwk);
1084
+ const mixed = await deriveRootAndChains(session.root_key, dh);
1085
+ session.root_key = mixed.rootKey;
1086
+ session.send_chain_key = mixed.chainA;
1087
+ session.recv_chain_key = mixed.chainB;
1088
+ session.prev_send_count = session.send_count;
1089
+ session.send_count = 0;
1090
+ session.my_ratchet_private_jwk = nextPriv;
1091
+ session.my_ratchet_public_jwk = nextPub;
1092
+ session.updated_at = Date.now();
1093
+ }
1094
+ async ratchetForReceive(session, theirRatchetPub) {
1095
+ const dh = await deriveEcdhSecret(session.my_ratchet_private_jwk, theirRatchetPub);
1096
+ const mixed = await deriveRootAndChains(session.root_key, dh);
1097
+ session.root_key = mixed.rootKey;
1098
+ session.recv_chain_key = mixed.chainA;
1099
+ session.send_chain_key = mixed.chainB;
1100
+ session.recv_count = 0;
1101
+ session.their_ratchet_public_jwk = theirRatchetPub;
1102
+ session.updated_at = Date.now();
1103
+ }
413
1104
  // ==========================================================================
414
1105
  // Authentication Methods
415
1106
  // ==========================================================================
@@ -421,13 +1112,23 @@ var Wolfronix = class {
421
1112
  * const { user_id, token } = await wfx.register('user@example.com', 'password123');
422
1113
  * ```
423
1114
  */
424
- async register(email, password) {
1115
+ async register(email, password, options = {}) {
425
1116
  if (!email || !password) {
426
1117
  throw new ValidationError("Email and password are required");
427
1118
  }
428
1119
  const keyPair = await generateKeyPair();
429
1120
  const publicKeyPEM = await exportKeyToPEM(keyPair.publicKey, "public");
430
1121
  const { encryptedKey, salt } = await wrapPrivateKey(keyPair.privateKey, password);
1122
+ const enableRecovery = options.enableRecovery !== false;
1123
+ const recoveryWords = enableRecovery ? options.recoveryPhrase ? options.recoveryPhrase.trim().split(/\s+/).filter(Boolean) : generateRecoveryWords(24) : [];
1124
+ const recoveryPhrase = recoveryWords.join(" ");
1125
+ let recoveryEncryptedPrivateKey = "";
1126
+ let recoverySalt = "";
1127
+ if (enableRecovery && recoveryPhrase) {
1128
+ const recoveryWrap = await wrapPrivateKey(keyPair.privateKey, recoveryPhrase);
1129
+ recoveryEncryptedPrivateKey = recoveryWrap.encryptedKey;
1130
+ recoverySalt = recoveryWrap.salt;
1131
+ }
431
1132
  const response = await this.request("POST", "/api/v1/keys/register", {
432
1133
  body: {
433
1134
  client_id: this.config.clientId,
@@ -435,18 +1136,30 @@ var Wolfronix = class {
435
1136
  // Using email as user_id for simplicity
436
1137
  public_key_pem: publicKeyPEM,
437
1138
  encrypted_private_key: encryptedKey,
438
- salt
1139
+ salt,
1140
+ recovery_encrypted_private_key: recoveryEncryptedPrivateKey,
1141
+ recovery_salt: recoverySalt
439
1142
  },
440
1143
  includeAuth: false
441
1144
  });
442
- if (response.success) {
1145
+ if (response.status === "success" || response.success) {
443
1146
  this.userId = email;
444
1147
  this.publicKey = keyPair.publicKey;
445
1148
  this.privateKey = keyPair.privateKey;
446
1149
  this.publicKeyPEM = publicKeyPEM;
447
1150
  this.token = "zk-session";
448
1151
  }
449
- return response;
1152
+ const out = {
1153
+ success: response.status === "success" || response.success === true,
1154
+ user_id: response.user_id || email,
1155
+ token: this.token || "zk-session",
1156
+ message: response.message || "Keys registered successfully"
1157
+ };
1158
+ if (enableRecovery && recoveryPhrase) {
1159
+ out.recoveryPhrase = recoveryPhrase;
1160
+ out.recoveryWords = recoveryWords;
1161
+ }
1162
+ return out;
450
1163
  }
451
1164
  /**
452
1165
  * Login with existing credentials
@@ -490,6 +1203,69 @@ var Wolfronix = class {
490
1203
  throw new AuthenticationError("Invalid password (decryption failed)");
491
1204
  }
492
1205
  }
1206
+ /**
1207
+ * Recover account keys using a 24-word recovery phrase and set a new password.
1208
+ * Returns a fresh local auth session if recovery succeeds.
1209
+ */
1210
+ async recoverAccount(email, recoveryPhrase, newPassword) {
1211
+ if (!email || !recoveryPhrase || !newPassword) {
1212
+ throw new ValidationError("email, recoveryPhrase, and newPassword are required");
1213
+ }
1214
+ const response = await this.request("POST", "/api/v1/keys/recover", {
1215
+ body: {
1216
+ client_id: this.config.clientId,
1217
+ user_id: email
1218
+ },
1219
+ includeAuth: false
1220
+ });
1221
+ if (!response.recovery_encrypted_private_key || !response.recovery_salt || !response.public_key_pem) {
1222
+ throw new AuthenticationError("Recovery material not found for this account");
1223
+ }
1224
+ const recoveredPrivateKey = await unwrapPrivateKey(
1225
+ response.recovery_encrypted_private_key,
1226
+ recoveryPhrase,
1227
+ response.recovery_salt
1228
+ );
1229
+ const newPasswordWrap = await wrapPrivateKey(recoveredPrivateKey, newPassword);
1230
+ await this.request("POST", "/api/v1/keys/update-password", {
1231
+ body: {
1232
+ client_id: this.config.clientId,
1233
+ user_id: email,
1234
+ encrypted_private_key: newPasswordWrap.encryptedKey,
1235
+ salt: newPasswordWrap.salt
1236
+ },
1237
+ includeAuth: false
1238
+ });
1239
+ this.privateKey = recoveredPrivateKey;
1240
+ this.publicKeyPEM = response.public_key_pem;
1241
+ this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
1242
+ this.userId = email;
1243
+ this.token = "zk-session";
1244
+ return {
1245
+ success: true,
1246
+ user_id: email,
1247
+ token: this.token,
1248
+ message: "Account recovered successfully"
1249
+ };
1250
+ }
1251
+ /**
1252
+ * Rotates long-term RSA identity keys and re-wraps with password (+ optional recovery phrase).
1253
+ * Use this periodically to reduce long-term key exposure.
1254
+ */
1255
+ async rotateIdentityKeys(password, recoveryPhrase) {
1256
+ this.ensureAuthenticated();
1257
+ if (!password) {
1258
+ throw new ValidationError("password is required");
1259
+ }
1260
+ if (recoveryPhrase !== void 0 && !recoveryPhrase.trim()) {
1261
+ throw new ValidationError("recoveryPhrase must be non-empty when provided");
1262
+ }
1263
+ throw new WolfronixError(
1264
+ "rotateIdentityKeys is not supported by the current server API. Use recoverAccount() to re-wrap the existing private key with a new password.",
1265
+ "NOT_SUPPORTED",
1266
+ 501
1267
+ );
1268
+ }
493
1269
  /**
494
1270
  * Set authentication token directly (useful for server-side apps)
495
1271
  *
@@ -546,20 +1322,8 @@ var Wolfronix = class {
546
1322
  async encrypt(file, filename) {
547
1323
  this.ensureAuthenticated();
548
1324
  const formData = new FormData();
549
- if (file instanceof File) {
550
- formData.append("file", file);
551
- } else if (file instanceof Blob) {
552
- formData.append("file", file, filename || "file");
553
- } else if (file instanceof ArrayBuffer) {
554
- const blob = new Blob([new Uint8Array(file)]);
555
- formData.append("file", blob, filename || "file");
556
- } else if (file instanceof Uint8Array) {
557
- const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
558
- const blob = new Blob([arrayBuffer]);
559
- formData.append("file", blob, filename || "file");
560
- } else {
561
- throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
562
- }
1325
+ const blob = this.toBlob(file);
1326
+ formData.append("file", blob, filename || (file instanceof File ? file.name : "file"));
563
1327
  formData.append("user_id", this.userId || "");
564
1328
  if (!this.publicKeyPEM) {
565
1329
  throw new Error("Public key not available. Is user logged in?");
@@ -573,6 +1337,65 @@ var Wolfronix = class {
573
1337
  file_id: String(response.file_id)
574
1338
  };
575
1339
  }
1340
+ /**
1341
+ * Resumable large-file encryption upload.
1342
+ * Splits a file into chunks (default 10MB) and uploads each chunk independently.
1343
+ * If upload fails mid-way, pass the returned state as `existingState` to resume.
1344
+ */
1345
+ async encryptResumable(file, options = {}) {
1346
+ this.ensureAuthenticated();
1347
+ const chunkSize = options.chunkSizeBytes || 10 * 1024 * 1024;
1348
+ if (chunkSize < 1024 * 1024) {
1349
+ throw new ValidationError("chunkSizeBytes must be at least 1MB");
1350
+ }
1351
+ const blob = this.toBlob(file);
1352
+ const filename = options.filename || (file instanceof File ? file.name : "file.bin");
1353
+ const totalChunks = Math.ceil(blob.size / chunkSize);
1354
+ const baseUploadId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1355
+ const state = options.existingState || {
1356
+ upload_id: baseUploadId,
1357
+ filename,
1358
+ file_size: blob.size,
1359
+ chunk_size_bytes: chunkSize,
1360
+ total_chunks: totalChunks,
1361
+ uploaded_chunks: [],
1362
+ chunk_file_ids: new Array(totalChunks).fill(""),
1363
+ created_at: Date.now(),
1364
+ updated_at: Date.now()
1365
+ };
1366
+ if (state.file_size !== blob.size || state.total_chunks !== totalChunks) {
1367
+ throw new ValidationError("existingState does not match current file/chunking settings");
1368
+ }
1369
+ const uploadedSet = new Set(state.uploaded_chunks);
1370
+ let uploaded = uploadedSet.size;
1371
+ for (let i = 0; i < totalChunks; i++) {
1372
+ if (uploadedSet.has(i)) {
1373
+ continue;
1374
+ }
1375
+ const start = i * chunkSize;
1376
+ const end = Math.min(start + chunkSize, blob.size);
1377
+ const chunkBlob = blob.slice(start, end);
1378
+ const chunkName = `${filename}.part-${String(i + 1).padStart(6, "0")}-of-${String(totalChunks).padStart(6, "0")}`;
1379
+ const enc = await this.encrypt(chunkBlob, chunkName);
1380
+ state.chunk_file_ids[i] = enc.file_id;
1381
+ state.uploaded_chunks.push(i);
1382
+ state.updated_at = Date.now();
1383
+ uploaded++;
1384
+ if (options.onProgress) {
1385
+ options.onProgress(uploaded, totalChunks);
1386
+ }
1387
+ }
1388
+ const result = {
1389
+ upload_id: state.upload_id,
1390
+ filename: state.filename,
1391
+ total_chunks: state.total_chunks,
1392
+ chunk_size_bytes: state.chunk_size_bytes,
1393
+ uploaded_chunks: state.uploaded_chunks.length,
1394
+ chunk_file_ids: state.chunk_file_ids,
1395
+ complete: state.uploaded_chunks.length === state.total_chunks
1396
+ };
1397
+ return { result, state };
1398
+ }
576
1399
  /**
577
1400
  * Decrypt and retrieve a file using zero-knowledge flow.
578
1401
  *
@@ -633,6 +1456,41 @@ var Wolfronix = class {
633
1456
  }
634
1457
  });
635
1458
  }
1459
+ /**
1460
+ * Decrypts and reassembles a chunked upload produced by `encryptResumable`.
1461
+ */
1462
+ async decryptChunkedToBuffer(manifest, role = "owner") {
1463
+ this.ensureAuthenticated();
1464
+ if (!manifest?.chunk_file_ids?.length) {
1465
+ throw new ValidationError("manifest.chunk_file_ids is required");
1466
+ }
1467
+ const chunks = [];
1468
+ let totalLength = 0;
1469
+ for (const fileId of manifest.chunk_file_ids) {
1470
+ if (!fileId) {
1471
+ throw new ValidationError("manifest contains empty chunk file ID");
1472
+ }
1473
+ const part = await this.decryptToBuffer(fileId, role);
1474
+ const bytes = new Uint8Array(part);
1475
+ chunks.push(bytes);
1476
+ totalLength += bytes.byteLength;
1477
+ }
1478
+ const merged = new Uint8Array(totalLength);
1479
+ let offset = 0;
1480
+ for (const part of chunks) {
1481
+ merged.set(part, offset);
1482
+ offset += part.byteLength;
1483
+ }
1484
+ return merged.buffer;
1485
+ }
1486
+ /**
1487
+ * Decrypts and reassembles a chunked upload into a Blob.
1488
+ * This is a browser-friendly alias over `decryptChunkedToBuffer`.
1489
+ */
1490
+ async decryptChunkedManifest(manifest, role = "owner") {
1491
+ const merged = await this.decryptChunkedToBuffer(manifest, role);
1492
+ return new Blob([merged], { type: "application/octet-stream" });
1493
+ }
636
1494
  /**
637
1495
  * Fetch the encrypted key_part_a for a file (for client-side decryption)
638
1496
  *
@@ -754,6 +1612,197 @@ var Wolfronix = class {
754
1612
  throw new Error("Decryption failed. You may not be the intended recipient.");
755
1613
  }
756
1614
  }
1615
+ /**
1616
+ * Create/share a pre-key bundle for Double Ratchet PFS session setup.
1617
+ * Exchange this bundle out-of-band with the peer.
1618
+ */
1619
+ async createPfsPreKeyBundle() {
1620
+ this.ensureAuthenticated();
1621
+ await this.ensurePfsIdentity();
1622
+ return {
1623
+ protocol: "wfx-dr-v1",
1624
+ user_id: this.userId || void 0,
1625
+ ratchet_pub_jwk: this.pfsIdentityPublicJwk,
1626
+ created_at: Date.now()
1627
+ };
1628
+ }
1629
+ /**
1630
+ * Initialize a local PFS ratchet session from peer bundle.
1631
+ * Both sides must call this with opposite `asInitiator` values.
1632
+ */
1633
+ async initPfsSession(sessionId, peerBundle, asInitiator) {
1634
+ this.ensureAuthenticated();
1635
+ if (!sessionId) {
1636
+ throw new ValidationError("sessionId is required");
1637
+ }
1638
+ if (!peerBundle || peerBundle.protocol !== PFS_PROTOCOL || !peerBundle.ratchet_pub_jwk) {
1639
+ throw new ValidationError("Invalid peerBundle");
1640
+ }
1641
+ await this.ensurePfsIdentity();
1642
+ const myPriv = this.pfsIdentityPrivateJwk;
1643
+ const myPub = this.pfsIdentityPublicJwk;
1644
+ const theirPub = peerBundle.ratchet_pub_jwk;
1645
+ const dh = await deriveEcdhSecret(myPriv, theirPub);
1646
+ const mixed = await deriveRootAndChains(toBase64(ZERO_32.buffer), dh);
1647
+ const session = {
1648
+ protocol: "wfx-dr-v1",
1649
+ session_id: sessionId,
1650
+ role: asInitiator ? "initiator" : "responder",
1651
+ root_key: mixed.rootKey,
1652
+ send_chain_key: asInitiator ? mixed.chainA : mixed.chainB,
1653
+ recv_chain_key: asInitiator ? mixed.chainB : mixed.chainA,
1654
+ send_count: 0,
1655
+ recv_count: 0,
1656
+ prev_send_count: 0,
1657
+ my_ratchet_private_jwk: myPriv,
1658
+ my_ratchet_public_jwk: myPub,
1659
+ their_ratchet_public_jwk: theirPub,
1660
+ skipped_keys: {},
1661
+ created_at: Date.now(),
1662
+ updated_at: Date.now()
1663
+ };
1664
+ this.pfsSessions.set(sessionId, session);
1665
+ return session;
1666
+ }
1667
+ /**
1668
+ * Export session state for persistence (e.g., localStorage/DB).
1669
+ */
1670
+ exportPfsSession(sessionId) {
1671
+ const session = this.getPfsSession(sessionId);
1672
+ return JSON.parse(JSON.stringify(session));
1673
+ }
1674
+ /**
1675
+ * Import session state from storage.
1676
+ */
1677
+ importPfsSession(session) {
1678
+ if (!session || session.protocol !== PFS_PROTOCOL || !session.session_id) {
1679
+ throw new ValidationError("Invalid PFS session payload");
1680
+ }
1681
+ this.pfsSessions.set(session.session_id, JSON.parse(JSON.stringify(session)));
1682
+ }
1683
+ /**
1684
+ * Encrypt a message using Double Ratchet session state.
1685
+ */
1686
+ async pfsEncryptMessage(sessionId, plaintext) {
1687
+ this.ensureAuthenticated();
1688
+ if (!plaintext) {
1689
+ throw new ValidationError("plaintext is required");
1690
+ }
1691
+ const session = this.getPfsSession(sessionId);
1692
+ await this.ratchetForSend(session);
1693
+ const n = session.send_count;
1694
+ const msgKey = await deriveMessageKey(session.send_chain_key, n);
1695
+ const enc = await encryptWithRawKey(msgKey, plaintext);
1696
+ session.send_chain_key = await deriveNextChainKey(session.send_chain_key);
1697
+ session.send_count += 1;
1698
+ session.updated_at = Date.now();
1699
+ return {
1700
+ v: 1,
1701
+ type: "pfs_ratchet",
1702
+ session_id: sessionId,
1703
+ n,
1704
+ pn: session.prev_send_count,
1705
+ ratchet_pub_jwk: session.my_ratchet_public_jwk,
1706
+ iv: enc.iv,
1707
+ ciphertext: enc.ciphertext,
1708
+ timestamp: Date.now()
1709
+ };
1710
+ }
1711
+ /**
1712
+ * Decrypt a Double Ratchet packet for a session.
1713
+ * Handles basic out-of-order delivery through skipped message keys.
1714
+ */
1715
+ async pfsDecryptMessage(sessionId, packet) {
1716
+ this.ensureAuthenticated();
1717
+ const session = this.getPfsSession(sessionId);
1718
+ const msg = typeof packet === "string" ? JSON.parse(packet) : packet;
1719
+ if (!msg || msg.type !== "pfs_ratchet" || msg.session_id !== sessionId) {
1720
+ throw new ValidationError("Invalid PFS message packet");
1721
+ }
1722
+ if (normalizeJwk(msg.ratchet_pub_jwk) !== normalizeJwk(session.their_ratchet_public_jwk)) {
1723
+ await this.ratchetForReceive(session, msg.ratchet_pub_jwk);
1724
+ }
1725
+ while (session.recv_count < msg.n) {
1726
+ const skippedKey = await deriveMessageKey(session.recv_chain_key, session.recv_count);
1727
+ session.skipped_keys[ratchetKeyId(session.their_ratchet_public_jwk, session.recv_count)] = toBase64(skippedKey);
1728
+ session.recv_chain_key = await deriveNextChainKey(session.recv_chain_key);
1729
+ session.recv_count += 1;
1730
+ }
1731
+ const skipId = ratchetKeyId(session.their_ratchet_public_jwk, msg.n);
1732
+ let msgKey;
1733
+ if (session.skipped_keys[skipId]) {
1734
+ msgKey = fromBase64(session.skipped_keys[skipId]);
1735
+ delete session.skipped_keys[skipId];
1736
+ } else {
1737
+ msgKey = await deriveMessageKey(session.recv_chain_key, msg.n);
1738
+ session.recv_chain_key = await deriveNextChainKey(session.recv_chain_key);
1739
+ session.recv_count = msg.n + 1;
1740
+ }
1741
+ session.updated_at = Date.now();
1742
+ return decryptWithRawKey(msgKey, msg.ciphertext, msg.iv);
1743
+ }
1744
+ /**
1745
+ * Group message encryption using sender-key fanout:
1746
+ * message encrypted once with AES key, AES key wrapped for each group member with their RSA public key.
1747
+ */
1748
+ async encryptGroupMessage(text, groupId, recipientIds) {
1749
+ this.ensureAuthenticated();
1750
+ if (!text || !groupId) {
1751
+ throw new ValidationError("text and groupId are required");
1752
+ }
1753
+ if (!recipientIds?.length) {
1754
+ throw new ValidationError("recipientIds cannot be empty");
1755
+ }
1756
+ const uniqueRecipients = Array.from(new Set(recipientIds.filter(Boolean)));
1757
+ if (this.userId && !uniqueRecipients.includes(this.userId)) {
1758
+ uniqueRecipients.push(this.userId);
1759
+ }
1760
+ const sessionKey = await generateSessionKey();
1761
+ const { encrypted: ciphertext, iv } = await encryptData(text, sessionKey);
1762
+ const rawSessionKey = await exportSessionKey(sessionKey);
1763
+ const recipientKeys = {};
1764
+ for (const rid of uniqueRecipients) {
1765
+ const pem = await this.getPublicKey(rid);
1766
+ const pub = await importKeyFromPEM(pem, "public");
1767
+ recipientKeys[rid] = await rsaEncrypt(rawSessionKey, pub);
1768
+ }
1769
+ const packet = {
1770
+ v: 1,
1771
+ type: "group_sender_key",
1772
+ sender_id: this.userId || "",
1773
+ group_id: groupId,
1774
+ timestamp: Date.now(),
1775
+ ciphertext,
1776
+ iv,
1777
+ recipient_keys: recipientKeys
1778
+ };
1779
+ return JSON.stringify(packet);
1780
+ }
1781
+ /**
1782
+ * Decrypt a packet produced by `encryptGroupMessage`.
1783
+ */
1784
+ async decryptGroupMessage(packetJson) {
1785
+ this.ensureAuthenticated();
1786
+ if (!this.privateKey || !this.userId) {
1787
+ throw new Error("Private key not available. Is user logged in?");
1788
+ }
1789
+ let packet;
1790
+ try {
1791
+ packet = JSON.parse(packetJson);
1792
+ } catch {
1793
+ throw new ValidationError("Invalid group packet format");
1794
+ }
1795
+ if (packet.type !== "group_sender_key" || !packet.recipient_keys || !packet.ciphertext || !packet.iv) {
1796
+ throw new ValidationError("Invalid group packet structure");
1797
+ }
1798
+ const wrappedKey = packet.recipient_keys[this.userId];
1799
+ if (!wrappedKey) {
1800
+ throw new PermissionDeniedError("You are not a recipient of this group message");
1801
+ }
1802
+ const rawSessionKey = await rsaDecrypt(wrappedKey, this.privateKey);
1803
+ const sessionKey = await importSessionKey(rawSessionKey);
1804
+ return decryptData(packet.ciphertext, packet.iv, sessionKey);
1805
+ }
757
1806
  // ==========================================================================
758
1807
  // Server-Side Message Encryption (Dual-Key Split)
759
1808
  // ==========================================================================