@rool-dev/sdk 0.12.4 → 2.0.0-dev.4adc2c2

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 (108) hide show
  1. package/README.md +436 -761
  2. package/dist/agents.d.ts +130 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +348 -0
  5. package/dist/agents.js.map +1 -0
  6. package/dist/auth-base.d.ts +43 -88
  7. package/dist/auth-base.d.ts.map +1 -1
  8. package/dist/auth-base.js +178 -298
  9. package/dist/auth-base.js.map +1 -1
  10. package/dist/auth-browser.d.ts +7 -25
  11. package/dist/auth-browser.d.ts.map +1 -1
  12. package/dist/auth-browser.js +45 -82
  13. package/dist/auth-browser.js.map +1 -1
  14. package/dist/auth-native.d.ts +9 -56
  15. package/dist/auth-native.d.ts.map +1 -1
  16. package/dist/auth-native.js +88 -177
  17. package/dist/auth-native.js.map +1 -1
  18. package/dist/auth-node.d.ts +32 -32
  19. package/dist/auth-node.d.ts.map +1 -1
  20. package/dist/auth-node.js +339 -255
  21. package/dist/auth-node.js.map +1 -1
  22. package/dist/auth-url.d.ts +6 -0
  23. package/dist/auth-url.d.ts.map +1 -0
  24. package/dist/auth-url.js +30 -0
  25. package/dist/auth-url.js.map +1 -0
  26. package/dist/client.d.ts +35 -277
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +275 -620
  29. package/dist/client.js.map +1 -1
  30. package/dist/events.d.ts +40 -0
  31. package/dist/events.d.ts.map +1 -0
  32. package/dist/events.js +175 -0
  33. package/dist/events.js.map +1 -0
  34. package/dist/files.d.ts +170 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +846 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/index.d.ts +16 -17
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -19
  41. package/dist/index.js.map +1 -1
  42. package/dist/machine.d.ts +49 -0
  43. package/dist/machine.d.ts.map +1 -0
  44. package/dist/machine.js +142 -0
  45. package/dist/machine.js.map +1 -0
  46. package/dist/problem.d.ts +11 -0
  47. package/dist/problem.d.ts.map +1 -0
  48. package/dist/problem.js +20 -0
  49. package/dist/problem.js.map +1 -0
  50. package/dist/structured.d.ts +84 -0
  51. package/dist/structured.d.ts.map +1 -0
  52. package/dist/structured.js +405 -0
  53. package/dist/structured.js.map +1 -0
  54. package/dist/types.d.ts +119 -520
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +11 -16
  57. package/dist/auth.d.ts +0 -85
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js +0 -181
  60. package/dist/auth.js.map +0 -1
  61. package/dist/client-info.d.ts +0 -18
  62. package/dist/client-info.d.ts.map +0 -1
  63. package/dist/client-info.js +0 -25
  64. package/dist/client-info.js.map +0 -1
  65. package/dist/event-emitter.d.ts +0 -45
  66. package/dist/event-emitter.d.ts.map +0 -1
  67. package/dist/event-emitter.js +0 -86
  68. package/dist/event-emitter.js.map +0 -1
  69. package/dist/graphql.d.ts +0 -90
  70. package/dist/graphql.d.ts.map +0 -1
  71. package/dist/graphql.js +0 -463
  72. package/dist/graphql.js.map +0 -1
  73. package/dist/logger.d.ts +0 -13
  74. package/dist/logger.d.ts.map +0 -1
  75. package/dist/logger.js +0 -17
  76. package/dist/logger.js.map +0 -1
  77. package/dist/path.d.ts +0 -6
  78. package/dist/path.d.ts.map +0 -1
  79. package/dist/path.js +0 -47
  80. package/dist/path.js.map +0 -1
  81. package/dist/reroute.d.ts +0 -22
  82. package/dist/reroute.d.ts.map +0 -1
  83. package/dist/reroute.js +0 -70
  84. package/dist/reroute.js.map +0 -1
  85. package/dist/rest.d.ts +0 -54
  86. package/dist/rest.d.ts.map +0 -1
  87. package/dist/rest.js +0 -171
  88. package/dist/rest.js.map +0 -1
  89. package/dist/router.d.ts +0 -20
  90. package/dist/router.d.ts.map +0 -1
  91. package/dist/router.js +0 -54
  92. package/dist/router.js.map +0 -1
  93. package/dist/space-session.d.ts +0 -30
  94. package/dist/space-session.d.ts.map +0 -1
  95. package/dist/space-session.js +0 -83
  96. package/dist/space-session.js.map +0 -1
  97. package/dist/space.d.ts +0 -161
  98. package/dist/space.d.ts.map +0 -1
  99. package/dist/space.js +0 -618
  100. package/dist/space.js.map +0 -1
  101. package/dist/subscription.d.ts +0 -36
  102. package/dist/subscription.d.ts.map +0 -1
  103. package/dist/subscription.js +0 -419
  104. package/dist/subscription.js.map +0 -1
  105. package/dist/webdav.d.ts +0 -171
  106. package/dist/webdav.d.ts.map +0 -1
  107. package/dist/webdav.js +0 -481
  108. package/dist/webdav.js.map +0 -1
