@majikah/majik-signature-client 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/backup/constants.d.ts +4 -4
- package/dist/core/backup/constants.js +9 -2
- package/dist/core/backup/types.d.ts +2 -1
- package/dist/core/client-state-manager.js +5 -0
- package/dist/core/stamp/majik-signature-stamp-manager.d.ts +57 -0
- package/dist/core/stamp/majik-signature-stamp-manager.js +216 -0
- package/dist/core/stamp/majik-signature-stamp.d.ts +64 -0
- package/dist/core/stamp/majik-signature-stamp.js +131 -0
- package/dist/core/storage/client-state/_types.d.ts +8 -0
- package/dist/core/storage/index.d.ts +4 -0
- package/dist/core/storage/index.js +3 -0
- package/dist/core/storage/sql-schema.d.ts +2 -0
- package/dist/core/storage/sql-schema.js +27 -0
- package/dist/core/storage/stamps/_types.d.ts +3 -0
- package/dist/core/storage/stamps/_types.js +1 -0
- package/dist/core/storage/stamps/adapter-idb.d.ts +3 -0
- package/dist/core/storage/stamps/adapter-idb.js +5 -0
- package/dist/core/storage/stamps/adapter-memory.d.ts +14 -0
- package/dist/core/storage/stamps/adapter-memory.js +32 -0
- package/dist/core/storage/stamps/adapter-sql.d.ts +18 -0
- package/dist/core/storage/stamps/adapter-sql.js +99 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/majik-signature-client.d.ts +42 -5
- package/dist/majik-signature-client.js +162 -45
- package/package.json +3 -2
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* MJKC = MajiK Contacts
|
|
7
7
|
* MJKA = MajiK App-data
|
|
8
8
|
*/
|
|
9
|
-
export declare const
|
|
10
|
-
readonly
|
|
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
|
|
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
|
|
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
|
|
10
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type BatchLockResult, 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
|
+
import { MajikKey } from "@majikah/majik-key";
|
|
6
|
+
export declare class MajikSignatureStampManagerError extends Error {
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
constructor(message: string, cause?: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare class MajikSignatureStampManager {
|
|
11
|
+
private readonly _cache;
|
|
12
|
+
private _decryptErrors;
|
|
13
|
+
private _adapter;
|
|
14
|
+
constructor(adapter?: MajikSignatureStampStorageAdapter);
|
|
15
|
+
get adapter(): MajikSignatureStampStorageAdapter;
|
|
16
|
+
setAdapter(adapter: MajikSignatureStampStorageAdapter): void;
|
|
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;
|
|
36
|
+
toJSON(): Promise<MajikSignatureStampJSON[]>;
|
|
37
|
+
bulkRestoreFromJSON(data: MajikSignatureStampJSON[]): Promise<void>;
|
|
38
|
+
static fromJSON(data: MajikSignatureStampJSON[], adapter?: MajikSignatureStampStorageAdapter): Promise<MajikSignatureStampManager>;
|
|
39
|
+
private _persist;
|
|
40
|
+
create(options: CreateStampOptions): Promise<MajikSignatureStamp>;
|
|
41
|
+
save(stamp: MajikSignatureStamp): Promise<void>;
|
|
42
|
+
load(id: string): Promise<MajikSignatureStamp | null>;
|
|
43
|
+
getStampOrThrow(id: string): Promise<MajikSignatureStamp>;
|
|
44
|
+
loadAll(): Promise<MajikSignatureStamp[]>;
|
|
45
|
+
delete(id: string): Promise<void>;
|
|
46
|
+
has(id: string): Promise<boolean>;
|
|
47
|
+
get(id: string): MajikSignatureStamp | undefined;
|
|
48
|
+
list(): MajikSignatureStamp[];
|
|
49
|
+
listByKind(kind: StampAssetKind): MajikSignatureStamp[];
|
|
50
|
+
listByFingerprint(fingerprint: string): MajikSignatureStamp[];
|
|
51
|
+
query(query: StorageQuery<MajikSignatureStampJSON>): Promise<MajikSignatureStampJSON[]>;
|
|
52
|
+
rename(id: string, newName: string): Promise<MajikSignatureStamp>;
|
|
53
|
+
/** Write-once at the MajikFile layer — id is preserved explicitly. */
|
|
54
|
+
replaceContent(id: string, rawBytes: Uint8Array, identity: MajikFileIdentity, mimeType?: string): Promise<MajikSignatureStamp>;
|
|
55
|
+
decryptContent(id: string, identity: Pick<MajikFileIdentity, "fingerprint" | "mlKemSecretKey">): Promise<Uint8Array>;
|
|
56
|
+
bulkRemove(ids: string[]): Promise<void>;
|
|
57
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { MajikFile, } from "@majikah/majik-file";
|
|
2
|
+
import { MajikSignatureStamp, } from "./majik-signature-stamp";
|
|
3
|
+
import { InMemoryStampstoreAdapter, } from "../storage";
|
|
4
|
+
export class MajikSignatureStampManagerError extends Error {
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "MajikSignatureStampManagerError";
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class MajikSignatureStampManager {
|
|
13
|
+
_cache = new Map();
|
|
14
|
+
_decryptErrors = new Map();
|
|
15
|
+
_adapter;
|
|
16
|
+
constructor(adapter) {
|
|
17
|
+
this._adapter = adapter ?? new InMemoryStampstoreAdapter();
|
|
18
|
+
}
|
|
19
|
+
get adapter() {
|
|
20
|
+
return this._adapter;
|
|
21
|
+
}
|
|
22
|
+
setAdapter(adapter) {
|
|
23
|
+
this._adapter = adapter;
|
|
24
|
+
}
|
|
25
|
+
// ── Hydration ─────────────────────────────────────────────────────────
|
|
26
|
+
async hydrate() {
|
|
27
|
+
this._cache.clear();
|
|
28
|
+
const all = await this._adapter.list();
|
|
29
|
+
for (const json of all) {
|
|
30
|
+
try {
|
|
31
|
+
const stamp = MajikSignatureStamp.fromJSON(json);
|
|
32
|
+
this._cache.set(stamp.id, stamp);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
console.warn(`MajikSignatureStampManager.hydrate: skipping malformed stamp "${json?.id}":`, err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
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
|
+
}
|
|
94
|
+
// ── Serialization ─────────────────────────────────────────────────────
|
|
95
|
+
async toJSON() {
|
|
96
|
+
return this._adapter.list();
|
|
97
|
+
}
|
|
98
|
+
async bulkRestoreFromJSON(data) {
|
|
99
|
+
if (!Array.isArray(data)) {
|
|
100
|
+
throw new MajikSignatureStampManagerError("bulkRestoreFromJSON: expected MajikSignatureStampJSON[]");
|
|
101
|
+
}
|
|
102
|
+
await this._adapter.bulkSave(data);
|
|
103
|
+
await this.hydrate();
|
|
104
|
+
}
|
|
105
|
+
static async fromJSON(data, adapter) {
|
|
106
|
+
const manager = new MajikSignatureStampManager(adapter);
|
|
107
|
+
await manager.bulkRestoreFromJSON(data);
|
|
108
|
+
return manager;
|
|
109
|
+
}
|
|
110
|
+
async _persist(stamp) {
|
|
111
|
+
await this._adapter.save(stamp.toJSON());
|
|
112
|
+
}
|
|
113
|
+
// ── Core CRUD ───────────────────────────────────────────────────────────
|
|
114
|
+
async create(options) {
|
|
115
|
+
const stamp = await MajikSignatureStamp.create(options);
|
|
116
|
+
await this._persist(stamp);
|
|
117
|
+
this._cache.set(stamp.id, stamp);
|
|
118
|
+
return stamp;
|
|
119
|
+
}
|
|
120
|
+
async save(stamp) {
|
|
121
|
+
await this._persist(stamp);
|
|
122
|
+
this._cache.set(stamp.id, stamp);
|
|
123
|
+
}
|
|
124
|
+
async load(id) {
|
|
125
|
+
const cached = this._cache.get(id);
|
|
126
|
+
if (cached)
|
|
127
|
+
return cached;
|
|
128
|
+
const json = await this._adapter.getById(id);
|
|
129
|
+
if (!json)
|
|
130
|
+
return null;
|
|
131
|
+
const stamp = MajikSignatureStamp.fromJSON(json);
|
|
132
|
+
this._cache.set(stamp.id, stamp);
|
|
133
|
+
return stamp;
|
|
134
|
+
}
|
|
135
|
+
async getStampOrThrow(id) {
|
|
136
|
+
const stamp = await this.load(id);
|
|
137
|
+
if (!stamp)
|
|
138
|
+
throw new MajikSignatureStampManagerError(`Stamp not found: ${id}`);
|
|
139
|
+
return stamp;
|
|
140
|
+
}
|
|
141
|
+
async loadAll() {
|
|
142
|
+
const all = await this._adapter.list();
|
|
143
|
+
for (const json of all) {
|
|
144
|
+
if (!this._cache.has(json.id)) {
|
|
145
|
+
try {
|
|
146
|
+
const stamp = MajikSignatureStamp.fromJSON(json);
|
|
147
|
+
this._cache.set(stamp.id, stamp);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.warn(`MajikSignatureStampManager.loadAll: skipping malformed stamp "${json?.id}":`, err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return [...this._cache.values()];
|
|
155
|
+
}
|
|
156
|
+
async delete(id) {
|
|
157
|
+
await this._adapter.remove(id);
|
|
158
|
+
this._cache.delete(id);
|
|
159
|
+
}
|
|
160
|
+
async has(id) {
|
|
161
|
+
if (this._cache.has(id))
|
|
162
|
+
return true;
|
|
163
|
+
return this._adapter.exists(id);
|
|
164
|
+
}
|
|
165
|
+
get(id) {
|
|
166
|
+
return this._cache.get(id);
|
|
167
|
+
}
|
|
168
|
+
list() {
|
|
169
|
+
return [...this._cache.values()];
|
|
170
|
+
}
|
|
171
|
+
listByKind(kind) {
|
|
172
|
+
return this.list().filter((s) => s.kind === kind);
|
|
173
|
+
}
|
|
174
|
+
listByFingerprint(fingerprint) {
|
|
175
|
+
return this.list().filter((s) => s.fingerprint === fingerprint);
|
|
176
|
+
}
|
|
177
|
+
async query(query) {
|
|
178
|
+
if (!this._adapter.query) {
|
|
179
|
+
throw new MajikSignatureStampManagerError("query: current adapter does not support query()");
|
|
180
|
+
}
|
|
181
|
+
return this._adapter.query(query);
|
|
182
|
+
}
|
|
183
|
+
// ── Mutations ───────────────────────────────────────────────────────────
|
|
184
|
+
async rename(id, newName) {
|
|
185
|
+
const stamp = await this.getStampOrThrow(id);
|
|
186
|
+
stamp.rename(newName);
|
|
187
|
+
await this._persist(stamp);
|
|
188
|
+
return stamp;
|
|
189
|
+
}
|
|
190
|
+
/** Write-once at the MajikFile layer — id is preserved explicitly. */
|
|
191
|
+
async replaceContent(id, rawBytes, identity, mimeType) {
|
|
192
|
+
const existing = await this.getStampOrThrow(id);
|
|
193
|
+
const replacement = await MajikSignatureStamp.create({
|
|
194
|
+
id: existing.id,
|
|
195
|
+
data: rawBytes,
|
|
196
|
+
identity,
|
|
197
|
+
kind: existing.kind,
|
|
198
|
+
name: existing.name,
|
|
199
|
+
mimeType: mimeType ?? existing.mimeType ?? undefined,
|
|
200
|
+
});
|
|
201
|
+
await this._persist(replacement);
|
|
202
|
+
this._cache.set(replacement.id, replacement);
|
|
203
|
+
return replacement;
|
|
204
|
+
}
|
|
205
|
+
async decryptContent(id, identity) {
|
|
206
|
+
const stamp = await this.getStampOrThrow(id);
|
|
207
|
+
return stamp.decryptContent(identity);
|
|
208
|
+
}
|
|
209
|
+
async bulkRemove(ids) {
|
|
210
|
+
if (ids.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
await this._adapter.bulkRemove(ids);
|
|
213
|
+
for (const id of ids)
|
|
214
|
+
this._cache.delete(id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MajikFile } from "@majikah/majik-file";
|
|
2
|
+
import type { MajikFileDecryptIdentity, MajikFileIdentity, MajikFileJSON } from "@majikah/majik-file";
|
|
3
|
+
export type StampAssetKind = "image" | "audio" | "video" | "text";
|
|
4
|
+
export interface MajikSignatureStampJSON {
|
|
5
|
+
id: string;
|
|
6
|
+
kind: StampAssetKind;
|
|
7
|
+
name: string;
|
|
8
|
+
mimeType: string | null;
|
|
9
|
+
sizeBytes: number;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
data: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
lastUpdate: string;
|
|
14
|
+
/** Full MajikFile metadata — required to reconstruct a working MajikFile
|
|
15
|
+
* instance via MajikFile.fromJSON() for decryptHydrate()/secureLock(). */
|
|
16
|
+
fileJson: MajikFileJSON;
|
|
17
|
+
}
|
|
18
|
+
export interface CreateStampOptions {
|
|
19
|
+
data: Uint8Array | ArrayBuffer;
|
|
20
|
+
identity: MajikFileIdentity;
|
|
21
|
+
kind: StampAssetKind;
|
|
22
|
+
name: string;
|
|
23
|
+
mimeType?: string;
|
|
24
|
+
/** Pass the existing id back in when replacing content on a stamp that
|
|
25
|
+
* already exists — MajikFile is write-once, so this is a *new* encrypted
|
|
26
|
+
* artifact under the hood, but the id stays stable for the caller. */
|
|
27
|
+
id?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare class MajikSignatureStamp {
|
|
30
|
+
private readonly _id;
|
|
31
|
+
private readonly _kind;
|
|
32
|
+
private _name;
|
|
33
|
+
private readonly _mimeType;
|
|
34
|
+
private readonly _sizeBytes;
|
|
35
|
+
private readonly _fingerprint;
|
|
36
|
+
private readonly _timestamp;
|
|
37
|
+
private readonly _fileJson;
|
|
38
|
+
private _file;
|
|
39
|
+
private _lastUpdate;
|
|
40
|
+
private _encryptedBinary;
|
|
41
|
+
private constructor();
|
|
42
|
+
get id(): string;
|
|
43
|
+
get kind(): StampAssetKind;
|
|
44
|
+
get name(): string;
|
|
45
|
+
get mimeType(): string | null;
|
|
46
|
+
get sizeBytes(): number;
|
|
47
|
+
get fingerprint(): string;
|
|
48
|
+
get timestamp(): string;
|
|
49
|
+
get lastUpdate(): string;
|
|
50
|
+
static create(options: CreateStampOptions): Promise<MajikSignatureStamp>;
|
|
51
|
+
/** Plaintext-only rename — no re-encryption, no MajikFile involvement. */
|
|
52
|
+
rename(newName: string): void;
|
|
53
|
+
toJSON(): MajikSignatureStampJSON;
|
|
54
|
+
static fromJSON(json: MajikSignatureStampJSON): MajikSignatureStamp;
|
|
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>;
|
|
64
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
_fileJson;
|
|
13
|
+
_file = null; // reconstructed lazily, cached per instance
|
|
14
|
+
_lastUpdate;
|
|
15
|
+
_encryptedBinary; // the .mjkb bytes — never the plaintext
|
|
16
|
+
constructor(json, binary) {
|
|
17
|
+
this._id = json.id;
|
|
18
|
+
this._kind = json.kind;
|
|
19
|
+
this._name = json.name;
|
|
20
|
+
this._mimeType = json.mimeType;
|
|
21
|
+
this._sizeBytes = json.sizeBytes;
|
|
22
|
+
this._fingerprint = json.fingerprint;
|
|
23
|
+
this._timestamp = json.timestamp;
|
|
24
|
+
this._lastUpdate = json.lastUpdate;
|
|
25
|
+
this._encryptedBinary = binary;
|
|
26
|
+
this._fileJson = json.fileJson;
|
|
27
|
+
}
|
|
28
|
+
get id() {
|
|
29
|
+
return this._id;
|
|
30
|
+
}
|
|
31
|
+
get kind() {
|
|
32
|
+
return this._kind;
|
|
33
|
+
}
|
|
34
|
+
get name() {
|
|
35
|
+
return this._name;
|
|
36
|
+
}
|
|
37
|
+
get mimeType() {
|
|
38
|
+
return this._mimeType;
|
|
39
|
+
}
|
|
40
|
+
get sizeBytes() {
|
|
41
|
+
return this._sizeBytes;
|
|
42
|
+
}
|
|
43
|
+
get fingerprint() {
|
|
44
|
+
return this._fingerprint;
|
|
45
|
+
}
|
|
46
|
+
get timestamp() {
|
|
47
|
+
return this._timestamp;
|
|
48
|
+
}
|
|
49
|
+
get lastUpdate() {
|
|
50
|
+
return this._lastUpdate;
|
|
51
|
+
}
|
|
52
|
+
// ── CREATE ──────────────────────────────────────────────────────────────
|
|
53
|
+
static async create(options) {
|
|
54
|
+
const { data, identity, kind, name, mimeType, id } = options;
|
|
55
|
+
if (!data)
|
|
56
|
+
throw new Error("MajikSignatureStamp.create: data is required");
|
|
57
|
+
if (!identity)
|
|
58
|
+
throw new Error("MajikSignatureStamp.create: identity is required");
|
|
59
|
+
if (!name?.trim())
|
|
60
|
+
throw new Error("MajikSignatureStamp.create: name is required");
|
|
61
|
+
const file = await MajikFile.create({
|
|
62
|
+
id,
|
|
63
|
+
data,
|
|
64
|
+
identity,
|
|
65
|
+
context: "user_upload",
|
|
66
|
+
originalName: name,
|
|
67
|
+
mimeType,
|
|
68
|
+
userId: identity.fingerprint,
|
|
69
|
+
});
|
|
70
|
+
const fileJson = file.toJSON();
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
const binary = file.toBinaryBytes();
|
|
73
|
+
const json = {
|
|
74
|
+
id: fileJson.id,
|
|
75
|
+
kind,
|
|
76
|
+
name,
|
|
77
|
+
mimeType: fileJson.mime_type,
|
|
78
|
+
sizeBytes: fileJson.size_original,
|
|
79
|
+
fingerprint: identity.fingerprint,
|
|
80
|
+
data: arrayToBase64(binary),
|
|
81
|
+
timestamp: fileJson.timestamp ?? now,
|
|
82
|
+
lastUpdate: fileJson.last_update ?? now,
|
|
83
|
+
fileJson,
|
|
84
|
+
};
|
|
85
|
+
const instance = new MajikSignatureStamp(json, binary);
|
|
86
|
+
instance._file = file; // reuse — already fully constructed, no need to refetch
|
|
87
|
+
return instance;
|
|
88
|
+
}
|
|
89
|
+
/** Plaintext-only rename — no re-encryption, no MajikFile involvement. */
|
|
90
|
+
rename(newName) {
|
|
91
|
+
if (!newName?.trim())
|
|
92
|
+
throw new Error("rename: name must be non-empty");
|
|
93
|
+
this._name = newName.trim();
|
|
94
|
+
this._lastUpdate = new Date().toISOString();
|
|
95
|
+
}
|
|
96
|
+
toJSON() {
|
|
97
|
+
return {
|
|
98
|
+
id: this._id,
|
|
99
|
+
kind: this._kind,
|
|
100
|
+
name: this._name,
|
|
101
|
+
mimeType: this._mimeType,
|
|
102
|
+
sizeBytes: this._sizeBytes,
|
|
103
|
+
fingerprint: this._fingerprint,
|
|
104
|
+
data: arrayToBase64(this._encryptedBinary),
|
|
105
|
+
timestamp: this._timestamp,
|
|
106
|
+
lastUpdate: this._lastUpdate,
|
|
107
|
+
fileJson: this._fileJson,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
static fromJSON(json) {
|
|
111
|
+
return new MajikSignatureStamp(json, base64ToUint8Array(json.data));
|
|
112
|
+
}
|
|
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);
|
|
117
|
+
}
|
|
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;
|
|
125
|
+
}
|
|
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);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|