@ghostlytools/cli 1.0.1 → 1.0.3
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.
- package/README.md +10 -7
- package/dist/assets/bark_ffi_wasm_bg-EY2uc0Bx.js.map +1 -1
- package/dist/assets/bitcoindevkit_bg-B-iLWrmX.js.map +1 -1
- package/dist/assets/fileBytes-Br_MpeNT.js +123 -0
- package/dist/assets/fileBytes-Br_MpeNT.js.map +1 -0
- package/dist/assets/{fileBytes-CLv0gryb.js → fileBytes-CpP9Vj7s.js} +2 -2
- package/dist/assets/{fileBytes-CLv0gryb.js.map → fileBytes-CpP9Vj7s.js.map} +1 -1
- package/dist/assets/{fileBytesIdb-CKCtzMTy.js → fileBytesIdb-vubSXPoO.js} +3 -2
- package/dist/assets/{fileBytesIdb-CKCtzMTy.js.map → fileBytesIdb-vubSXPoO.js.map} +1 -1
- package/dist/assets/{fileBytesOpfs-By55XVTI.js → fileBytesOpfs-6iSa6u2K.js} +20 -8
- package/dist/assets/fileBytesOpfs-6iSa6u2K.js.map +1 -0
- package/dist/assets/{hyperdhtRelay-_rJsllcP.js → hyperdhtRelay-VM9STKLd.js} +3 -3
- package/dist/assets/{hyperdhtRelay-_rJsllcP.js.map → hyperdhtRelay-VM9STKLd.js.map} +1 -1
- package/dist/assets/idb-DIN_Miwb.js +351 -0
- package/dist/assets/idb-DIN_Miwb.js.map +1 -0
- package/dist/assets/{mainline-B0mUxYPH.js → mainline-CcO2f8jy.js} +17 -10
- package/dist/assets/mainline-CcO2f8jy.js.map +1 -0
- package/dist/assets/restoredRows-tVxgyG23.js +79 -0
- package/dist/assets/restoredRows-tVxgyG23.js.map +1 -0
- package/dist/assets/{server-C-8wbsAX.js → server-BnNXYzR9.js} +2371 -358
- package/dist/assets/server-BnNXYzR9.js.map +1 -0
- package/dist/assets/src-7sdQpCoi.js +23665 -0
- package/dist/assets/src-7sdQpCoi.js.map +1 -0
- package/dist/ghostly.mjs +933 -23260
- package/dist/ghostly.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/assets/fileBytesOpfs-By55XVTI.js.map +0 -1
- package/dist/assets/mainline-B0mUxYPH.js.map +0 -1
- package/dist/assets/server-C-8wbsAX.js.map +0 -1
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
105
105
|
| `chat request <chat> <sats> [--memo t] [--method m] [--rail r]`, `chat pay-request <chat> <payment>` | Ask a contact to pay; pay the contact's request |
|
|
106
106
|
| `chat transport <chat> <auto\|dht\|webrtc\|iroh\|hyperdht>` | What carries the chat |
|
|
107
107
|
| `chat verify <chat> --code <code>` | Mark the contact verified after comparing the codes out of band |
|
|
108
|
-
| `chat wait <chat> [--until live\|text\|paired] [--timeout s]` | Wait for a chat to go live, carry text, or see its contact |
|
|
108
|
+
| `chat wait <chat> [--until live\|text\|paired] [--timeout s]` | Wait for a chat to go live, carry text, or see its contact. Live means a session that exists now, in this run of the daemon: a chat that was live before a restart counts once its contact answers again |
|
|
109
109
|
| `daemon [--detach]`, `daemon status`, `daemon stop`, `daemon restart` | Keep the profile online; `status` names its version and its socket (`socket`, for the [socket API](#the-socket-api)); `restart` stops it and starts this release's code in the background ([After an upgrade](#after-an-upgrade)). Ctrl-C, SIGTERM and the terminal closing (SIGHUP) stop it cleanly |
|
|
110
110
|
| `edit <chat> <message> [text… \| --text <text> \| --stdin] [--force] [--wait none\|confirmed] [--timeout s]` | Replace the text of a message you sent (1:1 chats, `group edit` for a group; `<message>` is the `messageId` `send` gave, or its wire id). The contact sees it in place, marked edited; `--wait confirmed` waits for its app to confirm (the default without a daemon). At most 100 edits a message, no time limit; an older contact app gets it once it shows edits |
|
|
111
111
|
| `engine <method> [json \| -] [--confirm-real] [--show-secret]`, `engine --list` | Any call of the app's engine |
|
|
@@ -116,16 +116,16 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
116
116
|
| `file send <chat> <path> [--name n] [--mime t] [--voice [ms] [--peaks …]] [--reply <message>]` | A file, or a voice note (its length and waveform measured from the file unless given); `--reply` quotes a message, as `send --reply` does |
|
|
117
117
|
| `file wait [<chat>] <file> [--timeout s]` | Wait until a transfer ends: exit `0` when the file is all here, `1` with the transfer's error when it failed (`details.retry`: `file resend` can go on), `4` on timeout (default 300 s) |
|
|
118
118
|
| `forward <chat\|group> <message>… --to <chat\|group>… [--force] [--wait none\|sent] [--timeout s]` | Forward messages to up to 5 chats and groups (`--to` again for each; `group:<id>` names a group when a chat has the same name; an id that starts with a dash is taken as it is, `--to -Ab…` or `--to=-Ab…`). Each is a new message of yours that says it was forwarded (`forwarded`, the hop count) and nothing of who wrote it. Files go from the bytes this profile holds, never fetched again; a group takes texts only. A text that looks like a seed, a key or ecash needs `--force`. Answers `{from, results: [{to, kind, messageIds, error}]}`; any refusal fails the command (exit 1) with the results in `details`. `--wait sent`: each text on its way (a group's taken by an edge), each file's transfer done |
|
|
119
|
-
| `group create <name> [--mesh]`, `group join <link>` | A community (a link anyone can open), or a private mesh |
|
|
120
|
-
| `group edit <group> <message> [text… \| --text <text> \| --stdin] [--mention <member>]… [--force]` | Replace the text of a message you sent to the group (`<message>` is the `messageId` `group send` gave). Every member sees it in place, marked edited; mentions whose `@name` is still in the text stay, `--mention` adds more. Answers `{group, messageId, edits, sent, edges}`; `--wait sent` and `--timeout` as for `group send`. A private group's members whose edge is down get it when it opens, a community's with the catch-up; a member on an older app keeps the old text |
|
|
119
|
+
| `group create <name> [--mesh]`, `group join <link>` | A community (a link anyone can open), or a private mesh. `group join` with the link of a community the profile was removed from asks to be let in again, and keeps its history |
|
|
120
|
+
| `group edit <group> <message> [text… \| --text <text> \| --stdin] [--mention <member>]… [--force]` | Replace the text of a message you sent to the group (`<message>` is the `messageId` `group send` gave). Every member sees it in place, marked edited; mentions whose `@name` is still in the text stay, `--mention` adds more. Answers `{group, messageId, edits, sent, edges}`; `--wait sent` and `--timeout` as for `group send`. The text the message already has is no edit: `sent: false, unchanged: true`. A private group's members whose edge is down get it when it opens, a community's with the catch-up; a member on an older app keeps the old text |
|
|
121
121
|
| `group hub <group> <member> [--pin \| --exclude \| --auto]` | Past 16 members: pin a member as a hub, keep one from being a hub, or leave it to their app (a daemon offers itself) |
|
|
122
122
|
| `group invite <group> <chat>`, `group remove <group> <member>`, `group admin <group> <member>` | Membership, for the admin (each prints the group; `--show-secret` for its link) |
|
|
123
123
|
| `group leave <group>`, `group forget <group> --yes`, `group accept\|decline <group>` | Membership |
|
|
124
124
|
| `group list [--show-secret]`, `group show <group> [--show-secret]`, `group history <group>` | Groups, members, history; a message names its author (`member` key, `nick` from the roster, or the name an author no longer in it had). The entry link prints as `<hidden>` without `--show-secret` |
|
|
125
|
-
| `group react <group> <message> <emoji> [--remove]` | React to a group's message |
|
|
125
|
+
| `group react <group> <message> <emoji> [--remove]` | React to a group's message. `refused` when the profile is not a member any more |
|
|
126
126
|
| `group rename <group> <name>` | A new name, for the admin (1 to 64 characters on one line); the picture stays. Members see it once it reaches them; until then, and on apps from before names, the group keeps the name it had when they got in |
|
|
127
127
|
| `group rotate <group>`, `group link <group> [--off] [--reset]`, `group picture <group> <jpeg> \| --clear` | A fresh secret; the link (printed: asking for it is asking for the secret); the picture |
|
|
128
|
-
| `group send <group> [text…] [--mention <member>]… [--reply <message>] [--button id:Label]… [--style id=primary\|neutral\|danger]… [--once] [--id id] [--wait none\|sent] [--timeout s]` | Send; each mentioned member is written as `@name` in the text. `--button` puts buttons under it, as `send --button`. Answers `{group, messageId, sent, edges}`: the id `--reply`, `group react` and `group edit` take,
|
|
128
|
+
| `group send <group> [text…] [--mention <member>]… [--reply <message>] [--button id:Label]… [--style id=primary\|neutral\|danger]… [--once] [--id id] [--wait none\|sent] [--timeout s]` | Send; each mentioned member is written as `@name` in the text. `--button` puts buttons under it, as `send --button`. Answers `{group, messageId, sent, edges}`: the id `--reply`, `group react` and `group edit` take, how many edges took it so far, and `sent`, true once one did. A profile that is not a member any more (removed from the group) gets `refused`, with the reason, and nothing is sent. A group has no receipts: `--wait sent` waits until at least one edge took it (a member's in a private group, one of your hubs' in a community), and exits 4 after `--timeout` (default 30 s) if none did; the message still goes when an edge opens, while the profile is online. Use it for a one-shot send with no daemon |
|
|
129
129
|
| `group typing <group> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | `typing` in a private group: the members whose edge is open see "Ana is typing…" (or recording, thinking, or your status line), with the same rules, 6 s hold and `--for`. Answers `{group, typing, kind, status, reached, sendTyping}` (`reached`: members with an open edge). A community does not carry typing yet (exit 1, `unavailable`) |
|
|
130
130
|
| `identity add <provider> [subject] [--signer id] [--field name=value]… [--days n]` | A proof: an in-app signer (NIP-46 and the like) finishes here; a tool or a published record answers with the statement |
|
|
131
131
|
| `identity complete <draft> [--evidence-file f \| --stdin]`, `identity cancel <draft>` | Finish it with the tool's output (or nothing, for a published record); needs the daemon that began it. Cancel drops one not finished |
|
|
@@ -139,7 +139,7 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
139
139
|
| `pay <invoice\|address\|lnurl> [--amount sats] [--network n] [--max-fee sats] [--confirm-real]` | Pay over Lightning (Testnet unless `--network mainnet`) |
|
|
140
140
|
| `payment list [--chat c]`, `payment check <chat> <payment>`, `payment reclaim <payment>` | Payments and requests |
|
|
141
141
|
| `pin <chat\|group> <message> [--remove]` | Pin a message at the top of a chat or group, for everyone in it: one per chat, a new pin replaces it, `--remove` unpins. In a community only the admin pins. A 1:1 pin reaches the contact on the next live session; the other side hears `chat.pinned` or `group.pinned` |
|
|
142
|
-
| `profile backup --out <file>`, `profile restore <file> <new profile>` |
|
|
142
|
+
| `profile backup --out <file>`, `profile restore <file> <new profile>` | A backup (WISP 05 envelope, written and read a piece at a time), encrypted with the passphrase from `--passphrase-file` or `GHOSTLY_BACKUP_PASSPHRASE`. `--no-passphrase` makes one that is not encrypted: the file then holds the profile's keys and wallet secrets in the clear. Neither given, nothing is made |
|
|
143
143
|
| `profile create <name> [--use] [--name <shown>]`, `profile list`, `profile use <name>` | Profiles |
|
|
144
144
|
| `profile picture <jpeg> \| --clear` | The picture contacts see (a JPEG within 512 px; 128 px is what the app sends) |
|
|
145
145
|
| `profile show`, `profile set [--name <name>] [--share-profile \| --no-share-profile]` | The name contacts see |
|
|
@@ -150,7 +150,7 @@ The rows are in alphabetical order of their first command, and every command of
|
|
|
150
150
|
| `service peer <chat>`, `service open <chat> <service> [--port p]`, `service close <chat> <service>` | A contact's app on a loopback port here (daemon). `url` is a link for your browser, `http://<random>.localhost:<port>/.ghostly-open/<token>`: it sets the service's cookie and shows the app on a host name of its own; any other host name, or a request without that cookie, gets 404. Chrome, Edge and curl reach `*.localhost` names by themselves; Safari and Node's `fetch` on macOS may not (add the name to `/etc/hosts` for them) |
|
|
151
151
|
| `settings get [--show-secret]`, `settings set <key> <json>` | Relays, Iroh relays, the HyperDHT relay, ICE servers, `sendTyping` (false: contacts are never told you type), … |
|
|
152
152
|
| `status` | The profile, its chats, whether WebRTC and calls run, the last event seq |
|
|
153
|
-
| `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n] [--branch b] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 4xx · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
|
|
153
|
+
| `task send <chat\|group> --title <title> [--status s] [--progress n \| --steps a/b] [--step "…"] [--item state:text]… [--pr-url url --pr-number n --additions n --deletions n --files n --pr-state s --pr-checks c] [--branch b] [--tag label]… [--parent task] [--link url]… [--id id] [--json json\|-\|file]`, `task update <chat\|group> <task> [fields…]` | A task card for a bot's work (WISP 4xx · Status Cards): a title, a status (queued, running, blocked, done, failed, cancelled), progress, the step it is on, up to 20 steps or log lines (`--item done:Codec`; a lowercase word before the colon must be an item state: pending, running, done, failed or skipped) and its pull request, shown as a small card that apps without cards read as text. Optional, and ignored by apps from before them: where the pull request stands (`--pr-state draft`, `open`, `merged` or `closed`; an open one puts the task in the board's Review column) and its checks (`--pr-checks passing`, `failing` or `pending`), up to 3 tags of 24 characters (`--tag core --tag web`; an update's tags replace the card's), and `--parent <task>`, a task of yours in the same chat that this one is a part of (sent first; its parts stack under it on the board). `send` prints the task's id and message; `update` merges the fields given over the card (an update's `--item` list replaces the card's: give all of them) and edits its message, at most once per 2.5 s (sooner updates merge into the next). `--json` takes the whole card; `null` in it removes a field. Only the CLI sends cards: the app shows them |
|
|
154
154
|
| `typing <chat> [--kind typing\|recording\|thinking] [--status "<text>"] [--for s] [--stop]` | Show the contact you are writing, recording or thinking, or a short status line in its place ("Transcribing your audio…", 40 characters, no links): live chats only, it holds 6 s there; `--for s` keeps it on that long (up to 600 s; a one-shot stays that long); a new kind or status shows at once; a message to the chat or `--stop` ends it |
|
|
155
155
|
| `wallet create <type> [--network testnet] [--provider id] [--value name=value]… [--stdin] [--invite code] [--api-key key]` | A wallet: `cashu`, `lightning` (a card: its source's form in `--value`), `arkade`, `spark` (on Mainnet, `--api-key` is your Breez API key), `bitcoin` (BDK), `fedimint` (`--invite` is the federation's invite code), `usdt`. With `--stdin`, secret fields come as `name=value` lines on stdin (`api-key=…` too), out of `ps` and the shell's history |
|
|
156
156
|
| `wallet faucet <type>`, `wallet add-mint <url> [--primary]` | Test coins; another Cashu mint |
|
|
@@ -263,6 +263,9 @@ commands, `group.created` events and `engine getState` print it as `<hidden>`; `
|
|
|
263
263
|
{"seq":6,"id":"message.received:f3gg…:peer_jY7N…","type":"message.received","at":1790450767762,"chat":"f3gg…","message":{"id":"peer_jY7N…","chat":"f3gg…","from":"peer","text":"hello bob","timestamp":1790450767735,"delivery":null,"deliveryError":null,"via":"datalink","nick":"Alice"}}
|
|
264
264
|
```
|
|
265
265
|
|
|
266
|
+
- A message's `timestamp` is its place in the history, which `chat history` and `group history` are ordered by: when
|
|
267
|
+
this profile sent it, or when it arrived here. A received message also has `sentAt`, the time its sender's clock
|
|
268
|
+
said, which orders nothing (a clock can be wrong).
|
|
266
269
|
- `seq` grows by one per event in the profile, across restarts; `id` is the same whenever the same fact is reported.
|
|
267
270
|
Dedupe by `id`; resume with `--since <seq>`, or let `--cursor <file>` remember the last event handled.
|
|
268
271
|
- Types: `daemon.started`, `chat.created`, `chat.removed`, `chat.renamed`, `chat.pairing` (`stage`: publishing,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bark_ffi_wasm_bg-EY2uc0Bx.js","names":["import.meta.
|
|
1
|
+
{"version":3,"file":"bark_ffi_wasm_bg-EY2uc0Bx.js","names":["import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"sources":["../../../../node_modules/@secondts/bark/web/bark_ffi_wasm_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_FKNdLzKnlftW3t_gzo81yw"],"x_google_ignoreList":[0],"mappings":";AAAA,IAAA,2BAAe,IAAA,IAAA,oCAAA,YAAA,GAAmD,CAAA,CAAnDA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.
|
|
1
|
+
{"version":3,"file":"bitcoindevkit_bg-B-iLWrmX.js","names":["import.meta.ROLLDOWN_FILE_URL_WPGYjwIlzWVNM1bYhOc83w"],"sources":["../../../../node_modules/@bitcoindevkit/bdk-wallet-web/bitcoindevkit_bg.wasm?url"],"sourcesContent":["export default import.meta.ROLLDOWN_FILE_URL_WPGYjwIlzWVNM1bYhOc83w"],"x_google_ignoreList":[0],"mappings":";AAAA,IAAA,2BAAe,IAAA,IAAA,oCAAA,YAAA,GAAmD,CAAA,CAAnDA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { i as databaseName } from "./idb-DIN_Miwb.js";
|
|
2
|
+
import "node:module";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
|
+
//#region \0rolldown/runtime.js
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) __defProp(target, name, {
|
|
9
|
+
get: all[name],
|
|
10
|
+
enumerable: true
|
|
11
|
+
});
|
|
12
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
return target;
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region ../browser/src/shared/fileBytesCommon.ts
|
|
17
|
+
/** What `fileBytes.ts` and its worker share: the worker is built alone, so this imports nothing. */
|
|
18
|
+
/** Reads and copies move this much at a time. */
|
|
19
|
+
var FILE_BYTES_STEP = 1048576;
|
|
20
|
+
var BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
21
|
+
/** base64url without padding (the worker has no @ghostly/core). */
|
|
22
|
+
function digestText(bytes) {
|
|
23
|
+
let out = "";
|
|
24
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
25
|
+
const n = bytes[i] << 16 | (bytes[i + 1] ?? 0) << 8 | (bytes[i + 2] ?? 0);
|
|
26
|
+
out += BASE64URL[n >> 18 & 63] + BASE64URL[n >> 12 & 63];
|
|
27
|
+
if (i + 1 < bytes.length) out += BASE64URL[n >> 6 & 63];
|
|
28
|
+
if (i + 2 < bytes.length) out += BASE64URL[n & 63];
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region ../browser/src/shared/fileBytes.ts
|
|
34
|
+
var fileBytes_exports = /* @__PURE__ */ __exportAll({
|
|
35
|
+
FILE_BYTES_STEP: () => FILE_BYTES_STEP,
|
|
36
|
+
SMALL_FILE_BYTES: () => SMALL_FILE_BYTES,
|
|
37
|
+
blobDigest: () => blobDigest,
|
|
38
|
+
checkFileId: () => checkFileId,
|
|
39
|
+
digestText: () => digestText,
|
|
40
|
+
dropFileSpace: () => dropFileSpace,
|
|
41
|
+
fileBytes: () => fileBytes,
|
|
42
|
+
fileBytesOf: () => fileBytesOf,
|
|
43
|
+
fileSpace: () => fileSpace,
|
|
44
|
+
registerFileBytes: () => registerFileBytes,
|
|
45
|
+
removeFileBytes: () => removeFileBytes,
|
|
46
|
+
resetFileBytes: () => resetFileBytes
|
|
47
|
+
});
|
|
48
|
+
/** Files sent from memory-sized sources stay whole in IndexedDB; anything larger goes through a `FileBytes`. */
|
|
49
|
+
var SMALL_FILE_BYTES = 16777216;
|
|
50
|
+
var ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
51
|
+
var SPACE = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
52
|
+
/** A file id every backend accepts as a file name as it is: never a path. */
|
|
53
|
+
function checkFileId(id) {
|
|
54
|
+
if (!ID.test(id)) throw new Error("Invalid file id");
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
/** This profile's folder name. */
|
|
58
|
+
function fileSpace() {
|
|
59
|
+
const space = databaseName();
|
|
60
|
+
if (!SPACE.test(space)) throw new Error("Invalid profile space");
|
|
61
|
+
return space;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* SHA-256 of a Blob, read a step at a time: memory stays at one step. Read with `slice().arrayBuffer()`, not
|
|
65
|
+
* `stream()`: in WebKit a Blob kept in IndexedDB that reads fine in slices failed as a stream ("The object can not be
|
|
66
|
+
* found here"), which failed a voice note after all its bytes had arrived.
|
|
67
|
+
*/
|
|
68
|
+
async function blobDigest(blob, onProgress) {
|
|
69
|
+
const hash = sha256.create();
|
|
70
|
+
for (let done = 0; done < blob.size;) {
|
|
71
|
+
const part = new Uint8Array(await blob.slice(done, Math.min(blob.size, done + FILE_BYTES_STEP)).arrayBuffer());
|
|
72
|
+
if (!part.length) throw new Error("The file is shorter than it says");
|
|
73
|
+
hash.update(part);
|
|
74
|
+
done += part.length;
|
|
75
|
+
onProgress?.(done);
|
|
76
|
+
}
|
|
77
|
+
return digestText(hash.digest());
|
|
78
|
+
}
|
|
79
|
+
var makers = /* @__PURE__ */ new Map([["opfs", () => import("./fileBytesOpfs-6iSa6u2K.js").then((m) => m.OpfsFileBytes.open())], ["idb", () => import("./fileBytesIdb-vubSXPoO.js").then((m) => new m.IdbFileBytes())]]);
|
|
80
|
+
var made = /* @__PURE__ */ new Map();
|
|
81
|
+
var preferred = ["opfs", "idb"];
|
|
82
|
+
/** How a backend is made. The platform registers its own (Desktop: `native`) before anything uses it. */
|
|
83
|
+
function registerFileBytes(kind, make, prefer = false) {
|
|
84
|
+
makers.set(kind, make);
|
|
85
|
+
made.delete(kind);
|
|
86
|
+
if (prefer) preferred = [kind, ...preferred.filter((k) => k !== kind)];
|
|
87
|
+
}
|
|
88
|
+
/** The backend of this kind, or null where this platform has none. */
|
|
89
|
+
function fileBytesOf(kind) {
|
|
90
|
+
let backend = made.get(kind);
|
|
91
|
+
if (!backend) {
|
|
92
|
+
const make = makers.get(kind);
|
|
93
|
+
backend = make ? make().catch(() => null) : Promise.resolve(null);
|
|
94
|
+
made.set(kind, backend);
|
|
95
|
+
}
|
|
96
|
+
return backend;
|
|
97
|
+
}
|
|
98
|
+
/** Where new files go: the first backend this platform has. */
|
|
99
|
+
async function fileBytes() {
|
|
100
|
+
for (const kind of preferred) {
|
|
101
|
+
const backend = await fileBytesOf(kind);
|
|
102
|
+
if (backend) return backend;
|
|
103
|
+
}
|
|
104
|
+
throw new Error("This device has no storage for files");
|
|
105
|
+
}
|
|
106
|
+
/** "Clear all data" and a deleted chat: the bytes of its files go with it, wherever they are. */
|
|
107
|
+
async function removeFileBytes(prefix = "") {
|
|
108
|
+
await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.removeWhere(prefix)));
|
|
109
|
+
}
|
|
110
|
+
/** A deleted profile's files, wherever they are. */
|
|
111
|
+
async function dropFileSpace(space) {
|
|
112
|
+
if (!SPACE.test(space)) throw new Error("Invalid profile space");
|
|
113
|
+
await Promise.all([...makers.keys()].map(async (kind) => (await fileBytesOf(kind))?.dropSpace?.(space)));
|
|
114
|
+
}
|
|
115
|
+
/** Tests: forget what was made and which backend comes first. */
|
|
116
|
+
function resetFileBytes(order = ["opfs", "idb"]) {
|
|
117
|
+
made.clear();
|
|
118
|
+
preferred = order;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { fileBytesOf as a, removeFileBytes as c, __exportAll as d, 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 };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=fileBytes-Br_MpeNT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileBytes-Br_MpeNT.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 /** 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/** 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 {
|
|
1
|
+
import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace, u as digestText } from "./fileBytes-Br_MpeNT.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-
|
|
154
|
+
//# sourceMappingURL=fileBytes-CpP9Vj7s.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileBytes-
|
|
1
|
+
{"version":3,"file":"fileBytes-CpP9Vj7s.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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as FILE_BYTES_STEP, r as checkFileId, u as digestText } from "./fileBytes-Br_MpeNT.js";
|
|
2
|
+
import { d as wrap, n as STORES, s as openDb } from "./idb-DIN_Miwb.js";
|
|
2
3
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
4
|
//#region ../browser/src/shared/fileBytesIdb.ts
|
|
4
5
|
var STEP = FILE_BYTES_STEP;
|
|
@@ -227,4 +228,4 @@ async function storageRoom() {
|
|
|
227
228
|
//#endregion
|
|
228
229
|
export { IdbFileBytes, storageRoom };
|
|
229
230
|
|
|
230
|
-
//# sourceMappingURL=fileBytesIdb-
|
|
231
|
+
//# sourceMappingURL=fileBytesIdb-vubSXPoO.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fileBytesIdb-CKCtzMTy.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-vubSXPoO.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 {
|
|
2
|
-
import { storageRoom } from "./fileBytesIdb-
|
|
1
|
+
import { l as FILE_BYTES_STEP, r as checkFileId, s as fileSpace } from "./fileBytes-Br_MpeNT.js";
|
|
2
|
+
import { storageRoom } from "./fileBytesIdb-vubSXPoO.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
|
|
@@ -9,7 +9,10 @@ import { storageRoom } from "./fileBytesIdb-CKCtzMTy.js";
|
|
|
9
9
|
var OpfsFileBytes = class OpfsFileBytes {
|
|
10
10
|
worker;
|
|
11
11
|
kind = "opfs";
|
|
12
|
-
|
|
12
|
+
/** Shared with the views `forSpace` makes: one worker, one run of request numbers. */
|
|
13
|
+
counter = { next: 1 };
|
|
14
|
+
/** Another profile's folder (`forSpace`); the active profile's when not set. */
|
|
15
|
+
fixedSpace;
|
|
13
16
|
waiting = /* @__PURE__ */ new Map();
|
|
14
17
|
constructor(worker) {
|
|
15
18
|
this.worker = worker;
|
|
@@ -19,7 +22,7 @@ var OpfsFileBytes = class OpfsFileBytes {
|
|
|
19
22
|
if ("progress" in reply) return pending.progress?.(reply.progress);
|
|
20
23
|
this.waiting.delete(reply.n);
|
|
21
24
|
if (reply.ok) pending.resolve(reply.value);
|
|
22
|
-
else pending.reject(new Error(reply.error));
|
|
25
|
+
else pending.reject(reply.name ? Object.assign(new Error(reply.error), { name: reply.name }) : new Error(reply.error));
|
|
23
26
|
};
|
|
24
27
|
worker.onerror = (event) => {
|
|
25
28
|
event.preventDefault();
|
|
@@ -37,8 +40,17 @@ var OpfsFileBytes = class OpfsFileBytes {
|
|
|
37
40
|
store.worker.terminate();
|
|
38
41
|
return null;
|
|
39
42
|
}
|
|
43
|
+
space() {
|
|
44
|
+
return this.fixedSpace ?? fileSpace();
|
|
45
|
+
}
|
|
46
|
+
/** A view on the same worker that reads and writes another profile's folder. */
|
|
47
|
+
forSpace(space) {
|
|
48
|
+
const view = Object.create(this);
|
|
49
|
+
view.fixedSpace = space;
|
|
50
|
+
return view;
|
|
51
|
+
}
|
|
40
52
|
call(request, transfer = [], progress) {
|
|
41
|
-
const n = this.next++;
|
|
53
|
+
const n = this.counter.next++;
|
|
42
54
|
return new Promise((resolve, reject) => {
|
|
43
55
|
this.waiting.set(n, {
|
|
44
56
|
resolve,
|
|
@@ -48,7 +60,7 @@ var OpfsFileBytes = class OpfsFileBytes {
|
|
|
48
60
|
this.worker.postMessage({
|
|
49
61
|
...request,
|
|
50
62
|
n,
|
|
51
|
-
space:
|
|
63
|
+
space: this.space()
|
|
52
64
|
}, transfer);
|
|
53
65
|
});
|
|
54
66
|
}
|
|
@@ -122,7 +134,7 @@ var OpfsFileBytes = class OpfsFileBytes {
|
|
|
122
134
|
async blob(id, type) {
|
|
123
135
|
await this.close(id);
|
|
124
136
|
try {
|
|
125
|
-
const file = await (await (await (await (await navigator.storage.getDirectory()).getDirectoryHandle("ghostly-files")).getDirectoryHandle(
|
|
137
|
+
const file = await (await (await (await (await navigator.storage.getDirectory()).getDirectoryHandle("ghostly-files")).getDirectoryHandle(this.space())).getFileHandle(checkFileId(id))).getFile();
|
|
126
138
|
return file.slice(0, file.size, type);
|
|
127
139
|
} catch {
|
|
128
140
|
return null;
|
|
@@ -142,4 +154,4 @@ var OpfsFileBytes = class OpfsFileBytes {
|
|
|
142
154
|
//#endregion
|
|
143
155
|
export { OpfsFileBytes };
|
|
144
156
|
|
|
145
|
-
//# sourceMappingURL=fileBytesOpfs-
|
|
157
|
+
//# sourceMappingURL=fileBytesOpfs-6iSa6u2K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fileBytesOpfs-6iSa6u2K.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 {
|
|
2
|
-
import { n as hyperdhtRelayProblem } from "./server-
|
|
1
|
+
import { oo as fromBase64Url } from "./src-7sdQpCoi.js";
|
|
2
|
+
import { n as hyperdhtRelayProblem } from "./server-BnNXYzR9.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-
|
|
342
|
+
//# sourceMappingURL=hyperdhtRelay-VM9STKLd.js.map
|