package/dist/files.js ADDED
@@ -0,0 +1,846 @@
1
+ const SYNC_WAIT_SECONDS = 30;
2
+ const SYNC_RETRY_MAX_MS = 5_000;
3
+ const DELETE_MULTIPLE_CONCURRENCY = 8;
4
+ const FILE_PROPERTIES = `
5
+ <d:creationdate/>
6
+ <d:displayname/>
7
+ <d:getcontentlength/>
8
+ <d:getcontenttype/>
9
+ <d:getetag/>
10
+ <d:getlastmodified/>
11
+ <d:resourcetype/>
12
+ <r:owner-id xmlns:r="urn:rool:dav"/>
13
+ <r:access xmlns:r="urn:rool:dav"/>`;
14
+ const PROPFIND_BODY = `<?xml version="1.0" encoding="utf-8"?>
15
+ <d:propfind xmlns:d="DAV:">
16
+ <d:prop>${FILE_PROPERTIES}
17
+ </d:prop>
18
+ </d:propfind>`;
19
+ const STORAGE_PROPFIND_BODY = `<?xml version="1.0" encoding="utf-8"?>
20
+ <d:propfind xmlns:d="DAV:">
21
+ <d:prop>
22
+ <d:quota-used-bytes/>
23
+ <d:quota-available-bytes/>
24
+ </d:prop>
25
+ </d:propfind>`;
26
+ function readMultipleBody(hrefs) {
27
+ return `<?xml version="1.0" encoding="utf-8"?>
28
+ <r:read-multiple xmlns:r="urn:rool:dav" xmlns:d="DAV:">
29
+ ${hrefs.map((href) => ` <d:href>${encodeXml(href)}</d:href>`).join("\n")}
30
+ </r:read-multiple>`;
31
+ }
32
+ function syncCollectionBody(token) {
33
+ return `<?xml version="1.0" encoding="utf-8"?>
34
+ <d:sync-collection xmlns:d="DAV:">
35
+ ${token ? `<d:sync-token>${encodeXml(token)}</d:sync-token>` : ""}
36
+ <d:sync-level>infinite</d:sync-level>
37
+ <d:prop>${FILE_PROPERTIES}
38
+ </d:prop>
39
+ </d:sync-collection>`;
40
+ }
41
+ export class RoolFileError extends Error {
42
+ status;
43
+ statusText;
44
+ body;
45
+ constructor(response, body) {
46
+ super(`Machine file request failed: ${response.status} ${response.statusText}`);
47
+ this.name = "RoolFileError";
48
+ this.status = response.status;
49
+ this.statusText = response.statusText;
50
+ this.body = body;
51
+ }
52
+ }
53
+ class SyncedMachineFileTree {
54
+ entries = new Map();
55
+ listeners = new Set();
56
+ get(path) {
57
+ const info = this.entries.get(path);
58
+ return info ? cloneFileInfo(info) : undefined;
59
+ }
60
+ etag(path) {
61
+ return this.entries.get(path)?.etag;
62
+ }
63
+ list(path, options = {}) {
64
+ const entries = [...this.entries.values()].filter((entry) => {
65
+ if (!path) {
66
+ return options.recursive || parentPath(entry.path) === undefined;
67
+ }
68
+ if (entry.path === path || !entry.path.startsWith(`${path}/`)) {
69
+ return false;
70
+ }
71
+ return options.recursive || parentPath(entry.path) === path;
72
+ });
73
+ return entries
74
+ .sort((left, right) => left.path.localeCompare(right.path))
75
+ .map(cloneFileInfo);
76
+ }
77
+ subscribe(listener) {
78
+ this.listeners.add(listener);
79
+ return () => this.listeners.delete(listener);
80
+ }
81
+ reconcile(nextEntries) {
82
+ const next = new Map(nextEntries.map((entry) => [entry.path, cloneFileInfo(entry)]));
83
+ const changed = nextEntries
84
+ .filter((entry) => !sameFileInfo(this.entries.get(entry.path), entry))
85
+ .map((entry) => entry.path);
86
+ const deleted = [...this.entries.keys()].filter((path) => !next.has(path));
87
+ this.entries = next;
88
+ this.emit({ reset: true, changed, deleted });
89
+ }
90
+ apply(updates) {
91
+ const changed = new Set();
92
+ const deleted = new Set();
93
+ for (const update of updates) {
94
+ if (update.info) {
95
+ if (!sameFileInfo(this.entries.get(update.path), update.info)) {
96
+ this.entries.set(update.path, cloneFileInfo(update.info));
97
+ changed.add(update.path);
98
+ }
99
+ continue;
100
+ }
101
+ for (const existingPath of [...this.entries.keys()]) {
102
+ const isDeletedPath = existingPath === update.path ||
103
+ existingPath.startsWith(`${update.path}/`);
104
+ if (!isDeletedPath)
105
+ continue;
106
+ this.entries.delete(existingPath);
107
+ changed.delete(existingPath);
108
+ deleted.add(existingPath);
109
+ }
110
+ }
111
+ if (changed.size === 0 && deleted.size === 0)
112
+ return;
113
+ this.emit({
114
+ reset: false,
115
+ changed: [...changed],
116
+ deleted: [...deleted],
117
+ });
118
+ }
119
+ emit(change) {
120
+ for (const listener of this.listeners)
121
+ listener(change);
122
+ }
123
+ }
124
+ class SyncedMachineFiles {
125
+ machineId;
126
+ request;
127
+ tree;
128
+ syncedTree = new SyncedMachineFileTree();
129
+ watchController = null;
130
+ startingWatch = null;
131
+ syncToken = null;
132
+ currentSyncError = null;
133
+ constructor(machineId, request) {
134
+ this.machineId = machineId;
135
+ this.request = request;
136
+ if (!machineId)
137
+ throw new Error("machineId is required");
138
+ this.tree = this.syncedTree;
139
+ }
140
+ get isWatching() {
141
+ return this.watchController !== null;
142
+ }
143
+ get watchError() {
144
+ return this.currentSyncError;
145
+ }
146
+ async watch() {
147
+ if (this.startingWatch)
148
+ return this.startingWatch;
149
+ if (this.watchController)
150
+ return;
151
+ const controller = new AbortController();
152
+ this.watchController = controller;
153
+ const startingWatch = this.initializeSync(controller);
154
+ this.startingWatch = startingWatch;
155
+ try {
156
+ await startingWatch;
157
+ }
158
+ catch (error) {
159
+ if (this.watchController === controller)
160
+ this.watchController = null;
161
+ controller.abort();
162
+ throw error;
163
+ }
164
+ finally {
165
+ if (this.startingWatch === startingWatch)
166
+ this.startingWatch = null;
167
+ }
168
+ }
169
+ unwatch() {
170
+ this.watchController?.abort();
171
+ this.watchController = null;
172
+ this.syncToken = null;
173
+ }
174
+ href(path) {
175
+ const prefix = `/v2/machines/${encodeURIComponent(this.machineId)}/dav`;
176
+ if (!path)
177
+ return prefix;
178
+ return `${prefix}/${encodeFilePath(path)}`;
179
+ }
180
+ async options() {
181
+ const response = await this.request(this.href(), { method: "OPTIONS" });
182
+ await assertStatus(response, 204);
183
+ return {
184
+ methods: (response.headers.get("Allow") ?? "")
185
+ .split(",")
186
+ .map((method) => method.trim())
187
+ .filter(Boolean),
188
+ acceptsRanges: response.headers.get("Accept-Ranges") === "bytes",
189
+ createsParents: (response.headers.get("DAV") ?? "")
190
+ .split(",")
191
+ .map((token) => token.trim())
192
+ .includes("rool-create-parents"),
193
+ readsMultiple: (response.headers.get("DAV") ?? "")
194
+ .split(",")
195
+ .map((token) => token.trim())
196
+ .includes("rool-read-multiple"),
197
+ synchronizes: (response.headers.get("DAV") ?? "")
198
+ .split(",")
199
+ .map((token) => token.trim())
200
+ .includes("sync-collection"),
201
+ };
202
+ }
203
+ async getStorageUsage(signal) {
204
+ const response = await this.request(this.href(), {
205
+ method: "PROPFIND",
206
+ headers: {
207
+ "Content-Type": "application/xml; charset=utf-8",
208
+ Depth: "0",
209
+ },
210
+ body: STORAGE_PROPFIND_BODY,
211
+ signal,
212
+ });
213
+ await assertStatus(response, 207);
214
+ return parseStorageUsage(await response.text());
215
+ }
216
+ async read(path, options = {}) {
217
+ const headers = conditionHeaders(options);
218
+ if (options.range)
219
+ headers.set("Range", rangeHeader(options.range));
220
+ if (options.ifRange)
221
+ headers.set("If-Range", options.ifRange);
222
+ const response = await this.request(this.href(path), {
223
+ method: "GET",
224
+ headers,
225
+ signal: options.signal,
226
+ });
227
+ if ([200, 206, 304].includes(response.status))
228
+ return response;
229
+ throw await fileError(response);
230
+ }
231
+ async readMultiple(paths, options = {}) {
232
+ if (paths.length === 0)
233
+ return [];
234
+ const response = await this.request(this.href(), {
235
+ method: "REPORT",
236
+ headers: { "Content-Type": "application/xml; charset=utf-8" },
237
+ body: readMultipleBody(paths.map((path) => this.href(path))),
238
+ signal: options.signal,
239
+ });
240
+ await assertStatus(response, 200);
241
+ return parseReadMultiple(new Uint8Array(await response.arrayBuffer()), response.headers.get("Content-Type"), this.machineId, paths);
242
+ }
243
+ async stat(path, signal) {
244
+ const entries = await this.propfind(path, "0", signal);
245
+ const entry = entries.find((candidate) => candidate.path === path);
246
+ if (!entry)
247
+ throw new Error(`PROPFIND did not return ${path}`);
248
+ return entry;
249
+ }
250
+ async list(path, options = {}) {
251
+ const depth = options.recursive ? "infinity" : "1";
252
+ const entries = await this.propfind(path, depth, options.signal);
253
+ return path
254
+ ? entries.filter((candidate) => candidate.path !== path)
255
+ : entries;
256
+ }
257
+ async write(path, body, options = {}) {
258
+ const headers = conditionHeaders(options);
259
+ headers.set("Prefer", "return=representation");
260
+ if (options.contentType)
261
+ headers.set("Content-Type", options.contentType);
262
+ if (options.createParents) {
263
+ headers.set("Rool-Create-Parents", options.createParents === "private" ? "private" : "1");
264
+ }
265
+ const response = await this.request(this.href(path), {
266
+ method: "PUT",
267
+ headers,
268
+ body,
269
+ signal: options.signal,
270
+ }, options.onUploadProgress);
271
+ await assertStatus(response, 200, 201);
272
+ const written = parsePropfind(await response.text(), this.machineId).find((candidate) => candidate.path === path);
273
+ if (!written)
274
+ throw new Error(`PUT did not return metadata for ${path}`);
275
+ return written;
276
+ }
277
+ async delete(path, options = {}) {
278
+ const response = await this.request(this.href(path), {
279
+ method: "DELETE",
280
+ headers: conditionHeaders(options),
281
+ signal: options.signal,
282
+ });
283
+ await assertStatus(response, 204);
284
+ }
285
+ async deleteMultiple(targets, options = {}) {
286
+ options.signal?.throwIfAborted();
287
+ const results = new Array(targets.length);
288
+ let nextIndex = 0;
289
+ const deleteNext = async () => {
290
+ while (nextIndex < targets.length) {
291
+ const index = nextIndex++;
292
+ const target = targets[index];
293
+ const request = typeof target === "string" ? { path: target } : target;
294
+ const { path, ...preconditions } = request;
295
+ try {
296
+ await this.delete(path, { ...preconditions, signal: options.signal });
297
+ results[index] = { ok: true, path };
298
+ }
299
+ catch (error) {
300
+ if (options.signal?.aborted)
301
+ throw options.signal.reason;
302
+ results[index] = { ok: false, path, error };
303
+ }
304
+ }
305
+ };
306
+ const concurrency = Math.min(DELETE_MULTIPLE_CONCURRENCY, targets.length);
307
+ await Promise.all(Array.from({ length: concurrency }, deleteNext));
308
+ return results;
309
+ }
310
+ async createDirectory(path, options = {}) {
311
+ const response = await this.request(this.href(path), {
312
+ method: "MKCOL",
313
+ signal: options.signal,
314
+ });
315
+ await assertStatus(response, 201);
316
+ }
317
+ async move(source, destination, options = {}) {
318
+ return this.transfer("MOVE", source, destination, options, "infinity");
319
+ }
320
+ async copy(source, destination, options = {}) {
321
+ const depth = options.recursive === false ? "0" : "infinity";
322
+ return this.transfer("COPY", source, destination, options, depth);
323
+ }
324
+ async initializeSync(controller) {
325
+ const report = await this.syncCollection(undefined, controller.signal);
326
+ if (report.status === "invalid") {
327
+ throw new Error("Initial sync-collection returned an invalid token");
328
+ }
329
+ if (controller.signal.aborted)
330
+ return;
331
+ this.syncedTree.reconcile(report.updates.flatMap((update) => (update.info ? [update.info] : [])));
332
+ this.syncToken = report.token;
333
+ this.currentSyncError = null;
334
+ void this.runSyncLoop(controller);
335
+ }
336
+ async runSyncLoop(controller) {
337
+ let retryMs = 250;
338
+ while (this.watchController === controller && !controller.signal.aborted) {
339
+ try {
340
+ const token = this.syncToken;
341
+ if (!token)
342
+ throw new Error("Machine file sync token is missing");
343
+ let report = await this.syncCollection(token, controller.signal, SYNC_WAIT_SECONDS);
344
+ if (report.status === "invalid") {
345
+ report = await this.syncCollection(undefined, controller.signal);
346
+ if (report.status === "invalid") {
347
+ throw new Error("Initial sync-collection returned an invalid token");
348
+ }
349
+ if (controller.signal.aborted)
350
+ return;
351
+ this.syncedTree.reconcile(report.updates.flatMap((update) => update.info ? [update.info] : []));
352
+ }
353
+ else {
354
+ if (controller.signal.aborted)
355
+ return;
356
+ this.syncedTree.apply(report.updates);
357
+ }
358
+ this.syncToken = report.token;
359
+ this.currentSyncError = null;
360
+ retryMs = 250;
361
+ }
362
+ catch (error) {
363
+ if (controller.signal.aborted)
364
+ return;
365
+ this.currentSyncError = error;
366
+ await abortableDelay(retryMs, controller.signal);
367
+ retryMs = Math.min(retryMs * 2, SYNC_RETRY_MAX_MS);
368
+ }
369
+ }
370
+ }
371
+ async syncCollection(token, signal, waitSeconds) {
372
+ const headers = new Headers({
373
+ "Content-Type": "application/xml; charset=utf-8",
374
+ Depth: "infinity",
375
+ });
376
+ if (waitSeconds !== undefined) {
377
+ headers.set("Prefer", `wait=${waitSeconds}`);
378
+ }
379
+ const response = await this.request(this.href(), {
380
+ method: "REPORT",
381
+ headers,
382
+ body: syncCollectionBody(token),
383
+ signal,
384
+ });
385
+ const body = await response.text();
386
+ if (response.status === 403 && hasXmlElement(body, "valid-sync-token")) {
387
+ return { status: "invalid" };
388
+ }
389
+ if (response.status !== 207)
390
+ throw new RoolFileError(response, body);
391
+ return {
392
+ status: "ok",
393
+ ...parseSyncCollection(body, this.machineId),
394
+ };
395
+ }
396
+ async propfind(path, depth, signal) {
397
+ const response = await this.request(this.href(path), {
398
+ method: "PROPFIND",
399
+ headers: {
400
+ "Content-Type": "application/xml; charset=utf-8",
401
+ Depth: depth,
402
+ },
403
+ body: PROPFIND_BODY,
404
+ signal,
405
+ });
406
+ await assertStatus(response, 207);
407
+ return parsePropfind(await response.text(), this.machineId);
408
+ }
409
+ async transfer(method, source, destination, options, depth) {
410
+ const headers = conditionHeaders(options);
411
+ headers.set("Depth", depth);
412
+ headers.set("Destination", this.href(destination));
413
+ headers.set("Overwrite", options.overwrite === false ? "F" : "T");
414
+ const response = await this.request(this.href(source), {
415
+ method,
416
+ headers,
417
+ signal: options.signal,
418
+ });
419
+ await assertStatus(response, 201, 204);
420
+ return {
421
+ source,
422
+ destination,
423
+ overwritten: response.status === 204,
424
+ };
425
+ }
426
+ }
427
+ export function createMachineFiles(machineId, request) {
428
+ return new SyncedMachineFiles(machineId, request);
429
+ }
430
+ function encodeFilePath(path) {
431
+ const isSpacePath = path === "/space" || path.startsWith("/space/");
432
+ const isDrivePath = path === "/rool-drive" || path.startsWith("/rool-drive/");
433
+ if (!isSpacePath && !isDrivePath) {
434
+ throw new Error("Machine file paths must be under /space or /rool-drive");
435
+ }
436
+ const segments = path.slice(1).split("/");
437
+ for (const segment of segments) {
438
+ if (!segment ||
439
+ segment === "." ||
440
+ segment === ".." ||
441
+ segment.includes("\0")) {
442
+ throw new Error(`Invalid machine file path: ${path}`);
443
+ }
444
+ }
445
+ return segments.map(encodeURIComponent).join("/");
446
+ }
447
+ function conditionHeaders(options) {
448
+ const headers = new Headers();
449
+ if (options.ifMatch)
450
+ headers.set("If-Match", options.ifMatch);
451
+ if (options.ifNoneMatch)
452
+ headers.set("If-None-Match", options.ifNoneMatch);
453
+ if (options.ifModifiedSince)
454
+ headers.set("If-Modified-Since", options.ifModifiedSince);
455
+ if (options.ifUnmodifiedSince)
456
+ headers.set("If-Unmodified-Since", options.ifUnmodifiedSince);
457
+ return headers;
458
+ }
459
+ function rangeHeader(range) {
460
+ if ("suffixLength" in range) {
461
+ if (!Number.isSafeInteger(range.suffixLength) || range.suffixLength <= 0) {
462
+ throw new Error("Machine file range suffixLength must be a positive integer");
463
+ }
464
+ return `bytes=-${range.suffixLength}`;
465
+ }
466
+ if (!Number.isSafeInteger(range.start) || range.start < 0) {
467
+ throw new Error("Machine file range start must be a non-negative integer");
468
+ }
469
+ if (range.end === undefined)
470
+ return `bytes=${range.start}-`;
471
+ if (!Number.isSafeInteger(range.end) || range.end < range.start) {
472
+ throw new Error("Machine file range end must be at least its start");
473
+ }
474
+ return `bytes=${range.start}-${range.end}`;
475
+ }
476
+ function parseReadMultiple(data, contentType, machineId, requestedPaths) {
477
+ const boundary = multipartBoundary(contentType);
478
+ const marker = new TextEncoder().encode(`--${boundary}`);
479
+ const crlf = new Uint8Array([13, 10]);
480
+ const headerEnd = new Uint8Array([13, 10, 13, 10]);
481
+ const decoder = new TextDecoder("utf-8", { fatal: true });
482
+ const results = [];
483
+ let offset = 0;
484
+ expectBytes(data, offset, marker, "multipart boundary");
485
+ offset += marker.length;
486
+ while (true) {
487
+ if (data[offset] === 45 && data[offset + 1] === 45) {
488
+ offset += 2;
489
+ if (offset < data.length) {
490
+ expectBytes(data, offset, crlf, "multipart closing newline");
491
+ offset += crlf.length;
492
+ }
493
+ if (offset !== data.length) {
494
+ throw new Error("read-multiple response has data after its boundary");
495
+ }
496
+ break;
497
+ }
498
+ expectBytes(data, offset, crlf, "multipart boundary newline");
499
+ offset += crlf.length;
500
+ const outerEnd = findBytes(data, headerEnd, offset);
501
+ if (outerEnd < 0) {
502
+ throw new Error("read-multiple part has incomplete headers");
503
+ }
504
+ const outerHeaders = parseHttpHeaders(decoder.decode(data.subarray(offset, outerEnd)));
505
+ if (outerHeaders.get("content-type")?.toLowerCase() !== "application/http") {
506
+ throw new Error("read-multiple part is not application/http");
507
+ }
508
+ offset = outerEnd + headerEnd.length;
509
+ const responseEnd = findBytes(data, headerEnd, offset);
510
+ if (responseEnd < 0) {
511
+ throw new Error("read-multiple HTTP response has incomplete headers");
512
+ }
513
+ const responseLines = decoder
514
+ .decode(data.subarray(offset, responseEnd))
515
+ .split("\r\n");
516
+ const statusMatch = /^HTTP\/1\.1\s+(\d{3})(?:\s|$)/.exec(responseLines.shift() ?? "");
517
+ if (!statusMatch) {
518
+ throw new Error("read-multiple part has an invalid HTTP status");
519
+ }
520
+ const status = Number(statusMatch[1]);
521
+ const headers = parseHttpHeaders(responseLines.join("\r\n"));
522
+ const rawLength = headers.get("content-length");
523
+ const length = rawLength === undefined ? NaN : Number(rawLength);
524
+ if (!Number.isSafeInteger(length) || length < 0) {
525
+ throw new Error("read-multiple part has an invalid Content-Length");
526
+ }
527
+ const href = headers.get("content-location");
528
+ if (!href) {
529
+ throw new Error("read-multiple part is missing Content-Location");
530
+ }
531
+ const path = machinePathFromHref(href, machineId);
532
+ const expectedPath = requestedPaths[results.length];
533
+ if (!path || path !== expectedPath) {
534
+ throw new Error(`read-multiple returned ${path ?? href}, expected ${expectedPath}`);
535
+ }
536
+ offset = responseEnd + headerEnd.length;
537
+ if (offset + length > data.length) {
538
+ throw new Error(`read-multiple body is truncated for ${path}`);
539
+ }
540
+ const body = data.slice(offset, offset + length);
541
+ offset += length;
542
+ expectBytes(data, offset, crlf, "read-multiple body newline");
543
+ offset += crlf.length;
544
+ if (status === 200) {
545
+ const etag = headers.get("etag");
546
+ const fileContentType = headers.get("content-type");
547
+ if (!etag || !fileContentType) {
548
+ throw new Error(`read-multiple omitted file metadata for ${path}`);
549
+ }
550
+ results.push({
551
+ ok: true,
552
+ path,
553
+ status: 200,
554
+ etag,
555
+ contentType: fileContentType,
556
+ size: length,
557
+ body,
558
+ });
559
+ }
560
+ else {
561
+ results.push({ ok: false, path, status });
562
+ }
563
+ expectBytes(data, offset, marker, "multipart boundary");
564
+ offset += marker.length;
565
+ }
566
+ if (results.length !== requestedPaths.length) {
567
+ throw new Error(`read-multiple returned ${results.length} of ${requestedPaths.length} results`);
568
+ }
569
+ return results;
570
+ }
571
+ function multipartBoundary(contentType) {
572
+ if (!/^multipart\/mixed(?:\s*;|$)/i.test(contentType ?? "")) {
573
+ throw new Error("read-multiple response is not multipart/mixed");
574
+ }
575
+ const match = /(?:^|;)\s*boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(contentType ?? "");
576
+ const boundary = match?.[1] ?? match?.[2];
577
+ if (!boundary || /[\r\n]/.test(boundary)) {
578
+ throw new Error("read-multiple response has no multipart boundary");
579
+ }
580
+ return boundary;
581
+ }
582
+ function parseHttpHeaders(value) {
583
+ const headers = new Map();
584
+ if (!value)
585
+ return headers;
586
+ for (const line of value.split("\r\n")) {
587
+ const separator = line.indexOf(":");
588
+ if (separator <= 0)
589
+ throw new Error(`Invalid multipart header: ${line}`);
590
+ const name = line.slice(0, separator).trim().toLowerCase();
591
+ const headerValue = line.slice(separator + 1).trim();
592
+ if (headers.has(name))
593
+ throw new Error(`Duplicate multipart header: ${name}`);
594
+ headers.set(name, headerValue);
595
+ }
596
+ return headers;
597
+ }
598
+ function findBytes(data, needle, offset) {
599
+ for (let index = offset; index <= data.length - needle.length; index++) {
600
+ let matches = true;
601
+ for (let needleIndex = 0; needleIndex < needle.length; needleIndex++) {
602
+ if (data[index + needleIndex] !== needle[needleIndex]) {
603
+ matches = false;
604
+ break;
605
+ }
606
+ }
607
+ if (matches)
608
+ return index;
609
+ }
610
+ return -1;
611
+ }
612
+ function expectBytes(data, offset, expected, description) {
613
+ for (let index = 0; index < expected.length; index++) {
614
+ if (data[offset + index] !== expected[index]) {
615
+ throw new Error(`Invalid ${description}`);
616
+ }
617
+ }
618
+ }
619
+ function parsePropfind(xml, machineId) {
620
+ return xmlElements(xml, "response").flatMap((responseXml) => {
621
+ const info = fileInfoFromResponse(responseXml, machineId);
622
+ return info ? [info] : [];
623
+ });
624
+ }
625
+ function parseStorageUsage(xml) {
626
+ const response = xmlElements(xml, "response")[0];
627
+ if (response === undefined) {
628
+ throw new Error("Storage PROPFIND returned no DAV:response");
629
+ }
630
+ return {
631
+ usedBytes: nonnegativeXmlInteger(response, "quota-used-bytes"),
632
+ availableBytes: nonnegativeXmlInteger(response, "quota-available-bytes"),
633
+ };
634
+ }
635
+ function nonnegativeXmlInteger(xml, name) {
636
+ const raw = requiredXmlText(xml, name);
637
+ const value = Number(raw);
638
+ if (!Number.isSafeInteger(value) || value < 0) {
639
+ throw new Error(`PROPFIND returned invalid DAV:${name}: ${raw}`);
640
+ }
641
+ return value;
642
+ }
643
+ function parseSyncCollection(xml, machineId) {
644
+ const token = requiredXmlText(xml, "sync-token");
645
+ const updates = [];
646
+ for (const responseXml of xmlElements(xml, "response")) {
647
+ const href = requiredXmlText(responseXml, "href");
648
+ const path = machinePathFromHref(href, machineId);
649
+ if (!path)
650
+ continue;
651
+ const hasProperties = hasXmlElement(responseXml, "propstat");
652
+ const deleted = !hasProperties && /HTTP\/1\.1\s+404\s+Not Found/i.test(responseXml);
653
+ if (deleted) {
654
+ updates.push({ path, info: null });
655
+ continue;
656
+ }
657
+ const info = fileInfoFromResponse(responseXml, machineId);
658
+ if (!info)
659
+ throw new Error(`sync-collection omitted metadata for ${path}`);
660
+ updates.push({ path, info });
661
+ }
662
+ return { token, updates };
663
+ }
664
+ function fileInfoFromResponse(responseXml, machineId) {
665
+ const href = requiredXmlText(responseXml, "href");
666
+ const path = machinePathFromHref(href, machineId);
667
+ if (!path)
668
+ return null;
669
+ const base = {
670
+ path,
671
+ name: requiredXmlText(responseXml, "displayname"),
672
+ etag: requiredXmlText(responseXml, "getetag"),
673
+ createdAt: requiredXmlText(responseXml, "creationdate"),
674
+ lastModified: requiredXmlText(responseXml, "getlastmodified"),
675
+ ownerId: optionalXmlText(responseXml, "owner-id"),
676
+ access: fileAccessFromResponse(responseXml),
677
+ };
678
+ const resourceType = requiredXmlElement(responseXml, "resourcetype");
679
+ if (hasXmlElement(resourceType, "collection")) {
680
+ return { ...base, kind: "directory" };
681
+ }
682
+ const rawSize = requiredXmlText(responseXml, "getcontentlength");
683
+ const size = Number(rawSize);
684
+ if (!Number.isSafeInteger(size) || size < 0) {
685
+ throw new Error(`PROPFIND returned invalid size ${rawSize} for ${path}`);
686
+ }
687
+ return {
688
+ ...base,
689
+ kind: "file",
690
+ size,
691
+ contentType: requiredXmlText(responseXml, "getcontenttype"),
692
+ };
693
+ }
694
+ const ACCESS_AUDIENCES = [
695
+ "resource-owner",
696
+ "machine-admins",
697
+ "machine-editors",
698
+ "machine-members",
699
+ ];
700
+ function fileAccessFromResponse(responseXml) {
701
+ const accessXml = requiredXmlElement(responseXml, "access");
702
+ const currentUserXml = requiredXmlElement(accessXml, "current-user");
703
+ const readableByXml = requiredXmlElement(accessXml, "readable-by");
704
+ const writableByXml = requiredXmlElement(accessXml, "writable-by");
705
+ return {
706
+ currentUser: {
707
+ read: hasXmlElement(currentUserXml, "read"),
708
+ write: hasXmlElement(currentUserXml, "write"),
709
+ },
710
+ readableBy: ACCESS_AUDIENCES.filter((audience) => hasXmlElement(readableByXml, audience)),
711
+ writableBy: ACCESS_AUDIENCES.filter((audience) => hasXmlElement(writableByXml, audience)),
712
+ };
713
+ }
714
+ function machinePathFromHref(href, machineId) {
715
+ const url = new URL(href, "http://rool.local");
716
+ const encodedParts = url.pathname.split("/");
717
+ if (encodedParts.shift() !== "") {
718
+ throw new Error(`PROPFIND returned invalid href: ${href}`);
719
+ }
720
+ if (encodedParts.at(-1) === "")
721
+ encodedParts.pop();
722
+ const parts = encodedParts.map(decodeURIComponent);
723
+ const isMachineHref = parts[0] === "v2" &&
724
+ parts[1] === "machines" &&
725
+ parts[2] === machineId &&
726
+ parts[3] === "dav";
727
+ if (!isMachineHref) {
728
+ throw new Error(`PROPFIND returned an href outside the machine: ${href}`);
729
+ }
730
+ const pathParts = parts.slice(4);
731
+ if (pathParts.length === 0)
732
+ return null;
733
+ if (pathParts[0] !== "space" && pathParts[0] !== "rool-drive") {
734
+ throw new Error(`PROPFIND returned an unsupported machine path: ${href}`);
735
+ }
736
+ for (const part of pathParts) {
737
+ if (!part || part === "." || part === ".." || part.includes("/")) {
738
+ throw new Error(`PROPFIND returned an invalid machine path: ${href}`);
739
+ }
740
+ }
741
+ return `/${pathParts.join("/")}`;
742
+ }
743
+ function xmlElements(xml, name) {
744
+ const pattern = new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?${name}\\b[^>]*>([\\s\\S]*?)<\\/(?:[A-Za-z_][\\w.-]*:)?${name}>`, "gi");
745
+ return [...xml.matchAll(pattern)].map((match) => match[1]);
746
+ }
747
+ function requiredXmlElement(xml, name) {
748
+ const value = xmlElements(xml, name)[0];
749
+ if (value !== undefined)
750
+ return value;
751
+ const selfClosing = new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?${name}\\b[^>]*/\\s*>`, "i").test(xml);
752
+ if (selfClosing)
753
+ return "";
754
+ throw new Error(`PROPFIND response is missing DAV:${name}`);
755
+ }
756
+ function requiredXmlText(xml, name) {
757
+ const value = decodeXml(requiredXmlElement(xml, name).replace(/<[^>]*>/g, ""));
758
+ if (!value)
759
+ throw new Error(`PROPFIND response has empty DAV:${name}`);
760
+ return value;
761
+ }
762
+ function optionalXmlText(xml, name) {
763
+ const element = xmlElements(xml, name)[0];
764
+ if (element === undefined)
765
+ return undefined;
766
+ return decodeXml(element.replace(/<[^>]*>/g, "")) || undefined;
767
+ }
768
+ function hasXmlElement(xml, name) {
769
+ return new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?${name}(?:\\s[^>]*)?\\s*\\/?\\s*>`, "i").test(xml);
770
+ }
771
+ function encodeXml(value) {
772
+ return value
773
+ .replace(/&/g, "&amp;")
774
+ .replace(/</g, "&lt;")
775
+ .replace(/>/g, "&gt;")
776
+ .replace(/"/g, "&quot;")
777
+ .replace(/'/g, "&apos;");
778
+ }
779
+ function decodeXml(value) {
780
+ return value
781
+ .trim()
782
+ .replace(/&#x([0-9a-f]+);/gi, (_entity, digits) => String.fromCodePoint(Number.parseInt(digits, 16)))
783
+ .replace(/&#([0-9]+);/g, (_entity, digits) => String.fromCodePoint(Number.parseInt(digits, 10)))
784
+ .replace(/&quot;/g, '"')
785
+ .replace(/&apos;/g, "'")
786
+ .replace(/&lt;/g, "<")
787
+ .replace(/&gt;/g, ">")
788
+ .replace(/&amp;/g, "&");
789
+ }
790
+ function cloneFileInfo(info) {
791
+ return {
792
+ ...info,
793
+ access: {
794
+ currentUser: { ...info.access.currentUser },
795
+ readableBy: [...info.access.readableBy],
796
+ writableBy: [...info.access.writableBy],
797
+ },
798
+ };
799
+ }
800
+ function sameFileInfo(current, next) {
801
+ if (!current || current.kind !== next.kind)
802
+ return false;
803
+ if (current.etag !== next.etag ||
804
+ current.name !== next.name ||
805
+ current.createdAt !== next.createdAt ||
806
+ current.lastModified !== next.lastModified ||
807
+ current.ownerId !== next.ownerId ||
808
+ current.access.currentUser.read !== next.access.currentUser.read ||
809
+ current.access.currentUser.write !== next.access.currentUser.write ||
810
+ current.access.readableBy.join("\0") !==
811
+ next.access.readableBy.join("\0") ||
812
+ current.access.writableBy.join("\0") !== next.access.writableBy.join("\0")) {
813
+ return false;
814
+ }
815
+ if (current.kind === "directory" || next.kind === "directory")
816
+ return true;
817
+ return current.size === next.size && current.contentType === next.contentType;
818
+ }
819
+ function parentPath(path) {
820
+ const separator = path.lastIndexOf("/");
821
+ if (separator === 0)
822
+ return undefined;
823
+ return path.slice(0, separator);
824
+ }
825
+ async function abortableDelay(milliseconds, signal) {
826
+ if (signal.aborted)
827
+ return;
828
+ await new Promise((resolve) => {
829
+ const timer = setTimeout(finish, milliseconds);
830
+ function finish() {
831
+ clearTimeout(timer);
832
+ signal.removeEventListener("abort", finish);
833
+ resolve();
834
+ }
835
+ signal.addEventListener("abort", finish, { once: true });
836
+ });
837
+ }
838
+ async function assertStatus(response, ...statuses) {
839
+ if (statuses.includes(response.status))
840
+ return;
841
+ throw await fileError(response);
842
+ }
843
+ async function fileError(response) {
844
+ return new RoolFileError(response, await response.text());
845
+ }
846
+ //# sourceMappingURL=files.js.map