@lumiapassport/ui-kit 1.2.0 → 1.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.
@@ -678,6 +678,546 @@ var init_keccak256 = __esm({
678
678
  }
679
679
  });
680
680
 
681
+ // src/iframe/lib/backup/crypto-utils.ts
682
+ var crypto_utils_exports = {};
683
+ __export(crypto_utils_exports, {
684
+ base64ToBytes: () => base64ToBytes,
685
+ bytesToBase64: () => bytesToBase64,
686
+ bytesToHex: () => bytesToHex2,
687
+ decryptKeyshare: () => decryptKeyshare,
688
+ deriveBackupPasswordFromPasskey: () => deriveBackupPasswordFromPasskey,
689
+ deriveKEKFromPasskey: () => deriveKEKFromPasskey,
690
+ deriveKeyFromPassword: () => deriveKeyFromPassword,
691
+ encryptKeyshare: () => encryptKeyshare,
692
+ envelopeDecryptKeyshare: () => envelopeDecryptKeyshare,
693
+ envelopeDecryptKeyshareWithPassword: () => envelopeDecryptKeyshareWithPassword,
694
+ envelopeEncryptKeyshare: () => envelopeEncryptKeyshare,
695
+ envelopeEncryptKeyshareWithPassword: () => envelopeEncryptKeyshareWithPassword
696
+ });
697
+ function base64ToBytes(base64) {
698
+ const binaryString = atob(base64);
699
+ const bytes = new Uint8Array(binaryString.length);
700
+ for (let i = 0; i < binaryString.length; i++) {
701
+ bytes[i] = binaryString.charCodeAt(i);
702
+ }
703
+ return bytes;
704
+ }
705
+ function bytesToBase64(bytes) {
706
+ let binary = "";
707
+ const len = bytes.byteLength;
708
+ for (let i = 0; i < len; i++) {
709
+ binary += String.fromCharCode(bytes[i]);
710
+ }
711
+ return btoa(binary);
712
+ }
713
+ function bytesToHex2(bytes) {
714
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
715
+ }
716
+ async function deriveKeyFromPassword(password, salt) {
717
+ const encoder3 = new TextEncoder();
718
+ const passwordKey = await crypto.subtle.importKey(
719
+ "raw",
720
+ encoder3.encode(password),
721
+ { name: "PBKDF2" },
722
+ false,
723
+ ["deriveBits", "deriveKey"]
724
+ );
725
+ return crypto.subtle.deriveKey(
726
+ {
727
+ name: "PBKDF2",
728
+ salt,
729
+ iterations: 15e4,
730
+ hash: "SHA-256"
731
+ },
732
+ passwordKey,
733
+ { name: "AES-GCM", length: 256 },
734
+ false,
735
+ ["encrypt", "decrypt"]
736
+ );
737
+ }
738
+ async function deriveKEKFromPasskey(userId, requiredCredentialId) {
739
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
740
+ const allowCredentials = requiredCredentialId ? [
741
+ {
742
+ type: "public-key",
743
+ id: base64ToBytes(requiredCredentialId),
744
+ transports: ["internal"]
745
+ }
746
+ ] : void 0;
747
+ const credential = await navigator.credentials.get({
748
+ publicKey: {
749
+ challenge,
750
+ rpId: window.location.hostname,
751
+ userVerification: "required",
752
+ allowCredentials,
753
+ extensions: {
754
+ prf: {
755
+ eval: {
756
+ first: new TextEncoder().encode(`backup-kek-${userId}`)
757
+ }
758
+ }
759
+ }
760
+ }
761
+ });
762
+ if (!credential?.getClientExtensionResults) {
763
+ throw new Error("Passkey authentication failed");
764
+ }
765
+ const prfResults = credential.getClientExtensionResults();
766
+ if (!prfResults?.prf?.results?.first) {
767
+ throw new Error("PRF extension not supported or failed");
768
+ }
769
+ const resultCredentialId = bytesToBase64(new Uint8Array(credential.rawId));
770
+ return {
771
+ kek: prfResults.prf.results.first,
772
+ credentialId: resultCredentialId
773
+ };
774
+ }
775
+ async function deriveBackupPasswordFromPasskey(userId, credentialId) {
776
+ const challenge = crypto.getRandomValues(new Uint8Array(32));
777
+ const allowCredentials = credentialId ? [
778
+ {
779
+ type: "public-key",
780
+ id: base64ToBytes(credentialId),
781
+ transports: ["internal"]
782
+ }
783
+ ] : void 0;
784
+ const credential = await navigator.credentials.get({
785
+ publicKey: {
786
+ challenge,
787
+ rpId: window.location.hostname,
788
+ userVerification: "required",
789
+ allowCredentials,
790
+ extensions: {
791
+ prf: {
792
+ eval: {
793
+ first: new TextEncoder().encode(`backup-password-${userId}`)
794
+ }
795
+ }
796
+ }
797
+ }
798
+ });
799
+ if (!credential?.getClientExtensionResults) {
800
+ throw new Error("Passkey authentication failed");
801
+ }
802
+ const prfResults = credential.getClientExtensionResults();
803
+ if (!prfResults?.prf?.results?.first) {
804
+ throw new Error("PRF extension not supported or failed");
805
+ }
806
+ const prfOutput = new Uint8Array(prfResults.prf.results.first);
807
+ const password = bytesToBase64(prfOutput);
808
+ const resultCredentialId = bytesToBase64(
809
+ new Uint8Array(credential.rawId)
810
+ );
811
+ return { password, credentialId: resultCredentialId };
812
+ }
813
+ async function encryptKeyshare(data, password, encryptionMethod, credentialId) {
814
+ const encoder3 = new TextEncoder();
815
+ const plaintext = JSON.stringify(data);
816
+ const salt = crypto.getRandomValues(new Uint8Array(16));
817
+ const passwordKey = await crypto.subtle.importKey("raw", encoder3.encode(password), "PBKDF2", false, ["deriveBits"]);
818
+ const keyMaterial = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1e5, hash: "SHA-256" }, passwordKey, 256);
819
+ const key = await crypto.subtle.importKey("raw", keyMaterial, "AES-GCM", false, ["encrypt"]);
820
+ const iv = crypto.getRandomValues(new Uint8Array(12));
821
+ const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoder3.encode(plaintext));
822
+ const checksumData = `${data.userId}:${data.sessionId}:${encryptionMethod}`;
823
+ const checksumBuffer = await crypto.subtle.digest("SHA-256", encoder3.encode(checksumData));
824
+ const checksum = bytesToBase64(new Uint8Array(checksumBuffer));
825
+ const timestamp = Date.now();
826
+ return {
827
+ data: bytesToBase64(new Uint8Array(encryptedData)),
828
+ iv: bytesToBase64(iv),
829
+ salt: bytesToBase64(salt),
830
+ version: "1.0",
831
+ checksum,
832
+ magic: "LUMIA_BACKUP_V1",
833
+ encryptionMethod,
834
+ createdAt: timestamp,
835
+ credentialId: encryptionMethod === "passkey" ? credentialId : void 0
836
+ };
837
+ }
838
+ async function envelopeEncryptKeyshare(data, userId) {
839
+ const { kek: kekBytes, credentialId } = await deriveKEKFromPasskey(userId);
840
+ console.log("[envelopeEncryptKeyshare] Using credential ID for encryption:", credentialId);
841
+ const dek = crypto.getRandomValues(new Uint8Array(32));
842
+ const kek = await crypto.subtle.importKey("raw", kekBytes, "AES-GCM", false, ["encrypt"]);
843
+ const plaintext = JSON.stringify(data);
844
+ const plaintextBytes = new TextEncoder().encode(plaintext);
845
+ const dekCryptoKey = await crypto.subtle.importKey("raw", dek, "AES-GCM", false, ["encrypt"]);
846
+ const dataIv = crypto.getRandomValues(new Uint8Array(12));
847
+ const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, plaintextBytes);
848
+ const wrapIv = crypto.getRandomValues(new Uint8Array(12));
849
+ const wrappedDekData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: wrapIv }, kek, dek);
850
+ const ciphertextWithIv = new Uint8Array(dataIv.length + encryptedData.byteLength);
851
+ ciphertextWithIv.set(dataIv, 0);
852
+ ciphertextWithIv.set(new Uint8Array(encryptedData), dataIv.length);
853
+ const wrappedDekWithIv = new Uint8Array(wrapIv.length + wrappedDekData.byteLength);
854
+ wrappedDekWithIv.set(wrapIv, 0);
855
+ wrappedDekWithIv.set(new Uint8Array(wrappedDekData), wrapIv.length);
856
+ return {
857
+ ciphertext_share: bytesToBase64(ciphertextWithIv),
858
+ wrapped_dek: bytesToBase64(wrappedDekWithIv),
859
+ kdf: { prf: "webauthn-prf", hkdf: "hkdf-sha256", info: "client-share" },
860
+ alg: { data: "aes-256-gcm", wrap: "aes-256-gcm" },
861
+ aad: { userId: data.userId, version: 1, deviceId: "lumia-ui-kit" },
862
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
863
+ expiresAt: null,
864
+ encryptionMethod: "passkey",
865
+ credentialId: credentialId || void 0
866
+ };
867
+ }
868
+ async function envelopeEncryptKeyshareWithPassword(data, password) {
869
+ console.log("[envelopeEncryptKeyshareWithPassword] Encrypting with password");
870
+ const dek = crypto.getRandomValues(new Uint8Array(32));
871
+ const salt = crypto.getRandomValues(new Uint8Array(16));
872
+ const kekKey = await deriveKeyFromPassword(password, salt);
873
+ const plaintext = JSON.stringify(data);
874
+ const plaintextBytes = new TextEncoder().encode(plaintext);
875
+ const dekCryptoKey = await crypto.subtle.importKey("raw", dek, "AES-GCM", false, ["encrypt"]);
876
+ const dataIv = crypto.getRandomValues(new Uint8Array(12));
877
+ const encryptedData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, plaintextBytes);
878
+ const wrapIv = crypto.getRandomValues(new Uint8Array(12));
879
+ const wrappedDekData = await crypto.subtle.encrypt({ name: "AES-GCM", iv: wrapIv }, kekKey, dek);
880
+ const ciphertextWithIv = new Uint8Array(dataIv.length + encryptedData.byteLength);
881
+ ciphertextWithIv.set(dataIv, 0);
882
+ ciphertextWithIv.set(new Uint8Array(encryptedData), dataIv.length);
883
+ const wrappedDekWithIv = new Uint8Array(salt.length + wrapIv.length + wrappedDekData.byteLength);
884
+ wrappedDekWithIv.set(salt, 0);
885
+ wrappedDekWithIv.set(wrapIv, salt.length);
886
+ wrappedDekWithIv.set(new Uint8Array(wrappedDekData), salt.length + wrapIv.length);
887
+ return {
888
+ ciphertext_share: bytesToBase64(ciphertextWithIv),
889
+ wrapped_dek: bytesToBase64(wrappedDekWithIv),
890
+ kdf: { prf: "pbkdf2", hkdf: "hkdf-sha256", info: "client-share" },
891
+ alg: { data: "aes-256-gcm", wrap: "aes-256-gcm" },
892
+ aad: { userId: data.userId, version: 1, deviceId: "lumia-ui-kit" },
893
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
894
+ expiresAt: null,
895
+ encryptionMethod: "password"
896
+ };
897
+ }
898
+ async function envelopeDecryptKeyshare(envelope, userId) {
899
+ console.log("[iframe][envelopeDecryptKeyshare] Starting decryption process");
900
+ console.log("[iframe][envelopeDecryptKeyshare] Envelope credential ID:", envelope.credentialId);
901
+ console.log("[iframe][envelopeDecryptKeyshare] Deriving KEK from passkey...");
902
+ const { kek: kekBytes } = await deriveKEKFromPasskey(userId, envelope.credentialId);
903
+ console.log("[iframe][envelopeDecryptKeyshare] KEK bytes length:", kekBytes.byteLength);
904
+ console.log("[iframe][envelopeDecryptKeyshare] Importing KEK as crypto key...");
905
+ const kek = await crypto.subtle.importKey("raw", kekBytes, "AES-GCM", false, ["decrypt"]);
906
+ console.log("[iframe][envelopeDecryptKeyshare] Decrypting wrapped DEK...");
907
+ const wrappedDekWithIv = base64ToBytes(envelope.wrapped_dek);
908
+ console.log("[iframe][envelopeDecryptKeyshare] Wrapped DEK with IV length:", wrappedDekWithIv.length);
909
+ const wrapIv = wrappedDekWithIv.slice(0, 12);
910
+ const wrappedDekData = wrappedDekWithIv.slice(12);
911
+ console.log("[iframe][envelopeDecryptKeyshare] Wrap IV length:", wrapIv.length, "Wrapped DEK data length:", wrappedDekData.length);
912
+ try {
913
+ const dekBytes = await crypto.subtle.decrypt({ name: "AES-GCM", iv: wrapIv }, kek, wrappedDekData);
914
+ console.log("[iframe][envelopeDecryptKeyshare] DEK decrypted successfully, length:", dekBytes.byteLength);
915
+ console.log("[iframe][envelopeDecryptKeyshare] Importing DEK as crypto key...");
916
+ const dekCryptoKey = await crypto.subtle.importKey("raw", dekBytes, "AES-GCM", false, ["decrypt"]);
917
+ console.log("[iframe][envelopeDecryptKeyshare] Decrypting main data...");
918
+ const ciphertextWithIv = base64ToBytes(envelope.ciphertext_share);
919
+ console.log("[iframe][envelopeDecryptKeyshare] Ciphertext with IV length:", ciphertextWithIv.length);
920
+ const dataIv = ciphertextWithIv.slice(0, 12);
921
+ const encryptedData = ciphertextWithIv.slice(12);
922
+ console.log("[iframe][envelopeDecryptKeyshare] Data IV length:", dataIv.length, "Encrypted data length:", encryptedData.length);
923
+ const decryptedData = await crypto.subtle.decrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, encryptedData);
924
+ console.log("[iframe][envelopeDecryptKeyshare] Main data decrypted successfully, length:", decryptedData.byteLength);
925
+ console.log("[iframe][envelopeDecryptKeyshare] Parsing JSON...");
926
+ const plaintext = new TextDecoder().decode(decryptedData);
927
+ const result = JSON.parse(plaintext);
928
+ console.log("[iframe][envelopeDecryptKeyshare] Decryption completed successfully");
929
+ return result;
930
+ } catch (error) {
931
+ console.error("[iframe][envelopeDecryptKeyshare] Decryption failed:", error);
932
+ if (error instanceof Error && error.name === "OperationError") {
933
+ const credentialHint = envelope.credentialId ? `The backup requires a specific passkey (ID: ${envelope.credentialId.slice(-8)}...)` : "The backup was encrypted with a different passkey than the one you selected";
934
+ throw new Error(`Passkey mismatch: ${credentialHint}. If you have multiple passkeys, please try again and select the correct passkey that was used when creating this backup.`);
935
+ }
936
+ throw new Error("Failed to decrypt keyshare: " + (error instanceof Error ? error.message : String(error)));
937
+ }
938
+ }
939
+ async function envelopeDecryptKeyshareWithPassword(envelope, password) {
940
+ console.log("[iframe][envelopeDecryptKeyshareWithPassword] Starting password-based decryption");
941
+ const wrappedDekWithSaltAndIv = base64ToBytes(envelope.wrapped_dek);
942
+ const salt = wrappedDekWithSaltAndIv.slice(0, 16);
943
+ const wrapIv = wrappedDekWithSaltAndIv.slice(16, 28);
944
+ const wrappedDekData = wrappedDekWithSaltAndIv.slice(28);
945
+ console.log("[iframe][envelopeDecryptKeyshareWithPassword] Salt length:", salt.length, "Wrap IV length:", wrapIv.length, "Wrapped DEK data length:", wrappedDekData.length);
946
+ try {
947
+ const kekKey = await deriveKeyFromPassword(password, salt);
948
+ const dekBytes = await crypto.subtle.decrypt({ name: "AES-GCM", iv: wrapIv }, kekKey, wrappedDekData);
949
+ console.log("[iframe][envelopeDecryptKeyshareWithPassword] DEK decrypted successfully, length:", dekBytes.byteLength);
950
+ const dekCryptoKey = await crypto.subtle.importKey("raw", dekBytes, "AES-GCM", false, ["decrypt"]);
951
+ const ciphertextWithIv = base64ToBytes(envelope.ciphertext_share);
952
+ const dataIv = ciphertextWithIv.slice(0, 12);
953
+ const encryptedData = ciphertextWithIv.slice(12);
954
+ const decryptedData = await crypto.subtle.decrypt({ name: "AES-GCM", iv: dataIv }, dekCryptoKey, encryptedData);
955
+ console.log("[iframe][envelopeDecryptKeyshareWithPassword] Main data decrypted successfully");
956
+ const plaintext = new TextDecoder().decode(decryptedData);
957
+ const result = JSON.parse(plaintext);
958
+ console.log("[iframe][envelopeDecryptKeyshareWithPassword] Decryption completed successfully");
959
+ return result;
960
+ } catch (error) {
961
+ console.error("[iframe][envelopeDecryptKeyshareWithPassword] Decryption failed:", error);
962
+ if (error instanceof Error && error.name === "OperationError") {
963
+ throw new Error("Incorrect password: Unable to decrypt the backup with the provided password.");
964
+ }
965
+ throw new Error("Failed to decrypt keyshare with password: " + (error instanceof Error ? error.message : String(error)));
966
+ }
967
+ }
968
+ async function decryptKeyshare(encryptedBackup, password) {
969
+ console.log("[iframe][decryptKeyshare] Starting file backup decryption");
970
+ if (encryptedBackup.magic && encryptedBackup.magic !== "LUMIA_BACKUP_V1") {
971
+ throw new Error("Invalid backup file format");
972
+ }
973
+ if (encryptedBackup.version && encryptedBackup.version !== "1.0") {
974
+ throw new Error(`Unsupported backup version: ${encryptedBackup.version}`);
975
+ }
976
+ try {
977
+ const decoder = new TextDecoder();
978
+ const dataField = encryptedBackup.data || encryptedBackup.encryptedData;
979
+ if (!encryptedBackup.salt || !encryptedBackup.iv || !dataField) {
980
+ console.error("[iframe][decryptKeyshare] Backup structure:", Object.keys(encryptedBackup));
981
+ throw new Error("Missing required encrypted backup fields (salt, iv, data/encryptedData)");
982
+ }
983
+ const salt = base64ToBytes(encryptedBackup.salt);
984
+ const iv = base64ToBytes(encryptedBackup.iv);
985
+ const encryptedData = base64ToBytes(dataField);
986
+ const encoder3 = new TextEncoder();
987
+ const passwordKey = await crypto.subtle.importKey(
988
+ "raw",
989
+ encoder3.encode(password),
990
+ "PBKDF2",
991
+ false,
992
+ ["deriveBits"]
993
+ );
994
+ const keyMaterial = await crypto.subtle.deriveBits(
995
+ {
996
+ name: "PBKDF2",
997
+ salt,
998
+ iterations: 1e5,
999
+ // File backups use 100,000 iterations
1000
+ hash: "SHA-256"
1001
+ },
1002
+ passwordKey,
1003
+ 256
1004
+ );
1005
+ const key = await crypto.subtle.importKey(
1006
+ "raw",
1007
+ keyMaterial,
1008
+ "AES-GCM",
1009
+ false,
1010
+ ["decrypt"]
1011
+ );
1012
+ const decryptedData = await crypto.subtle.decrypt(
1013
+ { name: "AES-GCM", iv },
1014
+ key,
1015
+ encryptedData
1016
+ );
1017
+ const plaintext = decoder.decode(decryptedData);
1018
+ const backupData = JSON.parse(plaintext);
1019
+ if (encryptedBackup.checksum) {
1020
+ const expected = `${backupData.userId}:${backupData.sessionId}:${encryptedBackup.encryptionMethod || "password"}`;
1021
+ const expectedBuf = await crypto.subtle.digest("SHA-256", encoder3.encode(expected));
1022
+ const expectedChecksum = bytesToBase64(new Uint8Array(expectedBuf));
1023
+ if (expectedChecksum !== encryptedBackup.checksum) {
1024
+ throw new Error("Backup integrity check failed - file may be corrupted or tampered with");
1025
+ }
1026
+ }
1027
+ console.log("[iframe][decryptKeyshare] File backup decrypted successfully");
1028
+ return backupData;
1029
+ } catch (error) {
1030
+ const message = error?.message || String(error);
1031
+ const name = error?.name || "Error";
1032
+ if (name === "OperationError" || message.includes("decrypt")) {
1033
+ throw new Error("Incorrect password - unable to decrypt backup file");
1034
+ }
1035
+ if (error instanceof SyntaxError || message.includes("JSON")) {
1036
+ throw new Error("Corrupted backup file - decryption succeeded but data is invalid");
1037
+ }
1038
+ throw new Error(`Failed to decrypt backup: ${message}`);
1039
+ }
1040
+ }
1041
+ var init_crypto_utils = __esm({
1042
+ "src/iframe/lib/backup/crypto-utils.ts"() {
1043
+ }
1044
+ });
1045
+
1046
+ // src/iframe/lib/backup/server-backup.ts
1047
+ var server_backup_exports = {};
1048
+ __export(server_backup_exports, {
1049
+ backupToServer: () => backupToServer,
1050
+ restoreFromServer: () => restoreFromServer
1051
+ });
1052
+ async function getShareVaultToken(scopes, accessToken) {
1053
+ const headers = {
1054
+ "Content-Type": "application/json"
1055
+ };
1056
+ if (accessToken) {
1057
+ headers["Authorization"] = `Bearer ${accessToken}`;
1058
+ }
1059
+ const response = await fetch(`${TSS_URL}/api/auth/token/exchange`, {
1060
+ method: "POST",
1061
+ headers,
1062
+ credentials: "include",
1063
+ body: JSON.stringify({
1064
+ scopes,
1065
+ audience: "lumia-passport-share-vault-service",
1066
+ ttl: 300
1067
+ })
1068
+ });
1069
+ if (!response.ok) {
1070
+ throw new Error(`Failed to get Share Vault token: ${response.status}`);
1071
+ }
1072
+ const data = await response.json();
1073
+ if (!data?.resourceToken) {
1074
+ throw new Error("Invalid token response");
1075
+ }
1076
+ return data.resourceToken;
1077
+ }
1078
+ async function uploadShareToVault(encryptedShare, accessToken) {
1079
+ const token = await getShareVaultToken(["share:put"], accessToken);
1080
+ const idempotencyKey = crypto.randomUUID ? crypto.randomUUID() : `backup-${Date.now()}`;
1081
+ const response = await fetch(`${SHARE_VAULT_URL}/v1/shares/me`, {
1082
+ method: "PUT",
1083
+ headers: {
1084
+ "Content-Type": "application/json",
1085
+ "Authorization": `Bearer ${token}`,
1086
+ "Idempotency-Key": idempotencyKey
1087
+ },
1088
+ body: JSON.stringify(encryptedShare)
1089
+ });
1090
+ if (!response.ok) {
1091
+ const errorText = await response.text();
1092
+ throw new Error(`Failed to upload share: ${response.status} ${response.statusText} - ${errorText}`);
1093
+ }
1094
+ }
1095
+ async function backupToServer(data, tokenClient, password, accessToken) {
1096
+ let encryptedShare;
1097
+ if (password) {
1098
+ encryptedShare = await envelopeEncryptKeyshareWithPassword(data, password);
1099
+ } else {
1100
+ encryptedShare = await envelopeEncryptKeyshare(data, data.userId);
1101
+ }
1102
+ await uploadShareToVault(encryptedShare, accessToken);
1103
+ }
1104
+ async function downloadShareFromVault(accessToken) {
1105
+ const token = await getShareVaultToken(["share:get"], accessToken);
1106
+ const response = await fetch(`${SHARE_VAULT_URL}/v1/shares/me`, {
1107
+ method: "GET",
1108
+ headers: {
1109
+ "Authorization": `Bearer ${token}`,
1110
+ "X-Client-Device-Id": "lumia-ui-kit",
1111
+ "X-Client-Device-Name": "Lumia UI Kit"
1112
+ }
1113
+ });
1114
+ if (!response.ok) {
1115
+ if (response.status === 404) {
1116
+ throw new Error("No backup found on server for this user");
1117
+ }
1118
+ const errorText = await response.text();
1119
+ throw new Error(`Failed to download share: ${response.status} ${response.statusText} - ${errorText}`);
1120
+ }
1121
+ const envelope = await response.json();
1122
+ if (!envelope.ciphertext_share || !envelope.wrapped_dek || !envelope.kdf || !envelope.alg || !envelope.aad) {
1123
+ throw new Error("Invalid envelope structure received from Share Vault");
1124
+ }
1125
+ return envelope;
1126
+ }
1127
+ async function restoreFromServer(userId, tokenClient, password, accessToken) {
1128
+ console.log("[iframe][restoreFromServer] Starting restore for userId:", userId);
1129
+ console.log("[iframe][restoreFromServer] Using password:", !!password);
1130
+ console.log("[iframe][restoreFromServer] Downloading share from vault...");
1131
+ const envelope = await downloadShareFromVault(accessToken);
1132
+ console.log("[iframe][restoreFromServer] Envelope encryption method:", envelope.encryptionMethod);
1133
+ const { envelopeDecryptKeyshare: envelopeDecryptKeyshare2, envelopeDecryptKeyshareWithPassword: envelopeDecryptKeyshareWithPassword2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
1134
+ const backupData = password ? await envelopeDecryptKeyshareWithPassword2(envelope, password) : await envelopeDecryptKeyshare2(envelope, userId);
1135
+ if (backupData.userId !== userId) {
1136
+ throw new Error("Server backup does not match current user");
1137
+ }
1138
+ console.log("[iframe][restoreFromServer] Decryption successful, returning backup data");
1139
+ return backupData;
1140
+ }
1141
+ var SHARE_VAULT_URL, TSS_URL;
1142
+ var init_server_backup = __esm({
1143
+ "src/iframe/lib/backup/server-backup.ts"() {
1144
+ init_crypto_utils();
1145
+ SHARE_VAULT_URL = typeof window.__LUMIA_SERVICES__ !== "undefined" && window.__LUMIA_SERVICES__?.shareVaultUrl || "https://api.lumiapassport.com/vault";
1146
+ TSS_URL = typeof window.__LUMIA_SERVICES__ !== "undefined" && window.__LUMIA_SERVICES__?.tssUrl || "https://api.lumiapassport.com/tss";
1147
+ }
1148
+ });
1149
+
1150
+ // src/iframe/lib/backup/local-backup.ts
1151
+ var local_backup_exports = {};
1152
+ __export(local_backup_exports, {
1153
+ backupToLocalFile: () => backupToLocalFile,
1154
+ restoreFromLocalFile: () => restoreFromLocalFile
1155
+ });
1156
+ async function backupToLocalFile(data, password) {
1157
+ let encryptionPassword = password;
1158
+ let credentialId;
1159
+ if (!encryptionPassword) {
1160
+ const result = await deriveBackupPasswordFromPasskey(data.userId);
1161
+ encryptionPassword = result.password;
1162
+ credentialId = result.credentialId;
1163
+ }
1164
+ const encrypted = await encryptKeyshare(
1165
+ data,
1166
+ encryptionPassword,
1167
+ password ? "password" : "passkey",
1168
+ credentialId
1169
+ );
1170
+ const backupData = {
1171
+ ...encrypted,
1172
+ userId: data.userId,
1173
+ createdAt: data.createdAt
1174
+ };
1175
+ const blob = new Blob([JSON.stringify(backupData, null, 2)], {
1176
+ type: "application/json"
1177
+ });
1178
+ const url = URL.createObjectURL(blob);
1179
+ const a = document.createElement("a");
1180
+ a.href = url;
1181
+ a.download = `lumia-passport-backup-${data.userId}-${Date.now()}.json`;
1182
+ document.body.appendChild(a);
1183
+ a.click();
1184
+ document.body.removeChild(a);
1185
+ URL.revokeObjectURL(url);
1186
+ }
1187
+ async function restoreFromLocalFile(fileContent, userId, password) {
1188
+ console.log("[iframe][restoreFromLocalFile] Starting restore from local file");
1189
+ const encryptedBackup = JSON.parse(fileContent);
1190
+ let decryptionPassword;
1191
+ let credentialId;
1192
+ if (password) {
1193
+ decryptionPassword = password;
1194
+ } else {
1195
+ const credentialIdFromBackup = encryptedBackup.encryptionMethod === "passkey" ? encryptedBackup.credentialId : void 0;
1196
+ const result = await deriveBackupPasswordFromPasskey(
1197
+ userId,
1198
+ credentialIdFromBackup
1199
+ ).catch(() => {
1200
+ throw new Error(
1201
+ "Restore requires either password or passkey authentication"
1202
+ );
1203
+ });
1204
+ decryptionPassword = result.password;
1205
+ credentialId = result.credentialId;
1206
+ }
1207
+ const { decryptKeyshare: decryptKeyshare2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
1208
+ const backupData = await decryptKeyshare2(encryptedBackup, decryptionPassword);
1209
+ if (backupData.userId !== userId) {
1210
+ throw new Error("Backup file does not match current user");
1211
+ }
1212
+ console.log("[iframe][restoreFromLocalFile] Restore successful");
1213
+ return backupData;
1214
+ }
1215
+ var init_local_backup = __esm({
1216
+ "src/iframe/lib/backup/local-backup.ts"() {
1217
+ init_crypto_utils();
1218
+ }
1219
+ });
1220
+
681
1221
  // src/iframe/lib/secure-messenger.ts
682
1222
  var SecureMessenger = class {
683
1223
  constructor() {
@@ -843,6 +1383,12 @@ var SecureMessenger = class {
843
1383
  "CHECK_KEYSHARE",
844
1384
  "GET_TRUSTED_APPS",
845
1385
  "REMOVE_TRUSTED_APP",
1386
+ "CREATE_BACKUP",
1387
+ "ENCRYPT_BACKUP_DATA",
1388
+ "RESTORE_BACKUP",
1389
+ "RESTORE_FROM_FILE",
1390
+ "GET_BACKUP_STATUS",
1391
+ "GET_CLOUD_PROVIDERS",
846
1392
  "REQUEST_NEW_TOKEN",
847
1393
  "RESPONSE",
848
1394
  "ERROR",
@@ -2622,11 +3168,477 @@ var AuthorizationManager = class {
2622
3168
  }
2623
3169
  };
2624
3170
 
3171
+ // src/iframe/lib/backup-manager.ts
3172
+ init_server_backup();
3173
+ init_local_backup();
3174
+
3175
+ // src/iframe/lib/backup/cloud-backup.ts
3176
+ init_crypto_utils();
3177
+
3178
+ // src/iframe/lib/backup/cloudStorage.ts
3179
+ var GoogleDriveProvider = class {
3180
+ constructor() {
3181
+ this.name = "Google Drive";
3182
+ this.id = "google-drive";
3183
+ this.icon = "\u{1F4C1}";
3184
+ // Can be replaced with proper icon
3185
+ this.accessToken = null;
3186
+ this.CLIENT_ID = typeof window.__GOOGLE_DRIVE_CLIENT_ID__ !== "undefined" && window.__GOOGLE_DRIVE_CLIENT_ID__ || "";
3187
+ this.DISCOVERY_DOC = "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest";
3188
+ this.SCOPES = "https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/drive.appdata";
3189
+ this.gapiInitialized = false;
3190
+ this.gisInitialized = false;
3191
+ this.initializeAPIs();
3192
+ }
3193
+ async initializeAPIs() {
3194
+ if (!window.gapi) {
3195
+ await this.loadScript("https://apis.google.com/js/api.js");
3196
+ }
3197
+ if (!window.google?.accounts) {
3198
+ await this.loadScript("https://accounts.google.com/gsi/client");
3199
+ }
3200
+ if (!this.gapiInitialized) {
3201
+ await new Promise((resolve) => {
3202
+ window.gapi.load("client", resolve);
3203
+ });
3204
+ await window.gapi.client.init({
3205
+ discoveryDocs: [this.DISCOVERY_DOC]
3206
+ });
3207
+ this.gapiInitialized = true;
3208
+ console.log("[GoogleDrive] Google API client initialized");
3209
+ }
3210
+ if (!this.gisInitialized) {
3211
+ this.gisInitialized = true;
3212
+ console.log("[GoogleDrive] Google Identity Services initialized");
3213
+ }
3214
+ }
3215
+ loadScript(src) {
3216
+ return new Promise((resolve, reject) => {
3217
+ if (document.querySelector(`script[src="${src}"]`)) {
3218
+ resolve();
3219
+ return;
3220
+ }
3221
+ const script = document.createElement("script");
3222
+ script.src = src;
3223
+ script.onload = () => resolve();
3224
+ script.onerror = () => reject(new Error(`Failed to load ${src}`));
3225
+ document.head.appendChild(script);
3226
+ });
3227
+ }
3228
+ isAvailable() {
3229
+ return !!this.CLIENT_ID && typeof window !== "undefined";
3230
+ }
3231
+ async authenticate() {
3232
+ if (!this.isAvailable()) {
3233
+ throw new Error(
3234
+ "Google Drive integration is not available in this environment"
3235
+ );
3236
+ }
3237
+ await this.initializeAPIs();
3238
+ return new Promise((resolve, reject) => {
3239
+ try {
3240
+ const tokenClient = window.google.accounts.oauth2.initTokenClient({
3241
+ client_id: this.CLIENT_ID,
3242
+ scope: this.SCOPES,
3243
+ callback: (response) => {
3244
+ if (response.error) {
3245
+ console.error("[GoogleDrive] Authentication error:", response.error);
3246
+ reject(new Error(`Google Drive authentication failed: ${response.error}`));
3247
+ return;
3248
+ }
3249
+ this.accessToken = response.access_token;
3250
+ window.gapi.client.setToken({ access_token: this.accessToken });
3251
+ console.log("[GoogleDrive] Successfully authenticated");
3252
+ resolve(true);
3253
+ }
3254
+ });
3255
+ tokenClient.requestAccessToken({ prompt: "consent" });
3256
+ } catch (error) {
3257
+ console.error("[GoogleDrive] Authentication initialization failed:", error);
3258
+ reject(new Error(`Google Drive authentication setup failed: ${error}`));
3259
+ }
3260
+ });
3261
+ }
3262
+ isAuthenticated() {
3263
+ return !!this.accessToken;
3264
+ }
3265
+ async signOut() {
3266
+ if (this.accessToken) {
3267
+ window.google?.accounts.oauth2.revoke(this.accessToken);
3268
+ this.accessToken = null;
3269
+ window.gapi?.client.setToken(null);
3270
+ console.log("[GoogleDrive] Signed out successfully");
3271
+ }
3272
+ }
3273
+ async upload(fileName, content, usePrivateStorage = true) {
3274
+ if (!this.isAuthenticated()) {
3275
+ throw new Error("Not authenticated with Google Drive");
3276
+ }
3277
+ await this.initializeAPIs();
3278
+ if (usePrivateStorage) {
3279
+ try {
3280
+ const fileId = await this.uploadToAppDataFolder(fileName, content);
3281
+ console.log("[GoogleDrive] File uploaded to appDataFolder successfully:", fileId);
3282
+ return fileId;
3283
+ } catch (error) {
3284
+ console.warn("[GoogleDrive] AppDataFolder upload failed, trying fallback to regular folder:", error);
3285
+ const fileId = await this.uploadToAppFolder(fileName, content);
3286
+ console.log("[GoogleDrive] File uploaded to app folder successfully:", fileId);
3287
+ return fileId;
3288
+ }
3289
+ } else {
3290
+ try {
3291
+ const fileId = await this.uploadToAppFolder(fileName, content);
3292
+ console.log("[GoogleDrive] File uploaded to app folder successfully:", fileId);
3293
+ return fileId;
3294
+ } catch (error) {
3295
+ console.error("[GoogleDrive] Upload failed:", error);
3296
+ throw new Error(
3297
+ `Failed to upload to Google Drive: ${error instanceof Error ? error.message : String(error)}`
3298
+ );
3299
+ }
3300
+ }
3301
+ }
3302
+ async uploadToAppDataFolder(fileName, content) {
3303
+ const metadata = {
3304
+ name: fileName,
3305
+ parents: ["appDataFolder"]
3306
+ // Store in app-specific folder for privacy
3307
+ };
3308
+ return this.performUpload(metadata, content);
3309
+ }
3310
+ async uploadToAppFolder(fileName, content) {
3311
+ const folderName = "Lumia Passport Backups";
3312
+ const folderId = await this.findOrCreateFolder(folderName);
3313
+ const metadata = {
3314
+ name: fileName,
3315
+ parents: [folderId]
3316
+ };
3317
+ return this.performUpload(metadata, content);
3318
+ }
3319
+ async findOrCreateFolder(folderName) {
3320
+ const searchResponse = await fetch(
3321
+ `https://www.googleapis.com/drive/v3/files?q=name='${folderName}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
3322
+ {
3323
+ headers: { Authorization: `Bearer ${this.accessToken}` }
3324
+ }
3325
+ );
3326
+ if (!searchResponse.ok) {
3327
+ throw new Error(`Failed to search for folder: ${searchResponse.status}`);
3328
+ }
3329
+ const searchResult = await searchResponse.json();
3330
+ if (searchResult.files && searchResult.files.length > 0) {
3331
+ return searchResult.files[0].id;
3332
+ }
3333
+ const createResponse = await fetch("https://www.googleapis.com/drive/v3/files", {
3334
+ method: "POST",
3335
+ headers: {
3336
+ Authorization: `Bearer ${this.accessToken}`,
3337
+ "Content-Type": "application/json"
3338
+ },
3339
+ body: JSON.stringify({
3340
+ name: folderName,
3341
+ mimeType: "application/vnd.google-apps.folder"
3342
+ })
3343
+ });
3344
+ if (!createResponse.ok) {
3345
+ throw new Error(`Failed to create folder: ${createResponse.status}`);
3346
+ }
3347
+ const createResult = await createResponse.json();
3348
+ console.log(`[GoogleDrive] Created folder '${folderName}':`, createResult.id);
3349
+ return createResult.id;
3350
+ }
3351
+ async performUpload(metadata, content) {
3352
+ const form = new FormData();
3353
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
3354
+ form.append("file", new Blob([content], { type: "application/json" }));
3355
+ const response = await fetch(
3356
+ "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
3357
+ {
3358
+ method: "POST",
3359
+ headers: {
3360
+ Authorization: `Bearer ${this.accessToken}`
3361
+ },
3362
+ body: form
3363
+ }
3364
+ );
3365
+ if (!response.ok) {
3366
+ const errorText = await response.text();
3367
+ throw new Error(
3368
+ `Google Drive upload failed: ${response.status} ${response.statusText} - ${errorText}`
3369
+ );
3370
+ }
3371
+ const result = await response.json();
3372
+ return result.id;
3373
+ }
3374
+ };
3375
+ function getAvailableCloudProviders() {
3376
+ const providers = [];
3377
+ const googleDrive = new GoogleDriveProvider();
3378
+ if (googleDrive.isAvailable()) {
3379
+ providers.push(googleDrive);
3380
+ }
3381
+ return providers;
3382
+ }
3383
+
3384
+ // src/iframe/lib/backup/cloud-backup.ts
3385
+ async function backupToCloud(data, providerId, password) {
3386
+ const encryptionMethod = password ? "password" : "passkey";
3387
+ let backupPassword;
3388
+ let credentialId;
3389
+ if (password) {
3390
+ backupPassword = password;
3391
+ } else {
3392
+ const result = await deriveBackupPasswordFromPasskey(data.userId).catch(() => {
3393
+ throw new Error("Backup requires either custom password or passkey authentication");
3394
+ });
3395
+ backupPassword = result.password;
3396
+ credentialId = result.credentialId;
3397
+ console.log("[backupToCloud] Using credential ID from passkey:", credentialId);
3398
+ }
3399
+ const encryptedBackup = await encryptKeyshare(data, backupPassword, encryptionMethod, credentialId);
3400
+ const timestamp = Date.now();
3401
+ const fileName = `lumia-keyshare-backup-${data.userId}-${timestamp}.json`;
3402
+ const fileContent = JSON.stringify(encryptedBackup, null, 2);
3403
+ const providers = getAvailableCloudProviders();
3404
+ if (providers.length === 0) {
3405
+ throw new Error(
3406
+ "No cloud storage providers available in this environment"
3407
+ );
3408
+ }
3409
+ const provider = providerId ? providers.find((p) => p.id === providerId) : providers[0];
3410
+ if (!provider) {
3411
+ throw new Error(
3412
+ providerId ? `Cloud provider '${providerId}' not available` : "No cloud storage provider available"
3413
+ );
3414
+ }
3415
+ console.info(`[BACKUP] Using ${provider.name} for cloud backup`);
3416
+ if (!provider.isAuthenticated()) {
3417
+ console.info(`[BACKUP] Authenticating with ${provider.name}`);
3418
+ const authenticated = await provider.authenticate();
3419
+ if (!authenticated) {
3420
+ throw new Error(`Failed to authenticate with ${provider.name}`);
3421
+ }
3422
+ }
3423
+ console.info(`[BACKUP] Uploading backup to ${provider.name}`);
3424
+ const fileId = await provider.upload(fileName, fileContent, true);
3425
+ console.info(`[BACKUP] Successfully uploaded backup to ${provider.name}, file ID: ${fileId}`);
3426
+ }
3427
+ function getCloudProviders() {
3428
+ const providers = getAvailableCloudProviders();
3429
+ return providers.map((p) => ({
3430
+ id: p.id,
3431
+ name: p.name,
3432
+ available: p.isAvailable()
3433
+ }));
3434
+ }
3435
+
3436
+ // src/iframe/lib/backup-manager.ts
3437
+ var BACKUP_VERSION = "1.0";
3438
+ var BackupManager = class {
3439
+ constructor() {
3440
+ this.tokenClient = new TokenRefreshApiClient();
3441
+ console.log("[iframe][BackupManager] Initialized");
3442
+ }
3443
+ /**
3444
+ * Get current keyshare data from iframe storage
3445
+ */
3446
+ getCurrentKeyshareBackupData(userId) {
3447
+ console.log("[iframe][BackupManager] Getting keyshare data for userId:", userId);
3448
+ try {
3449
+ const keyshareData = localStorage.getItem(`tss.${userId}.keyshare`);
3450
+ const sessionId = localStorage.getItem(`tss.${userId}.sessionId`);
3451
+ const ownerAddress = localStorage.getItem(`tss.${userId}.ownerAddress`);
3452
+ console.log("[iframe][BackupManager] localStorage keys check:", {
3453
+ hasKeyshare: !!keyshareData,
3454
+ hasSession: !!sessionId,
3455
+ hasOwner: !!ownerAddress,
3456
+ keyshareLength: keyshareData?.length
3457
+ });
3458
+ if (!keyshareData || !sessionId || !ownerAddress) {
3459
+ console.warn("[iframe][BackupManager] Missing keyshare data - cannot create backup");
3460
+ return null;
3461
+ }
3462
+ console.log("[iframe][BackupManager] Successfully retrieved all keyshare data");
3463
+ return {
3464
+ userId,
3465
+ sessionId,
3466
+ keyshare: keyshareData,
3467
+ ownerAddress,
3468
+ createdAt: Date.now(),
3469
+ version: BACKUP_VERSION
3470
+ };
3471
+ } catch (error) {
3472
+ console.error("[iframe][BackupManager] Error getting keyshare data:", error);
3473
+ return null;
3474
+ }
3475
+ }
3476
+ /**
3477
+ * Get backup status for all methods
3478
+ */
3479
+ getBackupStatus(userId) {
3480
+ const statusData = localStorage.getItem(`lumia-passport.backup.status.${userId}`);
3481
+ if (statusData) {
3482
+ try {
3483
+ return JSON.parse(statusData);
3484
+ } catch (error) {
3485
+ console.error("[iframe][BackupManager] Failed to parse backup status:", error);
3486
+ }
3487
+ }
3488
+ return {
3489
+ server: { lastBackup: void 0, error: void 0 },
3490
+ cloud: { lastBackup: void 0, error: void 0 },
3491
+ local: { lastBackup: void 0, error: void 0 }
3492
+ };
3493
+ }
3494
+ /**
3495
+ * Update backup status for a specific method
3496
+ */
3497
+ updateBackupStatus(userId, method, status) {
3498
+ const currentStatus = this.getBackupStatus(userId);
3499
+ currentStatus[method] = { ...currentStatus[method], ...status };
3500
+ localStorage.setItem(
3501
+ `lumia-passport.backup.status.${userId}`,
3502
+ JSON.stringify(currentStatus)
3503
+ );
3504
+ console.log(`[iframe][BackupManager] Updated ${method} backup status for ${userId}`);
3505
+ }
3506
+ /**
3507
+ * Create backup using specified method
3508
+ */
3509
+ async createBackup(userId, request, accessToken) {
3510
+ console.log("[iframe][BackupManager] Create backup request:", request);
3511
+ const data = this.getCurrentKeyshareBackupData(userId);
3512
+ if (!data) {
3513
+ const error = "No keyshare data found for backup";
3514
+ console.error("[iframe][BackupManager]", error);
3515
+ return {
3516
+ success: false,
3517
+ method: request.method,
3518
+ timestamp: Date.now(),
3519
+ error
3520
+ };
3521
+ }
3522
+ try {
3523
+ switch (request.method) {
3524
+ case "server":
3525
+ await backupToServer(data, this.tokenClient, request.password, accessToken);
3526
+ break;
3527
+ case "local":
3528
+ await backupToLocalFile(data, request.password);
3529
+ break;
3530
+ case "cloud":
3531
+ await backupToCloud(data, request.cloudProvider, request.password);
3532
+ break;
3533
+ default:
3534
+ throw new Error(`Unknown backup method: ${request.method}`);
3535
+ }
3536
+ const timestamp = Date.now();
3537
+ this.updateBackupStatus(userId, request.method, {
3538
+ lastBackup: timestamp,
3539
+ error: void 0
3540
+ });
3541
+ console.log(`[iframe][BackupManager] \u2705 ${request.method} backup completed`);
3542
+ return {
3543
+ success: true,
3544
+ method: request.method,
3545
+ timestamp
3546
+ };
3547
+ } catch (error) {
3548
+ const errorMessage = error.message || "Backup failed";
3549
+ console.error("[iframe][BackupManager] Backup failed:", errorMessage);
3550
+ this.updateBackupStatus(userId, request.method, {
3551
+ error: errorMessage
3552
+ });
3553
+ return {
3554
+ success: false,
3555
+ method: request.method,
3556
+ timestamp: Date.now(),
3557
+ error: errorMessage
3558
+ };
3559
+ }
3560
+ }
3561
+ /**
3562
+ * Restore keyshare from server backup
3563
+ */
3564
+ async restoreFromServer(userId, password, accessToken) {
3565
+ console.log("[iframe][BackupManager] Restore from server request for userId:", userId);
3566
+ try {
3567
+ const { restoreFromServer: restoreFromServer2 } = await Promise.resolve().then(() => (init_server_backup(), server_backup_exports));
3568
+ const backupData = await restoreFromServer2(userId, this.tokenClient, password, accessToken);
3569
+ console.log("[iframe][BackupManager] \u2705 Server restore completed");
3570
+ localStorage.setItem(`tss.${userId}.keyshare`, backupData.keyshare);
3571
+ localStorage.setItem(`tss.${userId}.sessionId`, backupData.sessionId);
3572
+ localStorage.setItem(`tss.${userId}.ownerAddress`, backupData.ownerAddress);
3573
+ console.log("[iframe][BackupManager] Keyshare saved to localStorage");
3574
+ return backupData;
3575
+ } catch (error) {
3576
+ const errorMessage = error.message || "Restore failed";
3577
+ console.error("[iframe][BackupManager] Restore failed:", errorMessage);
3578
+ throw error;
3579
+ }
3580
+ }
3581
+ /**
3582
+ * Encrypt backup data without uploading (for cloud/local backups)
3583
+ * Returns encrypted data that parent can upload/download
3584
+ */
3585
+ async encryptBackupData(userId, password) {
3586
+ console.log("[iframe][BackupManager] Encrypting backup data for userId:", userId);
3587
+ const data = this.getCurrentKeyshareBackupData(userId);
3588
+ if (!data) {
3589
+ throw new Error("No keyshare data found for backup");
3590
+ }
3591
+ const { encryptKeyshare: encryptKeyshare2, deriveBackupPasswordFromPasskey: deriveBackupPasswordFromPasskey2 } = await Promise.resolve().then(() => (init_crypto_utils(), crypto_utils_exports));
3592
+ let encryptionPassword = password;
3593
+ let credentialId;
3594
+ if (!encryptionPassword) {
3595
+ const result = await deriveBackupPasswordFromPasskey2(userId);
3596
+ encryptionPassword = result.password;
3597
+ credentialId = result.credentialId;
3598
+ }
3599
+ const encrypted = await encryptKeyshare2(
3600
+ data,
3601
+ encryptionPassword,
3602
+ password ? "password" : "passkey",
3603
+ credentialId
3604
+ );
3605
+ console.log("[iframe][BackupManager] \u2705 Backup data encrypted");
3606
+ return encrypted;
3607
+ }
3608
+ /**
3609
+ * Restore keyshare from local file backup
3610
+ */
3611
+ async restoreFromLocalFile(fileContent, userId, password) {
3612
+ console.log("[iframe][BackupManager] Restore from local file request for userId:", userId);
3613
+ try {
3614
+ const { restoreFromLocalFile: restoreFromLocalFile2 } = await Promise.resolve().then(() => (init_local_backup(), local_backup_exports));
3615
+ const backupData = await restoreFromLocalFile2(fileContent, userId, password);
3616
+ console.log("[iframe][BackupManager] \u2705 Local file restore completed");
3617
+ localStorage.setItem(`tss.${userId}.keyshare`, backupData.keyshare);
3618
+ localStorage.setItem(`tss.${userId}.sessionId`, backupData.sessionId);
3619
+ localStorage.setItem(`tss.${userId}.ownerAddress`, backupData.ownerAddress);
3620
+ console.log("[iframe][BackupManager] Keyshare saved to localStorage");
3621
+ return backupData;
3622
+ } catch (error) {
3623
+ const errorMessage = error.message || "Local file restore failed";
3624
+ console.error("[iframe][BackupManager] Local file restore failed:", errorMessage);
3625
+ throw error;
3626
+ }
3627
+ }
3628
+ /**
3629
+ * Get available cloud providers
3630
+ */
3631
+ getAvailableCloudProviders() {
3632
+ return getCloudProviders();
3633
+ }
3634
+ };
3635
+
2625
3636
  // src/iframe/main.ts
3637
+ var IFRAME_VERSION = "1.2.1";
2626
3638
  var IframeWallet = class {
2627
3639
  constructor() {
2628
3640
  console.log("=".repeat(60));
2629
- console.log(" Lumia Passport Secure Wallet - iframe version 0.1.8");
3641
+ console.log(` Lumia Passport Secure Wallet - iframe version ${IFRAME_VERSION}`);
2630
3642
  console.log("=".repeat(60));
2631
3643
  this.messenger = new SecureMessenger();
2632
3644
  this.sessionManager = new SessionManager();
@@ -2635,6 +3647,7 @@ var IframeWallet = class {
2635
3647
  this.authManager = new AuthorizationManager();
2636
3648
  this.storage = new StorageManager();
2637
3649
  this.trustedApps = new TrustedAppsManager();
3650
+ this.backupManager = new BackupManager();
2638
3651
  }
2639
3652
  async initialize() {
2640
3653
  console.log("[iframe] Initializing Lumia Passport Secure Wallet...");
@@ -2666,7 +3679,13 @@ var IframeWallet = class {
2666
3679
  }
2667
3680
  async handleMessage(message, origin) {
2668
3681
  const { type, messageId, projectId } = message;
2669
- console.log(`[iframe] \u{1F4E8} Message: ${type} from ${origin.substring(0, 30)}...`);
3682
+ console.log(`[iframe] \u{1F4E8} Message received:`, {
3683
+ type,
3684
+ messageId: messageId.substring(0, 20),
3685
+ origin: origin.substring(0, 30),
3686
+ hasData: !!message.data,
3687
+ dataKeys: message.data ? Object.keys(message.data) : []
3688
+ });
2670
3689
  try {
2671
3690
  switch (type) {
2672
3691
  case "SDK_AUTH":
@@ -2693,6 +3712,24 @@ var IframeWallet = class {
2693
3712
  case "REMOVE_TRUSTED_APP":
2694
3713
  await this.handleRemoveTrustedApp(message, origin);
2695
3714
  break;
3715
+ case "CREATE_BACKUP":
3716
+ await this.handleCreateBackup(message, origin);
3717
+ break;
3718
+ case "GET_BACKUP_STATUS":
3719
+ await this.handleGetBackupStatus(message, origin);
3720
+ break;
3721
+ case "GET_CLOUD_PROVIDERS":
3722
+ await this.handleGetCloudProviders(message, origin);
3723
+ break;
3724
+ case "RESTORE_BACKUP":
3725
+ await this.handleRestoreBackup(message, origin);
3726
+ break;
3727
+ case "RESTORE_FROM_FILE":
3728
+ await this.handleRestoreFromFile(message, origin);
3729
+ break;
3730
+ case "ENCRYPT_BACKUP_DATA":
3731
+ await this.handleEncryptBackupData(message, origin);
3732
+ break;
2696
3733
  default:
2697
3734
  throw new Error(`Unknown message type: ${type}`);
2698
3735
  }
@@ -2903,6 +3940,218 @@ var IframeWallet = class {
2903
3940
  );
2904
3941
  console.log(`[iframe] \u2705 REMOVE_TRUSTED_APP: App removed`);
2905
3942
  }
3943
+ async handleCreateBackup(message, origin) {
3944
+ const { sessionToken, userId, backupRequest, accessToken } = message.data;
3945
+ const { messageId } = message;
3946
+ console.log(`[iframe] CREATE_BACKUP received:`, { userId, method: backupRequest?.method, hasSessionToken: !!sessionToken, hasAccessToken: !!accessToken, origin });
3947
+ if (!userId) {
3948
+ console.error("[iframe] CREATE_BACKUP: userId is missing!");
3949
+ this.messenger.sendError(messageId, "userId is required", origin);
3950
+ return;
3951
+ }
3952
+ if (!backupRequest) {
3953
+ console.error("[iframe] CREATE_BACKUP: backupRequest is missing!");
3954
+ this.messenger.sendError(messageId, "backupRequest is required", origin);
3955
+ return;
3956
+ }
3957
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
3958
+ console.error("[iframe] CREATE_BACKUP: Session validation failed", { sessionToken, origin });
3959
+ this.messenger.sendError(messageId, "Invalid session", origin);
3960
+ return;
3961
+ }
3962
+ console.log("[iframe] CREATE_BACKUP: Session validated, creating backup...");
3963
+ try {
3964
+ const result = await this.backupManager.createBackup(userId, backupRequest, accessToken);
3965
+ console.log("[iframe] CREATE_BACKUP: Backup result:", result);
3966
+ this.messenger.sendResponse(
3967
+ messageId,
3968
+ {
3969
+ type: "LUMIA_PASSPORT_BACKUP_CREATED",
3970
+ result
3971
+ },
3972
+ origin
3973
+ );
3974
+ if (result.success) {
3975
+ console.log(`[iframe] \u2705 CREATE_BACKUP: Backup created successfully`);
3976
+ } else {
3977
+ console.error(`[iframe] \u274C CREATE_BACKUP: Backup failed - ${result.error}`);
3978
+ }
3979
+ } catch (error) {
3980
+ console.error("[iframe] CREATE_BACKUP: Exception:", error);
3981
+ this.messenger.sendError(messageId, error.message || "Backup failed", origin);
3982
+ }
3983
+ }
3984
+ async handleGetBackupStatus(message, origin) {
3985
+ const { sessionToken, userId } = message.data;
3986
+ const { messageId } = message;
3987
+ console.log(`[iframe] GET_BACKUP_STATUS: userId=${userId}`);
3988
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
3989
+ console.error("[iframe] GET_BACKUP_STATUS: Session validation failed");
3990
+ throw new Error("Invalid session");
3991
+ }
3992
+ const status = this.backupManager.getBackupStatus(userId);
3993
+ this.messenger.sendResponse(
3994
+ messageId,
3995
+ {
3996
+ type: "LUMIA_PASSPORT_BACKUP_STATUS",
3997
+ status
3998
+ },
3999
+ origin
4000
+ );
4001
+ console.log(`[iframe] \u2705 GET_BACKUP_STATUS: Status retrieved`);
4002
+ }
4003
+ async handleGetCloudProviders(message, origin) {
4004
+ const { sessionToken } = message.data;
4005
+ const { messageId } = message;
4006
+ console.log(`[iframe] GET_CLOUD_PROVIDERS`);
4007
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
4008
+ console.error("[iframe] GET_CLOUD_PROVIDERS: Session validation failed");
4009
+ throw new Error("Invalid session");
4010
+ }
4011
+ const providers = this.backupManager.getAvailableCloudProviders();
4012
+ this.messenger.sendResponse(
4013
+ messageId,
4014
+ {
4015
+ type: "LUMIA_PASSPORT_CLOUD_PROVIDERS",
4016
+ providers
4017
+ },
4018
+ origin
4019
+ );
4020
+ console.log(`[iframe] \u2705 GET_CLOUD_PROVIDERS: Providers retrieved`);
4021
+ }
4022
+ async handleRestoreBackup(message, origin) {
4023
+ const { sessionToken, userId, password, accessToken } = message.data;
4024
+ const { messageId } = message;
4025
+ console.log(`[iframe] RESTORE_BACKUP received:`, { userId, hasPassword: !!password, hasSessionToken: !!sessionToken, hasAccessToken: !!accessToken, origin });
4026
+ if (!userId) {
4027
+ console.error("[iframe] RESTORE_BACKUP: userId is missing!");
4028
+ this.messenger.sendError(messageId, "userId is required", origin);
4029
+ return;
4030
+ }
4031
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
4032
+ console.error("[iframe] RESTORE_BACKUP: Session validation failed", { sessionToken, origin });
4033
+ this.messenger.sendError(messageId, "Invalid session", origin);
4034
+ return;
4035
+ }
4036
+ console.log("[iframe] RESTORE_BACKUP: Session validated, restoring backup...");
4037
+ try {
4038
+ const backupData = await this.backupManager.restoreFromServer(userId, password, accessToken);
4039
+ console.log("[iframe] RESTORE_BACKUP: Restore successful");
4040
+ this.messenger.sendResponse(
4041
+ messageId,
4042
+ {
4043
+ type: "LUMIA_PASSPORT_BACKUP_RESTORED",
4044
+ result: {
4045
+ success: true,
4046
+ timestamp: Date.now(),
4047
+ data: backupData
4048
+ }
4049
+ },
4050
+ origin
4051
+ );
4052
+ console.log(`[iframe] \u2705 RESTORE_BACKUP: Backup restored successfully`);
4053
+ } catch (error) {
4054
+ console.error("[iframe] RESTORE_BACKUP: Exception:", error);
4055
+ this.messenger.sendResponse(
4056
+ messageId,
4057
+ {
4058
+ type: "LUMIA_PASSPORT_BACKUP_RESTORED",
4059
+ result: {
4060
+ success: false,
4061
+ timestamp: Date.now(),
4062
+ error: error.message || "Restore failed"
4063
+ }
4064
+ },
4065
+ origin
4066
+ );
4067
+ console.log(`[iframe] \u274C RESTORE_BACKUP: Restore failed - ${error.message}`);
4068
+ }
4069
+ }
4070
+ async handleRestoreFromFile(message, origin) {
4071
+ const { sessionToken, userId, fileContent, password } = message.data;
4072
+ const { messageId } = message;
4073
+ console.log(`[iframe] RESTORE_FROM_FILE received:`, { userId, hasPassword: !!password, hasFileContent: !!fileContent, hasSessionToken: !!sessionToken, origin });
4074
+ if (!userId) {
4075
+ console.error("[iframe] RESTORE_FROM_FILE: userId is missing!");
4076
+ this.messenger.sendError(messageId, "userId is required", origin);
4077
+ return;
4078
+ }
4079
+ if (!fileContent) {
4080
+ console.error("[iframe] RESTORE_FROM_FILE: fileContent is missing!");
4081
+ this.messenger.sendError(messageId, "fileContent is required", origin);
4082
+ return;
4083
+ }
4084
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
4085
+ console.error("[iframe] RESTORE_FROM_FILE: Session validation failed", { sessionToken, origin });
4086
+ this.messenger.sendError(messageId, "Invalid session", origin);
4087
+ return;
4088
+ }
4089
+ console.log("[iframe] RESTORE_FROM_FILE: Session validated, restoring from file...");
4090
+ try {
4091
+ const backupData = await this.backupManager.restoreFromLocalFile(fileContent, userId, password);
4092
+ console.log("[iframe] RESTORE_FROM_FILE: Restore successful");
4093
+ this.messenger.sendResponse(
4094
+ messageId,
4095
+ {
4096
+ type: "LUMIA_PASSPORT_FILE_RESTORED",
4097
+ result: {
4098
+ success: true,
4099
+ timestamp: Date.now(),
4100
+ data: backupData
4101
+ }
4102
+ },
4103
+ origin
4104
+ );
4105
+ console.log(`[iframe] \u2705 RESTORE_FROM_FILE: File restored successfully`);
4106
+ } catch (error) {
4107
+ console.error("[iframe] RESTORE_FROM_FILE: Exception:", error);
4108
+ this.messenger.sendResponse(
4109
+ messageId,
4110
+ {
4111
+ type: "LUMIA_PASSPORT_FILE_RESTORED",
4112
+ result: {
4113
+ success: false,
4114
+ timestamp: Date.now(),
4115
+ error: error.message || "File restore failed"
4116
+ }
4117
+ },
4118
+ origin
4119
+ );
4120
+ console.log(`[iframe] \u274C RESTORE_FROM_FILE: Restore failed - ${error.message}`);
4121
+ }
4122
+ }
4123
+ async handleEncryptBackupData(message, origin) {
4124
+ const { sessionToken, userId, password } = message.data;
4125
+ const { messageId } = message;
4126
+ console.log(`[iframe] ENCRYPT_BACKUP_DATA received:`, { userId, hasPassword: !!password, hasSessionToken: !!sessionToken, origin });
4127
+ if (!userId) {
4128
+ console.error("[iframe] ENCRYPT_BACKUP_DATA: userId is missing!");
4129
+ this.messenger.sendError(messageId, "userId is required", origin);
4130
+ return;
4131
+ }
4132
+ if (!this.sessionManager.validateSession(sessionToken, origin)) {
4133
+ console.error("[iframe] ENCRYPT_BACKUP_DATA: Session validation failed", { sessionToken, origin });
4134
+ this.messenger.sendError(messageId, "Invalid session", origin);
4135
+ return;
4136
+ }
4137
+ console.log("[iframe] ENCRYPT_BACKUP_DATA: Session validated, encrypting backup data...");
4138
+ try {
4139
+ const encryptedData = await this.backupManager.encryptBackupData(userId, password);
4140
+ console.log("[iframe] ENCRYPT_BACKUP_DATA: Encryption successful");
4141
+ this.messenger.sendResponse(
4142
+ messageId,
4143
+ {
4144
+ type: "LUMIA_PASSPORT_BACKUP_ENCRYPTED",
4145
+ encryptedData
4146
+ },
4147
+ origin
4148
+ );
4149
+ console.log(`[iframe] \u2705 ENCRYPT_BACKUP_DATA: Data encrypted successfully`);
4150
+ } catch (error) {
4151
+ console.error("[iframe] ENCRYPT_BACKUP_DATA: Exception:", error);
4152
+ this.messenger.sendError(messageId, error.message || "Encryption failed", origin);
4153
+ }
4154
+ }
2906
4155
  displayReadyIndicator() {
2907
4156
  const app = document.getElementById("app");
2908
4157
  if (!app) return;