@ghostlytools/cli 1.2.4 → 1.3.1

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 (100) hide show
  1. package/README.md +25 -9
  2. package/dist/assets/api-Dq2QXQDk.js +4172 -0
  3. package/dist/assets/api-Dq2QXQDk.js.map +1 -0
  4. package/dist/assets/api-TiB5QSG9.js +3 -0
  5. package/dist/assets/apiKit-CA2x5GJF.js +200 -0
  6. package/dist/assets/apiKit-CA2x5GJF.js.map +1 -0
  7. package/dist/assets/apps-BxV9i_Y4.js +786 -0
  8. package/dist/assets/apps-BxV9i_Y4.js.map +1 -0
  9. package/dist/assets/apps-C-T8jeji.js +2 -0
  10. package/dist/assets/{arkade-CdnKGDbg.js → arkade-C_qDmHnm.js} +2 -2
  11. package/dist/assets/{arkade-CdnKGDbg.js.map → arkade-C_qDmHnm.js.map} +1 -1
  12. package/dist/assets/backup-Bugnn3Ue.js +2 -0
  13. package/dist/assets/backup-CnMN2oUK.js +731 -0
  14. package/dist/assets/backup-CnMN2oUK.js.map +1 -0
  15. package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
  16. package/dist/assets/bitcoind-CZchoh43.js +349 -0
  17. package/dist/assets/bitcoind-CZchoh43.js.map +1 -0
  18. package/dist/assets/bitcoindProvider-DGw3xBSa.js +97 -0
  19. package/dist/assets/bitcoindProvider-DGw3xBSa.js.map +1 -0
  20. package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
  21. package/dist/assets/buttons-Bj7Vf7Pn.js +2 -0
  22. package/dist/assets/buttons-CvNBzh2S.js +496 -0
  23. package/dist/assets/buttons-CvNBzh2S.js.map +1 -0
  24. package/dist/assets/{endpoint-pgFkR2AS.js → endpoint-BOTFMpfS.js} +56 -18
  25. package/dist/assets/endpoint-BOTFMpfS.js.map +1 -0
  26. package/dist/assets/envelope-WjingYSc.js +90 -0
  27. package/dist/assets/envelope-WjingYSc.js.map +1 -0
  28. package/dist/assets/errors-DO5z6h_l.js +105 -0
  29. package/dist/assets/errors-DO5z6h_l.js.map +1 -0
  30. package/dist/assets/fedimint_client_wasm_bg-BtHHlFpp.js.map +1 -1
  31. package/dist/assets/{fileBytes-Dbifu5I5.js → fileBytes-CfSXjTFa.js} +2 -2
  32. package/dist/assets/{fileBytes-Dbifu5I5.js.map → fileBytes-CfSXjTFa.js.map} +1 -1
  33. package/dist/assets/fileBytes-CgbibpbV.js +2 -0
  34. package/dist/assets/{fileBytes-BcYZhNgF.js → fileBytes-Di7VZtmU.js} +6 -19
  35. package/dist/assets/fileBytes-Di7VZtmU.js.map +1 -0
  36. package/dist/assets/fileBytesIdb-CFP-DSkZ.js +2 -0
  37. package/dist/assets/{fileBytesIdb-DzSaqI0A.js → fileBytesIdb-D5u6H8X_.js} +4 -4
  38. package/dist/assets/{fileBytesIdb-DzSaqI0A.js.map → fileBytesIdb-D5u6H8X_.js.map} +1 -1
  39. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js → fileBytesOpfs-Dd1N4ERH.js} +3 -3
  40. package/dist/assets/{fileBytesOpfs-Dx8RZiV5.js.map → fileBytesOpfs-Dd1N4ERH.js.map} +1 -1
  41. package/dist/assets/groupFiles-IEUW3DQz.js +3202 -0
  42. package/dist/assets/groupFiles-IEUW3DQz.js.map +1 -0
  43. package/dist/assets/host-CWt-jtqz.js +4673 -0
  44. package/dist/assets/host-CWt-jtqz.js.map +1 -0
  45. package/dist/assets/hyperdhtRelay-BT_wRxab.js +21 -0
  46. package/dist/assets/hyperdhtRelay-BT_wRxab.js.map +1 -0
  47. package/dist/assets/{hyperdhtRelay-C1p0nkle.js → hyperdhtRelay-CPYwSLLS.js} +25 -10
  48. package/dist/assets/hyperdhtRelay-CPYwSLLS.js.map +1 -0
  49. package/dist/assets/{idb-CTF6LF5B.js → idb-B5L8wuOm.js} +7 -5
  50. package/dist/assets/idb-B5L8wuOm.js.map +1 -0
  51. package/dist/assets/irohWeb-4oChIOIp.js +544 -0
  52. package/dist/assets/irohWeb-4oChIOIp.js.map +1 -0
  53. package/dist/assets/{joinTrace-CO8mlhNO.js → joinTrace-CjRDFy85.js} +2 -7
  54. package/dist/assets/{joinTrace-CO8mlhNO.js.map → joinTrace-CjRDFy85.js.map} +1 -1
  55. package/dist/assets/joinTrace-D-fnt2p3.js +2 -0
  56. package/dist/assets/main-DbqcdShN.js +4409 -0
  57. package/dist/assets/main-DbqcdShN.js.map +1 -0
  58. package/dist/assets/{mainline-BH6s8HVi.js → mainline-ZkiOnevs.js} +11 -6
  59. package/dist/assets/mainline-ZkiOnevs.js.map +1 -0
  60. package/dist/assets/mints-BrlY773j.js +74 -0
  61. package/dist/assets/mints-BrlY773j.js.map +1 -0
  62. package/dist/assets/mints-CqTdA4Gk.js +2 -0
  63. package/dist/assets/networkReason-CetKkTPo.js +154 -0
  64. package/dist/assets/networkReason-CetKkTPo.js.map +1 -0
  65. package/dist/assets/nwc-CgkqyvsE.js +612 -0
  66. package/dist/assets/nwc-CgkqyvsE.js.map +1 -0
  67. package/dist/assets/nwcProvider-4eVz_8HP.js +77 -0
  68. package/dist/assets/nwcProvider-4eVz_8HP.js.map +1 -0
  69. package/dist/assets/privateFolder-Bk9C9oFv.js +38 -0
  70. package/dist/assets/privateFolder-Bk9C9oFv.js.map +1 -0
  71. package/dist/assets/profiles-BHc9pEYg.js +139 -0
  72. package/dist/assets/profiles-BHc9pEYg.js.map +1 -0
  73. package/dist/assets/registry-DlAKO0Fy.js +3793 -0
  74. package/dist/assets/registry-DlAKO0Fy.js.map +1 -0
  75. package/dist/assets/registry-OBJBnHVQ.js +2 -0
  76. package/dist/assets/resolve-CouHhCpJ.js +591 -0
  77. package/dist/assets/resolve-CouHhCpJ.js.map +1 -0
  78. package/dist/assets/{restoredRows-C_84q0Pd.js → restoredRows-Db3oUTko.js} +2 -2
  79. package/dist/assets/{restoredRows-C_84q0Pd.js.map → restoredRows-Db3oUTko.js.map} +1 -1
  80. package/dist/assets/{server-0zfNpABQ.js → server-Ss0gJ-nw.js} +1275 -1404
  81. package/dist/assets/server-Ss0gJ-nw.js.map +1 -0
  82. package/dist/assets/{src-CbLkHfz8.js → src-DDzSERyc.js} +882 -134
  83. package/dist/assets/src-DDzSERyc.js.map +1 -0
  84. package/dist/assets/turnRefs-B87dtrHM.js +323 -0
  85. package/dist/assets/turnRefs-B87dtrHM.js.map +1 -0
  86. package/dist/assets/types-C4_KafL_.js +122 -0
  87. package/dist/assets/types-C4_KafL_.js.map +1 -0
  88. package/dist/assets/{usdt-Qf4UuygS.js → usdt-Fj8CQO2z.js} +3 -3
  89. package/dist/assets/{usdt-Qf4UuygS.js.map → usdt-Fj8CQO2z.js.map} +1 -1
  90. package/dist/ghostly.mjs +4 -22077
  91. package/dist/ghostly.mjs.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/assets/endpoint-pgFkR2AS.js.map +0 -1
  94. package/dist/assets/fileBytes-BcYZhNgF.js.map +0 -1
  95. package/dist/assets/hyperdhtRelay-C1p0nkle.js.map +0 -1
  96. package/dist/assets/idb-CTF6LF5B.js.map +0 -1
  97. package/dist/assets/mainline-BH6s8HVi.js.map +0 -1
  98. package/dist/assets/rolldown-runtime-8H4AJuhK.js +0 -14
  99. package/dist/assets/server-0zfNpABQ.js.map +0 -1
  100. package/dist/assets/src-CbLkHfz8.js.map +0 -1
