@majikah/majik-key 0.5.0 → 0.6.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.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * All errors this module throws. Typed so callers can branch on
3
+ * `instanceof` instead of string-matching `.message` — and so every
4
+ * failure is loud and specific rather than a generic Error/undefined.
5
+ */
6
+ export declare class MajikKeyBackupError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ /** An optional peer dependency (jszip, @majikah/majik-bytes) wasn't resolvable at runtime. */
10
+ export declare class MissingOptionalDependencyError extends MajikKeyBackupError {
11
+ constructor(pkg: string, feature: string);
12
+ }
13
+ /** A JSON payload (bare, or decoded from a PNG/zip) failed shape validation. */
14
+ export declare class InvalidBackupJSONError extends MajikKeyBackupError {
15
+ constructor(reason: string);
16
+ }
17
+ /** A PNG file wasn't a valid MajikByte, or its decoded payload wasn't a valid backup. */
18
+ export declare class InvalidBackupPNGError extends MajikKeyBackupError {
19
+ constructor(reason: string);
20
+ }
21
+ /** A .zip archive contained no valid backup.png or backup.json anywhere inside it. */
22
+ export declare class InvalidBackupZipError extends MajikKeyBackupError {
23
+ constructor(reason: string);
24
+ }
25
+ /** A zip contained both a valid PNG backup and a valid JSON backup, but they describe different accounts. */
26
+ export declare class BackupIntegrityMismatchError extends MajikKeyBackupError {
27
+ constructor(details: string);
28
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * All errors this module throws. Typed so callers can branch on
3
+ * `instanceof` instead of string-matching `.message` — and so every
4
+ * failure is loud and specific rather than a generic Error/undefined.
5
+ */
6
+ export class MajikKeyBackupError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = this.constructor.name;
10
+ }
11
+ }
12
+ /** An optional peer dependency (jszip, @majikah/majik-bytes) wasn't resolvable at runtime. */
13
+ export class MissingOptionalDependencyError extends MajikKeyBackupError {
14
+ constructor(pkg, feature) {
15
+ super(`"${pkg}" is required to ${feature}. Install it with \`npm install ${pkg}\`.`);
16
+ }
17
+ }
18
+ /** A JSON payload (bare, or decoded from a PNG/zip) failed shape validation. */
19
+ export class InvalidBackupJSONError extends MajikKeyBackupError {
20
+ constructor(reason) {
21
+ super(`Invalid backup JSON: ${reason}`);
22
+ }
23
+ }
24
+ /** A PNG file wasn't a valid MajikByte, or its decoded payload wasn't a valid backup. */
25
+ export class InvalidBackupPNGError extends MajikKeyBackupError {
26
+ constructor(reason) {
27
+ super(`Invalid backup PNG: ${reason}`);
28
+ }
29
+ }
30
+ /** A .zip archive contained no valid backup.png or backup.json anywhere inside it. */
31
+ export class InvalidBackupZipError extends MajikKeyBackupError {
32
+ constructor(reason) {
33
+ super(`Invalid backup archive: ${reason}`);
34
+ }
35
+ }
36
+ /** A zip contained both a valid PNG backup and a valid JSON backup, but they describe different accounts. */
37
+ export class BackupIntegrityMismatchError extends MajikKeyBackupError {
38
+ constructor(details) {
39
+ super(`Backup archive contains conflicting payloads: ${details}`);
40
+ }
41
+ }
@@ -0,0 +1,4 @@
1
+ export { MajikKeyBackup } from "./majik-key-backup";
2
+ export type { BackupSource, CreateBackupParams, ToZipOptions } from "./types";
3
+ export { BACKUP_FORMAT_VERSION } from "./types";
4
+ export { MajikKeyBackupError, MissingOptionalDependencyError, InvalidBackupJSONError, InvalidBackupPNGError, InvalidBackupZipError, BackupIntegrityMismatchError, } from "./error";
@@ -0,0 +1,3 @@
1
+ export { MajikKeyBackup } from "./majik-key-backup";
2
+ export { BACKUP_FORMAT_VERSION } from "./types";
3
+ export { MajikKeyBackupError, MissingOptionalDependencyError, InvalidBackupJSONError, InvalidBackupPNGError, InvalidBackupZipError, BackupIntegrityMismatchError, } from "./error";
@@ -0,0 +1,49 @@
1
+ import type { MnemonicLanguage } from "../crypto/wordlist";
2
+ import { MnemonicJSON } from "../types";
3
+ import { CreateBackupParams, ToZipOptions } from "./types";
4
+ /**
5
+ * A validated Majik Key backup payload, and the single place that knows
6
+ * how to read/write it as JSON, a MajikByte PNG, or a .zip archive
7
+ * containing both plus a README.
8
+ *
9
+ * Every construction path (create, fromJSON, fromPNG, fromZIP) funnels
10
+ * through the same shape validator, so there is exactly one definition
11
+ * of "valid backup" anywhere in the app.
12
+ */
13
+ export declare class MajikKeyBackup {
14
+ private readonly data;
15
+ private constructor();
16
+ /** Builds a fresh backup from a newly generated seed. Pure — no I/O. */
17
+ static create(params: CreateBackupParams): MajikKeyBackup;
18
+ /** Validates and wraps an already-parsed JSON payload (e.g. a bare .json file the user dropped, no zip/PNG involved). */
19
+ static fromJSON(input: unknown): MajikKeyBackup;
20
+ /** Decodes a MajikByte PNG backup and validates the embedded payload. */
21
+ static fromPNG(file: File | Blob): Promise<MajikKeyBackup>;
22
+ /**
23
+ * Parses a `.zip` backup archive. Walks every file entry — JSZip
24
+ * already flattens nested folders into full relative paths in
25
+ * `archive.files`, so no manual recursion is needed even for a zip
26
+ * re-created one folder level deeper than expected.
27
+ *
28
+ * Classification is by magic bytes, not filename/extension, so a
29
+ * renamed or oddly-cased file is still found.
30
+ *
31
+ * PNG wins when both a valid PNG and a valid JSON backup are present
32
+ * (harder to tamper with — carries the MajikByte integrity check).
33
+ * Falls back to JSON only if no valid PNG is found anywhere in the
34
+ * archive. If both are present but describe different accounts,
35
+ * that's a real integrity problem and is surfaced as one, not
36
+ * silently resolved by picking a winner.
37
+ */
38
+ static fromZIP(file: File | Blob | Uint8Array): Promise<MajikKeyBackup>;
39
+ toJSON(): MnemonicJSON;
40
+ get id(): string;
41
+ get seed(): string[];
42
+ get seedPhrase(): string;
43
+ get language(): MnemonicLanguage | undefined;
44
+ get formatVersion(): number | undefined;
45
+ toPNG(): Promise<Blob>;
46
+ toZIP(_opts?: ToZipOptions): Promise<Blob>;
47
+ /** Convenience for callers building a native `save()` dialog default path. */
48
+ suggestedFileName(label?: string): string;
49
+ }
@@ -0,0 +1,193 @@
1
+ import { validateMnemonicJSONShape } from "./validator";
2
+ import { InvalidBackupPNGError, InvalidBackupZipError, BackupIntegrityMismatchError, } from "./error";
3
+ import { getJSZip, getMajikBytes, looksLikePNG, toSafeFileName, buildReadmeText, } from "./utils";
4
+ import { BACKUP_FORMAT_VERSION, } from "./types";
5
+ import { base64ToUtf8, utf8ToBase64 } from "../utils";
6
+ const BACKUP_JSON_FILENAME = "backup.json";
7
+ const BACKUP_PNG_FILENAME = "backup.png";
8
+ const README_FILENAME = "IMPORTANT README.txt";
9
+ /**
10
+ * A validated Majik Key backup payload, and the single place that knows
11
+ * how to read/write it as JSON, a MajikByte PNG, or a .zip archive
12
+ * containing both plus a README.
13
+ *
14
+ * Every construction path (create, fromJSON, fromPNG, fromZIP) funnels
15
+ * through the same shape validator, so there is exactly one definition
16
+ * of "valid backup" anywhere in the app.
17
+ */
18
+ export class MajikKeyBackup {
19
+ data;
20
+ constructor(data) {
21
+ this.data = data;
22
+ }
23
+ // ────────────────────────────────────────────────────────────────
24
+ // Static constructors
25
+ // ────────────────────────────────────────────────────────────────
26
+ /** Builds a fresh backup from a newly generated seed. Pure — no I/O. */
27
+ static create(params) {
28
+ const seedArray = Array.isArray(params.seed)
29
+ ? params.seed
30
+ : params.seed.trim().split(/\s+/);
31
+ const json = {
32
+ id: params.id,
33
+ seed: seedArray,
34
+ language: params.language,
35
+ phrase: params.phrase,
36
+ version: BACKUP_FORMAT_VERSION,
37
+ };
38
+ validateMnemonicJSONShape(json);
39
+ return new MajikKeyBackup(json);
40
+ }
41
+ /** Validates and wraps an already-parsed JSON payload (e.g. a bare .json file the user dropped, no zip/PNG involved). */
42
+ static fromJSON(input) {
43
+ validateMnemonicJSONShape(input);
44
+ return new MajikKeyBackup(input);
45
+ }
46
+ /** Decodes a MajikByte PNG backup and validates the embedded payload. */
47
+ static async fromPNG(file) {
48
+ const { MajikBytes } = await getMajikBytes();
49
+ const check = await MajikBytes.isValidPNG(file);
50
+ if (!check?.isValid) {
51
+ throw new InvalidBackupPNGError("not a recognized MajikByte PNG");
52
+ }
53
+ let decoded;
54
+ try {
55
+ const mbyte = await MajikBytes.fromPNG(file);
56
+ const base64 = mbyte.toStringValue();
57
+ decoded = JSON.parse(base64ToUtf8(base64));
58
+ }
59
+ catch (err) {
60
+ throw new InvalidBackupPNGError(`could not decode embedded payload (${err?.message ?? err})`);
61
+ }
62
+ validateMnemonicJSONShape(decoded);
63
+ return new MajikKeyBackup(decoded);
64
+ }
65
+ /**
66
+ * Parses a `.zip` backup archive. Walks every file entry — JSZip
67
+ * already flattens nested folders into full relative paths in
68
+ * `archive.files`, so no manual recursion is needed even for a zip
69
+ * re-created one folder level deeper than expected.
70
+ *
71
+ * Classification is by magic bytes, not filename/extension, so a
72
+ * renamed or oddly-cased file is still found.
73
+ *
74
+ * PNG wins when both a valid PNG and a valid JSON backup are present
75
+ * (harder to tamper with — carries the MajikByte integrity check).
76
+ * Falls back to JSON only if no valid PNG is found anywhere in the
77
+ * archive. If both are present but describe different accounts,
78
+ * that's a real integrity problem and is surfaced as one, not
79
+ * silently resolved by picking a winner.
80
+ */
81
+ static async fromZIP(file) {
82
+ const JSZip = await getJSZip();
83
+ let archive;
84
+ try {
85
+ archive = await JSZip.loadAsync(file);
86
+ }
87
+ catch (err) {
88
+ throw new InvalidBackupZipError(`could not open archive (${err?.message ?? err})`);
89
+ }
90
+ let pngResult = null;
91
+ let pngError = null;
92
+ let jsonResult = null;
93
+ for (const relativePath of Object.keys(archive.files)) {
94
+ const entry = archive.files[relativePath];
95
+ if (entry.dir)
96
+ continue;
97
+ const bytes = await entry.async("uint8array");
98
+ if (!pngResult && looksLikePNG(bytes)) {
99
+ try {
100
+ const blob = new Blob([bytes], { type: "image/png" });
101
+ pngResult = await MajikKeyBackup.fromPNG(blob);
102
+ }
103
+ catch (err) {
104
+ // Keep the first PNG-shaped-but-invalid error for the final
105
+ // message, but keep scanning — a later entry might still be
106
+ // a valid JSON backup.
107
+ pngError = pngError ?? err;
108
+ }
109
+ continue;
110
+ }
111
+ if (!jsonResult) {
112
+ try {
113
+ const text = new TextDecoder("utf-8").decode(bytes);
114
+ const parsed = JSON.parse(text);
115
+ jsonResult = MajikKeyBackup.fromJSON(parsed);
116
+ }
117
+ catch {
118
+ // Not every non-PNG entry is the backup JSON (e.g. the
119
+ // README) — skip silently, we only error if *nothing* valid
120
+ // turns up anywhere in the archive.
121
+ }
122
+ }
123
+ }
124
+ if (pngResult && jsonResult) {
125
+ const idsMatch = pngResult.data.id === jsonResult.data.id;
126
+ const seedsMatch = pngResult.data.seed.join(" ") === jsonResult.data.seed.join(" ");
127
+ if (!idsMatch || !seedsMatch) {
128
+ throw new BackupIntegrityMismatchError("the PNG and JSON backups inside this archive do not describe the same account");
129
+ }
130
+ }
131
+ if (pngResult)
132
+ return pngResult;
133
+ if (jsonResult)
134
+ return jsonResult;
135
+ const reason = pngError
136
+ ? `a PNG-shaped file was found but is invalid (${pngError?.message ?? pngError})`
137
+ : "no valid backup.png or backup.json found inside the archive";
138
+ throw new InvalidBackupZipError(reason);
139
+ }
140
+ // ────────────────────────────────────────────────────────────────
141
+ // Instance accessors
142
+ // ────────────────────────────────────────────────────────────────
143
+ toJSON() {
144
+ return { ...this.data };
145
+ }
146
+ get id() {
147
+ return this.data.id;
148
+ }
149
+ get seed() {
150
+ return [...this.data.seed];
151
+ }
152
+ get seedPhrase() {
153
+ return this.data.seed.join(" ");
154
+ }
155
+ get language() {
156
+ return this.data.language;
157
+ }
158
+ get formatVersion() {
159
+ return this.data.version;
160
+ }
161
+ // ────────────────────────────────────────────────────────────────
162
+ // Instance serializers
163
+ // ────────────────────────────────────────────────────────────────
164
+ async toPNG() {
165
+ const { MajikBytes } = await getMajikBytes();
166
+ const base64 = utf8ToBase64(JSON.stringify(this.toJSON()));
167
+ const mbyte = await MajikBytes.create(base64);
168
+ return mbyte.toPNG();
169
+ }
170
+ async toZIP(_opts = {}) {
171
+ const JSZip = await getJSZip();
172
+ const json = this.toJSON();
173
+ const pngBlob = await this.toPNG();
174
+ const pngBuffer = await pngBlob.arrayBuffer();
175
+ const zip = new JSZip();
176
+ zip.file(BACKUP_JSON_FILENAME, JSON.stringify(json));
177
+ zip.file(BACKUP_PNG_FILENAME, pngBuffer, { binary: true });
178
+ zip.file(README_FILENAME, buildReadmeText(new Date()));
179
+ return zip.generateAsync({
180
+ type: "blob",
181
+ compression: "DEFLATE",
182
+ compressionOptions: { level: 9 },
183
+ });
184
+ }
185
+ /** Convenience for callers building a native `save()` dialog default path. */
186
+ suggestedFileName(label) {
187
+ return toSafeFileName(`${label ?? "Majik Key"} - ${this.data.id} - SEED KEY - ${new Date().toISOString()}`);
188
+ }
189
+ }
190
+ // Freeze static methods (e.g., MajikKey.create, MajikKey.fromJSON)
191
+ Object.freeze(MajikKeyBackup);
192
+ // Freeze instance methods (e.g., this.lock, this.unlock)
193
+ Object.freeze(MajikKeyBackup.prototype);
@@ -0,0 +1,22 @@
1
+ import type { MnemonicLanguage } from "../crypto/wordlist";
2
+ /**
3
+ * Bump this whenever the *shape* of MnemonicJSON or the zip layout
4
+ * changes in a way that could break parsing of older backups. Written
5
+ * into every backup created by `MajikKeyBackup.create()`; read back
6
+ * (but not currently enforced) by `fromJSON`/`fromPNG`/`fromZIP`, so
7
+ * future versions can branch on it if the shape ever diverges.
8
+ */
9
+ export declare const BACKUP_FORMAT_VERSION = 1;
10
+ /** Which artifact inside a parsed backup archive supplied the winning payload. */
11
+ export type BackupSource = "png" | "json";
12
+ export interface CreateBackupParams {
13
+ /** Either the full mnemonic string ("word1 word2 ...") or a pre-split word array. */
14
+ seed: string | string[];
15
+ id: string;
16
+ language: MnemonicLanguage;
17
+ phrase?: string;
18
+ }
19
+ export interface ToZipOptions {
20
+ /** Used only to build the human-facing filename hint (suggestedFileName); purely cosmetic. */
21
+ label?: string;
22
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bump this whenever the *shape* of MnemonicJSON or the zip layout
3
+ * changes in a way that could break parsing of older backups. Written
4
+ * into every backup created by `MajikKeyBackup.create()`; read back
5
+ * (but not currently enforced) by `fromJSON`/`fromPNG`/`fromZIP`, so
6
+ * future versions can branch on it if the shape ever diverges.
7
+ */
8
+ export const BACKUP_FORMAT_VERSION = 1;
@@ -0,0 +1,14 @@
1
+ export declare function getJSZip(): Promise<any>;
2
+ export declare function getMajikBytes(): Promise<any>;
3
+ export declare function looksLikePNG(bytes: Uint8Array): boolean;
4
+ /** Zip local-file-header signature ("PK\x03\x04"), also covers empty-archive/spanned variants starting "PK". */
5
+ export declare function looksLikeZip(bytes: Uint8Array): boolean;
6
+ export declare function toSafeFileName(value: string): string;
7
+ /**
8
+ * Static body of the backup README. Update this whenever the copy
9
+ * needs to change — every zip generated by toZIP() picks it up
10
+ * automatically, nothing else needs to change.
11
+ */
12
+ export declare const README_TEXT = "Majik Key Backup\n\nIMPORTANT: Keep this file secure and private at all times. If lost or compromised, your account access may be permanently at risk.\n\nOverview\nThis backup ZIP file contains your raw JSON data and a Backup PNG. These files are essential for recovering your account.\n\nUsage Instructions\n- Storage: You may delete the JSON file and keep only the PNG file if preferred.\n- Customization: You can rename the PNG file for added discretion.\n- Recovery: This PNG allows you to securely re-import your account without exposing raw JSON data.\n\nCritical Handling Requirements\nTo prevent data corruption and ensure the backup remains functional, please follow these rules:\n\n- No Modifications: Do not edit, crop, or apply filters to the PNG image.\n- No Processing: Avoid running the image through compression tools or \"optimization\" software.\n- Storage Only: Store the image as is. Do not upload it to social media, messaging apps, or cloud platforms that automatically compress or manipulate images, as this will destroy the embedded data.\n\nIMPORTANT: Keep this file secure and private at all times. If lost or compromised, your account access may be permanently at risk.";
13
+ /** README_TEXT plus a creation timestamp line — the only per-backup variable part. */
14
+ export declare function buildReadmeText(createdAt?: Date): string;
@@ -0,0 +1,92 @@
1
+ import { MissingOptionalDependencyError } from "./error";
2
+ // ────────────────────────────────────────────────────────────────
3
+ // Lazy-loaded optional dependencies
4
+ // ────────────────────────────────────────────────────────────────
5
+ // jszip is only needed by toZIP()/fromZIP() — every other method on
6
+ // MajikKeyBackup (create, fromJSON, fromPNG, toJSON) never touches it,
7
+ // so no consumer pays for it unless they actually use the zip feature.
8
+ // Note: in bundled contexts (Vite/webpack/etc.) `import("jszip")` still
9
+ // needs jszip resolvable at build time — this buys code-splitting
10
+ // (its own lazy-fetched chunk), not "works with zero jszip installed".
11
+ // In plain-Node consumers (CLI, MCP server) it genuinely defers
12
+ // resolution to runtime, and throws the typed error below if absent.
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ let _jszip = null;
15
+ export async function getJSZip() {
16
+ if (_jszip)
17
+ return _jszip;
18
+ try {
19
+ const mod = await import("jszip");
20
+ _jszip = mod.default ?? mod;
21
+ }
22
+ catch {
23
+ throw new MissingOptionalDependencyError("jszip", "read or write .zip backups");
24
+ }
25
+ return _jszip;
26
+ }
27
+ // @majikah/majik-bytes is a hard dependency of this library (Josef
28
+ // controls it), so this loader exists purely to keep the import
29
+ // pattern consistent with getJSZip() — not for optionality.
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ let _majikBytes = null;
32
+ export async function getMajikBytes() {
33
+ if (_majikBytes)
34
+ return _majikBytes;
35
+ _majikBytes = await import("@majikah/majik-bytes");
36
+ return _majikBytes;
37
+ }
38
+ // ────────────────────────────────────────────────────────────────
39
+ // Binary sniffing — classify by magic bytes, never by file extension
40
+ // ────────────────────────────────────────────────────────────────
41
+ const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
42
+ export function looksLikePNG(bytes) {
43
+ if (bytes.length < PNG_MAGIC.length)
44
+ return false;
45
+ return PNG_MAGIC.every((byte, i) => bytes[i] === byte);
46
+ }
47
+ /** Zip local-file-header signature ("PK\x03\x04"), also covers empty-archive/spanned variants starting "PK". */
48
+ export function looksLikeZip(bytes) {
49
+ return bytes.length > 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
50
+ }
51
+ // ────────────────────────────────────────────────────────────────
52
+ // Encoding helpers
53
+ // ────────────────────────────────────────────────────────────────
54
+ export function toSafeFileName(value) {
55
+ return value
56
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
57
+ .replace(/\s+/g, " ")
58
+ .trim()
59
+ .replace(/[. ]+$/, "");
60
+ }
61
+ // ────────────────────────────────────────────────────────────────
62
+ // README — centralized so copy updates happen in exactly one place
63
+ // ────────────────────────────────────────────────────────────────
64
+ /**
65
+ * Static body of the backup README. Update this whenever the copy
66
+ * needs to change — every zip generated by toZIP() picks it up
67
+ * automatically, nothing else needs to change.
68
+ */
69
+ export const README_TEXT = `Majik Key Backup
70
+
71
+ IMPORTANT: Keep this file secure and private at all times. If lost or compromised, your account access may be permanently at risk.
72
+
73
+ Overview
74
+ This backup ZIP file contains your raw JSON data and a Backup PNG. These files are essential for recovering your account.
75
+
76
+ Usage Instructions
77
+ - Storage: You may delete the JSON file and keep only the PNG file if preferred.
78
+ - Customization: You can rename the PNG file for added discretion.
79
+ - Recovery: This PNG allows you to securely re-import your account without exposing raw JSON data.
80
+
81
+ Critical Handling Requirements
82
+ To prevent data corruption and ensure the backup remains functional, please follow these rules:
83
+
84
+ - No Modifications: Do not edit, crop, or apply filters to the PNG image.
85
+ - No Processing: Avoid running the image through compression tools or "optimization" software.
86
+ - Storage Only: Store the image as is. Do not upload it to social media, messaging apps, or cloud platforms that automatically compress or manipulate images, as this will destroy the embedded data.
87
+
88
+ IMPORTANT: Keep this file secure and private at all times. If lost or compromised, your account access may be permanently at risk.`;
89
+ /** README_TEXT plus a creation timestamp line — the only per-backup variable part. */
90
+ export function buildReadmeText(createdAt = new Date()) {
91
+ return `${README_TEXT}\n\nBackup created on: ${createdAt.toLocaleString()}\n`;
92
+ }
@@ -0,0 +1,11 @@
1
+ import { MnemonicJSON } from "../types";
2
+ /**
3
+ * Single source of truth for "is this a structurally valid
4
+ * MnemonicJSON". Called by `create`, `fromJSON`, `fromPNG` (post-decode),
5
+ * and `fromZIP` — never reimplemented at any entry point (DRY validation:
6
+ * one validator, many callers).
7
+ *
8
+ * Throws InvalidBackupJSONError with a specific reason on failure;
9
+ * narrows `input` to MnemonicJSON on success.
10
+ */
11
+ export declare function validateMnemonicJSONShape(input: unknown): asserts input is MnemonicJSON;
@@ -0,0 +1,36 @@
1
+ import { InvalidBackupJSONError } from "./error";
2
+ /**
3
+ * Single source of truth for "is this a structurally valid
4
+ * MnemonicJSON". Called by `create`, `fromJSON`, `fromPNG` (post-decode),
5
+ * and `fromZIP` — never reimplemented at any entry point (DRY validation:
6
+ * one validator, many callers).
7
+ *
8
+ * Throws InvalidBackupJSONError with a specific reason on failure;
9
+ * narrows `input` to MnemonicJSON on success.
10
+ */
11
+ export function validateMnemonicJSONShape(input) {
12
+ if (typeof input !== "object" || input === null) {
13
+ throw new InvalidBackupJSONError("payload is not an object");
14
+ }
15
+ const candidate = input;
16
+ if (typeof candidate.id !== "string" || !candidate.id.trim()) {
17
+ throw new InvalidBackupJSONError("missing or empty `id`");
18
+ }
19
+ if (!Array.isArray(candidate.seed) || candidate.seed.length === 0) {
20
+ throw new InvalidBackupJSONError("missing or empty `seed`");
21
+ }
22
+ if (!candidate.seed.every((word) => typeof word === "string" && word.trim().length > 0)) {
23
+ throw new InvalidBackupJSONError("`seed` must be an array of non-empty words");
24
+ }
25
+ if (candidate.phrase !== undefined && typeof candidate.phrase !== "string") {
26
+ throw new InvalidBackupJSONError("`phrase` must be a string when present");
27
+ }
28
+ if (candidate.language !== undefined &&
29
+ typeof candidate.language !== "string") {
30
+ throw new InvalidBackupJSONError("`language` must be a string when present");
31
+ }
32
+ if (candidate.version !== undefined &&
33
+ typeof candidate.version !== "number") {
34
+ throw new InvalidBackupJSONError("`version` must be a number when present");
35
+ }
36
+ }
@@ -108,7 +108,7 @@ async function _argon2id(input, salt, params) {
108
108
  if (_wasmAvailable === null) {
109
109
  _wasmAvailable = await _probeWasm();
110
110
  if (!_wasmAvailable) {
111
- console.warn("[majikah/crypto] hash-wasm unavailable, using @noble/hashes argon2id fallback");
111
+ console.warn("[@majikah/majik-key/crypto] hash-wasm unavailable, using @noble/hashes argon2id fallback");
112
112
  }
113
113
  }
114
114
  if (_wasmAvailable) {
@@ -119,7 +119,7 @@ async function _argon2id(input, salt, params) {
119
119
  // WASM loaded but failed at runtime (e.g. OOM, corrupted module)
120
120
  // Flip flag so we stop trying for the rest of this session
121
121
  _wasmAvailable = false;
122
- console.warn("[majikah/majik-key/crypto] hash-wasm runtime failure, falling back to @noble/hashes", err);
122
+ console.warn("[@majikah/majik-key/crypto] hash-wasm runtime failure, falling back to @noble/hashes", err);
123
123
  }
124
124
  }
125
125
  return _argon2idNoble(input, salt, params);
@@ -141,4 +141,6 @@ export interface MnemonicJSON {
141
141
  /** Optional passphrase, carried in plaintext for convenience during export/import. ⚠️ Not encrypted. */
142
142
  phrase?: string;
143
143
  language?: MnemonicLanguage;
144
+ /** Backup format version this payload was written with. See BACKUP_FORMAT_VERSION. */
145
+ version?: number;
144
146
  }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export type * from "./core/types";
3
3
  export * from "./core/error";
4
4
  export * from "./core/validator";
5
5
  export * from "./core/web3";
6
+ export * from "./core/backup";
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export * from "./majik-key";
2
2
  export * from "./core/error";
3
3
  export * from "./core/validator";
4
4
  export * from "./core/web3";
5
+ export * from "./core/backup";
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-key",
3
3
  "type": "module",
4
4
  "description": "A post-quantum ready seed phrase account library for the Majikah ecosystem. Manages deterministic X25519 and ML-KEM-768 identities with Argon2id protection and seamless legacy account migration.",
5
- "version": "0.5.0",
5
+ "version": "0.6.0",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -48,11 +48,12 @@
48
48
  "package": "npm run build && npm version patch && git push && git push --tags",
49
49
  "test": "vitest run",
50
50
  "test:watch": "vitest",
51
- "test:compat": "npx vitest test/majik-key-cross-compat.test.ts",
52
- "test:web3:bitcoin": "npx vitest test/majik-key-bitcoin.test.ts",
53
- "test:web3:solana": "npx vitest test/majik-key-solana.test.ts",
54
- "test:web3": "npx vitest test/majik-key-bitcoin.test.ts test/majik-key-solana.test.ts",
55
- "test:core": "npx vitest test/majik-key.test.ts"
51
+ "test:compat": "vitest run test/majik-key-cross-compat.test.ts",
52
+ "test:web3:bitcoin": "vitest run test/majik-key-bitcoin.test.ts",
53
+ "test:web3:solana": "vitest run test/majik-key-solana.test.ts",
54
+ "test:web3": "vitest run test/majik-key-bitcoin.test.ts test/majik-key-solana.test.ts",
55
+ "test:core": "vitest run test/majik-key.test.ts",
56
+ "test:backup": "vitest run test/backup/majik-key-backup.test.ts"
56
57
  },
57
58
  "dependencies": {
58
59
  "@majikah/majik-contact": "^0.0.6",
@@ -67,21 +68,25 @@
67
68
  "@stablelib/pbkdf2": "^2.0.1",
68
69
  "@stablelib/sha256": "^2.0.1",
69
70
  "@stablelib/x25519": "^2.0.1",
70
- "@thezelijah/majik-user": "^1.0.9",
71
+ "@thezelijah/majik-user": "^1.0.10",
71
72
  "ed2curve": "^0.3.0",
72
73
  "hash-wasm": "^4.12.0"
73
74
  },
74
75
  "devDependencies": {
76
+ "@majikah/majik-bytes": "^1.0.0",
75
77
  "@scure/btc-signer": "^2.4.1",
76
78
  "@solana/kit": "^8.3.0",
77
79
  "@types/ed2curve": "^0.2.4",
78
80
  "@types/node": "^26.6.2",
81
+ "jszip": "^3.10.2",
79
82
  "typescript": "^7.0.2",
80
83
  "vitest": "^5.0.1"
81
84
  },
82
85
  "peerDependencies": {
83
- "@scure/btc-signer": "^2.4.1",
84
- "@solana/kit": "^8.3.0"
86
+ "@majikah/majik-bytes": ">=1.0.0",
87
+ "jszip": ">=3.10.2",
88
+ "@scure/btc-signer": ">=2.4.1",
89
+ "@solana/kit": ">=8.3.0"
85
90
  },
86
91
  "peerDependenciesMeta": {
87
92
  "@solana/kit": {
@@ -89,6 +94,12 @@
89
94
  },
90
95
  "@scure/btc-signer": {
91
96
  "optional": true
97
+ },
98
+ "jszip": {
99
+ "optional": true
100
+ },
101
+ "@majikah/majik-bytes": {
102
+ "optional": true
92
103
  }
93
104
  }
94
105
  }