@thezelijah/majik-message 1.0.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.
Files changed (35) hide show
  1. package/LICENSE +67 -0
  2. package/README.md +265 -0
  3. package/dist/core/contacts/majik-contact-directory.d.ts +34 -0
  4. package/dist/core/contacts/majik-contact-directory.js +165 -0
  5. package/dist/core/contacts/majik-contact.d.ts +53 -0
  6. package/dist/core/contacts/majik-contact.js +135 -0
  7. package/dist/core/crypto/constants.d.ts +7 -0
  8. package/dist/core/crypto/constants.js +6 -0
  9. package/dist/core/crypto/crypto-provider.d.ts +20 -0
  10. package/dist/core/crypto/crypto-provider.js +70 -0
  11. package/dist/core/crypto/encryption-engine.d.ts +59 -0
  12. package/dist/core/crypto/encryption-engine.js +257 -0
  13. package/dist/core/crypto/keystore.d.ts +126 -0
  14. package/dist/core/crypto/keystore.js +575 -0
  15. package/dist/core/messages/envelope-cache.d.ts +51 -0
  16. package/dist/core/messages/envelope-cache.js +375 -0
  17. package/dist/core/messages/message-envelope.d.ts +36 -0
  18. package/dist/core/messages/message-envelope.js +161 -0
  19. package/dist/core/scanner/scanner-engine.d.ts +27 -0
  20. package/dist/core/scanner/scanner-engine.js +120 -0
  21. package/dist/core/types.d.ts +23 -0
  22. package/dist/core/types.js +1 -0
  23. package/dist/core/utils/APITranscoder.d.ts +114 -0
  24. package/dist/core/utils/APITranscoder.js +305 -0
  25. package/dist/core/utils/idb-majik-system.d.ts +15 -0
  26. package/dist/core/utils/idb-majik-system.js +37 -0
  27. package/dist/core/utils/majik-file-utils.d.ts +16 -0
  28. package/dist/core/utils/majik-file-utils.js +153 -0
  29. package/dist/core/utils/utilities.d.ts +22 -0
  30. package/dist/core/utils/utilities.js +80 -0
  31. package/dist/index.d.ts +13 -0
  32. package/dist/index.js +12 -0
  33. package/dist/majik-message.d.ts +202 -0
  34. package/dist/majik-message.js +940 -0
  35. package/package.json +97 -0
