@ghostlytools/cli 1.1.7 → 1.2.0

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 (35) hide show
  1. package/README.md +16 -11
  2. package/SKILL.md +2 -2
  3. package/dist/assets/arkade-3szZ1IpP.js +362 -0
  4. package/dist/assets/arkade-3szZ1IpP.js.map +1 -0
  5. package/dist/assets/{endpoint-D_3bYafG.js → endpoint-pgFkR2AS.js} +47 -10
  6. package/dist/assets/endpoint-pgFkR2AS.js.map +1 -0
  7. package/dist/assets/{fileBytes-3AQ2tJzW.js → fileBytes-BcYZhNgF.js} +3 -3
  8. package/dist/assets/{fileBytes-3AQ2tJzW.js.map → fileBytes-BcYZhNgF.js.map} +1 -1
  9. package/dist/assets/{fileBytes-CAZDydIe.js → fileBytes-Dbifu5I5.js} +2 -2
  10. package/dist/assets/{fileBytes-CAZDydIe.js.map → fileBytes-Dbifu5I5.js.map} +1 -1
  11. package/dist/assets/{fileBytesIdb-CmG00qE-.js → fileBytesIdb-DzSaqI0A.js} +3 -3
  12. package/dist/assets/{fileBytesIdb-CmG00qE-.js.map → fileBytesIdb-DzSaqI0A.js.map} +1 -1
  13. package/dist/assets/{fileBytesOpfs-CTgqt-p3.js → fileBytesOpfs-Dx8RZiV5.js} +3 -3
  14. package/dist/assets/{fileBytesOpfs-CTgqt-p3.js.map → fileBytesOpfs-Dx8RZiV5.js.map} +1 -1
  15. package/dist/assets/{hyperdhtRelay-DvXgebV7.js → hyperdhtRelay-DmTrpmba.js} +3 -3
  16. package/dist/assets/{hyperdhtRelay-DvXgebV7.js.map → hyperdhtRelay-DmTrpmba.js.map} +1 -1
  17. package/dist/assets/{idb-BIdus0WX.js → idb-CTF6LF5B.js} +62 -6
  18. package/dist/assets/idb-CTF6LF5B.js.map +1 -0
  19. package/dist/assets/{mainline-B7wHcLaW.js → mainline-BueFJx3f.js} +2 -2
  20. package/dist/assets/{mainline-B7wHcLaW.js.map → mainline-BueFJx3f.js.map} +1 -1
  21. package/dist/assets/{restoredRows-lhTKGmCO.js → restoredRows-C_84q0Pd.js} +2 -2
  22. package/dist/assets/{restoredRows-lhTKGmCO.js.map → restoredRows-C_84q0Pd.js.map} +1 -1
  23. package/dist/assets/{server-GJW_5wGI.js → server-BGXmSYx_.js} +1124 -1036
  24. package/dist/assets/server-BGXmSYx_.js.map +1 -0
  25. package/dist/assets/{src-Cp3o_yHN.js → src-BMUcYQ7b.js} +750 -242
  26. package/dist/assets/src-BMUcYQ7b.js.map +1 -0
  27. package/dist/assets/usdt-DklGBSeq.js +410 -0
  28. package/dist/assets/usdt-DklGBSeq.js.map +1 -0
  29. package/dist/ghostly.mjs +858 -137
  30. package/dist/ghostly.mjs.map +1 -1
  31. package/package.json +4 -4
  32. package/dist/assets/endpoint-D_3bYafG.js.map +0 -1
  33. package/dist/assets/idb-BIdus0WX.js.map +0 -1
  34. package/dist/assets/server-GJW_5wGI.js.map +0 -1
  35. package/dist/assets/src-Cp3o_yHN.js.map +0 -1
@@ -12,29 +12,54 @@ import DHT from "hyperdht";
12
12
  */
13
13
  var PREFACE_WAIT_MS = 2e3;
14
14
  /**
15
+ * The longest a stream that opened waits for the preface on a slow path: the 8 s a chat's move gives its dial. Past
16
+ * it nobody is waiting for the dial made again, and UDX gives a silent stream up at 13 s anyway.
17
+ */
18
+ var PREFACE_MAX_WAIT_MS = 8e3;
19
+ /**
20
+ * The preface wait for a stream whose smoothed RTT is `rtt` ms and whose dial took `opening` ms to open: three round
21
+ * trips once there is a sample, the opening itself before, between `floor` and `ceiling`.
22
+ */
23
+ function prefaceWait(rtt, opening = 0, floor = PREFACE_WAIT_MS, ceiling = PREFACE_MAX_WAIT_MS) {
24
+ const wait = rtt > 0 ? 3 * rtt : 1 * (opening || 0);
25
+ return Math.min(ceiling, Math.max(floor, wait));
26
+ }
27
+ /**
15
28
  * Dials with `dialOnce` until one brings the contact's preface. `dialOnce()` starts a connection and returns
16
29
  * `{ opened, ready, close }`: `opened` resolves true once the stream is open (the handshake is done), `ready` with the
17
- * bound channel once the contact's preface came, and `close()` drops it. A dial that opened and brought no preface
18
- * within `waitMs` is closed and made again, up to `maxDials` in all. A dial that fails before it opened (the contact
19
- * not found, refused) fails the whole connect at once, as before: there is nothing to wait for.
30
+ * bound channel once the contact's preface came, and `close()` drops it; `rtt()`, if given, is the stream's smoothed
31
+ * RTT in ms so far, 0 before its first sample (UDX's `rawStream.rtt`). A dial that opened and brought no preface
32
+ * within `waitMs`, or longer on a slow path (`prefaceWait`, at most `maxWaitMs`), is closed and made again, up to
33
+ * `maxDials` in all. The RTT is read again whenever the wait would end, so a sample taken after the stream opened
34
+ * counts. A dial that fails before it opened (the contact not found, refused) fails the whole connect at once, as
35
+ * before: there is nothing to wait for.
20
36
  */
