@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,339 @@
1
+ /**
2
+ * Google Drive Storage Adapter
3
+ *
4
+ * Uses Google Drive API v3 via OAuth2 with `drive.file` scope.
5
+ * The app can only see files it created or the user explicitly shared.
6
+ *
7
+ * Requires: Google API client ID from Google Cloud Console.
8
+ * Auth flow: Google Identity Services (GIS) popup or redirect.
9
+ */
10
+ const SCOPES = 'https://www.googleapis.com/auth/drive.file';
11
+ const DRIVE_API = 'https://www.googleapis.com/drive/v3';
12
+ const UPLOAD_API = 'https://www.googleapis.com/upload/drive/v3';
13
+ /**
14
+ * Google Drive adapter using the browser OAuth token flow.
15
+ *
16
+ * This is the easiest zero-infrastructure option when your users already live
17
+ * in Google Workspace or personal Drive.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const adapter = new GoogleDriveAdapter({ clientId: 'YOUR_GOOGLE_CLIENT_ID' });
22
+ * const engine = new Interocitor(adapter, { remotePath: '/MyApp' });
23
+ * ```
24
+ */
25
+ export class GoogleDriveAdapter {
26
+ constructor(config) {
27
+ this.name = 'google-drive';
28
+ this.accessToken = null;
29
+ // Cache: path → Google Drive file ID
30
+ this.fileIdCache = new Map();
31
+ this.folderIdCache = new Map();
32
+ this.config = config;
33
+ }
34
+ // ── Auth ─────────────────────────────────────────────────────────
35
+ async authenticate() {
36
+ // Google Identity Services (GIS) token model
37
+ // In production, use google.accounts.oauth2.initTokenClient
38
+ // This is a simplified version using the implicit grant flow
39
+ return new Promise((resolve) => {
40
+ const params = new URLSearchParams({
41
+ client_id: this.config.clientId,
42
+ redirect_uri: this.config.redirectUri || window.location.origin,
43
+ response_type: 'token',
44
+ scope: SCOPES,
45
+ include_granted_scopes: 'true',
46
+ });
47
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
48
+ // Check if we already have a token from a redirect
49
+ const hash = window.location.hash;
50
+ if (hash.includes('access_token=')) {
51
+ const tokenMatch = hash.match(/access_token=([^&]+)/);
52
+ if (tokenMatch) {
53
+ this.accessToken = decodeURIComponent(tokenMatch[1]);
54
+ // Clean the URL
55
+ window.history.replaceState(null, '', window.location.pathname);
56
+ resolve();
57
+ return;
58
+ }
59
+ }
60
+ // Check localStorage for cached token
61
+ const cached = localStorage.getItem('gdrive-access-token');
62
+ if (cached) {
63
+ this.accessToken = cached;
64
+ // Verify token is still valid
65
+ this.verifyToken().then(valid => {
66
+ if (valid) {
67
+ resolve();
68
+ }
69
+ else {
70
+ localStorage.removeItem('gdrive-access-token');
71
+ window.location.href = authUrl;
72
+ }
73
+ });
74
+ return;
75
+ }
76
+ // Redirect to Google OAuth
77
+ window.location.href = authUrl;
78
+ });
79
+ }
80
+ /** Set token directly (for apps that handle their own OAuth flow). */
81
+ setAccessToken(token) {
82
+ this.accessToken = token;
83
+ localStorage.setItem('gdrive-access-token', token);
84
+ }
85
+ /**
86
+ * Returns the Google OAuth clientId for embedding in a QR payload.
87
+ * The scanner uses this to configure their GoogleDriveAdapter.
88
+ * The OAuth flow (and resulting access token) is performed separately by the user.
89
+ */
90
+ getHandshakeConfig() {
91
+ return JSON.stringify({ clientId: this.config.clientId });
92
+ }
93
+ isAuthenticated() {
94
+ return this.accessToken !== null;
95
+ }
96
+ async verifyToken() {
97
+ try {
98
+ const res = await fetch(`${DRIVE_API}/about?fields=user`, {
99
+ headers: { Authorization: `Bearer ${this.accessToken}` },
100
+ });
101
+ return res.ok;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
107
+ headers() {
108
+ if (!this.accessToken)
109
+ throw new Error('Not authenticated');
110
+ return { Authorization: `Bearer ${this.accessToken}` };
111
+ }
112
+ // ── File ID resolution ───────────────────────────────────────────
113
+ /**
114
+ * Resolve a path like "/Interocitor/changes/dev_abc.ndjson"
115
+ * to a Google Drive file ID by walking the folder tree.
116
+ */
117
+ async resolveFileId(path) {
118
+ if (this.fileIdCache.has(path))
119
+ return this.fileIdCache.get(path);
120
+ const parts = path.split('/').filter(Boolean);
121
+ let parentId = 'root';
122
+ for (let i = 0; i < parts.length; i++) {
123
+ const name = parts[i];
124
+ const isLast = i === parts.length - 1;
125
+ const cacheKey = '/' + parts.slice(0, i + 1).join('/');
126
+ if (this.folderIdCache.has(cacheKey)) {
127
+ parentId = this.folderIdCache.get(cacheKey);
128
+ continue;
129
+ }
130
+ if (isLast && this.fileIdCache.has(cacheKey)) {
131
+ return this.fileIdCache.get(cacheKey);
132
+ }
133
+ const mimeFilter = isLast ? '' : " and mimeType='application/vnd.google-apps.folder'";
134
+ const q = `name='${name}' and '${parentId}' in parents and trashed=false${mimeFilter}`;
135
+ const res = await fetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id,name)`, { headers: this.headers() });
136
+ const data = await res.json();
137
+ if (!data.files || data.files.length === 0)
138
+ return null;
139
+ const fileId = data.files[0].id;
140
+ if (isLast) {
141
+ this.fileIdCache.set(path, fileId);
142
+ }
143
+ else {
144
+ this.folderIdCache.set(cacheKey, fileId);
145
+ }
146
+ parentId = fileId;
147
+ }
148
+ return parentId;
149
+ }
150
+ async resolveFolderId(path) {
151
+ if (this.folderIdCache.has(path))
152
+ return this.folderIdCache.get(path);
153
+ const parts = path.split('/').filter(Boolean);
154
+ let parentId = 'root';
155
+ for (let i = 0; i < parts.length; i++) {
156
+ const name = parts[i];
157
+ const cacheKey = '/' + parts.slice(0, i + 1).join('/');
158
+ if (this.folderIdCache.has(cacheKey)) {
159
+ parentId = this.folderIdCache.get(cacheKey);
160
+ continue;
161
+ }
162
+ const q = `name='${name}' and '${parentId}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'`;
163
+ const res = await fetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id)`, { headers: this.headers() });
164
+ const data = await res.json();
165
+ if (!data.files || data.files.length === 0)
166
+ return null;
167
+ parentId = data.files[0].id;
168
+ this.folderIdCache.set(cacheKey, parentId);
169
+ }
170
+ return parentId;
171
+ }
172
+ // ── StorageAdapter interface ───────────────────────────────────────
173
+ async ensureFolder(path) {
174
+ const parts = path.split('/').filter(Boolean);
175
+ let parentId = 'root';
176
+ for (let i = 0; i < parts.length; i++) {
177
+ const name = parts[i];
178
+ const cacheKey = '/' + parts.slice(0, i + 1).join('/');
179
+ if (this.folderIdCache.has(cacheKey)) {
180
+ parentId = this.folderIdCache.get(cacheKey);
181
+ continue;
182
+ }
183
+ // Check if exists
184
+ const q = `name='${name}' and '${parentId}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'`;
185
+ const res = await fetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=files(id)`, { headers: this.headers() });
186
+ const data = await res.json();
187
+ if (data.files && data.files.length > 0) {
188
+ parentId = data.files[0].id;
189
+ }
190
+ else {
191
+ // Create folder
192
+ const createRes = await fetch(`${DRIVE_API}/files`, {
193
+ method: 'POST',
194
+ headers: {
195
+ ...this.headers(),
196
+ 'Content-Type': 'application/json',
197
+ },
198
+ body: JSON.stringify({
199
+ name,
200
+ mimeType: 'application/vnd.google-apps.folder',
201
+ parents: [parentId],
202
+ }),
203
+ });
204
+ const created = await createRes.json();
205
+ parentId = created.id;
206
+ }
207
+ this.folderIdCache.set(cacheKey, parentId);
208
+ }
209
+ }
210
+ async listFiles(folderPath) {
211
+ const folderId = await this.resolveFolderId(folderPath);
212
+ if (!folderId)
213
+ return [];
214
+ const q = `'${folderId}' in parents and trashed=false and mimeType!='application/vnd.google-apps.folder'`;
215
+ const fields = 'files(id,name,size,modifiedTime)';
216
+ const res = await fetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=${fields}`, { headers: this.headers() });
217
+ const data = await res.json();
218
+ return (data.files || []).map((f) => {
219
+ const path = `${folderPath}/${f.name}`;
220
+ this.fileIdCache.set(path, f.id);
221
+ return {
222
+ name: f.name,
223
+ path,
224
+ size: parseInt(f.size ?? '0', 10),
225
+ modifiedTime: f.modifiedTime,
226
+ };
227
+ });
228
+ }
229
+ async listFolders(folderPath) {
230
+ const folderId = await this.resolveFolderId(folderPath);
231
+ if (!folderId)
232
+ return [];
233
+ const q = `'${folderId}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'`;
234
+ const fields = 'files(name)';
235
+ const res = await fetch(`${DRIVE_API}/files?q=${encodeURIComponent(q)}&fields=${fields}`, { headers: this.headers() });
236
+ const data = await res.json();
237
+ return (data.files || []).map((f) => f.name);
238
+ }
239
+ async readFile(path) {
240
+ const fileId = await this.resolveFileId(path);
241
+ if (!fileId)
242
+ throw new Error(`File not found: ${path}`);
243
+ const res = await fetch(`${DRIVE_API}/files/${fileId}?alt=media`, { headers: this.headers() });
244
+ if (!res.ok)
245
+ throw new Error(`Failed to read ${path}: ${res.status}`);
246
+ const buffer = await res.arrayBuffer();
247
+ return new Uint8Array(buffer);
248
+ }
249
+ async writeFile(path, data) {
250
+ const bytes = typeof data === 'string'
251
+ ? new TextEncoder().encode(data)
252
+ : data;
253
+ const existingId = await this.resolveFileId(path);
254
+ if (existingId) {
255
+ // Update existing file
256
+ const res = await fetch(`${UPLOAD_API}/files/${existingId}?uploadType=media`, {
257
+ method: 'PATCH',
258
+ headers: {
259
+ ...this.headers(),
260
+ 'Content-Type': 'application/octet-stream',
261
+ },
262
+ body: bytes,
263
+ });
264
+ if (!res.ok)
265
+ throw new Error(`Failed to write ${path}: ${res.status}`);
266
+ }
267
+ else {
268
+ // Create new file
269
+ const parts = path.split('/').filter(Boolean);
270
+ const fileName = parts.pop();
271
+ const parentPath = '/' + parts.join('/');
272
+ let parentId = await this.resolveFolderId(parentPath);
273
+ if (!parentId) {
274
+ await this.ensureFolder(parentPath);
275
+ parentId = await this.resolveFolderId(parentPath);
276
+ }
277
+ const metadata = {
278
+ name: fileName,
279
+ parents: [parentId],
280
+ };
281
+ // Multipart upload
282
+ const boundary = 'interocitor_boundary';
283
+ const body = `--${boundary}\r\n` +
284
+ `Content-Type: application/json; charset=UTF-8\r\n\r\n` +
285
+ `${JSON.stringify(metadata)}\r\n` +
286
+ `--${boundary}\r\n` +
287
+ `Content-Type: application/octet-stream\r\n\r\n`;
288
+ const bodyEnd = `\r\n--${boundary}--`;
289
+ const bodyBytes = new TextEncoder().encode(body);
290
+ const endBytes = new TextEncoder().encode(bodyEnd);
291
+ const combined = new Uint8Array(bodyBytes.length + bytes.length + endBytes.length);
292
+ combined.set(bodyBytes, 0);
293
+ combined.set(bytes, bodyBytes.length);
294
+ combined.set(endBytes, bodyBytes.length + bytes.length);
295
+ const res = await fetch(`${UPLOAD_API}/files?uploadType=multipart`, {
296
+ method: 'POST',
297
+ headers: {
298
+ ...this.headers(),
299
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
300
+ },
301
+ body: combined,
302
+ });
303
+ if (!res.ok)
304
+ throw new Error(`Failed to create ${path}: ${res.status}`);
305
+ const created = await res.json();
306
+ this.fileIdCache.set(path, created.id);
307
+ }
308
+ }
309
+ async deleteFile(path) {
310
+ const fileId = await this.resolveFileId(path);
311
+ if (!fileId)
312
+ return;
313
+ await fetch(`${DRIVE_API}/files/${fileId}`, {
314
+ method: 'DELETE',
315
+ headers: this.headers(),
316
+ });
317
+ this.fileIdCache.delete(path);
318
+ }
319
+ async getFileMetadata(path) {
320
+ const fileId = await this.resolveFileId(path);
321
+ if (!fileId)
322
+ return null;
323
+ const res = await fetch(`${DRIVE_API}/files/${fileId}?fields=id,name,size,modifiedTime`, { headers: this.headers() });
324
+ if (!res.ok)
325
+ return null;
326
+ const data = await res.json();
327
+ return {
328
+ name: data.name,
329
+ path,
330
+ size: parseInt(data.size ?? '0', 10),
331
+ modifiedTime: data.modifiedTime,
332
+ };
333
+ }
334
+ /** Clear caches (useful after compaction deletes files). */
335
+ clearCache() {
336
+ this.fileIdCache.clear();
337
+ this.folderIdCache.clear();
338
+ }
339
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * In-Memory Storage Adapter
3
+ *
4
+ * For testing and development. No network, no persistence.
5
+ * Also serves as a reference implementation for the StorageAdapter interface.
6
+ */
7
+ import type { StorageAdapter, FileEntry, StoredFileMetadata, StoredFileWriteOptions } from '../core/types.ts';
8
+ /**
9
+ * In-memory implementation of {@link StorageAdapter}.
10
+ *
11
+ * Useful for tests, demos, and local experiments where persistence is not
12
+ * required.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const adapter = new MemoryAdapter();
17
+ * const engine = new Interocitor(adapter, { remotePath: '/Demo' });
18
+ * ```
19
+ */
20
+ export declare class MemoryAdapter implements StorageAdapter {
21
+ readonly name = "memory";
22
+ private files;
23
+ private storedFileMetadata;
24
+ private folders;
25
+ private authenticated;
26
+ private ensuredFolders;
27
+ authenticate(): Promise<void>;
28
+ /**
29
+ * Memory adapter has no real backend — returns an empty config.
30
+ * Only useful in tests where both sides share the same in-memory store.
31
+ */
32
+ getHandshakeConfig(): string;
33
+ isAuthenticated(): boolean;
34
+ ensureFolder(path: string): Promise<void>;
35
+ /** Drop the per-session ensureFolder cache. Tests / mesh-swap callers. */
36
+ resetFolderCache(): void;
37
+ listFiles(folderPath: string): Promise<FileEntry[]>;
38
+ /** List immediate subfolder names under a path. */
39
+ listFolders(folderPath: string): Promise<string[]>;
40
+ readFile(path: string): Promise<Uint8Array>;
41
+ writeFile(path: string, data: Uint8Array | string): Promise<void>;
42
+ deleteFile(path: string): Promise<void>;
43
+ getFileMetadata(path: string): Promise<FileEntry | null>;
44
+ putStoredFile(path: string, data: Uint8Array | string, options?: StoredFileWriteOptions): Promise<StoredFileMetadata>;
45
+ getStoredFile(path: string): Promise<Uint8Array>;
46
+ deleteStoredFile(path: string): Promise<void>;
47
+ getStoredFileMetadata(path: string): Promise<StoredFileMetadata | null>;
48
+ /** Test helper: dump all files for inspection. */
49
+ dump(): Record<string, string>;
50
+ /** Test helper: reset all state. */
51
+ reset(): void;
52
+ }
53
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE9G;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,KAAK,CAAsE;IACnF,OAAO,CAAC,kBAAkB,CAA8C;IACxE,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,aAAa,CAAS;IAK9B,OAAO,CAAC,cAAc,CAA0B;IAE1C,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC;;;OAGG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/C,0EAA0E;IAC1E,gBAAgB,IAAI,IAAI;IAIlB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAsBzD,mDAAmD;IAC7C,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAuBlD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAYxD,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAqBzH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAUhD,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7C,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAO7E,kDAAkD;IAClD,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9B,oCAAoC;IACpC,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * In-Memory Storage Adapter
3
+ *
4
+ * For testing and development. No network, no persistence.
5
+ * Also serves as a reference implementation for the StorageAdapter interface.
6
+ */
7
+ /**
8
+ * In-memory implementation of {@link StorageAdapter}.
9
+ *
10
+ * Useful for tests, demos, and local experiments where persistence is not
11
+ * required.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const adapter = new MemoryAdapter();
16
+ * const engine = new Interocitor(adapter, { remotePath: '/Demo' });
17
+ * ```
18
+ */
19
+ export class MemoryAdapter {
20
+ constructor() {
21
+ this.name = 'memory';
22
+ this.files = new Map();
23
+ this.storedFileMetadata = new Map();
24
+ this.folders = new Set();
25
+ this.authenticated = false;
26
+ // Mirrors the cloud-adapter convention: cache "ensured" paths so a
27
+ // re-`ensureFolder` is a no-op. Memory adapter is cheap, but keeping
28
+ // the same shape lets tests assert call-count parity with the real
29
+ // adapters (cloudflare, webdav).
30
+ this.ensuredFolders = new Set();
31
+ }
32
+ async authenticate() {
33
+ this.authenticated = true;
34
+ }
35
+ /**
36
+ * Memory adapter has no real backend — returns an empty config.
37
+ * Only useful in tests where both sides share the same in-memory store.
38
+ */
39
+ getHandshakeConfig() {
40
+ return JSON.stringify({});
41
+ }
42
+ isAuthenticated() {
43
+ return this.authenticated;
44
+ }
45
+ async ensureFolder(path) {
46
+ if (this.ensuredFolders.has(path))
47
+ return;
48
+ this.folders.add(path);
49
+ this.ensuredFolders.add(path);
50
+ }
51
+ /** Drop the per-session ensureFolder cache. Tests / mesh-swap callers. */
52
+ resetFolderCache() {
53
+ this.ensuredFolders.clear();
54
+ }
55
+ async listFiles(folderPath) {
56
+ const prefix = folderPath.endsWith('/') ? folderPath : folderPath + '/';
57
+ const entries = [];
58
+ for (const [path, file] of this.files) {
59
+ if (path.startsWith(prefix)) {
60
+ const remaining = path.slice(prefix.length);
61
+ // Only direct children (no nested slashes)
62
+ if (!remaining.includes('/')) {
63
+ entries.push({
64
+ name: remaining,
65
+ path,
66
+ size: file.data.length,
67
+ modifiedTime: file.modifiedTime,
68
+ });
69
+ }
70
+ }
71
+ }
72
+ return entries;
73
+ }
74
+ /** List immediate subfolder names under a path. */
75
+ async listFolders(folderPath) {
76
+ const prefix = folderPath.endsWith('/') ? folderPath : folderPath + '/';
77
+ const names = new Set();
78
+ for (const path of this.files.keys()) {
79
+ if (path.startsWith(prefix)) {
80
+ const remaining = path.slice(prefix.length);
81
+ const slash = remaining.indexOf('/');
82
+ if (slash > 0) {
83
+ names.add(remaining.slice(0, slash));
84
+ }
85
+ }
86
+ }
87
+ for (const folder of this.folders) {
88
+ if (folder.startsWith(prefix)) {
89
+ const remaining = folder.slice(prefix.length);
90
+ if (remaining && !remaining.includes('/')) {
91
+ names.add(remaining);
92
+ }
93
+ }
94
+ }
95
+ return [...names];
96
+ }
97
+ async readFile(path) {
98
+ const file = this.files.get(path);
99
+ if (!file)
100
+ throw new Error(`File not found: ${path}`);
101
+ return file.data;
102
+ }
103
+ async writeFile(path, data) {
104
+ const bytes = typeof data === 'string'
105
+ ? new TextEncoder().encode(data)
106
+ : data;
107
+ this.files.set(path, {
108
+ data: bytes,
109
+ modifiedTime: new Date().toISOString(),
110
+ });
111
+ }
112
+ async deleteFile(path) {
113
+ this.files.delete(path);
114
+ }
115
+ async getFileMetadata(path) {
116
+ const file = this.files.get(path);
117
+ if (!file)
118
+ return null;
119
+ const name = path.split('/').pop() || path;
120
+ return {
121
+ name,
122
+ path,
123
+ size: file.data.length,
124
+ modifiedTime: file.modifiedTime,
125
+ };
126
+ }
127
+ async putStoredFile(path, data, options = {}) {
128
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
129
+ await this.writeFile(path, bytes);
130
+ const now = new Date().toISOString();
131
+ const meta = {
132
+ name: path.split('/').pop() || path,
133
+ path,
134
+ size: bytes.byteLength,
135
+ modifiedTime: now,
136
+ uploadedAt: now,
137
+ uploadedByDeviceId: options.uploadedByDeviceId,
138
+ plaintextSize: options.plaintextSize,
139
+ storedSize: bytes.byteLength,
140
+ contentType: options.contentType,
141
+ lastAccessedAt: undefined,
142
+ useCount: 0,
143
+ };
144
+ this.storedFileMetadata.set(path, meta);
145
+ return { ...meta };
146
+ }
147
+ async getStoredFile(path) {
148
+ const bytes = await this.readFile(path);
149
+ const meta = this.storedFileMetadata.get(path);
150
+ if (meta) {
151
+ const next = { ...meta, lastAccessedAt: new Date().toISOString(), useCount: (meta.useCount ?? 0) + 1 };
152
+ this.storedFileMetadata.set(path, next);
153
+ }
154
+ return bytes;
155
+ }
156
+ async deleteStoredFile(path) {
157
+ await this.deleteFile(path);
158
+ this.storedFileMetadata.delete(path);
159
+ }
160
+ async getStoredFileMetadata(path) {
161
+ const meta = this.storedFileMetadata.get(path);
162
+ if (meta)
163
+ return { ...meta };
164
+ const file = await this.getFileMetadata(path);
165
+ return file ? { ...file, storedSize: file.size } : null;
166
+ }
167
+ /** Test helper: dump all files for inspection. */
168
+ dump() {
169
+ const result = {};
170
+ const decoder = new TextDecoder();
171
+ for (const [path, file] of this.files) {
172
+ result[path] = decoder.decode(file.data);
173
+ }
174
+ return result;
175
+ }
176
+ /** Test helper: reset all state. */
177
+ reset() {
178
+ this.files.clear();
179
+ this.storedFileMetadata.clear();
180
+ this.folders.clear();
181
+ }
182
+ }
@@ -0,0 +1,70 @@
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
+ import type { StorageAdapter, FileEntry, StoredFileMetadata, StoredFileWriteOptions } from '../core/types.ts';
11
+ interface WebDAVConfig {
12
+ /** Base URL of the WebDAV endpoint, e.g. "https://cloud.example.com/remote.php/dav/files/username" */
13
+ baseUrl: string;
14
+ /** Auth: either { username, password } for Basic auth, or { token } for Bearer */
15
+ auth: {
16
+ username: string;
17
+ password: string;
18
+ } | {
19
+ token: string;
20
+ };
21
+ }
22
+ /**
23
+ * Browser-friendly WebDAV adapter for Nextcloud, ownCloud, and compatible DAV servers.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const adapter = new WebDAVAdapter({
28
+ * baseUrl: 'https://cloud.example.com/remote.php/dav/files/alice',
29
+ * auth: { username: 'alice', password: 'APP_PASSWORD' },
30
+ * });
31
+ * ```
32
+ */
33
+ export declare class WebDAVAdapter implements StorageAdapter {
34
+ readonly name = "webdav";
35
+ private config;
36
+ private authenticated;
37
+ private ensuredFolders;
38
+ constructor(config: WebDAVConfig);
39
+ private url;
40
+ private authHeader;
41
+ private headers;
42
+ authenticate(): Promise<void>;
43
+ /**
44
+ * Returns the WebDAV base URL (without credentials) for embedding in a QR payload.
45
+ * The scanner uses this to point their WebDAVAdapter at the same server.
46
+ * Auth (username/password or token) must be configured separately by the user.
47
+ */
48
+ getHandshakeConfig(): string;
49
+ isAuthenticated(): boolean;
50
+ ensureFolder(path: string): Promise<void>;
51
+ /** Drop the per-session ensureFolder cache. Call after mesh swap, poison,
52
+ * or any state where we cannot trust a previous "this folder exists"
53
+ * observation. */
54
+ resetFolderCache(): void;
55
+ listFiles(folderPath: string): Promise<FileEntry[]>;
56
+ private parsePropfindResponse;
57
+ listFolders(folderPath: string): Promise<string[]>;
58
+ readFile(path: string): Promise<Uint8Array>;
59
+ writeFile(path: string, data: Uint8Array | string): Promise<void>;
60
+ deleteFile(path: string): Promise<void>;
61
+ private parentDir;
62
+ private storedMetaPath;
63
+ putStoredFile(path: string, data: Uint8Array | string, options?: StoredFileWriteOptions): Promise<StoredFileMetadata>;
64
+ getStoredFile(path: string): Promise<Uint8Array>;
65
+ deleteStoredFile(path: string): Promise<void>;
66
+ getStoredFileMetadata(path: string): Promise<StoredFileMetadata | null>;
67
+ getFileMetadata(path: string): Promise<FileEntry | null>;
68
+ }
69
+ export {};
70
+ //# sourceMappingURL=webdav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webdav.d.ts","sourceRoot":"","sources":["../../src/adapters/webdav.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE9G,UAAU,YAAY;IACpB,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED;;;;;;;;;;GAUG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,aAAa,CAAS;IAM9B,OAAO,CAAC,cAAc,CAA0B;gBAEpC,MAAM,EAAE,YAAY;IAIhC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,OAAO;IAOT,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBnC;;;;OAIG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB/C;;uBAEmB;IACnB,gBAAgB,IAAI,IAAI;IAIlB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0BzD,OAAO,CAAC,qBAAqB;IAoCvB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmClD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY7C,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,cAAc;IAIhB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAuBzH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAahD,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7C,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC;IAUvE,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;CAyC/D"}