@majikah/majik-universal-id-client 0.0.12 → 0.0.14
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/client-state-manager.d.ts +105 -0
- package/dist/core/client-state-manager.js +250 -0
- package/dist/core/contacts/errors.d.ts +12 -0
- package/dist/core/contacts/errors.js +27 -0
- package/dist/core/contacts/majik-contact-directory.d.ts +6 -8
- package/dist/core/contacts/majik-contact-directory.js +18 -12
- package/dist/core/contacts/majik-contact-groups.d.ts +186 -0
- package/dist/core/contacts/majik-contact-groups.js +563 -0
- package/dist/core/contacts/majik-contact-manager.d.ts +150 -0
- package/dist/core/contacts/majik-contact-manager.js +627 -0
- package/dist/core/contacts/majik-contact-migration.d.ts +27 -0
- package/dist/core/contacts/majik-contact-migration.js +84 -0
- package/dist/core/contacts/types.d.ts +12 -0
- package/dist/core/contacts/types.js +4 -0
- package/dist/core/crypto/keystore-manager.d.ts +166 -0
- package/dist/core/crypto/keystore-manager.js +371 -0
- package/dist/core/storage/client-state/_types.d.ts +37 -0
- package/dist/core/storage/client-state/_types.js +16 -0
- package/dist/core/storage/client-state/adapter-idb.d.ts +17 -0
- package/dist/core/storage/client-state/adapter-idb.js +19 -0
- package/dist/core/storage/client-state/adapter-memory.d.ts +20 -0
- package/dist/core/storage/client-state/adapter-memory.js +44 -0
- package/dist/core/storage/client-state/adapter-sql.d.ts +41 -0
- package/dist/core/storage/client-state/adapter-sql.js +104 -0
- package/dist/core/storage/contact-directory/contacts/_types.d.ts +3 -0
- package/dist/core/storage/contact-directory/contacts/_types.js +1 -0
- package/dist/core/storage/contact-directory/contacts/adapter-idb.d.ts +3 -0
- package/dist/core/storage/contact-directory/contacts/adapter-idb.js +5 -0
- package/dist/core/storage/contact-directory/contacts/adapter-memory.d.ts +14 -0
- package/dist/core/storage/contact-directory/contacts/adapter-memory.js +32 -0
- package/dist/core/storage/contact-directory/contacts/adapter-sql.d.ts +18 -0
- package/dist/core/storage/contact-directory/contacts/adapter-sql.js +97 -0
- package/dist/core/storage/contact-directory/groups/_types.d.ts +3 -0
- package/dist/core/storage/contact-directory/groups/_types.js +1 -0
- package/dist/core/storage/contact-directory/groups/adapter-idb.d.ts +3 -0
- package/dist/core/storage/contact-directory/groups/adapter-idb.js +5 -0
- package/dist/core/storage/contact-directory/groups/adapter-memory.d.ts +14 -0
- package/dist/core/storage/contact-directory/groups/adapter-memory.js +32 -0
- package/dist/core/storage/contact-directory/groups/adapter-sql.d.ts +16 -0
- package/dist/core/storage/contact-directory/groups/adapter-sql.js +72 -0
- package/dist/core/storage/idb-adapter.d.ts +21 -0
- package/dist/core/storage/idb-adapter.js +107 -0
- package/dist/core/storage/index.d.ts +20 -0
- package/dist/core/storage/index.js +16 -0
- package/dist/core/storage/keystore/_types.d.ts +3 -0
- package/dist/core/storage/keystore/_types.js +1 -0
- package/dist/core/storage/keystore/adapter-idb.d.ts +3 -0
- package/dist/core/storage/keystore/adapter-idb.js +5 -0
- package/dist/core/storage/keystore/adapter-memory.d.ts +14 -0
- package/dist/core/storage/keystore/adapter-memory.js +32 -0
- package/dist/core/storage/keystore/adapter-sql.d.ts +18 -0
- package/dist/core/storage/keystore/adapter-sql.js +93 -0
- package/dist/core/storage/sql-db-manager.d.ts +13 -0
- package/dist/core/storage/sql-db-manager.js +59 -0
- package/dist/core/storage/sql-schema.d.ts +20 -0
- package/dist/core/storage/sql-schema.js +84 -0
- package/dist/core/storage/storage-adapter.d.ts +22 -0
- package/dist/core/storage/storage-adapter.js +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/majik-universal-id-client.d.ts +135 -97
- package/dist/majik-universal-id-client.js +462 -431
- package/package.json +4 -5
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file client-state-manager.ts
|
|
3
|
+
* @description ClientStateManager — typed read/write interface over a
|
|
4
|
+
* pluggable ClientStateStorageAdapter.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Async get / set / remove for each well-known client-state key
|
|
8
|
+
* - In-memory cache in front of the adapter (warm via hydrate())
|
|
9
|
+
* - Typed accessors for `accountOrder` and `invoiceDefaults` so callers
|
|
10
|
+
* never touch raw JSON strings
|
|
11
|
+
* - Generic `get` / `set` escape hatch for any future keys
|
|
12
|
+
* - Adapter can be swapped at runtime via setAdapter()
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* const stateManager = new ClientStateManager(new IDBClientStateAdapter());
|
|
17
|
+
* await stateManager.hydrate();
|
|
18
|
+
*
|
|
19
|
+
* await stateManager.setAccountOrder(["id1", "id2"]);
|
|
20
|
+
* const order = await stateManager.getAccountOrder(); // ["id1", "id2"]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* MajikBuwizClient owns this instance and calls hydrate() during its own
|
|
24
|
+
* hydrate() pass. Callers should never need to hydrate() again unless the
|
|
25
|
+
* adapter is swapped.
|
|
26
|
+
*/
|
|
27
|
+
import { AccountOrderValue, ClientStateEntry, ClientStateStorageAdapter } from "./storage/client-state/_types";
|
|
28
|
+
export declare class ClientStateManager {
|
|
29
|
+
/** In-memory cache — warmed by hydrate(), kept in sync on every write. */
|
|
30
|
+
private _cache;
|
|
31
|
+
private _adapter;
|
|
32
|
+
/**
|
|
33
|
+
* @param adapter Defaults to InMemoryClientStateAdapter (non-persistent).
|
|
34
|
+
* Pass IDBClientStateAdapter or SQLiteClientStateAdapter for persistence.
|
|
35
|
+
*/
|
|
36
|
+
constructor(adapter?: ClientStateStorageAdapter);
|
|
37
|
+
get adapter(): ClientStateStorageAdapter;
|
|
38
|
+
/**
|
|
39
|
+
* Swap the storage adapter at runtime.
|
|
40
|
+
*
|
|
41
|
+
* Does NOT migrate data. To migrate:
|
|
42
|
+
* ```ts
|
|
43
|
+
* const entries = stateManager.listCachedEntries();
|
|
44
|
+
* stateManager.setAdapter(newAdapter);
|
|
45
|
+
* await stateManager.hydrate();
|
|
46
|
+
* await stateManager.bulkSet(entries);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
setAdapter(adapter: ClientStateStorageAdapter): void;
|
|
50
|
+
/**
|
|
51
|
+
* Load all entries from the adapter into the in-memory cache.
|
|
52
|
+
* Call once after construction (MajikBuwizClient.hydrate() does this).
|
|
53
|
+
*/
|
|
54
|
+
hydrate(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Retrieve a raw JSON string for any key. Returns `null` if not found.
|
|
57
|
+
* Prefer the typed accessors (getAccountOrder, getInvoiceDefaults) over this.
|
|
58
|
+
*/
|
|
59
|
+
get(id: string): Promise<string | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Persist a raw JSON string for any key.
|
|
62
|
+
* Prefer the typed accessors over this.
|
|
63
|
+
*/
|
|
64
|
+
set(id: string, value: string): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Remove a single key.
|
|
67
|
+
*/
|
|
68
|
+
remove(id: string): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* Remove all stored state.
|
|
71
|
+
*/
|
|
72
|
+
clear(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Whether a key exists in the cache.
|
|
75
|
+
* Accurate after hydrate(); use exists() for an authoritative adapter check.
|
|
76
|
+
*/
|
|
77
|
+
hasCached(id: string): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Authoritative existence check against the adapter.
|
|
80
|
+
*/
|
|
81
|
+
exists(id: string): Promise<boolean>;
|
|
82
|
+
/**
|
|
83
|
+
* Snapshot of all cached entries — useful for adapter migration.
|
|
84
|
+
*/
|
|
85
|
+
listCachedEntries(): ClientStateEntry[];
|
|
86
|
+
/**
|
|
87
|
+
* Persist multiple entries in one adapter call.
|
|
88
|
+
*/
|
|
89
|
+
bulkSet(entries: ClientStateEntry[]): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Retrieve the ordered list of own account IDs.
|
|
92
|
+
* Returns `null` if no order has been persisted yet.
|
|
93
|
+
*/
|
|
94
|
+
getAccountOrder(): Promise<AccountOrderValue | null>;
|
|
95
|
+
/**
|
|
96
|
+
* Persist the ordered list of own account IDs.
|
|
97
|
+
*/
|
|
98
|
+
setAccountOrder(order: AccountOrderValue): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Remove the persisted account order (resets to insertion order on next
|
|
101
|
+
* hydrate).
|
|
102
|
+
*/
|
|
103
|
+
removeAccountOrder(): Promise<void>;
|
|
104
|
+
count(): Promise<number>;
|
|
105
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file client-state-manager.ts
|
|
3
|
+
* @description ClientStateManager — typed read/write interface over a
|
|
4
|
+
* pluggable ClientStateStorageAdapter.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* - Async get / set / remove for each well-known client-state key
|
|
8
|
+
* - In-memory cache in front of the adapter (warm via hydrate())
|
|
9
|
+
* - Typed accessors for `accountOrder` and `invoiceDefaults` so callers
|
|
10
|
+
* never touch raw JSON strings
|
|
11
|
+
* - Generic `get` / `set` escape hatch for any future keys
|
|
12
|
+
* - Adapter can be swapped at runtime via setAdapter()
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* const stateManager = new ClientStateManager(new IDBClientStateAdapter());
|
|
17
|
+
* await stateManager.hydrate();
|
|
18
|
+
*
|
|
19
|
+
* await stateManager.setAccountOrder(["id1", "id2"]);
|
|
20
|
+
* const order = await stateManager.getAccountOrder(); // ["id1", "id2"]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* MajikBuwizClient owns this instance and calls hydrate() during its own
|
|
24
|
+
* hydrate() pass. Callers should never need to hydrate() again unless the
|
|
25
|
+
* adapter is swapped.
|
|
26
|
+
*/
|
|
27
|
+
import { CLIENT_STATE_KEYS, } from "./storage/client-state/_types";
|
|
28
|
+
import { InMemoryClientStateAdapter } from "./storage/client-state/adapter-memory";
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// ClientStateManager
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
export class ClientStateManager {
|
|
33
|
+
/** In-memory cache — warmed by hydrate(), kept in sync on every write. */
|
|
34
|
+
_cache = new Map();
|
|
35
|
+
_adapter;
|
|
36
|
+
/**
|
|
37
|
+
* @param adapter Defaults to InMemoryClientStateAdapter (non-persistent).
|
|
38
|
+
* Pass IDBClientStateAdapter or SQLiteClientStateAdapter for persistence.
|
|
39
|
+
*/
|
|
40
|
+
constructor(adapter = new InMemoryClientStateAdapter()) {
|
|
41
|
+
this._adapter = adapter;
|
|
42
|
+
}
|
|
43
|
+
// ── Adapter management ────────────────────────────────────────────────────
|
|
44
|
+
get adapter() {
|
|
45
|
+
return this._adapter;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Swap the storage adapter at runtime.
|
|
49
|
+
*
|
|
50
|
+
* Does NOT migrate data. To migrate:
|
|
51
|
+
* ```ts
|
|
52
|
+
* const entries = stateManager.listCachedEntries();
|
|
53
|
+
* stateManager.setAdapter(newAdapter);
|
|
54
|
+
* await stateManager.hydrate();
|
|
55
|
+
* await stateManager.bulkSet(entries);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
setAdapter(adapter) {
|
|
59
|
+
this._adapter = adapter;
|
|
60
|
+
}
|
|
61
|
+
// ── Hydration ─────────────────────────────────────────────────────────────
|
|
62
|
+
/**
|
|
63
|
+
* Load all entries from the adapter into the in-memory cache.
|
|
64
|
+
* Call once after construction (MajikBuwizClient.hydrate() does this).
|
|
65
|
+
*/
|
|
66
|
+
async hydrate() {
|
|
67
|
+
const entries = await this._adapter.list();
|
|
68
|
+
this._cache.clear();
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
this._cache.set(entry.id, entry.value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ── Generic typed get / set / remove ─────────────────────────────────────
|
|
74
|
+
/**
|
|
75
|
+
* Retrieve a raw JSON string for any key. Returns `null` if not found.
|
|
76
|
+
* Prefer the typed accessors (getAccountOrder, getInvoiceDefaults) over this.
|
|
77
|
+
*/
|
|
78
|
+
async get(id) {
|
|
79
|
+
const cached = this._cache.get(id);
|
|
80
|
+
if (cached !== undefined)
|
|
81
|
+
return cached;
|
|
82
|
+
// Cache miss — should not happen after hydrate() but defensive
|
|
83
|
+
const entry = await this._adapter.getById(id);
|
|
84
|
+
if (!entry)
|
|
85
|
+
return null;
|
|
86
|
+
this._cache.set(id, entry.value);
|
|
87
|
+
return entry.value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Persist a raw JSON string for any key.
|
|
91
|
+
* Prefer the typed accessors over this.
|
|
92
|
+
*/
|
|
93
|
+
async set(id, value) {
|
|
94
|
+
const entry = { id, value };
|
|
95
|
+
await this._adapter.save(entry);
|
|
96
|
+
this._cache.set(id, value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Remove a single key.
|
|
100
|
+
*/
|
|
101
|
+
async remove(id) {
|
|
102
|
+
const removed = await this._adapter.remove(id);
|
|
103
|
+
this._cache.delete(id);
|
|
104
|
+
return removed;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Remove all stored state.
|
|
108
|
+
*/
|
|
109
|
+
async clear() {
|
|
110
|
+
await this._adapter.clear();
|
|
111
|
+
this._cache.clear();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Whether a key exists in the cache.
|
|
115
|
+
* Accurate after hydrate(); use exists() for an authoritative adapter check.
|
|
116
|
+
*/
|
|
117
|
+
hasCached(id) {
|
|
118
|
+
return this._cache.has(id);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Authoritative existence check against the adapter.
|
|
122
|
+
*/
|
|
123
|
+
async exists(id) {
|
|
124
|
+
if (this._cache.has(id))
|
|
125
|
+
return true;
|
|
126
|
+
return this._adapter.exists(id);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Snapshot of all cached entries — useful for adapter migration.
|
|
130
|
+
*/
|
|
131
|
+
listCachedEntries() {
|
|
132
|
+
return Array.from(this._cache.entries()).map(([id, value]) => ({
|
|
133
|
+
id,
|
|
134
|
+
value,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Persist multiple entries in one adapter call.
|
|
139
|
+
*/
|
|
140
|
+
async bulkSet(entries) {
|
|
141
|
+
if (entries.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
await this._adapter.bulkSave(entries);
|
|
144
|
+
for (const e of entries)
|
|
145
|
+
this._cache.set(e.id, e.value);
|
|
146
|
+
}
|
|
147
|
+
// ── Typed: account order ──────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Retrieve the ordered list of own account IDs.
|
|
150
|
+
* Returns `null` if no order has been persisted yet.
|
|
151
|
+
*/
|
|
152
|
+
async getAccountOrder() {
|
|
153
|
+
const raw = await this.get(CLIENT_STATE_KEYS.ACCOUNT_ORDER);
|
|
154
|
+
if (raw === null)
|
|
155
|
+
return null;
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(raw);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
console.warn("ClientStateManager: malformed account order — discarding.");
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Persist the ordered list of own account IDs.
|
|
166
|
+
*/
|
|
167
|
+
async setAccountOrder(order) {
|
|
168
|
+
await this.set(CLIENT_STATE_KEYS.ACCOUNT_ORDER, JSON.stringify(order));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Remove the persisted account order (resets to insertion order on next
|
|
172
|
+
* hydrate).
|
|
173
|
+
*/
|
|
174
|
+
async removeAccountOrder() {
|
|
175
|
+
await this.remove(CLIENT_STATE_KEYS.ACCOUNT_ORDER);
|
|
176
|
+
}
|
|
177
|
+
// ── Typed: invoice defaults ───────────────────────────────────────────────
|
|
178
|
+
// /**
|
|
179
|
+
// * Retrieve user-configured invoice defaults.
|
|
180
|
+
// * Returns `null` if none have been saved yet.
|
|
181
|
+
// */
|
|
182
|
+
// async getInvoiceDefaults(): Promise<InvoiceDefaults | null> {
|
|
183
|
+
// const raw = await this.get(CLIENT_STATE_KEYS.INVOICE_DEFAULTS);
|
|
184
|
+
// if (raw === null) return null;
|
|
185
|
+
// try {
|
|
186
|
+
// return JSON.parse(raw) as InvoiceDefaults;
|
|
187
|
+
// } catch {
|
|
188
|
+
// console.warn(
|
|
189
|
+
// "ClientStateManager: malformed invoice defaults — discarding.",
|
|
190
|
+
// );
|
|
191
|
+
// return null;
|
|
192
|
+
// }
|
|
193
|
+
// }
|
|
194
|
+
// /**
|
|
195
|
+
// * Persist user-configured invoice defaults.
|
|
196
|
+
// */
|
|
197
|
+
// async setInvoiceDefaults(defaults: InvoiceDefaults): Promise<void> {
|
|
198
|
+
// await this.set(
|
|
199
|
+
// CLIENT_STATE_KEYS.INVOICE_DEFAULTS,
|
|
200
|
+
// JSON.stringify(defaults),
|
|
201
|
+
// );
|
|
202
|
+
// }
|
|
203
|
+
// /**
|
|
204
|
+
// * Remove the persisted invoice defaults.
|
|
205
|
+
// */
|
|
206
|
+
// async removeInvoiceDefaults(): Promise<void> {
|
|
207
|
+
// await this.remove(CLIENT_STATE_KEYS.INVOICE_DEFAULTS);
|
|
208
|
+
// }
|
|
209
|
+
// async currentInvoiceNumber(): Promise<number> {
|
|
210
|
+
// const current = await this.getInvoiceDefaults();
|
|
211
|
+
// const counter = current?.invoiceNumberCounter ?? 0;
|
|
212
|
+
// return counter;
|
|
213
|
+
// }
|
|
214
|
+
// async incrementInvoiceNumber(): Promise<number> {
|
|
215
|
+
// // 1. Get current defaults
|
|
216
|
+
// const current = await this.getInvoiceDefaults();
|
|
217
|
+
// // 2. Initialize safely if missing
|
|
218
|
+
// const counter = current?.invoiceNumberCounter ?? 0;
|
|
219
|
+
// const updated: InvoiceDefaults = {
|
|
220
|
+
// ...(current ?? {
|
|
221
|
+
// currency: "PHP" as any, // fallback — adjust if you have a real default
|
|
222
|
+
// }),
|
|
223
|
+
// invoiceNumberCounter: counter + 1,
|
|
224
|
+
// };
|
|
225
|
+
// // 3. Persist
|
|
226
|
+
// await this.setInvoiceDefaults(updated);
|
|
227
|
+
// // 4. Return the new value (useful for generating invoice number)
|
|
228
|
+
// return updated.invoiceNumberCounter!;
|
|
229
|
+
// }
|
|
230
|
+
// async decrementInvoiceNumber(): Promise<number> {
|
|
231
|
+
// // 1. Get current defaults
|
|
232
|
+
// const current = await this.getInvoiceDefaults();
|
|
233
|
+
// // 2. Initialize safely if missing
|
|
234
|
+
// const counter = current?.invoiceNumberCounter ?? 0;
|
|
235
|
+
// const updated: InvoiceDefaults = {
|
|
236
|
+
// ...(current ?? {
|
|
237
|
+
// currency: "PHP" as any, // fallback — adjust if you have a real default
|
|
238
|
+
// }),
|
|
239
|
+
// invoiceNumberCounter: counter - 1,
|
|
240
|
+
// };
|
|
241
|
+
// // 3. Persist
|
|
242
|
+
// await this.setInvoiceDefaults(updated);
|
|
243
|
+
// // 4. Return the new value (useful for generating invoice number)
|
|
244
|
+
// return updated.invoiceNumberCounter!;
|
|
245
|
+
// }
|
|
246
|
+
// ── Async count ───────────────────────────────────────────────────────────
|
|
247
|
+
async count() {
|
|
248
|
+
return this._adapter.count();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class MajikContactManagerError extends Error {
|
|
2
|
+
cause?: unknown;
|
|
3
|
+
constructor(message: string, cause?: unknown);
|
|
4
|
+
}
|
|
5
|
+
export declare class MajikContactDirectoryError extends Error {
|
|
6
|
+
cause?: unknown;
|
|
7
|
+
constructor(message: string, cause?: unknown);
|
|
8
|
+
}
|
|
9
|
+
export declare class MajikContactGroupManagerError extends Error {
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
constructor(message: string, cause?: unknown);
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* -------------------------------
|
|
2
|
+
* Errors
|
|
3
|
+
* ------------------------------- */
|
|
4
|
+
export class MajikContactManagerError extends Error {
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "MajikContactManagerError";
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class MajikContactDirectoryError extends Error {
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "MajikContactDirectoryError";
|
|
17
|
+
this.cause = cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class MajikContactGroupManagerError extends Error {
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "MajikContactGroupManagerError";
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { MAJIK_API_RESPONSE } from "../types";
|
|
2
|
-
import { MajikContact, MajikContactData
|
|
3
|
-
|
|
4
|
-
contacts: SerializedMajikContact[];
|
|
5
|
-
}
|
|
6
|
-
export declare class MajikContactDirectoryError extends Error {
|
|
7
|
-
cause?: unknown;
|
|
8
|
-
constructor(message: string, cause?: unknown);
|
|
9
|
-
}
|
|
2
|
+
import { MajikContact, MajikContactData } from "@majikah/majik-contact";
|
|
3
|
+
import { MajikContactDirectoryData } from "./types";
|
|
10
4
|
export declare class MajikContactDirectory {
|
|
11
5
|
private contacts;
|
|
12
6
|
private fingerprintMap;
|
|
@@ -27,6 +21,10 @@ export declare class MajikContactDirectory {
|
|
|
27
21
|
blockContact(id: string): MajikContact;
|
|
28
22
|
unblockContact(id: string): MajikContact;
|
|
29
23
|
hasContact(id: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a contact exists by their public key (base64)
|
|
26
|
+
*/
|
|
27
|
+
hasContactByPublicKeyBase64(publicKeyBase64: string): Promise<boolean>;
|
|
30
28
|
clear(): this;
|
|
31
29
|
setMajikahStatus(id: string, status: boolean): MajikContact;
|
|
32
30
|
isMajikahIdentityChecked(id: string): boolean;
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
import { KEY_ALGO } from "../crypto/constants";
|
|
2
2
|
import { base64ToArrayBuffer } from "../utils/utilities";
|
|
3
3
|
import { MajikContact, } from "@majikah/majik-contact";
|
|
4
|
-
|
|
5
|
-
* Errors
|
|
6
|
-
* ------------------------------- */
|
|
7
|
-
export class MajikContactDirectoryError extends Error {
|
|
8
|
-
cause;
|
|
9
|
-
constructor(message, cause) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "MajikContactDirectoryError";
|
|
12
|
-
this.cause = cause;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
4
|
+
import { MajikContactDirectoryError } from "./errors";
|
|
15
5
|
/* -------------------------------
|
|
16
6
|
* MajikContactDirectory Class
|
|
17
7
|
* ------------------------------- */
|
|
@@ -27,6 +17,12 @@ export class MajikContactDirectory {
|
|
|
27
17
|
* Contact Management
|
|
28
18
|
* ================================ */
|
|
29
19
|
addContact(contact) {
|
|
20
|
+
if (!contact?.id ||
|
|
21
|
+
!contact?.publicKey ||
|
|
22
|
+
!contact?.fingerprint ||
|
|
23
|
+
!contact?.mlKey) {
|
|
24
|
+
throw new MajikContactDirectoryError("Invalid contact");
|
|
25
|
+
}
|
|
30
26
|
if (!(contact instanceof MajikContact)) {
|
|
31
27
|
throw new MajikContactDirectoryError("Invalid contact instance");
|
|
32
28
|
}
|
|
@@ -125,6 +121,16 @@ export class MajikContactDirectory {
|
|
|
125
121
|
hasContact(id) {
|
|
126
122
|
return this.contacts.has(id);
|
|
127
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Checks if a contact exists by their public key (base64)
|
|
126
|
+
*/
|
|
127
|
+
async hasContactByPublicKeyBase64(publicKeyBase64) {
|
|
128
|
+
if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
|
|
129
|
+
throw new MajikContactDirectoryError("Public key must be a non-empty base64 string");
|
|
130
|
+
}
|
|
131
|
+
const contact = await this.getContactByPublicKeyBase64(publicKeyBase64);
|
|
132
|
+
return contact !== undefined;
|
|
133
|
+
}
|
|
128
134
|
clear() {
|
|
129
135
|
this.contacts.clear();
|
|
130
136
|
this.fingerprintMap.clear();
|
|
@@ -174,7 +180,7 @@ export class MajikContactDirectory {
|
|
|
174
180
|
// Fallback: create a raw-key wrapper when the browser does not support the namedCurve
|
|
175
181
|
publicKey = { raw: new Uint8Array(raw) };
|
|
176
182
|
}
|
|
177
|
-
const contact = MajikContact.create(item.id, publicKey, item.mlKey, item.fingerprint, item.meta);
|
|
183
|
+
const contact = MajikContact.create(item.id, publicKey, item.mlKey, item.fingerprint, item.meta, item.edPublicKeyBase64, item.mlDsaPublicKeyBase64);
|
|
178
184
|
this.contacts.set(contact.id, contact);
|
|
179
185
|
this.fingerprintMap.set(contact.fingerprint, contact.id);
|
|
180
186
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { MajikContact, MajikContactGroup, MajikContactGroupMeta } from "@majikah/majik-contact";
|
|
2
|
+
import { MajikContactDirectory } from "./majik-contact-directory";
|
|
3
|
+
import { MAJIK_API_RESPONSE } from "../types";
|
|
4
|
+
import { MajikContactGroupManagerData } from "./types";
|
|
5
|
+
/**
|
|
6
|
+
* Manages the full lifecycle of MajikContactGroup instances.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Owns the canonical Map of all groups (user-created + system groups)
|
|
10
|
+
* - Maintains a reverse index: contactId → Set<groupId> for O(1) group lookups per contact
|
|
11
|
+
* - Hydrates group members into full MajikContact instances via an injected MajikContactDirectory
|
|
12
|
+
* - Automatically syncs system group side-effects (e.g. Blocked group ↔ MajikContact.block())
|
|
13
|
+
* - Provides a handleContactRemoved() hook for MajikContactDirectory to call on contact removal
|
|
14
|
+
* - Handles serialization / deserialization of all groups together
|
|
15
|
+
*
|
|
16
|
+
* System groups (Favorites, Blocked) are always present and are bootstrapped in the constructor.
|
|
17
|
+
* They cannot be deleted or renamed but their membership is fully manageable.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MajikContactGroupManager {
|
|
20
|
+
private groups;
|
|
21
|
+
/**
|
|
22
|
+
* Reverse index: contactId → Set of groupIds the contact belongs to.
|
|
23
|
+
* Kept in sync on every membership mutation so getGroupsForContact() is O(1).
|
|
24
|
+
*/
|
|
25
|
+
private contactGroupIndex;
|
|
26
|
+
private directory;
|
|
27
|
+
constructor(directory: MajikContactDirectory);
|
|
28
|
+
/**
|
|
29
|
+
* Ensures the two system groups always exist on construction.
|
|
30
|
+
* Safe to call multiple times — skips if already present.
|
|
31
|
+
*/
|
|
32
|
+
private bootstrapSystemGroups;
|
|
33
|
+
/**
|
|
34
|
+
* Creates and registers a new user group.
|
|
35
|
+
* Throws if a group with the same ID already exists.
|
|
36
|
+
*/
|
|
37
|
+
createGroup(id: string, name: string, meta?: Partial<Omit<MajikContactGroupMeta, "name">>, initialMemberIds?: string[]): MajikContactGroup;
|
|
38
|
+
/**
|
|
39
|
+
* Registers an already-constructed MajikContactGroup instance.
|
|
40
|
+
* Useful when importing groups from external sources.
|
|
41
|
+
* Throws if a group with the same ID already exists.
|
|
42
|
+
*/
|
|
43
|
+
addGroup(group: MajikContactGroup): this;
|
|
44
|
+
/**
|
|
45
|
+
* Removes a user group by ID.
|
|
46
|
+
* System groups cannot be deleted.
|
|
47
|
+
* Cleans up the reverse index for all former members.
|
|
48
|
+
*/
|
|
49
|
+
removeGroup(id: string): MAJIK_API_RESPONSE;
|
|
50
|
+
clear(): this;
|
|
51
|
+
getGroup(id: string): MajikContactGroup | undefined;
|
|
52
|
+
getGroupOrThrow(id: string): MajikContactGroup;
|
|
53
|
+
hasGroup(id: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Returns all groups, optionally filtered and/or sorted.
|
|
56
|
+
*
|
|
57
|
+
* @param includeSystem Include system groups (Favorites, Blocked). Default: true.
|
|
58
|
+
* @param sortedByName Sort results by group name. Default: false.
|
|
59
|
+
*/
|
|
60
|
+
listGroups(includeSystem?: boolean, sortedByName?: boolean): MajikContactGroup[];
|
|
61
|
+
/**
|
|
62
|
+
* Updates mutable metadata fields on a group.
|
|
63
|
+
* Name is protected on system groups (delegates to MajikContactGroup.updateName which throws).
|
|
64
|
+
*/
|
|
65
|
+
updateGroupMeta(id: string, meta: Partial<Pick<MajikContactGroupMeta, "name" | "description" | "color">>): MajikContactGroup;
|
|
66
|
+
/**
|
|
67
|
+
* Adds a contact to a group.
|
|
68
|
+
*
|
|
69
|
+
* - Validates the contact exists in the directory.
|
|
70
|
+
* - If the target group is the system Blocked group, also calls contact.block()
|
|
71
|
+
* on the directory to keep MajikContact state in sync.
|
|
72
|
+
* - Throws if the contact is already a member (strict — use addMemberIfAbsent for idempotent).
|
|
73
|
+
*/
|
|
74
|
+
addContactToGroup(groupId: string, contactId: string): MajikContactGroup;
|
|
75
|
+
/**
|
|
76
|
+
* Idempotent variant — does not throw if the contact is already a member.
|
|
77
|
+
* Still validates contact existence and handles Blocked sync.
|
|
78
|
+
*/
|
|
79
|
+
addContactToGroupIfAbsent(groupId: string, contactId: string): MajikContactGroup;
|
|
80
|
+
/**
|
|
81
|
+
* Adds multiple contacts to a group in one call.
|
|
82
|
+
* All contacts are validated before any mutation is applied (all-or-nothing).
|
|
83
|
+
*/
|
|
84
|
+
addContactsToGroup(groupId: string, contactIds: string[]): MajikContactGroup;
|
|
85
|
+
/**
|
|
86
|
+
* Removes a contact from a group.
|
|
87
|
+
*
|
|
88
|
+
* - If removing from the system Blocked group, also calls contact.unblock()
|
|
89
|
+
* to keep MajikContact state in sync.
|
|
90
|
+
* - Throws if the contact is not a member (strict — use removeContactFromGroupIfPresent for idempotent).
|
|
91
|
+
*/
|
|
92
|
+
removeContactFromGroup(groupId: string, contactId: string): MajikContactGroup;
|
|
93
|
+
/**
|
|
94
|
+
* Idempotent variant — does not throw if the contact is not a member.
|
|
95
|
+
*/
|
|
96
|
+
removeContactFromGroupIfPresent(groupId: string, contactId: string): MajikContactGroup;
|
|
97
|
+
/**
|
|
98
|
+
* Moves a contact from one group to another atomically.
|
|
99
|
+
* Throws if the contact is not a member of the source group.
|
|
100
|
+
*/
|
|
101
|
+
moveContact(contactId: string, fromGroupId: string, toGroupId: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Returns all group IDs the contact belongs to.
|
|
104
|
+
* O(1) — backed by the reverse index.
|
|
105
|
+
*/
|
|
106
|
+
getGroupIdsForContact(contactId: string): string[];
|
|
107
|
+
/**
|
|
108
|
+
* Returns all groups the contact belongs to as MajikContactGroup instances.
|
|
109
|
+
*/
|
|
110
|
+
getGroupsForContact(contactId: string): MajikContactGroup[];
|
|
111
|
+
/**
|
|
112
|
+
* Returns all hydrated MajikContact instances that are members of the given group.
|
|
113
|
+
* Contacts that exist in the group but have been removed from the directory are silently skipped.
|
|
114
|
+
*/
|
|
115
|
+
getContactsInGroup(groupId: string): MajikContact[];
|
|
116
|
+
/**
|
|
117
|
+
* Returns hydrated contacts that are members of the given group,
|
|
118
|
+
* sorted by their display label (or ID if no label set).
|
|
119
|
+
*/
|
|
120
|
+
getContactsInGroupSorted(groupId: string): MajikContact[];
|
|
121
|
+
/**
|
|
122
|
+
* Returns true if the contact is a member of the given group.
|
|
123
|
+
*/
|
|
124
|
+
isContactInGroup(groupId: string, contactId: string): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Returns true if the contact is in the system Favorites group.
|
|
127
|
+
*/
|
|
128
|
+
isFavorite(contactId: string): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if the contact is in the system Blocked group.
|
|
131
|
+
*/
|
|
132
|
+
isBlocked(contactId: string): boolean;
|
|
133
|
+
addToFavorites(contactId: string): MajikContactGroup;
|
|
134
|
+
removeFromFavorites(contactId: string): MajikContactGroup;
|
|
135
|
+
/**
|
|
136
|
+
* Adds the contact to the Blocked group and calls contact.block() on the directory.
|
|
137
|
+
*/
|
|
138
|
+
blockContact(contactId: string): MajikContactGroup;
|
|
139
|
+
/**
|
|
140
|
+
* Removes the contact from the Blocked group.
|
|
141
|
+
* Calls contact.unblock() only if the contact is not re-blocked by any other mechanism.
|
|
142
|
+
*/
|
|
143
|
+
unblockContact(contactId: string): MajikContactGroup;
|
|
144
|
+
getFavoritesGroup(): MajikContactGroup;
|
|
145
|
+
getBlockedGroup(): MajikContactGroup;
|
|
146
|
+
/**
|
|
147
|
+
* Must be called whenever a contact is removed from MajikContactDirectory.
|
|
148
|
+
* Auto-removes the contact from every group it belongs to and cleans up the reverse index.
|
|
149
|
+
*
|
|
150
|
+
* Usage:
|
|
151
|
+
* const response = directory.removeContact(id);
|
|
152
|
+
* if (response.success) manager.handleContactRemoved(id);
|
|
153
|
+
*/
|
|
154
|
+
handleContactRemoved(contactId: string): void;
|
|
155
|
+
toJSON(): MajikContactGroupManagerData;
|
|
156
|
+
/**
|
|
157
|
+
* Restores all groups from serialized data.
|
|
158
|
+
* Clears existing user groups but preserves system groups (they are re-bootstrapped).
|
|
159
|
+
* Rebuilds the reverse index from scratch.
|
|
160
|
+
*/
|
|
161
|
+
fromJSON(data: MajikContactGroupManagerData): this;
|
|
162
|
+
private indexAdd;
|
|
163
|
+
private indexRemove;
|
|
164
|
+
/**
|
|
165
|
+
* Unblocks a contact on the directory only if the contact is no longer
|
|
166
|
+
* a member of the system Blocked group. Guards against a race where
|
|
167
|
+
* the contact was re-added before unblock is processed.
|
|
168
|
+
*/
|
|
169
|
+
private syncUnblock;
|
|
170
|
+
private assertDirectory;
|
|
171
|
+
private assertGroupId;
|
|
172
|
+
private assertContactId;
|
|
173
|
+
private assertContactIdArray;
|
|
174
|
+
private assertGroupInstance;
|
|
175
|
+
private assertNotSystemId;
|
|
176
|
+
/**
|
|
177
|
+
* Resolves a contact from the directory and throws a descriptive error if not found.
|
|
178
|
+
*/
|
|
179
|
+
private getContactOrThrow;
|
|
180
|
+
/**
|
|
181
|
+
* Validates that all provided contact IDs exist in the directory.
|
|
182
|
+
* Collects all missing IDs and throws once with the full list,
|
|
183
|
+
* rather than failing on the first missing contact.
|
|
184
|
+
*/
|
|
185
|
+
private assertContactsExist;
|
|
186
|
+
}
|