@@ -82,9 +82,35 @@ function redial(dialOnce, { waitMs = PREFACE_WAIT_MS, maxWaitMs = PREFACE_MAX_WA
82
82
  //#endregion
83
83
  //#region ../../native/transports/hyperdht/endpoint.mjs
84
84
  var MAX_FRAME = 61440;
85
+ /** The top bit of a frame's length word: the frame is a binary message, not UTF-8 text (WISP 103). */
86
+ var BINARY = 2147483648;
85
87
  var PREFACE = "ghostly/paired-chat/1";
86
88
  /** How long a closing channel waits for what it sent last to go out before the stream is destroyed anyway. */
87
89
  var CLOSE_FLUSH_MS = 300;
90
+ /**
91
+ * Bytes a channel may hold before `send` throws: above what every sender here lets itself queue (the CLI's 1 MiB high
92
+ * water mark and an app's quarter above it; the Desktop page's send budget over the sidecar's `SEND_ROOM`), so it
93
+ * trips only for one that ignores its own line.
94
+ */
95
+ var MAX_BUFFERED = 2097152;
96
+ /**
97
+ * `drained()` resolves once a channel holds no more than this: no more than the sidecar's `SEND_ROOM`, so its wait for
98
+ * room never spins on a `drained()` that resolves at once.
99
+ */
100
+ var DRAINED_AT = 65536;
101
+ /**
102
+ * How often a wait in `drained()` looks again. Each stream says `drain` only once its own queue is empty, which on a fast
103
+ * link comes first; on a slow one this finds the queue at `DRAINED_AT` before it runs dry.
104
+ */
105
+ var DRAIN_POLL_MS = 10;
106
+ /**
107
+ * What a connection's stream still holds: secret-stream's queue and the UDX stream's under it. Both are streamx
108
+ * streams, which count it in `_writableState.buffered` (hyperdht is pinned; hyperdhtEndpointBuffered.test.ts reads it).
109
+ * secret-stream has no `writableLength`: read from there, it was always 0.
110
+ */
111
+ function held(socket) {
112
+ return (socket._writableState?.buffered ?? 0) + (socket.rawStream?._writableState?.buffered ?? 0);
113
+ }
88
114
  /** Native UDP + NoiseSecretStream. The binding comes from the authenticated
89
115
  * stream itself, never from an application frame supplied by the other peer. */
90
116
  async function createHyperEndpoint(seed, options = {}) {
@@ -121,8 +147,9 @@ async function createHyperEndpoint(seed, options = {}) {
121
147
  socket.destroy(/* @__PURE__ */ new Error("HyperDHT handshake timed out"));
122
148
  }, 2e4);
123
149
  const channel = {
150
+ binary: true,
124
151
  get bufferedAmount() {
125
- return socket.writableLength || 0;
152
+ return held(socket);
126
153
  },
127
154
  get onMessage() {
128
155
  return reader;
@@ -138,25 +165,34 @@ async function createHyperEndpoint(seed, options = {}) {
138
165
  closeHandler = value;
139
166
  if (closed) value?.();
140
167
  },
141
- send(text) {
142
- if (closed || typeof text !== "string" || !text.length || Buffer.byteLength(text) > 61440 || this.bufferedAmount > 122880) throw new Error("Native channel is closed or exceeds its frame budget");
143
- const payload = Buffer.from(text);
168
+ send(data) {
169
+ const binary = data instanceof Uint8Array;
170
+ if (closed || !(binary || typeof data === "string") || !data.length || (binary ? data.length : Buffer.byteLength(data)) > 61440 || this.bufferedAmount > MAX_BUFFERED) throw new Error("Native channel is closed or exceeds its frame budget");
171
+ const payload = binary ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data);
144
172
  const header = Buffer.alloc(4);
145
- header.writeUInt32BE(payload.length);
173
+ header.writeUInt32BE((binary ? BINARY : 0) + payload.length);
146
174
  socket.write(Buffer.concat([header, payload]));
147
175
  },
148
176
  drained() {
149
- if (!this.bufferedAmount) return Promise.resolve();
177
+ if (this.bufferedAmount <= DRAINED_AT) return Promise.resolve();
178
+ if (closed) return Promise.reject(/* @__PURE__ */ new Error("Native channel closed"));
150
179
  return new Promise((resolve, reject) => {
151
- const done = () => {
152
- socket.off("close", gone);
153
- resolve();
180
+ const raw = socket.rawStream;
181
+ const check = () => {
182
+ if (this.bufferedAmount <= DRAINED_AT) finish(resolve);
154
183
  };
155
- const gone = () => {
156
- socket.off("drain", done);
157
- reject(/* @__PURE__ */ new Error("Native channel closed"));
158
- };
159
- socket.once("drain", done);
184
+ const gone = () => finish(() => reject(/* @__PURE__ */ new Error("Native channel closed")));
185
+ const poll = setInterval(check, DRAIN_POLL_MS);
186
+ poll.unref?.();
187
+ function finish(settle) {
188
+ clearInterval(poll);
189
+ socket.off("drain", check);
190
+ raw?.off("drain", check);
191
+ socket.off("close", gone);
192
+ settle();
193
+ }
194
+ socket.on("drain", check);
195
+ raw?.on("drain", check);
160
196
  socket.once("close", gone);
161
197
  });
162
198
  },
@@ -192,14 +228,16 @@ async function createHyperEndpoint(seed, options = {}) {
192
228
  socket.on("data", (chunk) => {
193
229
  buffer = Buffer.concat([buffer, chunk]);
194
230
  while (buffer.length >= 4) {
195
- const length = buffer.readUInt32BE(0);
196
- if (length === 0 || length > 61440) {
231
+ const word = buffer.readUInt32BE(0);
232
+ const binary = word >= BINARY, length = word % BINARY;
233
+ if (length === 0 || length > 61440 || binary && !preface) {
197
234
  socket.destroy(/* @__PURE__ */ new Error("Invalid frame length"));
198
235
  return;
199
236
  }
200
237
  if (buffer.length < 4 + length) break;
201
238
  let text;
202
- try {
239
+ if (binary) text = new Uint8Array(buffer.subarray(4, 4 + length));
240
+ else try {
203
241
  text = new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(4, 4 + length));
204
242
  } catch {
205
243
  socket.destroy(/* @__PURE__ */ new Error("Invalid UTF-8 frame"));
@@ -292,4 +330,4 @@ async function createHyperEndpoint(seed, options = {}) {
292
330
  //#endregion
293
331
  export { MAX_FRAME, createHyperEndpoint };
294
332
 
295
- //# sourceMappingURL=endpoint-pgFkR2AS.js.map
333
+ //# sourceMappingURL=endpoint-BOTFMpfS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoint-BOTFMpfS.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\n/** The top bit of a frame's length word: the frame is a binary message, not UTF-8 text (WISP 103). */\nconst BINARY = 0x80000000\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 * Bytes a channel may hold before `send` throws: above what every sender here lets itself queue (the CLI's 1 MiB high\n * water mark and an app's quarter above it; the Desktop page's send budget over the sidecar's `SEND_ROOM`), so it\n * trips only for one that ignores its own line.\n */\nconst MAX_BUFFERED = 2 * 1024 * 1024\n/**\n * `drained()` resolves once a channel holds no more than this: no more than the sidecar's `SEND_ROOM`, so its wait for\n * room never spins on a `drained()` that resolves at once.\n */\nconst DRAINED_AT = 64 * 1024\n/**\n * How often a wait in `drained()` looks again. Each stream says `drain` only once its own queue is empty, which on a fast\n * link comes first; on a slow one this finds the queue at `DRAINED_AT` before it runs dry.\n */\nconst DRAIN_POLL_MS = 10\n\n/**\n * What a connection's stream still holds: secret-stream's queue and the UDX stream's under it. Both are streamx\n * streams, which count it in `_writableState.buffered` (hyperdht is pinned; hyperdhtEndpointBuffered.test.ts reads it).\n * secret-stream has no `writableLength`: read from there, it was always 0.\n */\nfunction held(socket) {\n return (socket._writableState?.buffered ?? 0) + (socket.rawStream?._writableState?.buffered ?? 0)\n}\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 // It carries binary messages (WISP 100 § Text and binary messages): a capability that needs them may be offered.\n binary: true,\n get bufferedAmount() { return held(socket) },\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(data) {\n const binary = data instanceof Uint8Array\n if (closed || !(binary || typeof data === 'string') || !data.length || (binary ? data.length : Buffer.byteLength(data)) > MAX_FRAME || this.bufferedAmount > MAX_BUFFERED)\n throw new Error('Native channel is closed or exceeds its frame budget')\n const payload = binary ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : Buffer.from(data)\n const header = Buffer.alloc(4); header.writeUInt32BE((binary ? BINARY : 0) + payload.length)\n socket.write(Buffer.concat([header, payload]))\n },\n drained() {\n if (this.bufferedAmount <= DRAINED_AT) return Promise.resolve()\n if (closed) return Promise.reject(new Error('Native channel closed'))\n return new Promise((resolve, reject) => {\n const raw = socket.rawStream\n const check = () => { if (this.bufferedAmount <= DRAINED_AT) finish(resolve) }\n const gone = () => finish(() => reject(new Error('Native channel closed')))\n const poll = setInterval(check, DRAIN_POLL_MS)\n poll.unref?.()\n function finish(settle) {\n clearInterval(poll); socket.off('drain', check); raw?.off('drain', check); socket.off('close', gone); settle()\n }\n socket.on('drain', check); raw?.on('drain', check); 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 word = buffer.readUInt32BE(0)\n const binary = word >= BINARY, length = word % BINARY\n // A binary frame before the preface is another protocol.\n if (length === 0 || length > MAX_FRAME || (binary && !preface)) { socket.destroy(new Error('Invalid frame length')); return }\n if (buffer.length < 4 + length) break\n let text\n if (binary) text = new Uint8Array(buffer.subarray(4, 4 + length))\n else {\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 }\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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"}
@@ -0,0 +1,90 @@
1
+ import "./src-DDzSERyc.js";
2
+ //#region ../browser/src/backup/codec.ts
3
+ function fromBase64Url(text) {
4
+ if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error("Invalid base64url");
5
+ const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text.length % 4) % 4));
6
+ const bytes = new Uint8Array(binary.length);
7
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
8
+ return bytes;
9
+ }
10
+ //#endregion
11
+ //#region ../browser/src/backup/envelope.ts
12
+ var BACKUP_MEDIA_TYPE = "application/vnd.ghostly.backup+json";
13
+ var ITERATIONS = 6e5;
14
+ async function keyFrom(passphrase, salt, iterations) {
15
+ const material = await crypto.subtle.importKey("raw", new TextEncoder().encode(passphrase), "PBKDF2", false, ["deriveKey"]);
16
+ return crypto.subtle.deriveKey({
17
+ name: "PBKDF2",
18
+ hash: "SHA-256",
19
+ salt,
20
+ iterations
21
+ }, material, {
22
+ name: "AES-GCM",
23
+ length: 256
24
+ }, false, ["encrypt", "decrypt"]);
25
+ }
26
+ /** A profile with years of files is large, but not this large: past it, a bundle is refused, not read. */
27
+ var MAX_BACKUP_BYTES = 1073741824;
28
+ async function through(bytes, stream) {
29
+ const reader = new Blob([bytes]).stream().pipeThrough(stream).getReader();
30
+ const chunks = [];
31
+ let size = 0;
32
+ for (let next = await reader.read(); !next.done; next = await reader.read()) {
33
+ size += next.value.length;
34
+ if (size > 1073741824) {
35
+ await reader.cancel();
36
+ throw new Error("This backup is too large to restore");
37
+ }
38
+ chunks.push(next.value);
39
+ }
40
+ const out = new Uint8Array(size);
41
+ let at = 0;
42
+ for (const chunk of chunks) {
43
+ out.set(chunk, at);
44
+ at += chunk.length;
45
+ }
46
+ return out;
47
+ }
48
+ /**
49
+ * The clear header is authenticated with the ciphertext (AES-GCM associated data): changing any of it,
50
+ * the compression included, makes the bundle fail to open instead of being read differently.
51
+ */
52
+ var header = (e) => new TextEncoder().encode([
53
+ e.format,
54
+ e.version,
55
+ e.kdf.name,
56
+ e.kdf.iterations,
57
+ e.kdf.salt,
58
+ e.cipher.name,
59
+ e.cipher.iv,
60
+ e.compression
61
+ ].join("\n"));
62
+ /** Checks, decrypts and decompresses a bundle. Nothing from a bundle is used before this succeeds. */
63
+ async function open(text, passphrase) {
64
+ if (text.length > 1503238553.6) throw new Error("This backup is too large to restore");
65
+ let envelope;
66
+ try {
67
+ envelope = JSON.parse(text);
68
+ } catch {
69
+ throw new Error("This is not a Ghostly backup");
70
+ }
71
+ if (envelope?.format !== "ghostly-backup") throw new Error("This is not a Ghostly backup");
72
+ if (envelope.version !== 1) throw new Error("This backup comes from a newer Ghostly; update to restore it");
73
+ if (envelope.kdf?.name !== "PBKDF2-SHA256" || !(envelope.kdf.iterations >= ITERATIONS) || envelope.kdf.iterations > 1e7 || envelope.cipher?.name !== "AES-256-GCM" || !["gzip", "none"].includes(envelope.compression)) throw new Error("Unsupported backup encryption");
74
+ let body;
75
+ try {
76
+ body = new Uint8Array(await crypto.subtle.decrypt({
77
+ name: "AES-GCM",
78
+ iv: fromBase64Url(envelope.cipher.iv),
79
+ additionalData: header(envelope)
80
+ }, await keyFrom(passphrase, fromBase64Url(envelope.kdf.salt), envelope.kdf.iterations), fromBase64Url(envelope.ciphertext)));
81
+ } catch {
82
+ throw new Error("Wrong passphrase, or the backup was changed");
83
+ }
84
+ const plain = envelope.compression === "gzip" ? await through(body, new DecompressionStream("gzip")) : body;
85
+ return new TextDecoder().decode(plain);
86
+ }
87
+ //#endregion
88
+ export { fromBase64Url as i, MAX_BACKUP_BYTES as n, open as r, BACKUP_MEDIA_TYPE as t };
89
+
90
+ //# sourceMappingURL=envelope-WjingYSc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-WjingYSc.js","names":[],"sources":["../../../browser/src/backup/codec.ts","../../../browser/src/backup/envelope.ts"],"sourcesContent":["import { toBase64Url } from \"@ghostly/core\";\n\n/**\n * Base64url without padding. Core's encoder, not `btoa`: every picture, voice note and kept file (up to 16 MiB each)\n * goes through it on the page's thread, where `btoa` spends over a second per 16 MiB.\n */\nexport { toBase64Url };\nexport function fromBase64Url(text: string): Uint8Array {\n if (!/^[A-Za-z0-9_-]*$/.test(text)) throw new Error(\"Invalid base64url\");\n const binary = atob(text.replace(/-/g, \"+\").replace(/_/g, \"/\") + \"=\".repeat((4 - (text.length % 4)) % 4));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\ntype Tagged = { $ghostly: \"bigint\" | \"bytes\" | \"blob\"; value: string; type?: string };\n/** Data that happens to look like a tag (a contact can send anything) travels as its entries instead. */\ntype Escaped = { $ghostly: \"object\"; entries: [string, unknown][] };\n\n/**\n * Blobs cannot be read synchronously: turn them into tagged values first, everywhere in the tree. An\n * object of the data's own with a `$ghostly` key is escaped, so it comes back as it was and not as a tag.\n */\nasync function inlineBlobs(value: unknown): Promise<unknown> {\n if (typeof Blob !== \"undefined\" && value instanceof Blob) return { $ghostly: \"blob\", type: value.type, value: toBase64Url(new Uint8Array(await value.arrayBuffer())) } satisfies Tagged;\n if (Array.isArray(value)) return Promise.all(value.map(inlineBlobs));\n if (value && typeof value === \"object\" && !(value instanceof Uint8Array) && !(value instanceof ArrayBuffer) && !ArrayBuffer.isView(value)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = await inlineBlobs(v);\n return Object.prototype.hasOwnProperty.call(out, \"$ghostly\") ? ({ $ghostly: \"object\", entries: Object.entries(out) } satisfies Escaped) : out;\n }\n return value;\n}\n\n/** JSON that also carries what IndexedDB holds: bigints, bytes and blobs (WISP 05). */\nexport async function encode(value: unknown): Promise<string> {\n return JSON.stringify(await inlineBlobs(value), (_key, v) => {\n if (typeof v === \"bigint\") return { $ghostly: \"bigint\", value: String(v) } satisfies Tagged;\n if (v instanceof Uint8Array) return { $ghostly: \"bytes\", value: toBase64Url(v) } satisfies Tagged;\n if (v instanceof ArrayBuffer) return { $ghostly: \"bytes\", value: toBase64Url(new Uint8Array(v)) } satisfies Tagged;\n return v;\n });\n}\n/** Never throws on a tag it cannot read: that value stays as it is, and the rest of the backup restores. */\nexport function decode(text: string): unknown {\n return JSON.parse(text, (_key, v) => {\n const escaped = v as Partial<Escaped> | null;\n if (escaped && typeof escaped === \"object\" && escaped.$ghostly === \"object\" && Array.isArray(escaped.entries)) {\n const out: Record<string, unknown> = {};\n for (const entry of escaped.entries) if (Array.isArray(entry) && typeof entry[0] === \"string\") Object.defineProperty(out, entry[0], { value: entry[1], enumerable: true, writable: true, configurable: true });\n return out;\n }\n const tag = v as Partial<Tagged> | null;\n if (!tag || typeof tag !== \"object\" || typeof tag.$ghostly !== \"string\" || typeof tag.value !== \"string\") return v;\n if (tag.$ghostly === \"bigint\") return /^-?\\d+$/.test(tag.value) ? BigInt(tag.value) : v;\n if (!/^[A-Za-z0-9_-]*$/.test(tag.value)) return v;\n if (tag.$ghostly === \"bytes\") return fromBase64Url(tag.value);\n if (tag.$ghostly === \"blob\") return new Blob([fromBase64Url(tag.value) as BlobPart], { type: typeof tag.type === \"string\" ? tag.type : \"\" });\n return v;\n });\n}\n","import { fromBase64Url, toBase64Url } from \"./codec\";\n\n/** The clear part of a bundle (WISP 05): only what decryption needs. */\nexport interface Envelope {\n format: \"ghostly-backup\";\n version: 1;\n kdf: { name: \"PBKDF2-SHA256\"; iterations: number; salt: string };\n cipher: { name: \"AES-256-GCM\"; iv: string };\n compression: \"gzip\" | \"none\";\n ciphertext: string;\n}\nexport const BACKUP_MEDIA_TYPE = \"application/vnd.ghostly.backup+json\";\nconst ITERATIONS = 600_000;\n\nasync function keyFrom(passphrase: string, salt: Uint8Array, iterations: number): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey(\"raw\", new TextEncoder().encode(passphrase), \"PBKDF2\", false, [\"deriveKey\"]);\n return crypto.subtle.deriveKey({ name: \"PBKDF2\", hash: \"SHA-256\", salt: salt as BufferSource, iterations }, material, { name: \"AES-GCM\", length: 256 }, false, [\"encrypt\", \"decrypt\"]);\n}\n/** A profile with years of files is large, but not this large: past it, a bundle is refused, not read. */\nexport const MAX_BACKUP_BYTES = 1024 * 1024 * 1024;\n\nasync function through(bytes: Uint8Array, stream: CompressionStream | DecompressionStream): Promise<Uint8Array> {\n const reader = new Blob([bytes as BlobPart]).stream().pipeThrough(stream).getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (let next = await reader.read(); !next.done; next = await reader.read()) {\n size += next.value.length;\n if (size > MAX_BACKUP_BYTES) { await reader.cancel(); throw new Error(\"This backup is too large to restore\"); }\n chunks.push(next.value);\n }\n const out = new Uint8Array(size);\n let at = 0;\n for (const chunk of chunks) { out.set(chunk, at); at += chunk.length; }\n return out;\n}\n\n/**\n * The clear header is authenticated with the ciphertext (AES-GCM associated data): changing any of it,\n * the compression included, makes the bundle fail to open instead of being read differently.\n */\nconst header = (e: Pick<Envelope, \"format\" | \"version\" | \"kdf\" | \"cipher\" | \"compression\">) =>\n new TextEncoder().encode([e.format, e.version, e.kdf.name, e.kdf.iterations, e.kdf.salt, e.cipher.name, e.cipher.iv, e.compression].join(\"\\n\"));\nconst canCompress = () => typeof CompressionStream !== \"undefined\";\n\n/** Compresses and encrypts a payload under a passphrase the person chose. */\nexport async function seal(payload: string, passphrase: string): Promise<string> {\n if (passphrase.length < 12) throw new Error(\"Use at least 12 characters for the backup passphrase\");\n const salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12));\n const plain = new TextEncoder().encode(payload);\n const compression = canCompress() ? \"gzip\" : \"none\";\n const body = compression === \"gzip\" ? await through(plain, new CompressionStream(\"gzip\")) : plain;\n const clear = { format: \"ghostly-backup\", version: 1, kdf: { name: \"PBKDF2-SHA256\", iterations: ITERATIONS, salt: toBase64Url(salt) }, cipher: { name: \"AES-256-GCM\", iv: toBase64Url(iv) }, compression } as const;\n const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: \"AES-GCM\", iv, additionalData: header(clear) as BufferSource }, await keyFrom(passphrase, salt, ITERATIONS), body as BufferSource));\n const envelope: Envelope = { ...clear, ciphertext: toBase64Url(ciphertext) };\n return JSON.stringify(envelope);\n}\n\n/** Checks, decrypts and decompresses a bundle. Nothing from a bundle is used before this succeeds. */\nexport async function open(text: string, passphrase: string): Promise<string> {\n if (text.length > MAX_BACKUP_BYTES * 1.4) throw new Error(\"This backup is too large to restore\");\n let envelope: Envelope;\n try { envelope = JSON.parse(text) as Envelope; } catch { throw new Error(\"This is not a Ghostly backup\"); }\n if (envelope?.format !== \"ghostly-backup\") throw new Error(\"This is not a Ghostly backup\");\n if (envelope.version !== 1) throw new Error(\"This backup comes from a newer Ghostly; update to restore it\");\n if (envelope.kdf?.name !== \"PBKDF2-SHA256\" || !(envelope.kdf.iterations >= ITERATIONS) || envelope.kdf.iterations > 10_000_000 || envelope.cipher?.name !== \"AES-256-GCM\" || ![\"gzip\", \"none\"].includes(envelope.compression)) throw new Error(\"Unsupported backup encryption\");\n let body: Uint8Array;\n try {\n body = new Uint8Array(await crypto.subtle.decrypt({ name: \"AES-GCM\", iv: fromBase64Url(envelope.cipher.iv) as BufferSource, additionalData: header(envelope) as BufferSource }, await keyFrom(passphrase, fromBase64Url(envelope.kdf.salt), envelope.kdf.iterations), fromBase64Url(envelope.ciphertext) as BufferSource));\n } catch { throw new Error(\"Wrong passphrase, or the backup was changed\"); }\n const plain = envelope.compression === \"gzip\" ? await through(body, new DecompressionStream(\"gzip\")) : body;\n return new TextDecoder().decode(plain);\n}\n"],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ //#region src/errors.ts
2
+ var EXIT = {
3
+ engine: 1,
4
+ refused: 1,
5
+ unavailable: 1,
6
+ bad_request: 1,
7
+ busy: 1,
8
+ internal: 1,
9
+ usage: 2,
10
+ not_found: 3,
11
+ timeout: 4,
12
+ confirm: 5
13
+ };
14
+ var CliError = class extends Error {
15
+ code;
16
+ details;
17
+ constructor(code, message, details) {
18
+ super(message);
19
+ this.code = code;
20
+ this.details = details;
21
+ this.name = "CliError";
22
+ }
23
+ toJSON() {
24
+ return {
25
+ code: this.code,
26
+ message: this.message,
27
+ ...this.details ? { details: this.details } : {}
28
+ };
29
+ }
30
+ };
31
+ /** Anything thrown, as the error a bot sees: the engine's own refusals keep their words. */
32
+ function asCliError(error) {
33
+ if (error instanceof CliError) return error;
34
+ if (error && typeof error === "object" && "code" in error && "message" in error && typeof error.code === "string" && Object.hasOwn(EXIT, error.code)) return new CliError(error.code, String(error.message));
35
+ if (error instanceof Error && error.code === "unreachable") return new CliError("unavailable", error.message);
36
+ const known = engineKnown(error);
37
+ if (known) return known;
38
+ const local = localFileError(error);
39
+ if (local) return local;
40
+ const unopened = profileOpenError(error);
41
+ if (unopened) return unopened;
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ if (/real money|confirmedReal|Mainnet/i.test(message) && /confirm/i.test(message)) return new CliError("confirm", message);
44
+ if (/other network|cross-network|network does not match/i.test(message)) return new CliError("refused", message);
45
+ return new CliError("engine", message);
46
+ }
47
+ /**
48
+ * The engine's known errors (`EngineError`, packages/core engineErrors.ts) a bot branches on, by their `engineCode`: a
49
+ * host or the network not answering is `unavailable` (try again later), one too slow `timeout`, a value the person gave
50
+ * `bad_request`, money that is not there `refused`. Any other keeps `engine`. Read by its shape, as `profileOpenError`.
51
+ */
52
+ var ENGINE_CODES = {
53
+ mintUnreachable: "unavailable",
54
+ lnurlUnreachable: "unavailable",
55
+ hostUnreachable: "unavailable",
56
+ networkUnreachable: "unavailable",
57
+ usdtRpcUnavailable: "unavailable",
58
+ hostTimedOut: "timeout",
59
+ networkTimedOut: "timeout",
60
+ invalidMintUrl: "bad_request",
61
+ mintNotHttps: "bad_request",
62
+ invalidMaxFee: "bad_request",
63
+ notEnoughSats: "refused",
64
+ ecashAlreadySpent: "refused"
65
+ };
66
+ function engineKnown(error) {
67
+ if (!(error instanceof Error)) return void 0;
68
+ const { engineCode, values } = error;
69
+ if (typeof engineCode !== "string" || !Object.hasOwn(ENGINE_CODES, engineCode)) return void 0;
70
+ if (engineCode === "lnurlUnreachable" && typeof values?.host === "string") return new CliError("unavailable", `Could not reach ${values.host}`);
71
+ return new CliError(ENGINE_CODES[engineCode], error.message);
72
+ }
73
+ /**
74
+ * The profile's database did not open (the engine's `ProfileOpenError`, packages/browser/src/shared/idb.ts): `engine`
75
+ * (exit 1) with the engine's own words, which name the versions when a newer ghostly stored it, and the reason and
76
+ * the versions in `details` for a bot. Read by its shape: this module is also the thin client's, which loads no engine.
77
+ */
78
+ function profileOpenError(error) {
79
+ if (!(error instanceof Error) || error.name !== "ProfileOpenError") return void 0;
80
+ const failure = error.failure;
81
+ if (!failure || typeof failure.reason !== "string") return void 0;
82
+ return new CliError("engine", error.message, {
83
+ reason: failure.reason === "newer" ? "newer_profile" : `profile_${failure.reason}`,
84
+ ...typeof failure.storedVersion === "number" ? { storedVersion: failure.storedVersion } : {},
85
+ ...typeof failure.supportedVersion === "number" ? { supportedVersion: failure.supportedVersion } : {}
86
+ });
87
+ }
88
+ /**
89
+ * A file or folder on this machine the command could not open (`file save --dir`, `profile backup --out`): the
90
+ * person's path, not the engine refusing, so it says so with the code a bot branches on. Before, a folder that did
91
+ * not exist was `engine` (exit 1) with Node's `ENOENT: no such file or directory, open '…'`.
92
+ */
93
+ function localFileError(error) {
94
+ if (!error || typeof error !== "object" || typeof error.syscall !== "string") return void 0;
95
+ const { code, path } = error;
96
+ const where = typeof path === "string" ? path : "the path";
97
+ if (code === "ENOENT" || code === "ENOTDIR") return new CliError("not_found", `No such file or folder: ${where}`);
98
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") return new CliError("refused", `Not allowed to write or read ${where}`);
99
+ if (code === "EISDIR") return new CliError("bad_request", `${where} is a folder: name a file`);
100
+ if (code === "ENAMETOOLONG") return new CliError("bad_request", `A name in ${where} is too long for this file system (at most 255 bytes)`);
101
+ }
102
+ //#endregion
103
+ export { EXIT as n, asCliError as r, CliError as t };
104
+
105
+ //# sourceMappingURL=errors-DO5z6h_l.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DO5z6h_l.js","names":[],"sources":["../../src/errors.ts"],"sourcesContent":["/**\n * Errors that reach a bot: a stable `code` to branch on, a message for people, and the exit status the CLI\n * leaves with (WISP 1100 § Exit codes). The daemon sends `{code, message}` over the socket; the CLI prints\n * `{\"error\": {code, message}}` on stdout and exits with `exit`.\n */\nexport type ErrorCode =\n | \"bad_request\" | \"usage\" | \"not_found\" | \"refused\" | \"unavailable\" | \"confirm\" | \"timeout\"\n | \"engine\" | \"busy\" | \"internal\";\n\nexport const EXIT: Record<ErrorCode, number> = {\n engine: 1, refused: 1, unavailable: 1, bad_request: 1, busy: 1, internal: 1,\n usage: 2, not_found: 3, timeout: 4, confirm: 5,\n};\n\nexport class CliError extends Error {\n constructor(readonly code: ErrorCode, message: string, readonly details?: Record<string, unknown>) {\n super(message);\n this.name = \"CliError\";\n }\n toJSON(): { code: ErrorCode; message: string; details?: Record<string, unknown> } {\n return { code: this.code, message: this.message, ...(this.details ? { details: this.details } : {}) };\n }\n}\n\n/** Anything thrown, as the error a bot sees: the engine's own refusals keep their words. */\nexport function asCliError(error: unknown): CliError {\n if (error instanceof CliError) return error;\n if (error && typeof error === \"object\" && \"code\" in error && \"message\" in error && typeof (error as { code: unknown }).code === \"string\"\n && Object.hasOwn(EXIT, (error as { code: string }).code)) return new CliError((error as { code: ErrorCode }).code, String((error as { message: unknown }).message));\n // A contact the chat's link could not reach (its `GhostlyHttpError` \"unreachable\"): not there now, not the engine failing.\n if (error instanceof Error && (error as { code?: unknown }).code === \"unreachable\") return new CliError(\"unavailable\", error.message);\n const known = engineKnown(error);\n if (known) return known;\n const local = localFileError(error);\n if (local) return local;\n const unopened = profileOpenError(error);\n if (unopened) return unopened;\n const message = error instanceof Error ? error.message : String(error);\n if (/real money|confirmedReal|Mainnet/i.test(message) && /confirm/i.test(message)) return new CliError(\"confirm\", message);\n if (/other network|cross-network|network does not match/i.test(message)) return new CliError(\"refused\", message);\n return new CliError(\"engine\", message);\n}\n\n/**\n * The engine's known errors (`EngineError`, packages/core engineErrors.ts) a bot branches on, by their `engineCode`: a\n * host or the network not answering is `unavailable` (try again later), one too slow `timeout`, a value the person gave\n * `bad_request`, money that is not there `refused`. Any other keeps `engine`. Read by its shape, as `profileOpenError`.\n */\nconst ENGINE_CODES: Record<string, ErrorCode> = {\n mintUnreachable: \"unavailable\", lnurlUnreachable: \"unavailable\", hostUnreachable: \"unavailable\", networkUnreachable: \"unavailable\", usdtRpcUnavailable: \"unavailable\",\n hostTimedOut: \"timeout\", networkTimedOut: \"timeout\",\n invalidMintUrl: \"bad_request\", mintNotHttps: \"bad_request\", invalidMaxFee: \"bad_request\",\n notEnoughSats: \"refused\", ecashAlreadySpent: \"refused\",\n};\n\nfunction engineKnown(error: unknown): CliError | undefined {\n if (!(error instanceof Error)) return undefined;\n const { engineCode, values } = error as { engineCode?: unknown; values?: { host?: unknown } };\n if (typeof engineCode !== \"string\" || !Object.hasOwn(ENGINE_CODES, engineCode)) return undefined;\n // The app's reason for a Lightning address it could not read (a browser's CORS) is no reason in a terminal.\n if (engineCode === \"lnurlUnreachable\" && typeof values?.host === \"string\") return new CliError(\"unavailable\", `Could not reach ${values.host}`);\n return new CliError(ENGINE_CODES[engineCode], error.message);\n}\n\n/**\n * The profile's database did not open (the engine's `ProfileOpenError`, packages/browser/src/shared/idb.ts): `engine`\n * (exit 1) with the engine's own words, which name the versions when a newer ghostly stored it, and the reason and\n * the versions in `details` for a bot. Read by its shape: this module is also the thin client's, which loads no engine.\n */\nfunction profileOpenError(error: unknown): CliError | undefined {\n if (!(error instanceof Error) || error.name !== \"ProfileOpenError\") return undefined;\n const failure = (error as { failure?: { reason?: unknown; storedVersion?: unknown; supportedVersion?: unknown } }).failure;\n if (!failure || typeof failure.reason !== \"string\") return undefined;\n return new CliError(\"engine\", error.message, {\n reason: failure.reason === \"newer\" ? \"newer_profile\" : `profile_${failure.reason}`,\n ...(typeof failure.storedVersion === \"number\" ? { storedVersion: failure.storedVersion } : {}),\n ...(typeof failure.supportedVersion === \"number\" ? { supportedVersion: failure.supportedVersion } : {}),\n });\n}\n\n/**\n * A file or folder on this machine the command could not open (`file save --dir`, `profile backup --out`): the\n * person's path, not the engine refusing, so it says so with the code a bot branches on. Before, a folder that did\n * not exist was `engine` (exit 1) with Node's `ENOENT: no such file or directory, open '…'`.\n */\nfunction localFileError(error: unknown): CliError | undefined {\n if (!error || typeof error !== \"object\" || typeof (error as { syscall?: unknown }).syscall !== \"string\") return undefined;\n const { code, path } = error as { code?: unknown; path?: unknown };\n const where = typeof path === \"string\" ? path : \"the path\";\n if (code === \"ENOENT\" || code === \"ENOTDIR\") return new CliError(\"not_found\", `No such file or folder: ${where}`);\n if (code === \"EACCES\" || code === \"EPERM\" || code === \"EROFS\") return new CliError(\"refused\", `Not allowed to write or read ${where}`);\n if (code === \"EISDIR\") return new CliError(\"bad_request\", `${where} is a folder: name a file`);\n if (code === \"ENAMETOOLONG\") return new CliError(\"bad_request\", `A name in ${where} is too long for this file system (at most 255 bytes)`);\n return undefined;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fedimint_client_wasm_bg-BtHHlFpp.js","names":["import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"sources":["../../../../node_modules/@fedimint/fedimint-client-wasm-bundler/fedimint_client_wasm_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"ignoreList":[0],"mappings":";AAAA,IAAA,kCAAe,IAAA,IAAA,2CAAA,YAAA,GAAmD,CAAA,CAAnDA"}
1
+ {"version":3,"file":"fedimint_client_wasm_bg-BtHHlFpp.js","names":["import.meta.ROLLDOWN_FILE_URL_VRAku6fjghkApIISiBWPzg"],"sources":["../../../../node_modules/@fedimint/fedimint-client-wasm-bundler/fedimint_client_wasm_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_VRAku6fjghkApIISiBWPzg"],"ignoreList":[0],"mappings":";AAAA,IAAA,kCAAe,IAAA,IAAA,2CAAA,YAAA,GAAmD,CAAA,CAAnDA"}
@@ -1,4 +1,4 @@
1
- import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace, u as digestText } from "./fileBytes-BcYZhNgF.js";
1
+ import { c as fileSpace, f as FILE_BYTES_STEP, i as checkFileId, p as digestText } from "./fileBytes-Di7VZtmU.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-Dbifu5I5.js.map
154
+ //# sourceMappingURL=fileBytes-CfSXjTFa.js.map
@@ -1 +1 @@
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
+ {"version":3,"file":"fileBytes-CfSXjTFa.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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"}
@@ -0,0 +1,2 @@
1
+ import { a as dropFileSpace, c as fileSpace, d as resetFileBytes, f as FILE_BYTES_STEP, i as checkFileId, l as registerFileBytes, n as SMALL_FILE_BYTES, o as fileBytes, p as digestText, r as blobDigest, s as fileBytesOf, t as NO_ROOM_TO_PLAY, u as removeFileBytes } from "./fileBytes-Di7VZtmU.js";
2
+ export { FILE_BYTES_STEP, NO_ROOM_TO_PLAY, SMALL_FILE_BYTES, blobDigest, checkFileId, digestText, dropFileSpace, fileBytes, fileBytesOf, fileSpace, registerFileBytes, removeFileBytes, resetFileBytes };
@@ -1,5 +1,4 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
- import { o as databaseName } from "./idb-CTF6LF5B.js";
1
+ import { o as databaseName } from "./idb-B5L8wuOm.js";
3
2
  import { sha256 } from "@noble/hashes/sha2.js";
4
3
  //#region ../browser/src/shared/fileBytesCommon.ts
5
4
  /** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */
@@ -19,20 +18,8 @@ function digestText(bytes) {
19
18
  }
20
19
  //#endregion
21
20
  //#region ../browser/src/shared/fileBytes.ts
22
- var fileBytes_exports = /* @__PURE__ */ __exportAll({
23
- FILE_BYTES_STEP: () => FILE_BYTES_STEP,
24
- SMALL_FILE_BYTES: () => SMALL_FILE_BYTES,
25
- blobDigest: () => blobDigest,
26
- checkFileId: () => checkFileId,
27
- digestText: () => digestText,
28
- dropFileSpace: () => dropFileSpace,
29
- fileBytes: () => fileBytes,
30
- fileBytesOf: () => fileBytesOf,
31
- fileSpace: () => fileSpace,
32
- registerFileBytes: () => registerFileBytes,
33
- removeFileBytes: () => removeFileBytes,
34
- resetFileBytes: () => resetFileBytes
35
- });
21
+ /** Why `FileBytes.stream` refused a file this device holds: no room on the disk for the player's copy of it. */
22
+ var NO_ROOM_TO_PLAY = "no_room_to_play";
36
23
  /** Files sent from memory-sized sources stay whole in IndexedDB; anything larger goes through a `FileBytes`. */
37
24
  var SMALL_FILE_BYTES = 16777216;
38
25
  var ID = /^[A-Za-z0-9_-]{1,200}$/;
@@ -64,7 +51,7 @@ async function blobDigest(blob, onProgress) {
64
51
  }
65
52
  return digestText(hash.digest());
66
53
  }
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())]]);
54
+ var makers = /* @__PURE__ */ new Map([["opfs", () => import("./fileBytesOpfs-Dd1N4ERH.js").then((m) => m.OpfsFileBytes.open())], ["idb", () => import("./fileBytesIdb-CFP-DSkZ.js").then((m) => new m.IdbFileBytes())]]);
68
55
  var made = /* @__PURE__ */ new Map();
69
56
  var preferred = ["opfs", "idb"];
70
57
  /** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */
@@ -106,6 +93,6 @@ function resetFileBytes(order = ["opfs", "idb"]) {
106
93
  preferred = order;
107
94
  }
108
95
  //#endregion
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 };
96
+ export { dropFileSpace as a, fileSpace as c, resetFileBytes as d, FILE_BYTES_STEP as f, checkFileId as i, registerFileBytes as l, SMALL_FILE_BYTES as n, fileBytes as o, digestText as p, blobDigest as r, fileBytesOf as s, NO_ROOM_TO_PLAY as t, removeFileBytes as u };
110
97
 
111
- //# sourceMappingURL=fileBytes-BcYZhNgF.js.map
98
+ //# sourceMappingURL=fileBytes-Di7VZtmU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileBytes-Di7VZtmU.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`. Rejects with\n * `NO_ROOM_TO_PLAY` when the disk has no room for the copy the player makes of it (Linux's WebKitGTK): save it instead.\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/** Why `FileBytes.stream` refused a file this device holds: no room on the disk for the player's copy of it. */\nexport const NO_ROOM_TO_PLAY = \"no_room_to_play\";\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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"}
@@ -0,0 +1,2 @@
1
+ import { t as IdbFileBytes } from "./fileBytesIdb-D5u6H8X_.js";
2
+ export { IdbFileBytes };
@@ -1,5 +1,5 @@
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";
1
+ import { i as STORES, m as wrap, u as openDb } from "./idb-B5L8wuOm.js";
2
+ import { f as FILE_BYTES_STEP, i as checkFileId, p as digestText } from "./fileBytes-Di7VZtmU.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;
@@ -226,6 +226,6 @@ async function storageRoom() {
226
226
  }
227
227
  }
228
228
  //#endregion
229
- export { IdbFileBytes, storageRoom };
229
+ export { storageRoom as n, IdbFileBytes as t };
230
230
 
231
- //# sourceMappingURL=fileBytesIdb-DzSaqI0A.js.map
231
+ //# sourceMappingURL=fileBytesIdb-D5u6H8X_.js.map