sliftutils 1.7.10 → 1.7.12

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 (84) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/examplestorage/exampleserver.ts +0 -2
  3. package/index.d.ts +301 -54
  4. package/misc/dist/environment.ts.cache +3 -3
  5. package/misc/dist/getSecret.ts.cache +139 -0
  6. package/misc/dist/strings.ts.cache +13 -0
  7. package/misc/dist/zip.ts.cache +9 -0
  8. package/misc/getSecret.ts +38 -16
  9. package/misc/https/cloudflareHelpers.d.ts +19 -0
  10. package/misc/https/cloudflareHelpers.ts +69 -0
  11. package/misc/https/dist/certs.ts.cache +115 -179
  12. package/misc/https/dist/cloudflareHelpers.ts.cache +68 -0
  13. package/misc/https/dist/dns.ts.cache +112 -100
  14. package/misc/https/dist/hostServer.ts.cache +130 -0
  15. package/misc/https/dist/httpsCerts.ts.cache +24 -5
  16. package/misc/https/dist/persistentLocalStorage.ts.cache +3 -2
  17. package/misc/https/dns.d.ts +41 -12
  18. package/misc/https/dns.ts +104 -101
  19. package/misc/https/hostServer.d.ts +0 -6
  20. package/misc/https/hostServer.ts +9 -5
  21. package/package.json +2 -2
  22. package/render-utils/dist/observer.tsx.cache +3 -2
  23. package/spec.txt +0 -90
  24. package/storage/ArchivesDisk.d.ts +2 -0
  25. package/storage/ArchivesDisk.ts +7 -3
  26. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +1242 -0
  27. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +513 -0
  28. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +352 -0
  29. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +306 -0
  30. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +435 -0
  31. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +58 -0
  32. package/storage/BulkDatabase2/dist/blockCache.ts.cache +179 -0
  33. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +149 -0
  34. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +101 -0
  35. package/storage/BulkDatabase2/dist/streamLog.ts.cache +227 -0
  36. package/storage/BulkDatabase2/dist/syncClient.ts.cache +104 -0
  37. package/storage/IArchives.d.ts +47 -15
  38. package/storage/IArchives.ts +86 -31
  39. package/storage/backblaze.d.ts +15 -0
  40. package/storage/backblaze.ts +199 -79
  41. package/storage/dist/ArchivesDisk.ts.cache +373 -0
  42. package/storage/dist/FileFolderAPI.tsx.cache +508 -65
  43. package/storage/dist/IArchives.ts.cache +44 -0
  44. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +8 -2
  45. package/storage/dist/JSONStorage.ts.cache +6 -2
  46. package/storage/dist/PendingManager.tsx.cache +1 -1
  47. package/storage/dist/TransactionStorage.ts.cache +231 -92
  48. package/storage/dist/backblaze.ts.cache +797 -0
  49. package/storage/dist/fileSystemPointer.ts.cache +36 -3
  50. package/storage/dist/remoteFileStorage.ts.cache +495 -0
  51. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -1
  52. package/storage/remoteStorage/ArchivesRemote.ts +28 -8
  53. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -0
  54. package/storage/remoteStorage/ArchivesUrl.ts +16 -4
  55. package/storage/remoteStorage/blobStore.d.ts +63 -3
  56. package/storage/remoteStorage/blobStore.ts +419 -101
  57. package/storage/remoteStorage/createArchives.d.ts +31 -10
  58. package/storage/remoteStorage/createArchives.ts +470 -253
  59. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +197 -0
  60. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +85 -0
  61. package/storage/remoteStorage/dist/accessPage.tsx.cache +222 -0
  62. package/storage/remoteStorage/dist/blobStore.ts.cache +837 -0
  63. package/storage/remoteStorage/dist/cliArgs.ts.cache +16 -0
  64. package/storage/remoteStorage/dist/createArchives.ts.cache +610 -0
  65. package/storage/remoteStorage/dist/remoteConfig.ts.cache +206 -0
  66. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +223 -0
  67. package/storage/remoteStorage/dist/storageController.ts.cache +500 -0
  68. package/storage/remoteStorage/dist/storageServer.ts.cache +106 -0
  69. package/storage/remoteStorage/dist/storageServerCli.ts.cache +39 -0
  70. package/storage/remoteStorage/dist/storageServerState.ts.cache +518 -0
  71. package/storage/remoteStorage/remoteConfig.d.ts +8 -0
  72. package/storage/remoteStorage/remoteConfig.ts +105 -5
  73. package/storage/remoteStorage/sourceWrapper.d.ts +41 -0
  74. package/storage/remoteStorage/sourceWrapper.ts +223 -0
  75. package/storage/remoteStorage/spec.md +31 -0
  76. package/storage/remoteStorage/storageController.d.ts +11 -0
  77. package/storage/remoteStorage/storageController.ts +109 -40
  78. package/storage/remoteStorage/storageServer.d.ts +0 -5
  79. package/storage/remoteStorage/storageServer.ts +17 -10
  80. package/storage/remoteStorage/storageServerCli.ts +0 -3
  81. package/storage/remoteStorage/storageServerState.d.ts +7 -1
  82. package/storage/remoteStorage/storageServerState.ts +230 -48
  83. package/testsite/server.ts +0 -2
  84. package/yarn.lock +11 -6
