@majikah/majik-key 0.1.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.
- package/LICENSE +67 -0
- package/README.md +911 -0
- package/dist/core/crypto/constants.d.ts +6 -0
- package/dist/core/crypto/constants.js +3 -0
- package/dist/core/crypto/crypto-provider.d.ts +21 -0
- package/dist/core/crypto/crypto-provider.js +74 -0
- package/dist/core/crypto/encryption-engine.d.ts +36 -0
- package/dist/core/crypto/encryption-engine.js +114 -0
- package/dist/core/database/system/identity.d.ts +61 -0
- package/dist/core/database/system/identity.js +171 -0
- package/dist/core/error.d.ts +4 -0
- package/dist/core/error.js +11 -0
- package/dist/core/majik-contact.d.ts +72 -0
- package/dist/core/majik-contact.js +195 -0
- package/dist/core/types.d.ts +19 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utils.d.ts +28 -0
- package/dist/core/utils.js +107 -0
- package/dist/core/validator.d.ts +10 -0
- package/dist/core/validator.js +80 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/majik-key.d.ts +269 -0
- package/dist/majik-key.js +763 -0
- package/package.json +63 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* -------------------------------
|
|
2
|
+
* Types
|
|
3
|
+
* ------------------------------- */
|
|
4
|
+
import { arrayBufferToBase64, base64ToArrayBuffer } from "./utils";
|
|
5
|
+
/* -------------------------------
|
|
6
|
+
* Errors
|
|
7
|
+
* ------------------------------- */
|
|
8
|
+
export class MajikContactError extends Error {
|
|
9
|
+
cause;
|
|
10
|
+
constructor(message, cause) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "MajikContactError";
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/* -------------------------------
|
|
17
|
+
* MajikContact Class
|
|
18
|
+
* ------------------------------- */
|
|
19
|
+
export class MajikContact {
|
|
20
|
+
id;
|
|
21
|
+
publicKey;
|
|
22
|
+
fingerprint;
|
|
23
|
+
meta;
|
|
24
|
+
majikah_registered;
|
|
25
|
+
constructor(data) {
|
|
26
|
+
this.assertId(data.id);
|
|
27
|
+
this.assertPublicKey(data.publicKey);
|
|
28
|
+
this.assertFingerprint(data.fingerprint);
|
|
29
|
+
this.id = data.id;
|
|
30
|
+
this.publicKey = data.publicKey;
|
|
31
|
+
this.fingerprint = data.fingerprint;
|
|
32
|
+
this.meta = {
|
|
33
|
+
label: data.meta?.label || "",
|
|
34
|
+
notes: data.meta?.notes || "",
|
|
35
|
+
blocked: data.meta?.blocked || false,
|
|
36
|
+
createdAt: data.meta?.createdAt || new Date().toISOString(),
|
|
37
|
+
updatedAt: data.meta?.updatedAt || new Date().toISOString(),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
static create(id, publicKey, fingerprint, meta) {
|
|
41
|
+
return new MajikContact({
|
|
42
|
+
id,
|
|
43
|
+
publicKey,
|
|
44
|
+
fingerprint,
|
|
45
|
+
meta,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
assertId(id) {
|
|
49
|
+
if (!id || typeof id !== "string") {
|
|
50
|
+
throw new MajikContactError("Contact ID must be a non-empty string");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
assertPublicKey(key) {
|
|
54
|
+
// Accept either a WebCrypto CryptoKey (with .type === 'public')
|
|
55
|
+
// or a raw-key wrapper object that contains a Uint8Array `raw` field.
|
|
56
|
+
if (!key)
|
|
57
|
+
throw new MajikContactError("Invalid public key");
|
|
58
|
+
const anyKey = key;
|
|
59
|
+
if (anyKey && typeof anyKey === "object") {
|
|
60
|
+
if (anyKey.type === "public")
|
|
61
|
+
return;
|
|
62
|
+
if (anyKey.raw instanceof Uint8Array)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw new MajikContactError("Invalid public key");
|
|
66
|
+
}
|
|
67
|
+
assertFingerprint(fingerprint) {
|
|
68
|
+
if (!fingerprint || typeof fingerprint !== "string") {
|
|
69
|
+
throw new MajikContactError("Fingerprint must be a non-empty string");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
updateTimestamp() {
|
|
73
|
+
this.meta.updatedAt = new Date().toISOString();
|
|
74
|
+
}
|
|
75
|
+
updateLabel(label) {
|
|
76
|
+
if (typeof label !== "string")
|
|
77
|
+
throw new MajikContactError("Label must be a string");
|
|
78
|
+
this.meta.label = label;
|
|
79
|
+
this.updateTimestamp();
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
updateNotes(notes) {
|
|
83
|
+
if (typeof notes !== "string")
|
|
84
|
+
throw new MajikContactError("Notes must be a string");
|
|
85
|
+
this.meta.notes = notes;
|
|
86
|
+
this.updateTimestamp();
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
isBlocked() {
|
|
90
|
+
return this.meta.blocked || false;
|
|
91
|
+
}
|
|
92
|
+
setBlocked(blocked) {
|
|
93
|
+
if (typeof blocked !== "boolean")
|
|
94
|
+
throw new MajikContactError("Blocked must be boolean");
|
|
95
|
+
this.meta.blocked = blocked;
|
|
96
|
+
this.updateTimestamp();
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
// Idempotent block/unblock for safe scanning
|
|
100
|
+
block() {
|
|
101
|
+
if (!this.isBlocked())
|
|
102
|
+
this.setBlocked(true);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
unblock() {
|
|
106
|
+
if (this.isBlocked())
|
|
107
|
+
this.setBlocked(false);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
isMajikahIdentityChecked() {
|
|
111
|
+
return this.majikah_registered !== undefined;
|
|
112
|
+
}
|
|
113
|
+
isMajikahRegistered() {
|
|
114
|
+
return this.majikah_registered || false;
|
|
115
|
+
}
|
|
116
|
+
setMajikahStatus(status) {
|
|
117
|
+
this.majikah_registered = status;
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
async getDisplayName() {
|
|
121
|
+
return this.meta.label || (await this.getPublicKeyBase64());
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
|
|
125
|
+
*/
|
|
126
|
+
async getPublicKeyBase64() {
|
|
127
|
+
try {
|
|
128
|
+
// If it's a CryptoKey, export with SubtleCrypto
|
|
129
|
+
const raw = await crypto.subtle.exportKey("raw", this.publicKey);
|
|
130
|
+
return arrayBufferToBase64(raw);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// Fallback: publicKey may be a wrapper with `raw` Uint8Array
|
|
134
|
+
const maybe = this.publicKey;
|
|
135
|
+
if (maybe && maybe.raw instanceof Uint8Array) {
|
|
136
|
+
return arrayBufferToBase64(maybe.raw.buffer);
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async toJSON() {
|
|
142
|
+
return {
|
|
143
|
+
id: this.id,
|
|
144
|
+
fingerprint: this.fingerprint,
|
|
145
|
+
meta: { ...this.meta },
|
|
146
|
+
publicKeyBase64: await this.getPublicKeyBase64(),
|
|
147
|
+
majikah_registered: this.majikah_registered,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reconstruct a MajikContact from its serialized form
|
|
152
|
+
*/
|
|
153
|
+
static fromJSON(serialized) {
|
|
154
|
+
try {
|
|
155
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(serialized.publicKeyBase64));
|
|
156
|
+
return new MajikContact({
|
|
157
|
+
id: serialized.id,
|
|
158
|
+
fingerprint: serialized.fingerprint,
|
|
159
|
+
meta: serialized.meta,
|
|
160
|
+
publicKey: { raw: publicKeyRaw },
|
|
161
|
+
majikah_registered: serialized.majikah_registered,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw new MajikContactError("Failed to deserialize MajikContact", err);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Create a new MajikContact from a MajikMessageIdentityJSON
|
|
170
|
+
*/
|
|
171
|
+
static async fromIdentityJSON(identityJSON) {
|
|
172
|
+
try {
|
|
173
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(identityJSON.public_key));
|
|
174
|
+
const contactData = {
|
|
175
|
+
id: identityJSON.id,
|
|
176
|
+
publicKey: { raw: publicKeyRaw },
|
|
177
|
+
fingerprint: identityJSON.id,
|
|
178
|
+
meta: {
|
|
179
|
+
label: identityJSON.label,
|
|
180
|
+
createdAt: identityJSON.timestamp,
|
|
181
|
+
updatedAt: identityJSON.timestamp,
|
|
182
|
+
blocked: identityJSON.restricted,
|
|
183
|
+
},
|
|
184
|
+
majikah_registered: true,
|
|
185
|
+
};
|
|
186
|
+
return new MajikContact(contactData);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
throw new MajikContactError("Failed to create MajikContact from MajikMessageIdentityJSON", err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
static isBlocked(contact) {
|
|
193
|
+
return !!contact.meta.blocked;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ISODateString = string;
|
|
2
|
+
export type MajikMessageAccountID = string;
|
|
3
|
+
export type MajikMessagePublicKey = string;
|
|
4
|
+
export type MajikMessageChatID = string;
|
|
5
|
+
export interface MajikKeyJSON {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
publicKey: string;
|
|
9
|
+
fingerprint: string;
|
|
10
|
+
encryptedPrivateKey: string;
|
|
11
|
+
salt: string;
|
|
12
|
+
backup: string;
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
15
|
+
export interface MnemonicJSON {
|
|
16
|
+
seed: string[];
|
|
17
|
+
id: string;
|
|
18
|
+
phrase?: string;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MnemonicJSON } from "./types";
|
|
2
|
+
export declare function keyToBase64(key: CryptoKey | {
|
|
3
|
+
raw: Uint8Array;
|
|
4
|
+
}): Promise<string>;
|
|
5
|
+
export declare function base64ToKey(base64: string): Promise<CryptoKey | {
|
|
6
|
+
raw: Uint8Array;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function arrayToBase64(data: Uint8Array): string;
|
|
9
|
+
export declare function base64ToUint8Array(base64: string): Uint8Array;
|
|
10
|
+
export declare function arrayBufferToBase64(buffer: ArrayBuffer): string;
|
|
11
|
+
export declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
|
|
12
|
+
export declare function base64ToUtf8(base64: string): string;
|
|
13
|
+
export declare function utf8ToBase64(str: string): string;
|
|
14
|
+
export declare function concatArrayBuffers(a: ArrayBuffer, b: ArrayBuffer): ArrayBuffer;
|
|
15
|
+
export declare function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array;
|
|
16
|
+
/**
|
|
17
|
+
* Converts a space-separated seed phrase string into MnemonicJSON
|
|
18
|
+
*/
|
|
19
|
+
export declare function seedToJSON(seed: string, id: string, phrase?: string): MnemonicJSON;
|
|
20
|
+
/**
|
|
21
|
+
* Converts MnemonicJSON into a single space-separated string
|
|
22
|
+
*/
|
|
23
|
+
export declare function jsonToSeed(json: MnemonicJSON): string;
|
|
24
|
+
export declare function seedStringToArray(seed: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Convert an array of words to a mnemonic string
|
|
27
|
+
*/
|
|
28
|
+
export declare function seedArrayToString(seed: string[]): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/* ================================
|
|
2
|
+
* Utilities
|
|
3
|
+
* ================================ */
|
|
4
|
+
import { KEY_ALGO } from "./crypto/constants";
|
|
5
|
+
export async function keyToBase64(key) {
|
|
6
|
+
const anyKey = key;
|
|
7
|
+
if (anyKey && anyKey.raw instanceof Uint8Array) {
|
|
8
|
+
return arrayBufferToBase64(anyKey.raw.buffer);
|
|
9
|
+
}
|
|
10
|
+
const raw = await crypto.subtle.exportKey("raw", key);
|
|
11
|
+
return arrayBufferToBase64(raw);
|
|
12
|
+
}
|
|
13
|
+
export async function base64ToKey(base64) {
|
|
14
|
+
const raw = base64ToArrayBuffer(base64);
|
|
15
|
+
try {
|
|
16
|
+
return await crypto.subtle.importKey("raw", raw, KEY_ALGO, true, []);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
// WebCrypto may not support X25519; return a raw-key wrapper as fallback
|
|
20
|
+
const ua = new Uint8Array(raw);
|
|
21
|
+
const wrapper = { type: "public", raw: ua };
|
|
22
|
+
return wrapper;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// utils/utilities.ts
|
|
26
|
+
export function arrayToBase64(data) {
|
|
27
|
+
let binary = "";
|
|
28
|
+
const bytes = data;
|
|
29
|
+
const len = bytes.byteLength;
|
|
30
|
+
for (let i = 0; i < len; i++) {
|
|
31
|
+
binary += String.fromCharCode(bytes[i]);
|
|
32
|
+
}
|
|
33
|
+
return btoa(binary);
|
|
34
|
+
}
|
|
35
|
+
export function base64ToUint8Array(base64) {
|
|
36
|
+
return new Uint8Array(base64ToArrayBuffer(base64));
|
|
37
|
+
}
|
|
38
|
+
export function arrayBufferToBase64(buffer) {
|
|
39
|
+
const bytes = new Uint8Array(buffer);
|
|
40
|
+
let binary = "";
|
|
41
|
+
const chunkSize = 0x8000;
|
|
42
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
43
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
|
44
|
+
}
|
|
45
|
+
return btoa(binary);
|
|
46
|
+
}
|
|
47
|
+
export function base64ToArrayBuffer(base64) {
|
|
48
|
+
const binary = atob(base64);
|
|
49
|
+
const bytes = new Uint8Array(binary.length);
|
|
50
|
+
for (let i = 0; i < binary.length; i++) {
|
|
51
|
+
bytes[i] = binary.charCodeAt(i);
|
|
52
|
+
}
|
|
53
|
+
return bytes.buffer;
|
|
54
|
+
}
|
|
55
|
+
export function base64ToUtf8(base64) {
|
|
56
|
+
const buf = base64ToArrayBuffer(base64);
|
|
57
|
+
return new TextDecoder().decode(new Uint8Array(buf));
|
|
58
|
+
}
|
|
59
|
+
export function utf8ToBase64(str) {
|
|
60
|
+
const bytes = new TextEncoder().encode(str);
|
|
61
|
+
return arrayBufferToBase64(bytes.buffer);
|
|
62
|
+
}
|
|
63
|
+
export function concatArrayBuffers(a, b) {
|
|
64
|
+
const tmp = new Uint8Array(a.byteLength + b.byteLength);
|
|
65
|
+
tmp.set(new Uint8Array(a), 0);
|
|
66
|
+
tmp.set(new Uint8Array(b), a.byteLength);
|
|
67
|
+
return tmp.buffer;
|
|
68
|
+
}
|
|
69
|
+
export function concatUint8Arrays(a, b) {
|
|
70
|
+
const out = new Uint8Array(a.byteLength + b.byteLength);
|
|
71
|
+
out.set(a, 0);
|
|
72
|
+
out.set(b, a.byteLength);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Converts a space-separated seed phrase string into MnemonicJSON
|
|
77
|
+
*/
|
|
78
|
+
export function seedToJSON(seed, id, phrase) {
|
|
79
|
+
return {
|
|
80
|
+
seed: seed
|
|
81
|
+
.trim()
|
|
82
|
+
.split(/\s+/)
|
|
83
|
+
.map((w) => w.toLowerCase())
|
|
84
|
+
.filter(Boolean),
|
|
85
|
+
id,
|
|
86
|
+
phrase,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Converts MnemonicJSON into a single space-separated string
|
|
91
|
+
*/
|
|
92
|
+
export function jsonToSeed(json) {
|
|
93
|
+
return seedArrayToString(json.seed);
|
|
94
|
+
}
|
|
95
|
+
export function seedStringToArray(seed) {
|
|
96
|
+
return seed
|
|
97
|
+
.trim()
|
|
98
|
+
.split(/\s+/)
|
|
99
|
+
.map((w) => w.toLowerCase())
|
|
100
|
+
.filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Convert an array of words to a mnemonic string
|
|
104
|
+
*/
|
|
105
|
+
export function seedArrayToString(seed) {
|
|
106
|
+
return seed.join(" ");
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MajikKeyJSON } from "./types";
|
|
2
|
+
export declare class MajikKeyValidator {
|
|
3
|
+
static validateMnemonic(mnemonic: string): void;
|
|
4
|
+
static validatePassphrase(passphrase: string, fieldName?: string): void;
|
|
5
|
+
static validateLabel(label: string | undefined): void;
|
|
6
|
+
static validateId(id: string): void;
|
|
7
|
+
static validateJSON(json: unknown): MajikKeyJSON;
|
|
8
|
+
static assert(condition: unknown, message: string): asserts condition;
|
|
9
|
+
static assertString(value: unknown, field: string): asserts value is string;
|
|
10
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { validateMnemonic } from "@scure/bip39";
|
|
2
|
+
import { wordlist } from "@scure/bip39/wordlists/english";
|
|
3
|
+
/* -------------------------------
|
|
4
|
+
* Validators
|
|
5
|
+
* ------------------------------- */
|
|
6
|
+
import { MajikKeyError } from "./error";
|
|
7
|
+
export class MajikKeyValidator {
|
|
8
|
+
static validateMnemonic(mnemonic) {
|
|
9
|
+
if (!mnemonic || typeof mnemonic !== "string") {
|
|
10
|
+
throw new MajikKeyError("Mnemonic must be a non-empty string");
|
|
11
|
+
}
|
|
12
|
+
const trimmed = mnemonic.trim();
|
|
13
|
+
if (!trimmed) {
|
|
14
|
+
throw new MajikKeyError("Mnemonic cannot be empty or whitespace");
|
|
15
|
+
}
|
|
16
|
+
if (!validateMnemonic(trimmed, wordlist)) {
|
|
17
|
+
throw new MajikKeyError("Invalid BIP39 mnemonic phrase");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
static validatePassphrase(passphrase, fieldName = "Passphrase") {
|
|
21
|
+
if (!passphrase || typeof passphrase !== "string") {
|
|
22
|
+
throw new MajikKeyError(`${fieldName} must be a non-empty string`);
|
|
23
|
+
}
|
|
24
|
+
if (!passphrase.trim()) {
|
|
25
|
+
throw new MajikKeyError(`${fieldName} cannot be empty or whitespace`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static validateLabel(label) {
|
|
29
|
+
if (label !== undefined && typeof label !== "string") {
|
|
30
|
+
throw new MajikKeyError("Label must be a string if provided");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
static validateId(id) {
|
|
34
|
+
if (!id || typeof id !== "string") {
|
|
35
|
+
throw new MajikKeyError("ID must be a non-empty string");
|
|
36
|
+
}
|
|
37
|
+
if (!id.trim()) {
|
|
38
|
+
throw new MajikKeyError("ID cannot be empty or whitespace");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
static validateJSON(json) {
|
|
42
|
+
if (!json || typeof json !== "object") {
|
|
43
|
+
throw new MajikKeyError("Invalid JSON: must be an object");
|
|
44
|
+
}
|
|
45
|
+
const obj = json;
|
|
46
|
+
if (!obj.id || typeof obj.id !== "string") {
|
|
47
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'id' field");
|
|
48
|
+
}
|
|
49
|
+
if (!obj.publicKey || typeof obj.publicKey !== "string") {
|
|
50
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'publicKey' field");
|
|
51
|
+
}
|
|
52
|
+
if (!obj.fingerprint || typeof obj.fingerprint !== "string") {
|
|
53
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'fingerprint' field");
|
|
54
|
+
}
|
|
55
|
+
if (!obj.encryptedPrivateKey ||
|
|
56
|
+
typeof obj.encryptedPrivateKey !== "string") {
|
|
57
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'encryptedPrivateKey' field");
|
|
58
|
+
}
|
|
59
|
+
if (!obj.salt || typeof obj.salt !== "string") {
|
|
60
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'salt' field");
|
|
61
|
+
}
|
|
62
|
+
if (!obj.backup || typeof obj.backup !== "string") {
|
|
63
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'backup' field");
|
|
64
|
+
}
|
|
65
|
+
if (!obj.timestamp || typeof obj.timestamp !== "string") {
|
|
66
|
+
throw new MajikKeyError("Invalid JSON: missing or invalid 'timestamp' field");
|
|
67
|
+
}
|
|
68
|
+
if (obj.label !== undefined && typeof obj.label !== "string") {
|
|
69
|
+
throw new MajikKeyError("Invalid JSON: 'label' must be a string if provided");
|
|
70
|
+
}
|
|
71
|
+
return obj;
|
|
72
|
+
}
|
|
73
|
+
static assert(condition, message) {
|
|
74
|
+
if (!condition)
|
|
75
|
+
throw new Error(message);
|
|
76
|
+
}
|
|
77
|
+
static assertString(value, field) {
|
|
78
|
+
this.assert(typeof value === "string" && value.trim().length > 0, `${field} must be a non-empty string`);
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED