@majikah/majik-universal-id-client 0.0.1

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.
@@ -0,0 +1,177 @@
1
+ import { hash } from "@stablelib/sha256";
2
+ import { arrayToBase64 } from "./utils/utilities";
3
+ /**
4
+ * Utility assertions
5
+ */
6
+ function assert(condition, message) {
7
+ if (!condition)
8
+ throw new Error(message);
9
+ }
10
+ function assertString(value, field) {
11
+ assert(typeof value === "string" && value.trim().length > 0, `${field} must be a non-empty string`);
12
+ }
13
+ function assertISODate(value, field) {
14
+ const date = new Date(value);
15
+ assert(!isNaN(date.getTime()), `${field} must be a valid ISO timestamp`);
16
+ }
17
+ function sha256(input) {
18
+ const hashed = hash(new TextEncoder().encode(input));
19
+ return arrayToBase64(hashed);
20
+ }
21
+ /**
22
+ * MajikMessageIdentity
23
+ * Immutable identity container with integrity verification
24
+ */
25
+ export class MajikMessageIdentity {
26
+ // 🔒 Private backing fields
27
+ _id;
28
+ _userId;
29
+ _publicKey;
30
+ _mlKey;
31
+ _phash;
32
+ _label;
33
+ _timestamp;
34
+ _restricted;
35
+ /**
36
+ * Constructor is private to enforce controlled creation
37
+ */
38
+ constructor(params) {
39
+ assertString(params.id, "id");
40
+ assertString(params.userId, "user_id");
41
+ assertString(params.publicKey, "public_key");
42
+ assertString(params.mlKey, "ml_key");
43
+ assertString(params.phash, "phash");
44
+ assertString(params.label, "label");
45
+ assertISODate(params.timestamp, "timestamp");
46
+ assert(typeof params.restricted === "boolean", "restricted must be boolean");
47
+ this._id = params.id;
48
+ this._userId = params.userId;
49
+ this._publicKey = params.publicKey;
50
+ this._mlKey = params.mlKey;
51
+ this._phash = params.phash;
52
+ this._label = params.label;
53
+ this._timestamp = params.timestamp;
54
+ this._restricted = params.restricted;
55
+ // Final integrity check at construction
56
+ assert(this.validateIntegrity(), "Identity integrity validation failed");
57
+ }
58
+ // ─────────────────────────────
59
+ // Static factory
60
+ // ─────────────────────────────
61
+ /**
62
+ * Create a new immutable identity from MajikUser
63
+ */
64
+ static create(user, account, options) {
65
+ assert(user, "MajikUser is required");
66
+ const userValidResult = user.validate();
67
+ if (!userValidResult.isValid) {
68
+ throw new Error(`Invalid MajikUser: ${userValidResult.errors.join(", ")}`);
69
+ }
70
+ const label = options?.label || account?.meta?.label || user.displayName;
71
+ assertString(label, "label");
72
+ const timestamp = new Date().toISOString();
73
+ const publicKey = account.publicKeyBase64;
74
+ const phash = sha256(`${user.id}:${publicKey}:${account.id}:${account.mlKey}`);
75
+ return new MajikMessageIdentity({
76
+ id: account.id,
77
+ userId: user.id,
78
+ publicKey: publicKey,
79
+ mlKey: account.mlKey,
80
+ phash,
81
+ label,
82
+ timestamp,
83
+ restricted: options?.restricted ?? false,
84
+ });
85
+ }
86
+ // ─────────────────────────────
87
+ // Getters (safe, read-only)
88
+ // ─────────────────────────────
89
+ get id() {
90
+ return this._id;
91
+ }
92
+ get userID() {
93
+ return this._userId;
94
+ }
95
+ get publicKey() {
96
+ return this._publicKey;
97
+ }
98
+ get phash() {
99
+ return this._phash;
100
+ }
101
+ get label() {
102
+ return this._label;
103
+ }
104
+ get timestamp() {
105
+ return this._timestamp;
106
+ }
107
+ get restricted() {
108
+ return this._restricted;
109
+ }
110
+ // ─────────────────────────────
111
+ // Mutators (restricted)
112
+ // ─────────────────────────────
113
+ /**
114
+ * Only mutable field
115
+ */
116
+ set label(label) {
117
+ assertString(label, "label");
118
+ this._label = label;
119
+ }
120
+ // ─────────────────────────────
121
+ // Identity checks
122
+ // ─────────────────────────────
123
+ /**
124
+ * Returns true if identity is restricted
125
+ */
126
+ isRestricted() {
127
+ return this._restricted === true;
128
+ }
129
+ /**
130
+ * Verify identity integrity
131
+ * Detects tampering of id/public_key
132
+ */
133
+ validateIntegrity() {
134
+ const expected = sha256(`${this._userId}:${this._publicKey}:${this._id}:${this._mlKey}`);
135
+ return expected === this._phash;
136
+ }
137
+ /**
138
+ * Explicit verification helper
139
+ */
140
+ matches(userId, publicKey) {
141
+ assertString(userId, "userId");
142
+ assertString(publicKey, "publicKey");
143
+ const hash = sha256(`${userId}:${publicKey}:${this._id}:${this._mlKey}`);
144
+ return hash === this._phash;
145
+ }
146
+ // ─────────────────────────────
147
+ // Serialization
148
+ // ─────────────────────────────
149
+ toJSON() {
150
+ return {
151
+ id: this._id,
152
+ user_id: this._userId,
153
+ public_key: this._publicKey,
154
+ ml_key: this._mlKey,
155
+ phash: this._phash,
156
+ label: this._label,
157
+ timestamp: this._timestamp,
158
+ restricted: this._restricted,
159
+ };
160
+ }
161
+ static fromJSON(json) {
162
+ const obj = typeof json === "string" ? JSON.parse(json) : json;
163
+ assert(typeof obj === "object" && obj !== null, "Invalid JSON object");
164
+ const identity = new MajikMessageIdentity({
165
+ id: obj.id,
166
+ userId: obj.user_id,
167
+ publicKey: obj.public_key,
168
+ mlKey: obj.ml_key,
169
+ phash: obj.phash,
170
+ label: obj.label,
171
+ timestamp: obj.timestamp,
172
+ restricted: obj.restricted,
173
+ });
174
+ assert(identity.validateIntegrity(), "Invalid phash in JSON");
175
+ return identity;
176
+ }
177
+ }
@@ -0,0 +1,86 @@
1
+ export type ISODateString = string;
2
+ export type MajikMessageAccountID = string;
3
+ export type MajikMessagePublicKey = string;
4
+ export type MajikMessageChatID = string;
5
+ export type MajikMessageThreadID = string;
6
+ export type MajikMessageMailID = string;
7
+ export interface MAJIK_API_RESPONSE {
8
+ success: boolean;
9
+ message: string;
10
+ code?: string;
11
+ }
12
+ /**
13
+ * types.ts — @majikah/majik-envelope
14
+ *
15
+ * ML-KEM-768 (v3) envelope types only.
16
+ * v1 (X25519 solo) and v2 (X25519 group) have been removed.
17
+ */
18
+ /**
19
+ * Single-recipient envelope payload.
20
+ * The ML-KEM shared secret is used directly as the AES-256-GCM key.
21
+ */
22
+ export interface SinglePayload {
23
+ iv: string;
24
+ ciphertext: string;
25
+ mlKemCipherText: string;
26
+ }
27
+ /**
28
+ * Per-recipient key entry in a group envelope.
29
+ * encryptedAesKey = groupAesKey XOR mlKemSharedSecret (32-byte XOR one-time-pad).
30
+ */
31
+ export interface GroupKey {
32
+ fingerprint: string;
33
+ mlKemCipherText: string;
34
+ encryptedAesKey: string;
35
+ }
36
+ /**
37
+ * Multi-recipient envelope payload.
38
+ * Message is encrypted once with a random AES key.
39
+ * Each recipient gets their own ML-KEM encapsulation of that AES key.
40
+ */
41
+ export interface GroupPayload {
42
+ iv: string;
43
+ ciphertext: string;
44
+ keys: GroupKey[];
45
+ }
46
+ export type EnvelopePayload = SinglePayload | GroupPayload;
47
+ export declare function isSinglePayload(p: EnvelopePayload): p is SinglePayload;
48
+ export declare function isGroupPayload(p: EnvelopePayload): p is GroupPayload;
49
+ export interface MajikEnvelopeJSON {
50
+ version: 3;
51
+ fingerprint: string;
52
+ payload: EnvelopePayload;
53
+ plaintext?: string;
54
+ }
55
+ export interface MAJIK_API_RESPONSE {
56
+ success: boolean;
57
+ message: string;
58
+ data?: unknown;
59
+ }
60
+ export interface MnemonicJSON {
61
+ id: string;
62
+ seed: string[];
63
+ phrase?: string;
64
+ }
65
+ export interface MajikKeyJSON {
66
+ id: string;
67
+ label: string;
68
+ publicKey: string;
69
+ fingerprint: string;
70
+ encryptedPrivateKey: string;
71
+ salt: string;
72
+ backup: string;
73
+ timestamp: string;
74
+ kdfVersion: number;
75
+ mlKemPublicKey?: string;
76
+ encryptedMlKemSecretKey?: string;
77
+ }
78
+ export interface MajikKeyMetadata {
79
+ id: string;
80
+ fingerprint: string;
81
+ label: string;
82
+ timestamp: Date;
83
+ isLocked: boolean;
84
+ kdfVersion: number;
85
+ hasMlKem: boolean;
86
+ }
@@ -0,0 +1,7 @@
1
+ // ─── Type Guards ──────────────────────────────────────────────────────────────
2
+ export function isSinglePayload(p) {
3
+ return "mlKemCipherText" in p && !("keys" in p);
4
+ }
5
+ export function isGroupPayload(p) {
6
+ return "keys" in p && Array.isArray(p.keys);
7
+ }
@@ -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
+ }