| 1 | diff --git a/README.md b/README.md |
| 2 | index 68b7995..3948262 100644 |
| 3 | --- a/README.md |
| 4 | +++ b/README.md |
| 5 | @@ -45,6 +45,10 @@ The web interface uses plain HTML pages and forms. Connect a Nostr signer with N |
| 6 | |
| 7 | The browsing pages provide repository lists, source files, branches, history, diffs, file previews, raw downloads, synchronization status, and storage status. Access follows the tenant's current permissions. |
| 8 | |
| 9 | +See [Files and private repositories](docs/files-and-private-repositories.md) for encrypted folders, resumable uploads, storage allowances, private Git hosting and Blossom draft support. |
| 10 | + |
| 11 | +See [Git collaboration](docs/git-collaboration.md) for issues, pull requests, conversation synchronization and participant outboxes. |
| 12 | + |
| 13 | The `/tools` page reports whether the browser supports native WebMCP controls. See [WebMCP tools](docs/webmcp.md) for browser-agent integration and authorization behavior. |
| 14 | |
| 15 | ## Deployment |
| 16 | diff --git a/docs/files-and-private-repositories.md b/docs/files-and-private-repositories.md |
| 17 | new file mode 100644 |
| 18 | index 0000000..9cf9e06 |
| 19 | --- /dev/null |
| 20 | +++ b/docs/files-and-private-repositories.md |
| 21 | @@ -0,0 +1,77 @@ |
| 22 | +# Files and private repositories |
| 23 | + |
| 24 | +Use **Files** to search your uploads, import a public HTTPS URL or Blossom URI, inspect a file, and download it. Owners and moderators can browse the tenant's full inventory. Other users see the files they have uploaded or claimed. |
| 25 | + |
| 26 | +## Encrypted files |
| 27 | + |
| 28 | +Choose **Encrypt and upload** to encrypt a file in the browser before sending it to the relay. The default uses a fresh AES-256-GCM key and nonce for each upload. The resulting share link keeps the key, nonce, original name and file type in its URL fragment. Keep that link: the relay cannot recover the key. Anyone who has the complete link and permission to download the blob can decrypt it. |
| 29 | + |
| 30 | +The optional **Deduplicated key** mode follows the [BUD-15 proposal](https://github.com/hzrd149/blossom/pull/104). It derives the encryption key from the file's content, allowing identical files to share storage. This also reveals when files are identical and permits guesses about predictable content. Use the default random key for sensitive or predictable files. Both modes verify the ciphertext hash before decrypting; the deduplicated mode also verifies the plaintext hash. |
| 31 | + |
| 32 | +Encrypted files remain subject to the tenant's download policy. Sending an encrypted link does not grant membership in a private tenant. Removing a file or revoking membership cannot recall copies someone has already downloaded. |
| 33 | + |
| 34 | +Random-key files can be shared through [NIP-17 file messages](https://github.com/nostr-protocol/nips/blob/master/17.md). Connect a signer with NIP-44 support and enter the recipient's public key. Both parties need signed kind 10050 inbox relay lists available to this relay. The browser sends encrypted gift wraps to those inbox relays, including a copy for the sender. It refuses delivery when the required lists are missing. |
| 35 | + |
| 36 | +## Folders and large files |
| 37 | + |
| 38 | +Under **Files > Folders and large files**, choose a folder or a file to split into chunks. Files, chunks and all parent manifests use deduplicated keys. Names, child keys and file metadata appear only inside encrypted manifests. Keep the complete share link to browse the folder or download individual files. |
| 39 | + |
| 40 | +Folders use [draft BUD-16](https://github.com/hzrd149/blossom/pull/105). Large files use [draft BUD-17](https://github.com/hzrd149/blossom/pull/106), with 2 MiB plaintext chunks and up to 174 links per manifest. Larger directories use nested manifests that appear as one directory in the browser. Every retrieved object is checked against its hash before decryption, and file downloads verify their declared sizes. |
| 41 | + |
| 42 | +The browser workspace accepts up to 256 MiB per file, 1 GiB per folder, 10,000 files and 32 path levels. Browser folder selection includes files and their paths; empty folders are omitted. These browser limits apply in addition to the tenant's storage allowances. |
| 43 | + |
| 44 | +Cancel stops the current upload. Retry reuses the selected files while the page stays open. Completed chunks may remain in the Files inventory after an interrupted folder upload. Removing a root manifest does not remove its child blobs, which may be shared by other folders. |
| 45 | + |
| 46 | +## Resumable uploads |
| 47 | + |
| 48 | +Encrypted file uploads use [draft BUD-14](https://github.com/hzrd149/blossom/pull/102) when the server advertises it. Cancel and retry keep the encrypted bytes and key in memory while the page stays open. Closing or reloading the page loses that local state. The uploader falls back to BUD-13 when multipart support is unavailable. |
| 49 | + |
| 50 | +Other clients can send binary chunks to `PATCH /<sha256>` with `Upload-Type`, `Upload-Length`, `Upload-Offset` and `Content-Length`. Chunks may overlap or arrive out of order. A partial upload stays unavailable for download until its complete hash is verified. Blossom proofs name the final hash; NIP-98 proofs bind each chunk's request body. |
| 51 | + |
| 52 | +Partial uploads reserve the complete file size against the uploader's allowance. Reservations expire after 60 seconds of inactivity. Incomplete uploads can survive a server restart within that window. The draft provides no server endpoint for discovering received offsets, so clients must track their own progress. |
| 53 | + |
| 54 | +Temporary storage accepts chunks up to 8 MiB. A tenant can hold 1 GiB across 64 partial uploads, with up to 16 partial uploads per uploader and 32 chunk requests in flight. These bounds apply even when durable storage allowances are unlimited. |
| 55 | + |
| 56 | +## Upload limits |
| 57 | + |
| 58 | +Set byte allowances with an owner-authorized `setpolicy` patch: |
| 59 | + |
| 60 | +```json |
| 61 | +{ |
| 62 | + "fileLimits": { |
| 63 | + "maxFileBytes": 104857600, |
| 64 | + "userStorageBytes": 1073741824 |
| 65 | + } |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +This example limits each stored blob to 100 MiB and allows 1 GiB of claimed storage per uploader. Encrypted chunks and manifests each count toward storage. The per-blob limit applies to each chunk, rather than the reassembled size of a chunked file. Zero means unlimited. Changes apply to subsequent uploads; lowering an allowance does not delete existing files. |
| 70 | + |
| 71 | +Each uploader pays the full size of every file they claim, even when another uploader already stores identical bytes. Repeated uploads by the same user count once. Removing an uploader's claim releases their allowance; shared content remains while another claim exists. An owner can remove the stored file for everyone. |
| 72 | + |
| 73 | +Clients can check an upload with [BUD-06](https://github.com/hzrd149/blossom/blob/master/buds/06.md) `HEAD /upload`. The result is advisory; the upload itself checks the current limits. File descriptors include the optional Nostr metadata field defined by [BUD-08](https://github.com/hzrd149/blossom/blob/master/buds/08.md). A [BUD-13](https://github.com/hzrd149/blossom/pull/100) remote upload may name up to eight `url` sources. |
| 74 | + |
| 75 | +## Private Git hosting |
| 76 | + |
| 77 | +Private repositories require a dedicated [GRASP-08 private tenant](https://ngit.dev/protocol/grasp/specification/08). Enable `features.grasp` and `features.grasp08`, and set `reads` to `members` before publishing a private repository announcement. Members share access to the tenant's repository data. This is access control: the server still holds readable Git objects. |
| 78 | + |
| 79 | +Git clients authenticate with a signed kind 27235 event whose URL is the repository root ending in `.git` and whose method is `GET`. The proof can be reused across that repository's Git HTTP requests while its timestamp is within a minute of the server's clock. Relay events require NIP-42 authentication and current membership. |
| 80 | + |
| 81 | +Open tenants reject private repository announcements. A tenant that already holds a private repository announcement stays private, including after that announcement is removed. Policy changes and configuration imports cannot reopen a private tenant. Moving data to a public tenant requires a deliberate migration. |
| 82 | + |
| 83 | +Under **Manage > Connect**, edit your encrypted kind 10318 private service list. URLs and other private entries are encrypted to your own signer with NIP-44. The editor saves the event on the current relay; clients must be able to find it there. |
| 84 | + |
| 85 | +## Blossom drafts |
| 86 | + |
| 87 | +These formats may change before adoption. |
| 88 | + |
| 89 | +| Proposal | Support | What it adds | |
| 90 | +| --- | --- | --- | |
| 91 | +| [BUD-13](https://github.com/hzrd149/blossom/pull/100) | Server | Uploads to `/<sha256>` and imports from remote `url` sources | |
| 92 | +| [BUD-14](https://github.com/hzrd149/blossom/pull/102) | Server and browser | Resumable uploads with multipart `PATCH` | |
| 93 | +| [BUD-15](https://github.com/hzrd149/blossom/pull/104) | Browser | Optional deduplicated encryption | |
| 94 | +| [BUD-16](https://github.com/hzrd149/blossom/pull/105) | Browser | Encrypted folder manifests and browsing | |
| 95 | +| [BUD-17](https://github.com/hzrd149/blossom/pull/106) | Browser | Chunked files and nested directory manifests | |
| 96 | +| [BUD-18](https://github.com/hzrd149/blossom/pull/107) | Not yet | Addressing files within immutable or named trees | |
| 97 | + |
| 98 | +[Hashtree](https://github.com/mmalmi/hashtree) provides implementation references and test vectors for the encryption, manifest and tree formats. |
| 99 | diff --git a/docs/fixi.md b/docs/fixi.md |
| 100 | index 7c3c95e..74a4441 100644 |
| 101 | --- a/docs/fixi.md |
| 102 | +++ b/docs/fixi.md |
| 103 | @@ -10,7 +10,7 @@ b70458c212d0409f8a6319c286351865d92587292ad4f9bc0dfd1c46ffcad855 |
| 104 | |
| 105 | `/fixi.js` is served locally with no CDN dependency. The page loads it before the existing signer bundle. The Sync page uses Fixi's `fx-action` request for the job status stream, which renders server-escaped table HTML; `/connect.json` remains a JSON API for the configured connections. Mutation forms remain handled by the existing signer bridge because every request body must be serialized once, hashed exactly, signed as NIP-98, and submitted once. Fixi is not allowed to issue those signed mutations or retry them. |
| 106 | |
| 107 | -The signer bridge handles NIP-07, NIP-46, Nostr Connect, Amber links, session login and management actions. Fixi updates page links and GET forms within the current tenant without a full reload. Filters, repository refs, browser history and page titles remain available. Downloads, external links and modified clicks retain normal browser behavior. A failed request displays an error in the footer and leaves the current page usable. Navigation and content also work without JavaScript, including on phones. |
| 108 | +The signer bridge handles NIP-07, NIP-46, Nostr Connect, Amber links, session login and management actions. Encrypted uploads, NIP-34 publishing and NIP-17 file messages sign through the same bridge and never retry a signed request on their own. Fixi updates page links and GET forms within the current tenant without a full reload. Filters, repository refs, browser history and page titles remain available. Downloads, external links and modified clicks retain normal browser behavior. A failed request displays an error in the footer and leaves the current page usable. Navigation and content also work without JavaScript, including on phones. |
| 109 | |
| 110 | Signed actions require JavaScript and submit once while their controls are disabled. Connect previews update in place. Job status uses an authenticated stream that sends updates when the results change. |
| 111 | |
| 112 | diff --git a/docs/git-collaboration.md b/docs/git-collaboration.md |
| 113 | new file mode 100644 |
| 114 | index 0000000..407ded9 |
| 115 | --- /dev/null |
| 116 | +++ b/docs/git-collaboration.md |
| 117 | @@ -0,0 +1,31 @@ |
| 118 | +# Git collaboration |
| 119 | + |
| 120 | +Open a repository and choose **Issues** or **Pull requests**. Search titles and descriptions, filter by status or label, and follow a conversation from its detail page. Reading works without JavaScript. Publishing requires JavaScript and a connected NIP-07 or NIP-46 signer. |
| 121 | + |
| 122 | +## Issues and pull requests |
| 123 | + |
| 124 | +Create an issue with a title, description and optional comma-separated labels. A pull request also needs the full Git commit ID and an HTTP or HTTPS clone URL. The optional merge base identifies the common ancestor with the target branch. |
| 125 | + |
| 126 | +Replies can address the original post or an individual comment. The author, repository owner and current maintainers can change status. Issues support open, resolved, closed and draft states; pull requests use merged in place of resolved. Marking a pull request merged records its status; merge and push the Git changes with your Git client. |
| 127 | + |
| 128 | +Pull request diffs use commits available in the hosted repository. Missing commits produce an availability message. Diff previews are limited to 512 KiB, with further display limits for long output. Clone the repository to review the complete change. |
| 129 | + |
| 130 | +Events follow [NIP-34](https://github.com/nostr-protocol/nips/blob/master/34.md), with replies using [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md). Markdown is supported in issues and pull request descriptions; comments are plain text. Status reflects the newest visible event signed by an authorized author or maintainer. |
| 131 | + |
| 132 | +The browser tools page exposes the same lists and detail pages to browser agents. See [Browser tools](webmcp.md). |
| 133 | + |
| 134 | +## Synchronization |
| 135 | + |
| 136 | +Enable `features.grasp` and `features.grasp02` to synchronize repository announcements, state and conversations from the repository's announced relays. History synchronization prefers NIP-77 reconciliation and falls back to paginated relay queries. Live subscriptions follow new events as they arrive. Missing Git objects for signed branch state and pull request commits are fetched from the announced sources. Pull request commits stay available in the hosted repository under `refs/nostr/<event-id>`. |
| 137 | + |
| 138 | +Enable `features.grasp03` as well to discover participant outboxes from their NIP-65 relay lists and kind 10317 GRASP lists. Profile and relay-list events are cached locally. New discussion participants are included in subsequent discovery passes. |
| 139 | + |
| 140 | +Synchronization runs on a schedule. A repository that synchronizes successfully is checked again after 55 minutes. One that fails is retried after five minutes. A pass that runs out of time keeps its progress and continues from the next relay or participant on the following pass. Live subscriptions and outbox discovery are limited so a busy repository does not open unbounded connections. |
| 141 | + |
| 142 | +History responses are limited to 10,000 events per filter. A history that reaches that limit is reported as incomplete instead of silently claiming success, and larger histories are not yet guaranteed to converge. |
| 143 | + |
| 144 | +## Private repositories |
| 145 | + |
| 146 | +Private synchronization requires a GRASP-08 tenant and operator-configured private peers. Relay discovery cannot authorize a new private destination. Both event queries and Git object repair stay within the configured peer list, and private relay connections authenticate with NIP-42. Members still need permission to read the local tenant. |
| 147 | + |
| 148 | +See [Files and private repositories](files-and-private-repositories.md) for private hosting setup. |
| 149 | diff --git a/docs/testing.md b/docs/testing.md |
| 150 | index 70768e3..636c815 100644 |
| 151 | --- a/docs/testing.md |
| 152 | +++ b/docs/testing.md |
| 153 | @@ -5,6 +5,8 @@ The normal development loop is local: |
| 154 | ```sh |
| 155 | make test-internal-race |
| 156 | make test |
| 157 | +npm run test:files |
| 158 | +npm run test:collaboration |
| 159 | ``` |
| 160 | |
| 161 | The repository also contains a multistage Docker build. `make docker-test` runs `TEST_PACKAGES` (default `./...`) under Go 1.27.1 on Debian Bookworm with the race detector. `make docker-build` builds the self-hosted runtime image. |
| 162 | @@ -78,8 +80,14 @@ node scripts/git-performance.mjs --relay ws://127.0.0.1:17447 \ |
| 163 | |
| 164 | Use `--only strudel,atlas` to select fixtures. Each run creates distinct signed repository identities, checks all transferred refs and HEAD, runs `git fsck`, and measures relay EVENT/REQ traffic throughout the Git phases. The incremental push adds a commit with an unchanged tree; it is a small metadata update, not a new binary upload. |
| 165 | |
| 166 | -Add `--private` to test private repository announcements and NIP-98 Git authorization. The harness authenticates its control WebSocket with the repository signer and starts a loopback signing proxy for native Git. That proxy hashes each request through a temporary file and signs the exact URL, method and body; its overhead is included in private timings. Private server authorization also spools before invoking Git, so malformed payloads cannot mutate the repository and upload size does not determine Go heap size. Do not interpret private timings as a like-for-like unauthenticated native Git comparison. |
| 167 | +Add `--private` to both commands to test a GRASP-08 private tenant. The private fixture uses the documented benchmark owner key, so it is already a tenant member. The harness authenticates its control WebSocket with the repository signer and starts a loopback signing proxy for native Git. The proxy adds a repository-root proof with the literal `GET` method to each Git request, and its temporary file handling is included in private timings. Signed repository state authorizes each pushed ref. Use a fresh data directory for each comparison. Do not interpret private timings as a like-for-like unauthenticated native Git comparison. |
| 168 | |
| 169 | For native Git comparisons, `go run ./scripts/git-http-baseline --root /path/to/fresh-bare-repos --listen 127.0.0.1:17449` exposes a benchmark-only HTTP server. Initialize one empty bare `<name>.git` per fixture, set its symbolic HEAD to the corpus manifest's `head_branch`, and enable `http.receivepack`. Add `--baseline http://127.0.0.1:17449` to the workload. Use fresh baseline repositories for each run so initial pushes transfer the complete corpus. |
| 170 | |
| 171 | The raw JSON records failures as failures; interrupted or absent phases are not successful samples. The observer separates Go process memory from container memory, which also includes native Git and file cache. Run performance measurements separately from compilation and test suites on the same host. |
| 172 | + |
| 173 | +## Git collaboration checks |
| 174 | + |
| 175 | +Run `npm run test:collaboration` to check signed issue, reply, status and pull request events. Run `go test ./internal/daemon ./internal/gitrelay ./internal/replication ./internal/webui` for collaboration queries, access controls, synchronization, Git repair and page rendering. |
| 176 | + |
| 177 | +The UX fixture includes an issue, a threaded comment, a status change and a pull request with a real Git diff. Use a disposable daemon when testing signed browser publishing. |
| 178 | diff --git a/docs/webmcp.md b/docs/webmcp.md |
| 179 | index e5735a5..3566b53 100644 |
| 180 | --- a/docs/webmcp.md |
| 181 | +++ b/docs/webmcp.md |
| 182 | @@ -1,6 +1,6 @@ |
| 183 | # Browser tools |
| 184 | |
| 185 | -Open **Manage > Tools** to see the tools available to your browser agent. The agent can browse repositories and files, inspect service status, manage background jobs, create backups and update relay settings. |
| 186 | +Open **Manage > Tools** to see the tools available to your browser agent. The agent can browse repositories, files, issues and pull requests, inspect service status, manage background jobs, create backups and update relay settings. |
| 187 | |
| 188 | Choose **Sign in** in the main navigation and connect your Nostr signer. Reading protected information uses your browser session. Management actions request a signature and follow your account permissions. A queued backup or job has not finished until its status says so. |
| 189 | |
| 190 | diff --git a/scripts/conformance/README.md b/scripts/conformance/README.md |
| 191 | index eb9a778..7e4d9a4 100644 |
| 192 | --- a/scripts/conformance/README.md |
| 193 | +++ b/scripts/conformance/README.md |
| 194 | @@ -11,3 +11,5 @@ Setup provisions a tenant with `tiny tenant create --name NAME --owner PUBKEY -- |
| 195 | Each run preserves `environment.txt`, `stdout.log`, `stderr.log`, and `status` under `artifacts/conformance/<UTC-stamp>/`. Set `CONFORMANCE_ARTIFACTS` to choose another destination. Pass extra vitest arguments after the script name to select files, for example `node scripts/conformance/run.mjs nip01`. |
| 196 | |
| 197 | The NIP-11 test does not require a numeric `limitation.max_limit` because self-hosted relays have no hosted quota. |
| 198 | + |
| 199 | +The GRASP-08 stock Git test is opt-in because it changes the tenant policy to a members-only private service. Run it against a disposable tenant with `TINY_GRASP08=1 TINY_PREPROVISIONED=1 CLAIM_SK=<owner-secret> RELAY_URL=ws://127.0.0.1:<port> node scripts/conformance/run.mjs grasp08`. It publishes temporary repository metadata and uses one reusable repository-root proof for push, clone and fetch. |