sliftutils 1.7.87 → 1.7.89

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 (127) hide show
  1. package/.claude/settings.local.json +3 -1
  2. package/dist/treeSummary.ts.cache +2 -2
  3. package/index.d.ts +675 -296
  4. package/misc/dist/environment.ts.cache +2 -2
  5. package/misc/dist/getSecret.ts.cache +2 -2
  6. package/misc/dist/strings.ts.cache +2 -2
  7. package/misc/dist/zip.ts.cache +2 -2
  8. package/misc/https/dist/certs.ts.cache +2 -2
  9. package/misc/https/dist/cloudflareHelpers.ts.cache +2 -2
  10. package/misc/https/dist/dns.ts.cache +2 -2
  11. package/misc/https/dist/hostServer.ts.cache +49 -21
  12. package/misc/https/dist/hostsFile.ts.cache +82 -0
  13. package/misc/https/dist/httpsCerts.ts.cache +2 -2
  14. package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
  15. package/misc/https/hostServer.d.ts +6 -2
  16. package/misc/https/hostServer.ts +52 -19
  17. package/misc/https/hostsFile.d.ts +7 -0
  18. package/misc/https/hostsFile.ts +66 -0
  19. package/package.json +1 -1
  20. package/render-utils/dist/observer.tsx.cache +2 -2
  21. package/storage/ArchivesDisk.d.ts +3 -6
  22. package/storage/ArchivesDisk.ts +26 -2
  23. package/storage/BulkDatabase2/BulkDatabaseReader.ts +1 -1
  24. package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
  25. package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
  26. package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
  27. package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +3 -3
  28. package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
  29. package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
  30. package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
  31. package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
  32. package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
  33. package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
  34. package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
  35. package/storage/IArchives.d.ts +37 -24
  36. package/storage/IArchives.ts +47 -62
  37. package/storage/LogMap.d.ts +56 -0
  38. package/storage/LogMap.ts +220 -0
  39. package/storage/archiveHelpers.d.ts +36 -0
  40. package/storage/archiveHelpers.ts +103 -0
  41. package/storage/backblaze.d.ts +7 -12
  42. package/storage/backblaze.ts +53 -48
  43. package/storage/dist/ArchivesDisk.ts.cache +28 -2
  44. package/storage/dist/FileFolderAPI.tsx.cache +2 -2
  45. package/storage/dist/IArchives.ts.cache +16 -52
  46. package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
  47. package/storage/dist/JSONStorage.ts.cache +2 -2
  48. package/storage/dist/LogMap.ts.cache +212 -0
  49. package/storage/dist/PendingManager.tsx.cache +2 -2
  50. package/storage/dist/TransactionStorage.ts.cache +2 -2
  51. package/storage/dist/backblaze.ts.cache +58 -44
  52. package/storage/dist/fileSystemPointer.ts.cache +2 -2
  53. package/storage/dist/remoteFileStorage.ts.cache +2 -2
  54. package/storage/remoteStorage/ArchivesDelayed.d.ts +66 -0
  55. package/storage/remoteStorage/ArchivesDelayed.ts +237 -0
  56. package/storage/remoteStorage/ArchivesRemote.d.ts +5 -6
  57. package/storage/remoteStorage/ArchivesRemote.ts +26 -5
  58. package/storage/remoteStorage/ArchivesUrl.d.ts +2 -5
  59. package/storage/remoteStorage/ArchivesUrl.ts +2 -2
  60. package/storage/remoteStorage/accessPage.tsx +12 -0
  61. package/storage/remoteStorage/accessStats.d.ts +1 -1
  62. package/storage/remoteStorage/accessStats.ts +3 -3
  63. package/storage/remoteStorage/blobStore.d.ts +162 -189
  64. package/storage/remoteStorage/blobStore.ts +495 -952
  65. package/storage/remoteStorage/bucketDisk.d.ts +24 -3
  66. package/storage/remoteStorage/bucketDisk.ts +56 -22
  67. package/storage/remoteStorage/certTrustModal.d.ts +1 -0
  68. package/storage/remoteStorage/certTrustModal.tsx +48 -0
  69. package/storage/remoteStorage/cliArgs.d.ts +2 -0
  70. package/storage/remoteStorage/cliArgs.ts +5 -0
  71. package/storage/remoteStorage/createArchives.d.ts +8 -7
  72. package/storage/remoteStorage/createArchives.ts +65 -26
  73. package/storage/remoteStorage/deployTakeover.ts +1 -1
  74. package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +220 -0
  75. package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +25 -5
  76. package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +3 -3
  77. package/storage/remoteStorage/dist/accessPage.tsx.cache +13 -3
  78. package/storage/remoteStorage/dist/accessStats.ts.cache +6 -6
  79. package/storage/remoteStorage/dist/blobStore.ts.cache +483 -965
  80. package/storage/remoteStorage/dist/bucketDisk.ts.cache +60 -29
  81. package/storage/remoteStorage/dist/certTrustModal.tsx.cache +55 -0
  82. package/storage/remoteStorage/dist/cliArgs.ts.cache +9 -4
  83. package/storage/remoteStorage/dist/createArchives.ts.cache +63 -26
  84. package/storage/remoteStorage/dist/deployTakeover.ts.cache +5 -5
  85. package/storage/remoteStorage/dist/intermediateManagement.ts.cache +286 -0
  86. package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
  87. package/storage/remoteStorage/dist/remoteConfig.ts.cache +122 -56
  88. package/storage/remoteStorage/dist/serverConfig.ts.cache +4 -4
  89. package/storage/remoteStorage/dist/sourceWrapper.ts.cache +7 -11
  90. package/storage/remoteStorage/dist/sourcesList.ts.cache +3 -6
  91. package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
  92. package/storage/remoteStorage/dist/storageController.ts.cache +27 -12
  93. package/storage/remoteStorage/dist/storageServer.ts.cache +27 -7
  94. package/storage/remoteStorage/dist/storageServerCli.ts.cache +7 -3
  95. package/storage/remoteStorage/dist/storageServerState.ts.cache +133 -696
  96. package/storage/remoteStorage/dist/storeConfig.ts.cache +52 -0
  97. package/storage/remoteStorage/dist/storePlan.ts.cache +110 -81
  98. package/storage/remoteStorage/dist/storeSources.ts.cache +51 -0
  99. package/storage/remoteStorage/dist/storeSync.ts.cache +650 -0
  100. package/storage/remoteStorage/dist/validation.ts.cache +14 -4
  101. package/storage/remoteStorage/intermediateManagement.d.ts +12 -0
  102. package/storage/remoteStorage/intermediateManagement.ts +264 -0
  103. package/storage/remoteStorage/remoteConfig.d.ts +32 -0
  104. package/storage/remoteStorage/remoteConfig.ts +115 -50
  105. package/storage/remoteStorage/serverConfig.d.ts +1 -1
  106. package/storage/remoteStorage/serverConfig.ts +1 -1
  107. package/storage/remoteStorage/sourceWrapper.ts +4 -8
  108. package/storage/remoteStorage/sourcesList.d.ts +0 -2
  109. package/storage/remoteStorage/sourcesList.ts +0 -3
  110. package/storage/remoteStorage/spec.md +18 -4
  111. package/storage/remoteStorage/storageController.d.ts +10 -0
  112. package/storage/remoteStorage/storageController.ts +29 -16
  113. package/storage/remoteStorage/storageServer.d.ts +2 -0
  114. package/storage/remoteStorage/storageServer.ts +28 -4
  115. package/storage/remoteStorage/storageServerCli.ts +5 -1
  116. package/storage/remoteStorage/storageServerState.d.ts +34 -21
  117. package/storage/remoteStorage/storageServerState.ts +147 -662
  118. package/storage/remoteStorage/storeConfig.d.ts +30 -0
  119. package/storage/remoteStorage/storeConfig.ts +62 -0
  120. package/storage/remoteStorage/storePlan.d.ts +29 -17
  121. package/storage/remoteStorage/storePlan.ts +112 -85
  122. package/storage/remoteStorage/storeSources.d.ts +11 -0
  123. package/storage/remoteStorage/storeSources.ts +47 -0
  124. package/storage/remoteStorage/storeSync.d.ts +53 -0
  125. package/storage/remoteStorage/storeSync.ts +632 -0
  126. package/storage/remoteStorage/validation.d.ts +2 -0
  127. package/storage/remoteStorage/validation.ts +10 -0
