@web_of_trust/adapter-automerge 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/AutomergeOutboxStore.d.ts +23 -0
- package/dist/AutomergeOutboxStore.d.ts.map +1 -0
- package/dist/AutomergeReplicationAdapter.d.ts +132 -0
- package/dist/AutomergeReplicationAdapter.d.ts.map +1 -0
- package/dist/AutomergeSpaceMetadataStorage.d.ts +23 -0
- package/dist/AutomergeSpaceMetadataStorage.d.ts.map +1 -0
- package/dist/CompactionService.d.ts +28 -0
- package/dist/CompactionService.d.ts.map +1 -0
- package/dist/EncryptedMessagingNetworkAdapter.d.ts +62 -0
- package/dist/EncryptedMessagingNetworkAdapter.d.ts.map +1 -0
- package/dist/InMemoryRepoStorageAdapter.d.ts +21 -0
- package/dist/InMemoryRepoStorageAdapter.d.ts.map +1 -0
- package/dist/PersonalDocManager.d.ts +166 -0
- package/dist/PersonalDocManager.d.ts.map +1 -0
- package/dist/PersonalNetworkAdapter.d.ts +27 -0
- package/dist/PersonalNetworkAdapter.d.ts.map +1 -0
- package/dist/SyncOnlyStorageAdapter.d.ts +27 -0
- package/dist/SyncOnlyStorageAdapter.d.ts.map +1 -0
- package/dist/index-Dz-MvOzM.js +86 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1621 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
var re = Object.defineProperty;
|
|
2
|
+
var ae = (c, e, t) => e in c ? re(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t;
|
|
3
|
+
var d = (c, e, t) => ae(c, typeof e != "symbol" ? e + "" : e, t);
|
|
4
|
+
import { NetworkAdapter as W, Repo as L, parseAutomergeUrl as Y, stringifyAutomergeUrl as ie } from "@automerge/automerge-repo";
|
|
5
|
+
import * as D from "@automerge/automerge";
|
|
6
|
+
import { verifyEnvelope as X, EncryptedSyncService as P, signEnvelope as Q, VaultClient as Z, base64ToUint8 as E, VaultPushScheduler as N, CompactStorageManager as ce, getMetrics as x, registerDebugApi as de } from "@web_of_trust/core";
|
|
7
|
+
class le extends W {
|
|
8
|
+
// spaceId -> Set<DID>
|
|
9
|
+
constructor(t, n, s) {
|
|
10
|
+
super();
|
|
11
|
+
d(this, "messaging");
|
|
12
|
+
d(this, "identity");
|
|
13
|
+
d(this, "groupKeyService");
|
|
14
|
+
d(this, "ready", !1);
|
|
15
|
+
d(this, "readyResolve");
|
|
16
|
+
d(this, "readyPromise");
|
|
17
|
+
d(this, "unsubMessage");
|
|
18
|
+
/** Track message IDs we sent, so we can ignore our own echoes from the relay */
|
|
19
|
+
d(this, "sentMessageIds", /* @__PURE__ */ new Set());
|
|
20
|
+
/** Phantom peerId used for multi-device self-sync */
|
|
21
|
+
d(this, "selfPeerId", null);
|
|
22
|
+
// Document -> Space mapping (needed to find the right group key)
|
|
23
|
+
d(this, "docToSpace", /* @__PURE__ */ new Map());
|
|
24
|
+
// Known peers per space
|
|
25
|
+
d(this, "spacePeers", /* @__PURE__ */ new Map());
|
|
26
|
+
this.messaging = t, this.identity = n, this.groupKeyService = s, this.readyPromise = new Promise((o) => {
|
|
27
|
+
this.readyResolve = o;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// --- NetworkAdapter interface ---
|
|
31
|
+
isReady() {
|
|
32
|
+
return this.ready;
|
|
33
|
+
}
|
|
34
|
+
whenReady() {
|
|
35
|
+
return this.readyPromise;
|
|
36
|
+
}
|
|
37
|
+
connect(t, n) {
|
|
38
|
+
var s;
|
|
39
|
+
this.peerId = t, this.peerMetadata = n, this.unsubMessage = this.messaging.onMessage(async (o) => {
|
|
40
|
+
if (o.type === "content") {
|
|
41
|
+
if (this.sentMessageIds.has(o.id)) {
|
|
42
|
+
this.sentMessageIds.delete(o.id);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (o.signature && !await X(o)) {
|
|
46
|
+
console.warn("[EncryptedSync] Rejected message with invalid signature from", o.fromDid);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const i = JSON.parse(o.payload);
|
|
51
|
+
if (!i.syncData) return;
|
|
52
|
+
const r = i.spaceId, a = i.generation, l = this.groupKeyService.getKeyByGeneration(r, a);
|
|
53
|
+
if (!l) {
|
|
54
|
+
console.debug(`[EncryptedSync] No group key yet for space ${r} gen ${a} — will sync after metadata arrives`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const h = {
|
|
58
|
+
ciphertext: new Uint8Array(i.ciphertext),
|
|
59
|
+
nonce: new Uint8Array(i.nonce),
|
|
60
|
+
spaceId: r,
|
|
61
|
+
generation: a,
|
|
62
|
+
fromDid: o.fromDid
|
|
63
|
+
}, p = await P.decryptChange(h, l), m = i.documentId;
|
|
64
|
+
if (!m) return;
|
|
65
|
+
const y = o.fromDid === this.identity.getDid() && this.selfPeerId ? this.selfPeerId : o.fromDid, u = {
|
|
66
|
+
type: i.messageType || "sync",
|
|
67
|
+
senderId: y,
|
|
68
|
+
targetId: this.peerId,
|
|
69
|
+
documentId: m,
|
|
70
|
+
data: p
|
|
71
|
+
};
|
|
72
|
+
this.emit("message", u);
|
|
73
|
+
} catch (i) {
|
|
74
|
+
console.debug("EncryptedMessagingNetworkAdapter: failed to process message", i);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}), this.ready = !0, (s = this.readyResolve) == null || s.call(this), this.emit("ready", void 0);
|
|
78
|
+
}
|
|
79
|
+
send(t) {
|
|
80
|
+
if (!this.ready || !t.data || !t.documentId) return;
|
|
81
|
+
const n = this.docToSpace.get(t.documentId);
|
|
82
|
+
if (!n) return;
|
|
83
|
+
const s = this.groupKeyService.getCurrentKey(n);
|
|
84
|
+
if (!s) return;
|
|
85
|
+
const o = this.groupKeyService.getCurrentGeneration(n), i = t.targetId, r = i === this.selfPeerId ? this.identity.getDid() : i;
|
|
86
|
+
(async () => {
|
|
87
|
+
try {
|
|
88
|
+
const a = await P.encryptChange(
|
|
89
|
+
t.data,
|
|
90
|
+
s,
|
|
91
|
+
n,
|
|
92
|
+
o,
|
|
93
|
+
this.identity.getDid()
|
|
94
|
+
), l = {
|
|
95
|
+
syncData: !0,
|
|
96
|
+
spaceId: n,
|
|
97
|
+
documentId: t.documentId,
|
|
98
|
+
messageType: t.type,
|
|
99
|
+
generation: o,
|
|
100
|
+
ciphertext: Array.from(a.ciphertext),
|
|
101
|
+
nonce: Array.from(a.nonce)
|
|
102
|
+
}, h = crypto.randomUUID();
|
|
103
|
+
this.sentMessageIds.add(h), setTimeout(() => this.sentMessageIds.delete(h), 3e4);
|
|
104
|
+
const p = {
|
|
105
|
+
v: 1,
|
|
106
|
+
id: h,
|
|
107
|
+
type: "content",
|
|
108
|
+
fromDid: this.identity.getDid(),
|
|
109
|
+
toDid: r,
|
|
110
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
111
|
+
encoding: "json",
|
|
112
|
+
payload: JSON.stringify(l),
|
|
113
|
+
signature: ""
|
|
114
|
+
};
|
|
115
|
+
await Q(p, (m) => this.identity.sign(m)), await this.messaging.send(p);
|
|
116
|
+
} catch (a) {
|
|
117
|
+
console.debug("[EncryptedSync] Failed to send sync message:", a);
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
}
|
|
121
|
+
disconnect() {
|
|
122
|
+
var t;
|
|
123
|
+
(t = this.unsubMessage) == null || t.call(this), this.unsubMessage = void 0, this.sentMessageIds.clear(), this.ready = !1;
|
|
124
|
+
}
|
|
125
|
+
// --- Space/Document registration ---
|
|
126
|
+
/**
|
|
127
|
+
* Register a document -> space mapping.
|
|
128
|
+
* Needed so we can look up the right group key when sending/receiving.
|
|
129
|
+
*/
|
|
130
|
+
registerDocument(t, n) {
|
|
131
|
+
this.docToSpace.set(t, n);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Unregister a document mapping.
|
|
135
|
+
*/
|
|
136
|
+
unregisterDocument(t) {
|
|
137
|
+
this.docToSpace.delete(t);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Register a peer (DID) as a member of a space.
|
|
141
|
+
* Emits peer-candidate so automerge-repo starts syncing with this peer.
|
|
142
|
+
*/
|
|
143
|
+
registerSpacePeer(t, n) {
|
|
144
|
+
let s = this.spacePeers.get(t);
|
|
145
|
+
s || (s = /* @__PURE__ */ new Set(), this.spacePeers.set(t, s)), !s.has(n) && (s.add(n), this.emit("peer-candidate", {
|
|
146
|
+
peerId: n,
|
|
147
|
+
peerMetadata: { isEphemeral: !0 }
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Register a phantom peer representing "self on another device".
|
|
152
|
+
* Uses a different peerId so automerge-repo doesn't skip it as self,
|
|
153
|
+
* but send() routes messages to our own DID (relay delivers to other devices).
|
|
154
|
+
*/
|
|
155
|
+
registerSelfPeer(t) {
|
|
156
|
+
this.selfPeerId || (this.selfPeerId = `${this.identity.getDid()}#other-device`), this.registerSpacePeer(t, this.selfPeerId);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Unregister a peer from a space.
|
|
160
|
+
*/
|
|
161
|
+
unregisterSpacePeer(t, n) {
|
|
162
|
+
const s = this.spacePeers.get(t);
|
|
163
|
+
if (s) {
|
|
164
|
+
s.delete(n);
|
|
165
|
+
for (const [, o] of this.spacePeers)
|
|
166
|
+
if (o.has(n)) return;
|
|
167
|
+
this.emit("peer-disconnected", { peerId: n });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const M = class M {
|
|
172
|
+
constructor() {
|
|
173
|
+
}
|
|
174
|
+
static getInstance() {
|
|
175
|
+
return M.instance || (M.instance = new M()), M.instance;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Compact an Automerge document binary (strip change history).
|
|
179
|
+
* Yields to the browser between steps to keep the UI responsive.
|
|
180
|
+
*
|
|
181
|
+
* @param binary - Automerge.save() output (includes history)
|
|
182
|
+
* @returns Compacted binary (history-free)
|
|
183
|
+
*/
|
|
184
|
+
async compact(e) {
|
|
185
|
+
const t = D.load(e);
|
|
186
|
+
await J();
|
|
187
|
+
const n = JSON.parse(JSON.stringify(t));
|
|
188
|
+
return await J(), D.save(D.from(n));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Whether the service is using a Web Worker (true) or main-thread with yielding (false).
|
|
192
|
+
*/
|
|
193
|
+
get isUsingWorker() {
|
|
194
|
+
return !1;
|
|
195
|
+
}
|
|
196
|
+
destroy() {
|
|
197
|
+
M.instance = null;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
d(M, "instance", null);
|
|
201
|
+
let T = M;
|
|
202
|
+
function J() {
|
|
203
|
+
var c;
|
|
204
|
+
return typeof globalThis < "u" && "scheduler" in globalThis && typeof ((c = globalThis.scheduler) == null ? void 0 : c.yield) == "function" ? globalThis.scheduler.yield() : new Promise((e) => setTimeout(e, 0));
|
|
205
|
+
}
|
|
206
|
+
class he {
|
|
207
|
+
constructor(e, t, n, s) {
|
|
208
|
+
d(this, "id");
|
|
209
|
+
d(this, "spaceState");
|
|
210
|
+
d(this, "docHandle");
|
|
211
|
+
d(this, "vaultScheduler");
|
|
212
|
+
d(this, "compactScheduler");
|
|
213
|
+
d(this, "remoteUpdateCallbacks", /* @__PURE__ */ new Set());
|
|
214
|
+
d(this, "closed", !1);
|
|
215
|
+
d(this, "localChanging", !1);
|
|
216
|
+
d(this, "unsubChange");
|
|
217
|
+
this.id = e.info.id, this.spaceState = e, this.docHandle = t, this.vaultScheduler = n, this.compactScheduler = s;
|
|
218
|
+
const o = () => {
|
|
219
|
+
this.localChanging || this._notifyRemoteUpdate();
|
|
220
|
+
};
|
|
221
|
+
this.docHandle.on("change", o), this.unsubChange = () => this.docHandle.off("change", o);
|
|
222
|
+
}
|
|
223
|
+
info() {
|
|
224
|
+
return { ...this.spaceState.info };
|
|
225
|
+
}
|
|
226
|
+
getDoc() {
|
|
227
|
+
return this.docHandle.doc();
|
|
228
|
+
}
|
|
229
|
+
getMeta() {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
transact(e, t) {
|
|
233
|
+
if (this.closed) throw new Error("Handle is closed");
|
|
234
|
+
this.localChanging = !0;
|
|
235
|
+
try {
|
|
236
|
+
this.docHandle.change(e);
|
|
237
|
+
} finally {
|
|
238
|
+
this.localChanging = !1;
|
|
239
|
+
}
|
|
240
|
+
this.vaultScheduler && (t != null && t.stream ? this.vaultScheduler.pushDebounced() : this.vaultScheduler.pushImmediate()), this.compactScheduler && (t != null && t.stream ? this.compactScheduler.pushDebounced() : this.compactScheduler.pushImmediate());
|
|
241
|
+
}
|
|
242
|
+
onRemoteUpdate(e) {
|
|
243
|
+
return this.remoteUpdateCallbacks.add(e), () => {
|
|
244
|
+
this.remoteUpdateCallbacks.delete(e);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
_notifyRemoteUpdate() {
|
|
248
|
+
this.compactScheduler && this.compactScheduler.pushDebounced();
|
|
249
|
+
for (const e of this.remoteUpdateCallbacks)
|
|
250
|
+
e();
|
|
251
|
+
}
|
|
252
|
+
close() {
|
|
253
|
+
var e;
|
|
254
|
+
this.closed = !0, (e = this.unsubChange) == null || e.call(this), this.remoteUpdateCallbacks.clear(), this.spaceState.handles.delete(this);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
class xe {
|
|
258
|
+
constructor(e) {
|
|
259
|
+
d(this, "identity");
|
|
260
|
+
d(this, "messaging");
|
|
261
|
+
d(this, "groupKeyService");
|
|
262
|
+
d(this, "metadataStorage");
|
|
263
|
+
d(this, "repoStorage");
|
|
264
|
+
d(this, "compactStore", null);
|
|
265
|
+
d(this, "vault", null);
|
|
266
|
+
d(this, "spaces", /* @__PURE__ */ new Map());
|
|
267
|
+
d(this, "state", "idle");
|
|
268
|
+
d(this, "memberChangeCallbacks", /* @__PURE__ */ new Set());
|
|
269
|
+
d(this, "spacesSubscribers", /* @__PURE__ */ new Set());
|
|
270
|
+
d(this, "unsubscribeMessaging", null);
|
|
271
|
+
/** Local seq counter per doc — avoids a getDocInfo HTTP call (and its 404) on first push */
|
|
272
|
+
d(this, "vaultSeqs", /* @__PURE__ */ new Map());
|
|
273
|
+
/** VaultPushScheduler per space — handles immediate/debounced vault pushes */
|
|
274
|
+
d(this, "vaultSchedulers", /* @__PURE__ */ new Map());
|
|
275
|
+
/** VaultPushScheduler per space for CompactStore (2s debounce) */
|
|
276
|
+
d(this, "compactSchedulers", /* @__PURE__ */ new Map());
|
|
277
|
+
/** Optional filter to restrict which spaces are restored (e.g. by appTag) */
|
|
278
|
+
d(this, "spaceFilter");
|
|
279
|
+
d(this, "repo");
|
|
280
|
+
d(this, "networkAdapter");
|
|
281
|
+
this.identity = e.identity, this.messaging = e.messaging, this.groupKeyService = e.groupKeyService, this.metadataStorage = e.metadataStorage ?? null, this.repoStorage = e.repoStorage, this.compactStore = e.compactStore ?? null, this.spaceFilter = e.spaceFilter ?? null, e.vaultUrl && (this.vault = new Z(e.vaultUrl, e.identity));
|
|
282
|
+
}
|
|
283
|
+
/** Sign an envelope with our identity and send it */
|
|
284
|
+
async _signAndSend(e) {
|
|
285
|
+
await Q(e, (t) => this.identity.sign(t)), await this.messaging.send(e);
|
|
286
|
+
}
|
|
287
|
+
async start() {
|
|
288
|
+
this.networkAdapter = new le(
|
|
289
|
+
this.messaging,
|
|
290
|
+
this.identity,
|
|
291
|
+
this.groupKeyService
|
|
292
|
+
), this.repo = new L({
|
|
293
|
+
peerId: this.identity.getDid(),
|
|
294
|
+
network: [this.networkAdapter],
|
|
295
|
+
storage: this.repoStorage,
|
|
296
|
+
// Share all documents with all peers (our NetworkAdapter handles routing)
|
|
297
|
+
sharePolicy: async () => !0
|
|
298
|
+
}), await this.restoreSpacesFromMetadata(), this.state = "idle", this._notifySpacesSubscribers(), this.unsubscribeMessaging = this.messaging.onMessage(
|
|
299
|
+
(e) => this.handleMessage(e)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Restore spaces from metadata storage.
|
|
304
|
+
* Called on start() and can be called again after remote sync
|
|
305
|
+
* delivers new space metadata (e.g. multi-device sync).
|
|
306
|
+
* Only loads spaces that aren't already known.
|
|
307
|
+
*/
|
|
308
|
+
async restoreSpacesFromMetadata() {
|
|
309
|
+
if (!this.metadataStorage || !this.repo) return;
|
|
310
|
+
const e = await this.metadataStorage.loadAllSpaceMetadata();
|
|
311
|
+
let t = !1;
|
|
312
|
+
for (const n of e) {
|
|
313
|
+
if (this.spaces.has(n.info.id) || this.spaceFilter && !this.spaceFilter(n.info)) continue;
|
|
314
|
+
const s = /* @__PURE__ */ new Map();
|
|
315
|
+
for (const [a, l] of Object.entries(n.memberEncryptionKeys))
|
|
316
|
+
s.set(a, l);
|
|
317
|
+
const o = {
|
|
318
|
+
info: n.info,
|
|
319
|
+
documentId: n.documentId,
|
|
320
|
+
documentUrl: n.documentUrl,
|
|
321
|
+
handles: /* @__PURE__ */ new Set(),
|
|
322
|
+
memberEncryptionKeys: s
|
|
323
|
+
};
|
|
324
|
+
this.spaces.set(n.info.id, o), this.networkAdapter.registerDocument(o.documentId, n.info.id);
|
|
325
|
+
for (const a of n.info.members)
|
|
326
|
+
a !== this.identity.getDid() && this.networkAdapter.registerSpacePeer(n.info.id, a);
|
|
327
|
+
this.networkAdapter.registerSelfPeer(n.info.id);
|
|
328
|
+
const i = await this.metadataStorage.loadGroupKeys(n.info.id);
|
|
329
|
+
for (const a of i)
|
|
330
|
+
this.groupKeyService.importKey(a.spaceId, a.key, a.generation);
|
|
331
|
+
if (this.compactStore && await this._restoreFromCompactStore(o)) {
|
|
332
|
+
t = !0, console.log("[ReplicationAdapter] Restored space from CompactStore:", n.info.name || n.info.id);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (this.vault && await this._restoreFromVault(o)) {
|
|
336
|
+
t = !0, console.log("[ReplicationAdapter] Restored space from vault:", n.info.name || n.info.id);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
let r = !1;
|
|
340
|
+
try {
|
|
341
|
+
r = (await this.repo.find(o.documentUrl, {
|
|
342
|
+
allowableStates: ["ready", "unavailable"],
|
|
343
|
+
signal: AbortSignal.timeout(2e3)
|
|
344
|
+
})).isReady();
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
r ? (t = !0, console.log("[ReplicationAdapter] Restored space from metadata:", n.info.name || n.info.id)) : (console.log("[ReplicationAdapter] Space registered, waiting for doc sync:", n.info.name || n.info.id), t = !0, this._waitForDoc(o));
|
|
348
|
+
}
|
|
349
|
+
t && this._notifySpacesSubscribers();
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Try to restore a space doc from the vault.
|
|
353
|
+
* Returns true if doc was successfully imported from vault.
|
|
354
|
+
*/
|
|
355
|
+
async _restoreFromVault(e) {
|
|
356
|
+
var n, s;
|
|
357
|
+
if (!this.vault) return !1;
|
|
358
|
+
const t = this.groupKeyService.getCurrentKey(e.info.id);
|
|
359
|
+
if (!t)
|
|
360
|
+
return console.warn("[ReplicationAdapter] No group key for vault restore:", e.info.name || e.info.id), !1;
|
|
361
|
+
try {
|
|
362
|
+
const o = await this.vault.getChanges(e.info.id);
|
|
363
|
+
if ((n = o.snapshot) != null && n.data) {
|
|
364
|
+
const i = E(o.snapshot.data), r = i[0], a = i.slice(1, 1 + r), l = i.slice(1 + r), h = this.groupKeyService.getCurrentGeneration(e.info.id), p = await P.decryptChange(
|
|
365
|
+
{ ciphertext: l, nonce: a, spaceId: e.info.id, generation: h, fromDid: "" },
|
|
366
|
+
t
|
|
367
|
+
), m = this.repo.import(p, {
|
|
368
|
+
docId: e.documentId
|
|
369
|
+
});
|
|
370
|
+
m.isReady() || m.doneLoading();
|
|
371
|
+
for (const u of o.changes) {
|
|
372
|
+
const g = E(u.data), w = g[0], f = g.slice(1, 1 + w), R = g.slice(1 + w), _ = await P.decryptChange(
|
|
373
|
+
{ ciphertext: R, nonce: f, spaceId: e.info.id, generation: h, fromDid: u.authorDid },
|
|
374
|
+
t
|
|
375
|
+
);
|
|
376
|
+
m.merge(this.repo.import(_, void 0));
|
|
377
|
+
}
|
|
378
|
+
await this.repo.flush([e.documentId]);
|
|
379
|
+
const y = Math.max(
|
|
380
|
+
((s = o.snapshot) == null ? void 0 : s.upToSeq) ?? 0,
|
|
381
|
+
...o.changes.map((u) => u.seq ?? 0)
|
|
382
|
+
);
|
|
383
|
+
return y > 0 && this.vaultSeqs.set(e.info.id, y), !0;
|
|
384
|
+
}
|
|
385
|
+
if (o.changes.length > 0) {
|
|
386
|
+
const i = this.groupKeyService.getCurrentGeneration(e.info.id), r = E(o.changes[0].data), a = r[0], l = r.slice(1, 1 + a), h = r.slice(1 + a), p = await P.decryptChange(
|
|
387
|
+
{ ciphertext: h, nonce: l, spaceId: e.info.id, generation: i, fromDid: o.changes[0].authorDid },
|
|
388
|
+
t
|
|
389
|
+
), m = this.repo.import(p, {
|
|
390
|
+
docId: e.documentId
|
|
391
|
+
});
|
|
392
|
+
m.isReady() || m.doneLoading();
|
|
393
|
+
for (let u = 1; u < o.changes.length; u++) {
|
|
394
|
+
const g = E(o.changes[u].data), w = g[0], f = g.slice(1, 1 + w), R = g.slice(1 + w), _ = await P.decryptChange(
|
|
395
|
+
{ ciphertext: R, nonce: f, spaceId: e.info.id, generation: i, fromDid: o.changes[u].authorDid },
|
|
396
|
+
t
|
|
397
|
+
);
|
|
398
|
+
m.merge(this.repo.import(_, void 0));
|
|
399
|
+
}
|
|
400
|
+
await this.repo.flush([e.documentId]);
|
|
401
|
+
const y = Math.max(...o.changes.map((u) => u.seq ?? 0));
|
|
402
|
+
return y > 0 && this.vaultSeqs.set(e.info.id, y), !0;
|
|
403
|
+
}
|
|
404
|
+
} catch (o) {
|
|
405
|
+
const i = e.info.name || e.info.id;
|
|
406
|
+
o instanceof DOMException && o.name === "OperationError" ? console.warn(`[ReplicationAdapter] Vault decryption failed for "${i}" — likely removed from space or key rotated`) : console.warn("[ReplicationAdapter] Vault restore failed for", i, ":", o);
|
|
407
|
+
}
|
|
408
|
+
return !1;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Try to restore a space doc from the CompactStore.
|
|
412
|
+
* Returns true if doc was successfully imported.
|
|
413
|
+
*/
|
|
414
|
+
async _restoreFromCompactStore(e) {
|
|
415
|
+
if (!this.compactStore) return !1;
|
|
416
|
+
try {
|
|
417
|
+
const t = await this.compactStore.load(e.info.id);
|
|
418
|
+
if (!t) return !1;
|
|
419
|
+
const n = this.repo.import(t, {
|
|
420
|
+
docId: e.documentId
|
|
421
|
+
});
|
|
422
|
+
return n.isReady() || n.doneLoading(), await this.repo.flush([e.documentId]), !0;
|
|
423
|
+
} catch (t) {
|
|
424
|
+
return console.warn("[ReplicationAdapter] CompactStore restore failed for", e.info.name || e.info.id, ":", t), !1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Save a snapshot to the CompactStore.
|
|
429
|
+
* Two-phase: save with history immediately (fast), then compact in Worker.
|
|
430
|
+
*/
|
|
431
|
+
async _saveToCompactStore(e) {
|
|
432
|
+
if (!this.compactStore) return;
|
|
433
|
+
const t = this.repo.handles[e.documentId], n = t == null ? void 0 : t.doc();
|
|
434
|
+
if (!n) return;
|
|
435
|
+
const s = D.save(n);
|
|
436
|
+
await this.compactStore.save(e.info.id, s);
|
|
437
|
+
const i = await T.getInstance().compact(s);
|
|
438
|
+
i && i.length > 0 && await this.compactStore.save(e.info.id, i);
|
|
439
|
+
}
|
|
440
|
+
async _pushSnapshotToVault(e) {
|
|
441
|
+
if (!this.vault) return;
|
|
442
|
+
const t = this.groupKeyService.getCurrentKey(e.info.id);
|
|
443
|
+
if (!t) return;
|
|
444
|
+
const n = this.repo.handles[e.documentId], s = n == null ? void 0 : n.doc();
|
|
445
|
+
if (!s) return;
|
|
446
|
+
const o = D.save(s), r = await T.getInstance().compact(o), a = this.groupKeyService.getCurrentGeneration(e.info.id), l = await P.encryptChange(
|
|
447
|
+
r,
|
|
448
|
+
t,
|
|
449
|
+
e.info.id,
|
|
450
|
+
a,
|
|
451
|
+
this.identity.getDid()
|
|
452
|
+
), p = (this.vaultSeqs.get(e.info.id) ?? 0) + 1;
|
|
453
|
+
await this.vault.putSnapshot(
|
|
454
|
+
e.info.id,
|
|
455
|
+
l.ciphertext,
|
|
456
|
+
l.nonce,
|
|
457
|
+
p
|
|
458
|
+
), this.vaultSeqs.set(e.info.id, p);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Background wait for a space doc that isn't locally available yet.
|
|
462
|
+
* The doc may arrive via sync from another device.
|
|
463
|
+
*/
|
|
464
|
+
_waitForDoc(e) {
|
|
465
|
+
(async () => {
|
|
466
|
+
try {
|
|
467
|
+
(await this.repo.find(e.documentUrl, {
|
|
468
|
+
allowableStates: ["ready"],
|
|
469
|
+
signal: AbortSignal.timeout(3e4)
|
|
470
|
+
})).isReady() && this.spaces.has(e.info.id) && (console.log("[ReplicationAdapter] Doc arrived via sync for space:", e.info.name || e.info.id), this._notifySpacesSubscribers());
|
|
471
|
+
} catch {
|
|
472
|
+
console.log("[ReplicationAdapter] Doc did not arrive within timeout for space:", e.info.name || e.info.id);
|
|
473
|
+
}
|
|
474
|
+
})();
|
|
475
|
+
}
|
|
476
|
+
async stop() {
|
|
477
|
+
this.unsubscribeMessaging && (this.unsubscribeMessaging(), this.unsubscribeMessaging = null);
|
|
478
|
+
for (const e of this.vaultSchedulers.values()) e.destroy();
|
|
479
|
+
this.vaultSchedulers.clear();
|
|
480
|
+
for (const e of this.compactSchedulers.values()) e.destroy();
|
|
481
|
+
this.compactSchedulers.clear(), this.vaultSeqs.clear();
|
|
482
|
+
for (const e of this.spaces.values())
|
|
483
|
+
for (const t of e.handles)
|
|
484
|
+
t.close();
|
|
485
|
+
this.repo && (this.networkAdapter.disconnect(), await this.repo.shutdown()), this.state = "idle";
|
|
486
|
+
}
|
|
487
|
+
getState() {
|
|
488
|
+
return this.state;
|
|
489
|
+
}
|
|
490
|
+
async createSpace(e, t, n) {
|
|
491
|
+
const s = crypto.randomUUID(), o = this.repo.create(t);
|
|
492
|
+
await o.whenReady(), await this.groupKeyService.createKey(s), this.networkAdapter.registerDocument(o.documentId, s), this.networkAdapter.registerSelfPeer(s);
|
|
493
|
+
const i = {
|
|
494
|
+
id: s,
|
|
495
|
+
type: e,
|
|
496
|
+
name: n == null ? void 0 : n.name,
|
|
497
|
+
description: n == null ? void 0 : n.description,
|
|
498
|
+
appTag: n == null ? void 0 : n.appTag,
|
|
499
|
+
members: [this.identity.getDid()],
|
|
500
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
501
|
+
}, r = {
|
|
502
|
+
info: i,
|
|
503
|
+
documentId: o.documentId,
|
|
504
|
+
documentUrl: o.url,
|
|
505
|
+
handles: /* @__PURE__ */ new Set(),
|
|
506
|
+
memberEncryptionKeys: /* @__PURE__ */ new Map()
|
|
507
|
+
};
|
|
508
|
+
return this.spaces.set(s, r), this._notifySpacesSubscribers(), await this._persistSpaceMetadata(r), await this.repo.flush([o.documentId]), this._saveToCompactStore(r).catch(() => {
|
|
509
|
+
}), this._pushSnapshotToVault(r).catch(() => {
|
|
510
|
+
}), { ...i };
|
|
511
|
+
}
|
|
512
|
+
async getSpaces() {
|
|
513
|
+
return this._getSpacesSnapshot();
|
|
514
|
+
}
|
|
515
|
+
watchSpaces() {
|
|
516
|
+
return {
|
|
517
|
+
subscribe: (e) => (this.spacesSubscribers.add(e), () => {
|
|
518
|
+
this.spacesSubscribers.delete(e);
|
|
519
|
+
}),
|
|
520
|
+
getValue: () => this._getSpacesSnapshot()
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
_getSpacesSnapshot() {
|
|
524
|
+
return Array.from(this.spaces.values()).map((e) => ({ ...e.info }));
|
|
525
|
+
}
|
|
526
|
+
_notifySpacesSubscribers() {
|
|
527
|
+
const e = this._getSpacesSnapshot();
|
|
528
|
+
for (const t of this.spacesSubscribers)
|
|
529
|
+
t(e);
|
|
530
|
+
}
|
|
531
|
+
async getSpace(e) {
|
|
532
|
+
const t = this.spaces.get(e);
|
|
533
|
+
return t ? { ...t.info } : null;
|
|
534
|
+
}
|
|
535
|
+
async openSpace(e) {
|
|
536
|
+
const t = this.spaces.get(e);
|
|
537
|
+
if (!t)
|
|
538
|
+
throw new Error(`Unknown space: ${e}`);
|
|
539
|
+
const n = await Promise.race([
|
|
540
|
+
this.repo.find(t.documentUrl, {
|
|
541
|
+
allowableStates: ["ready", "unavailable"]
|
|
542
|
+
}),
|
|
543
|
+
new Promise(
|
|
544
|
+
(r, a) => setTimeout(() => a(new Error(`Space document timeout: ${e}`)), 1e4)
|
|
545
|
+
)
|
|
546
|
+
]);
|
|
547
|
+
if (!n.isReady())
|
|
548
|
+
throw new Error(`Space document not ready: ${e}`);
|
|
549
|
+
let s = this.vaultSchedulers.get(e) ?? null;
|
|
550
|
+
!s && this.vault && (s = new N({
|
|
551
|
+
pushFn: () => this._pushSnapshotToVault(t),
|
|
552
|
+
getHeadsFn: () => {
|
|
553
|
+
const r = n.doc();
|
|
554
|
+
return r ? D.getHeads(r).join(",") : null;
|
|
555
|
+
},
|
|
556
|
+
debounceMs: 5e3
|
|
557
|
+
}), this.vaultSchedulers.set(e, s));
|
|
558
|
+
let o = this.compactSchedulers.get(e) ?? null;
|
|
559
|
+
!o && this.compactStore && (o = new N({
|
|
560
|
+
pushFn: () => this._saveToCompactStore(t),
|
|
561
|
+
getHeadsFn: () => {
|
|
562
|
+
const r = n.doc();
|
|
563
|
+
return r ? D.getHeads(r).join(",") : null;
|
|
564
|
+
},
|
|
565
|
+
debounceMs: 2e3
|
|
566
|
+
}), this.compactSchedulers.set(e, o));
|
|
567
|
+
const i = new he(t, n, s, o);
|
|
568
|
+
return t.handles.add(i), i;
|
|
569
|
+
}
|
|
570
|
+
async addMember(e, t, n) {
|
|
571
|
+
const s = this.spaces.get(e);
|
|
572
|
+
if (!s) throw new Error(`Unknown space: ${e}`);
|
|
573
|
+
s.info.members.includes(t) || (s.info.members.push(t), this._notifySpacesSubscribers()), s.memberEncryptionKeys.set(t, n), this.networkAdapter.registerSpacePeer(e, t);
|
|
574
|
+
const o = this.groupKeyService.getCurrentKey(e);
|
|
575
|
+
if (!o) throw new Error(`No group key for space: ${e}`);
|
|
576
|
+
const i = this.groupKeyService.getCurrentGeneration(e), r = await this.identity.encryptForRecipient(
|
|
577
|
+
o,
|
|
578
|
+
n
|
|
579
|
+
), a = this.repo.handles[s.documentId], l = a == null ? void 0 : a.doc();
|
|
580
|
+
if (!l) throw new Error(`Cannot access doc for space: ${e}`);
|
|
581
|
+
const h = D.save(l), p = await P.encryptChange(
|
|
582
|
+
h,
|
|
583
|
+
o,
|
|
584
|
+
e,
|
|
585
|
+
i,
|
|
586
|
+
this.identity.getDid()
|
|
587
|
+
), m = {
|
|
588
|
+
spaceId: e,
|
|
589
|
+
spaceType: s.info.type,
|
|
590
|
+
spaceName: s.info.name,
|
|
591
|
+
appTag: s.info.appTag,
|
|
592
|
+
members: s.info.members,
|
|
593
|
+
createdAt: s.info.createdAt,
|
|
594
|
+
generation: i,
|
|
595
|
+
documentUrl: s.documentUrl,
|
|
596
|
+
encryptedGroupKey: {
|
|
597
|
+
ciphertext: Array.from(r.ciphertext),
|
|
598
|
+
nonce: Array.from(r.nonce),
|
|
599
|
+
ephemeralPublicKey: Array.from(r.ephemeralPublicKey)
|
|
600
|
+
},
|
|
601
|
+
encryptedDoc: {
|
|
602
|
+
ciphertext: Array.from(p.ciphertext),
|
|
603
|
+
nonce: Array.from(p.nonce)
|
|
604
|
+
}
|
|
605
|
+
}, y = {
|
|
606
|
+
v: 1,
|
|
607
|
+
id: crypto.randomUUID(),
|
|
608
|
+
type: "space-invite",
|
|
609
|
+
fromDid: this.identity.getDid(),
|
|
610
|
+
toDid: t,
|
|
611
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
612
|
+
encoding: "json",
|
|
613
|
+
payload: JSON.stringify(m),
|
|
614
|
+
signature: ""
|
|
615
|
+
};
|
|
616
|
+
await this._signAndSend(y);
|
|
617
|
+
for (const u of s.info.members) {
|
|
618
|
+
if (u === this.identity.getDid() || u === t) continue;
|
|
619
|
+
const g = {
|
|
620
|
+
spaceId: e,
|
|
621
|
+
action: "added",
|
|
622
|
+
memberDid: t,
|
|
623
|
+
members: s.info.members
|
|
624
|
+
}, w = {
|
|
625
|
+
v: 1,
|
|
626
|
+
id: crypto.randomUUID(),
|
|
627
|
+
type: "member-update",
|
|
628
|
+
fromDid: this.identity.getDid(),
|
|
629
|
+
toDid: u,
|
|
630
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
631
|
+
encoding: "json",
|
|
632
|
+
payload: JSON.stringify(g),
|
|
633
|
+
signature: ""
|
|
634
|
+
};
|
|
635
|
+
await this._signAndSend(w);
|
|
636
|
+
}
|
|
637
|
+
await this._persistSpaceMetadata(s), this._pushSnapshotToVault(s).catch(() => {
|
|
638
|
+
});
|
|
639
|
+
for (const u of this.memberChangeCallbacks)
|
|
640
|
+
u({ spaceId: e, did: t, action: "added" });
|
|
641
|
+
}
|
|
642
|
+
async removeMember(e, t) {
|
|
643
|
+
const n = this.spaces.get(e);
|
|
644
|
+
if (!n) throw new Error(`Unknown space: ${e}`);
|
|
645
|
+
n.info.members = n.info.members.filter((r) => r !== t), n.memberEncryptionKeys.delete(t), this._notifySpacesSubscribers(), this.networkAdapter.unregisterSpacePeer(e, t);
|
|
646
|
+
const s = await this.groupKeyService.rotateKey(e), o = this.groupKeyService.getCurrentGeneration(e);
|
|
647
|
+
for (const [r, a] of n.memberEncryptionKeys.entries()) {
|
|
648
|
+
if (r === this.identity.getDid()) continue;
|
|
649
|
+
const l = await this.identity.encryptForRecipient(s, a), h = {
|
|
650
|
+
spaceId: e,
|
|
651
|
+
generation: o,
|
|
652
|
+
encryptedGroupKey: {
|
|
653
|
+
ciphertext: Array.from(l.ciphertext),
|
|
654
|
+
nonce: Array.from(l.nonce),
|
|
655
|
+
ephemeralPublicKey: Array.from(l.ephemeralPublicKey)
|
|
656
|
+
}
|
|
657
|
+
}, p = {
|
|
658
|
+
v: 1,
|
|
659
|
+
id: crypto.randomUUID(),
|
|
660
|
+
type: "group-key-rotation",
|
|
661
|
+
fromDid: this.identity.getDid(),
|
|
662
|
+
toDid: r,
|
|
663
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
664
|
+
encoding: "json",
|
|
665
|
+
payload: JSON.stringify(h),
|
|
666
|
+
signature: ""
|
|
667
|
+
};
|
|
668
|
+
await this._signAndSend(p);
|
|
669
|
+
}
|
|
670
|
+
const i = [...n.info.members, t];
|
|
671
|
+
for (const r of i) {
|
|
672
|
+
if (r === this.identity.getDid()) continue;
|
|
673
|
+
const a = {
|
|
674
|
+
spaceId: e,
|
|
675
|
+
action: "removed",
|
|
676
|
+
memberDid: t,
|
|
677
|
+
members: n.info.members
|
|
678
|
+
}, l = {
|
|
679
|
+
v: 1,
|
|
680
|
+
id: crypto.randomUUID(),
|
|
681
|
+
type: "member-update",
|
|
682
|
+
fromDid: this.identity.getDid(),
|
|
683
|
+
toDid: r,
|
|
684
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
685
|
+
encoding: "json",
|
|
686
|
+
payload: JSON.stringify(a),
|
|
687
|
+
signature: ""
|
|
688
|
+
};
|
|
689
|
+
await this._signAndSend(l);
|
|
690
|
+
}
|
|
691
|
+
await this._persistSpaceMetadata(n);
|
|
692
|
+
for (const r of this.memberChangeCallbacks)
|
|
693
|
+
r({ spaceId: e, did: t, action: "removed" });
|
|
694
|
+
}
|
|
695
|
+
onMemberChange(e) {
|
|
696
|
+
return this.memberChangeCallbacks.add(e), () => {
|
|
697
|
+
this.memberChangeCallbacks.delete(e);
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
async leaveSpace(e) {
|
|
701
|
+
throw new Error("leaveSpace not implemented for Automerge adapter");
|
|
702
|
+
}
|
|
703
|
+
async updateSpace(e, t) {
|
|
704
|
+
throw new Error("updateSpace not implemented for Automerge adapter");
|
|
705
|
+
}
|
|
706
|
+
getKeyGeneration(e) {
|
|
707
|
+
return this.groupKeyService.getCurrentGeneration(e);
|
|
708
|
+
}
|
|
709
|
+
async requestSync(e) {
|
|
710
|
+
}
|
|
711
|
+
async _persistSpaceMetadata(e) {
|
|
712
|
+
if (!this.metadataStorage) return;
|
|
713
|
+
const t = {};
|
|
714
|
+
for (const [s, o] of e.memberEncryptionKeys.entries())
|
|
715
|
+
t[s] = o;
|
|
716
|
+
await this.metadataStorage.saveSpaceMetadata({
|
|
717
|
+
info: e.info,
|
|
718
|
+
documentId: e.documentId,
|
|
719
|
+
documentUrl: e.documentUrl,
|
|
720
|
+
memberEncryptionKeys: t
|
|
721
|
+
});
|
|
722
|
+
const n = this.groupKeyService.getCurrentGeneration(e.info.id);
|
|
723
|
+
for (let s = 0; s <= n; s++) {
|
|
724
|
+
const o = this.groupKeyService.getKeyByGeneration(e.info.id, s);
|
|
725
|
+
o && o.length > 0 && await this.metadataStorage.saveGroupKey({ spaceId: e.info.id, generation: s, key: o });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async handleMessage(e) {
|
|
729
|
+
if (e.signature && !await X(e)) {
|
|
730
|
+
console.warn("[ReplicationAdapter] Rejected message with invalid signature from", e.fromDid);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
switch (e.type) {
|
|
734
|
+
case "space-invite":
|
|
735
|
+
await this.handleSpaceInvite(e);
|
|
736
|
+
break;
|
|
737
|
+
case "group-key-rotation":
|
|
738
|
+
await this.handleKeyRotation(e);
|
|
739
|
+
break;
|
|
740
|
+
case "member-update":
|
|
741
|
+
await this.handleMemberUpdate(e);
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async handleSpaceInvite(e) {
|
|
746
|
+
const t = JSON.parse(e.payload), n = {
|
|
747
|
+
ciphertext: new Uint8Array(t.encryptedGroupKey.ciphertext),
|
|
748
|
+
nonce: new Uint8Array(t.encryptedGroupKey.nonce),
|
|
749
|
+
ephemeralPublicKey: new Uint8Array(t.encryptedGroupKey.ephemeralPublicKey)
|
|
750
|
+
}, s = await this.identity.decryptForMe(n);
|
|
751
|
+
this.groupKeyService.importKey(t.spaceId, s, t.generation);
|
|
752
|
+
const o = {
|
|
753
|
+
ciphertext: new Uint8Array(t.encryptedDoc.ciphertext),
|
|
754
|
+
nonce: new Uint8Array(t.encryptedDoc.nonce),
|
|
755
|
+
spaceId: t.spaceId,
|
|
756
|
+
generation: t.generation,
|
|
757
|
+
fromDid: e.fromDid
|
|
758
|
+
}, i = await P.decryptChange(o, s), { documentId: r } = Y(t.documentUrl), a = this.repo.import(i, { docId: r });
|
|
759
|
+
a.isReady() || a.doneLoading(), this.networkAdapter.registerDocument(a.documentId, t.spaceId);
|
|
760
|
+
const l = t.members || [];
|
|
761
|
+
for (const m of l)
|
|
762
|
+
m !== this.identity.getDid() && this.networkAdapter.registerSpacePeer(t.spaceId, m);
|
|
763
|
+
this.networkAdapter.registerSelfPeer(t.spaceId);
|
|
764
|
+
const p = {
|
|
765
|
+
info: {
|
|
766
|
+
id: t.spaceId,
|
|
767
|
+
type: t.spaceType,
|
|
768
|
+
name: t.spaceName,
|
|
769
|
+
appTag: t.appTag,
|
|
770
|
+
members: l,
|
|
771
|
+
createdAt: t.createdAt
|
|
772
|
+
},
|
|
773
|
+
documentId: a.documentId,
|
|
774
|
+
documentUrl: a.url,
|
|
775
|
+
handles: /* @__PURE__ */ new Set(),
|
|
776
|
+
memberEncryptionKeys: /* @__PURE__ */ new Map()
|
|
777
|
+
};
|
|
778
|
+
this.spaces.set(t.spaceId, p), this._notifySpacesSubscribers(), await this._persistSpaceMetadata(p), await this.repo.flush([a.documentId]), this._saveToCompactStore(p).catch(() => {
|
|
779
|
+
}), this._pushSnapshotToVault(p).catch(() => {
|
|
780
|
+
});
|
|
781
|
+
for (const m of this.memberChangeCallbacks)
|
|
782
|
+
m({ spaceId: t.spaceId, did: this.identity.getDid(), action: "added" });
|
|
783
|
+
}
|
|
784
|
+
async handleKeyRotation(e) {
|
|
785
|
+
const t = JSON.parse(e.payload), n = this.spaces.get(t.spaceId);
|
|
786
|
+
if (n && !n.info.members.includes(e.fromDid)) {
|
|
787
|
+
console.warn("[ReplicationAdapter] Rejected key-rotation from non-member:", e.fromDid);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const s = {
|
|
791
|
+
ciphertext: new Uint8Array(t.encryptedGroupKey.ciphertext),
|
|
792
|
+
nonce: new Uint8Array(t.encryptedGroupKey.nonce),
|
|
793
|
+
ephemeralPublicKey: new Uint8Array(t.encryptedGroupKey.ephemeralPublicKey)
|
|
794
|
+
}, o = await this.identity.decryptForMe(s);
|
|
795
|
+
this.groupKeyService.importKey(t.spaceId, o, t.generation), n && await this._persistSpaceMetadata(n);
|
|
796
|
+
}
|
|
797
|
+
async handleMemberUpdate(e) {
|
|
798
|
+
const t = JSON.parse(e.payload), n = this.spaces.get(t.spaceId);
|
|
799
|
+
if (!n) return;
|
|
800
|
+
if (t.action === "removed") {
|
|
801
|
+
if (e.fromDid !== n.info.members[0]) {
|
|
802
|
+
console.warn("[ReplicationAdapter] Rejected member removal from non-creator:", e.fromDid);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
} else if (!n.info.members.includes(e.fromDid)) {
|
|
806
|
+
console.warn("[ReplicationAdapter] Rejected member-update from non-member:", e.fromDid);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
const s = this.identity.getDid();
|
|
810
|
+
if (t.action === "removed" && t.memberDid === s && !t.members.includes(s)) {
|
|
811
|
+
console.log("[ReplicationAdapter] Removed from space:", n.info.name || n.info.id);
|
|
812
|
+
for (const l of n.handles)
|
|
813
|
+
l.close();
|
|
814
|
+
for (const l of n.info.members)
|
|
815
|
+
l !== s && this.networkAdapter.unregisterSpacePeer(t.spaceId, l);
|
|
816
|
+
this.networkAdapter.unregisterDocument(n.documentId), this.spaces.delete(t.spaceId), this.metadataStorage && (await this.metadataStorage.deleteSpaceMetadata(t.spaceId), await this.metadataStorage.deleteGroupKeys(t.spaceId));
|
|
817
|
+
const r = this.vaultSchedulers.get(t.spaceId);
|
|
818
|
+
r && (r.destroy(), this.vaultSchedulers.delete(t.spaceId));
|
|
819
|
+
const a = this.compactSchedulers.get(t.spaceId);
|
|
820
|
+
a && (a.destroy(), this.compactSchedulers.delete(t.spaceId)), this.compactStore && this.compactStore.delete(t.spaceId).catch(() => {
|
|
821
|
+
}), this.vaultSeqs.delete(t.spaceId), this._notifySpacesSubscribers();
|
|
822
|
+
for (const l of this.memberChangeCallbacks)
|
|
823
|
+
l({ spaceId: t.spaceId, did: s, action: "removed" });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const i = new Set(n.info.members);
|
|
827
|
+
n.info.members = t.members, this._notifySpacesSubscribers();
|
|
828
|
+
for (const r of t.members)
|
|
829
|
+
r !== s && !i.has(r) && this.networkAdapter.registerSpacePeer(t.spaceId, r);
|
|
830
|
+
for (const r of i)
|
|
831
|
+
t.members.includes(r) || this.networkAdapter.unregisterSpacePeer(t.spaceId, r);
|
|
832
|
+
await this._persistSpaceMetadata(n);
|
|
833
|
+
for (const r of t.members)
|
|
834
|
+
if (!i.has(r))
|
|
835
|
+
for (const a of this.memberChangeCallbacks)
|
|
836
|
+
a({ spaceId: t.spaceId, did: r, action: "added" });
|
|
837
|
+
for (const r of i)
|
|
838
|
+
if (!t.members.includes(r))
|
|
839
|
+
for (const a of this.memberChangeCallbacks)
|
|
840
|
+
a({ spaceId: t.spaceId, did: r, action: "removed" });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
class ue extends W {
|
|
844
|
+
constructor(t, n, s) {
|
|
845
|
+
super();
|
|
846
|
+
d(this, "messaging");
|
|
847
|
+
d(this, "personalKey");
|
|
848
|
+
d(this, "myDid");
|
|
849
|
+
d(this, "documentId", null);
|
|
850
|
+
d(this, "ready", !1);
|
|
851
|
+
d(this, "readyResolve");
|
|
852
|
+
d(this, "readyPromise");
|
|
853
|
+
d(this, "unsubMessage", null);
|
|
854
|
+
/** Track message IDs we sent, so we can ignore our own echoes from the relay */
|
|
855
|
+
d(this, "sentMessageIds", /* @__PURE__ */ new Set());
|
|
856
|
+
/** Gate incoming messages until the doc handle is confirmed ready (avoids automerge-repo 60s timeout) */
|
|
857
|
+
d(this, "docReady", !1);
|
|
858
|
+
this.messaging = t, this.personalKey = n, this.myDid = s, this.readyPromise = new Promise((o) => {
|
|
859
|
+
this.readyResolve = o;
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
/** Register the personal document ID for routing */
|
|
863
|
+
setDocumentId(t) {
|
|
864
|
+
this.documentId = t;
|
|
865
|
+
}
|
|
866
|
+
/** Signal that the doc handle is ready — incoming messages will be emitted to the repo */
|
|
867
|
+
setDocReady() {
|
|
868
|
+
this.docReady = !0;
|
|
869
|
+
}
|
|
870
|
+
// --- NetworkAdapter interface ---
|
|
871
|
+
isReady() {
|
|
872
|
+
return this.ready;
|
|
873
|
+
}
|
|
874
|
+
whenReady() {
|
|
875
|
+
return this.readyPromise;
|
|
876
|
+
}
|
|
877
|
+
connect(t) {
|
|
878
|
+
var n;
|
|
879
|
+
this.peerId = t, this.unsubMessage = this.messaging.onMessage(async (s) => {
|
|
880
|
+
if (s.type === "personal-sync" && !(!this.documentId || !this.docReady)) {
|
|
881
|
+
if (this.sentMessageIds.has(s.id)) {
|
|
882
|
+
this.sentMessageIds.delete(s.id);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
const o = JSON.parse(s.payload), i = {
|
|
887
|
+
ciphertext: new Uint8Array(o.ciphertext),
|
|
888
|
+
nonce: new Uint8Array(o.nonce),
|
|
889
|
+
spaceId: "__personal__",
|
|
890
|
+
generation: 0,
|
|
891
|
+
fromDid: s.fromDid
|
|
892
|
+
}, r = await P.decryptChange(i, this.personalKey), a = {
|
|
893
|
+
type: o.messageType || "sync",
|
|
894
|
+
senderId: s.fromDid,
|
|
895
|
+
targetId: this.peerId,
|
|
896
|
+
documentId: this.documentId,
|
|
897
|
+
data: r
|
|
898
|
+
};
|
|
899
|
+
this.emit("message", a);
|
|
900
|
+
} catch (o) {
|
|
901
|
+
console.debug("[PersonalNetworkAdapter] Failed to process message:", o);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}), this.ready = !0, (n = this.readyResolve) == null || n.call(this), this.emit("ready", void 0);
|
|
905
|
+
}
|
|
906
|
+
send(t) {
|
|
907
|
+
this.ready && (!t.data || !this.documentId || t.documentId === this.documentId && (async () => {
|
|
908
|
+
try {
|
|
909
|
+
const n = await P.encryptChange(
|
|
910
|
+
t.data,
|
|
911
|
+
this.personalKey,
|
|
912
|
+
"__personal__",
|
|
913
|
+
0,
|
|
914
|
+
this.myDid
|
|
915
|
+
), s = {
|
|
916
|
+
messageType: t.type,
|
|
917
|
+
ciphertext: Array.from(n.ciphertext),
|
|
918
|
+
nonce: Array.from(n.nonce)
|
|
919
|
+
}, o = crypto.randomUUID();
|
|
920
|
+
this.sentMessageIds.add(o), setTimeout(() => this.sentMessageIds.delete(o), 3e4);
|
|
921
|
+
const i = {
|
|
922
|
+
v: 1,
|
|
923
|
+
id: o,
|
|
924
|
+
type: "personal-sync",
|
|
925
|
+
fromDid: this.myDid,
|
|
926
|
+
toDid: this.myDid,
|
|
927
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
928
|
+
encoding: "json",
|
|
929
|
+
payload: JSON.stringify(s),
|
|
930
|
+
signature: ""
|
|
931
|
+
};
|
|
932
|
+
await this.messaging.send(i);
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
})());
|
|
936
|
+
}
|
|
937
|
+
disconnect() {
|
|
938
|
+
this.unsubMessage && (this.unsubMessage(), this.unsubMessage = null), this.sentMessageIds.clear(), this.docReady = !1, this.ready = !1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
class pe {
|
|
942
|
+
constructor(e = "wot-sync-states") {
|
|
943
|
+
d(this, "dbName");
|
|
944
|
+
d(this, "db", null);
|
|
945
|
+
d(this, "readyPromise");
|
|
946
|
+
this.dbName = e, this.readyPromise = this.open();
|
|
947
|
+
}
|
|
948
|
+
/** Wait until the IDB is open. */
|
|
949
|
+
async ready() {
|
|
950
|
+
return this.readyPromise;
|
|
951
|
+
}
|
|
952
|
+
async open() {
|
|
953
|
+
return new Promise((e, t) => {
|
|
954
|
+
const n = indexedDB.open(this.dbName, 1);
|
|
955
|
+
n.onupgradeneeded = () => {
|
|
956
|
+
const s = n.result;
|
|
957
|
+
s.objectStoreNames.contains("sync-states") || s.createObjectStore("sync-states");
|
|
958
|
+
}, n.onsuccess = () => {
|
|
959
|
+
this.db = n.result, e();
|
|
960
|
+
}, n.onerror = () => t(n.error);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
isSyncState(e) {
|
|
964
|
+
return e.some((t) => t === "sync-state");
|
|
965
|
+
}
|
|
966
|
+
keyToString(e) {
|
|
967
|
+
return JSON.stringify(e);
|
|
968
|
+
}
|
|
969
|
+
stringToKey(e) {
|
|
970
|
+
return JSON.parse(e);
|
|
971
|
+
}
|
|
972
|
+
keyMatchesPrefix(e, t) {
|
|
973
|
+
if (e.length < t.length) return !1;
|
|
974
|
+
for (let n = 0; n < t.length; n++)
|
|
975
|
+
if (e[n] !== t[n]) return !1;
|
|
976
|
+
return !0;
|
|
977
|
+
}
|
|
978
|
+
async load(e) {
|
|
979
|
+
if (!this.isSyncState(e)) return;
|
|
980
|
+
await this.readyPromise;
|
|
981
|
+
const t = this.db;
|
|
982
|
+
return new Promise((n, s) => {
|
|
983
|
+
const r = t.transaction("sync-states", "readonly").objectStore("sync-states").get(this.keyToString(e));
|
|
984
|
+
r.onsuccess = () => n(r.result ?? void 0), r.onerror = () => s(r.error);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
async save(e, t) {
|
|
988
|
+
if (!this.isSyncState(e)) return;
|
|
989
|
+
await this.readyPromise;
|
|
990
|
+
const n = this.db;
|
|
991
|
+
return new Promise((s, o) => {
|
|
992
|
+
const a = n.transaction("sync-states", "readwrite").objectStore("sync-states").put(t, this.keyToString(e));
|
|
993
|
+
a.onsuccess = () => s(), a.onerror = () => o(a.error);
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
async remove(e) {
|
|
997
|
+
if (!this.isSyncState(e)) return;
|
|
998
|
+
await this.readyPromise;
|
|
999
|
+
const t = this.db;
|
|
1000
|
+
return new Promise((n, s) => {
|
|
1001
|
+
const r = t.transaction("sync-states", "readwrite").objectStore("sync-states").delete(this.keyToString(e));
|
|
1002
|
+
r.onsuccess = () => n(), r.onerror = () => s(r.error);
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
async loadRange(e) {
|
|
1006
|
+
await this.readyPromise;
|
|
1007
|
+
const t = this.db;
|
|
1008
|
+
return new Promise((n, s) => {
|
|
1009
|
+
const i = t.transaction("sync-states", "readonly").objectStore("sync-states"), r = [], a = i.openCursor();
|
|
1010
|
+
a.onsuccess = () => {
|
|
1011
|
+
const l = a.result;
|
|
1012
|
+
if (l) {
|
|
1013
|
+
const h = this.stringToKey(l.key);
|
|
1014
|
+
this.keyMatchesPrefix(h, e) && r.push({ key: h, data: l.value }), l.continue();
|
|
1015
|
+
} else
|
|
1016
|
+
n(r);
|
|
1017
|
+
}, a.onerror = () => s(a.error);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
async removeRange(e) {
|
|
1021
|
+
await this.readyPromise;
|
|
1022
|
+
const t = this.db;
|
|
1023
|
+
return new Promise((n, s) => {
|
|
1024
|
+
const r = t.transaction("sync-states", "readwrite").objectStore("sync-states").openCursor();
|
|
1025
|
+
r.onsuccess = () => {
|
|
1026
|
+
const a = r.result;
|
|
1027
|
+
if (a) {
|
|
1028
|
+
const l = this.stringToKey(a.key);
|
|
1029
|
+
this.keyMatchesPrefix(l, e) && a.delete(), a.continue();
|
|
1030
|
+
} else
|
|
1031
|
+
n();
|
|
1032
|
+
}, r.onerror = () => s(r.error);
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
close() {
|
|
1036
|
+
this.db && (this.db.close(), this.db = null);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const ye = 20;
|
|
1040
|
+
async function ge(c, e) {
|
|
1041
|
+
try {
|
|
1042
|
+
const t = await new Promise((o) => {
|
|
1043
|
+
const i = indexedDB.open(c, 1);
|
|
1044
|
+
i.onerror = () => o(0), i.onupgradeneeded = (r) => {
|
|
1045
|
+
const a = r.target.result;
|
|
1046
|
+
a.objectStoreNames.contains(e) || a.createObjectStore(e);
|
|
1047
|
+
}, i.onsuccess = () => {
|
|
1048
|
+
const r = i.result;
|
|
1049
|
+
try {
|
|
1050
|
+
const h = r.transaction(e, "readonly").objectStore(e).count();
|
|
1051
|
+
h.onsuccess = () => {
|
|
1052
|
+
r.close(), o(h.result);
|
|
1053
|
+
}, h.onerror = () => {
|
|
1054
|
+
r.close(), o(0);
|
|
1055
|
+
};
|
|
1056
|
+
} catch {
|
|
1057
|
+
r.close(), o(0);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
}), n = x();
|
|
1061
|
+
n.setIdbChunkCount(t), console.log(`[personal-doc] IndexedDB chunk count: ${t}`);
|
|
1062
|
+
const s = t <= ye;
|
|
1063
|
+
return n.setHealthCheckResult(s), s;
|
|
1064
|
+
} catch {
|
|
1065
|
+
return !0;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
async function me(c, e) {
|
|
1069
|
+
return new Promise((t) => {
|
|
1070
|
+
const n = indexedDB.open(c, 1);
|
|
1071
|
+
let s = !1;
|
|
1072
|
+
n.onupgradeneeded = () => {
|
|
1073
|
+
s = !0;
|
|
1074
|
+
const o = n.result;
|
|
1075
|
+
o.objectStoreNames.contains(e) || o.createObjectStore(e);
|
|
1076
|
+
}, n.onsuccess = () => {
|
|
1077
|
+
const o = n.result;
|
|
1078
|
+
if (s) {
|
|
1079
|
+
o.close(), indexedDB.deleteDatabase(c), t(!1);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const a = o.transaction(e, "readonly").objectStore(e).count();
|
|
1084
|
+
a.onsuccess = () => {
|
|
1085
|
+
o.close(), t(a.result > 0);
|
|
1086
|
+
}, a.onerror = () => {
|
|
1087
|
+
o.close(), t(!1);
|
|
1088
|
+
};
|
|
1089
|
+
} catch {
|
|
1090
|
+
o.close(), t(!1);
|
|
1091
|
+
}
|
|
1092
|
+
}, n.onerror = () => t(!1);
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
const G = "wot-personal-doc", q = "doc", fe = "personal";
|
|
1096
|
+
async function we() {
|
|
1097
|
+
return new Promise((c) => {
|
|
1098
|
+
const e = indexedDB.open(G, 1);
|
|
1099
|
+
e.onupgradeneeded = () => {
|
|
1100
|
+
e.result.close(), c(null);
|
|
1101
|
+
}, e.onsuccess = () => {
|
|
1102
|
+
const t = e.result;
|
|
1103
|
+
if (!t.objectStoreNames.contains(q)) {
|
|
1104
|
+
t.close(), c(null);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const o = t.transaction(q, "readonly").objectStore(q).get(fe);
|
|
1108
|
+
o.onsuccess = () => {
|
|
1109
|
+
t.close(), c(o.result ?? null);
|
|
1110
|
+
}, o.onerror = () => {
|
|
1111
|
+
t.close(), c(null);
|
|
1112
|
+
};
|
|
1113
|
+
}, e.onerror = () => c(null);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
async function be() {
|
|
1117
|
+
return new Promise((c) => {
|
|
1118
|
+
const e = indexedDB.deleteDatabase(G);
|
|
1119
|
+
e.onsuccess = () => c(), e.onerror = () => c();
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
let I = null, v = null, C = null, B = /* @__PURE__ */ new Set(), $ = !1, O = null, U = null, F = 0, S = null, k = null, b = null;
|
|
1123
|
+
const A = "__personal__", ee = "wot-compact-store", te = "wot-personal-sync-states";
|
|
1124
|
+
function z() {
|
|
1125
|
+
return {
|
|
1126
|
+
profile: null,
|
|
1127
|
+
contacts: {},
|
|
1128
|
+
verifications: {},
|
|
1129
|
+
attestations: {},
|
|
1130
|
+
attestationMetadata: {},
|
|
1131
|
+
outbox: {},
|
|
1132
|
+
spaces: {},
|
|
1133
|
+
groupKeys: {}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function ne() {
|
|
1137
|
+
for (const c of B)
|
|
1138
|
+
try {
|
|
1139
|
+
c();
|
|
1140
|
+
} catch {
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
async function Se(c) {
|
|
1144
|
+
const e = await c.deriveFrameworkKey("personal-doc-v1"), t = e.slice(0, 16), n = ie(t), { documentId: s } = Y(n);
|
|
1145
|
+
return { documentId: s, documentUrl: n, personalKey: e };
|
|
1146
|
+
}
|
|
1147
|
+
async function De(c, e) {
|
|
1148
|
+
var t, n;
|
|
1149
|
+
try {
|
|
1150
|
+
const s = await c.getChanges(A);
|
|
1151
|
+
if ((t = s.snapshot) != null && t.data) {
|
|
1152
|
+
const o = E(s.snapshot.data), i = o[0], r = o.slice(1, 1 + i), a = o.slice(1 + i), l = await P.decryptChange(
|
|
1153
|
+
{ ciphertext: a, nonce: r, spaceId: A, generation: 0, fromDid: "" },
|
|
1154
|
+
e
|
|
1155
|
+
);
|
|
1156
|
+
return F = ((n = s.snapshot) == null ? void 0 : n.upToSeq) ?? 0, l;
|
|
1157
|
+
}
|
|
1158
|
+
} catch (s) {
|
|
1159
|
+
console.warn("[personal-doc] Vault snapshot decrypt failed — deleting and falling back to wot-profiles restore"), x().logError("load:vault:decrypt-failed", s);
|
|
1160
|
+
try {
|
|
1161
|
+
await c.deleteDoc(A), console.debug("[personal-doc] Deleted undecryptable vault snapshot — fresh snapshot will be pushed after restore");
|
|
1162
|
+
} catch (o) {
|
|
1163
|
+
x().logError("delete:vault:cleanup", o);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
async function Ie() {
|
|
1169
|
+
if (!(!k || !I))
|
|
1170
|
+
try {
|
|
1171
|
+
const c = I.doc();
|
|
1172
|
+
if (!c) return;
|
|
1173
|
+
const e = Date.now(), t = D.save(c), n = Date.now() - e;
|
|
1174
|
+
if (!t || t.length === 0) return;
|
|
1175
|
+
const s = Date.now();
|
|
1176
|
+
await k.save(A, t), x().logSave("compact-store", Date.now() - s, t.length, n);
|
|
1177
|
+
const o = T.getInstance(), i = await o.compact(t);
|
|
1178
|
+
i && i.length > 0 && (await k.save(A, i), console.debug(`[personal-doc] Compacted: ${t.length}B → ${i.length}B (worker=${o.isUsingWorker})`));
|
|
1179
|
+
} catch (c) {
|
|
1180
|
+
x().logError("save:compact-store", c);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
async function Pe() {
|
|
1184
|
+
if (!(!O || !U || !v || !I))
|
|
1185
|
+
try {
|
|
1186
|
+
const c = I.doc();
|
|
1187
|
+
if (!c) return;
|
|
1188
|
+
if (!(c.profile || Object.keys(c.contacts).length > 0 || Object.keys(c.spaces).length > 0)) {
|
|
1189
|
+
console.debug("[personal-doc] Skip vault push — no meaningful data yet");
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
const t = D.save(c);
|
|
1193
|
+
if (!t || t.length === 0) return;
|
|
1194
|
+
const s = await T.getInstance().compact(t);
|
|
1195
|
+
if (!s || s.length === 0) return;
|
|
1196
|
+
const o = await P.encryptChange(
|
|
1197
|
+
s,
|
|
1198
|
+
U,
|
|
1199
|
+
A,
|
|
1200
|
+
0,
|
|
1201
|
+
""
|
|
1202
|
+
);
|
|
1203
|
+
F++;
|
|
1204
|
+
const i = Date.now();
|
|
1205
|
+
await O.putSnapshot(A, o.ciphertext, o.nonce, F), x().logSave("vault", Date.now() - i, s.length);
|
|
1206
|
+
} catch (c) {
|
|
1207
|
+
x().logError("save:vault", c);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function Re(c, e, t) {
|
|
1211
|
+
if (I && v) {
|
|
1212
|
+
const y = I.doc();
|
|
1213
|
+
if (y) return y;
|
|
1214
|
+
}
|
|
1215
|
+
const n = Date.now(), { documentId: s, documentUrl: o, personalKey: i } = await Se(c), r = c.getDid();
|
|
1216
|
+
t && (O = new Z(t, c), U = i), k = new ce(ee), await k.open();
|
|
1217
|
+
const a = new pe(te);
|
|
1218
|
+
v = new L({
|
|
1219
|
+
peerId: `${r}-personal`,
|
|
1220
|
+
network: [],
|
|
1221
|
+
storage: a,
|
|
1222
|
+
sharePolicy: async () => !0
|
|
1223
|
+
});
|
|
1224
|
+
const l = x();
|
|
1225
|
+
l.setImpl("compact-store"), de(l);
|
|
1226
|
+
let h, p = "";
|
|
1227
|
+
try {
|
|
1228
|
+
const y = Date.now(), u = await k.load(A), g = Date.now();
|
|
1229
|
+
if (u && u.length > 0) {
|
|
1230
|
+
h = v.import(u, { docId: s });
|
|
1231
|
+
const w = Date.now();
|
|
1232
|
+
h.isReady() || h.doneLoading();
|
|
1233
|
+
const f = h.doc(), R = Date.now();
|
|
1234
|
+
if (console.debug(`[personal-doc] CompactStore load breakdown: idb=${g - y}ms import=${w - g}ms doc=${R - w}ms size=${u.length}B`), f && typeof f == "object") {
|
|
1235
|
+
p = "compact-store";
|
|
1236
|
+
const _ = Date.now() - y;
|
|
1237
|
+
l.logLoad("compact-store", _, u.length, {
|
|
1238
|
+
contacts: Object.keys(f.contacts ?? {}).length,
|
|
1239
|
+
attestations: Object.keys(f.attestations ?? {}).length,
|
|
1240
|
+
spaces: Object.keys(f.spaces ?? {}).length
|
|
1241
|
+
}), l.setDocStats(
|
|
1242
|
+
u.length,
|
|
1243
|
+
Object.keys(f.contacts ?? {}).length,
|
|
1244
|
+
Object.keys(f.attestations ?? {}).length,
|
|
1245
|
+
Object.keys(f.spaces ?? {}).length
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
} catch (y) {
|
|
1250
|
+
l.logError("load:compact-store", y);
|
|
1251
|
+
}
|
|
1252
|
+
if (!p)
|
|
1253
|
+
try {
|
|
1254
|
+
const y = Date.now(), u = await me("automerge-personal", "documents");
|
|
1255
|
+
if (console.debug(`[personal-doc] Migration check: exists=${u} took=${Date.now() - y}ms`), u) {
|
|
1256
|
+
if (await ge("automerge-personal", "documents")) {
|
|
1257
|
+
const { IndexedDBStorageAdapter: w } = await import("./index-Dz-MvOzM.js"), f = new L({
|
|
1258
|
+
peerId: `${r}-migration`,
|
|
1259
|
+
network: [],
|
|
1260
|
+
storage: new w("automerge-personal"),
|
|
1261
|
+
sharePolicy: async () => !0
|
|
1262
|
+
});
|
|
1263
|
+
try {
|
|
1264
|
+
const R = Date.now(), K = (await Promise.race([
|
|
1265
|
+
f.find(o),
|
|
1266
|
+
new Promise((j, H) => setTimeout(() => H(new Error("migration find timeout")), 8e3))
|
|
1267
|
+
])).doc();
|
|
1268
|
+
if (K && typeof K == "object") {
|
|
1269
|
+
const j = D.save(K);
|
|
1270
|
+
await k.save(A, j), h = v.import(j, { docId: s }), h.isReady() || h.doneLoading(), p = "migration";
|
|
1271
|
+
const H = Date.now() - R, V = l._idbChunkCount ?? 0;
|
|
1272
|
+
l.logMigration(typeof V == "number" ? V : 0, j.length), l.logLoad("migration", H, j.length, {
|
|
1273
|
+
contacts: Object.keys(K.contacts ?? {}).length,
|
|
1274
|
+
attestations: Object.keys(K.attestations ?? {}).length,
|
|
1275
|
+
spaces: Object.keys(K.spaces ?? {}).length,
|
|
1276
|
+
source: "automerge-personal"
|
|
1277
|
+
}), l.setDocStats(
|
|
1278
|
+
j.length,
|
|
1279
|
+
Object.keys(K.contacts ?? {}).length,
|
|
1280
|
+
Object.keys(K.attestations ?? {}).length,
|
|
1281
|
+
Object.keys(K.spaces ?? {}).length
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
} finally {
|
|
1285
|
+
try {
|
|
1286
|
+
f.shutdown();
|
|
1287
|
+
} catch {
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
await new Promise((w) => {
|
|
1292
|
+
const f = indexedDB.deleteDatabase("automerge-personal");
|
|
1293
|
+
f.onsuccess = () => w(), f.onerror = () => w(), f.onblocked = () => w();
|
|
1294
|
+
}).catch(() => {
|
|
1295
|
+
}), console.debug("[personal-doc] Cleaned up old automerge-personal IDB");
|
|
1296
|
+
}
|
|
1297
|
+
} catch (y) {
|
|
1298
|
+
l.logError("load:migration", y);
|
|
1299
|
+
}
|
|
1300
|
+
if (!p && O && U) {
|
|
1301
|
+
const y = Date.now(), u = await De(O, U);
|
|
1302
|
+
if (u && u.length > 0) {
|
|
1303
|
+
h = v.import(u, { docId: s }), h.isReady() || h.doneLoading();
|
|
1304
|
+
const g = h.doc();
|
|
1305
|
+
if (g && typeof g == "object") {
|
|
1306
|
+
p = "vault";
|
|
1307
|
+
const w = Date.now() - y;
|
|
1308
|
+
l.logLoad("vault", w, u.length, {
|
|
1309
|
+
contacts: Object.keys(g.contacts ?? {}).length,
|
|
1310
|
+
attestations: Object.keys(g.attestations ?? {}).length,
|
|
1311
|
+
spaces: Object.keys(g.spaces ?? {}).length
|
|
1312
|
+
}), l.setDocStats(
|
|
1313
|
+
u.length,
|
|
1314
|
+
Object.keys(g.contacts ?? {}).length,
|
|
1315
|
+
Object.keys(g.attestations ?? {}).length,
|
|
1316
|
+
Object.keys(g.spaces ?? {}).length
|
|
1317
|
+
), await k.save(A, u);
|
|
1318
|
+
}
|
|
1319
|
+
} else
|
|
1320
|
+
console.debug("[personal-doc] Vault has no data");
|
|
1321
|
+
}
|
|
1322
|
+
if (!p) {
|
|
1323
|
+
const y = await we();
|
|
1324
|
+
if (y) {
|
|
1325
|
+
const u = { ...z(), ...y };
|
|
1326
|
+
h = v.import(new Uint8Array(0), { docId: s }), h.isReady() || h.doneLoading(), h.change((g) => {
|
|
1327
|
+
Object.assign(g, u);
|
|
1328
|
+
}), p = "migration", l.logLoad("migration", 0, 0, { source: "old-idb" }), await be();
|
|
1329
|
+
} else
|
|
1330
|
+
h = v.import(new Uint8Array(0), { docId: s }), h.isReady() || h.doneLoading(), h.change((u) => {
|
|
1331
|
+
Object.assign(u, z());
|
|
1332
|
+
}), p = "new", l.logLoad("new", 0, 0);
|
|
1333
|
+
}
|
|
1334
|
+
if (b = new N({
|
|
1335
|
+
pushFn: Ie,
|
|
1336
|
+
getHeadsFn: () => {
|
|
1337
|
+
const y = h.doc();
|
|
1338
|
+
return y ? D.getHeads(y).join(",") : null;
|
|
1339
|
+
},
|
|
1340
|
+
debounceMs: 2e3
|
|
1341
|
+
}), p === "compact-store") {
|
|
1342
|
+
const y = h.doc();
|
|
1343
|
+
y && b.setLastPushedHeads(D.getHeads(y).join(","));
|
|
1344
|
+
}
|
|
1345
|
+
if (O) {
|
|
1346
|
+
S = new N({
|
|
1347
|
+
pushFn: Pe,
|
|
1348
|
+
getHeadsFn: () => {
|
|
1349
|
+
const u = h.doc();
|
|
1350
|
+
return u ? D.getHeads(u).join(",") : null;
|
|
1351
|
+
},
|
|
1352
|
+
debounceMs: 5e3
|
|
1353
|
+
});
|
|
1354
|
+
const y = h.doc();
|
|
1355
|
+
y && p === "vault" && S.setLastPushedHeads(D.getHeads(y).join(",")), p !== "vault" && p !== "new" && S.pushDebounced();
|
|
1356
|
+
}
|
|
1357
|
+
I = h, e && (C = new ue(e, i, r), C.setDocumentId(s), v.networkSubsystem.addNetworkAdapter(C), C.setDocReady()), h.on("change", () => {
|
|
1358
|
+
$ || (console.debug("[personal-doc] Remote change detected, notifying listeners"), ne(), b == null || b.pushDebounced());
|
|
1359
|
+
}), C && C.emit("peer-candidate", {
|
|
1360
|
+
peerId: r,
|
|
1361
|
+
peerMetadata: { isEphemeral: !0 }
|
|
1362
|
+
});
|
|
1363
|
+
const m = h.doc();
|
|
1364
|
+
if (!m) throw new Error("Failed to initialize personal doc");
|
|
1365
|
+
return console.debug(`[personal-doc] initPersonalDoc total: ${Date.now() - n}ms (loaded from: ${p})`), m;
|
|
1366
|
+
}
|
|
1367
|
+
function se() {
|
|
1368
|
+
if (!I)
|
|
1369
|
+
throw new Error("Personal doc not initialized. Call initPersonalDoc() first.");
|
|
1370
|
+
const c = I.doc();
|
|
1371
|
+
if (!c)
|
|
1372
|
+
throw new Error("Personal doc not ready.");
|
|
1373
|
+
return c;
|
|
1374
|
+
}
|
|
1375
|
+
function Oe() {
|
|
1376
|
+
return I !== null && I.doc() !== void 0;
|
|
1377
|
+
}
|
|
1378
|
+
function oe(c, e) {
|
|
1379
|
+
if (!I)
|
|
1380
|
+
throw new Error("Personal doc not initialized. Call initPersonalDoc() first.");
|
|
1381
|
+
$ = !0;
|
|
1382
|
+
try {
|
|
1383
|
+
I.change(c);
|
|
1384
|
+
} finally {
|
|
1385
|
+
$ = !1;
|
|
1386
|
+
}
|
|
1387
|
+
ne(), e != null && e.background ? (b == null || b.pushDebounced(), S == null || S.pushDebounced()) : (b == null || b.pushImmediate(), S == null || S.pushImmediate());
|
|
1388
|
+
const t = I.doc();
|
|
1389
|
+
if (!t) throw new Error("Doc disappeared after change");
|
|
1390
|
+
return t;
|
|
1391
|
+
}
|
|
1392
|
+
function ve(c) {
|
|
1393
|
+
return B.add(c), () => {
|
|
1394
|
+
B.delete(c);
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
async function _e() {
|
|
1398
|
+
await (b == null ? void 0 : b.flush()), await (S == null ? void 0 : S.flush());
|
|
1399
|
+
}
|
|
1400
|
+
async function ke() {
|
|
1401
|
+
const c = v, e = C;
|
|
1402
|
+
I = null, v = null, C = null, O = null, U = null, F = 0, b && (b.destroy(), b = null), S && (S.destroy(), S = null), k && (k.close(), k = null), B.clear();
|
|
1403
|
+
try {
|
|
1404
|
+
e == null || e.disconnect(), c == null || c.shutdown();
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
async function je() {
|
|
1409
|
+
await ke();
|
|
1410
|
+
for (const c of [G, "automerge-personal", ee, te])
|
|
1411
|
+
await Promise.race([
|
|
1412
|
+
new Promise((e) => {
|
|
1413
|
+
const t = indexedDB.deleteDatabase(c);
|
|
1414
|
+
t.onsuccess = () => e(), t.onblocked = () => {
|
|
1415
|
+
console.warn(`[personal-doc] deleteDatabase(${c}) blocked`), e();
|
|
1416
|
+
}, t.onerror = () => e();
|
|
1417
|
+
}),
|
|
1418
|
+
new Promise((e) => setTimeout(e, 2e3))
|
|
1419
|
+
// timeout fallback
|
|
1420
|
+
]);
|
|
1421
|
+
}
|
|
1422
|
+
class Ue {
|
|
1423
|
+
constructor() {
|
|
1424
|
+
d(this, "data", /* @__PURE__ */ new Map());
|
|
1425
|
+
}
|
|
1426
|
+
keyToString(e) {
|
|
1427
|
+
return e.join("/");
|
|
1428
|
+
}
|
|
1429
|
+
async load(e) {
|
|
1430
|
+
return this.data.get(this.keyToString(e));
|
|
1431
|
+
}
|
|
1432
|
+
async save(e, t) {
|
|
1433
|
+
this.data.set(this.keyToString(e), t);
|
|
1434
|
+
}
|
|
1435
|
+
async remove(e) {
|
|
1436
|
+
this.data.delete(this.keyToString(e));
|
|
1437
|
+
}
|
|
1438
|
+
async loadRange(e) {
|
|
1439
|
+
const t = this.keyToString(e), n = [];
|
|
1440
|
+
for (const [s, o] of this.data.entries())
|
|
1441
|
+
s.startsWith(t) && n.push({ key: s.split("/"), data: o });
|
|
1442
|
+
return n;
|
|
1443
|
+
}
|
|
1444
|
+
async removeRange(e) {
|
|
1445
|
+
const t = this.keyToString(e);
|
|
1446
|
+
for (const n of this.data.keys())
|
|
1447
|
+
n.startsWith(t) && this.data.delete(n);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function Ae(c, e) {
|
|
1451
|
+
return `${c}:${e}`;
|
|
1452
|
+
}
|
|
1453
|
+
class Te {
|
|
1454
|
+
constructor(e) {
|
|
1455
|
+
d(this, "getPersonalDoc");
|
|
1456
|
+
d(this, "changePersonalDoc");
|
|
1457
|
+
this.getPersonalDoc = (e == null ? void 0 : e.getPersonalDoc) ?? se, this.changePersonalDoc = (e == null ? void 0 : e.changePersonalDoc) ?? oe;
|
|
1458
|
+
}
|
|
1459
|
+
async saveSpaceMetadata(e) {
|
|
1460
|
+
this.changePersonalDoc((t) => {
|
|
1461
|
+
const n = {
|
|
1462
|
+
id: e.info.id,
|
|
1463
|
+
type: e.info.type,
|
|
1464
|
+
name: e.info.name ?? null,
|
|
1465
|
+
description: e.info.description ?? null,
|
|
1466
|
+
members: [...e.info.members],
|
|
1467
|
+
createdAt: e.info.createdAt
|
|
1468
|
+
};
|
|
1469
|
+
e.info.appTag != null && (n.appTag = e.info.appTag), t.spaces[e.info.id] = {
|
|
1470
|
+
info: n,
|
|
1471
|
+
documentId: e.documentId,
|
|
1472
|
+
documentUrl: e.documentUrl,
|
|
1473
|
+
memberEncryptionKeys: Object.fromEntries(
|
|
1474
|
+
Object.entries(e.memberEncryptionKeys).map(
|
|
1475
|
+
([s, o]) => [s, Array.from(o)]
|
|
1476
|
+
)
|
|
1477
|
+
)
|
|
1478
|
+
};
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
async loadSpaceMetadata(e) {
|
|
1482
|
+
const n = this.getPersonalDoc().spaces[e];
|
|
1483
|
+
return n ? this.deserialize(n) : null;
|
|
1484
|
+
}
|
|
1485
|
+
async loadAllSpaceMetadata() {
|
|
1486
|
+
const e = this.getPersonalDoc();
|
|
1487
|
+
return Object.values(e.spaces).map((t) => this.deserialize(t));
|
|
1488
|
+
}
|
|
1489
|
+
async deleteSpaceMetadata(e) {
|
|
1490
|
+
this.changePersonalDoc((t) => {
|
|
1491
|
+
delete t.spaces[e];
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
async saveGroupKey(e) {
|
|
1495
|
+
const t = Ae(e.spaceId, e.generation);
|
|
1496
|
+
this.changePersonalDoc((n) => {
|
|
1497
|
+
n.groupKeys[t] = {
|
|
1498
|
+
spaceId: e.spaceId,
|
|
1499
|
+
generation: e.generation,
|
|
1500
|
+
key: Array.from(e.key)
|
|
1501
|
+
};
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
async loadGroupKeys(e) {
|
|
1505
|
+
const t = this.getPersonalDoc();
|
|
1506
|
+
return Object.values(t.groupKeys).filter((n) => n.spaceId === e).map((n) => ({
|
|
1507
|
+
spaceId: n.spaceId,
|
|
1508
|
+
generation: n.generation,
|
|
1509
|
+
key: new Uint8Array(n.key)
|
|
1510
|
+
}));
|
|
1511
|
+
}
|
|
1512
|
+
async deleteGroupKeys(e) {
|
|
1513
|
+
this.changePersonalDoc((t) => {
|
|
1514
|
+
for (const [n, s] of Object.entries(t.groupKeys))
|
|
1515
|
+
s.spaceId === e && delete t.groupKeys[n];
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
async clearAll() {
|
|
1519
|
+
this.changePersonalDoc((e) => {
|
|
1520
|
+
for (const t of Object.keys(e.spaces))
|
|
1521
|
+
delete e.spaces[t];
|
|
1522
|
+
for (const t of Object.keys(e.groupKeys))
|
|
1523
|
+
delete e.groupKeys[t];
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
deserialize(e) {
|
|
1527
|
+
return {
|
|
1528
|
+
info: {
|
|
1529
|
+
id: e.info.id,
|
|
1530
|
+
type: e.info.type,
|
|
1531
|
+
...e.info.name != null ? { name: e.info.name } : {},
|
|
1532
|
+
...e.info.description != null ? { description: e.info.description } : {},
|
|
1533
|
+
...e.info.appTag != null ? { appTag: e.info.appTag } : {},
|
|
1534
|
+
members: [...e.info.members],
|
|
1535
|
+
createdAt: e.info.createdAt
|
|
1536
|
+
},
|
|
1537
|
+
documentId: e.documentId,
|
|
1538
|
+
documentUrl: e.documentUrl,
|
|
1539
|
+
memberEncryptionKeys: Object.fromEntries(
|
|
1540
|
+
Object.entries(e.memberEncryptionKeys).map(
|
|
1541
|
+
([t, n]) => [t, new Uint8Array(n)]
|
|
1542
|
+
)
|
|
1543
|
+
)
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
class Ee {
|
|
1548
|
+
constructor(e) {
|
|
1549
|
+
d(this, "getPersonalDoc");
|
|
1550
|
+
d(this, "changePersonalDoc");
|
|
1551
|
+
d(this, "onPersonalDocChange");
|
|
1552
|
+
this.getPersonalDoc = (e == null ? void 0 : e.getPersonalDoc) ?? se, this.changePersonalDoc = (e == null ? void 0 : e.changePersonalDoc) ?? oe, this.onPersonalDocChange = (e == null ? void 0 : e.onPersonalDocChange) ?? ve;
|
|
1553
|
+
}
|
|
1554
|
+
async enqueue(e) {
|
|
1555
|
+
await this.has(e.id) || this.changePersonalDoc((n) => {
|
|
1556
|
+
n.outbox[e.id] = {
|
|
1557
|
+
envelopeJson: JSON.stringify(e),
|
|
1558
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1559
|
+
retryCount: 0
|
|
1560
|
+
};
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
async dequeue(e) {
|
|
1564
|
+
this.changePersonalDoc((t) => {
|
|
1565
|
+
delete t.outbox[e];
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
async getPending() {
|
|
1569
|
+
const e = this.getPersonalDoc();
|
|
1570
|
+
return Object.entries(e.outbox).map(([t, n]) => ({
|
|
1571
|
+
envelope: JSON.parse(n.envelopeJson),
|
|
1572
|
+
createdAt: n.createdAt,
|
|
1573
|
+
retryCount: n.retryCount
|
|
1574
|
+
})).sort((t, n) => t.createdAt.localeCompare(n.createdAt));
|
|
1575
|
+
}
|
|
1576
|
+
async has(e) {
|
|
1577
|
+
const t = this.getPersonalDoc();
|
|
1578
|
+
return e in t.outbox;
|
|
1579
|
+
}
|
|
1580
|
+
async incrementRetry(e) {
|
|
1581
|
+
this.changePersonalDoc((t) => {
|
|
1582
|
+
t.outbox[e] && (t.outbox[e].retryCount += 1);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
async count() {
|
|
1586
|
+
const e = this.getPersonalDoc();
|
|
1587
|
+
return Object.keys(e.outbox).length;
|
|
1588
|
+
}
|
|
1589
|
+
watchPendingCount() {
|
|
1590
|
+
const e = this, t = () => {
|
|
1591
|
+
const s = e.getPersonalDoc();
|
|
1592
|
+
return Object.keys(s.outbox).length;
|
|
1593
|
+
};
|
|
1594
|
+
let n = t();
|
|
1595
|
+
return {
|
|
1596
|
+
subscribe: (s) => e.onPersonalDocChange(() => {
|
|
1597
|
+
const o = t();
|
|
1598
|
+
o !== n && (n = o, s(n));
|
|
1599
|
+
}),
|
|
1600
|
+
getValue: () => n
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
export {
|
|
1605
|
+
Ee as AutomergeOutboxStore,
|
|
1606
|
+
xe as AutomergeReplicationAdapter,
|
|
1607
|
+
Te as AutomergeSpaceMetadataStorage,
|
|
1608
|
+
T as CompactionService,
|
|
1609
|
+
le as EncryptedMessagingNetworkAdapter,
|
|
1610
|
+
Ue as InMemoryRepoStorageAdapter,
|
|
1611
|
+
ue as PersonalNetworkAdapter,
|
|
1612
|
+
pe as SyncOnlyStorageAdapter,
|
|
1613
|
+
oe as changePersonalDoc,
|
|
1614
|
+
je as deletePersonalDocDB,
|
|
1615
|
+
_e as flushPersonalDoc,
|
|
1616
|
+
se as getPersonalDoc,
|
|
1617
|
+
Re as initPersonalDoc,
|
|
1618
|
+
Oe as isPersonalDocInitialized,
|
|
1619
|
+
ve as onPersonalDocChange,
|
|
1620
|
+
ke as resetPersonalDoc
|
|
1621
|
+
};
|