@@ -0,0 +1,120 @@
1
+ import { MessageEnvelope, MessageEnvelopeError, } from "../messages/message-envelope";
2
+ /* -------------------------------
3
+ * ScannerEngine
4
+ * ------------------------------- */
5
+ export class ScannerEngine {
6
+ observer;
7
+ contactDirectory;
8
+ envelopeCache;
9
+ onEnvelopeFound;
10
+ onUntrusted;
11
+ onError;
12
+ processedNodes = new WeakSet();
13
+ constructor(config) {
14
+ this.contactDirectory = config.contactDirectory;
15
+ this.envelopeCache = config.envelopeCache;
16
+ this.onEnvelopeFound = config.onEnvelopeFound;
17
+ this.onUntrusted = config.onUntrusted;
18
+ this.onError = config.onError;
19
+ }
20
+ /* -------------------------------
21
+ * Scan a single string
22
+ * ------------------------------- */
23
+ async scanText(text) {
24
+ if (!text || typeof text !== "string")
25
+ return;
26
+ const lines = text.split(/\s+/);
27
+ for (const line of lines) {
28
+ if (!MessageEnvelope.isEnvelopeCandidate(line))
29
+ continue;
30
+ try {
31
+ const envelope = MessageEnvelope.fromMatchedString(line);
32
+ // 1️⃣ Check cache first
33
+ if (this.envelopeCache && (await this.envelopeCache.has(envelope))) {
34
+ continue; // already processed
35
+ }
36
+ // 2️⃣ Check contact trust
37
+ const isTrusted = this.contactDirectory.hasContactForEnvelope(envelope);
38
+ if (!isTrusted) {
39
+ this.onUntrusted?.(line);
40
+ continue;
41
+ }
42
+ // 3️⃣ Check if contact is blocked
43
+ const contact = this.contactDirectory.getContactByFingerprint(envelope.extractFingerprint());
44
+ if (contact?.isBlocked()) {
45
+ this.onUntrusted?.(line);
46
+ continue;
47
+ }
48
+ // 4️⃣ Call callback
49
+ this.onEnvelopeFound?.(envelope);
50
+ // 5️⃣ Store in cache
51
+ await this.envelopeCache?.set(envelope);
52
+ }
53
+ catch (err) {
54
+ if (err instanceof MessageEnvelopeError) {
55
+ this.onUntrusted?.(err.raw ?? String(line));
56
+ }
57
+ else {
58
+ this.onError?.(err, { raw: line });
59
+ }
60
+ }
61
+ }
62
+ }
63
+ /* -------------------------------
64
+ * Scan all text nodes under a DOM node
65
+ * ------------------------------- */
66
+ scanDOM(rootNode) {
67
+ const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT, null);
68
+ let node = walker.nextNode();
69
+ while (node) {
70
+ // Skip text nodes that belong to input-like elements to avoid collisions
71
+ const parent = node.parentElement;
72
+ const isInputLike = parent
73
+ ? ["INPUT", "TEXTAREA", "SELECT"].includes(parent.tagName) ||
74
+ parent.isContentEditable
75
+ : false;
76
+ if (!isInputLike && !this.processedNodes.has(node)) {
77
+ this.scanText(node.textContent ?? "");
78
+ this.processedNodes.add(node);
79
+ }
80
+ node = walker.nextNode();
81
+ }
82
+ }
83
+ /* -------------------------------
84
+ * Observe DOM changes and scan dynamically
85
+ * ------------------------------- */
86
+ startDOMObserver(rootNode) {
87
+ if (this.observer)
88
+ return; // already observing
89
+ this.observer = new MutationObserver((mutations) => {
90
+ for (const mutation of mutations) {
91
+ if (mutation.type === "characterData" && mutation.target) {
92
+ const targetNode = mutation.target;
93
+ const parent = targetNode.parentElement || null;
94
+ const isInputLike = parent
95
+ ? ["INPUT", "TEXTAREA", "SELECT"].includes(parent.tagName) ||
96
+ parent.isContentEditable
97
+ : false;
98
+ if (!isInputLike && !this.processedNodes.has(targetNode)) {
99
+ this.scanText(targetNode.textContent ?? "");
100
+ this.processedNodes.add(targetNode);
101
+ }
102
+ }
103
+ else if (mutation.type === "childList") {
104
+ mutation.addedNodes.forEach((node) => this.scanDOM(node));
105
+ }
106
+ }
107
+ });
108
+ this.observer.observe(rootNode, {
109
+ childList: true,
110
+ subtree: true,
111
+ characterData: true,
112
+ });
113
+ // Initial scan
114
+ this.scanDOM(rootNode);
115
+ }
116
+ stopDOMObserver() {
117
+ this.observer?.disconnect();
118
+ this.observer = undefined;
119
+ }
120
+ }
@@ -0,0 +1,23 @@
1
+ export type ISODateString = string;
2
+ export interface MAJIK_API_RESPONSE {
3
+ success: boolean;
4
+ message: string;
5
+ code?: string;
6
+ }
7
+ export interface SingleRecipientPayload {
8
+ iv: string;
9
+ ciphertext: string;
10
+ ephemeralPublicKey: string;
11
+ }
12
+ export interface MultiRecipientPayload {
13
+ iv: string;
14
+ ciphertext: string;
15
+ ephemeralPublicKey: string;
16
+ keys: RecipientKeys[];
17
+ }
18
+ export interface RecipientKeys {
19
+ fingerprint: string;
20
+ ephemeralEncryptedKey: string;
21
+ nonce: string;
22
+ }
23
+ export type EnvelopePayload = SingleRecipientPayload | MultiRecipientPayload;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,114 @@
1
+ export interface EncryptedData {
2
+ rqc: Uint8Array;
3
+ iv: Uint8Array;
4
+ cipher: Uint8Array;
5
+ }
6
+ export interface SerializedEncryptedData {
7
+ rqc: string;
8
+ iv: string;
9
+ cipher: string;
10
+ }
11
+ declare class APITranscoder {
12
+ /**
13
+ * Generates a 32-byte cipher key encoded as a base64 string, suitable for use with Fernet encryption.
14
+ * @returns {string} The generated cipher key ("rqc") in base64 format.
15
+ */
16
+ static generateRQC(): string;
17
+ /**
18
+ * Generates a transformed version of the rqc by reversing and interleaving the bytes.
19
+ * If rqc is not provided, a new one will be generated automatically.
20
+ * @param {string} [rqc] - Optional. The original 32-byte cipher key in base64 format.
21
+ * @returns {string} The transformed key as a base64 string.
22
+ */
23
+ static generateRQX(rqc?: string | null): string;
24
+ /**
25
+ * Decodes the transformed rqx back to the original rqc base64 string.
26
+ * @param {string} rqx - The transformed key in base64 format.
27
+ * @returns {string} The original rqc in base64 format.
28
+ */
29
+ static decodeRQX(rqx: string): string;
30
+ static hashData(inputJson: Record<string, unknown>): string;
31
+ /**
32
+ * Generates a SHA-256 hash for a given string.
33
+ * Validates that the input is a non-empty string.
34
+ * @param {string} string - The string to hash.
35
+ * @returns {string} The hash of the string in hexadecimal format.
36
+ * @throws {Error} If the input is not a valid non-empty string.
37
+ */
38
+ static hashString(string: string): string;
39
+ /**
40
+ * Verifies that the hash of a decoded JSON object matches a provided hash.
41
+ * @param {Object} decodedJson - The JSON object to verify.
42
+ * @param {string} providedHash - The hash to compare against.
43
+ * @returns {boolean} True if the hashes match, false otherwise.
44
+ * @throws {Error} If inputs are invalid.
45
+ */
46
+ static verifyHashJSON(decodedJson: Record<string, unknown>, providedHash: string): boolean;
47
+ /**
48
+ * Verifies that the hash of a given string matches a provided hash.
49
+ * @param {string} string - The string to verify.
50
+ * @param {string} providedHash - The hash to compare against.
51
+ * @returns {boolean} True if the hashes match, false otherwise.
52
+ * @throws {Error} If inputs are invalid.
53
+ */
54
+ static verifyHashString(string: string, providedHash: string): boolean;
55
+ /**
56
+ * Decrypts an encrypted payload using AES-GCM encryption and returns the original JSON.
57
+ * @param {string} encrypted - The encrypted object data.
58
+ * @returns {Object} The decrypted and parsed JSON object.
59
+ * @throws {Error} Throws an error if decryption fails.
60
+ */
61
+ static decryptPayload(encrypted: SerializedEncryptedData): Record<string, unknown>;
62
+ /**
63
+ * Encrypts a JSON object using AES-GCM encryption with the provided cipher key.
64
+ * @param {Object} json - The JSON object to encrypt.
65
+ * @param {string} rqc - The 32-byte cipher key in base64 format.
66
+ * @returns {Object} An object containing the encrypted payload and the transformed key, potentially URL-encoded.
67
+ * @throws {Error} Throws an error if encryption fails.
68
+ */
69
+ static encryptPayload(json: Record<string, unknown>, rqc: string): SerializedEncryptedData;
70
+ }
71
+ export default APITranscoder;
72
+ export declare function generateRQKey(auth: string): string;
73
+ export declare function getAuthFromRQKey(rqkey: string): string;
74
+ export declare function validateRQKey(rqkey: string): boolean;
75
+ export declare function getDecodedRQKey(rqkey: string): string;
76
+ export declare function getSecureKeyFromRQKey(rqkey: string): string | number | object;
77
+ /**
78
+ * Converts input to a string and reverses it.
79
+ * If the input is a number, it is converted to a string.
80
+ * If the input is an object (JSON), it is stringified.
81
+ * If secure is true, it returns a Base64 encoded result.
82
+ * @param {any} input - The input to reverse.
83
+ * @param {boolean} secure - Whether to Base64 encode the result.
84
+ * @returns {string} - The reversed string, possibly encoded.
85
+ */
86
+ export declare function secureReverse(input: string | object | number, secure?: boolean): string;
87
+ /**
88
+ * Decodes the reversed string based on the mode provided.
89
+ * If secure is true, the reversed string is decoded from Base64 first.
90
+ * @param {string} reversedString - The reversed string (possibly Base64 encoded).
91
+ * @param {string|null} mode - The mode to decode ('json', 'number', or 'string').
92
+ * @param {boolean} secure - Whether the input is Base64 encoded.
93
+ * @returns {any} - The decoded result based on the mode.
94
+ */
95
+ export declare function decodeReverse(reversedString: string, mode?: string, secure?: boolean): string | object | number;
96
+ /**
97
+ * Converts a stringified JSON into a Uint8Array (UTF-8 bytes)
98
+ */
99
+ export declare function jsonStringToBytes(jsonString: string): Uint8Array;
100
+ /**
101
+ * Converts a Uint8Array (UTF-8 bytes) back into a stringified JSON
102
+ */
103
+ export declare function bytesToJsonString(bytes: Uint8Array): string;
104
+ /**
105
+ * Converts EncryptedData (Uint8Array fields) into base64 strings
106
+ * for safe transport / storage (JSON, URLs, APIs, etc.)
107
+ */
108
+ export declare function encryptedDataToBase64(data: EncryptedData): SerializedEncryptedData;
109
+ /**
110
+ * Converts SerializedEncryptedData (base64 fields)
111
+ * back into EncryptedData (Uint8Array fields)
112
+ */
113
+ export declare function encryptedDataFromBase64(data: SerializedEncryptedData): EncryptedData;
114
+ export declare function base64ToJson<T = unknown>(base64: string): T;
@@ -0,0 +1,305 @@
1
+ import { hash } from "@stablelib/sha256";
2
+ import { AES } from "@stablelib/aes";
3
+ import { GCM } from "@stablelib/gcm";
4
+ import { randomBytes } from "@stablelib/random";
5
+ import { arrayToBase64, base64ToUint8Array } from "./utilities";
6
+ class APITranscoder {
7
+ /**
8
+ * Generates a 32-byte cipher key encoded as a base64 string, suitable for use with Fernet encryption.
9
+ * @returns {string} The generated cipher key ("rqc") in base64 format.
10
+ */
11
+ static generateRQC() {
12
+ const bytes = randomBytes(32); // 32 bytes = 256 bits
13
+ const fKey = arrayToBase64(bytes);
14
+ return fKey; // Converts the byte array to a base64 string
15
+ }
16
+ /**
17
+ * Generates a transformed version of the rqc by reversing and interleaving the bytes.
18
+ * If rqc is not provided, a new one will be generated automatically.
19
+ * @param {string} [rqc] - Optional. The original 32-byte cipher key in base64 format.
20
+ * @returns {string} The transformed key as a base64 string.
21
+ */
22
+ static generateRQX(rqc = null) {
23
+ // Auto-generate rqc if it's empty or null
24
+ if (!rqc) {
25
+ rqc = this.generateRQC();
26
+ }
27
+ // Decode the input rqc to bytes
28
+ const rqcBytes = Uint8Array.from(atob(rqc), (c) => c.charCodeAt(0)); // Decode base64 to byte array
29
+ const intArray = Array.from(rqcBytes);
30
+ // Reverse the array
31
+ const reversedArray = [...intArray].reverse();
32
+ // Interleave original and reversed arrays
33
+ const interleavedArray = [];
34
+ for (let i = 0; i < intArray.length; i++) {
35
+ interleavedArray.push(intArray[i], reversedArray[i]);
36
+ }
37
+ // Convert the interleaved array to a base64 string and return
38
+ return btoa(String.fromCharCode(...interleavedArray)); // Convert the byte array to base64 string
39
+ }
40
+ /**
41
+ * Decodes the transformed rqx back to the original rqc base64 string.
42
+ * @param {string} rqx - The transformed key in base64 format.
43
+ * @returns {string} The original rqc in base64 format.
44
+ */
45
+ static decodeRQX(rqx) {
46
+ const interleavedArray = Uint8Array.from(atob(rqx), (c) => c.charCodeAt(0)); // Decode base64 to byte array
47
+ // Separate the original and reversed arrays from the interleaved array
48
+ const originalArray = [];
49
+ const reversedArray = [];
50
+ for (let i = 0; i < interleavedArray.length; i += 2) {
51
+ originalArray.push(interleavedArray[i]);
52
+ reversedArray.push(interleavedArray[i + 1]);
53
+ }
54
+ // Verify reversedArray matches the reverse of originalArray (optional, for validation)
55
+ if (reversedArray.join() !== [...originalArray].reverse().join()) {
56
+ throw new Error("Decoded rqx does not match original rqc format.");
57
+ }
58
+ // Convert the original array back to a base64 string representing the original rqc
59
+ return btoa(String.fromCharCode(...originalArray)); // Convert to base64 string
60
+ }
61
+ static hashData(inputJson) {
62
+ const jsonString = JSON.stringify(inputJson, Object.keys(inputJson).sort());
63
+ const hashString = hash(jsonStringToBytes(jsonString));
64
+ return hashString.toString();
65
+ }
66
+ /**
67
+ * Generates a SHA-256 hash for a given string.
68
+ * Validates that the input is a non-empty string.
69
+ * @param {string} string - The string to hash.
70
+ * @returns {string} The hash of the string in hexadecimal format.
71
+ * @throws {Error} If the input is not a valid non-empty string.
72
+ */
73
+ static hashString(string) {
74
+ const hashed = hash(new TextEncoder().encode(string));
75
+ return hashed.toString();
76
+ }
77
+ /**
78
+ * Verifies that the hash of a decoded JSON object matches a provided hash.
79
+ * @param {Object} decodedJson - The JSON object to verify.
80
+ * @param {string} providedHash - The hash to compare against.
81
+ * @returns {boolean} True if the hashes match, false otherwise.
82
+ * @throws {Error} If inputs are invalid.
83
+ */
84
+ static verifyHashJSON(decodedJson, providedHash) {
85
+ const generatedHash = this.hashData(decodedJson);
86
+ return generatedHash === providedHash;
87
+ }
88
+ /**
89
+ * Verifies that the hash of a given string matches a provided hash.
90
+ * @param {string} string - The string to verify.
91
+ * @param {string} providedHash - The hash to compare against.
92
+ * @returns {boolean} True if the hashes match, false otherwise.
93
+ * @throws {Error} If inputs are invalid.
94
+ */
95
+ static verifyHashString(string, providedHash) {
96
+ if (typeof string !== "string" || string.trim() === "") {
97
+ throw new Error("Invalid input: 'string' must be a non-empty string.");
98
+ }
99
+ if (typeof providedHash !== "string" || providedHash.trim() === "") {
100
+ throw new Error("Invalid input: 'providedHash' must be a non-empty string.");
101
+ }
102
+ const generatedHash = this.hashString(string);
103
+ return generatedHash === providedHash;
104
+ }
105
+ /**
106
+ * Decrypts an encrypted payload using AES-GCM encryption and returns the original JSON.
107
+ * @param {string} encrypted - The encrypted object data.
108
+ * @returns {Object} The decrypted and parsed JSON object.
109
+ * @throws {Error} Throws an error if decryption fails.
110
+ */
111
+ static decryptPayload(encrypted) {
112
+ try {
113
+ const serialized = encryptedDataFromBase64(encrypted);
114
+ const { rqc, iv, cipher } = serialized;
115
+ const aes = new AES(rqc);
116
+ const gcm = new GCM(aes);
117
+ const decrypted = gcm.open(iv, cipher);
118
+ if (!decrypted) {
119
+ throw new Error("Decryption failed or payload was tampered.");
120
+ }
121
+ const base64String = arrayToBase64(decrypted);
122
+ const parsedData = base64ToJson(base64String);
123
+ return parsedData;
124
+ }
125
+ catch (error) {
126
+ throw new Error(`Decryption failed: ${error}`);
127
+ }
128
+ }
129
+ /**
130
+ * Encrypts a JSON object using AES-GCM encryption with the provided cipher key.
131
+ * @param {Object} json - The JSON object to encrypt.
132
+ * @param {string} rqc - The 32-byte cipher key in base64 format.
133
+ * @returns {Object} An object containing the encrypted payload and the transformed key, potentially URL-encoded.
134
+ * @throws {Error} Throws an error if encryption fails.
135
+ */
136
+ static encryptPayload(json, rqc) {
137
+ try {
138
+ const jsonString = JSON.stringify(json);
139
+ const data = jsonStringToBytes(jsonString);
140
+ const bufferRQC = base64ToUint8Array(rqc);
141
+ const iv = randomBytes(12);
142
+ const aes = new AES(bufferRQC);
143
+ const gcm = new GCM(aes);
144
+ const cipher = gcm.seal(iv, data);
145
+ const encrypted = { rqc: bufferRQC, iv, cipher };
146
+ const serialized = encryptedDataToBase64(encrypted);
147
+ return serialized;
148
+ }
149
+ catch (error) {
150
+ throw new Error(`Encryption failed: ${error}`);
151
+ }
152
+ }
153
+ }
154
+ export default APITranscoder;
155
+ export function generateRQKey(auth) {
156
+ const now = Math.floor(Date.now() / 1000);
157
+ const appendedKey = now + ":" + auth + ":" + secureReverse(auth, true);
158
+ return btoa(appendedKey);
159
+ }
160
+ export function getAuthFromRQKey(rqkey) {
161
+ const decodedKey = getDecodedRQKey(rqkey);
162
+ const auth = decodedKey.split(":")[1];
163
+ return auth;
164
+ }
165
+ export function validateRQKey(rqkey) {
166
+ const auth = getAuthFromRQKey(rqkey);
167
+ const secureKey = getSecureKeyFromRQKey(rqkey);
168
+ if (auth !== secureKey) {
169
+ console.error("Access denied. This key is not valid.");
170
+ return false;
171
+ }
172
+ return true;
173
+ }
174
+ export function getDecodedRQKey(rqkey) {
175
+ return atob(rqkey);
176
+ }
177
+ export function getSecureKeyFromRQKey(rqkey) {
178
+ const decodedKey = getDecodedRQKey(rqkey);
179
+ const secureKey = decodeReverse(decodedKey.split(":")[2], "string", true);
180
+ return secureKey;
181
+ }
182
+ /**
183
+ * Converts input to a string and reverses it.
184
+ * If the input is a number, it is converted to a string.
185
+ * If the input is an object (JSON), it is stringified.
186
+ * If secure is true, it returns a Base64 encoded result.
187
+ * @param {any} input - The input to reverse.
188
+ * @param {boolean} secure - Whether to Base64 encode the result.
189
+ * @returns {string} - The reversed string, possibly encoded.
190
+ */
191
+ export function secureReverse(input, secure = true) {
192
+ let str;
193
+ if (typeof input === "number") {
194
+ str = input.toString();
195
+ }
196
+ else if (typeof input === "object") {
197
+ str = JSON.stringify(input);
198
+ }
199
+ else {
200
+ str = input;
201
+ }
202
+ let reversedString = str.split("").reverse().join("");
203
+ if (secure) {
204
+ // reversedString = btoa(reversedString);
205
+ reversedString = Buffer.from(reversedString).toString("base64");
206
+ }
207
+ return reversedString;
208
+ }
209
+ /**
210
+ * Decodes the reversed string based on the mode provided.
211
+ * If secure is true, the reversed string is decoded from Base64 first.
212
+ * @param {string} reversedString - The reversed string (possibly Base64 encoded).
213
+ * @param {string|null} mode - The mode to decode ('json', 'number', or 'string').
214
+ * @param {boolean} secure - Whether the input is Base64 encoded.
215
+ * @returns {any} - The decoded result based on the mode.
216
+ */
217
+ export function decodeReverse(reversedString, mode = "string", secure = true) {
218
+ // If secure is true, decode the reversed string from Base64
219
+ if (secure) {
220
+ reversedString = atob(reversedString);
221
+ }
222
+ const restoredString = reversedString.split("").reverse().join("");
223
+ if (mode === "json") {
224
+ try {
225
+ return JSON.parse(restoredString);
226
+ }
227
+ catch (error) {
228
+ throw new Error(`Invalid JSON format. ${error}`);
229
+ }
230
+ }
231
+ else if (mode === "number") {
232
+ const number = parseFloat(restoredString);
233
+ if (!number) {
234
+ throw new Error("Reversed string is not a valid number");
235
+ }
236
+ return number;
237
+ }
238
+ else {
239
+ return restoredString; // Treat as string if mode is 'string' or null
240
+ }
241
+ }
242
+ /* ---------------------------------
243
+ * JSON <-> Bytes Utilities
244
+ * --------------------------------- */
245
+ /**
246
+ * Converts a stringified JSON into a Uint8Array (UTF-8 bytes)
247
+ */
248
+ export function jsonStringToBytes(jsonString) {
249
+ return new TextEncoder().encode(jsonString);
250
+ }
251
+ /**
252
+ * Converts a Uint8Array (UTF-8 bytes) back into a stringified JSON
253
+ */
254
+ export function bytesToJsonString(bytes) {
255
+ return new TextDecoder().decode(bytes);
256
+ }
257
+ /**
258
+ * Converts EncryptedData (Uint8Array fields) into base64 strings
259
+ * for safe transport / storage (JSON, URLs, APIs, etc.)
260
+ */
261
+ export function encryptedDataToBase64(data) {
262
+ return {
263
+ rqc: arrayToBase64(data.rqc),
264
+ iv: arrayToBase64(data.iv),
265
+ cipher: arrayToBase64(data.cipher),
266
+ };
267
+ }
268
+ /**
269
+ * Converts SerializedEncryptedData (base64 fields)
270
+ * back into EncryptedData (Uint8Array fields)
271
+ */
272
+ export function encryptedDataFromBase64(data) {
273
+ return {
274
+ rqc: base64ToUint8Array(data.rqc),
275
+ iv: base64ToUint8Array(data.iv),
276
+ cipher: base64ToUint8Array(data.cipher),
277
+ };
278
+ }
279
+ export function base64ToJson(base64) {
280
+ try {
281
+ // 🔹 Step 0: Clean the string (remove whitespace / newlines)
282
+ const cleanBase64 = base64.replace(/\s+/g, "");
283
+ let jsonString;
284
+ if (typeof atob === "function") {
285
+ // 🔹 Browser / Service Worker (atob is available globally)
286
+ jsonString = decodeURIComponent(atob(cleanBase64)
287
+ .split("")
288
+ .map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, "0")}`)
289
+ .join(""));
290
+ }
291
+ else if (typeof Buffer !== "undefined") {
292
+ // 🔹 Node.js fallback (Buffer is available)
293
+ jsonString = Buffer.from(cleanBase64, "base64").toString("utf-8");
294
+ }
295
+ else {
296
+ throw new Error("No base64 decode available in this environment");
297
+ }
298
+ // 🔹 Parse JSON
299
+ return JSON.parse(jsonString);
300
+ }
301
+ catch (err) {
302
+ console.error("base64ToJson error:", err);
303
+ throw new Error("Failed to decode Base64 JSON");
304
+ }
305
+ }
@@ -0,0 +1,15 @@
1
+ import { type IDBPDatabase } from "idb";
2
+ export interface ZWorldIDBSaveData {
3
+ id: string;
4
+ data: Blob;
5
+ savedAt: number;
6
+ }
7
+ interface ZWorldAutosaveSchema {
8
+ majikdata: ZWorldIDBSaveData;
9
+ }
10
+ export declare function initDB(): Promise<IDBPDatabase<ZWorldAutosaveSchema>>;
11
+ export declare function idbSaveBlob(id: string, data: Blob): Promise<void>;
12
+ export declare function idbLoadBlob(id: string): Promise<ZWorldIDBSaveData | undefined>;
13
+ export declare function deleteBlob(id: string): Promise<void>;
14
+ export declare function clearAllBlobs(): Promise<void>;
15
+ export {};
@@ -0,0 +1,37 @@
1
+ // lib/indexedDB.ts
2
+ import { openDB } from "idb";
3
+ let dbPromise;
4
+ export function initDB() {
5
+ if (!dbPromise) {
6
+ dbPromise = openDB("MajikAutosaveDB", 1, {
7
+ upgrade(db) {
8
+ if (!db.objectStoreNames.contains("majikdata")) {
9
+ db.createObjectStore("majikdata", { keyPath: "id" });
10
+ }
11
+ },
12
+ });
13
+ }
14
+ return dbPromise;
15
+ }
16
+ export async function idbSaveBlob(id, data) {
17
+ const db = await initDB();
18
+ await db.put("majikdata", { id, data, savedAt: Date.now() });
19
+ }
20
+ export async function idbLoadBlob(id) {
21
+ try {
22
+ const db = await initDB();
23
+ return await db.get("majikdata", id);
24
+ }
25
+ catch (err) {
26
+ console.error(`Failed to load blob with id "${id}":`, err);
27
+ return undefined;
28
+ }
29
+ }
30
+ export async function deleteBlob(id) {
31
+ const db = await initDB();
32
+ return db.delete("majikdata", id);
33
+ }
34
+ export async function clearAllBlobs() {
35
+ const db = await initDB();
36
+ return db.clear("majikdata");
37
+ }
@@ -0,0 +1,16 @@
1
+ interface MajikFileData {
2
+ /** JSON object */
3
+ j: unknown;
4
+ /** STX Timestamp */
5
+ s: string;
6
+ /** Version */
7
+ v: string;
8
+ }
9
+ export declare function importMajikFileData(file: File): Promise<MajikFileData>;
10
+ export declare function loadSavedMajikFileData(file: Blob): Promise<MajikFileData>;
11
+ export declare function autoSaveMajikFileData(json: unknown, version?: string): Blob;
12
+ export declare function exportMajikFileData(json: unknown, name: string, version?: string): void;
13
+ export declare function prepareToBlobFile(data: string): Blob;
14
+ export declare function base64EncodeUtf8(str: string): string;
15
+ export declare function base64DecodeUtf8(base64: string): string;
16
+ export {};