@@ -8,9 +8,13 @@ import { delay } from "socket-function/src/batching";
8
8
  import { getExternalIP } from "socket-function/src/networking";
9
9
  import { forwardPort } from "socket-function/src/forwardPort";
10
10
  import { magenta } from "socket-function/src/formatting/logColors";
11
- import { getOwnMachineId, getThreadKeyCert, loadIdentityCA } from "./certs";
11
+ import { getOwnMachineId, getThreadKeyCert, loadIdentityCA, getIdentityCA, createX509, generateKeyPair } from "./certs";
12
12
  import { generateCert, getAccountKey, parseCert } from "./httpsCerts";
13
13
  import { setRecord } from "./dns";
14
+ import { setHostsEntry } from "./hostsFile";
15
+
16
+ // A self-signed cert is signed by our own CA and never renews via ACME, so it gets a long life
17
+ const SELF_SIGNED_LIFESPAN = 1000 * 60 * 60 * 24 * 365 * 10;
14
18
 
15
19
  // Renew somewhere randomly between 40% and 60% of the way through the cert lifetime. The random threshold staggers parallel processes on the same machine, so usually one process renews early, and the others see the renewed cert on disk (we re-read the disk before every renewal check) and never renew themselves.
16
20
  const RENEW_THRESHOLD_MIN = 0.4;