@@ -1,13 +1,14 @@
1
1
  module.allowclient = true;
2
2
 
3
3
  import { SocketFunction } from "socket-function/SocketFunction";
4
+ import { SocketFunctionHook } from "socket-function/SocketFunctionTypes";
4
5
  import { getNodeIdIP } from "socket-function/src/nodeCache";
5
6
  import { setHTTPResultHeaders, getCurrentHTTPRequest } from "socket-function/src/callHTTPHandler";
6
7
  import { performLocalCall } from "socket-function/src/callManager";
7
8
  import { RequireController } from "socket-function/require/RequireController";
8
9
  import { timeInMinute } from "socket-function/src/misc";
9
10
  import { getCommonName, getPublicIdentifier, getOwnMachineId, verify, verifyMachineIdForPublicKey } from "../../misc/https/certs";
10
- import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus } from "../IArchives";
11
+ import { ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, IMMUTABLE_CACHE_TIME } from "../IArchives";
11
12
  import { ROUTING_FILE } from "./remoteConfig";
12
13
  import {
13
14
  getStorageServerConfig, getTrust, getRequests, getLoadedBucket, writeBucketFile,
@@ -143,6 +144,9 @@ async function getBucket(account: string, bucketName: string): Promise<LoadedBuc
143
144
  }
144
145
 
