@interocitor/core 0.0.0-beta.10

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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +706 -0
  3. package/dist/adapters/cloudflare.d.ts +78 -0
  4. package/dist/adapters/cloudflare.d.ts.map +1 -0
  5. package/dist/adapters/cloudflare.js +325 -0
  6. package/dist/adapters/google-drive.d.ts +64 -0
  7. package/dist/adapters/google-drive.d.ts.map +1 -0
  8. package/dist/adapters/google-drive.js +339 -0
  9. package/dist/adapters/memory.d.ts +53 -0
  10. package/dist/adapters/memory.d.ts.map +1 -0
  11. package/dist/adapters/memory.js +182 -0
  12. package/dist/adapters/webdav.d.ts +70 -0
  13. package/dist/adapters/webdav.d.ts.map +1 -0
  14. package/dist/adapters/webdav.js +323 -0
  15. package/dist/core/codec.d.ts +20 -0
  16. package/dist/core/codec.d.ts.map +1 -0
  17. package/dist/core/codec.js +102 -0
  18. package/dist/core/compaction.d.ts +45 -0
  19. package/dist/core/compaction.d.ts.map +1 -0
  20. package/dist/core/compaction.js +190 -0
  21. package/dist/core/connected-stores.d.ts +77 -0
  22. package/dist/core/connected-stores.d.ts.map +1 -0
  23. package/dist/core/connected-stores.js +76 -0
  24. package/dist/core/crdt.d.ts +36 -0
  25. package/dist/core/crdt.d.ts.map +1 -0
  26. package/dist/core/crdt.js +174 -0
  27. package/dist/core/errors.d.ts +47 -0
  28. package/dist/core/errors.d.ts.map +1 -0
  29. package/dist/core/errors.js +61 -0
  30. package/dist/core/flush.d.ts +9 -0
  31. package/dist/core/flush.d.ts.map +1 -0
  32. package/dist/core/flush.js +98 -0
  33. package/dist/core/hlc.d.ts +25 -0
  34. package/dist/core/hlc.d.ts.map +1 -0
  35. package/dist/core/hlc.js +75 -0
  36. package/dist/core/ids.d.ts +49 -0
  37. package/dist/core/ids.d.ts.map +1 -0
  38. package/dist/core/ids.js +132 -0
  39. package/dist/core/internals.d.ts +33 -0
  40. package/dist/core/internals.d.ts.map +1 -0
  41. package/dist/core/internals.js +72 -0
  42. package/dist/core/manifest.d.ts +56 -0
  43. package/dist/core/manifest.d.ts.map +1 -0
  44. package/dist/core/manifest.js +203 -0
  45. package/dist/core/pull.d.ts +26 -0
  46. package/dist/core/pull.d.ts.map +1 -0
  47. package/dist/core/pull.js +113 -0
  48. package/dist/core/row-id.d.ts +12 -0
  49. package/dist/core/row-id.d.ts.map +1 -0
  50. package/dist/core/row-id.js +11 -0
  51. package/dist/core/schema-types.d.ts +26 -0
  52. package/dist/core/schema-types.d.ts.map +1 -0
  53. package/dist/core/schema-types.js +31 -0
  54. package/dist/core/schema-types.type-test.d.ts +2 -0
  55. package/dist/core/schema-types.type-test.d.ts.map +1 -0
  56. package/dist/core/schema-types.type-test.js +224 -0
  57. package/dist/core/sync-engine.d.ts +364 -0
  58. package/dist/core/sync-engine.d.ts.map +1 -0
  59. package/dist/core/sync-engine.js +2475 -0
  60. package/dist/core/table.d.ts +260 -0
  61. package/dist/core/table.d.ts.map +1 -0
  62. package/dist/core/table.js +461 -0
  63. package/dist/core/types.d.ts +952 -0
  64. package/dist/core/types.d.ts.map +1 -0
  65. package/dist/core/types.js +6 -0
  66. package/dist/crypto/encryption.d.ts +61 -0
  67. package/dist/crypto/encryption.d.ts.map +1 -0
  68. package/dist/crypto/encryption.js +216 -0
  69. package/dist/crypto/keys.d.ts +48 -0
  70. package/dist/crypto/keys.d.ts.map +1 -0
  71. package/dist/crypto/keys.js +54 -0
  72. package/dist/handshake/channel.d.ts +117 -0
  73. package/dist/handshake/channel.d.ts.map +1 -0
  74. package/dist/handshake/channel.js +245 -0
  75. package/dist/handshake/index.d.ts +216 -0
  76. package/dist/handshake/index.d.ts.map +1 -0
  77. package/dist/handshake/index.js +199 -0
  78. package/dist/handshake/qr-public.d.ts +3 -0
  79. package/dist/handshake/qr-public.d.ts.map +1 -0
  80. package/dist/handshake/qr-public.js +1 -0
  81. package/dist/handshake/qr.d.ts +100 -0
  82. package/dist/handshake/qr.d.ts.map +1 -0
  83. package/dist/handshake/qr.js +102 -0
  84. package/dist/index.d.ts +50 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +50 -0
  87. package/dist/storage/credential-store.d.ts +122 -0
  88. package/dist/storage/credential-store.d.ts.map +1 -0
  89. package/dist/storage/credential-store.js +356 -0
  90. package/dist/storage/local-store.d.ts +64 -0
  91. package/dist/storage/local-store.d.ts.map +1 -0
  92. package/dist/storage/local-store.js +490 -0
  93. package/dist/storage/reset.d.ts +10 -0
  94. package/dist/storage/reset.d.ts.map +1 -0
  95. package/dist/storage/reset.js +18 -0
  96. package/package.json +76 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * WebDAV Storage Adapter