@@ -30,6 +34,10 @@ export type HostServerConfig = {
30
34
  setDNSRecord?: boolean;
31
35
  publicIp?: string;
32
36
  allowHostnames?: string[];
37
+ /** LAN-only: do NOT forward the port (no UPnP/NAT mapping), for a server reachable only on the local network. */
38
+ internal?: boolean;
39
+ /** Serve a TLS cert signed by this machine's CA instead of obtaining a real (ACME) one. See getFreshHTTPSCert. */
40
+ selfSigned?: boolean;
33
41
  /** When the port is busy (e.g. the previous deploy still holds it), mount on an alternate port instead (the socket server's built-in free-port scan), and keep trying to take the real port - once it frees, a raw TCP relay on the real port forwards to our listener (SocketFunction can only mount once per process). */
34
42
  portFallback?: {
35
43
  /** Delay until the next main-port acquisition attempt (tightened around the predecessor's scheduled death) */
@@ -50,12 +58,18 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
50
58
 
51
59
  if (config.setDNSRecord) {
52
60
  let ip = config.publicIp || await getExternalIP();
53
- await setRecord("A", domain, ip);
61
+ if (config.selfSigned) {
62
+ // A self-signed server uses no Cloudflare at all (no ACME challenge and no public DNS record), so we resolve the ip-domain through THIS machine's hosts file instead. Other machines that need to reach it must resolve the domain themselves (a real DNS record, or the same hosts entry).
63
+ setHostsEntry({ ip, hostname: domain });
64
+ setHostsEntry({ ip: "127.0.0.1", hostname: "127-0-0-1." + rootDomain });
65
+ } else {
66
+ await setRecord("A", domain, ip);
67
+ }
54
68
  }
55
69
 
56
- let keyCert = await getFreshHTTPSCert(domain);
70
+ let keyCert = await getFreshHTTPSCert(domain, config.selfSigned);
57
71
  let certListeners: ((value: { key: string; cert: string }) => void)[] = [];
58
- void runCertRenewalLoop(domain, newKeyCert => {
72
+ void runCertRenewalLoop(domain, config.selfSigned, newKeyCert => {
59
73
  keyCert = newKeyCert;
60
74
  for (let listener of certListeners) {
61
75
  listener(newKeyCert);
@@ -65,7 +79,8 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
65
79
  // With portFallback, a busy port is not an error: the underlying server scans for a free port instead (useAvailablePortIfPortInUse), and the nodeId tells us which port we actually got
66
80
  let nodeId = await SocketFunction.mount({
67
81
  public: true,
68
- autoForwardPort: true,
82
+ // --internal servers are LAN-only, so we never open a port on the router
83
+ autoForwardPort: !config.internal,
69
84
  port,
70
85
  useAvailablePortIfPortInUse: !!config.portFallback,
71
86
  ...getThreadKeyCert(rootDomain),
@@ -93,7 +108,7 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
93
108
  if (fallback && servingPort !== port) {
94
109
  console.warn(`Port ${port} is in use (presumably by our predecessor); serving on alternate port ${servingPort} until it frees`);
95
110
  await fallback.onPortInUse?.();
96
- void runMainPortAcquireLoop(domain, port, servingPort, fallback);
111
+ void runMainPortAcquireLoop(domain, port, servingPort, fallback, !!config.internal);
97
112
  }
98
113
  fallback?.onListening(servingPort, servingPort === port);
99
114
  console.log(magenta(`Hosting https://${domain}:${servingPort} (nodeId ${nodeId})`));
@@ -101,7 +116,7 @@ export async function hostServer(config: HostServerConfig): Promise<string> {
101
116
  }
102
117
 
103
118
  // SocketFunction can only mount once per process, so "taking over" the main port is a raw TCP relay forwarding to our real listener - TLS/SNI passes straight through, and clients address servers by ip domain (wildcard negotiation), so the extra hop is invisible.
104
- async function runMainPortAcquireLoop(domain: string, mainPort: number, servingPort: number, fallback: NonNullable<HostServerConfig["portFallback"]>): Promise<void> {
119
+ async function runMainPortAcquireLoop(domain: string, mainPort: number, servingPort: number, fallback: NonNullable<HostServerConfig["portFallback"]>, internal: boolean): Promise<void> {
105
120
  while (true) {
106
121
  await delay(fallback.getAcquireDelay());
107
122
  let acquired = await new Promise<boolean>(resolve => {
@@ -124,24 +139,35 @@ async function runMainPortAcquireLoop(domain: string, mainPort: number, servingP
124
139
  });
125
140
  if (acquired) {
126
141
  console.log(magenta(`Acquired main port ${mainPort} for https://${domain} (relaying to our listener on ${servingPort})`));
127
- // The mount only forwarded OUR serving port; the main port needs its own mapping (the predecessor's mapping pointed at the same machine+port, but dies with it)
128
- await forwardPort({ externalPort: mainPort, internalPort: mainPort });
142
+ // The mount only forwarded OUR serving port; the main port needs its own mapping (the predecessor's mapping pointed at the same machine+port, but dies with it). LAN-only (--internal) servers never open router ports.
143
+ if (!internal) {
144
+ await forwardPort({ externalPort: mainPort, internalPort: mainPort });
145
+ }
129
146
  fallback.onListening(mainPort, true);
130
147
  return;
131
148
  }
132
149
  }
133
150
  }
134
151
 
135
- function getCertDiskPath(domain: string) {
136
- return os.homedir() + `/httpscert_${domain}.json`;
152
+ // Self-signed certs cache under a distinct name, so switching a domain between ACME and self-signed never reads back the wrong kind
153
+ function getCertDiskPath(domain: string, selfSigned?: boolean) {
154
+ return os.homedir() + `/httpscert_${selfSigned ? "selfsigned_" : ""}${domain}.json`;
137
155
  }
138
- function readCertFromDisk(domain: string): { key: string; cert: string } | undefined {
156
+ function readCertFromDisk(domain: string, selfSigned?: boolean): { key: string; cert: string } | undefined {
139
157
  try {
140
- return JSON.parse(fs.readFileSync(getCertDiskPath(domain), "utf8")) as { key: string; cert: string };
158
+ return JSON.parse(fs.readFileSync(getCertDiskPath(domain, selfSigned), "utf8")) as { key: string; cert: string };
141
159
  } catch {
142
160
  return undefined;
143
161
  }
144
162
  }
163
+
164
+ // A TLS cert for `domain` signed by this machine's identity CA, presented as a chain (leaf + CA) so a client that trusts the CA validates it. The key part of the ip-domain scheme is unchanged: browsers ignore nameConstraints and validate the SAN + chain; a browser that hasn't trusted the CA prompts the user, who accepts it once (the trust-cert flow), and node peers trust it via the CA.
165
+ function generateSelfSignedCert(domain: string): { key: string; cert: string } {
166
+ let rootDomain = domain.split(".").slice(-2).join(".");
167
+ let ca = getIdentityCA(rootDomain);
168
+ let leaf = createX509({ domain, issuer: ca, keyPair: generateKeyPair(), lifeSpan: SELF_SIGNED_LIFESPAN });
169
+ return { key: leaf.key.toString(), cert: leaf.cert.toString() + "\n" + ca.cert.toString() };
170
+ }
145
171
  function getRenewTime(certPem: string, threshold: number) {
146
172
  let certObj = parseCert(certPem);
147
173
  let start = +new Date(certObj.validity.notBefore);
@@ -149,12 +175,19 @@ function getRenewTime(certPem: string, threshold: number) {
149
175
  return start + (end - start) * threshold;
150
176
  }
151
177
 
152
- /** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. */
153
- export async function getFreshHTTPSCert(domain: string): Promise<{ key: string; cert: string }> {
154
- let keyCert = readCertFromDisk(domain);
178
+ /** Returns the cached HTTPS cert for the domain, creating/renewing it first if it is past this process's renewal threshold. Reads the disk cache on every call, so a renewal done by a parallel process is picked up instead of renewing again. selfSigned: sign it with this machine's CA instead of getting a real ACME cert. */
179
+ export async function getFreshHTTPSCert(domain: string, selfSigned?: boolean): Promise<{ key: string; cert: string }> {
180
+ let keyCert = readCertFromDisk(domain, selfSigned);
155
181
  if (keyCert && getRenewTime(keyCert.cert, renewThreshold) > Date.now()) {
156
182
  return keyCert;
157
183
  }
184
+ if (selfSigned) {
185
+ // Cached so the served cert is stable across restarts (a browser's accepted-certificate exception is per exact cert)
186
+ console.log(magenta(keyCert ? `Self-signed cert for ${domain} is expiring, regenerating` : `No self-signed cert on disk for ${domain}, creating one (signed by this machine's CA)`));
187
+ let generated = generateSelfSignedCert(domain);
188
+ fs.writeFileSync(getCertDiskPath(domain, true), JSON.stringify(generated));
189
+ return generated;
190
+ }
158
191
  if (keyCert) {
159
192
  console.log(magenta(`HTTPS cert for ${domain} is past ${(renewThreshold * 100).toFixed(0)}% of its lifetime, renewing`));
160
193
  } else {
@@ -176,12 +209,12 @@ export async function getFreshHTTPSCert(domain: string): Promise<{ key: string;
176
209
  return { key: keyCert.key, cert: keyCert.cert };
177
210
  }
178
211
 
179
- async function runCertRenewalLoop(domain: string, onNewCert: (keyCert: { key: string; cert: string }) => void) {
180
- let lastCert = readCertFromDisk(domain)?.cert;
212
+ async function runCertRenewalLoop(domain: string, selfSigned: boolean | undefined, onNewCert: (keyCert: { key: string; cert: string }) => void) {
213
+ let lastCert = readCertFromDisk(domain, selfSigned)?.cert;
181
214
  while (true) {
182
215
  await delay(CERT_CHECK_INTERVAL);
183
216
  try {
184
- let keyCert = await getFreshHTTPSCert(domain);
217
+ let keyCert = await getFreshHTTPSCert(domain, selfSigned);
185
218
  if (keyCert.cert !== lastCert) {
186
219
  lastCert = keyCert.cert;
187
220
  console.log(magenta(`HTTPS cert for ${domain} updated, applying to running server`));
@@ -0,0 +1,7 @@
1
+ /** Ensures the hosts file maps `hostname` to `ip` (adding our tagged line, or updating it/an existing line for the same hostname). Idempotent. Returns false, with a warning telling the user the line to add by hand, if the file can't be written (writing the hosts file needs admin/root). */
2
+ export declare function setHostsEntry(config: {
3
+ ip: string;
4
+ hostname: string;
5
+ }): boolean;
6
+ /** Removes our managed entry for `hostname` (only lines we added, tagged with the marker). No-op if absent or the file can't be written. */
7
+ export declare function removeHostsEntry(hostname: string): void;
@@ -0,0 +1,66 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+
4
+ // Cross-platform hosts-file editing, WITHOUT any third-party dependency. The approach (per-platform path, platform line endings, space-separated "<ip> <hostname>" entries, update-matching-hostname-else-append) is the same one the widely-used `hostile` npm package uses - but that package pulls in several dependencies (once, split, @ljharb/through), so this reimplements the ~40 lines we actually need instead of importing them. Used with --selfSigned, where we resolve the ip-domain locally via the hosts file instead of a public (Cloudflare) DNS record. NOTE: a hosts entry only affects THIS machine, so a self-signed server reachable from other machines still needs those machines to resolve its domain (a real DNS record, or the same hosts entry).
5
+
6
+ const WINDOWS = os.platform() === "win32";
7
+ // The canonical hosts-file locations (same as hostile)
8
+ const HOSTS_PATH = WINDOWS ? "C:/Windows/System32/drivers/etc/hosts" : "/etc/hosts";
9
+ const EOL = WINDOWS ? "\r\n" : "\n";
10
+ // Tags the lines we manage, so they are recognizable (and safe to rewrite/remove) without touching the user's own entries
11
+ const MARKER = "# managed by sliftutils hostsFile";
12
+
13
+ // The hostname of a line, or undefined for a blank/comment/malformed line. The marker is a comment, so it is stripped here too - matching works the same for our lines and the user's.
14
+ function lineHostname(line: string): string | undefined {
15
+ let noComment = line.replace(/#.*/, "").trim();
16
+ if (!noComment) return undefined;
17
+ let parts = noComment.split(/\s+/);
18
+ if (parts.length < 2) return undefined;
19
+ return parts[1];
20
+ }
21
+
22
+ /** Ensures the hosts file maps `hostname` to `ip` (adding our tagged line, or updating it/an existing line for the same hostname). Idempotent. Returns false, with a warning telling the user the line to add by hand, if the file can't be written (writing the hosts file needs admin/root). */
23
+ export function setHostsEntry(config: { ip: string; hostname: string }): boolean {
24
+ let { ip, hostname } = config;
25
+ let desired = `${ip} ${hostname} ${MARKER}`;
26
+ let lines: string[];
27
+ try {
28
+ lines = fs.readFileSync(HOSTS_PATH, "utf8").split(/\r?\n/);
29
+ } catch (e) {
30
+ console.error(`Could not read the hosts file ${HOSTS_PATH} to map ${hostname} -> ${ip}: ${(e as Error).stack ?? e}. Add this line yourself: ${ip} ${hostname}`);
31
+ return false;
32
+ }
33
+ // A trailing newline leaves a final "" - drop trailing blanks so we don't accumulate them, and re-add exactly one newline at the end when we write
34
+ while (lines.length && lines[lines.length - 1] === "") lines.pop();
35
+
36
+ let index = lines.findIndex(line => lineHostname(line) === hostname);
37
+ if (index >= 0) {
38
+ if (lines[index] === desired) return true;
39
+ lines[index] = desired;
40
+ } else {
41
+ lines.push(desired);
42
+ }
43
+ try {
44
+ fs.writeFileSync(HOSTS_PATH, lines.join(EOL) + EOL);
45
+ } catch (e) {
46
+ console.error(`Could not write the hosts file ${HOSTS_PATH} to map ${hostname} -> ${ip} (writing it needs admin/root): ${(e as Error).stack ?? e}. Add this line yourself: ${ip} ${hostname}`);
47
+ return false;
48
+ }
49
+ return true;
50
+ }
51
+
52
+ /** Removes our managed entry for `hostname` (only lines we added, tagged with the marker). No-op if absent or the file can't be written. */
53
+ export function removeHostsEntry(hostname: string): void {
54
+ let lines: string[];
55
+ try {
56
+ lines = fs.readFileSync(HOSTS_PATH, "utf8").split(/\r?\n/);
57
+ } catch {
58
+ return;
59
+ }
60
+ let kept = lines.filter(line => !(line.includes(MARKER) && lineHostname(line) === hostname));
61
+ if (kept.length === lines.length) return;
62
+ while (kept.length && kept[kept.length - 1] === "") kept.pop();
63
+ try {
64
+ fs.writeFileSync(HOSTS_PATH, kept.join(EOL) + EOL);
65
+ } catch { }
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.87",
3
+ "version": "1.7.89",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -1,4 +1,4 @@
1
- "use strict"; var console = (function shimConsole() { let baseConsole = globalThis.console; if (typeof window !== "undefined") return baseConsole; let name = __filename.replaceAll("\\", "/").split("/").pop(); let dirName = __dirname.replaceAll("\\", "/").split("/").pop(); let context = { __FILE__: __filename, __DIR__: __dirname, __NAME__: name, __DIRNAME__: dirName }; let newConsole = { ...baseConsole }; for (let shim of ["log", "info", "warn", "error"]) { newConsole[shim] = (...args) => { args.push(context); baseConsole[shim](...args); }; } newConsole.__context = context; return newConsole; })(); "use strict";
1
+ "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
3
3
  //exports.observer = void 0;
4
4
  process.env.NODE_ENV = "production";
@@ -36,4 +36,4 @@ function observer(Constructor) {
36
36
  }
37
37
  exports.observer = observer;
38
38
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJvYnNlcnZlci50c3giXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsT0FBTyxDQUFDLEdBQUcsQ0FBQyxRQUFRLEdBQUcsWUFBWSxDQUFDO0FBR3BDLCtCQUE0QztBQUM1Qyx1RUFBK0Q7QUFDL0QsSUFBQSxzQkFBTyxFQUFDLE9BQU8sRUFBRSxNQUFNLEVBQUUsYUFBYSxFQUFFLElBQUksQ0FBQyxDQUFDO0FBQzlDLG1FQUFxRTtBQUVyRSxJQUFJLG9CQUFvQixHQUFHLENBQUMsQ0FBQztBQUM3QixTQUFnQixRQUFRLENBU3BCLFdBQWM7SUFFZCxJQUFJLElBQUksR0FBRyxXQUFXLENBQUMsSUFBSSxDQUFDO0lBQzVCLE9BQU8sS0FBTSxTQUFRLFdBQVc7UUFBekI7O1lBR0gsYUFBUSxHQUFHLElBQUksZUFBUSxDQUFDLFVBQVUsSUFBSSxJQUFJLG9CQUFvQixFQUFFLEVBQUUsRUFBRSxHQUFHLEVBQUU7Z0JBQ3JFLEtBQUssQ0FBQyxXQUFXLEVBQUUsQ0FBQztZQUN4QixDQUFDLENBQUMsQ0FBQztRQWNQLENBQUM7UUFsQkcsYUFBYTtRQUNiLE1BQU0sS0FBSyxJQUFJLEtBQUssT0FBTyxXQUFXLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUk5QyxvQkFBb0I7O1lBQ2hCLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDeEIsTUFBQSxLQUFLLENBQUMsb0JBQW9CLG9EQUFJLENBQUM7UUFDbkMsQ0FBQztRQUNELE1BQU0sQ0FBQyxHQUFHLElBQVc7WUFDakIsSUFBSSxNQUE2QixDQUFDO1lBQ2xDLElBQUksQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLEdBQUcsRUFBRTtnQkFDckIsSUFBQSxzQkFBWSxFQUFDLEdBQUcsRUFBRTtvQkFDZCxNQUFNLEdBQUksS0FBSyxDQUFDLE1BQWMsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO2dCQUM1QyxDQUFDLEVBQUUsR0FBRyxJQUFJLFNBQVMsQ0FBQyxDQUFDO1lBQ3pCLENBQUMsQ0FBQyxDQUFDO1lBQ0gsT0FBTyxNQUFNLENBQUM7UUFDbEIsQ0FBQztLQUNKLENBQUM7QUFDTixDQUFDO0FBaENELDRCQWdDQyIsInNvdXJjZXNDb250ZW50IjpbInByb2Nlc3MuZW52Lk5PREVfRU5WID0gXCJwcm9kdWN0aW9uXCI7XG5cbmltcG9ydCAqIGFzIHByZWFjdCBmcm9tIFwicHJlYWN0XCI7XG5pbXBvcnQgeyBvYnNlcnZhYmxlLCBSZWFjdGlvbiB9IGZyb20gXCJtb2J4XCI7XG5pbXBvcnQgeyBzZXRGbGFnIH0gZnJvbSBcInNvY2tldC1mdW5jdGlvbi9yZXF1aXJlL2NvbXBpbGVGbGFnc1wiO1xuc2V0RmxhZyhyZXF1aXJlLCBcIm1vYnhcIiwgXCJhbGxvd2NsaWVudFwiLCB0cnVlKTtcbmltcG9ydCB7IG1lYXN1cmVCbG9jayB9IGZyb20gXCJzb2NrZXQtZnVuY3Rpb24vc3JjL3Byb2ZpbGluZy9tZWFzdXJlXCI7XG5cbmxldCBnbG9iYWxDb25zdHJ1Y3RPcmRlciA9IDE7XG5leHBvcnQgZnVuY3Rpb24gb2JzZXJ2ZXI8XG4gICAgVCBleHRlbmRzIHtcbiAgICAgICAgbmV3KC4uLmFyZ3M6IGFueVtdKToge1xuICAgICAgICAgICAgcmVuZGVyKCk6IHByZWFjdC5Db21wb25lbnRDaGlsZDtcbiAgICAgICAgICAgIGZvcmNlVXBkYXRlKGNhbGxiYWNrPzogKCkgPT4gdm9pZCk6IHZvaWQ7XG4gICAgICAgICAgICBjb21wb25lbnRXaWxsVW5tb3VudD8oKTogdm9pZDtcbiAgICAgICAgfVxuICAgIH1cbj4oXG4gICAgQ29uc3RydWN0b3I6IFRcbikge1xuICAgIGxldCBuYW1lID0gQ29uc3RydWN0b3IubmFtZTtcbiAgICByZXR1cm4gY2xhc3MgZXh0ZW5kcyBDb25zdHJ1Y3RvciB7XG4gICAgICAgIC8vIEB0cy1pZ25vcmVcbiAgICAgICAgc3RhdGljIGdldCBuYW1lKCkgeyByZXR1cm4gQ29uc3RydWN0b3IubmFtZTsgfVxuICAgICAgICByZWFjdGlvbiA9IG5ldyBSZWFjdGlvbihgcmVuZGVyLiR7bmFtZX0uJHtnbG9iYWxDb25zdHJ1Y3RPcmRlcisrfWAsICgpID0+IHtcbiAgICAgICAgICAgIHN1cGVyLmZvcmNlVXBkYXRlKCk7XG4gICAgICAgIH0pO1xuICAgICAgICBjb21wb25lbnRXaWxsVW5tb3VudCgpIHtcbiAgICAgICAgICAgIHRoaXMucmVhY3Rpb24uZGlzcG9zZSgpO1xuICAgICAgICAgICAgc3VwZXIuY29tcG9uZW50V2lsbFVubW91bnQ/LigpO1xuICAgICAgICB9XG4gICAgICAgIHJlbmRlciguLi5hcmdzOiBhbnlbXSkge1xuICAgICAgICAgICAgbGV0IG91dHB1dDogcHJlYWN0LkNvbXBvbmVudENoaWxkO1xuICAgICAgICAgICAgdGhpcy5yZWFjdGlvbi50cmFjaygoKSA9PiB7XG4gICAgICAgICAgICAgICAgbWVhc3VyZUJsb2NrKCgpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgb3V0cHV0ID0gKHN1cGVyLnJlbmRlciBhcyBhbnkpKC4uLmFyZ3MpO1xuICAgICAgICAgICAgICAgIH0sIGAke25hbWV9fHJlbmRlcmApO1xuICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICByZXR1cm4gb3V0cHV0O1xuICAgICAgICB9XG4gICAgfTtcbn0iXX0=
39
- /* _JS_SOURCE_HASH = "e0e7abcd5640a6bddad57729ac4d2990ec02f5c7da073c7f9033ee0405567fb7"; */
39
+ /* _JS_SOURCE_HASH = "66bf117905c840638829362c0f5010e30300b34621cecc47d4fbc18f0fe6fe54"; */
@@ -1,6 +1,6 @@
1
1
  /// <reference types="node" />
2
2
  /// <reference types="node" />
3
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SetConfig } from "./IArchives";
3
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig } from "./IArchives";
4
4
  export declare class ArchivesDisk implements IArchives {
5
5
  private folder;
6
6
  constructor(folder: string);
@@ -21,6 +21,7 @@ export declare class ArchivesDisk implements IArchives {
21
21
  private filePath;
22
22
  set(key: string, data: Buffer, config?: SetConfig): Promise<string>;
23
23
  del(key: string, config?: DelConfig): Promise<void>;
24
+ move(config: MoveFileConfig): Promise<void>;
24
25
  get(key: string, config?: GetConfig): Promise<Buffer | undefined>;
25
26
  get2(key: string, config?: GetConfig): Promise<{
26
27
  data: Buffer;
@@ -34,11 +35,7 @@ export declare class ArchivesDisk implements IArchives {
34
35
  find(prefix: string, config?: FindConfig): Promise<string[]>;
35
36
  findInfo(prefix: string, config?: FindConfig): Promise<ArchiveFileInfo[]>;
36
37
  private collectFiles;
37
- setLargeFile(config: {
38
- path: string;
39
- lastModified?: number;
40
- getNextData(): Promise<Buffer | undefined>;
41
- }): Promise<void>;
38
+ setLargeFile(config: SetLargeFileConfig): Promise<void>;
42
39
  startLargeUpload(): Promise<string>;
43
40
  appendLargeUpload(id: string, data: Buffer): Promise<void>;
44
41
  finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
@@ -3,7 +3,7 @@ import path from "path";
3
3
  import { lazy } from "socket-function/src/caching";
4
4
  import { runInfinitePoll } from "socket-function/src/batching";
5
5
  import { sort, binarySearchBasic } from "socket-function/src/misc";
6
- import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, SetConfig, assertValidLastModified } from "./IArchives";
6
+ import { IArchives, ArchiveFileInfo, ArchivesConfig, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig, assertValidLastModified } from "./IArchives";
7
7
  import { filterChanges } from "./remoteStorage/remoteConfig";
8
8
 
9
9
  // The base file-system IArchives: storage is one-to-one with the file system, every key is exactly one real file under <folder>/files, so the file system itself is the index. File handles are cached and reused, and closed once idle (see FileHandleCache). All operations on a file run in serial, so they can't collide with each other or with handle closing. Used as the disk synchronization source of BlobStore (see remoteStorage/blobStore.ts).
@@ -191,6 +191,29 @@ export class ArchivesDisk implements IArchives {
191
191
  });
192
192
  }
193
193
 
194
+ public async move(config: MoveFileConfig): Promise<void> {
195
+ await this.init();
196
+ if (config.fromPath === config.toPath) return;
197
+ let fromPath = this.filePath(config.fromPath);
198
+ let toPath = this.filePath(config.toPath);
199
+ await this.handles.run(fromPath, async () => {
200
+ await this.handles.closeNow(fromPath);
201
+ await this.handles.run(toPath, async () => {
202
+ await this.handles.closeNow(toPath);
203
+ await fs.promises.mkdir(path.dirname(toPath), { recursive: true });
204
+ try {
205
+ await fs.promises.rename(fromPath, toPath);
206
+ } catch (e: any) {
207
+ if (e.code !== "ENOENT") throw e;
208
+ throw new Error(`Cannot move ${JSON.stringify(config.fromPath)} to ${JSON.stringify(config.toPath)} on ${this.getDebugName()}: the source file does not exist`);
209
+ }
210
+ // The rename preserves the mtime, but a move must stamp FRESH (see IArchives.move) - the old stamp is how a moved file loses to a stale tombstone at its new path
211
+ let now = new Date();
212
+ await fs.promises.utimes(toPath, now, now);
213
+ });
214
+ });
215
+ }
216
+
194
217
  public async get(key: string, config?: GetConfig): Promise<Buffer | undefined> {
195
218
  let result = await this.get2(key, config);
196
219
  return result && result.data || undefined;
@@ -276,7 +299,8 @@ export class ArchivesDisk implements IArchives {
276
299
  }
277
300
  }
278
301
 
279
- public async setLargeFile(config: { path: string; lastModified?: number; getNextData(): Promise<Buffer | undefined> }): Promise<void> {
302
+ // The raw disk has no windows, routes, or immutability, so the rest of the SetConfig has nothing to apply to here (the store wrapping this disk is what enforces them)
303
+ public async setLargeFile(config: SetLargeFileConfig): Promise<void> {
280
304
  let id = await this.startLargeUpload();
281
305
  try {
282
306
  while (true) {
@@ -164,7 +164,7 @@ export class BulkDatabaseReader<T extends { key: string }> {
164
164
  return { key, value: r.value as T[C], time: r.time };
165
165
  }
166
166
 
167
- // ── sync (reactive) reads ────────────────────────────────────────────────────────────────────────
167
+ // ── synchronized (reactive) reads ────────────────────────────────────────────────────────────────────────
168
168
  getSingleFieldSync<C extends keyof T>(key: string, column: C): T[C] | undefined {
169
169
  return this.getSingleFieldObjSync(key, column)?.value;
170
170
  }