@majikah/majik-signature-client 0.2.0 → 0.3.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.
@@ -6,14 +6,14 @@
6
6
  * MJKC = MajiK Contacts
7
7
  * MJKA = MajiK App-data
8
8
  */
9
- export declare const MAJIK_MESSAGE_BACKUP_MAGIC: {
10
- readonly chats: Uint8Array;
9
+ export declare const MAJIK_SIGNATURE_BACKUP_MAGIC: {
10
+ readonly stamps: Uint8Array;
11
11
  readonly contacts: Uint8Array;
12
12
  readonly appData: Uint8Array;
13
13
  };
14
14
  /** The union of valid magic-byte headers. */
15
- export type MajikMessageBackupMagic = (typeof MAJIK_MESSAGE_BACKUP_MAGIC)[keyof typeof MAJIK_MESSAGE_BACKUP_MAGIC];
15
+ export type MajikMessageBackupMagic = (typeof MAJIK_SIGNATURE_BACKUP_MAGIC)[keyof typeof MAJIK_SIGNATURE_BACKUP_MAGIC];
16
16
  /** Which backup type a file contains, as a discriminant string. */
17
- export type MajikMessageBackupType = keyof typeof MAJIK_MESSAGE_BACKUP_MAGIC;
17
+ export type MajikMessageBackupType = keyof typeof MAJIK_SIGNATURE_BACKUP_MAGIC;
18
18
  /** Byte length of every backup file header (4-byte tag + 2-byte version). */
19
19
  export declare const MAJIK_MESSAGE_BACKUP_MAGIC_SIZE = 6;
@@ -6,8 +6,15 @@
6
6
  * MJKC = MajiK Contacts
7
7
  * MJKA = MajiK App-data
8
8
  */
9
- export const MAJIK_MESSAGE_BACKUP_MAGIC = {
10
- chats: new Uint8Array([0x4d, 0x4a, 0x4b, 0x49, 0x00, 0x01]), // "MJKI" v1
9
+ export const MAJIK_SIGNATURE_BACKUP_MAGIC = {
10
+ stamps: new Uint8Array([
11
+ 0x53,
12
+ 0x54,
13
+ 0x4d,
14
+ 0x50, // "STMP"
15
+ 0x00,
16
+ 0x01, // Version 1
17
+ ]),
11
18
  contacts: new Uint8Array([0x4d, 0x4a, 0x4b, 0x43, 0x00, 0x01]), // "MJKC" v1
12
19
  appData: new Uint8Array([0x4d, 0x4a, 0x4b, 0x41, 0x00, 0x01]), // "MJKA" v1
13
20
  };
@@ -1,6 +1,7 @@
1
1
  import { MajikContact, MajikContactGroup } from "@majikah/majik-contact";
2
2
  import { MajikContactManagerJSON } from "../contacts/types";
3
3
  import { UserAppPreferences } from "../storage";
4
+ import { MajikSignatureStampJSON } from "../stamp/majik-signature-stamp";
4
5
  export interface ContactManagerSnapshot {
5
6
  /** Raw JSON payload — used internally by restoreContacts for bulk writes */
6
7
  managerJSON: MajikContactManagerJSON;
@@ -12,7 +13,7 @@ export interface ContactManagerSnapshot {
12
13
  export interface AppDataSnapshot {
13
14
  contacts: MajikContact[];
14
15
  groups: MajikContactGroup[];
16
+ stamps: MajikSignatureStampJSON[];
15
17
  preferences: UserAppPreferences | null;
16
- /** @internal Raw manager JSON — used by restoreAppDataSelective, not for display */
17
18
  _contactsManagerJSON: MajikContactManagerJSON;
18
19
  }
@@ -226,4 +226,9 @@ export const DEFAULT_USER_APP_PREFERENCES = {
226
226
  privacy: {
227
227
  shareAnalytics: true,
228
228
  },
229
+ security: {
230
+ key: {
231
+ autoLockOnMinimize: false,
232
+ },
233
+ },
229
234
  };
@@ -0,0 +1,37 @@
1
+ import type { MajikFileIdentity } from "@majikah/majik-file";
2
+ import { MajikSignatureStamp, MajikSignatureStampJSON, StampAssetKind, CreateStampOptions } from "./majik-signature-stamp";
3
+ import { StorageQuery } from "../storage/storage-adapter";
4
+ import { MajikSignatureStampStorageAdapter } from "../storage";
5
+ export declare class MajikSignatureStampManagerError extends Error {
6
+ cause?: unknown;
7
+ constructor(message: string, cause?: unknown);
8
+ }
9
+ export declare class MajikSignatureStampManager {
10
+ private readonly _cache;
11
+ private _adapter;
12
+ constructor(adapter?: MajikSignatureStampStorageAdapter);
13
+ get adapter(): MajikSignatureStampStorageAdapter;
14
+ setAdapter(adapter: MajikSignatureStampStorageAdapter): void;
15
+ hydrate(): Promise<void>;
16
+ toJSON(): Promise<MajikSignatureStampJSON[]>;
17
+ bulkRestoreFromJSON(data: MajikSignatureStampJSON[]): Promise<void>;
18
+ static fromJSON(data: MajikSignatureStampJSON[], adapter?: MajikSignatureStampStorageAdapter): Promise<MajikSignatureStampManager>;
19
+ private _persist;
20
+ create(options: CreateStampOptions): Promise<MajikSignatureStamp>;
21
+ save(stamp: MajikSignatureStamp): Promise<void>;
22
+ load(id: string): Promise<MajikSignatureStamp | null>;
23
+ getStampOrThrow(id: string): Promise<MajikSignatureStamp>;
24
+ loadAll(): Promise<MajikSignatureStamp[]>;
25
+ delete(id: string): Promise<void>;
26
+ has(id: string): Promise<boolean>;
27
+ get(id: string): MajikSignatureStamp | undefined;
28
+ list(): MajikSignatureStamp[];
29
+ listByKind(kind: StampAssetKind): MajikSignatureStamp[];
30
+ listByFingerprint(fingerprint: string): MajikSignatureStamp[];
31
+ query(query: StorageQuery<MajikSignatureStampJSON>): Promise<MajikSignatureStampJSON[]>;
32
+ rename(id: string, newName: string): Promise<MajikSignatureStamp>;
33
+ /** Write-once at the MajikFile layer — id is preserved explicitly. */
34
+ replaceContent(id: string, rawBytes: Uint8Array, identity: MajikFileIdentity, mimeType?: string): Promise<MajikSignatureStamp>;
35
+ decryptContent(id: string, identity: Pick<MajikFileIdentity, "fingerprint" | "mlKemSecretKey">): Promise<Uint8Array>;
36
+ bulkRemove(ids: string[]): Promise<void>;
37
+ }
@@ -0,0 +1,159 @@
1
+ import { MajikSignatureStamp, } from "./majik-signature-stamp";
2
+ import { InMemoryStampstoreAdapter, } from "../storage";
3
+ export class MajikSignatureStampManagerError extends Error {
4
+ cause;
5
+ constructor(message, cause) {
6
+ super(message);
7
+ this.name = "MajikSignatureStampManagerError";
8
+ this.cause = cause;
9
+ }
10
+ }
11
+ export class MajikSignatureStampManager {
12
+ _cache = new Map();
13
+ _adapter;
14
+ constructor(adapter) {
15
+ this._adapter = adapter ?? new InMemoryStampstoreAdapter();
16
+ }
17
+ get adapter() {
18
+ return this._adapter;
19
+ }
20
+ setAdapter(adapter) {
21
+ this._adapter = adapter;
22
+ }
23
+ // ── Hydration ─────────────────────────────────────────────────────────
24
+ async hydrate() {
25
+ this._cache.clear();
26
+ const all = await this._adapter.list();
27
+ for (const json of all) {
28
+ try {
29
+ const stamp = MajikSignatureStamp.fromJSON(json);
30
+ this._cache.set(stamp.id, stamp);
31
+ }
32
+ catch (err) {
33
+ console.warn(`MajikSignatureStampManager.hydrate: skipping malformed stamp "${json?.id}":`, err);
34
+ }
35
+ }
36
+ }
37
+ // ── Serialization ─────────────────────────────────────────────────────
38
+ async toJSON() {
39
+ return this._adapter.list();
40
+ }
41
+ async bulkRestoreFromJSON(data) {
42
+ if (!Array.isArray(data)) {
43
+ throw new MajikSignatureStampManagerError("bulkRestoreFromJSON: expected MajikSignatureStampJSON[]");
44
+ }
45
+ await this._adapter.bulkSave(data);
46
+ await this.hydrate();
47
+ }
48
+ static async fromJSON(data, adapter) {
49
+ const manager = new MajikSignatureStampManager(adapter);
50
+ await manager.bulkRestoreFromJSON(data);
51
+ return manager;
52
+ }
53
+ async _persist(stamp) {
54
+ await this._adapter.save(stamp.toJSON());
55
+ }
56
+ // ── Core CRUD ───────────────────────────────────────────────────────────
57
+ async create(options) {
58
+ const stamp = await MajikSignatureStamp.create(options);
59
+ await this._persist(stamp);
60
+ this._cache.set(stamp.id, stamp);
61
+ return stamp;
62
+ }
63
+ async save(stamp) {
64
+ await this._persist(stamp);
65
+ this._cache.set(stamp.id, stamp);
66
+ }
67
+ async load(id) {
68
+ const cached = this._cache.get(id);
69
+ if (cached)
70
+ return cached;
71
+ const json = await this._adapter.getById(id);
72
+ if (!json)
73
+ return null;
74
+ const stamp = MajikSignatureStamp.fromJSON(json);
75
+ this._cache.set(stamp.id, stamp);
76
+ return stamp;
77
+ }
78
+ async getStampOrThrow(id) {
79
+ const stamp = await this.load(id);
80
+ if (!stamp)
81
+ throw new MajikSignatureStampManagerError(`Stamp not found: ${id}`);
82
+ return stamp;
83
+ }
84
+ async loadAll() {
85
+ const all = await this._adapter.list();
86
+ for (const json of all) {
87
+ if (!this._cache.has(json.id)) {
88
+ try {
89
+ const stamp = MajikSignatureStamp.fromJSON(json);
90
+ this._cache.set(stamp.id, stamp);
91
+ }
92
+ catch (err) {
93
+ console.warn(`MajikSignatureStampManager.loadAll: skipping malformed stamp "${json?.id}":`, err);
94
+ }
95
+ }
96
+ }
97
+ return [...this._cache.values()];
98
+ }
99
+ async delete(id) {
100
+ await this._adapter.remove(id);
101
+ this._cache.delete(id);
102
+ }
103
+ async has(id) {
104
+ if (this._cache.has(id))
105
+ return true;
106
+ return this._adapter.exists(id);
107
+ }
108
+ get(id) {
109
+ return this._cache.get(id);
110
+ }
111
+ list() {
112
+ return [...this._cache.values()];
113
+ }
114
+ listByKind(kind) {
115
+ return this.list().filter((s) => s.kind === kind);
116
+ }
117
+ listByFingerprint(fingerprint) {
118
+ return this.list().filter((s) => s.fingerprint === fingerprint);
119
+ }
120
+ async query(query) {
121
+ if (!this._adapter.query) {
122
+ throw new MajikSignatureStampManagerError("query: current adapter does not support query()");
123
+ }
124
+ return this._adapter.query(query);
125
+ }
126
+ // ── Mutations ───────────────────────────────────────────────────────────
127
+ async rename(id, newName) {
128
+ const stamp = await this.getStampOrThrow(id);
129
+ stamp.rename(newName);
130
+ await this._persist(stamp);
131
+ return stamp;
132
+ }
133
+ /** Write-once at the MajikFile layer — id is preserved explicitly. */
134
+ async replaceContent(id, rawBytes, identity, mimeType) {
135
+ const existing = await this.getStampOrThrow(id);
136
+ const replacement = await MajikSignatureStamp.create({
137
+ id: existing.id,
138
+ data: rawBytes,
139
+ identity,
140
+ kind: existing.kind,
141
+ name: existing.name,
142
+ mimeType: mimeType ?? existing.mimeType ?? undefined,
143
+ });
144
+ await this._persist(replacement);
145
+ this._cache.set(replacement.id, replacement);
146
+ return replacement;
147
+ }
148
+ async decryptContent(id, identity) {
149
+ const stamp = await this.getStampOrThrow(id);
150
+ return stamp.decryptContent(identity);
151
+ }
152
+ async bulkRemove(ids) {
153
+ if (ids.length === 0)
154
+ return;
155
+ await this._adapter.bulkRemove(ids);
156
+ for (const id of ids)
157
+ this._cache.delete(id);
158
+ }
159
+ }
@@ -0,0 +1,52 @@
1
+ import type { MajikFileIdentity } from "@majikah/majik-file";
2
+ export type StampAssetKind = "image" | "audio" | "video" | "text";
3
+ export interface MajikSignatureStampJSON {
4
+ id: string;
5
+ kind: StampAssetKind;
6
+ name: string;
7
+ mimeType: string | null;
8
+ sizeBytes: number;
9
+ fingerprint: string;
10
+ data: string;
11
+ timestamp: string;
12
+ lastUpdate: string;
13
+ }
14
+ export interface CreateStampOptions {
15
+ data: Uint8Array | ArrayBuffer;
16
+ identity: MajikFileIdentity;
17
+ kind: StampAssetKind;
18
+ name: string;
19
+ mimeType?: string;
20
+ /** Pass the existing id back in when replacing content on a stamp that
21
+ * already exists — MajikFile is write-once, so this is a *new* encrypted
22
+ * artifact under the hood, but the id stays stable for the caller. */
23
+ id?: string;
24
+ }
25
+ export declare class MajikSignatureStamp {
26
+ private readonly _id;
27
+ private readonly _kind;
28
+ private _name;
29
+ private readonly _mimeType;
30
+ private readonly _sizeBytes;
31
+ private readonly _fingerprint;
32
+ private readonly _timestamp;
33
+ private _lastUpdate;
34
+ private _encryptedBinary;
35
+ private constructor();
36
+ get id(): string;
37
+ get kind(): StampAssetKind;
38
+ get name(): string;
39
+ get mimeType(): string | null;
40
+ get sizeBytes(): number;
41
+ get fingerprint(): string;
42
+ get timestamp(): string;
43
+ get lastUpdate(): string;
44
+ static create(options: CreateStampOptions): Promise<MajikSignatureStamp>;
45
+ /** Plaintext-only rename — no re-encryption, no MajikFile involvement. */
46
+ rename(newName: string): void;
47
+ toJSON(): MajikSignatureStampJSON;
48
+ 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;
52
+ }
@@ -0,0 +1,119 @@
1
+ // majik-signature-stamp.ts
2
+ import { MajikFile } from "@majikah/majik-file";
3
+ import { arrayToBase64, base64ToUint8Array } from "../utils/utilities";
4
+ export class MajikSignatureStamp {
5
+ _id;
6
+ _kind;
7
+ _name;
8
+ _mimeType;
9
+ _sizeBytes;
10
+ _fingerprint;
11
+ _timestamp;
12
+ _lastUpdate;
13
+ _encryptedBinary; // the .mjkb bytes — never the plaintext
14
+ constructor(json, binary) {
15
+ this._id = json.id;
16
+ this._kind = json.kind;
17
+ this._name = json.name;
18
+ this._mimeType = json.mimeType;
19
+ this._sizeBytes = json.sizeBytes;
20
+ this._fingerprint = json.fingerprint;
21
+ this._timestamp = json.timestamp;
22
+ this._lastUpdate = json.lastUpdate;
23
+ this._encryptedBinary = binary;
24
+ }
25
+ get id() {
26
+ return this._id;
27
+ }
28
+ get kind() {
29
+ return this._kind;
30
+ }
31
+ get name() {
32
+ return this._name;
33
+ }
34
+ get mimeType() {
35
+ return this._mimeType;
36
+ }
37
+ get sizeBytes() {
38
+ return this._sizeBytes;
39
+ }
40
+ get fingerprint() {
41
+ return this._fingerprint;
42
+ }
43
+ get timestamp() {
44
+ return this._timestamp;
45
+ }
46
+ get lastUpdate() {
47
+ return this._lastUpdate;
48
+ }
49
+ // ── CREATE ──────────────────────────────────────────────────────────────
50
+ static async create(options) {
51
+ const { data, identity, kind, name, mimeType, id } = options;
52
+ if (!data)
53
+ throw new Error("MajikSignatureStamp.create: data is required");
54
+ if (!identity)
55
+ throw new Error("MajikSignatureStamp.create: identity is required");
56
+ if (!name?.trim())
57
+ throw new Error("MajikSignatureStamp.create: name is required");
58
+ const file = await MajikFile.create({
59
+ id,
60
+ data,
61
+ identity,
62
+ context: "user_upload",
63
+ originalName: name,
64
+ mimeType,
65
+ userId: identity.fingerprint,
66
+ });
67
+ const fileJson = file.toJSON();
68
+ const now = new Date().toISOString();
69
+ const binary = file.toBinaryBytes();
70
+ const json = {
71
+ id: fileJson.id,
72
+ kind,
73
+ name,
74
+ mimeType: fileJson.mime_type,
75
+ sizeBytes: fileJson.size_original,
76
+ fingerprint: identity.fingerprint,
77
+ data: arrayToBase64(binary),
78
+ timestamp: fileJson.timestamp ?? now,
79
+ lastUpdate: fileJson.last_update ?? now,
80
+ };
81
+ return new MajikSignatureStamp(json, binary);
82
+ }
83
+ /** Plaintext-only rename — no re-encryption, no MajikFile involvement. */
84
+ rename(newName) {
85
+ if (!newName?.trim())
86
+ throw new Error("rename: name must be non-empty");
87
+ this._name = newName.trim();
88
+ this._lastUpdate = new Date().toISOString();
89
+ }
90
+ // ── SERIALISATION ───────────────────────────────────────────────────────
91
+ toJSON() {
92
+ return {
93
+ id: this._id,
94
+ kind: this._kind,
95
+ name: this._name,
96
+ mimeType: this._mimeType,
97
+ sizeBytes: this._sizeBytes,
98
+ fingerprint: this._fingerprint,
99
+ data: arrayToBase64(this._encryptedBinary),
100
+ timestamp: this._timestamp,
101
+ lastUpdate: this._lastUpdate,
102
+ };
103
+ }
104
+ static fromJSON(json) {
105
+ return new MajikSignatureStamp(json, base64ToUint8Array(json.data));
106
+ }
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
+ }
114
+ return MajikFile.decrypt(this._encryptedBinary, identity);
115
+ }
116
+ toBinaryBytes() {
117
+ return this._encryptedBinary;
118
+ }
119
+ }
@@ -33,6 +33,7 @@ export interface UserAppPreferences {
33
33
  general: GeneralPreferences;
34
34
  signing: SigningPreferences;
35
35
  privacy: PrivacyPreferences;
36
+ security: SecurityPreferences;
36
37
  }
37
38
  export interface GeneralPreferences {
38
39
  history?: HistoryPreferences;
@@ -47,6 +48,13 @@ export interface SigningPreferences {
47
48
  export interface PrivacyPreferences {
48
49
  shareAnalytics?: boolean;
49
50
  }
51
+ export interface SecurityPreferences {
52
+ key?: KeyPreferences;
53
+ }
54
+ export interface KeyPreferences {
55
+ autoLockOnMinimize?: boolean;
56
+ autoLockInterval?: number;
57
+ }
50
58
  /**
51
59
  * Pluggable persistence backend for client-level state.
52
60
  *
@@ -18,3 +18,7 @@ export * from "./keystore/adapter-idb";
18
18
  export * from "./keystore/adapter-sql";
19
19
  export * from "./keystore/adapter-memory";
20
20
  export type * from "./keystore/_types";
21
+ export * from "./stamps/adapter-idb";
22
+ export * from "./stamps/adapter-sql";
23
+ export * from "./stamps/adapter-memory";
24
+ export type * from "./stamps/_types";
@@ -14,3 +14,6 @@ export * from "./contact-directory/groups/adapter-memory";
14
14
  export * from "./keystore/adapter-idb";
15
15
  export * from "./keystore/adapter-sql";
16
16
  export * from "./keystore/adapter-memory";
17
+ export * from "./stamps/adapter-idb";
18
+ export * from "./stamps/adapter-sql";
19
+ export * from "./stamps/adapter-memory";
@@ -9,6 +9,7 @@ export declare const MAJIKAH_SQL_TABLES: {
9
9
  readonly MAJIK_KEYS: "majik_keys";
10
10
  readonly MAJIK_CONTACTS: "majik_contacts";
11
11
  readonly MAJIK_CONTACT_GROUPS: "majik_contact_groups";
12
+ readonly MAJIK_SIGNATURE_STAMPS: "majik_signature_stamps";
12
13
  };
13
14
  export type MajikahSQLTable = (typeof MAJIKAH_SQL_TABLES)[keyof typeof MAJIKAH_SQL_TABLES];
14
15
  export declare function buildSchemaSQL(schemas: MajikahSQLSchema[]): MajikahSQLSchema;
@@ -16,5 +17,6 @@ export declare const MAJIKAH_SQL_SCHEMA_MAJIK_CLIENT_STATE: MajikahSQLSchema;
16
17
  export declare const MAJIKAH_SQL_SCHEMA_MAJIK_KEYS: MajikahSQLSchema;
17
18
  export declare const MAJIKAH_SQL_SCHEMA_MAJIK_CONTACTS: MajikahSQLSchema;
18
19
  export declare const MAJIKAH_SQL_SCHEMA_MAJIK_CONTACT_GROUPS: MajikahSQLSchema;
20
+ export declare const MAJIKAH_SQL_SCHEMA_MAJIK_SIGNATURE_STAMPS: MajikahSQLSchema;
19
21
  export declare const MAJIKAH_SQL_SCHEMA_FULL: MajikahSQLSchema;
20
22
  export {};
@@ -8,6 +8,7 @@ export const MAJIKAH_SQL_TABLES = {
8
8
  MAJIK_KEYS: "majik_keys",
9
9
  MAJIK_CONTACTS: "majik_contacts",
10
10
  MAJIK_CONTACT_GROUPS: "majik_contact_groups",
11
+ MAJIK_SIGNATURE_STAMPS: "majik_signature_stamps",
11
12
  };
12
13
  function normalizeSQL(sql) {
13
14
  return sql
@@ -76,9 +77,35 @@ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS} (
76
77
  CREATE INDEX IF NOT EXISTS idx_majik_contact_groups_created_at
77
78
  ON ${MAJIKAH_SQL_TABLES.MAJIK_CONTACT_GROUPS}(created_at);
78
79
  `;
80
+ export const MAJIKAH_SQL_SCHEMA_MAJIK_SIGNATURE_STAMPS = `
81
+ CREATE TABLE IF NOT EXISTS ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS} (
82
+ id TEXT PRIMARY KEY,
83
+ json TEXT NOT NULL,
84
+ kind TEXT NOT NULL,
85
+ name TEXT NOT NULL,
86
+ mime_type TEXT,
87
+ size_bytes INTEGER NOT NULL,
88
+ fingerprint TEXT NOT NULL,
89
+ created_at TEXT NOT NULL,
90
+ updated_at TEXT NOT NULL
91
+ );
92
+
93
+ CREATE INDEX IF NOT EXISTS idx_majik_signature_stamps_kind
94
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}(kind);
95
+
96
+ CREATE INDEX IF NOT EXISTS idx_majik_signature_stamps_fingerprint
97
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}(fingerprint);
98
+
99
+ CREATE INDEX IF NOT EXISTS idx_majik_signature_stamps_updated_at
100
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}(updated_at);
101
+
102
+ CREATE INDEX IF NOT EXISTS idx_majik_signature_stamps_fingerprint_kind
103
+ ON ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}(fingerprint, kind);
104
+ `;
79
105
  export const MAJIKAH_SQL_SCHEMA_FULL = buildSchemaSQL([
80
106
  MAJIKAH_SQL_SCHEMA_MAJIK_CLIENT_STATE,
81
107
  MAJIKAH_SQL_SCHEMA_MAJIK_KEYS,
82
108
  MAJIKAH_SQL_SCHEMA_MAJIK_CONTACTS,
83
109
  MAJIKAH_SQL_SCHEMA_MAJIK_CONTACT_GROUPS,
110
+ MAJIKAH_SQL_SCHEMA_MAJIK_SIGNATURE_STAMPS,
84
111
  ]);
@@ -0,0 +1,3 @@
1
+ import { MajikSignatureStampJSON } from "../../stamp/majik-signature-stamp";
2
+ import { MajikStorageAdapter } from "../storage-adapter";
3
+ export type MajikSignatureStampStorageAdapter = MajikStorageAdapter<MajikSignatureStampJSON>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import { MajikSignatureStampJSON } from "../../stamp/majik-signature-stamp";
2
+ import { IDBGenericAdapter } from "../idb-adapter";
3
+ export declare const IDB_ADAPTER_STAMPSTORE: IDBGenericAdapter<MajikSignatureStampJSON>;
@@ -0,0 +1,5 @@
1
+ import { IDBGenericAdapter } from "../idb-adapter";
2
+ const IDB_DB_NAME = "majik-signature-stamps";
3
+ const IDB_STORE_NAME = "stamps";
4
+ const IDB_VERSION = 1;
5
+ export const IDB_ADAPTER_STAMPSTORE = new IDBGenericAdapter(IDB_DB_NAME, IDB_STORE_NAME, IDB_VERSION);
@@ -0,0 +1,14 @@
1
+ import { MajikSignatureStampJSON } from "../../stamp/majik-signature-stamp";
2
+ import { MajikSignatureStampStorageAdapter } from "./_types";
3
+ export declare class InMemoryStampstoreAdapter implements MajikSignatureStampStorageAdapter {
4
+ private _store;
5
+ save(item: MajikSignatureStampJSON): Promise<void>;
6
+ getById(id: string): Promise<MajikSignatureStampJSON | null>;
7
+ list(): Promise<MajikSignatureStampJSON[]>;
8
+ remove(id: string): Promise<boolean>;
9
+ clear(): Promise<void>;
10
+ count(): Promise<number>;
11
+ exists(id: string): Promise<boolean>;
12
+ bulkSave(items: MajikSignatureStampJSON[]): Promise<void>;
13
+ bulkRemove(ids: string[]): Promise<void>;
14
+ }
@@ -0,0 +1,32 @@
1
+ export class InMemoryStampstoreAdapter {
2
+ _store = new Map();
3
+ async save(item) {
4
+ this._store.set(item.id, item);
5
+ }
6
+ async getById(id) {
7
+ return this._store.get(id) ?? null;
8
+ }
9
+ async list() {
10
+ return Array.from(this._store.values());
11
+ }
12
+ async remove(id) {
13
+ return this._store.delete(id);
14
+ }
15
+ async clear() {
16
+ this._store.clear();
17
+ }
18
+ async count() {
19
+ return this._store.size;
20
+ }
21
+ async exists(id) {
22
+ return this._store.has(id);
23
+ }
24
+ async bulkSave(items) {
25
+ for (const item of items)
26
+ this._store.set(item.id, item);
27
+ }
28
+ async bulkRemove(ids) {
29
+ for (const id of ids)
30
+ this._store.delete(id);
31
+ }
32
+ }
@@ -0,0 +1,18 @@
1
+ import { MajikSignatureStampJSON } from "../../stamp/majik-signature-stamp";
2
+ import { MajikSignatureStampStorageAdapter } from "./_types";
3
+ import { SQLiteDatabase } from "../sql-db-manager";
4
+ import { StorageQuery } from "../storage-adapter";
5
+ export declare class SQLiteStampstoreAdapter implements MajikSignatureStampStorageAdapter {
6
+ private db;
7
+ constructor(db: SQLiteDatabase);
8
+ save(stamp: MajikSignatureStampJSON): Promise<void>;
9
+ getById(id: string): Promise<MajikSignatureStampJSON | null>;
10
+ list(): Promise<MajikSignatureStampJSON[]>;
11
+ remove(id: string): Promise<boolean>;
12
+ clear(): Promise<void>;
13
+ count(): Promise<number>;
14
+ exists(id: string): Promise<boolean>;
15
+ bulkSave(stamps: MajikSignatureStampJSON[]): Promise<void>;
16
+ bulkRemove(ids: string[]): Promise<void>;
17
+ query(query: StorageQuery<MajikSignatureStampJSON>): Promise<MajikSignatureStampJSON[]>;
18
+ }
@@ -0,0 +1,99 @@
1
+ import { MAJIKAH_SQL_TABLES } from "../sql-schema";
2
+ export class SQLiteStampstoreAdapter {
3
+ db;
4
+ constructor(db) {
5
+ this.db = db;
6
+ }
7
+ async save(stamp) {
8
+ await this.db.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}
9
+ (id, json, kind, name, mime_type, size_bytes, fingerprint, created_at, updated_at)
10
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
11
+ stamp.id,
12
+ JSON.stringify(stamp),
13
+ stamp.kind,
14
+ stamp.name,
15
+ stamp.mimeType,
16
+ stamp.sizeBytes,
17
+ stamp.fingerprint,
18
+ stamp.timestamp,
19
+ stamp.lastUpdate,
20
+ ]);
21
+ }
22
+ async getById(id) {
23
+ const row = await this.db.get(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS} WHERE id = ?`, [id]);
24
+ return row ? JSON.parse(row.json) : null;
25
+ }
26
+ async list() {
27
+ const rows = await this.db.all(`SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}`);
28
+ return rows.map((r) => JSON.parse(r.json));
29
+ }
30
+ async remove(id) {
31
+ const exists = await this.exists(id);
32
+ if (!exists)
33
+ return false;
34
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS} WHERE id = ?`, [id]);
35
+ return true;
36
+ }
37
+ async clear() {
38
+ await this.db.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}`);
39
+ }
40
+ async count() {
41
+ const row = await this.db.get(`SELECT COUNT(*) as n FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}`);
42
+ return row?.n ?? 0;
43
+ }
44
+ async exists(id) {
45
+ const row = await this.db.get(`SELECT 1 FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS} WHERE id = ?`, [id]);
46
+ return !!row;
47
+ }
48
+ async bulkSave(stamps) {
49
+ if (stamps.length === 0)
50
+ return;
51
+ await this.db.transaction(async (tx) => {
52
+ for (const s of stamps) {
53
+ await tx.run(`INSERT OR REPLACE INTO ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}
54
+ (id, json, kind, name, mime_type, size_bytes, fingerprint, created_at, updated_at)
55
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
56
+ s.id,
57
+ JSON.stringify(s),
58
+ s.kind,
59
+ s.name,
60
+ s.mimeType,
61
+ s.sizeBytes,
62
+ s.fingerprint,
63
+ s.timestamp,
64
+ s.lastUpdate,
65
+ ]);
66
+ }
67
+ });
68
+ }
69
+ async bulkRemove(ids) {
70
+ if (ids.length === 0)
71
+ return;
72
+ await this.db.transaction(async (tx) => {
73
+ for (const id of ids) {
74
+ await tx.run(`DELETE FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS} WHERE id = ?`, [id]);
75
+ }
76
+ });
77
+ }
78
+ async query(query) {
79
+ const clauses = [];
80
+ const values = [];
81
+ if (query.where) {
82
+ for (const [key, value] of Object.entries(query.where)) {
83
+ clauses.push(`${key} = ?`);
84
+ values.push(value);
85
+ }
86
+ }
87
+ let sql = `SELECT json FROM ${MAJIKAH_SQL_TABLES.MAJIK_SIGNATURE_STAMPS}`;
88
+ if (clauses.length > 0)
89
+ sql += ` WHERE ${clauses.join(" AND ")}`;
90
+ if (query.orderBy)
91
+ sql += ` ORDER BY ${String(query.orderBy)} ${query.orderDirection ?? "asc"}`;
92
+ if (query.limit)
93
+ sql += ` LIMIT ${query.limit}`;
94
+ if (query.offset)
95
+ sql += ` OFFSET ${query.offset}`;
96
+ const rows = await this.db.all(sql, values);
97
+ return rows.map((r) => JSON.parse(r.json));
98
+ }
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { MajikContactManagerJSON } from "./contacts/types";
2
+ import { MajikSignatureStampJSON } from "./stamp/majik-signature-stamp";
2
3
  import { UserAppPreferences } from "./storage";
3
4
  export type ISODateString = string;
4
5
  export type MajikMessageAccountID = string;
@@ -88,5 +89,6 @@ export interface MajikKeyMetadata {
88
89
  }
89
90
  export interface AppBackUpData {
90
91
  contacts: MajikContactManagerJSON;
92
+ stamps?: MajikSignatureStampJSON[];
91
93
  preferences?: UserAppPreferences;
92
94
  }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export * from "./core/contacts/majik-contact-groups";
6
6
  export * from "./core/crypto/constants";
7
7
  export * from "./core/crypto/keystore-manager";
8
8
  export * from "./core/storage";
9
+ export * from "./core/stamp/majik-signature-stamp";
9
10
  export * from "./core/utils/utilities";
10
11
  export * from "./core/identity";
11
12
  export { migrateMajikMessageJSON } from "./core/contacts/majik-contact-migration";
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./core/contacts/majik-contact-groups";
5
5
  export * from "./core/crypto/constants";
6
6
  export * from "./core/crypto/keystore-manager";
7
7
  export * from "./core/storage";
8
+ export * from "./core/stamp/majik-signature-stamp";
8
9
  export * from "./core/utils/utilities";
9
10
  export * from "./core/identity";
10
11
  export { migrateMajikMessageJSON } from "./core/contacts/majik-contact-migration";
@@ -23,10 +23,12 @@ import { MajikContactManager, MajikContactManagerAdapters } from "./core/contact
23
23
  import { MajikContactManagerJSON } from "./core/contacts/types";
24
24
  import { MajikContact, MajikContactGroup, MajikContactGroupMeta, MajikContactMeta, SerializedMajikContact } from "@majikah/majik-contact";
25
25
  import { ClientStateManager } from "./core/client-state-manager";
26
- import { ClientStateStorageAdapter, MajikKeyStorageAdapter, SQLiteDatabase, UserAppPreferences } from "./core/storage";
26
+ import { ClientStateStorageAdapter, MajikKeyStorageAdapter, MajikSignatureStampStorageAdapter, SQLiteDatabase, UserAppPreferences } from "./core/storage";
27
27
  import { AppDataSnapshot, ContactManagerSnapshot } from "./core/backup/types";
28
28
  import { MnemonicLanguage } from "@majikah/majik-key/dist/core/crypto/wordlist";
29
- type MajikSignatureClientEvents = "sign" | "verify" | "unlock" | "lock" | "error" | "new-account" | "new-contact" | "new-contact-group" | "removed-account" | "removed-contact" | "removed-contact-group" | "contact-group-change" | "active-account-change" | "restore-backup";
29
+ import { MajikSignatureStampManager } from "./core/stamp/majik-signature-stamp-manager";
30
+ import { MajikSignatureStamp, MajikSignatureStampJSON, StampAssetKind } from "./core/stamp/majik-signature-stamp";
31
+ type MajikSignatureClientEvents = "sign" | "verify" | "unlock" | "lock" | "error" | "new-stamp" | "removed-stamp" | "new-account" | "new-contact" | "new-contact-group" | "removed-account" | "removed-contact" | "removed-contact-group" | "contact-group-change" | "active-account-change" | "restore-backup";
30
32
  type EventCallback = (...args: any[]) => void;
31
33
  export interface MajikSignatureClientConfig {
32
34
  dbSQL?: SQLiteDatabase;
@@ -46,9 +48,11 @@ export interface MajikSignatureClientConfig {
46
48
  * is ignored.
47
49
  */
48
50
  clientStateManager?: ClientStateManager;
51
+ stampsManager?: MajikSignatureStampManager;
49
52
  adapters?: {
50
53
  contacts?: MajikContactManagerAdapters;
51
54
  keys?: MajikKeyStorageAdapter;
55
+ stamps?: MajikSignatureStampStorageAdapter;
52
56
  /**
53
57
  * Adapter for client-level state (account order, invoice defaults, etc.).
54
58
  * Defaults to IDB_ADAPTER_CLIENT_STATE in browser environments.
@@ -79,6 +83,7 @@ export declare class MajikSignatureClient {
79
83
  private _db;
80
84
  private _contacts;
81
85
  private _keys;
86
+ private _stamps;
82
87
  private _state;
83
88
  private _ownAccounts;
84
89
  private _ownAccountsOrder;
@@ -89,6 +94,8 @@ export declare class MajikSignatureClient {
89
94
  get keyManager(): MajikKeyManager;
90
95
  /** Expose the client state manager for direct access if needed. */
91
96
  get stateManager(): ClientStateManager;
97
+ /** Expose the stamp manager for direct access if needed. */
98
+ get stampManager(): MajikSignatureStampManager;
92
99
  /**
93
100
  * Load all domains from their adapters and restore client state.
94
101
  * Call once on startup.
@@ -824,6 +831,19 @@ export declare class MajikSignatureClient {
824
831
  isSealed(file: Blob, options?: {
825
832
  mimeType?: string;
826
833
  }): Promise<boolean>;
834
+ createStamp(data: Uint8Array | ArrayBuffer, kind: StampAssetKind, name: string, options?: {
835
+ mimeType?: string;
836
+ accountId?: string;
837
+ }): Promise<MajikSignatureStamp>;
838
+ listStamps(kind?: StampAssetKind): MajikSignatureStamp[];
839
+ getStamp(id: string): Promise<MajikSignatureStamp | null>;
840
+ decryptStampContent(id: string, accountId?: string): Promise<Uint8Array>;
841
+ renameStamp(id: string, newName: string): Promise<MajikSignatureStamp>;
842
+ replaceStampContent(id: string, data: Uint8Array | ArrayBuffer, options?: {
843
+ mimeType?: string;
844
+ accountId?: string;
845
+ }): Promise<MajikSignatureStamp>;
846
+ removeStamp(id: string): Promise<boolean>;
827
847
  /**
828
848
  * Sign an image with dual-layer embedding.
829
849
  *
@@ -941,6 +961,12 @@ export declare class MajikSignatureClient {
941
961
  * throughout MajikMessage — consistent account/contact resolution in one place.
942
962
  */
943
963
  private _resolveSignerPublicKeys;
964
+ /**
965
+ * Resolve a MajikFileIdentity from an unlocked account, for stamp
966
+ * encryption/decryption. Requires the account to have ML-KEM keys and
967
+ * to already be unlocked — call ensureIdentityUnlocked() first if needed.
968
+ */
969
+ private _resolveStampIdentity;
944
970
  /**
945
971
  * Wipe all data from every adapter and reset in-memory state.
946
972
  * The client remains usable — call hydrate() or add new accounts after reset.
@@ -948,6 +974,9 @@ export declare class MajikSignatureClient {
948
974
  resetData(): Promise<void>;
949
975
  private _registerOwnAccount;
950
976
  backupContacts(): Promise<Blob>;
977
+ backupStamps(): Promise<Blob>;
978
+ private _parseStampsBackup;
979
+ readStampsBackup(input: Blob | ArrayBufferLike | ArrayBufferView): Promise<MajikSignatureStampJSON[]>;
951
980
  backupAppData(): Promise<Blob>;
952
981
  /**
953
982
  * Parses a contacts backup blob into a ContactManagerSnapshot —
@@ -955,6 +984,9 @@ export declare class MajikSignatureClient {
955
984
  * No side-effects; nothing is written to the live store.
956
985
  */
957
986
  private _parseContactsBackup;
987
+ restoreStamps(input: Blob | ArrayBufferLike | ArrayBufferView): Promise<{
988
+ restored: number;
989
+ }>;
958
990
  /**
959
991
  * Restores contacts (and optionally groups) from a contacts backup blob.
960
992
  *
@@ -979,7 +1011,7 @@ export declare class MajikSignatureClient {
979
1011
  readContactsBackup(input: Blob | ArrayBufferLike | ArrayBufferView): Promise<ContactManagerSnapshot>;
980
1012
  /**
981
1013
  * Restores all data from a full backup blob produced by `backupAppData()`.
982
- * Contacts and groups are restored before chatMessages so recipients resolve
1014
+ * Contacts and groups are restored before stamps so recipients resolve
983
1015
  * correctly.
984
1016
  */
985
1017
  restoreAppData(blob: Blob): Promise<{
@@ -990,9 +1022,9 @@ export declare class MajikSignatureClient {
990
1022
  * Probes the first bytes of a blob and returns which backup type it is,
991
1023
  * without fully parsing it. Useful for file-picker validation UI.
992
1024
  *
993
- * @returns `"chats" | "contacts" | "appData" | "unknown"`
1025
+ * @returns `"stamps" | "contacts" | "appData" | "unknown"`
994
1026
  */
995
- static probeBackupType(blob: Blob): Promise<"chats" | "contacts" | "appData" | "unknown">;
1027
+ static probeBackupType(blob: Blob): Promise<"stamps" | "contacts" | "appData" | "unknown">;
996
1028
  /**
997
1029
  * Parses an app data backup blob into an AppDataSnapshot.
998
1030
  * No side-effects; nothing is written to the live store.
@@ -1008,6 +1040,7 @@ export declare class MajikSignatureClient {
1008
1040
  * The caller controls exactly which domains are written.
1009
1041
  */
1010
1042
  restoreAppDataSelective(snapshot: AppDataSnapshot, options?: {
1043
+ stamps?: boolean;
1011
1044
  contacts?: boolean;
1012
1045
  groups?: boolean;
1013
1046
  preferences?: boolean;
@@ -19,15 +19,18 @@ import { MajikSignature } from "@majikah/majik-signature";
19
19
  import { base64ToUint8Array } from "./core/utils/utilities";
20
20
  import { MajikContactManager, } from "./core/contacts/majik-contact-manager";
21
21
  import { ClientStateManager } from "./core/client-state-manager";
22
- import { InMemoryClientStateAdapter, InMemoryKeystoreAdapter, } from "./core/storage";
22
+ import { InMemoryClientStateAdapter, InMemoryKeystoreAdapter, InMemoryStampstoreAdapter, } from "./core/storage";
23
23
  import { MajikCompressedJSON } from "@majikah/majik-cjson";
24
24
  import { prependMagic, readBackupBlob } from "./core/backup/utils";
25
- import { MAJIK_MESSAGE_BACKUP_MAGIC, MAJIK_MESSAGE_BACKUP_MAGIC_SIZE, } from "./core/backup/constants";
25
+ import { MAJIK_SIGNATURE_BACKUP_MAGIC, MAJIK_MESSAGE_BACKUP_MAGIC_SIZE, } from "./core/backup/constants";
26
+ import { MajikSignatureStampManager } from "./core/stamp/majik-signature-stamp-manager";
27
+ import { MajikSignatureStamp, } from "./core/stamp/majik-signature-stamp";
26
28
  // ─── MajikSignatureClient ─────────────────────────────────────────────────────
27
29
  export class MajikSignatureClient {
28
30
  _db;
29
31
  _contacts;
30
32
  _keys;
33
+ _stamps;
31
34
  _state;
32
35
  _ownAccounts = new Map();
33
36
  _ownAccountsOrder = [];
@@ -41,8 +44,9 @@ export class MajikSignatureClient {
41
44
  this._keys =
42
45
  config.keyManager ??
43
46
  new MajikKeyManager(config.adapters?.keys ?? new InMemoryKeystoreAdapter());
44
- // this._chats =
45
- // config.
47
+ this._stamps =
48
+ config.stampsManager ??
49
+ new MajikSignatureStampManager(config.adapters?.stamps ?? new InMemoryStampstoreAdapter());
46
50
  this._state =
47
51
  config.clientStateManager ??
48
52
  new ClientStateManager(config.adapters?.clientState ?? new InMemoryClientStateAdapter());
@@ -52,6 +56,8 @@ export class MajikSignatureClient {
52
56
  "unlock",
53
57
  "lock",
54
58
  "error",
59
+ "new-stamp",
60
+ "removed-stamp",
55
61
  "new-account",
56
62
  "new-contact",
57
63
  "new-contact-group",
@@ -72,6 +78,10 @@ export class MajikSignatureClient {
72
78
  get stateManager() {
73
79
  return this._state;
74
80
  }
81
+ /** Expose the stamp manager for direct access if needed. */
82
+ get stampManager() {
83
+ return this._stamps;
84
+ }
75
85
  // ── Hydration ─────────────────────────────────────────────────────────────
76
86
  /**
77
87
  * Load all domains from their adapters and restore client state.
@@ -87,6 +97,7 @@ export class MajikSignatureClient {
87
97
  await this._keys.hydrate();
88
98
  // 2. Contacts + groups
89
99
  await this._contacts.hydrate();
100
+ await this._stamps.hydrate();
90
101
  // 4. Client state — account order, app settings, etc.
91
102
  await this._state.hydrate();
92
103
  // 5. Own accounts — rebuild from keys loaded in step 1
@@ -1642,6 +1653,51 @@ export class MajikSignatureClient {
1642
1653
  throw err;
1643
1654
  }
1644
1655
  }
1656
+ // ── Stamps (encrypted reusable assets — signatures, audio, video, text) ────
1657
+ async createStamp(data, kind, name, options) {
1658
+ try {
1659
+ const identity = this._resolveStampIdentity(options?.accountId);
1660
+ const stamp = await this._stamps.create({
1661
+ data,
1662
+ identity,
1663
+ kind,
1664
+ name,
1665
+ mimeType: options?.mimeType,
1666
+ });
1667
+ this._emit("new-stamp", stamp);
1668
+ return stamp;
1669
+ }
1670
+ catch (err) {
1671
+ this._emit("error", err, { context: "createStamp" });
1672
+ throw err;
1673
+ }
1674
+ }
1675
+ listStamps(kind) {
1676
+ return kind ? this._stamps.listByKind(kind) : this._stamps.list();
1677
+ }
1678
+ async getStamp(id) {
1679
+ return this._stamps.load(id);
1680
+ }
1681
+ async decryptStampContent(id, accountId) {
1682
+ const identity = this._resolveStampIdentity(accountId);
1683
+ return this._stamps.decryptContent(id, identity);
1684
+ }
1685
+ async renameStamp(id, newName) {
1686
+ return this._stamps.rename(id, newName);
1687
+ }
1688
+ async replaceStampContent(id, data, options) {
1689
+ const identity = this._resolveStampIdentity(options?.accountId);
1690
+ const raw = data instanceof Uint8Array ? data : new Uint8Array(data);
1691
+ return this._stamps.replaceContent(id, raw, identity, options?.mimeType);
1692
+ }
1693
+ async removeStamp(id) {
1694
+ const existed = await this._stamps.has(id);
1695
+ if (!existed)
1696
+ return false;
1697
+ await this._stamps.delete(id);
1698
+ this._emit("removed-stamp", id);
1699
+ return true;
1700
+ }
1645
1701
  // ── STAMP (compression-resistant image signing) ───────────────────────────
1646
1702
  //
1647
1703
  // These methods delegate to MajikImageSignature, passing `MajikSignature`
@@ -1815,6 +1871,33 @@ export class MajikSignatureClient {
1815
1871
  }
1816
1872
  return null;
1817
1873
  }
1874
+ /**
1875
+ * Resolve a MajikFileIdentity from an unlocked account, for stamp
1876
+ * encryption/decryption. Requires the account to have ML-KEM keys and
1877
+ * to already be unlocked — call ensureIdentityUnlocked() first if needed.
1878
+ */
1879
+ _resolveStampIdentity(accountId) {
1880
+ const id = accountId ?? this.getActiveAccount()?.id;
1881
+ if (!id)
1882
+ throw new Error("No active account — call setActiveAccount() first");
1883
+ const key = this._keys.get(id);
1884
+ if (!key)
1885
+ throw new Error(`Account not found in keystore: "${id}"`);
1886
+ if (key.isLocked) {
1887
+ throw new Error(`Account "${id}" is locked. Call unlockAccount() before using stamps.`);
1888
+ }
1889
+ const mlKemSecretKey = this._keys.getMlKemSecretKey(id);
1890
+ if (!mlKemSecretKey) {
1891
+ throw new Error(`Account "${id}" has no ML-KEM keys. Re-import via ` +
1892
+ `importAccountFromMnemonicBackup() to enable stamp encryption.`);
1893
+ }
1894
+ return {
1895
+ publicKey: key.publicKeyBase64,
1896
+ fingerprint: key.fingerprint,
1897
+ mlKemPublicKey: key.mlKemPublicKey,
1898
+ mlKemSecretKey,
1899
+ };
1900
+ }
1818
1901
  // ==========================================================================
1819
1902
  // ── RESET ─────────────────────────────────────────────────────────────────
1820
1903
  // ==========================================================================
@@ -1826,6 +1909,7 @@ export class MajikSignatureClient {
1826
1909
  try {
1827
1910
  await this._keys.adapter.clear();
1828
1911
  await this._contacts.clear();
1912
+ await this._stamps.adapter.clear(); // NEW
1829
1913
  await this._state.clear();
1830
1914
  if (this._db) {
1831
1915
  await this._db.vacuum();
@@ -1834,6 +1918,7 @@ export class MajikSignatureClient {
1834
1918
  this._ownAccounts.clear();
1835
1919
  this._ownAccountsOrder = [];
1836
1920
  this._keys = new MajikKeyManager(this._keys.adapter);
1921
+ this._stamps = new MajikSignatureStampManager(this._stamps.adapter); // NEW
1837
1922
  this._emit("active-account-change", null);
1838
1923
  }
1839
1924
  catch (err) {
@@ -1864,21 +1949,40 @@ export class MajikSignatureClient {
1864
1949
  const managerJSON = await this._contacts.toJSON();
1865
1950
  const cj = MajikCompressedJSON.create(managerJSON);
1866
1951
  const payload = cj.toBinary();
1867
- const stamped = prependMagic(MAJIK_MESSAGE_BACKUP_MAGIC.contacts, payload);
1952
+ const stamped = prependMagic(MAJIK_SIGNATURE_BACKUP_MAGIC.contacts, payload);
1953
+ return new Blob([stamped], {
1954
+ type: "application/octet-stream",
1955
+ });
1956
+ }
1957
+ async backupStamps() {
1958
+ const stampsJSON = await this._stamps.toJSON();
1959
+ const cj = MajikCompressedJSON.create(stampsJSON);
1960
+ const payload = cj.toBinary();
1961
+ const stamped = prependMagic(MAJIK_SIGNATURE_BACKUP_MAGIC.stamps, payload);
1868
1962
  return new Blob([stamped], {
1869
1963
  type: "application/octet-stream",
1870
1964
  });
1871
1965
  }
1966
+ async _parseStampsBackup(input) {
1967
+ const payload = await readBackupBlob(input, MAJIK_SIGNATURE_BACKUP_MAGIC.stamps, "stamps");
1968
+ const cj = await MajikCompressedJSON.fromMJKCJSON(payload);
1969
+ return cj.payload;
1970
+ }
1971
+ async readStampsBackup(input) {
1972
+ return this._parseStampsBackup(input);
1973
+ }
1872
1974
  async backupAppData() {
1873
1975
  const contactsJSON = await this._contacts.toJSON();
1976
+ const stampsJSON = await this._stamps.toJSON(); // NEW
1874
1977
  const userPref = await this.getUserAppPreferences();
1875
1978
  const backupJSON = {
1876
1979
  contacts: contactsJSON,
1980
+ stamps: stampsJSON, // NEW
1877
1981
  preferences: userPref ?? undefined,
1878
1982
  };
1879
1983
  const cj = MajikCompressedJSON.create(backupJSON);
1880
1984
  const payload = cj.toBinary();
1881
- const stamped = prependMagic(MAJIK_MESSAGE_BACKUP_MAGIC.appData, payload);
1985
+ const stamped = prependMagic(MAJIK_SIGNATURE_BACKUP_MAGIC.appData, payload);
1882
1986
  return new Blob([stamped], {
1883
1987
  type: "application/octet-stream",
1884
1988
  });
@@ -1893,7 +1997,7 @@ export class MajikSignatureClient {
1893
1997
  * No side-effects; nothing is written to the live store.
1894
1998
  */
1895
1999
  async _parseContactsBackup(input) {
1896
- const payload = await readBackupBlob(input, MAJIK_MESSAGE_BACKUP_MAGIC.contacts, "contacts");
2000
+ const payload = await readBackupBlob(input, MAJIK_SIGNATURE_BACKUP_MAGIC.contacts, "contacts");
1897
2001
  const cj = await MajikCompressedJSON.fromMJKCJSON(payload);
1898
2002
  const managerJSON = cj.payload;
1899
2003
  // Hydrate a throw-away manager so callers get real instances, not raw JSON
@@ -1904,13 +2008,11 @@ export class MajikSignatureClient {
1904
2008
  return { managerJSON, contacts, groups };
1905
2009
  }
1906
2010
  // ── Public restore (saves to store) ───────────────────────────────────────
1907
- // async restoreChatMessages(
1908
- // input: Blob | ArrayBufferLike | ArrayBufferView,
1909
- // ): Promise<{ restored: number }> {
1910
- // const chatMessages = await this._parseChatMessagesBackup(input);
1911
- // await Promise.all(chatMessages.map((inv) => this._chatMessages.save(inv)));
1912
- // return { restored: chatMessages.length };
1913
- // }
2011
+ async restoreStamps(input) {
2012
+ const stampsJSON = await this._parseStampsBackup(input);
2013
+ await Promise.all(stampsJSON.map((json) => this._stamps.save(MajikSignatureStamp.fromJSON(json))));
2014
+ return { restored: stampsJSON.length };
2015
+ }
1914
2016
  /**
1915
2017
  * Restores contacts (and optionally groups) from a contacts backup blob.
1916
2018
  *
@@ -1956,11 +2058,6 @@ export class MajikSignatureClient {
1956
2058
  return { contacts: contactCount, groups: groupCount };
1957
2059
  }
1958
2060
  // ── Public read (no side-effects) ─────────────────────────────────────────
1959
- // async readInvoicesBackup(
1960
- // input: Blob | ArrayBufferLike | ArrayBufferView,
1961
- // ): Promise<MajikInvoice[]> {
1962
- // return this._parseInvoicesBackup(input);
1963
- // }
1964
2061
  /**
1965
2062
  * Parses a contacts backup without writing anything to the live store.
1966
2063
  * Returns contacts and user-defined groups for the caller to preview.
@@ -1970,11 +2067,11 @@ export class MajikSignatureClient {
1970
2067
  }
1971
2068
  /**
1972
2069
  * Restores all data from a full backup blob produced by `backupAppData()`.
1973
- * Contacts and groups are restored before chatMessages so recipients resolve
2070
+ * Contacts and groups are restored before stamps so recipients resolve
1974
2071
  * correctly.
1975
2072
  */
1976
2073
  async restoreAppData(blob) {
1977
- const payload = await readBackupBlob(blob, MAJIK_MESSAGE_BACKUP_MAGIC.appData, "app data");
2074
+ const payload = await readBackupBlob(blob, MAJIK_SIGNATURE_BACKUP_MAGIC.appData, "app data");
1978
2075
  const cj = await MajikCompressedJSON.fromMJKCJSON(payload);
1979
2076
  const data = cj.payload;
1980
2077
  // 1. Contacts
@@ -1998,13 +2095,12 @@ export class MajikSignatureClient {
1998
2095
  }
1999
2096
  }
2000
2097
  }
2001
- // 2. Chats
2002
- // await Promise.all(
2003
- // (data.chatMessages ?? []).map((json) => {
2004
- // const invoice = MajikInvoice.fromJSON(json);
2005
- // return this._chatMessages.save(invoice);
2006
- // }),
2007
- // );
2098
+ // 2. Stamps
2099
+ // inside restoreAppData(), after the contacts/groups block:
2100
+ await Promise.all((data.stamps ?? []).map((json) => {
2101
+ const stamp = MajikSignatureStamp.fromJSON(json);
2102
+ return this._stamps.save(stamp);
2103
+ }));
2008
2104
  if (data.preferences) {
2009
2105
  await this.setUserAppPreferences(data.preferences);
2010
2106
  }
@@ -2017,11 +2113,11 @@ export class MajikSignatureClient {
2017
2113
  * Probes the first bytes of a blob and returns which backup type it is,
2018
2114
  * without fully parsing it. Useful for file-picker validation UI.
2019
2115
  *
2020
- * @returns `"chats" | "contacts" | "appData" | "unknown"`
2116
+ * @returns `"stamps" | "contacts" | "appData" | "unknown"`
2021
2117
  */
2022
2118
  static async probeBackupType(blob) {
2023
2119
  const header = new Uint8Array(await blob.slice(0, MAJIK_MESSAGE_BACKUP_MAGIC_SIZE).arrayBuffer());
2024
- for (const [type, magic] of Object.entries(MAJIK_MESSAGE_BACKUP_MAGIC)) {
2120
+ for (const [type, magic] of Object.entries(MAJIK_SIGNATURE_BACKUP_MAGIC)) {
2025
2121
  if (magic.every((byte, i) => header[i] === byte))
2026
2122
  return type;
2027
2123
  }
@@ -2033,7 +2129,7 @@ export class MajikSignatureClient {
2033
2129
  * No side-effects; nothing is written to the live store.
2034
2130
  */
2035
2131
  async _parseAppDataBackup(input) {
2036
- const payload = await readBackupBlob(input, MAJIK_MESSAGE_BACKUP_MAGIC.appData, "app data");
2132
+ const payload = await readBackupBlob(input, MAJIK_SIGNATURE_BACKUP_MAGIC.appData, "app data");
2037
2133
  const cj = await MajikCompressedJSON.fromMJKCJSON(payload);
2038
2134
  const data = cj.payload;
2039
2135
  const tempManager = await MajikContactManager.fromJSON(data.contacts);
@@ -2042,8 +2138,8 @@ export class MajikSignatureClient {
2042
2138
  return {
2043
2139
  contacts,
2044
2140
  groups,
2141
+ stamps: data.stamps ?? [], // NEW
2045
2142
  preferences: data.preferences ?? null,
2046
- // Keep raw manager JSON for the restore path
2047
2143
  _contactsManagerJSON: data.contacts,
2048
2144
  };
2049
2145
  }
@@ -2061,15 +2157,13 @@ export class MajikSignatureClient {
2061
2157
  * The caller controls exactly which domains are written.
2062
2158
  */
2063
2159
  async restoreAppDataSelective(snapshot, options = {}) {
2064
- const {
2065
- // chatMessages: doChatMessages = true,
2066
- contacts: doContacts = true, groups: doGroups = true, preferences: doPreferences = true, overwriteContacts = true, } = options;
2067
- let invoiceCount = 0;
2160
+ const { stamps: doStamps = true, contacts: doContacts = true, groups: doGroups = true, preferences: doPreferences = true, overwriteContacts = true, } = options;
2161
+ let stampCount = 0;
2068
2162
  let contactCount = 0;
2069
2163
  let groupCount = 0;
2070
2164
  let defaultsRestored = false;
2071
2165
  let preferencesRestored = false;
2072
- // 1. Contacts first — chatMessages may reference them
2166
+ // 1. Contacts first
2073
2167
  if (doContacts) {
2074
2168
  for (const contact of snapshot.contacts) {
2075
2169
  const exists = !!this._contacts.getContact(contact.id);
@@ -2097,20 +2191,18 @@ export class MajikSignatureClient {
2097
2191
  groupCount++;
2098
2192
  }
2099
2193
  }
2100
- // 3. Chats
2101
- // if (doChatMessages) {
2102
- // await Promise.all(
2103
- // snapshot.chats.map((inv) => this._chatMessages.save(inv)),
2104
- // );
2105
- // invoiceCount = snapshot.chatMessages.length;
2106
- // }
2194
+ // 3. Stamps
2195
+ if (doStamps) {
2196
+ await Promise.all(snapshot.stamps.map((json) => this._stamps.save(MajikSignatureStamp.fromJSON(json))));
2197
+ stampCount = snapshot.stamps.length;
2198
+ }
2107
2199
  // 5. App preferences
2108
2200
  if (doPreferences && snapshot.preferences) {
2109
2201
  await this.setUserAppPreferences(snapshot.preferences);
2110
2202
  preferencesRestored = true;
2111
2203
  }
2112
2204
  const restoredData = {
2113
- chatMessages: invoiceCount,
2205
+ stamps: stampCount,
2114
2206
  contacts: contactCount,
2115
2207
  groups: groupCount,
2116
2208
  invoiceDefaults: defaultsRestored,
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.2.0",
5
+ "version": "0.3.0",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -54,6 +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
58
  "@majikah/majik-key": "^0.2.12",
58
59
  "@majikah/majik-signature": "^0.0.30",
59
60
  "@stablelib/ed25519": "^2.1.0",