21
- function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxDials = 3 } = {}) {
37
+ function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxWaitMs = PREFACE_MAX_WAIT_MS, maxDials = 3 } = {}) {
22
38
  return new Promise((resolve, reject) => {
23
39
  let settled = false;
24
40
  let dials = 0;
25
41
  const attempt = () => {
26
42
  dials++;
43
+ const dialledAt = Date.now();
27
44
  const dial = dialOnce();
28
45
  let timer = null;
29
46
  let stalled = false;
30
47
  dial.opened.then((open) => {
31
48
  if (!open || settled) return;
32
- timer = setTimeout(() => {
33
- if (settled || dials >= maxDials) return;
49
+ const openedAt = Date.now();
50
+ const check = () => {
51
+ if (settled) return;
52
+ const left = openedAt + prefaceWait(dial.rtt?.(), openedAt - dialledAt, waitMs, maxWaitMs) - Date.now();
53
+ if (left > 0) {
54
+ timer = setTimeout(check, left);
55
+ return;
56
+ }
57
+ if (dials >= maxDials) return;
34
58
  stalled = true;
35
59
  dial.close();
36
60
  attempt();
37
- }, waitMs);
61
+ };
62
+ timer = setTimeout(check, waitMs);
38
63
  }, () => {});
39
64
  dial.ready.then((bound) => {
40
65
  clearTimeout(timer);
@@ -58,6 +83,8 @@ function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxDials = 3 } = {}) {
58
83
  //#region ../../native/transports/hyperdht/endpoint.mjs
59
84
  var MAX_FRAME = 61440;
60
85
  var PREFACE = "ghostly/paired-chat/1";
86
+ /** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */
87
+ var CLOSE_FLUSH_MS = 300;
61
88
  /** Native UDP + NoiseSecretStream. The binding comes from the authenticated
62
89
  * stream itself, never from an application frame supplied by the other peer. */
63
90
  async function createHyperEndpoint(seed, options = {}) {
@@ -81,6 +108,7 @@ async function createHyperEndpoint(seed, options = {}) {
81
108
  let reader = null;
82
109
  let closeHandler = null;
83
110
  let closed = false;
111
+ let ending = null;
84
112
  let pending = [];
85
113
  let buffer = Buffer.alloc(0);
86
114
  let preface = false;
@@ -133,12 +161,19 @@ async function createHyperEndpoint(seed, options = {}) {
133
161
  });
134
162
  },
135
163
  close() {
136
- socket.destroy();
164
+ if (closed || ending) return ending ?? Promise.resolve();
165
+ ending = new Promise((resolve) => {
166
+ socket.once("close", resolve);
167
+ });
168
+ setTimeout(() => socket.destroy(), CLOSE_FLUSH_MS).unref?.();
169
+ socket.end();
170
+ return ending;
137
171
  }
138
172
  };
139
173
  channels.add(channel);
140
174
  onChannel?.(channel);
141
175
  socket.on("error", (error) => rejectOpen(error));
176
+ socket.on("end", () => socket.end());
142
177
  socket.on("close", () => {
143
178
  closed = true;
144
179
  pending = [];
@@ -236,6 +271,7 @@ async function createHyperEndpoint(seed, options = {}) {
236
271
  return {
237
272
  opened: socket.opened,
238
273
  ready,
274
+ rtt: () => socket.rawStream?.rtt ?? 0,
239
275
  close() {
240
276
  if (counted) channels.delete(counted);
241
277
  socket.destroy();
@@ -245,8 +281,9 @@ async function createHyperEndpoint(seed, options = {}) {
245
281
  },
246
282
  async close() {
247
283
  stopped = true;
248
- for (const channel of channels) channel.close();
284
+ const ended = [...channels].map((channel) => channel.close());
249
285
  waiting.length = 0;
286
+ await Promise.all(ended);
250
287
  await server.close();
251
288
  await node.destroy();
252
289
  }
@@ -255,4 +292,4 @@ async function createHyperEndpoint(seed, options = {}) {
255
292
  //#endregion
256
293
  export { MAX_FRAME, createHyperEndpoint };
257
294
 
258
- //# sourceMappingURL=endpoint-D_3bYafG.js.map
295
+ //# sourceMappingURL=endpoint-pgFkR2AS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-pgFkR2AS.js","names":[],"sources":["../../../../native/transports/hyperdht/redial.mjs","../../../../native/transports/hyperdht/endpoint.mjs"],"sourcesContent":["/**\n * How long a HyperDHT dial whose stream opened may go without the contact's preface before it is dialled again.\n *\n * The first connection a freshly started process makes sometimes opens (the handshake went through the DHT and the\n * stream is connected to the right address) and then carries nothing: the contact never sees it, and UDX gives up\n * only after 13 s. A connection made again at once goes through in milliseconds. Found as a chat moved to HyperDHT\n * right after the contact's app restarted taking about 28 s in CI (the move's 8 s, then its 20 s retry), 1 run in 6;\n * reproduced with hyperdht alone under load (bug hunt r11h). On loopback the preface comes within milliseconds of\n * the stream opening, one round trip on the internet.\n */\nexport const PREFACE_WAIT_MS = 2000\n\n/**\n * The longest a stream that opened waits for the preface on a slow path: the 8 s a chat's move gives its dial. Past\n * it nobody is waiting for the dial made again, and UDX gives a silent stream up at 13 s anyway.\n */\nexport const PREFACE_MAX_WAIT_MS = 8000\n\n/**\n * How many of the stream's smoothed round trips the preface may take. A contact that waits for our first packet\n * before its side of the stream connects (we are behind a firewall) writes its preface one round trip after our\n * stream opened; a lost packet adds a retransmission timeout (at least one round trip more). Three keep a slow path's\n * preface from being dialled again for nothing (a fixed 2 s on a 3 s path made three dials) and leave every path\n * under 667 ms on the 2 s.\n */\nexport const RTT_FACTOR = 3\n\n/**\n * Before the stream has its first RTT sample, how many times the dial's own opening (from the dial to the stream\n * open) the preface may take. The opening is the DHT lookup plus the handshake through a DHT node, so it took at\n * least two round trips of the path, and the preface comes one round trip after the stream opened: once covers it\n * (on a 3 s path the first sample comes only 3 s after the stream opened, past the 2 s). It is not more because a\n * stalled stream never gets a sample, and its opening is mostly the lookup (1-3 s on the public DHT even for a near\n * contact): twice a 2.5 s opening would dial again only 7.5 s after the dial, past the move's 8 s once the second\n * dial's own opening is added. A dial that opened in milliseconds (loopback, a near contact) stays on the 2 s.\n */\nexport const OPENING_FACTOR = 1\n\n/**\n * The preface wait for a stream whose smoothed RTT is `rtt` ms and whose dial took `opening` ms to open: three round\n * trips once there is a sample, the opening itself before, between `floor` and `ceiling`.\n */\nexport function prefaceWait(rtt, opening = 0, floor = PREFACE_WAIT_MS, ceiling = PREFACE_MAX_WAIT_MS) {\n const wait = rtt > 0 ? RTT_FACTOR * rtt : OPENING_FACTOR * (opening || 0)\n return Math.min(ceiling, Math.max(floor, wait))\n}\n\n/**\n * At most this many dials for one `connect`: the first, and two more if they stalled, 2 s apart, all within the 8 s a\n * chat's move gives its dial. Under load a dial made again stalled too about once in 20 (bug hunt r11h).\n */\nexport const MAX_DIALS = 3\n\n/**\n * Dials with `dialOnce` until one brings the contact's preface. `dialOnce()` starts a connection and returns\n * `{ opened, ready, close }`: `opened` resolves true once the stream is open (the handshake is done), `ready` with the\n * bound channel once the contact's preface came, and `close()` drops it; `rtt()`, if given, is the stream's smoothed\n * RTT in ms so far, 0 before its first sample (UDX's `rawStream.rtt`). A dial that opened and brought no preface\n * within `waitMs`, or longer on a slow path (`prefaceWait`, at most `maxWaitMs`), is closed and made again, up to\n * `maxDials` in all. The RTT is read again whenever the wait would end, so a sample taken after the stream opened\n * counts. A dial that fails before it opened (the contact not found, refused) fails the whole connect at once, as\n * before: there is nothing to wait for.\n */\nexport function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxWaitMs = PREFACE_MAX_WAIT_MS, maxDials = MAX_DIALS } = {}) {\n return new Promise((resolve, reject) => {\n let settled = false\n let dials = 0\n const attempt = () => {\n dials++\n const dialledAt = Date.now()\n const dial = dialOnce()\n let timer = null\n let stalled = false\n dial.opened.then(open => {\n if (!open || settled) return\n const openedAt = Date.now()\n const check = () => {\n if (settled) return\n const left = openedAt + prefaceWait(dial.rtt?.(), openedAt - dialledAt, waitMs, maxWaitMs) - Date.now()\n if (left > 0) { timer = setTimeout(check, left); return }\n if (dials >= maxDials) return\n stalled = true\n dial.close()\n attempt()\n }\n timer = setTimeout(check, waitMs)\n }, () => {})\n dial.ready.then(bound => {\n clearTimeout(timer)\n if (settled || stalled) { bound.channel.close(); return }\n settled = true\n resolve(bound)\n }, error => {\n clearTimeout(timer)\n // A dial closed here for stalling ends as closed: the next one carries the connect.\n if (settled || stalled) return\n settled = true\n reject(error)\n })\n }\n attempt()\n })\n}\n","import DHT from 'hyperdht'\nimport { redial } from './redial.mjs'\n\nexport const MAX_FRAME = 60 * 1024\nconst PREFACE = 'ghostly/paired-chat/1'\n/** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */\nconst CLOSE_FLUSH_MS = 300\n\n/** Native UDP + NoiseSecretStream. The binding comes from the authenticated\n * stream itself, never from an application frame supplied by the other peer. */\nexport async function createHyperEndpoint(seed, options = {}) {\n if (!(seed instanceof Uint8Array) || seed.length !== 32) throw new Error('Invalid HyperDHT seed')\n const keyPair = DHT.keyPair(Buffer.from(seed))\n const node = new DHT({ ...options, keyPair })\n const channels = new Set()\n let stopped = false\n let incomingHandler = null\n const waiting = []\n const descriptor = { publicKey: keyPair.publicKey.toString('hex') }\n\n /** `onChannel`: hears the channel as soon as it counts against the limit (a dial that stalls is dropped from it). */\n async function attach(socket, onChannel) {\n if (stopped || channels.size >= 2) { socket.destroy(); throw new Error('Native connection limit reached') }\n let reader = null\n let closeHandler = null\n let closed = false\n let ending = null\n let pending = []\n let buffer = Buffer.alloc(0)\n let preface = false\n let resolveOpen, rejectOpen\n const ready = new Promise((resolve, reject) => { resolveOpen = resolve; rejectOpen = reject })\n const timer = setTimeout(() => { socket.destroy(new Error('HyperDHT handshake timed out')) }, 20000)\n const channel = {\n get bufferedAmount() { return socket.writableLength || 0 },\n get onMessage() { return reader },\n set onMessage(value) { reader = value; if (value) for (const text of pending.splice(0)) value(text) },\n get onClose() { return closeHandler },\n set onClose(value) { closeHandler = value; if (closed) value?.() },\n send(text) {\n if (closed || typeof text !== 'string' || !text.length || Buffer.byteLength(text) > MAX_FRAME || this.bufferedAmount > 120 * 1024)\n throw new Error('Native channel is closed or exceeds its frame budget')\n const payload = Buffer.from(text)\n const header = Buffer.alloc(4); header.writeUInt32BE(payload.length)\n socket.write(Buffer.concat([header, payload]))\n },\n drained() {\n if (!this.bufferedAmount) return Promise.resolve()\n return new Promise((resolve, reject) => {\n const done = () => { socket.off('close', gone); resolve() }\n const gone = () => { socket.off('drain', done); reject(new Error('Native channel closed')) }\n socket.once('drain', done); socket.once('close', gone)\n })\n },\n // What was sent last (a goodbye, as an app stops) goes out before the stream ends: destroyed at once, it was\n // dropped unsent. A contact that never finishes the stream has CLOSE_FLUSH_MS.\n close() {\n if (closed || ending) return ending ?? Promise.resolve()\n ending = new Promise(resolve => { socket.once('close', resolve) })\n const timer = setTimeout(() => socket.destroy(), CLOSE_FLUSH_MS)\n timer.unref?.()\n socket.end()\n return ending\n },\n }\n channels.add(channel)\n onChannel?.(channel)\n socket.on('error', error => rejectOpen(error))\n // The contact ended its side (its channel closed): this one ends too, so the stream closes now, not on its timer.\n socket.on('end', () => socket.end())\n socket.on('close', () => {\n closed = true; pending = []; clearTimeout(timer); channels.delete(channel)\n rejectOpen(new Error('Native channel closed')); closeHandler?.()\n })\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n socket.destroy(new Error('Noise session binding unavailable')); return\n }\n channel.send(PREFACE)\n }\n socket.on('data', chunk => {\n // Secretstream chunks are bounded by its record layer. Process all frames\n // before retaining a partial one; never allocate based on an unchecked size.\n buffer = Buffer.concat([buffer, chunk])\n while (buffer.length >= 4) {\n const length = buffer.readUInt32BE(0)\n if (length === 0 || length > MAX_FRAME) { socket.destroy(new Error('Invalid frame length')); return }\n if (buffer.length < 4 + length) break\n let text\n try { text = new TextDecoder('utf-8', { fatal: true }).decode(buffer.subarray(4, 4 + length)) }\n catch { socket.destroy(new Error('Invalid UTF-8 frame')); return }\n buffer = buffer.subarray(4 + length)\n if (!preface) {\n if (text !== PREFACE) { socket.destroy(new Error('Unexpected application protocol')); return }\n preface = true; clearTimeout(timer)\n resolveOpen({ channel, binding: { transport: 'hyperdht/1', context: socket.handshakeHash.toString('hex'),\n identities: [socket.publicKey.toString('hex'), socket.remotePublicKey.toString('hex')].sort() } })\n } else if (reader) reader(text)\n else if (pending.length < 64) pending.push(text)\n else { socket.destroy(new Error('Receive queue exceeded')); return }\n }\n })\n void socket.opened.then(ok => { if (ok) opened(); else rejectOpen(new Error('Noise handshake failed')) })\n return ready\n }\n\n const server = node.createServer(socket => {\n void attach(socket).then(bound => {\n if (stopped) bound.channel.close()\n else if (incomingHandler) incomingHandler(bound)\n else waiting.push(bound)\n }).catch(() => {})\n })\n try { await server.listen(keyPair) }\n catch (error) { await node.destroy(); throw error }\n return {\n transport: 'hyperdht/1', descriptor, onDescriptor: null,\n get onConnection() { return incomingHandler },\n set onConnection(value) { incomingHandler = value; if (value) for (const bound of waiting.splice(0)) value(bound) },\n async connect(address) {\n if (stopped || !address || !/^[a-f0-9]{64}$/.test(address.publicKey)) throw new Error('Invalid HyperDHT endpoint')\n const remote = Buffer.from(address.publicKey, 'hex')\n // A dial whose stream opened and carried nothing is made again (`redial.mjs`): it is dropped from the limit at\n // once, so the next one has its place even while another channel is live. The wait follows the stream's RTT.\n return redial(() => {\n const socket = node.connect(remote, { keyPair })\n let counted = null\n const ready = attach(socket, channel => { counted = channel })\n return { opened: socket.opened, ready, rtt: () => socket.rawStream?.rtt ?? 0,\n close() { if (counted) channels.delete(counted); socket.destroy() } }\n })\n },\n // The channels end first (their last frames out, CLOSE_FLUSH_MS at most): the node's socket goes with it.\n async close() { stopped = true; const ended = [...channels].map(channel => channel.close()); waiting.length = 0; await Promise.all(ended); await server.close(); await node.destroy() },\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { o as databaseName } from "./idb-BIdus0WX.js";
2
+ import { o as databaseName } from "./idb-CTF6LF5B.js";
3
3
  import { sha256 } from "@noble/hashes/sha2.js";
4
4
  //#region ../browser/src/shared/fileBytesCommon.ts
5
5
  /** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */
@@ -64,7 +64,7 @@ async function blobDigest(blob, onProgress) {
64
64
  }
65
65
  return digestText(hash.digest());
66
66
  }
67
- var makers = /* @__PURE__ */ new Map([["opfs", () => import("./fileBytesOpfs-CTgqt-p3.js").then((m) => m.OpfsFileBytes.open())], ["idb", () => import("./fileBytesIdb-CmG00qE-.js").then((m) => new m.IdbFileBytes())]]);
67
+ var makers = /* @__PURE__ */ new Map([["opfs", () => import("./fileBytesOpfs-Dx8RZiV5.js").then((m) => m.OpfsFileBytes.open())], ["idb", () => import("./fileBytesIdb-DzSaqI0A.js").then((m) => new m.IdbFileBytes())]]);
68
68
  var made = /* @__PURE__ */ new Map();
69
69
  var preferred = ["opfs", "idb"];
70
70
  /** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */
@@ -108,4 +108,4 @@ function resetFileBytes(order = ["opfs", "idb"]) {
108
108
  //#endregion
109
109
  export { fileBytesOf as a, removeFileBytes as c, fileBytes as i, FILE_BYTES_STEP as l, blobDigest as n, fileBytes_exports as o, checkFileId as r, fileSpace as s, SMALL_FILE_BYTES as t, digestText as u };
110
110
 
111
- //# sourceMappingURL=fileBytes-3AQ2tJzW.js.map
111
+ //# sourceMappingURL=fileBytes-BcYZhNgF.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytes-3AQ2tJzW.js","names":[],"sources":["../../../browser/src/shared/fileBytesCommon.ts","../../../browser/src/shared/fileBytes.ts"],"sourcesContent":["/** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */\n\n/** Reads and copies move this much at a time. */\nexport const FILE_BYTES_STEP = 1024 * 1024;\n\nconst BASE64URL = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** base64url without padding (the worker has no @ghostly/core). */\nexport function digestText(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);\n out += BASE64URL[(n >> 18) & 63] + BASE64URL[(n >> 12) & 63];\n if (i + 1 < bytes.length) out += BASE64URL[(n >> 6) & 63];\n if (i + 2 < bytes.length) out += BASE64URL[n & 63];\n }\n return out;\n}\n","import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { databaseName } from \"./idb\";\nimport { digestText, FILE_BYTES_STEP } from \"./fileBytesCommon\";\n\n/**\n * Where the bytes of a file live when they may be more than memory holds: a file sent or received in a chat.\n * Every implementation keeps one file per id under the profile's space (`databaseName()`), is written in\n * order (`append` at the stored size, `truncate` to go back), and is read in ranges, so no caller ever needs\n * the whole file at once.\n *\n * - `opfs`: the origin-private file system, written by a worker with sync access handles (web, extension).\n * - `native`: a real file in the app's data folder, through Rust (Desktop).\n * - `idb`: 1 MiB records in IndexedDB, where neither of those exists (and in tests).\n */\nexport interface FileBytes {\n readonly kind: FileBytesKind;\n /** Copies a picked file in, hashing it on the way. Resolves to its SHA-256, base64url. */\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string>;\n /** Writes at `offset`, which must be the stored size. Resolves once written; `flush` makes it durable. */\n append(id: string, offset: number, bytes: Uint8Array): Promise<void>;\n /** What was appended so far survives a crash. */\n flush(id: string): Promise<void>;\n /** Writing is over for now: flushed, and the file let go so it can be read elsewhere. */\n close(id: string): Promise<void>;\n /** Bytes stored, or null when there is no such file. */\n size(id: string): Promise<number | null>;\n /** Cuts the file to `size` bytes (a transfer going back to its last durable point). */\n truncate(id: string, size: number): Promise<void>;\n /** At most `length` bytes from `offset`; fewer only at the end of the file. */\n read(id: string, offset: number, length: number): Promise<Uint8Array>;\n /** SHA-256 of what is stored, base64url: read back from storage, so it checks what was written. */\n digest(id: string): Promise<string>;\n /**\n * The file as a Blob to show or save, backed by storage rather than memory, or null where this platform\n * cannot hand one out (Desktop, above `NATIVE_BLOB_MAX`: saved with `save` instead).\n */\n blob(id: string, type: string): Promise<Blob | null>;\n remove(id: string): Promise<void>;\n /** Removes every file whose id starts with `prefix` (\"\" for all of this space). */\n removeWhere(prefix: string): Promise<void>;\n /** Bytes this device can still take for files, or null when the platform does not say. */\n room(): Promise<number | null>;\n /** Saves a copy where the person chooses, where the platform has a dialog for it. False when they cancelled. */\n save?(id: string, name: string): Promise<boolean>;\n /**\n * A URL a `<video>` or `<audio>` plays the stored file from, served in ranges by the platform (Desktop, for a file\n * too large for `blob`), or null when there is no such file. The URL works until `release`.\n */\n stream?(id: string, type: string): Promise<FileStream | null>;\n /**\n * What this space's files take on disk, where they are real files (Desktop): the files, and the copies staged for a\n * save (a backup, a file on its way through the save dialog) apart. Absent elsewhere: there the browser's own\n * estimate counts them.\n */\n usage?(): Promise<FileUsage>;\n /** Removes another profile's folder (a deleted profile). IndexedDB pieces go with the profile's database. */\n dropSpace?(space: string): Promise<void>;\n /**\n * The same storage for another profile's space: a backup of a profile that is not the active one reads its files\n * through it, and a restore writes a new profile's files through it before that profile exists. Absent where files\n * are pieces in the profile's own database (`idb`): those are read and written with that database.\n */\n forSpace?(space: string): FileBytes;\n}\n\nexport type FileBytesKind = \"opfs\" | \"native\" | \"idb\";\n\n/** Bytes a space's files take (`FileBytes.usage`): the files and how many, and the copies staged for a save. */\nexport interface FileUsage { files: number; count: number; staged: number }\n\n/** A stored file being served to a media element: its URL, and how to stop serving it. */\nexport interface FileStream {\n url: string;\n release(): void;\n}\n\n/** Files sent from memory-sized sources stay whole in IndexedDB; anything larger goes through a `FileBytes`. */\nexport const SMALL_FILE_BYTES = 16 * 1024 * 1024;\n\nexport { FILE_BYTES_STEP, digestText } from \"./fileBytesCommon\";\n\nconst ID = /^[A-Za-z0-9_-]{1,200}$/;\nconst SPACE = /^[A-Za-z0-9_.-]{1,100}$/;\n\n/** A file id every backend accepts as a file name as it is: never a path. */\nexport function checkFileId(id: string): string {\n if (!ID.test(id)) throw new Error(\"Invalid file id\");\n return id;\n}\n\n/** This profile's folder name. */\nexport function fileSpace(): string {\n const space = databaseName();\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n return space;\n}\n\n/**\n * SHA-256 of a Blob, read a step at a time: memory stays at one step. Read with `slice().arrayBuffer()`, not\n * `stream()`: in WebKit a Blob kept in IndexedDB that reads fine in slices failed as a stream (\"The object can not be\n * found here\"), which failed a voice note after all its bytes had arrived.\n */\nexport async function blobDigest(blob: Blob, onProgress?: (done: number) => void): Promise<string> {\n const hash = sha256.create();\n for (let done = 0; done < blob.size;) {\n const part = new Uint8Array(await blob.slice(done, Math.min(blob.size, done + FILE_BYTES_STEP)).arrayBuffer());\n if (!part.length) throw new Error(\"The file is shorter than it says\");\n hash.update(part);\n done += part.length;\n onProgress?.(done);\n }\n return digestText(hash.digest());\n}\n\ntype Maker = () => Promise<FileBytes | null>;\nconst makers = new Map<FileBytesKind, Maker>([\n [\"opfs\", () => import(\"./fileBytesOpfs\").then((m) => m.OpfsFileBytes.open())],\n [\"idb\", () => import(\"./fileBytesIdb\").then((m) => new m.IdbFileBytes())],\n]);\nconst made = new Map<FileBytesKind, Promise<FileBytes | null>>();\nlet preferred: FileBytesKind[] = [\"opfs\", \"idb\"];\n\n/** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */\nexport function registerFileBytes(kind: FileBytesKind, make: Maker, prefer = false): void {\n makers.set(kind, make);\n made.delete(kind);\n if (prefer) preferred = [kind, ...preferred.filter((k) => k !== kind)];\n}\n\n/** The backend of this kind, or null where this platform has none. */\nexport function fileBytesOf(kind: FileBytesKind): Promise<FileBytes | null> {\n let backend = made.get(kind);\n if (!backend) {\n const make = makers.get(kind);\n backend = make ? make().catch(() => null) : Promise.resolve(null);\n made.set(kind, backend);\n }\n return backend;\n}\n\n/** Where new files go: the first backend this platform has. */\nexport async function fileBytes(): Promise<FileBytes> {\n for (const kind of preferred) {\n const backend = await fileBytesOf(kind);\n if (backend) return backend;\n }\n throw new Error(\"This device has no storage for files\");\n}\n\n/** \"Clear all data\" and a deleted chat: the bytes of its files go with it, wherever they are. */\nexport async function removeFileBytes(prefix = \"\"): Promise<void> {\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.removeWhere(prefix)));\n}\n\n/** A deleted profile's files, wherever they are. */\nexport async function dropFileSpace(space: string): Promise<void> {\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.dropSpace?.(space)));\n}\n\n/** Tests: forget what was made and which backend comes first. */\nexport function resetFileBytes(order: FileBytesKind[] = [\"opfs\", \"idb\"]): void {\n made.clear();\n preferred = order;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytes-BcYZhNgF.js","names":[],"sources":["../../../browser/src/shared/fileBytesCommon.ts","../../../browser/src/shared/fileBytes.ts"],"sourcesContent":["/** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */\n\n/** Reads and copies move this much at a time. */\nexport const FILE_BYTES_STEP = 1024 * 1024;\n\nconst BASE64URL = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\n/** base64url without padding (the worker has no @ghostly/core). */\nexport function digestText(bytes: Uint8Array): string {\n let out = \"\";\n for (let i = 0; i < bytes.length; i += 3) {\n const n = (bytes[i] << 16) | ((bytes[i + 1] ?? 0) << 8) | (bytes[i + 2] ?? 0);\n out += BASE64URL[(n >> 18) & 63] + BASE64URL[(n >> 12) & 63];\n if (i + 1 < bytes.length) out += BASE64URL[(n >> 6) & 63];\n if (i + 2 < bytes.length) out += BASE64URL[n & 63];\n }\n return out;\n}\n","import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { databaseName } from \"./idb\";\nimport { digestText, FILE_BYTES_STEP } from \"./fileBytesCommon\";\n\n/**\n * Where the bytes of a file live when they may be more than memory holds: a file sent or received in a chat.\n * Every implementation keeps one file per id under the profile's space (`databaseName()`), is written in\n * order (`append` at the stored size, `truncate` to go back), and is read in ranges, so no caller ever needs\n * the whole file at once.\n *\n * - `opfs`: the origin-private file system, written by a worker with sync access handles (web, extension).\n * - `native`: a real file in the app's data folder, through Rust (Desktop).\n * - `idb`: 1 MiB records in IndexedDB, where neither of those exists (and in tests).\n */\nexport interface FileBytes {\n readonly kind: FileBytesKind;\n /** Copies a picked file in, hashing it on the way. Resolves to its SHA-256, base64url. */\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string>;\n /** Writes at `offset`, which must be the stored size. Resolves once written; `flush` makes it durable. */\n append(id: string, offset: number, bytes: Uint8Array): Promise<void>;\n /** What was appended so far survives a crash. */\n flush(id: string): Promise<void>;\n /** Writing is over for now: flushed, and the file let go so it can be read elsewhere. */\n close(id: string): Promise<void>;\n /** Bytes stored, or null when there is no such file. */\n size(id: string): Promise<number | null>;\n /** Cuts the file to `size` bytes (a transfer going back to its last durable point). */\n truncate(id: string, size: number): Promise<void>;\n /** At most `length` bytes from `offset`; fewer only at the end of the file. */\n read(id: string, offset: number, length: number): Promise<Uint8Array>;\n /** SHA-256 of what is stored, base64url: read back from storage, so it checks what was written. */\n digest(id: string): Promise<string>;\n /**\n * The file as a Blob to show or save, backed by storage rather than memory, or null where this platform\n * cannot hand one out (Desktop, above `NATIVE_BLOB_MAX`: saved with `save` instead).\n */\n blob(id: string, type: string): Promise<Blob | null>;\n remove(id: string): Promise<void>;\n /** Removes every file whose id starts with `prefix` (\"\" for all of this space). */\n removeWhere(prefix: string): Promise<void>;\n /** Bytes this device can still take for files, or null when the platform does not say. */\n room(): Promise<number | null>;\n /** Saves a copy where the person chooses, where the platform has a dialog for it. False when they cancelled. */\n save?(id: string, name: string): Promise<boolean>;\n /**\n * A URL a `<video>` or `<audio>` plays the stored file from, served in ranges by the platform (Desktop, for a file\n * too large for `blob`), or null when there is no such file. The URL works until `release`.\n */\n stream?(id: string, type: string): Promise<FileStream | null>;\n /**\n * What this space's files take on disk, where they are real files (Desktop): the files, and the copies staged for a\n * save (a backup, a file on its way through the save dialog) apart. Absent elsewhere: there the browser's own\n * estimate counts them.\n */\n usage?(): Promise<FileUsage>;\n /** Removes another profile's folder (a deleted profile). IndexedDB pieces go with the profile's database. */\n dropSpace?(space: string): Promise<void>;\n /**\n * The same storage for another profile's space: a backup of a profile that is not the active one reads its files\n * through it, and a restore writes a new profile's files through it before that profile exists. Absent where files\n * are pieces in the profile's own database (`idb`): those are read and written with that database.\n */\n forSpace?(space: string): FileBytes;\n}\n\nexport type FileBytesKind = \"opfs\" | \"native\" | \"idb\";\n\n/** Bytes a space's files take (`FileBytes.usage`): the files and how many, and the copies staged for a save. */\nexport interface FileUsage { files: number; count: number; staged: number }\n\n/** A stored file being served to a media element: its URL, and how to stop serving it. */\nexport interface FileStream {\n url: string;\n release(): void;\n}\n\n/** Files sent from memory-sized sources stay whole in IndexedDB; anything larger goes through a `FileBytes`. */\nexport const SMALL_FILE_BYTES = 16 * 1024 * 1024;\n\nexport { FILE_BYTES_STEP, digestText } from \"./fileBytesCommon\";\n\nconst ID = /^[A-Za-z0-9_-]{1,200}$/;\nconst SPACE = /^[A-Za-z0-9_.-]{1,100}$/;\n\n/** A file id every backend accepts as a file name as it is: never a path. */\nexport function checkFileId(id: string): string {\n if (!ID.test(id)) throw new Error(\"Invalid file id\");\n return id;\n}\n\n/** This profile's folder name. */\nexport function fileSpace(): string {\n const space = databaseName();\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n return space;\n}\n\n/**\n * SHA-256 of a Blob, read a step at a time: memory stays at one step. Read with `slice().arrayBuffer()`, not\n * `stream()`: in WebKit a Blob kept in IndexedDB that reads fine in slices failed as a stream (\"The object can not be\n * found here\"), which failed a voice note after all its bytes had arrived.\n */\nexport async function blobDigest(blob: Blob, onProgress?: (done: number) => void): Promise<string> {\n const hash = sha256.create();\n for (let done = 0; done < blob.size;) {\n const part = new Uint8Array(await blob.slice(done, Math.min(blob.size, done + FILE_BYTES_STEP)).arrayBuffer());\n if (!part.length) throw new Error(\"The file is shorter than it says\");\n hash.update(part);\n done += part.length;\n onProgress?.(done);\n }\n return digestText(hash.digest());\n}\n\ntype Maker = () => Promise<FileBytes | null>;\nconst makers = new Map<FileBytesKind, Maker>([\n [\"opfs\", () => import(\"./fileBytesOpfs\").then((m) => m.OpfsFileBytes.open())],\n [\"idb\", () => import(\"./fileBytesIdb\").then((m) => new m.IdbFileBytes())],\n]);\nconst made = new Map<FileBytesKind, Promise<FileBytes | null>>();\nlet preferred: FileBytesKind[] = [\"opfs\", \"idb\"];\n\n/** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */\nexport function registerFileBytes(kind: FileBytesKind, make: Maker, prefer = false): void {\n makers.set(kind, make);\n made.delete(kind);\n if (prefer) preferred = [kind, ...preferred.filter((k) => k !== kind)];\n}\n\n/** The backend of this kind, or null where this platform has none. */\nexport function fileBytesOf(kind: FileBytesKind): Promise<FileBytes | null> {\n let backend = made.get(kind);\n if (!backend) {\n const make = makers.get(kind);\n backend = make ? make().catch(() => null) : Promise.resolve(null);\n made.set(kind, backend);\n }\n return backend;\n}\n\n/** Where new files go: the first backend this platform has. */\nexport async function fileBytes(): Promise<FileBytes> {\n for (const kind of preferred) {\n const backend = await fileBytesOf(kind);\n if (backend) return backend;\n }\n throw new Error(\"This device has no storage for files\");\n}\n\n/** \"Clear all data\" and a deleted chat: the bytes of its files go with it, wherever they are. */\nexport async function removeFileBytes(prefix = \"\"): Promise<void> {\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.removeWhere(prefix)));\n}\n\n/** A deleted profile's files, wherever they are. */\nexport async function dropFileSpace(space: string): Promise<void> {\n if (!SPACE.test(space)) throw new Error(\"Invalid profile space\");\n await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.dropSpace?.(space)));\n}\n\n/** Tests: forget what was made and which backend comes first. */\nexport function resetFileBytes(order: FileBytesKind[] = [\"opfs\", \"idb\"]): void {\n made.clear();\n preferred = order;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace, u as digestText } from "./fileBytes-3AQ2tJzW.js";
1
+ import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace, u as digestText } from "./fileBytes-BcYZhNgF.js";
2
2
  import { mkdir, open, readdir, rm, stat, statfs } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { createHash } from "node:crypto";
@@ -151,4 +151,4 @@ var NodeFileBytes = class {
151
151
  //#endregion
152
152
  export { NodeFileBytes };
153
153
 
154
- //# sourceMappingURL=fileBytes-CAZDydIe.js.map
154
+ //# sourceMappingURL=fileBytes-Dbifu5I5.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytes-CAZDydIe.js","names":[],"sources":["../../src/runtime/fileBytes.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, open, readdir, rm, stat, statfs, type FileHandle } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, fileSpace, type FileBytes } from \"@ghostly/browser/shared/fileBytes\";\n\n/**\n * Files as real files in the profile's folder (`files/<space>/<id>`, 0600 in 0700 folders), the Node twin of the\n * Desktop's `native` backend (`shared/fileBytesNative.ts`, which goes through Rust). Registered as `native` and\n * preferred, so every file of any size is a file on disk, read and written a step at a time.\n */\nexport class NodeFileBytes implements FileBytes {\n readonly kind = \"native\" as const;\n private readonly writers = new Map<string, Promise<FileHandle>>();\n\n constructor(private readonly root: string) {}\n\n private folder(space = fileSpace()): string { return join(this.root, space); }\n private path(id: string): string { return join(this.folder(), checkFileId(id)); }\n\n private writer(id: string): Promise<FileHandle> {\n const path = this.path(id);\n let handle = this.writers.get(path);\n if (!handle) {\n handle = mkdir(this.folder(), { recursive: true, mode: 0o700 }).then(() => open(path, \"a\", 0o600));\n this.writers.set(path, handle);\n handle.catch(() => this.writers.delete(path));\n }\n return handle;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n await this.remove(id);\n try {\n for (let offset = 0; offset < source.size; offset += FILE_BYTES_STEP) {\n const bytes = new Uint8Array(await source.slice(offset, offset + FILE_BYTES_STEP).arrayBuffer());\n await this.append(id, offset, bytes);\n onProgress?.(offset + bytes.length);\n }\n if (source.size === 0) await this.append(id, 0, new Uint8Array());\n await this.close(id);\n return await this.digest(id);\n } catch (error) {\n await this.remove(id).catch(() => {});\n throw error;\n }\n }\n\n async append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n const size = (await this.size(id)) ?? 0;\n if (offset !== size) throw new Error(`Append at ${offset}, but the file holds ${size} bytes`);\n const handle = await this.writer(id);\n if (bytes.length) await handle.write(bytes);\n }\n\n async flush(id: string): Promise<void> {\n const handle = this.writers.get(this.path(id));\n if (handle) await (await handle).sync();\n }\n\n async close(id: string): Promise<void> {\n const path = this.path(id);\n const handle = this.writers.get(path);\n if (!handle) return;\n this.writers.delete(path);\n const file = await handle;\n await file.sync();\n await file.close();\n }\n\n async size(id: string): Promise<number | null> {\n const path = this.path(id);\n try { return (await stat(path)).size; } catch { return null; }\n }\n\n async truncate(id: string, size: number): Promise<void> {\n await this.close(id);\n const handle = await open(this.path(id), \"r+\");\n try { await handle.truncate(size); await handle.sync(); } finally { await handle.close(); }\n }\n\n async read(id: string, offset: number, length: number): Promise<Uint8Array> {\n const handle = await open(this.path(id), \"r\");\n try {\n const buffer = Buffer.alloc(Math.min(length, 16 * FILE_BYTES_STEP));\n const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);\n return new Uint8Array(buffer.subarray(0, bytesRead));\n } finally { await handle.close(); }\n }\n\n async digest(id: string): Promise<string> {\n const hash = createHash(\"sha256\");\n const handle = await open(this.path(id), \"r\");\n try {\n for await (const chunk of handle.createReadStream({ highWaterMark: FILE_BYTES_STEP })) hash.update(chunk as Buffer);\n } finally { await handle.close().catch(() => {}); }\n return digestText(new Uint8Array(hash.digest()));\n }\n\n /** The file read into memory, to show; a large one only ever goes to disk (`ghostly file save`). */\n async blob(id: string, type: string): Promise<Blob | null> {\n const size = await this.size(id);\n if (size === null || size > 64 * 1024 * 1024) return null;\n const parts: Uint8Array[] = [];\n for (let offset = 0; offset < size; offset += FILE_BYTES_STEP) parts.push(await this.read(id, offset, FILE_BYTES_STEP));\n return new Blob(parts as BlobPart[], { type });\n }\n\n async remove(id: string): Promise<void> {\n await this.close(id).catch(() => {});\n await rm(this.path(id), { force: true });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n if (prefix && !/^[A-Za-z0-9_-]{1,200}$/.test(prefix)) throw new Error(\"Invalid file id\");\n let names: string[];\n try { names = await readdir(this.folder()); } catch { return; }\n await Promise.all(names.filter((name) => name.startsWith(prefix)).map((name) => this.remove(name)));\n }\n\n async room(): Promise<number | null> {\n try {\n await mkdir(this.root, { recursive: true, mode: 0o700 });\n const fs = await statfs(this.root);\n return fs.bavail * fs.bsize;\n } catch { return null; }\n }\n\n async dropSpace(space: string): Promise<void> {\n await rm(this.folder(space), { recursive: true, force: true });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytes-Dbifu5I5.js","names":[],"sources":["../../src/runtime/fileBytes.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, open, readdir, rm, stat, statfs, type FileHandle } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, fileSpace, type FileBytes } from \"@ghostly/browser/shared/fileBytes\";\n\n/**\n * Files as real files in the profile's folder (`files/<space>/<id>`, 0600 in 0700 folders), the Node twin of the\n * Desktop's `native` backend (`shared/fileBytesNative.ts`, which goes through Rust). Registered as `native` and\n * preferred, so every file of any size is a file on disk, read and written a step at a time.\n */\nexport class NodeFileBytes implements FileBytes {\n readonly kind = \"native\" as const;\n private readonly writers = new Map<string, Promise<FileHandle>>();\n\n constructor(private readonly root: string) {}\n\n private folder(space = fileSpace()): string { return join(this.root, space); }\n private path(id: string): string { return join(this.folder(), checkFileId(id)); }\n\n private writer(id: string): Promise<FileHandle> {\n const path = this.path(id);\n let handle = this.writers.get(path);\n if (!handle) {\n handle = mkdir(this.folder(), { recursive: true, mode: 0o700 }).then(() => open(path, \"a\", 0o600));\n this.writers.set(path, handle);\n handle.catch(() => this.writers.delete(path));\n }\n return handle;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n await this.remove(id);\n try {\n for (let offset = 0; offset < source.size; offset += FILE_BYTES_STEP) {\n const bytes = new Uint8Array(await source.slice(offset, offset + FILE_BYTES_STEP).arrayBuffer());\n await this.append(id, offset, bytes);\n onProgress?.(offset + bytes.length);\n }\n if (source.size === 0) await this.append(id, 0, new Uint8Array());\n await this.close(id);\n return await this.digest(id);\n } catch (error) {\n await this.remove(id).catch(() => {});\n throw error;\n }\n }\n\n async append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n const size = (await this.size(id)) ?? 0;\n if (offset !== size) throw new Error(`Append at ${offset}, but the file holds ${size} bytes`);\n const handle = await this.writer(id);\n if (bytes.length) await handle.write(bytes);\n }\n\n async flush(id: string): Promise<void> {\n const handle = this.writers.get(this.path(id));\n if (handle) await (await handle).sync();\n }\n\n async close(id: string): Promise<void> {\n const path = this.path(id);\n const handle = this.writers.get(path);\n if (!handle) return;\n this.writers.delete(path);\n const file = await handle;\n await file.sync();\n await file.close();\n }\n\n async size(id: string): Promise<number | null> {\n const path = this.path(id);\n try { return (await stat(path)).size; } catch { return null; }\n }\n\n async truncate(id: string, size: number): Promise<void> {\n await this.close(id);\n const handle = await open(this.path(id), \"r+\");\n try { await handle.truncate(size); await handle.sync(); } finally { await handle.close(); }\n }\n\n async read(id: string, offset: number, length: number): Promise<Uint8Array> {\n const handle = await open(this.path(id), \"r\");\n try {\n const buffer = Buffer.alloc(Math.min(length, 16 * FILE_BYTES_STEP));\n const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);\n return new Uint8Array(buffer.subarray(0, bytesRead));\n } finally { await handle.close(); }\n }\n\n async digest(id: string): Promise<string> {\n const hash = createHash(\"sha256\");\n const handle = await open(this.path(id), \"r\");\n try {\n for await (const chunk of handle.createReadStream({ highWaterMark: FILE_BYTES_STEP })) hash.update(chunk as Buffer);\n } finally { await handle.close().catch(() => {}); }\n return digestText(new Uint8Array(hash.digest()));\n }\n\n /** The file read into memory, to show; a large one only ever goes to disk (`ghostly file save`). */\n async blob(id: string, type: string): Promise<Blob | null> {\n const size = await this.size(id);\n if (size === null || size > 64 * 1024 * 1024) return null;\n const parts: Uint8Array[] = [];\n for (let offset = 0; offset < size; offset += FILE_BYTES_STEP) parts.push(await this.read(id, offset, FILE_BYTES_STEP));\n return new Blob(parts as BlobPart[], { type });\n }\n\n async remove(id: string): Promise<void> {\n await this.close(id).catch(() => {});\n await rm(this.path(id), { force: true });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n if (prefix && !/^[A-Za-z0-9_-]{1,200}$/.test(prefix)) throw new Error(\"Invalid file id\");\n let names: string[];\n try { names = await readdir(this.folder()); } catch { return; }\n await Promise.all(names.filter((name) => name.startsWith(prefix)).map((name) => this.remove(name)));\n }\n\n async room(): Promise<number | null> {\n try {\n await mkdir(this.root, { recursive: true, mode: 0o700 });\n const fs = await statfs(this.root);\n return fs.bavail * fs.bsize;\n } catch { return null; }\n }\n\n async dropSpace(space: string): Promise<void> {\n await rm(this.folder(space), { recursive: true, force: true });\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { i as STORES, l as openDb, p as wrap } from "./idb-BIdus0WX.js";
2
- import { l as FILE_BYTES_STEP, r as checkFileId, u as digestText } from "./fileBytes-3AQ2tJzW.js";
1
+ import { i as STORES, m as wrap, u as openDb } from "./idb-CTF6LF5B.js";
2
+ import { l as FILE_BYTES_STEP, r as checkFileId, u as digestText } from "./fileBytes-BcYZhNgF.js";
3
3
  import { sha256 } from "@noble/hashes/sha2.js";
4
4
  //#region ../browser/src/shared/fileBytesIdb.ts
5
5
  var STEP = FILE_BYTES_STEP;
@@ -228,4 +228,4 @@ async function storageRoom() {
228
228
  //#endregion
229
229
  export { IdbFileBytes, storageRoom };
230
230
 
231
- //# sourceMappingURL=fileBytesIdb-CmG00qE-.js.map
231
+ //# sourceMappingURL=fileBytesIdb-DzSaqI0A.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytesIdb-CmG00qE-.js","names":[],"sources":["../../../browser/src/shared/fileBytesIdb.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { STORES, openDb, wrap } from \"./idb\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, type FileBytes } from \"./fileBytes\";\n\n/** One piece of a file. Every piece but the last is exactly `FILE_BYTES_STEP` long. */\ninterface Chunk { id: string; index: number; length: number; data: Blob }\n\n/** The piece being filled, kept in memory until it is full or flushed: at most one step per open file. */\ninterface Tail { index: number; bytes: Uint8Array; length: number; dirty: boolean }\n\nconst STEP = FILE_BYTES_STEP;\n\nasync function chunks(mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(STORES.fileChunks, mode).objectStore(STORES.fileChunks);\n}\nconst range = (id: string) => IDBKeyRange.bound([id, 0], [id, Infinity]);\n\n/**\n * Files in IndexedDB, in 1 MiB pieces stored as Blobs (which browsers keep on disk), for platforms without\n * the origin-private file system. Reading one hands out a Blob made of the pieces, so a download never\n * loads the file into memory either.\n */\nexport class IdbFileBytes implements FileBytes {\n readonly kind = \"idb\" as const;\n private readonly tails = new Map<string, Tail>();\n /** Writes to one file run one after another. */\n private readonly queues = new Map<string, Promise<unknown>>();\n\n private serial<T>(id: string, work: () => Promise<T>): Promise<T> {\n const run = (this.queues.get(id) ?? Promise.resolve()).then(work, work);\n const settled = run.catch(() => {});\n this.queues.set(id, settled);\n void settled.then(() => { if (this.queues.get(id) === settled) this.queues.delete(id); });\n return run;\n }\n\n private async list(id: string): Promise<Omit<Chunk, \"data\">[]> {\n const store = await chunks(\"readonly\");\n const keys = (await wrap(store.getAllKeys(range(id)))) as [string, number][];\n if (!keys.length) return [];\n const last = (await wrap(store.get(keys[keys.length - 1]))) as Chunk;\n return keys.map(([, index], i) => ({ id, index, length: i === keys.length - 1 ? last.length : STEP }));\n }\n\n private async put(chunk: Chunk): Promise<void> {\n await wrap((await chunks(\"readwrite\")).put(chunk));\n }\n\n /** The piece being filled, loaded from storage the first time a file is written. */\n private async tail(id: string): Promise<Tail> {\n let tail = this.tails.get(id);\n if (tail) return tail;\n const pieces = await this.list(id);\n const last = pieces[pieces.length - 1];\n tail = { index: 0, bytes: new Uint8Array(STEP), length: 0, dirty: false };\n if (last && last.length < STEP) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, last.index]))) as Chunk;\n tail.index = last.index;\n tail.bytes.set(new Uint8Array(await stored.data.arrayBuffer()));\n tail.length = last.length;\n } else if (last) tail.index = last.index + 1;\n this.tails.set(id, tail);\n return tail;\n }\n\n private async sizeNow(id: string): Promise<number | null> {\n const tail = this.tails.get(id);\n if (tail) return tail.index * STEP + tail.length;\n const pieces = await this.list(id);\n if (!pieces.length) return null;\n return (pieces.length - 1) * STEP + pieces[pieces.length - 1].length;\n }\n\n private async flushNow(id: string): Promise<void> {\n const tail = this.tails.get(id);\n if (!tail?.dirty) return;\n await this.put({ id, index: tail.index, length: tail.length, data: new Blob([tail.bytes.slice(0, tail.length)]) });\n tail.dirty = false;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n checkFileId(id);\n await this.remove(id);\n const hash = sha256.create();\n let offset = 0;\n const reader = source.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hash.update(value);\n await this.append(id, offset, value);\n offset += value.length;\n onProgress?.(offset);\n }\n await this.close(id);\n } catch (error) {\n await reader.cancel().catch(() => {});\n await this.remove(id).catch(() => {});\n throw error;\n }\n return digestText(hash.digest());\n }\n\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n const tail = await this.tail(id);\n if (offset !== tail.index * STEP + tail.length) throw new Error(\"File write out of order\");\n let pos = 0;\n while (pos < bytes.length) {\n const take = Math.min(STEP - tail.length, bytes.length - pos);\n tail.bytes.set(bytes.subarray(pos, pos + take), tail.length);\n tail.length += take; pos += take; tail.dirty = true;\n if (tail.length === STEP) {\n await this.flushNow(id);\n tail.index += 1; tail.length = 0;\n }\n }\n });\n }\n\n flush(id: string): Promise<void> {\n return this.serial(id, () => this.flushNow(id));\n }\n\n close(id: string): Promise<void> {\n return this.serial(id, async () => { await this.flushNow(id); this.tails.delete(id); });\n }\n\n size(id: string): Promise<number | null> {\n checkFileId(id);\n return this.serial(id, () => this.sizeNow(id));\n }\n\n truncate(id: string, size: number): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n this.tails.delete(id);\n const pieces = await this.list(id);\n const keep = Math.floor(size / STEP), rest = size % STEP;\n const store = await chunks(\"readwrite\");\n await Promise.all(pieces.filter((piece) => piece.index > keep || (piece.index === keep && rest === 0))\n .map((piece) => wrap(store.delete([id, piece.index]))));\n const cut = pieces.find((piece) => piece.index === keep);\n if (cut && rest > 0 && cut.length > rest) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, keep]))) as Chunk;\n await this.put({ id, index: keep, length: rest, data: stored.data.slice(0, rest) });\n }\n });\n }\n\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n const out: Uint8Array[] = [];\n const store = await chunks(\"readonly\");\n const end = offset + length;\n for (let index = Math.floor(offset / STEP); index * STEP < end; index++) {\n const piece = (await wrap(store.get([id, index]))) as Chunk | undefined;\n if (!piece) break;\n const from = Math.max(0, offset - index * STEP), to = Math.min(piece.length, end - index * STEP);\n if (from < to) out.push(new Uint8Array(await piece.data.slice(from, to).arrayBuffer()));\n if (piece.length < STEP) break;\n }\n if (out.length === 1) return out[0];\n const joined = new Uint8Array(out.reduce((n, part) => n + part.length, 0));\n let pos = 0;\n for (const part of out) { joined.set(part, pos); pos += part.length; }\n return joined;\n });\n }\n\n async digest(id: string): Promise<string> {\n const hash = sha256.create();\n for await (const part of this.pieces(id)) hash.update(new Uint8Array(await part.arrayBuffer()));\n return digestText(hash.digest());\n }\n\n private async *pieces(id: string): AsyncGenerator<Blob> {\n checkFileId(id);\n await this.flush(id);\n const pieces = await this.list(id);\n for (const piece of pieces) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, piece.index]))) as Chunk | undefined;\n if (stored) yield stored.data;\n }\n }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n const parts: Blob[] = [];\n for await (const part of this.pieces(id)) parts.push(part);\n return parts.length ? new Blob(parts, { type }) : (await this.size(id)) === 0 ? new Blob([], { type }) : null;\n }\n\n remove(id: string): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n this.tails.delete(id);\n await wrap((await chunks(\"readwrite\")).delete(range(id)));\n });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n for (const id of [...this.tails.keys()]) if (id.startsWith(prefix)) this.tails.delete(id);\n const store = await chunks(\"readwrite\");\n await wrap(prefix ? store.delete(IDBKeyRange.bound([prefix, 0], [`${prefix}￿`, Infinity])) : store.clear());\n }\n\n async room(): Promise<number | null> {\n return storageRoom();\n }\n}\n\n/** What the browser still lets this origin store, or null when it does not say. */\nexport async function storageRoom(): Promise<number | null> {\n try {\n const estimate = await navigator.storage?.estimate?.();\n if (!estimate?.quota) return null;\n return Math.max(0, estimate.quota - (estimate.usage ?? 0));\n } catch { return null; }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytesIdb-DzSaqI0A.js","names":[],"sources":["../../../browser/src/shared/fileBytesIdb.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2.js\";\nimport { STORES, openDb, wrap } from \"./idb\";\nimport { FILE_BYTES_STEP, checkFileId, digestText, type FileBytes } from \"./fileBytes\";\n\n/** One piece of a file. Every piece but the last is exactly `FILE_BYTES_STEP` long. */\ninterface Chunk { id: string; index: number; length: number; data: Blob }\n\n/** The piece being filled, kept in memory until it is full or flushed: at most one step per open file. */\ninterface Tail { index: number; bytes: Uint8Array; length: number; dirty: boolean }\n\nconst STEP = FILE_BYTES_STEP;\n\nasync function chunks(mode: IDBTransactionMode): Promise<IDBObjectStore> {\n return (await openDb()).transaction(STORES.fileChunks, mode).objectStore(STORES.fileChunks);\n}\nconst range = (id: string) => IDBKeyRange.bound([id, 0], [id, Infinity]);\n\n/**\n * Files in IndexedDB, in 1 MiB pieces stored as Blobs (which browsers keep on disk), for platforms without\n * the origin-private file system. Reading one hands out a Blob made of the pieces, so a download never\n * loads the file into memory either.\n */\nexport class IdbFileBytes implements FileBytes {\n readonly kind = \"idb\" as const;\n private readonly tails = new Map<string, Tail>();\n /** Writes to one file run one after another. */\n private readonly queues = new Map<string, Promise<unknown>>();\n\n private serial<T>(id: string, work: () => Promise<T>): Promise<T> {\n const run = (this.queues.get(id) ?? Promise.resolve()).then(work, work);\n const settled = run.catch(() => {});\n this.queues.set(id, settled);\n void settled.then(() => { if (this.queues.get(id) === settled) this.queues.delete(id); });\n return run;\n }\n\n private async list(id: string): Promise<Omit<Chunk, \"data\">[]> {\n const store = await chunks(\"readonly\");\n const keys = (await wrap(store.getAllKeys(range(id)))) as [string, number][];\n if (!keys.length) return [];\n const last = (await wrap(store.get(keys[keys.length - 1]))) as Chunk;\n return keys.map(([, index], i) => ({ id, index, length: i === keys.length - 1 ? last.length : STEP }));\n }\n\n private async put(chunk: Chunk): Promise<void> {\n await wrap((await chunks(\"readwrite\")).put(chunk));\n }\n\n /** The piece being filled, loaded from storage the first time a file is written. */\n private async tail(id: string): Promise<Tail> {\n let tail = this.tails.get(id);\n if (tail) return tail;\n const pieces = await this.list(id);\n const last = pieces[pieces.length - 1];\n tail = { index: 0, bytes: new Uint8Array(STEP), length: 0, dirty: false };\n if (last && last.length < STEP) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, last.index]))) as Chunk;\n tail.index = last.index;\n tail.bytes.set(new Uint8Array(await stored.data.arrayBuffer()));\n tail.length = last.length;\n } else if (last) tail.index = last.index + 1;\n this.tails.set(id, tail);\n return tail;\n }\n\n private async sizeNow(id: string): Promise<number | null> {\n const tail = this.tails.get(id);\n if (tail) return tail.index * STEP + tail.length;\n const pieces = await this.list(id);\n if (!pieces.length) return null;\n return (pieces.length - 1) * STEP + pieces[pieces.length - 1].length;\n }\n\n private async flushNow(id: string): Promise<void> {\n const tail = this.tails.get(id);\n if (!tail?.dirty) return;\n await this.put({ id, index: tail.index, length: tail.length, data: new Blob([tail.bytes.slice(0, tail.length)]) });\n tail.dirty = false;\n }\n\n async stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n checkFileId(id);\n await this.remove(id);\n const hash = sha256.create();\n let offset = 0;\n const reader = source.stream().getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n hash.update(value);\n await this.append(id, offset, value);\n offset += value.length;\n onProgress?.(offset);\n }\n await this.close(id);\n } catch (error) {\n await reader.cancel().catch(() => {});\n await this.remove(id).catch(() => {});\n throw error;\n }\n return digestText(hash.digest());\n }\n\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n const tail = await this.tail(id);\n if (offset !== tail.index * STEP + tail.length) throw new Error(\"File write out of order\");\n let pos = 0;\n while (pos < bytes.length) {\n const take = Math.min(STEP - tail.length, bytes.length - pos);\n tail.bytes.set(bytes.subarray(pos, pos + take), tail.length);\n tail.length += take; pos += take; tail.dirty = true;\n if (tail.length === STEP) {\n await this.flushNow(id);\n tail.index += 1; tail.length = 0;\n }\n }\n });\n }\n\n flush(id: string): Promise<void> {\n return this.serial(id, () => this.flushNow(id));\n }\n\n close(id: string): Promise<void> {\n return this.serial(id, async () => { await this.flushNow(id); this.tails.delete(id); });\n }\n\n size(id: string): Promise<number | null> {\n checkFileId(id);\n return this.serial(id, () => this.sizeNow(id));\n }\n\n truncate(id: string, size: number): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n this.tails.delete(id);\n const pieces = await this.list(id);\n const keep = Math.floor(size / STEP), rest = size % STEP;\n const store = await chunks(\"readwrite\");\n await Promise.all(pieces.filter((piece) => piece.index > keep || (piece.index === keep && rest === 0))\n .map((piece) => wrap(store.delete([id, piece.index]))));\n const cut = pieces.find((piece) => piece.index === keep);\n if (cut && rest > 0 && cut.length > rest) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, keep]))) as Chunk;\n await this.put({ id, index: keep, length: rest, data: stored.data.slice(0, rest) });\n }\n });\n }\n\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n checkFileId(id);\n return this.serial(id, async () => {\n await this.flushNow(id);\n const out: Uint8Array[] = [];\n const store = await chunks(\"readonly\");\n const end = offset + length;\n for (let index = Math.floor(offset / STEP); index * STEP < end; index++) {\n const piece = (await wrap(store.get([id, index]))) as Chunk | undefined;\n if (!piece) break;\n const from = Math.max(0, offset - index * STEP), to = Math.min(piece.length, end - index * STEP);\n if (from < to) out.push(new Uint8Array(await piece.data.slice(from, to).arrayBuffer()));\n if (piece.length < STEP) break;\n }\n if (out.length === 1) return out[0];\n const joined = new Uint8Array(out.reduce((n, part) => n + part.length, 0));\n let pos = 0;\n for (const part of out) { joined.set(part, pos); pos += part.length; }\n return joined;\n });\n }\n\n async digest(id: string): Promise<string> {\n const hash = sha256.create();\n for await (const part of this.pieces(id)) hash.update(new Uint8Array(await part.arrayBuffer()));\n return digestText(hash.digest());\n }\n\n private async *pieces(id: string): AsyncGenerator<Blob> {\n checkFileId(id);\n await this.flush(id);\n const pieces = await this.list(id);\n for (const piece of pieces) {\n const stored = (await wrap((await chunks(\"readonly\")).get([id, piece.index]))) as Chunk | undefined;\n if (stored) yield stored.data;\n }\n }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n const parts: Blob[] = [];\n for await (const part of this.pieces(id)) parts.push(part);\n return parts.length ? new Blob(parts, { type }) : (await this.size(id)) === 0 ? new Blob([], { type }) : null;\n }\n\n remove(id: string): Promise<void> {\n checkFileId(id);\n return this.serial(id, async () => {\n this.tails.delete(id);\n await wrap((await chunks(\"readwrite\")).delete(range(id)));\n });\n }\n\n async removeWhere(prefix: string): Promise<void> {\n for (const id of [...this.tails.keys()]) if (id.startsWith(prefix)) this.tails.delete(id);\n const store = await chunks(\"readwrite\");\n await wrap(prefix ? store.delete(IDBKeyRange.bound([prefix, 0], [`${prefix}￿`, Infinity])) : store.clear());\n }\n\n async room(): Promise<number | null> {\n return storageRoom();\n }\n}\n\n/** What the browser still lets this origin store, or null when it does not say. */\nexport async function storageRoom(): Promise<number | null> {\n try {\n const estimate = await navigator.storage?.estimate?.();\n if (!estimate?.quota) return null;\n return Math.max(0, estimate.quota - (estimate.usage ?? 0));\n } catch { return null; }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace } from "./fileBytes-3AQ2tJzW.js";
2
- import { storageRoom } from "./fileBytesIdb-CmG00qE-.js";
1
+ import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace } from "./fileBytes-BcYZhNgF.js";
2
+ import { storageRoom } from "./fileBytesIdb-DzSaqI0A.js";
3
3
  //#region ../browser/src/shared/fileBytesOpfs.ts
4
4
  /**
5
5
  * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one
@@ -154,4 +154,4 @@ var OpfsFileBytes = class OpfsFileBytes {
154
154
  //#endregion
155
155
  export { OpfsFileBytes };
156
156
 
157
- //# sourceMappingURL=fileBytesOpfs-CTgqt-p3.js.map
157
+ //# sourceMappingURL=fileBytesOpfs-Dx8RZiV5.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fileBytesOpfs-CTgqt-p3.js","names":[],"sources":["../../../browser/src/shared/fileBytesOpfs.ts"],"sourcesContent":["import { FILE_BYTES_STEP, checkFileId, fileSpace, type FileBytes } from \"./fileBytes\";\nimport { storageRoom } from \"./fileBytesIdb\";\nimport type { WorkerReply, WorkerRequest } from \"./fileBytesWorker\";\n\ntype Request = WorkerRequest extends infer R ? R extends unknown ? Omit<R, \"n\" | \"space\"> : never : never;\n\n/**\n * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one\n * profile see the same folder; a file is written by one of them at a time (the page copies a file it sends,\n * the peer writes one it receives), and read as a Blob only once it is closed.\n */\nexport class OpfsFileBytes implements FileBytes {\n readonly kind = \"opfs\" as const;\n /** Shared with the views `forSpace` makes: one worker, one run of request numbers. */\n private readonly counter = { next: 1 };\n /** Another profile's folder (`forSpace`); the active profile's when not set. */\n private fixedSpace?: string;\n private readonly waiting = new Map<number, { resolve(value: unknown): void; reject(error: Error): void; progress?(n: number): void }>();\n\n private constructor(private readonly worker: Worker) {\n worker.onmessage = (event: MessageEvent<WorkerReply>) => {\n const reply = event.data, pending = this.waiting.get(reply.n);\n if (!pending) return;\n if (\"progress\" in reply) return pending.progress?.(reply.progress);\n this.waiting.delete(reply.n);\n // An error keeps the name the worker gave it: \"no room left\" is told from any other failure by its name.\n if (reply.ok) pending.resolve(reply.value); else pending.reject(reply.name ? Object.assign(new Error(reply.error), { name: reply.name }) : new Error(reply.error));\n };\n worker.onerror = (event) => {\n event.preventDefault();\n for (const pending of this.waiting.values()) pending.reject(new Error(`File storage stopped: ${event.message || \"unknown error\"}`));\n this.waiting.clear();\n };\n }\n\n /** The worker, if this browser can run it and gives it sync access handles; null otherwise. */\n static async open(): Promise<OpfsFileBytes | null> {\n if (typeof Worker === \"undefined\" || typeof navigator === \"undefined\" || !navigator.storage?.getDirectory) return null;\n const store = new OpfsFileBytes(new Worker(new URL(\"./fileBytesWorker.ts\", import.meta.url), { type: \"module\" }));\n try {\n if (await store.call<boolean>({ op: \"probe\" })) return store;\n } catch { /* no OPFS here (a private window, an old engine) */ }\n store.worker.terminate();\n return null;\n }\n\n private space(): string { return this.fixedSpace ?? fileSpace(); }\n\n /** A view on the same worker that reads and writes another profile's folder. */\n forSpace(space: string): OpfsFileBytes {\n const view = Object.create(this) as OpfsFileBytes;\n view.fixedSpace = space;\n return view;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.counter.next++;\n return new Promise<T>((resolve, reject) => {\n this.waiting.set(n, { resolve: resolve as (value: unknown) => void, reject, progress });\n this.worker.postMessage({ ...request, n, space: this.space() } as WorkerRequest, transfer);\n });\n }\n\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n return this.call({ op: \"stage\", id: checkFileId(id), source }, [], onProgress);\n }\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n // A copy goes to the worker: the caller's buffer may be a view into something it still uses.\n const copy = bytes.slice();\n return this.call({ op: \"append\", id: checkFileId(id), offset, bytes: copy }, [copy.buffer]);\n }\n flush(id: string): Promise<void> { return this.call({ op: \"flush\", id: checkFileId(id) }); }\n close(id: string): Promise<void> { return this.call({ op: \"close\", id: checkFileId(id) }); }\n size(id: string): Promise<number | null> { return this.call({ op: \"size\", id: checkFileId(id) }); }\n truncate(id: string, size: number): Promise<void> { return this.call({ op: \"truncate\", id: checkFileId(id), size }); }\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n return this.call({ op: \"read\", id: checkFileId(id), offset, length: Math.min(length, 16 * FILE_BYTES_STEP) });\n }\n digest(id: string): Promise<string> { return this.call({ op: \"digest\", id: checkFileId(id) }); }\n remove(id: string): Promise<void> { return this.call({ op: \"remove\", id: checkFileId(id) }); }\n removeWhere(prefix: string): Promise<void> { return this.call({ op: \"removeWhere\", prefix }); }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n // Closed first: a file with a sync access handle open cannot be read as a Blob.\n await this.close(id);\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n const file = await (await (await root.getDirectoryHandle(this.space())).getFileHandle(checkFileId(id))).getFile();\n // A slice with a type: still the file on disk, never read into memory.\n return file.slice(0, file.size, type);\n } catch { return null; }\n }\n\n room(): Promise<number | null> { return storageRoom(); }\n\n async dropSpace(space: string): Promise<void> {\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n await root.removeEntry(space, { recursive: true });\n } catch (error) { if ((error as { name?: string })?.name !== \"NotFoundError\") throw error; }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fileBytesOpfs-Dx8RZiV5.js","names":[],"sources":["../../../browser/src/shared/fileBytesOpfs.ts"],"sourcesContent":["import { FILE_BYTES_STEP, checkFileId, fileSpace, type FileBytes } from \"./fileBytes\";\nimport { storageRoom } from \"./fileBytesIdb\";\nimport type { WorkerReply, WorkerRequest } from \"./fileBytesWorker\";\n\ntype Request = WorkerRequest extends infer R ? R extends unknown ? Omit<R, \"n\" | \"space\"> : never : never;\n\n/**\n * Files in the origin-private file system, written by `fileBytesWorker.ts`. Every page and the peer of one\n * profile see the same folder; a file is written by one of them at a time (the page copies a file it sends,\n * the peer writes one it receives), and read as a Blob only once it is closed.\n */\nexport class OpfsFileBytes implements FileBytes {\n readonly kind = \"opfs\" as const;\n /** Shared with the views `forSpace` makes: one worker, one run of request numbers. */\n private readonly counter = { next: 1 };\n /** Another profile's folder (`forSpace`); the active profile's when not set. */\n private fixedSpace?: string;\n private readonly waiting = new Map<number, { resolve(value: unknown): void; reject(error: Error): void; progress?(n: number): void }>();\n\n private constructor(private readonly worker: Worker) {\n worker.onmessage = (event: MessageEvent<WorkerReply>) => {\n const reply = event.data, pending = this.waiting.get(reply.n);\n if (!pending) return;\n if (\"progress\" in reply) return pending.progress?.(reply.progress);\n this.waiting.delete(reply.n);\n // An error keeps the name the worker gave it: \"no room left\" is told from any other failure by its name.\n if (reply.ok) pending.resolve(reply.value); else pending.reject(reply.name ? Object.assign(new Error(reply.error), { name: reply.name }) : new Error(reply.error));\n };\n worker.onerror = (event) => {\n event.preventDefault();\n for (const pending of this.waiting.values()) pending.reject(new Error(`File storage stopped: ${event.message || \"unknown error\"}`));\n this.waiting.clear();\n };\n }\n\n /** The worker, if this browser can run it and gives it sync access handles; null otherwise. */\n static async open(): Promise<OpfsFileBytes | null> {\n if (typeof Worker === \"undefined\" || typeof navigator === \"undefined\" || !navigator.storage?.getDirectory) return null;\n const store = new OpfsFileBytes(new Worker(new URL(\"./fileBytesWorker.ts\", import.meta.url), { type: \"module\" }));\n try {\n if (await store.call<boolean>({ op: \"probe\" })) return store;\n } catch { /* no OPFS here (a private window, an old engine) */ }\n store.worker.terminate();\n return null;\n }\n\n private space(): string { return this.fixedSpace ?? fileSpace(); }\n\n /** A view on the same worker that reads and writes another profile's folder. */\n forSpace(space: string): OpfsFileBytes {\n const view = Object.create(this) as OpfsFileBytes;\n view.fixedSpace = space;\n return view;\n }\n\n private call<T>(request: Request, transfer: Transferable[] = [], progress?: (n: number) => void): Promise<T> {\n const n = this.counter.next++;\n return new Promise<T>((resolve, reject) => {\n this.waiting.set(n, { resolve: resolve as (value: unknown) => void, reject, progress });\n this.worker.postMessage({ ...request, n, space: this.space() } as WorkerRequest, transfer);\n });\n }\n\n stage(id: string, source: Blob, onProgress?: (copied: number) => void): Promise<string> {\n return this.call({ op: \"stage\", id: checkFileId(id), source }, [], onProgress);\n }\n append(id: string, offset: number, bytes: Uint8Array): Promise<void> {\n // A copy goes to the worker: the caller's buffer may be a view into something it still uses.\n const copy = bytes.slice();\n return this.call({ op: \"append\", id: checkFileId(id), offset, bytes: copy }, [copy.buffer]);\n }\n flush(id: string): Promise<void> { return this.call({ op: \"flush\", id: checkFileId(id) }); }\n close(id: string): Promise<void> { return this.call({ op: \"close\", id: checkFileId(id) }); }\n size(id: string): Promise<number | null> { return this.call({ op: \"size\", id: checkFileId(id) }); }\n truncate(id: string, size: number): Promise<void> { return this.call({ op: \"truncate\", id: checkFileId(id), size }); }\n read(id: string, offset: number, length: number): Promise<Uint8Array> {\n return this.call({ op: \"read\", id: checkFileId(id), offset, length: Math.min(length, 16 * FILE_BYTES_STEP) });\n }\n digest(id: string): Promise<string> { return this.call({ op: \"digest\", id: checkFileId(id) }); }\n remove(id: string): Promise<void> { return this.call({ op: \"remove\", id: checkFileId(id) }); }\n removeWhere(prefix: string): Promise<void> { return this.call({ op: \"removeWhere\", prefix }); }\n\n async blob(id: string, type: string): Promise<Blob | null> {\n // Closed first: a file with a sync access handle open cannot be read as a Blob.\n await this.close(id);\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n const file = await (await (await root.getDirectoryHandle(this.space())).getFileHandle(checkFileId(id))).getFile();\n // A slice with a type: still the file on disk, never read into memory.\n return file.slice(0, file.size, type);\n } catch { return null; }\n }\n\n room(): Promise<number | null> { return storageRoom(); }\n\n async dropSpace(space: string): Promise<void> {\n try {\n const root = await (await navigator.storage.getDirectory()).getDirectoryHandle(\"ghostly-files\");\n await root.removeEntry(space, { recursive: true });\n } catch (error) { if ((error as { name?: string })?.name !== \"NotFoundError\") throw error; }\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { Hc as fromBase64Url } from "./src-Cp3o_yHN.js";
2
- import { n as hyperdhtRelayProblem } from "./server-GJW_5wGI.js";
1
+ import { Gc as fromBase64Url } from "./src-BMUcYQ7b.js";
2
+ import { n as hyperdhtRelayProblem } from "./server-BGXmSYx_.js";
3
3
  import { ed25519 } from "@noble/curves/ed25519.js";
4
4
  //#region ../browser/src/platform/hyperdhtRelay.ts
5
5
  /**
@@ -339,4 +339,4 @@ async function createRelayedHyperEndpoint(seedB64, url, options = {}) {
339
339
  //#endregion
340
340
  export { createRelayedHyperEndpoint };
341
341
 
342
- //# sourceMappingURL=hyperdhtRelay-DvXgebV7.js.map
342
+ //# sourceMappingURL=hyperdhtRelay-DmTrpmba.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hyperdhtRelay-DvXgebV7.js","names":[],"sources":["../../../browser/src/platform/hyperdhtRelay.ts"],"sourcesContent":["import { ed25519 } from \"@noble/curves/ed25519.js\";\nimport { fromBase64Url, type BoundChannel, type FrameChannel, type NativeEndpoint } from \"@ghostly/core\";\nimport { hyperdhtRelayProblem } from \"../shared/hyperdhtRelay\";\n\nexport { hyperdhtRelayProblem as relayUrlProblem };\n\n/**\n * HyperDHT (WISP 103) for a browser, through a dht-relay: a server that runs the UDP half of HyperDHT and\n * talks to the browser over one WebSocket. It runs non-custodial: the browser keeps its keys, runs the\n * Noise handshake and the encrypted stream itself, and the relay moves handshake messages and ciphertext.\n * The binding (the handshake hash and both keys) is computed here, at the endpoint, as the Desktop does.\n *\n * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how\n * much it sends. Not the frames, not the keys' secrets, not the handshake hash.\n *\n * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed\n * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.\n */\n\nconst PREFACE = \"ghostly/paired-chat/1\";\nconst MAX_FRAME = 60 * 1024;\nconst SEND_BUDGET = 120 * 1024;\nconst HANDSHAKE_MS = 20_000;\nconst RELAY_OPEN_MS = 10_000;\nconst LISTEN_MS = 20_000;\nconst MAX_CHANNELS = 2;\n/**\n * How the relay client is always made: never custodial. dht-relay's default is custodial, which sends every\n * secret key to the relay so it can run the handshake and the stream itself. Nothing here takes this from\n * settings or options; test/hyperdhtRelay.test.ts fails if a secret key ever reaches the relay's wire.\n */\nconst NON_CUSTODIAL = Object.freeze({ custodial: false as const });\n\n/**\n * The descriptor a browser's HyperDHT endpoint gives out: its key, that it is reached only through a relay, and\n * which one (the connection panel names its host; a Desktop dialling it ignores both).\n */\nexport interface RelayedHyperDescriptor { publicKey: string; relayed: true; relay: string }\n\n/** The parts of dht-relay and its streams this adapter uses. */\ninterface SecretSocket {\n handshakeHash: Uint8Array | null;\n publicKey: Uint8Array;\n remotePublicKey: Uint8Array;\n opened?: Promise<boolean>;\n destroyed?: boolean;\n write(data: Uint8Array): boolean;\n destroy(error?: Error): void;\n on(event: string, listener: (...args: never[]) => void): unknown;\n once(event: string, listener: (...args: never[]) => void): unknown;\n off(event: string, listener: (...args: never[]) => void): unknown;\n}\ninterface RelayedServer {\n listen(keyPair: KeyPair): Promise<void>;\n close(): Promise<void>;\n}\ninterface RelayedNode {\n ready(): Promise<void>;\n connect(remotePublicKey: Uint8Array, options: { keyPair: KeyPair }): SecretSocket;\n createServer(options: object, onconnection: (socket: SecretSocket) => void): RelayedServer;\n destroy(): Promise<void>;\n}\ninterface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array }\n\nconst hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst fromHex = (text: string) => Uint8Array.from(text.match(/../g)!, byte => parseInt(byte, 16));\n\n/** HyperDHT's key pair for a seed: libsodium's layout (the seed, then the public key). */\nexport function hyperKeyPair(seed: Uint8Array): KeyPair {\n if (seed.length !== 32) throw new Error(\"Invalid HyperDHT seed\");\n const publicKey = ed25519.getPublicKey(seed);\n const secretKey = new Uint8Array(64);\n secretKey.set(seed); secretKey.set(publicKey, 32);\n return { publicKey, secretKey };\n}\n\n/**\n * One WebSocket per relay for the whole app, shared by every chat's endpoint (each listens on its own\n * key). It closes when the last endpoint does. When the relay goes away, every endpoint on it says it is\n * unavailable, and the next one to start opens a new connection.\n */\ninterface RelayClient {\n node: RelayedNode;\n endpoints: Set<{ unavailable(): void }>;\n gone: boolean;\n /** Settles when the relay connection closes. */\n closed: Promise<void>;\n}\nconst clients = new Map<string, Promise<RelayClient>>();\n\nasync function relayClient(url: string, createSocket: (url: string) => WebSocket): Promise<RelayClient> {\n const existing = clients.get(url);\n if (existing) {\n const client = await existing.catch(() => null);\n if (client && !client.gone) return client;\n if (clients.get(url) === existing) clients.delete(url);\n }\n const opening = (async () => {\n const [{ default: RelayedDHT }, { default: WebSocketStream }] = await Promise.all([\n // @ts-expect-error dht-relay ships no types: RelayedNode and SecretSocket above describe what this uses.\n import(\"@hyperswarm/dht-relay\"),\n // @ts-expect-error The same package's WebSocket stream, untyped too.\n import(\"@hyperswarm/dht-relay/ws\"),\n ]);\n const socket = createSocket(url);\n socket.binaryType = \"arraybuffer\";\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => { socket.close(); reject(new Error(\"The HyperDHT relay did not answer\")); }, RELAY_OPEN_MS);\n socket.addEventListener(\"open\", () => { clearTimeout(timer); resolve(); }, { once: true });\n socket.addEventListener(\"error\", () => { clearTimeout(timer); reject(new Error(\"Could not reach the HyperDHT relay\")); }, { once: true });\n socket.addEventListener(\"close\", () => { clearTimeout(timer); reject(new Error(\"The HyperDHT relay closed the connection\")); }, { once: true });\n });\n // Non-custodial: the relay is told public keys only, never a secret key (dht-relay's default sends them).\n // The node's own key is never used to listen or dial: a random one says nothing about this app.\n const node = new RelayedDHT(new WebSocketStream(true, socket), { ...NON_CUSTODIAL, keyPair: hyperKeyPair(crypto.getRandomValues(new Uint8Array(32))) }) as RelayedNode;\n let closed!: () => void;\n const client: RelayClient = { node, endpoints: new Set(), gone: false, closed: new Promise(resolve => { closed = resolve; }) };\n socket.addEventListener(\"close\", () => {\n client.gone = true; closed();\n if (clients.get(url) === opening) clients.delete(url);\n for (const endpoint of [...client.endpoints]) endpoint.unavailable();\n });\n return client;\n })();\n clients.set(url, opening);\n opening.catch(() => { if (clients.get(url) === opening) clients.delete(url); });\n return opening;\n}\n\nfunction release(url: string, client: RelayClient, endpoint: { unavailable(): void }): void {\n client.endpoints.delete(endpoint);\n if (client.endpoints.size || client.gone) return;\n client.gone = true;\n void clients.get(url)?.then(current => { if (current === client) clients.delete(url); }).catch(() => {});\n void client.node.destroy().catch(() => {});\n}\n\n/** One authenticated stream as a frame channel, with a bounded queue before the session installs its reader. */\nclass RelayedChannel implements FrameChannel {\n private reader: FrameChannel[\"onMessage\"] = null;\n private buffered: string[] = [];\n private closed = false;\n private pendingBytes = 0;\n onClose: FrameChannel[\"onClose\"] = null;\n constructor(private readonly socket: SecretSocket) {\n socket.on(\"drain\", () => { this.pendingBytes = 0; });\n }\n get bufferedAmount(): number { return this.pendingBytes; }\n readonly sendBudget = SEND_BUDGET;\n get onMessage() { return this.reader; }\n set onMessage(reader: FrameChannel[\"onMessage\"]) {\n this.reader = reader;\n if (reader) for (const text of this.buffered.splice(0)) reader(text);\n }\n receive(text: string): boolean {\n if (this.closed) return true;\n if (this.reader) this.reader(text);\n else if (this.buffered.length < 64) this.buffered.push(text);\n else return false;\n return true;\n }\n send(data: string | Uint8Array): void {\n if (typeof data !== \"string\" || this.closed || !data.length) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n const payload = new TextEncoder().encode(data);\n if (payload.length > MAX_FRAME || this.pendingBytes + payload.length > SEND_BUDGET) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n // The stream takes every write; past its high-water mark it says so until it drains.\n if (!writeFrame(this.socket, payload) || this.pendingBytes) this.pendingBytes += payload.length + 4;\n }\n drained(): Promise<void> {\n if (!this.pendingBytes) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const done = () => { this.socket.off(\"close\", gone); resolve(); };\n const gone = () => { this.socket.off(\"drain\", done); reject(new Error(\"Native channel closed\")); };\n this.socket.once(\"drain\", done); this.socket.once(\"close\", gone);\n });\n }\n closedByPeer(): void {\n if (this.closed) return;\n this.closed = true; this.buffered = [];\n this.onClose?.();\n }\n close(): void {\n if (this.closed) return;\n this.socket.destroy();\n this.closedByPeer();\n }\n}\n\nfunction writeFrame(socket: SecretSocket, payload: Uint8Array): boolean {\n const frame = new Uint8Array(4 + payload.length);\n new DataView(frame.buffer).setUint32(0, payload.length);\n frame.set(payload, 4);\n return socket.write(frame);\n}\n\n/**\n * Waits for the peer's preface on an authenticated stream (sending ours as soon as the handshake is\n * done), then hands the channel over with the binding the stream itself gives. A frame of the wrong size,\n * bytes that are not UTF-8 or another protocol close the stream.\n */\nfunction attach(socket: SecretSocket, channels: Set<RelayedChannel>): Promise<BoundChannel> {\n if (channels.size >= MAX_CHANNELS) { socket.destroy(); return Promise.reject(new Error(\"Native connection limit reached\")); }\n const channel = new RelayedChannel(socket);\n channels.add(channel);\n return new Promise<BoundChannel>((resolve, reject) => {\n let buffer = new Uint8Array(0);\n let prefaced = false;\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const timer = setTimeout(() => socket.destroy(new Error(\"HyperDHT handshake timed out\")), HANDSHAKE_MS);\n const fail = (error: Error) => { socket.destroy(error); };\n socket.on(\"error\", (error: Error) => reject(error));\n socket.on(\"close\", () => {\n clearTimeout(timer); channels.delete(channel);\n reject(new Error(\"Native channel closed\"));\n channel.closedByPeer();\n });\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n fail(new Error(\"Noise session binding unavailable\")); return;\n }\n writeFrame(socket, new TextEncoder().encode(PREFACE));\n };\n if (socket.opened) void socket.opened.then(ok => { if (ok) opened(); else reject(new Error(\"Noise handshake failed\")); });\n else socket.once(\"open\", opened);\n socket.on(\"data\", (chunk: Uint8Array) => {\n const joined = new Uint8Array(buffer.length + chunk.length);\n joined.set(buffer); joined.set(chunk, buffer.length);\n buffer = joined;\n while (buffer.length >= 4) {\n const length = new DataView(buffer.buffer, buffer.byteOffset, 4).getUint32(0);\n if (length === 0 || length > MAX_FRAME) { fail(new Error(\"Invalid frame length\")); return; }\n if (buffer.length < 4 + length) break;\n let text: string;\n try { text = decoder.decode(buffer.subarray(4, 4 + length)); }\n catch { fail(new Error(\"Invalid UTF-8 frame\")); return; }\n buffer = buffer.slice(4 + length);\n if (!prefaced) {\n if (text !== PREFACE) { fail(new Error(\"Unexpected application protocol\")); return; }\n prefaced = true; clearTimeout(timer);\n resolve({ channel, binding: { transport: \"hyperdht/1\", context: hex(socket.handshakeHash!),\n identities: [hex(socket.publicKey), hex(socket.remotePublicKey)].sort() as [string, string] } });\n } else if (!channel.receive(text)) { fail(new Error(\"Receive queue exceeded\")); return; }\n }\n });\n });\n}\n\nexport interface RelayedHyperOptions {\n /** How to open the WebSocket (tests pass Node's `ws`). Default: the browser's. */\n createSocket?: (url: string) => WebSocket;\n}\n\n/**\n * A chat's HyperDHT endpoint through the relay at `url`: it listens on the key of `seedB64` (the chat's\n * own transport seed, as on the Desktop) and dials a contact's descriptor. Fails, and so is not offered,\n * when the relay cannot be reached or does not let it listen.\n */\nexport async function createRelayedHyperEndpoint(seedB64: string, url: string, options: RelayedHyperOptions = {}): Promise<NativeEndpoint> {\n const problem = hyperdhtRelayProblem(url);\n if (problem) throw new Error(problem);\n const keyPair = hyperKeyPair(fromBase64Url(seedB64));\n const client = await relayClient(url, options.createSocket ?? (address => new WebSocket(address)));\n const channels = new Set<RelayedChannel>();\n let stopped = false;\n let handler: NativeEndpoint[\"onConnection\"] = null;\n const waiting: BoundChannel[] = [];\n let endpoint: NativeEndpoint | null = null;\n const member = {\n unavailable() {\n if (stopped) return;\n stopped = true;\n for (const channel of channels) channel.close();\n endpoint?.onUnavailable?.();\n },\n };\n client.endpoints.add(member);\n const server = client.node.createServer({}, socket => {\n void attach(socket, channels).then(bound => {\n if (stopped) bound.channel.close();\n else if (handler) handler(bound);\n else if (waiting.length < MAX_CHANNELS) waiting.push(bound);\n else bound.channel.close();\n }).catch(() => {});\n });\n try {\n await Promise.race([\n server.listen(keyPair),\n client.closed.then(() => { throw new Error(\"The HyperDHT relay closed the connection\"); }),\n new Promise((_, reject) => setTimeout(() => reject(new Error(\"The HyperDHT relay did not let this chat listen\")), LISTEN_MS)),\n ]);\n if (client.gone) throw new Error(\"The HyperDHT relay closed the connection\");\n } catch (error) {\n stopped = true;\n void server.close().catch(() => {});\n release(url, client, member);\n throw error;\n }\n const descriptor: RelayedHyperDescriptor = { publicKey: hex(keyPair.publicKey), relayed: true, relay: url };\n endpoint = {\n transport: \"hyperdht/1\", descriptor, onDescriptor: null, onUnavailable: null,\n get onConnection() { return handler; },\n set onConnection(value) { handler = value; if (value) for (const bound of waiting.splice(0)) value(bound); },\n async connect(address) {\n const publicKey = (address as { publicKey?: unknown } | null)?.publicKey;\n if (stopped || typeof publicKey !== \"string\" || !/^[a-f0-9]{64}$/.test(publicKey)) throw new Error(\"Invalid HyperDHT endpoint\");\n return attach(client.node.connect(fromHex(publicKey), { keyPair }), channels);\n },\n async close() {\n stopped = true;\n for (const channel of channels) channel.close();\n waiting.length = 0;\n await server.close().catch(() => {});\n release(url, client, member);\n },\n };\n return endpoint;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"hyperdhtRelay-DmTrpmba.js","names":[],"sources":["../../../browser/src/platform/hyperdhtRelay.ts"],"sourcesContent":["import { ed25519 } from \"@noble/curves/ed25519.js\";\nimport { fromBase64Url, type BoundChannel, type FrameChannel, type NativeEndpoint } from \"@ghostly/core\";\nimport { hyperdhtRelayProblem } from \"../shared/hyperdhtRelay\";\n\nexport { hyperdhtRelayProblem as relayUrlProblem };\n\n/**\n * HyperDHT (WISP 103) for a browser, through a dht-relay: a server that runs the UDP half of HyperDHT and\n * talks to the browser over one WebSocket. It runs non-custodial: the browser keeps its keys, runs the\n * Noise handshake and the encrypted stream itself, and the relay moves handshake messages and ciphertext.\n * The binding (the handshake hash and both keys) is computed here, at the endpoint, as the Desktop does.\n *\n * What the relay sees: this browser's address, the HyperDHT keys it listens on and dials, and when and how\n * much it sends. Not the frames, not the keys' secrets, not the handshake hash.\n *\n * The wire is the Desktop's (native/transports/hyperdht/endpoint.mjs): the same preface, u32 length-prefixed\n * UTF-8 frames of at most 60 KiB, so a browser and a Desktop talk HyperDHT to each other.\n */\n\nconst PREFACE = \"ghostly/paired-chat/1\";\nconst MAX_FRAME = 60 * 1024;\nconst SEND_BUDGET = 120 * 1024;\nconst HANDSHAKE_MS = 20_000;\nconst RELAY_OPEN_MS = 10_000;\nconst LISTEN_MS = 20_000;\nconst MAX_CHANNELS = 2;\n/**\n * How the relay client is always made: never custodial. dht-relay's default is custodial, which sends every\n * secret key to the relay so it can run the handshake and the stream itself. Nothing here takes this from\n * settings or options; test/hyperdhtRelay.test.ts fails if a secret key ever reaches the relay's wire.\n */\nconst NON_CUSTODIAL = Object.freeze({ custodial: false as const });\n\n/**\n * The descriptor a browser's HyperDHT endpoint gives out: its key, that it is reached only through a relay, and\n * which one (the connection panel names its host; a Desktop dialling it ignores both).\n */\nexport interface RelayedHyperDescriptor { publicKey: string; relayed: true; relay: string }\n\n/** The parts of dht-relay and its streams this adapter uses. */\ninterface SecretSocket {\n handshakeHash: Uint8Array | null;\n publicKey: Uint8Array;\n remotePublicKey: Uint8Array;\n opened?: Promise<boolean>;\n destroyed?: boolean;\n write(data: Uint8Array): boolean;\n destroy(error?: Error): void;\n on(event: string, listener: (...args: never[]) => void): unknown;\n once(event: string, listener: (...args: never[]) => void): unknown;\n off(event: string, listener: (...args: never[]) => void): unknown;\n}\ninterface RelayedServer {\n listen(keyPair: KeyPair): Promise<void>;\n close(): Promise<void>;\n}\ninterface RelayedNode {\n ready(): Promise<void>;\n connect(remotePublicKey: Uint8Array, options: { keyPair: KeyPair }): SecretSocket;\n createServer(options: object, onconnection: (socket: SecretSocket) => void): RelayedServer;\n destroy(): Promise<void>;\n}\ninterface KeyPair { publicKey: Uint8Array; secretKey: Uint8Array }\n\nconst hex = (bytes: Uint8Array) => Array.from(bytes, b => b.toString(16).padStart(2, \"0\")).join(\"\");\nconst fromHex = (text: string) => Uint8Array.from(text.match(/../g)!, byte => parseInt(byte, 16));\n\n/** HyperDHT's key pair for a seed: libsodium's layout (the seed, then the public key). */\nexport function hyperKeyPair(seed: Uint8Array): KeyPair {\n if (seed.length !== 32) throw new Error(\"Invalid HyperDHT seed\");\n const publicKey = ed25519.getPublicKey(seed);\n const secretKey = new Uint8Array(64);\n secretKey.set(seed); secretKey.set(publicKey, 32);\n return { publicKey, secretKey };\n}\n\n/**\n * One WebSocket per relay for the whole app, shared by every chat's endpoint (each listens on its own\n * key). It closes when the last endpoint does. When the relay goes away, every endpoint on it says it is\n * unavailable, and the next one to start opens a new connection.\n */\ninterface RelayClient {\n node: RelayedNode;\n endpoints: Set<{ unavailable(): void }>;\n gone: boolean;\n /** Settles when the relay connection closes. */\n closed: Promise<void>;\n}\nconst clients = new Map<string, Promise<RelayClient>>();\n\nasync function relayClient(url: string, createSocket: (url: string) => WebSocket): Promise<RelayClient> {\n const existing = clients.get(url);\n if (existing) {\n const client = await existing.catch(() => null);\n if (client && !client.gone) return client;\n if (clients.get(url) === existing) clients.delete(url);\n }\n const opening = (async () => {\n const [{ default: RelayedDHT }, { default: WebSocketStream }] = await Promise.all([\n // @ts-expect-error dht-relay ships no types: RelayedNode and SecretSocket above describe what this uses.\n import(\"@hyperswarm/dht-relay\"),\n // @ts-expect-error The same package's WebSocket stream, untyped too.\n import(\"@hyperswarm/dht-relay/ws\"),\n ]);\n const socket = createSocket(url);\n socket.binaryType = \"arraybuffer\";\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => { socket.close(); reject(new Error(\"The HyperDHT relay did not answer\")); }, RELAY_OPEN_MS);\n socket.addEventListener(\"open\", () => { clearTimeout(timer); resolve(); }, { once: true });\n socket.addEventListener(\"error\", () => { clearTimeout(timer); reject(new Error(\"Could not reach the HyperDHT relay\")); }, { once: true });\n socket.addEventListener(\"close\", () => { clearTimeout(timer); reject(new Error(\"The HyperDHT relay closed the connection\")); }, { once: true });\n });\n // Non-custodial: the relay is told public keys only, never a secret key (dht-relay's default sends them).\n // The node's own key is never used to listen or dial: a random one says nothing about this app.\n const node = new RelayedDHT(new WebSocketStream(true, socket), { ...NON_CUSTODIAL, keyPair: hyperKeyPair(crypto.getRandomValues(new Uint8Array(32))) }) as RelayedNode;\n let closed!: () => void;\n const client: RelayClient = { node, endpoints: new Set(), gone: false, closed: new Promise(resolve => { closed = resolve; }) };\n socket.addEventListener(\"close\", () => {\n client.gone = true; closed();\n if (clients.get(url) === opening) clients.delete(url);\n for (const endpoint of [...client.endpoints]) endpoint.unavailable();\n });\n return client;\n })();\n clients.set(url, opening);\n opening.catch(() => { if (clients.get(url) === opening) clients.delete(url); });\n return opening;\n}\n\nfunction release(url: string, client: RelayClient, endpoint: { unavailable(): void }): void {\n client.endpoints.delete(endpoint);\n if (client.endpoints.size || client.gone) return;\n client.gone = true;\n void clients.get(url)?.then(current => { if (current === client) clients.delete(url); }).catch(() => {});\n void client.node.destroy().catch(() => {});\n}\n\n/** One authenticated stream as a frame channel, with a bounded queue before the session installs its reader. */\nclass RelayedChannel implements FrameChannel {\n private reader: FrameChannel[\"onMessage\"] = null;\n private buffered: string[] = [];\n private closed = false;\n private pendingBytes = 0;\n onClose: FrameChannel[\"onClose\"] = null;\n constructor(private readonly socket: SecretSocket) {\n socket.on(\"drain\", () => { this.pendingBytes = 0; });\n }\n get bufferedAmount(): number { return this.pendingBytes; }\n readonly sendBudget = SEND_BUDGET;\n get onMessage() { return this.reader; }\n set onMessage(reader: FrameChannel[\"onMessage\"]) {\n this.reader = reader;\n if (reader) for (const text of this.buffered.splice(0)) reader(text);\n }\n receive(text: string): boolean {\n if (this.closed) return true;\n if (this.reader) this.reader(text);\n else if (this.buffered.length < 64) this.buffered.push(text);\n else return false;\n return true;\n }\n send(data: string | Uint8Array): void {\n if (typeof data !== \"string\" || this.closed || !data.length) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n const payload = new TextEncoder().encode(data);\n if (payload.length > MAX_FRAME || this.pendingBytes + payload.length > SEND_BUDGET) throw new Error(\"Native channel is closed or exceeds its frame budget\");\n // The stream takes every write; past its high-water mark it says so until it drains.\n if (!writeFrame(this.socket, payload) || this.pendingBytes) this.pendingBytes += payload.length + 4;\n }\n drained(): Promise<void> {\n if (!this.pendingBytes) return Promise.resolve();\n return new Promise((resolve, reject) => {\n const done = () => { this.socket.off(\"close\", gone); resolve(); };\n const gone = () => { this.socket.off(\"drain\", done); reject(new Error(\"Native channel closed\")); };\n this.socket.once(\"drain\", done); this.socket.once(\"close\", gone);\n });\n }\n closedByPeer(): void {\n if (this.closed) return;\n this.closed = true; this.buffered = [];\n this.onClose?.();\n }\n close(): void {\n if (this.closed) return;\n this.socket.destroy();\n this.closedByPeer();\n }\n}\n\nfunction writeFrame(socket: SecretSocket, payload: Uint8Array): boolean {\n const frame = new Uint8Array(4 + payload.length);\n new DataView(frame.buffer).setUint32(0, payload.length);\n frame.set(payload, 4);\n return socket.write(frame);\n}\n\n/**\n * Waits for the peer's preface on an authenticated stream (sending ours as soon as the handshake is\n * done), then hands the channel over with the binding the stream itself gives. A frame of the wrong size,\n * bytes that are not UTF-8 or another protocol close the stream.\n */\nfunction attach(socket: SecretSocket, channels: Set<RelayedChannel>): Promise<BoundChannel> {\n if (channels.size >= MAX_CHANNELS) { socket.destroy(); return Promise.reject(new Error(\"Native connection limit reached\")); }\n const channel = new RelayedChannel(socket);\n channels.add(channel);\n return new Promise<BoundChannel>((resolve, reject) => {\n let buffer = new Uint8Array(0);\n let prefaced = false;\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n const timer = setTimeout(() => socket.destroy(new Error(\"HyperDHT handshake timed out\")), HANDSHAKE_MS);\n const fail = (error: Error) => { socket.destroy(error); };\n socket.on(\"error\", (error: Error) => reject(error));\n socket.on(\"close\", () => {\n clearTimeout(timer); channels.delete(channel);\n reject(new Error(\"Native channel closed\"));\n channel.closedByPeer();\n });\n const opened = () => {\n if (socket.handshakeHash?.length !== 64 || socket.publicKey?.length !== 32 || socket.remotePublicKey?.length !== 32) {\n fail(new Error(\"Noise session binding unavailable\")); return;\n }\n writeFrame(socket, new TextEncoder().encode(PREFACE));\n };\n if (socket.opened) void socket.opened.then(ok => { if (ok) opened(); else reject(new Error(\"Noise handshake failed\")); });\n else socket.once(\"open\", opened);\n socket.on(\"data\", (chunk: Uint8Array) => {\n const joined = new Uint8Array(buffer.length + chunk.length);\n joined.set(buffer); joined.set(chunk, buffer.length);\n buffer = joined;\n while (buffer.length >= 4) {\n const length = new DataView(buffer.buffer, buffer.byteOffset, 4).getUint32(0);\n if (length === 0 || length > MAX_FRAME) { fail(new Error(\"Invalid frame length\")); return; }\n if (buffer.length < 4 + length) break;\n let text: string;\n try { text = decoder.decode(buffer.subarray(4, 4 + length)); }\n catch { fail(new Error(\"Invalid UTF-8 frame\")); return; }\n buffer = buffer.slice(4 + length);\n if (!prefaced) {\n if (text !== PREFACE) { fail(new Error(\"Unexpected application protocol\")); return; }\n prefaced = true; clearTimeout(timer);\n resolve({ channel, binding: { transport: \"hyperdht/1\", context: hex(socket.handshakeHash!),\n identities: [hex(socket.publicKey), hex(socket.remotePublicKey)].sort() as [string, string] } });\n } else if (!channel.receive(text)) { fail(new Error(\"Receive queue exceeded\")); return; }\n }\n });\n });\n}\n\nexport interface RelayedHyperOptions {\n /** How to open the WebSocket (tests pass Node's `ws`). Default: the browser's. */\n createSocket?: (url: string) => WebSocket;\n}\n\n/**\n * A chat's HyperDHT endpoint through the relay at `url`: it listens on the key of `seedB64` (the chat's\n * own transport seed, as on the Desktop) and dials a contact's descriptor. Fails, and so is not offered,\n * when the relay cannot be reached or does not let it listen.\n */\nexport async function createRelayedHyperEndpoint(seedB64: string, url: string, options: RelayedHyperOptions = {}): Promise<NativeEndpoint> {\n const problem = hyperdhtRelayProblem(url);\n if (problem) throw new Error(problem);\n const keyPair = hyperKeyPair(fromBase64Url(seedB64));\n const client = await relayClient(url, options.createSocket ?? (address => new WebSocket(address)));\n const channels = new Set<RelayedChannel>();\n let stopped = false;\n let handler: NativeEndpoint[\"onConnection\"] = null;\n const waiting: BoundChannel[] = [];\n let endpoint: NativeEndpoint | null = null;\n const member = {\n unavailable() {\n if (stopped) return;\n stopped = true;\n for (const channel of channels) channel.close();\n endpoint?.onUnavailable?.();\n },\n };\n client.endpoints.add(member);\n const server = client.node.createServer({}, socket => {\n void attach(socket, channels).then(bound => {\n if (stopped) bound.channel.close();\n else if (handler) handler(bound);\n else if (waiting.length < MAX_CHANNELS) waiting.push(bound);\n else bound.channel.close();\n }).catch(() => {});\n });\n try {\n await Promise.race([\n server.listen(keyPair),\n client.closed.then(() => { throw new Error(\"The HyperDHT relay closed the connection\"); }),\n new Promise((_, reject) => setTimeout(() => reject(new Error(\"The HyperDHT relay did not let this chat listen\")), LISTEN_MS)),\n ]);\n if (client.gone) throw new Error(\"The HyperDHT relay closed the connection\");\n } catch (error) {\n stopped = true;\n void server.close().catch(() => {});\n release(url, client, member);\n throw error;\n }\n const descriptor: RelayedHyperDescriptor = { publicKey: hex(keyPair.publicKey), relayed: true, relay: url };\n endpoint = {\n transport: \"hyperdht/1\", descriptor, onDescriptor: null, onUnavailable: null,\n get onConnection() { return handler; },\n set onConnection(value) { handler = value; if (value) for (const bound of waiting.splice(0)) value(bound); },\n async connect(address) {\n const publicKey = (address as { publicKey?: unknown } | null)?.publicKey;\n if (stopped || typeof publicKey !== \"string\" || !/^[a-f0-9]{64}$/.test(publicKey)) throw new Error(\"Invalid HyperDHT endpoint\");\n return attach(client.node.connect(fromHex(publicKey), { keyPair }), channels);\n },\n async close() {\n stopped = true;\n for (const channel of channels) channel.close();\n waiting.length = 0;\n await server.close().catch(() => {});\n release(url, client, member);\n },\n };\n return endpoint;\n}\n"],"mappings":"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"}