145
146
  class RemoteStorageControllerBase {
147
+ // Latency measurement (see SourceWrapper's pinging); no auth, it measures the transport
148
+ async ping(): Promise<void> { }
149
+
146
150
  // Proves the caller owns the machine key for the machineId in its certificate. The signature
147
151
  // must be fresh and bound to this server, so it cannot be replayed to (or from) other servers.
148
152
  async authenticate(token: AuthToken): Promise<{ machineId: string; ip: string }> {
@@ -226,7 +230,6 @@ class RemoteStorageControllerBase {
226
230
  // volunteers a list of requesting IPs, so a trusted user can't accidentally approve a random
227
231
  // request from a machine they didn't mean to trust.
228
232
  async listRequestsForIP(account: string, ip: string): Promise<AccessRequest[]> {
229
- await requireAccess(account);
230
233
  let requests = await getRequests();
231
234
  return (await requests.get(ip) || []).filter(x => x.account === account);
232
235
  }
@@ -262,12 +265,10 @@ class RemoteStorageControllerBase {
262
265
  // Only returns requests for the given IP, so you cannot accidentally grant a request from an
263
266
  // IP you didn't explicitly type in.
264
267
  async adminListRequests(ip: string): Promise<AccessRequest[]> {
265
- requireAdmin();
266
268
  let requests = await getRequests();
267
269
  return await requests.get(ip) || [];
268
270
  }
269
271
  async adminGrantAccess(requestId: string): Promise<TrustRecord> {
270
- requireAdmin();
271
272
  let trust = await getTrust();
272
273
  let requests = await getRequests();
273
274
  for (let ip of await requests.getKeys()) {
@@ -290,15 +291,13 @@ class RemoteStorageControllerBase {
290
291
  let result = await this.get2(account, bucketName, path, range);
291
292
  return result && result.data || undefined;
292
293
  }
293
- async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number } | undefined> {
294
- await requireAccess(account);
294
+ async get2(account: string, bucketName: string, path: string, range?: { start: number; end: number }): Promise<{ data: Buffer; writeTime: number; size: number } | undefined> {
295
295
  assertValidPath(path);
296
296
  let bucket = await getBucket(account, bucketName);
297
297
  if (!bucket) return undefined;
298
298
  return await bucket.store.get2(path, { range });
299
299
  }
300
300
  async set(account: string, bucketName: string, path: string, data: Buffer, lastModified?: number): Promise<void> {
301
- await requireAccess(account);
302
301
  assertValidName(bucketName, "bucket name");
303
302
  assertValidPath(path);
304
303
  // Handles bucket creation (writes of ROUTING_FILE), reconfiguration, fast mode, and
@@ -306,27 +305,23 @@ class RemoteStorageControllerBase {
306
305
  await writeBucketFile(account, bucketName, path, Buffer.from(data), { lastModified });
307
306
  }
308
307
  async del(account: string, bucketName: string, path: string): Promise<void> {
309
- await requireAccess(account);
310
308
  assertValidName(bucketName, "bucket name");
311
309
  assertValidPath(path);
312
310
  await deleteBucketFile(account, bucketName, path);
313
311
  }
314
312
  async getInfo(account: string, bucketName: string, path: string): Promise<{ writeTime: number; size: number } | undefined> {
315
- await requireAccess(account);
316
313
  assertValidPath(path);
317
314
  let bucket = await getBucket(account, bucketName);
318
315
  if (!bucket) return undefined;
319
316
  return await bucket.store.getInfo(path);
320
317
  }
321
318
  async findInfo(account: string, bucketName: string, prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]> {
322
- await requireAccess(account);
323
319
  let bucket = await getBucket(account, bucketName);
324
320
  if (!bucket) return [];
325
321
  return await bucket.store.findInfo(prefix, config);
326
322
  }
327
323
  // Fast (served from the store's BulkDatabase2 index, not a scan) — see IArchives.getChangesAfter
328
324
  async getChangesAfter(account: string, bucketName: string, time: number): Promise<ArchiveFileInfo[]> {
329
- await requireAccess(account);
330
325
  let bucket = await getBucket(account, bucketName);
331
326
  if (!bucket) return [];
332
327
  if (!bucket.store.getChangesAfter) {
@@ -335,13 +330,26 @@ class RemoteStorageControllerBase {
335
330
  return await bucket.store.getChangesAfter(time);
336
331
  }
337
332
  async getArchivesConfig(account: string, bucketName: string): Promise<ArchivesConfig> {
338
- await requireAccess(account);
339
333
  let bucket = await getBucket(account, bucketName);
340
334
  // Missing buckets say true, matching what they become once created (the default store type)
341
- return { supportsChangesAfter: !bucket || !!bucket.store.getChangesAfter };
335
+ let progress = bucket?.store.getSyncProgress?.();
336
+ return {
337
+ supportsChangesAfter: !bucket || !!bucket.store.getChangesAfter,
338
+ remoteConfig: bucket?.routing,
339
+ index: progress?.index,
340
+ indexSources: progress?.sources,
341
+ readerDiskLimit: progress?.readerDiskLimit,
342
+ syncing: progress?.syncing,
343
+ };
344
+ }
345
+ /** Walks the whole index for exact totals (overall and per holding source) - more expensive
346
+ * than the maintained counters that getArchivesConfig returns, but immune to counter drift. */
347
+ async getIndexInfo(account: string, bucketName: string): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] } | undefined> {
348
+ let bucket = await getBucket(account, bucketName);
349
+ if (!bucket || !bucket.store.computeIndexTotals) return undefined;
350
+ return await bucket.store.computeIndexTotals();
342
351
  }
343
352
  async getSyncStatus(account: string, bucketName: string): Promise<ArchivesSyncStatus> {
344
- await requireAccess(account);
345
353
  let bucket = await getBucket(account, bucketName);
346
354
  if (!bucket) return { allScansComplete: true, indexSize: 0, sources: [] };
347
355
  if (!bucket.store.getSyncStatus) {
@@ -352,14 +360,13 @@ class RemoteStorageControllerBase {
352
360
 
353
361
  async startLargeFile(account: string, bucketName: string, path: string): Promise<string> {
354
362
  assertWritesAllowed();
355
- await requireAccess(account);
356
363
  // Validates now, so the upload doesn't fail at the end
357
364
  assertValidPath(path);
358
365
  let bucket = await getBucket(account, bucketName);
359
366
  if (!bucket) {
360
367
  throw new Error(`Bucket ${account}/${bucketName} does not exist. Write its routing config to ${JSON.stringify(ROUTING_FILE)} to create it.`);
361
368
  }
362
- await assertMutable(bucket, path);
369
+ await assertMutable(bucket, path, Date.now());
363
370
  let id = await bucket.store.startLargeUpload();
364
371
  largeUploadInfo.set(id, { account, bucketName, path });
365
372
  return id;
@@ -368,7 +375,6 @@ class RemoteStorageControllerBase {
368
375
  assertWritesAllowed();
369
376
  let info = largeUploadInfo.get(uploadId);
370
377
  if (!info) throw new Error(`Unknown large upload ${uploadId}`);
371
- await requireAccess(info.account);
372
378
  let bucket = await getBucket(info.account, info.bucketName);
373
379
  if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
374
380
  await bucket.store.appendLargeUpload(uploadId, Buffer.from(data));
@@ -377,7 +383,6 @@ class RemoteStorageControllerBase {
377
383
  assertWritesAllowed();
378
384
  let info = largeUploadInfo.get(uploadId);
379
385
  if (!info) throw new Error(`Unknown large upload ${uploadId}`);
380
- await requireAccess(info.account);
381
386
  largeUploadInfo.delete(uploadId);
382
387
  let bucket = await getBucket(info.account, info.bucketName);
383
388
  if (!bucket) throw new Error(`Bucket ${info.account}/${info.bucketName} no longer exists`);
@@ -386,7 +391,6 @@ class RemoteStorageControllerBase {
386
391
  async cancelLargeFile(uploadId: string): Promise<void> {
387
392
  let info = largeUploadInfo.get(uploadId);
388
393
  if (!info) return;
389
- await requireAccess(info.account);
390
394
  largeUploadInfo.delete(uploadId);
391
395
  let bucket = await getBucket(info.account, info.bucketName);
392
396
  if (!bucket) return;
@@ -424,44 +428,109 @@ class RemoteStorageControllerBase {
424
428
  if (!bucket.self?.public) {
425
429
  throw new Error(`Bucket ${account}/${bucketName} is not public, so its files cannot be read over plain URLs`);
426
430
  }
427
- let result = await bucket.store.get2(filePath);
428
- if (!result) {
431
+ // The index answers existence + write time + size without touching any data, so
432
+ // If-Modified-Since and range validation cost nothing
433
+ let info = await bucket.store.getInfo(filePath);
434
+ if (!info || !info.size) {
429
435
  return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
430
436
  }
431
437
  let ext = filePath.split(".").pop() || "";
432
- return setHTTPResultHeaders(result.data, {
438
+ let headers: { [header: string]: string } = {
433
439
  "Content-Type": CONTENT_TYPES[ext.toLowerCase()] || "application/octet-stream",
434
- "Last-Modified": new Date(result.writeTime).toUTCString(),
435
- });
440
+ "Last-Modified": new Date(info.writeTime).toUTCString(),
441
+ "Accept-Ranges": "bytes",
442
+ };
443
+ if (bucket.self?.immutable) {
444
+ headers["Cache-Control"] = `max-age=${IMMUTABLE_CACHE_TIME / 1000}`;
445
+ }
446
+ let ifModifiedSince = request?.headers["if-modified-since"];
447
+ if (typeof ifModifiedSince === "string") {
448
+ let since = new Date(ifModifiedSince).getTime();
449
+ // Last-Modified is served at 1 second resolution, so compare at that resolution
450
+ if (since && Math.floor(info.writeTime / 1000) * 1000 <= since) {
451
+ return setHTTPResultHeaders(Buffer.from(""), { ...headers, status: "304" });
452
+ }
453
+ }
454
+ let range: { start: number; end: number } | undefined;
455
+ let rangeHeader = request?.headers["range"];
456
+ if (typeof rangeHeader === "string") {
457
+ // Single-range form only (bytes=start-end / start- / -suffix); anything else serves the full file
458
+ let match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
459
+ if (match && (match[1] || match[2])) {
460
+ let start: number;
461
+ let endInclusive = info.size - 1;
462
+ if (!match[1]) {
463
+ start = Math.max(0, info.size - +match[2]);
464
+ } else {
465
+ start = +match[1];
466
+ if (match[2]) {
467
+ endInclusive = Math.min(+match[2], info.size - 1);
468
+ }
469
+ }
470
+ if (start >= info.size || start > endInclusive) {
471
+ return setHTTPResultHeaders(Buffer.from(""), { ...headers, "Content-Range": `bytes */${info.size}`, status: "416" });
472
+ }
473
+ range = { start, end: endInclusive + 1 };
474
+ }
475
+ }
476
+ let result = await bucket.store.get2(filePath, { range });
477
+ if (!result) {
478
+ return setHTTPResultHeaders(Buffer.from(""), { status: "404" });
479
+ }
480
+ if (range) {
481
+ return setHTTPResultHeaders(result.data, { ...headers, "Content-Range": `bytes ${range.start}-${range.end - 1}/${info.size}`, status: "206" });
482
+ }
483
+ return setHTTPResultHeaders(result.data, headers);
436
484
  }
437
485
  }
438
486
 
439
487
  const largeUploadInfo = new Map<string, { account: string; bucketName: string; path: string }>();
440
488
 
489
+ // Access checks run as hooks on the register shape below, keyed off the call's arguments, so the
490
+ // method bodies don't each repeat them
491
+ const accountAccess: SocketFunctionHook = async (context) => {
492
+ await requireAccess(String(context.call.args[0]));
493
+ };
494
+ const uploadAccess: SocketFunctionHook = async (context) => {
495
+ let info = largeUploadInfo.get(String(context.call.args[0]));
496
+ // Unknown upload ids are handled by the methods themselves (throw / no-op)
497
+ if (!info) return;
498
+ await requireAccess(info.account);
499
+ };
500
+ const adminAccess: SocketFunctionHook = async () => {
501
+ requireAdmin();
502
+ };
503
+
441
504
  export const RemoteStorageController = SocketFunction.register(
442
505
  REMOTE_STORAGE_CLASS_GUID,
443
506
  new RemoteStorageControllerBase(),
444
507
  () => ({
508
+ ping: {},
445
509
  authenticate: {},
446
510
  requestAccess: {},
447
511
  getAccessState: {},
448
- listRequestsForIP: {},
512
+ listRequestsForIP: { hooks: [accountAccess] },
449
513
  grantAccess: {},
450
- adminListRequests: {},
451
- adminGrantAccess: {},
452
- get: {},
453
- get2: {},
454
- set: {},
455
- del: {},
456
- getInfo: {},
457
- findInfo: {},
458
- getChangesAfter: {},
459
- getArchivesConfig: {},
460
- getSyncStatus: {},
461
- startLargeFile: {},
462
- uploadPart: {},
463
- finishLargeFile: {},
464
- cancelLargeFile: {},
514
+ adminListRequests: { hooks: [adminAccess] },
515
+ adminGrantAccess: { hooks: [adminAccess] },
516
+ get: { hooks: [accountAccess] },
517
+ get2: { hooks: [accountAccess] },
518
+ set: { hooks: [accountAccess] },
519
+ del: { hooks: [accountAccess] },
520
+ getInfo: { hooks: [accountAccess] },
521
+ findInfo: { hooks: [accountAccess] },
522
+ getChangesAfter: { hooks: [accountAccess] },
523
+ getArchivesConfig: { hooks: [accountAccess] },
524
+ getIndexInfo: { hooks: [accountAccess] },
525
+ getSyncStatus: { hooks: [accountAccess] },
526
+ startLargeFile: { hooks: [accountAccess] },
527
+ uploadPart: { hooks: [uploadAccess] },
528
+ finishLargeFile: { hooks: [uploadAccess] },
529
+ cancelLargeFile: { hooks: [uploadAccess] },
465
530
  httpEntry: {},
531
+ }),
532
+ () => ({
533
+ noClientHooks: true,
534
+ noDefaultHooks: true,
466
535
  })
467
536
  );
@@ -2,11 +2,6 @@ import "./accessPage";
2
2
  export type HostStorageServerConfig = {
3
3
  url: string;
4
4
  folder: string;
5
- cloudflareApiToken?: {
6
- key: string;
7
- } | {
8
- path: string;
9
- };
10
5
  lowSpaceThresholdBytes?: number;
11
6
  };
12
7
  export declare function hostStorageServer(config: HostStorageServerConfig): Promise<void>;
@@ -5,7 +5,6 @@ import { SocketFunction } from "socket-function/SocketFunction";
5
5
  import { getExternalIP } from "socket-function/src/networking";
6
6
  import { RequireController } from "socket-function/require/RequireController";
7
7
  import { hostServer } from "../../misc/https/hostServer";
8
- import { getSecret } from "../../misc/getSecret";
9
8
  import { RemoteStorageController } from "./storageController";
10
9
  import { setStorageServerConfig, setWritesRejectedReason } from "./storageServerState";
11
10
  import { parseStorageUrl } from "./ArchivesRemote";
@@ -23,14 +22,13 @@ const HARD_REJECT_FRACTION = 0.1;
23
22
  // grantAccess.js bootstrap (next to this file) is what the access page's shown SSH command points at.
24
23
 
25
24
  export type HostStorageServerConfig = {
26
- // Full URL of this storage server, e.g. "https://storage.example.com:4444". The domain and
27
- // port are extracted from it (bucket routing URLs clients use look like
28
- // https://storage.example.com:4444/file/<account>/<bucketName>/storage/storagerouting.json).
25
+ // Full URL of this storage server, e.g. "https://1-2-3-4.example.com:4444". The subdomain must
26
+ // be an ip domain - this machine's external IP with dashes, or 127-0-0-1 for local testing
27
+ // (see the validation in hostStorageServer). The domain and port are extracted from it (bucket
28
+ // routing URLs clients use look like
29
+ // https://1-2-3-4.example.com:4444/file/<account>/<bucketName>/storage/storagerouting.json).
29
30
  url: string;
30
31
  folder: string;
31
- // Set hostServer.ts:HostServerConfig:cloudflareApiToken. Defaults to getSecret("cloudflare.json")
32
- // (~/cloudflare.json, { key: string }).
33
- cloudflareApiToken?: { key: string } | { path: string };
34
32
  // When free space on the folder's drive drops below this many bytes, the server console.errors
35
33
  // every 15 minutes. Below 10% of it, the server also rejects write operations (creating files,
36
34
  // large uploads, new buckets) — reads, findInfo, and deletes still work so the user can free
@@ -79,12 +77,22 @@ function getGrantAccessCliPath(): string {
79
77
  export async function hostStorageServer(config: HostStorageServerConfig): Promise<void> {
80
78
  let { url, folder } = config;
81
79
  let { address: domain, port } = parseStorageUrl(url);
80
+ let rootDomain = domain.split(".").slice(-2).join(".");
81
+ let externalIP = (await getExternalIP()).trim();
82
+ // The subdomain must be an ip domain: the domain's A record points at exactly one machine, so a
83
+ // dynamic name would let the same script run on two servers and silently fight over it. Encoding
84
+ // the IP makes that mistake fail loudly - the wrong machine's domain just doesn't match.
85
+ let allowedDomains = [`127-0-0-1.${rootDomain}`, `${externalIP.replaceAll(".", "-")}.${rootDomain}`];
86
+ if (!allowedDomains.includes(domain)) {
87
+ throw new Error(`The storage server domain is based on the machine's IP (the subdomain is the IP with dots replaced by dashes). Expected ${allowedDomains.join(" or ")}, was ${domain}. Your external IP is ${externalIP}.`);
88
+ }
89
+ await fsp.mkdir(folder, { recursive: true });
82
90
  let lowSpaceThreshold = config.lowSpaceThresholdBytes ?? DEFAULT_LOW_SPACE_THRESHOLD_BYTES;
83
91
  setStorageServerConfig({
84
92
  domain,
85
93
  port,
86
- rootDomain: domain.split(".").slice(-2).join("."),
87
- sshTarget: `${os.userInfo().username}@${await getExternalIP()}`,
94
+ rootDomain,
95
+ sshTarget: `${os.userInfo().username}@${externalIP}`,
88
96
  serverCommand: `node ${getGrantAccessCliPath()} --url ${url}`,
89
97
  folder: path.resolve(folder),
90
98
  });
@@ -113,7 +121,6 @@ export async function hostStorageServer(config: HostStorageServerConfig): Promis
113
121
  await hostServer({
114
122
  domain,
115
123
  port,
116
- cloudflareApiToken: config.cloudflareApiToken || { key: await getSecret("cloudflare.json.key") },
117
124
  setDNSRecord: true,
118
125
  });
119
126
  }
@@ -16,8 +16,6 @@ async function main() {
16
16
  if (!url) throw new Error(`--url is required (ex: --url https://storage.example.com:4444)`);
17
17
  let folder = getArg("folder");
18
18
  if (!folder) throw new Error(`--folder is required (the folder all data is stored in)`);
19
- // Optional: hostStorageServer falls back to ~/cloudflare.json
20
- let cloudflareApiToken = getArg("cloudflareApiToken");
21
19
  let lowSpaceThresholdBytes: number | undefined;
22
20
  let lowSpaceThreshold = getArg("lowSpaceThreshold");
23
21
  if (lowSpaceThreshold) {
@@ -30,7 +28,6 @@ async function main() {
30
28
  await hostStorageServer({
31
29
  url,
32
30
  folder,
33
- cloudflareApiToken: cloudflareApiToken && { path: cloudflareApiToken } || undefined,
34
31
  lowSpaceThresholdBytes,
35
32
  });
36
33
  }
@@ -20,16 +20,22 @@ export declare function getWritesRejectedReason(): string | undefined;
20
20
  export declare function assertWritesAllowed(): void;
21
21
  export declare function getTrust(): Promise<IStorage<TrustRecord>>;
22
22
  export declare function getRequests(): Promise<IStorage<AccessRequest[]>>;
23
+ /** Makes machineIds the complete trust list for the account: machines not in the list lose access, machines already trusted keep their existing record, and missing ones are added. */
24
+ export declare function setTrustedMachines(config: {
25
+ account: string;
26
+ machineIds: string[];
27
+ }): Promise<void>;
23
28
  export type LoadedBucket = {
24
29
  account: string;
25
30
  bucketName: string;
26
31
  routing: RemoteConfig;
27
32
  routingJSON: string;
33
+ selfEntries: HostedConfig[];
28
34
  self: HostedConfig | undefined;
29
35
  store: IBucketStore;
30
36
  };
31
37
  export declare function getLoadedBucket(account: string, bucketName: string): Promise<LoadedBucket | undefined>;
32
- export declare function assertMutable(bucket: LoadedBucket, filePath: string): Promise<void>;
38
+ export declare function assertMutable(bucket: LoadedBucket, filePath: string, writeTime: number): Promise<void>;
33
39
  export declare function writeBucketFile(account: string, bucketName: string, filePath: string, data: Buffer, config?: {
34
40
  lastModified?: number;
35
41
  }): Promise<void>;