3
+ *
4
+ * For self-hosted clouds: Nextcloud, ownCloud, or any WebDAV server.
5
+ * "My own cloud" option — user runs their own server.
6
+ *
7
+ * WebDAV is HTTP-based, works from any browser.
8
+ * Most implementations support Basic auth or Bearer tokens.
9
+ */
10
+ /**
11
+ * Browser-friendly WebDAV adapter for Nextcloud, ownCloud, and compatible DAV servers.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const adapter = new WebDAVAdapter({
16
+ * baseUrl: 'https://cloud.example.com/remote.php/dav/files/alice',
17
+ * auth: { username: 'alice', password: 'APP_PASSWORD' },
18
+ * });
19
+ * ```
20
+ */
21
+ export class WebDAVAdapter {
22
+ constructor(config) {
23
+ this.name = 'webdav';
24
+ this.authenticated = false;
25
+ // Per-session cache of folders we have already MKCOL'd. WebDAV folders
26
+ // are stable for the life of the mesh; a caller (Interocitor.connect)
27
+ // re-runs the same 4-folder ensureFolder loop on every reload, which
28
+ // costs 4 sequential MKCOL round-trips for no benefit. Cache wipes on
29
+ // explicit `resetFolderCache()` (mesh swap / poison) or process exit.
30
+ this.ensuredFolders = new Set();
31
+ this.config = config;
32
+ }
33
+ url(path) {
34
+ const base = this.config.baseUrl.replace(/\/$/, '');
35
+ const clean = path.startsWith('/') ? path : '/' + path;
36
+ return base + clean;
37
+ }
38
+ authHeader() {
39
+ const auth = this.config.auth;
40
+ if ('token' in auth) {
41
+ return `Bearer ${auth.token}`;
42
+ }
43
+ return `Basic ${btoa(`${auth.username}:${auth.password}`)}`;
44
+ }
45
+ headers(extra) {
46
+ return {
47
+ Authorization: this.authHeader(),
48
+ ...extra,
49
+ };
50
+ }
51
+ async authenticate() {
52
+ // Verify credentials by doing a PROPFIND on root
53
+ const res = await fetch(this.url('/'), {
54
+ method: 'PROPFIND',
55
+ headers: this.headers({ Depth: '0' }),
56
+ });
57
+ if (res.status === 207 || res.ok) {
58
+ this.authenticated = true;
59
+ }
60
+ else if (res.status === 401) {
61
+ throw new Error('WebDAV authentication failed');
62
+ }
63
+ else {
64
+ throw new Error(`WebDAV error: ${res.status}`);
65
+ }
66
+ }
67
+ /**
68
+ * Returns the WebDAV base URL (without credentials) for embedding in a QR payload.
69
+ * The scanner uses this to point their WebDAVAdapter at the same server.
70
+ * Auth (username/password or token) must be configured separately by the user.
71
+ */
72
+ getHandshakeConfig() {
73
+ return JSON.stringify({ baseUrl: this.config.baseUrl });
74
+ }
75
+ isAuthenticated() {
76
+ return this.authenticated;
77
+ }
78
+ async ensureFolder(path) {
79
+ if (this.ensuredFolders.has(path))
80
+ return;
81
+ const parts = path.split('/').filter(Boolean);
82
+ let current = '';
83
+ for (const part of parts) {
84
+ current += '/' + part;
85
+ if (this.ensuredFolders.has(current))
86
+ continue; // ancestor cached
87
+ const res = await fetch(this.url(current), {
88
+ method: 'MKCOL',
89
+ headers: this.headers(),
90
+ });
91
+ // 201 Created, 405 Already Exists — both fine
92
+ if (!res.ok && res.status !== 405) {
93
+ throw new Error(`Failed to create folder ${current}: ${res.status}`);
94
+ }
95
+ this.ensuredFolders.add(current);
96
+ }
97
+ this.ensuredFolders.add(path);
98
+ }
99
+ /** Drop the per-session ensureFolder cache. Call after mesh swap, poison,
100
+ * or any state where we cannot trust a previous "this folder exists"
101
+ * observation. */
102
+ resetFolderCache() {
103
+ this.ensuredFolders.clear();
104
+ }
105
+ async listFiles(folderPath) {
106
+ const res = await fetch(this.url(folderPath), {
107
+ method: 'PROPFIND',
108
+ headers: this.headers({
109
+ Depth: '1',
110
+ 'Content-Type': 'application/xml',
111
+ }),
112
+ body: `<?xml version="1.0" encoding="UTF-8"?>
113
+ <d:propfind xmlns:d="DAV:">
114
+ <d:prop>
115
+ <d:getcontentlength/>
116
+ <d:getlastmodified/>
117
+ <d:resourcetype/>
118
+ <d:getetag/>
119
+ </d:prop>
120
+ </d:propfind>`,
121
+ });
122
+ if (res.status !== 207) {
123
+ throw new Error(`PROPFIND failed: ${res.status}`);
124
+ }
125
+ const xml = await res.text();
126
+ return this.parsePropfindResponse(xml, folderPath);
127
+ }
128
+ parsePropfindResponse(xml, basePath) {
129
+ const parser = new DOMParser();
130
+ const doc = parser.parseFromString(xml, 'application/xml');
131
+ const responses = doc.getElementsByTagNameNS('DAV:', 'response');
132
+ const entries = [];
133
+ for (let i = 0; i < responses.length; i++) {
134
+ const response = responses[i];
135
+ const href = response.getElementsByTagNameNS('DAV:', 'href')[0]?.textContent || '';
136
+ // Skip the folder itself (first response)
137
+ if (i === 0)
138
+ continue;
139
+ // Skip sub-folders
140
+ const resourceType = response.getElementsByTagNameNS('DAV:', 'collection');
141
+ if (resourceType.length > 0)
142
+ continue;
143
+ const size = response.getElementsByTagNameNS('DAV:', 'getcontentlength')[0]?.textContent || '0';
144
+ const modified = response.getElementsByTagNameNS('DAV:', 'getlastmodified')[0]?.textContent || '';
145
+ const etag = response.getElementsByTagNameNS('DAV:', 'getetag')[0]?.textContent || undefined;
146
+ // Extract filename from href
147
+ const name = decodeURIComponent(href.split('/').filter(Boolean).pop() || '');
148
+ entries.push({
149
+ name,
150
+ path: `${basePath}/${name}`,
151
+ size: parseInt(size, 10),
152
+ modifiedTime: modified ? new Date(modified).toISOString() : '',
153
+ etag,
154
+ });
155
+ }
156
+ return entries;
157
+ }
158
+ async listFolders(folderPath) {
159
+ const res = await fetch(this.url(folderPath), {
160
+ method: 'PROPFIND',
161
+ headers: this.headers({
162
+ Depth: '1',
163
+ 'Content-Type': 'application/xml',
164
+ }),
165
+ body: `<?xml version="1.0" encoding="UTF-8"?>
166
+ <d:propfind xmlns:d="DAV:">
167
+ <d:prop>
168
+ <d:resourcetype/>
169
+ </d:prop>
170
+ </d:propfind>`,
171
+ });
172
+ if (res.status !== 207)
173
+ return [];
174
+ const xml = await res.text();
175
+ const parser = new DOMParser();
176
+ const doc = parser.parseFromString(xml, 'application/xml');
177
+ const responses = doc.getElementsByTagNameNS('DAV:', 'response');
178
+ const folders = [];
179
+ for (let i = 1; i < responses.length; i++) {
180
+ const response = responses[i];
181
+ const isCollection = response.getElementsByTagNameNS('DAV:', 'collection').length > 0;
182
+ if (!isCollection)
183
+ continue;
184
+ const href = response.getElementsByTagNameNS('DAV:', 'href')[0]?.textContent || '';
185
+ const name = decodeURIComponent(href.split('/').filter(Boolean).pop() || '');
186
+ if (name)
187
+ folders.push(name);
188
+ }
189
+ return folders;
190
+ }
191
+ async readFile(path) {
192
+ const res = await fetch(this.url(path), {
193
+ method: 'GET',
194
+ headers: this.headers(),
195
+ });
196
+ if (!res.ok)
197
+ throw new Error(`Failed to read ${path}: ${res.status}`);
198
+ const buffer = await res.arrayBuffer();
199
+ return new Uint8Array(buffer);
200
+ }
201
+ async writeFile(path, data) {
202
+ const body = typeof data === 'string'
203
+ ? new TextEncoder().encode(data)
204
+ : data;
205
+ const res = await fetch(this.url(path), {
206
+ method: 'PUT',
207
+ headers: this.headers({ 'Content-Type': 'application/octet-stream' }),
208
+ body: body,
209
+ });
210
+ if (!res.ok && res.status !== 201 && res.status !== 204) {
211
+ throw new Error(`Failed to write ${path}: ${res.status}`);
212
+ }
213
+ }
214
+ async deleteFile(path) {
215
+ const res = await fetch(this.url(path), {
216
+ method: 'DELETE',
217
+ headers: this.headers(),
218
+ });
219
+ // 204 No Content or 404 Not Found — both acceptable
220
+ if (!res.ok && res.status !== 404) {
221
+ throw new Error(`Failed to delete ${path}: ${res.status}`);
222
+ }
223
+ }
224
+ parentDir(path) {
225
+ const clean = path.startsWith('/') ? path : `/${path}`;
226
+ const idx = clean.lastIndexOf('/');
227
+ return idx <= 0 ? '/' : clean.slice(0, idx);
228
+ }
229
+ storedMetaPath(path) {
230
+ return `${path}.interocitor-meta.json`;
231
+ }
232
+ async putStoredFile(path, data, options = {}) {
233
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
234
+ await this.ensureFolder(this.parentDir(path));
235
+ await this.writeFile(path, bytes);
236
+ const file = await this.getFileMetadata(path);
237
+ const now = new Date().toISOString();
238
+ const meta = {
239
+ name: file?.name ?? path.split('/').pop() ?? path,
240
+ path,
241
+ size: file?.size ?? bytes.byteLength,
242
+ modifiedTime: file?.modifiedTime ?? now,
243
+ etag: file?.etag,
244
+ uploadedAt: now,
245
+ uploadedByDeviceId: options.uploadedByDeviceId,
246
+ plaintextSize: options.plaintextSize,
247
+ storedSize: bytes.byteLength,
248
+ contentType: options.contentType,
249
+ useCount: 0,
250
+ };
251
+ await this.writeFile(this.storedMetaPath(path), JSON.stringify(meta));
252
+ return meta;
253
+ }
254
+ async getStoredFile(path) {
255
+ const bytes = await this.readFile(path);
256
+ const meta = await this.getStoredFileMetadata(path);
257
+ if (meta) {
258
+ await this.writeFile(this.storedMetaPath(path), JSON.stringify({
259
+ ...meta,
260
+ lastAccessedAt: new Date().toISOString(),
261
+ useCount: (meta.useCount ?? 0) + 1,
262
+ }));
263
+ }
264
+ return bytes;
265
+ }
266
+ async deleteStoredFile(path) {
267
+ await this.deleteFile(path);
268
+ try {
269
+ await this.deleteFile(this.storedMetaPath(path));
270
+ }
271
+ catch { }
272
+ }
273
+ async getStoredFileMetadata(path) {
274
+ try {
275
+ const raw = await this.readFile(this.storedMetaPath(path));
276
+ return JSON.parse(new TextDecoder().decode(raw));
277
+ }
278
+ catch {
279
+ const file = await this.getFileMetadata(path);
280
+ return file ? { ...file, storedSize: file.size } : null;
281
+ }
282
+ }
283
+ async getFileMetadata(path) {
284
+ const res = await fetch(this.url(path), {
285
+ method: 'PROPFIND',
286
+ headers: this.headers({
287
+ Depth: '0',
288
+ 'Content-Type': 'application/xml',
289
+ }),
290
+ body: `<?xml version="1.0" encoding="UTF-8"?>
291
+ <d:propfind xmlns:d="DAV:">
292
+ <d:prop>
293
+ <d:getcontentlength/>
294
+ <d:getlastmodified/>
295
+ <d:getetag/>
296
+ </d:prop>
297
+ </d:propfind>`,
298
+ });
299
+ if (res.status === 404)
300
+ return null;
301
+ if (res.status !== 207)
302
+ return null;
303
+ const xml = await res.text();
304
+ // PROPFIND with Depth:0 on a file returns one entry for itself
305
+ // but our parser skips index 0, so we handle it differently
306
+ const parser = new DOMParser();
307
+ const doc = parser.parseFromString(xml, 'application/xml');
308
+ const response = doc.getElementsByTagNameNS('DAV:', 'response')[0];
309
+ if (!response)
310
+ return null;
311
+ const size = response.getElementsByTagNameNS('DAV:', 'getcontentlength')[0]?.textContent || '0';
312
+ const modified = response.getElementsByTagNameNS('DAV:', 'getlastmodified')[0]?.textContent || '';
313
+ const etag = response.getElementsByTagNameNS('DAV:', 'getetag')[0]?.textContent || undefined;
314
+ const name = path.split('/').filter(Boolean).pop() || '';
315
+ return {
316
+ name,
317
+ path,
318
+ size: parseInt(size, 10),
319
+ modifiedTime: modified ? new Date(modified).toISOString() : '',
320
+ etag,
321
+ };
322
+ }
323
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Codec — encryption/decryption of change and snapshot payloads.
3
+ *
4
+ * Extracted from Interocitor to keep the orchestrator lean.
5
+ * Not part of the public API.
6
+ */
7
+ import type { ChangeEntry, Manifest, Snapshot, LocalStoreAdapter } from './types.ts';
8
+ export interface CodecState {
9
+ encryptionKey: CryptoKey | null;
10
+ encrypted: boolean;
11
+ manifest: Manifest | null;
12
+ }
13
+ export declare function encodeForCloud(state: CodecState, plaintext: string): Promise<string>;
14
+ export declare function decodeFromCloud(state: CodecState, data: string): Promise<string>;
15
+ export declare function assertExpectedMeshId(local: LocalStoreAdapter, manifest: Manifest | null, meshId: string): Promise<void>;
16
+ export declare function encodeChangePayload(state: CodecState, entry: ChangeEntry): Promise<string>;
17
+ export declare function decodeChangePayload(state: CodecState, local: LocalStoreAdapter, data: string, path: string): Promise<ChangeEntry>;
18
+ export declare function encodeSnapshotPayload(state: CodecState, snapshot: Snapshot): Promise<string>;
19
+ export declare function decodeSnapshotPayload(state: CodecState, local: LocalStoreAdapter, data: string, path: string): Promise<Snapshot>;
20
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/core/codec.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,EAGR,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,SAAS,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtF;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAQhG;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CAgBtB;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAQlG;AAED,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CAgBnB"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Codec — encryption/decryption of change and snapshot payloads.
3
+ *
4
+ * Extracted from Interocitor to keep the orchestrator lean.
5
+ * Not part of the public API.
6
+ */
7
+ import { encryptEntry, decryptEntry } from "../crypto/encryption.js";
8
+ export async function encodeForCloud(state, plaintext) {
9
+ if (!state.encrypted || !state.encryptionKey)
10
+ return plaintext;
11
+ return encryptEntry(state.encryptionKey, plaintext);
12
+ }
13
+ export async function decodeFromCloud(state, data) {
14
+ if (!state.encrypted || !state.encryptionKey)
15
+ return data;
16
+ try {
17
+ return await decryptEntry(state.encryptionKey, data);
18
+ }
19
+ catch (err) {
20
+ // Re-throw with explicit context. Decode errors at this layer mean the
21
+ // active key cannot decrypt this payload — either the wrong key was
22
+ // loaded, or the payload was written under a different key (mesh swap,
23
+ // passphrase rotated, two devices bound to same dbName but different
24
+ // passphrases). Caller wraps this in poisonRemote with the file path.
25
+ const reason = err instanceof Error ? err.message : String(err);
26
+ let keyFingerprint = '<unknown>';
27
+ try {
28
+ const raw = await crypto.subtle.exportKey('raw', state.encryptionKey);
29
+ const hash = await crypto.subtle.digest('SHA-256', raw);
30
+ const bytes = new Uint8Array(hash);
31
+ const hex = Array.from(bytes.slice(0, 6)).map(b => b.toString(16).padStart(2, '0')).join('');
32
+ keyFingerprint = `sha256-${hex}`;
33
+ }
34
+ catch { /* ignore */ }
35
+ console.log('[interocitor:decode] decodeFromCloud() — DECRYPT FAIL', {
36
+ keyFingerprint,
37
+ meshId: state.manifest?.meshId,
38
+ payloadFirst32: data.slice(0, 32),
39
+ payloadLen: data.length,
40
+ reason,
41
+ });
42
+ const e = new Error(`Decryption failed: payload not decryptable with the active mesh key (${reason}). The remote was likely written under a different key/mesh.`);
43
+ e.cause = err;
44
+ throw e;
45
+ }
46
+ }
47
+ export async function assertExpectedMeshId(local, manifest, meshId) {
48
+ if (!meshId) {
49
+ throw new Error('Remote mesh is missing meshId');
50
+ }
51
+ const manifestMeshId = manifest?.meshId;
52
+ if (manifestMeshId && manifestMeshId !== meshId) {
53
+ throw new Error(`Remote mesh mismatch: expected ${manifestMeshId}, got ${meshId}`);
54
+ }
55
+ const storedMeshId = await local.getMeta('meshId');
56
+ if (typeof storedMeshId === 'string' && storedMeshId && storedMeshId !== meshId) {
57
+ throw new Error(`Remote mesh mismatch: expected ${storedMeshId}, got ${meshId}`);
58
+ }
59
+ await local.setMeta('meshId', meshId);
60
+ }
61
+ export async function encodeChangePayload(state, entry) {
62
+ const meshId = state.manifest?.meshId;
63
+ if (!meshId) {
64
+ throw new Error('Cannot encode change payload before manifest is loaded');
65
+ }
66
+ const payload = { meshId, kind: 'change', entry };
67
+ return encodeForCloud(state, JSON.stringify(payload));
68
+ }
69
+ export async function decodeChangePayload(state, local, data, path) {
70
+ const decoded = await decodeFromCloud(state, data);
71
+ const payload = JSON.parse(decoded);
72
+ const payloadRecord = payload;
73
+ if ('mesh' in payloadRecord && !('meshId' in payloadRecord)) {
74
+ throw new Error(`Remote change payload has invalid shape: ${path}`);
75
+ }
76
+ if (payload.kind !== 'change' || !payload.entry) {
77
+ throw new Error(`Remote change payload has invalid shape: ${path}`);
78
+ }
79
+ await assertExpectedMeshId(local, state.manifest, String(payload.meshId || ''));
80
+ return payload.entry;
81
+ }
82
+ export async function encodeSnapshotPayload(state, snapshot) {
83
+ const meshId = state.manifest?.meshId;
84
+ if (!meshId) {
85
+ throw new Error('Cannot encode snapshot payload before manifest is loaded');
86
+ }
87
+ const payload = { meshId, kind: 'snapshot', snapshot };
88
+ return encodeForCloud(state, JSON.stringify(payload));
89
+ }
90
+ export async function decodeSnapshotPayload(state, local, data, path) {
91
+ const decoded = await decodeFromCloud(state, data);
92
+ const payload = JSON.parse(decoded);
93
+ const payloadRecord = payload;
94
+ if ('mesh' in payloadRecord && !('meshId' in payloadRecord)) {
95
+ throw new Error(`Remote snapshot payload has invalid shape: ${path}`);
96
+ }
97
+ if (payload.kind !== 'snapshot' || !payload.snapshot) {
98
+ throw new Error(`Remote snapshot payload has invalid shape: ${path}`);
99
+ }
100
+ await assertExpectedMeshId(local, state.manifest, String(payload.meshId || ''));
101
+ return payload.snapshot;
102
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Compaction — snapshot + manifest rotation + change file pruning.
3
+ *
4
+ * Concurrency note:
5
+ * - No built-in lock or CAS on manifest pointer.
6
+ * - Concurrent compaction can race and overwrite the pointer.
7
+ * - Recommended: acquire a remote lease (e.g. mainline/compact-lock.json)
8
+ * and abort if another compactor is active, or if manifest generation
9
+ * changes after the lease is acquired.
10
+ *
11
+ * Extracted from Interocitor. Not part of the public API.
12
+ */
13
+ import type { StorageAdapter, LocalStoreAdapter, Manifest, Row, SyncEvent } from './types.ts';
14
+ import type { HLC } from './types.ts';
15
+ import type { CodecState } from './codec.ts';
16
+ export interface CompactContext {
17
+ adapter: StorageAdapter;
18
+ local: LocalStoreAdapter;
19
+ remotePath: string;
20
+ manifest: Manifest;
21
+ codecState: CodecState;
22
+ hlc: HLC;
23
+ deviceId: string;
24
+ serverId: string;
25
+ emit: (event: SyncEvent) => void;
26
+ pull: () => Promise<void>;
27
+ offlineGraceMs?: number;
28
+ }
29
+ export declare function compact(ctx: CompactContext): Promise<Manifest>;
30
+ export interface RehydrateContext {
31
+ adapter: StorageAdapter;
32
+ local: LocalStoreAdapter;
33
+ codecState: CodecState;
34
+ manifest: Manifest | null;
35
+ hlc: HLC;
36
+ deviceId: string;
37
+ tables: Record<string, Record<string, Row>>;
38
+ knownTables: Set<string>;
39
+ emit: (event: SyncEvent) => void;
40
+ poisonRemote: (error: unknown, path?: string) => Promise<Error>;
41
+ pull: () => Promise<void>;
42
+ }
43
+ /** Returns the updated HLC after rehydration. */
44
+ export declare function rehydrate(ctx: RehydrateContext): Promise<HLC>;
45
+ //# sourceMappingURL=compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,QAAQ,EAGR,GAAG,EACH,SAAS,EAEV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAkCD,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsGpE;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,iDAAiD;AACjD,wBAAsB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAiDnE"}