@majikah/majik-signature-client 0.3.0 → 0.3.2

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.
@@ -189,7 +189,10 @@ export class ClientStateManager {
189
189
  return DEFAULT_USER_APP_PREFERENCES;
190
190
  }
191
191
  try {
192
- return JSON.parse(raw);
192
+ return {
193
+ ...DEFAULT_USER_APP_PREFERENCES,
194
+ ...JSON.parse(raw),
195
+ };
193
196
  }
194
197
  catch (e) {
195
198
  console.warn("ClientStateManager: Problem retrieving user app preferences: ", e);
@@ -1,18 +1,38 @@
1
- import type { MajikFileIdentity } from "@majikah/majik-file";
1
+ import { type BatchLockResult, type MajikFileIdentity } from "@majikah/majik-file";
2
2
  import { MajikSignatureStamp, MajikSignatureStampJSON, StampAssetKind, CreateStampOptions } from "./majik-signature-stamp";
3
3
  import { StorageQuery } from "../storage/storage-adapter";
4
4
  import { MajikSignatureStampStorageAdapter } from "../storage";
5
+ import { MajikKey } from "@majikah/majik-key";
5
6
  export declare class MajikSignatureStampManagerError extends Error {
6
7
  cause?: unknown;
7
8
  constructor(message: string, cause?: unknown);
8
9
  }
9
10
  export declare class MajikSignatureStampManager {
10
11
  private readonly _cache;
12
+ private _decryptErrors;
11
13
  private _adapter;
12
14
  constructor(adapter?: MajikSignatureStampStorageAdapter);
13
15
  get adapter(): MajikSignatureStampStorageAdapter;
14
16
  setAdapter(adapter: MajikSignatureStampStorageAdapter): void;
15
17
  hydrate(): Promise<void>;
18
+ /**
19
+ * Hydrate every stamp for `fingerprint`. If `key` is given (must be
20
+ * unlocked, matching that fingerprint), each stamp is decrypted so its
21
+ * plaintext is ready for preview. Stamps that fail to decrypt are
22
+ * silently excluded from the cache — never a hard throw. Reasons are
23
+ * kept in getDecryptError() for optional diagnostics.
24
+ *
25
+ * NOTE: this intentionally does NOT call MajikFile.batchDecrypt() —
26
+ * that method gates on MajikFile.canDecrypt(), which currently compares
27
+ * key.fingerprint against a list of public keys and will reject every
28
+ * file. Once that's fixed upstream, this can call batchDecrypt() directly
29
+ * instead of looping decryptHydrate() manually.
30
+ */
31
+ hydrateForFingerprint(fingerprint: string, key?: MajikKey): Promise<void>;
32
+ getDecryptError(id: string): string | undefined;
33
+ /** Wipe decrypted plaintext from every currently cached stamp's MajikFile.
34
+ * MajikFile.batchLock() has no canDecrypt-style gate, safe to use as-is. */
35
+ lockAll(): BatchLockResult;
16
36
  toJSON(): Promise<MajikSignatureStampJSON[]>;
17
37
  bulkRestoreFromJSON(data: MajikSignatureStampJSON[]): Promise<void>;
18
38
  static fromJSON(data: MajikSignatureStampJSON[], adapter?: MajikSignatureStampStorageAdapter): Promise<MajikSignatureStampManager>;
@@ -1,3 +1,4 @@
1
+ import { MajikFile, } from "@majikah/majik-file";
1
2
  import { MajikSignatureStamp, } from "./majik-signature-stamp";
2
3
  import { InMemoryStampstoreAdapter, } from "../storage";
3
4
  export class MajikSignatureStampManagerError extends Error {
@@ -10,6 +11,7 @@ export class MajikSignatureStampManagerError extends Error {
10
11
  }
11
12
  export class MajikSignatureStampManager {
12
13
  _cache = new Map();
14
+ _decryptErrors = new Map();
13
15
  _adapter;
14
16
  constructor(adapter) {
15
17
  this._adapter = adapter ?? new InMemoryStampstoreAdapter();
@@ -34,6 +36,61 @@ export class MajikSignatureStampManager {
34
36
  }
35
37
  }
36
38
  }
39
+ /**
40
+ * Hydrate every stamp for `fingerprint`. If `key` is given (must be
41
+ * unlocked, matching that fingerprint), each stamp is decrypted so its
42
+ * plaintext is ready for preview. Stamps that fail to decrypt are
43
+ * silently excluded from the cache — never a hard throw. Reasons are
44
+ * kept in getDecryptError() for optional diagnostics.
45
+ *
46
+ * NOTE: this intentionally does NOT call MajikFile.batchDecrypt() —
47
+ * that method gates on MajikFile.canDecrypt(), which currently compares
48
+ * key.fingerprint against a list of public keys and will reject every
49
+ * file. Once that's fixed upstream, this can call batchDecrypt() directly
50
+ * instead of looping decryptHydrate() manually.
51
+ */
52
+ async hydrateForFingerprint(fingerprint, key) {
53
+ this._cache.clear();
54
+ this._decryptErrors.clear();
55
+ const rows = this._adapter.query
56
+ ? await this._adapter.query({ where: { fingerprint } })
57
+ : (await this._adapter.list()).filter((r) => r.fingerprint === fingerprint);
58
+ const stamps = [];
59
+ for (const json of rows) {
60
+ try {
61
+ stamps.push(MajikSignatureStamp.fromJSON(json));
62
+ }
63
+ catch (err) {
64
+ console.warn(`hydrateForFingerprint: skipping malformed stamp "${json?.id}":`, err);
65
+ }
66
+ }
67
+ if (!key) {
68
+ for (const s of stamps)
69
+ this._cache.set(s.id, s);
70
+ return;
71
+ }
72
+ const results = await Promise.allSettled(stamps.map((stamp) => stamp.toMajikFile().decryptHydrate(key)));
73
+ results.forEach((result, i) => {
74
+ const stamp = stamps[i];
75
+ if (result.status === "fulfilled") {
76
+ this._cache.set(stamp.id, stamp);
77
+ }
78
+ else {
79
+ this._decryptErrors.set(stamp.id, result.reason instanceof Error
80
+ ? result.reason.message
81
+ : String(result.reason));
82
+ }
83
+ });
84
+ }
85
+ getDecryptError(id) {
86
+ return this._decryptErrors.get(id);
87
+ }
88
+ /** Wipe decrypted plaintext from every currently cached stamp's MajikFile.
89
+ * MajikFile.batchLock() has no canDecrypt-style gate, safe to use as-is. */
90
+ lockAll() {
91
+ const files = this.list().map((s) => s.toMajikFile());
92
+ return MajikFile.batchLock(files);
93
+ }
37
94
  // ── Serialization ─────────────────────────────────────────────────────
38
95
  async toJSON() {
39
96
  return this._adapter.list();
@@ -1,4 +1,5 @@
1
- import type { MajikFileIdentity } from "@majikah/majik-file";
1
+ import { MajikFile } from "@majikah/majik-file";
2
+ import type { MajikFileDecryptIdentity, MajikFileIdentity, MajikFileJSON } from "@majikah/majik-file";
2
3
  export type StampAssetKind = "image" | "audio" | "video" | "text";
3
4
  export interface MajikSignatureStampJSON {
4
5
  id: string;
@@ -10,6 +11,9 @@ export interface MajikSignatureStampJSON {
10
11
  data: string;
11
12
  timestamp: string;
12
13
  lastUpdate: string;
14
+ /** Full MajikFile metadata — required to reconstruct a working MajikFile
15
+ * instance via MajikFile.fromJSON() for decryptHydrate()/secureLock(). */
16
+ fileJson: MajikFileJSON;
13
17
  }
14
18
  export interface CreateStampOptions {
15
19
  data: Uint8Array | ArrayBuffer;
@@ -30,6 +34,8 @@ export declare class MajikSignatureStamp {
30
34
  private readonly _sizeBytes;
31
35
  private readonly _fingerprint;
32
36
  private readonly _timestamp;
37
+ private readonly _fileJson;
38
+ private _file;
33
39
  private _lastUpdate;
34
40
  private _encryptedBinary;
35
41
  private constructor();
@@ -46,7 +52,13 @@ export declare class MajikSignatureStamp {
46
52
  rename(newName: string): void;
47
53
  toJSON(): MajikSignatureStampJSON;
48
54
  static fromJSON(json: MajikSignatureStampJSON): MajikSignatureStamp;
49
- /** Decrypt and return the raw plaintext content bytes (image/audio/video/text). */
50
- decryptContent(identity: Pick<MajikFileIdentity, "fingerprint" | "mlKemSecretKey">): Promise<Uint8Array>;
51
- toBinaryBytes(): Uint8Array;
55
+ /** Reconstruct (or return the cached) MajikFile backing this stamp. */
56
+ toMajikFile(): MajikFile;
57
+ /** Cached decrypted bytes, if hydrateForFingerprint()/decryptHydrate() has
58
+ * already run for this stamp this session. Undefined otherwise, or after
59
+ * a lock. */
60
+ get decryptedBytes(): Uint8Array | undefined;
61
+ /** Accepts a full MajikKey OR a bare {fingerprint, mlKemSecretKey} —
62
+ * matches MajikFile's own MajikFileDecryptIdentity shape. */
63
+ decryptContent(identity: MajikFileDecryptIdentity): Promise<Uint8Array>;
52
64
  }
@@ -9,6 +9,8 @@ export class MajikSignatureStamp {
9
9
  _sizeBytes;
10
10
  _fingerprint;
11
11
  _timestamp;
12
+ _fileJson;
13
+ _file = null; // reconstructed lazily, cached per instance
12
14
  _lastUpdate;
13
15
  _encryptedBinary; // the .mjkb bytes — never the plaintext
14
16
  constructor(json, binary) {
@@ -21,6 +23,7 @@ export class MajikSignatureStamp {
21
23
  this._timestamp = json.timestamp;
22
24
  this._lastUpdate = json.lastUpdate;
23
25
  this._encryptedBinary = binary;
26
+ this._fileJson = json.fileJson;
24
27
  }
25
28
  get id() {
26
29
  return this._id;
@@ -77,8 +80,11 @@ export class MajikSignatureStamp {
77
80
  data: arrayToBase64(binary),
78
81
  timestamp: fileJson.timestamp ?? now,
79
82
  lastUpdate: fileJson.last_update ?? now,
83
+ fileJson,
80
84
  };
81
- return new MajikSignatureStamp(json, binary);
85
+ const instance = new MajikSignatureStamp(json, binary);
86
+ instance._file = file; // reuse — already fully constructed, no need to refetch
87
+ return instance;
82
88
  }
83
89
  /** Plaintext-only rename — no re-encryption, no MajikFile involvement. */
84
90
  rename(newName) {
@@ -87,7 +93,6 @@ export class MajikSignatureStamp {
87
93
  this._name = newName.trim();
88
94
  this._lastUpdate = new Date().toISOString();
89
95
  }
90
- // ── SERIALISATION ───────────────────────────────────────────────────────
91
96
  toJSON() {
92
97
  return {
93
98
  id: this._id,
@@ -99,21 +104,28 @@ export class MajikSignatureStamp {
99
104
  data: arrayToBase64(this._encryptedBinary),
100
105
  timestamp: this._timestamp,
101
106
  lastUpdate: this._lastUpdate,
107
+ fileJson: this._fileJson,
102
108
  };
103
109
  }
104
110
  static fromJSON(json) {
105
111
  return new MajikSignatureStamp(json, base64ToUint8Array(json.data));
106
112
  }
107
- // ── DECRYPT ─────────────────────────────────────────────────────────────
108
- /** Decrypt and return the raw plaintext content bytes (image/audio/video/text). */
109
- async decryptContent(identity) {
110
- if (this._fingerprint !== identity.fingerprint) {
111
- throw new Error(`decryptContent: stamp was encrypted under fingerprint "${this._fingerprint}", ` +
112
- `but the provided identity has fingerprint "${identity.fingerprint}"`);
113
+ /** Reconstruct (or return the cached) MajikFile backing this stamp. */
114
+ toMajikFile() {
115
+ if (!this._file) {
116
+ this._file = MajikFile.fromJSON(this._fileJson, this._encryptedBinary);
113
117
  }
114
- return MajikFile.decrypt(this._encryptedBinary, identity);
118
+ return this._file;
119
+ }
120
+ /** Cached decrypted bytes, if hydrateForFingerprint()/decryptHydrate() has
121
+ * already run for this stamp this session. Undefined otherwise, or after
122
+ * a lock. */
123
+ get decryptedBytes() {
124
+ return this._file?.hasDecryptedFile ? this._file.decryptedFile : undefined;
115
125
  }
116
- toBinaryBytes() {
117
- return this._encryptedBinary;
126
+ /** Accepts a full MajikKey OR a bare {fingerprint, mlKemSecretKey} —
127
+ * matches MajikFile's own MajikFileDecryptIdentity shape. */
128
+ async decryptContent(identity) {
129
+ return MajikFile.decrypt(this._encryptedBinary, identity);
118
130
  }
119
131
  }
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./core/crypto/constants";
7
7
  export * from "./core/crypto/keystore-manager";
8
8
  export * from "./core/storage";
9
9
  export * from "./core/stamp/majik-signature-stamp";
10
+ export { DEFAULT_USER_APP_PREFERENCES } from "./core/client-state-manager";
10
11
  export * from "./core/utils/utilities";
11
12
  export * from "./core/identity";
12
13
  export { migrateMajikMessageJSON } from "./core/contacts/majik-contact-migration";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export * from "./core/crypto/constants";
6
6
  export * from "./core/crypto/keystore-manager";
7
7
  export * from "./core/storage";
8
8
  export * from "./core/stamp/majik-signature-stamp";
9
+ export { DEFAULT_USER_APP_PREFERENCES } from "./core/client-state-manager";
9
10
  export * from "./core/utils/utilities";
10
11
  export * from "./core/identity";
11
12
  export { migrateMajikMessageJSON } from "./core/contacts/majik-contact-migration";
@@ -132,6 +132,8 @@ export declare class MajikSignatureClient {
132
132
  resetUserAppPreferences(): Promise<void>;
133
133
  isAnalyticsEnabled(): Promise<boolean>;
134
134
  isAutoSealEnabled(): Promise<boolean>;
135
+ isAutoLockOnMinimizeEnabled(): Promise<boolean>;
136
+ autoLockInterval(): Promise<number | undefined>;
135
137
  generateMnemonic(strength?: 128 | 256, language?: MnemonicLanguage): Promise<string>;
136
138
  createAccount(mnemonic: string, passphrase: string, label?: string): Promise<{
137
139
  id: string;
@@ -836,6 +838,10 @@ export declare class MajikSignatureClient {
836
838
  accountId?: string;
837
839
  }): Promise<MajikSignatureStamp>;
838
840
  listStamps(kind?: StampAssetKind): MajikSignatureStamp[];
841
+ hydrateStampsForActiveAccount(): Promise<void>;
842
+ lockStamps(): void;
843
+ getDecryptedStampBytes(id: string): Uint8Array | undefined;
844
+ listStampsForActiveAccount(kind?: StampAssetKind): MajikSignatureStamp[];
839
845
  getStamp(id: string): Promise<MajikSignatureStamp | null>;
840
846
  decryptStampContent(id: string, accountId?: string): Promise<Uint8Array>;
841
847
  renameStamp(id: string, newName: string): Promise<MajikSignatureStamp>;
@@ -200,6 +200,14 @@ export class MajikSignatureClient {
200
200
  const appPreferences = await this._state.getUserAppPreferences();
201
201
  return appPreferences.signing.autoSeal ?? false;
202
202
  }
203
+ async isAutoLockOnMinimizeEnabled() {
204
+ const appPreferences = await this._state.getUserAppPreferences();
205
+ return appPreferences.security?.key?.autoLockOnMinimize ?? false;
206
+ }
207
+ async autoLockInterval() {
208
+ const appPreferences = await this._state.getUserAppPreferences();
209
+ return appPreferences.security?.key?.autoLockInterval;
210
+ }
203
211
  // ==========================================================================
204
212
  // ── ACCOUNT MANAGEMENT ────────────────────────────────────────────────────
205
213
  // ==========================================================================
@@ -1312,7 +1320,7 @@ export class MajikSignatureClient {
1312
1320
  */
1313
1321
  async stripSignature(file, options) {
1314
1322
  try {
1315
- return MajikSignature.stripFrom(file, options);
1323
+ return await MajikSignature.stripFrom(file, options);
1316
1324
  }
1317
1325
  catch (err) {
1318
1326
  this._emit("error", err, { context: "stripSignature" });
@@ -1675,6 +1683,31 @@ export class MajikSignatureClient {
1675
1683
  listStamps(kind) {
1676
1684
  return kind ? this._stamps.listByKind(kind) : this._stamps.list();
1677
1685
  }
1686
+ async hydrateStampsForActiveAccount() {
1687
+ const id = this.getActiveAccount()?.id;
1688
+ if (!id)
1689
+ throw new Error("No active account — call setActiveAccount() first");
1690
+ const key = this._keys.get(id);
1691
+ if (!key)
1692
+ throw new Error(`Account not found in keystore: "${id}"`);
1693
+ if (key.isLocked) {
1694
+ throw new Error(`Account "${id}" is locked. Call unlockAccount() before hydrating stamps.`);
1695
+ }
1696
+ await this._stamps.hydrateForFingerprint(key.fingerprint, key); // pass key directly, no identity-builder needed
1697
+ }
1698
+ lockStamps() {
1699
+ this._stamps.lockAll();
1700
+ }
1701
+ getDecryptedStampBytes(id) {
1702
+ return this._stamps.get(id)?.decryptedBytes;
1703
+ }
1704
+ listStampsForActiveAccount(kind) {
1705
+ const key = this.getActiveAccountKey();
1706
+ if (!key)
1707
+ return [];
1708
+ const all = kind ? this._stamps.listByKind(kind) : this._stamps.list();
1709
+ return all.filter((s) => s.fingerprint === key.fingerprint);
1710
+ }
1678
1711
  async getStamp(id) {
1679
1712
  return this._stamps.load(id);
1680
1713
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@majikah/majik-signature-client",
3
3
  "type": "module",
4
4
  "description": "Majik Signature Client is a hybrid post-quantum content signing and verification library for the Majikah ecosystem. Built on top of Majik Key, it provides tamper-proof, forgery-resistant digital signatures for any content format — using a dual-algorithm architecture that combines classical Ed25519 with post-quantum ML-DSA-87 (FIPS-204).",
5
- "version": "0.3.0",
5
+ "version": "0.3.2",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -43,7 +43,7 @@
43
43
  "url": "https://github.com/Majikah/majik-signature-client/issues"
44
44
  },
45
45
  "scripts": {
46
- "update": "npm install @majikah/majik-key@latest @majikah/majik-signature@latest @majikah/majik-contact@latest",
46
+ "update": "npm install @majikah/majik-key@latest @majikah/majik-signature@latest @majikah/majik-contact@latest @majikah/majik-file@latest",
47
47
  "build": "tsc",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "prepublishOnly": "npm run build",
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@majikah/majik-cjson": "^0.0.3",
56
56
  "@majikah/majik-contact": "^0.0.6",
57
- "@majikah/majik-file": "^0.1.7",
57
+ "@majikah/majik-file": "^0.1.8",
58
58
  "@majikah/majik-key": "^0.2.12",
59
59
  "@majikah/majik-signature": "^0.0.30",
60
60
  "@stablelib/ed25519": "^2.1.0",