clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

docs: repository inventory explains retained bytes, scan costs and live admission without promising collection

Dami committed 5272f96
commit
5272f96ddf6f505fd91f1c8f27418c179afc5b60
parent
05ac41274d12b3aa2938badbd5f7aad730d9a848
author
Dami <dami.etoile@gmail.com>
7 files changed, +131 -47
MREADME.md4+-
Mdocs/10-architecture.md25+++++++
Mdocs/13-scripts-and-agents.md6++
Mdocs/14-http-reference.md18+++++-
Mdocs/15-costs-and-margins.md107++++++++++++++++++------------
Mdocs/22-grasp-01-git-hosting.md16++++-
Msrc/landing.ts2+-
M README.md +2 -2
@@ -19,7 +19,7 @@ That answers with a relay at a memorable name, open to anyone for 14 days. Claim
1919 - **A community.** Members, invite links, an invite tree, moderators with a log of what they did, a signed roster and one NIP-29 group per relay, so group-aware clients show it as a place.2020 - **A site.** Notes and articles as pages with link previews, an Atom feed, and a card with a QR of the group address. NIP-5A also serves root, named and snapshot websites from signed manifests on isolated hostnames, with verified files, optional mirroring and sign-in that follows the relay's read rule.2121 - **Encrypted group transport.** Opt-in Marmot KeyPackages and opaque MLS group messages, with account admission and storage caps for ephemeral authors. Clients manage encryption and group membership.22-- **A Git host.** Opt-in GRASP-01 Smart HTTP, backed by [ntig](https://github.com/FelineStateMachine/ntig). Signed NIP-34 state authorizes branch and tag changes; events wait for their Git objects before publication. Hosting has [explicit storage and transaction limits](docs/22-grasp-01-git-hosting.md#git-http).22+- **A Git host.** Opt-in GRASP-01 Smart HTTP, backed by [ntig](https://github.com/FelineStateMachine/ntig). Signed NIP-34 state authorizes branch and tag changes; events wait for their Git objects before publication. Hosting has [explicit storage and transaction limits](docs/22-grasp-01-git-hosting.md#git-http), and the owner can request a bounded read-only inventory without authorizing deletion.2323 - **A file host.** Blossom and NIP-96 on the same bucket, with mirroring, pre-flight checks and file reports.2424 - **A console.** Nine tabs on the relay's own page, signed by a browser extension or a remote signer app on a phone. Presets set up an outbox, an inbox, a private relay, a chat, a search replica, a media host or a quiet relay with every costly feature off, in one click. The template collection also includes static sites, public or member-published Marmot transport and Git repositories.2525 - **A file.** Everything the console sets, as one document with a schema: check it offline, see what applying would change, apply it, pull it back. Keep your relay in your repository. The presets are the same files, in `relay-templates/`.
@@ -46,7 +46,7 @@ Site hosting and mirroring are on by default; Marmot and GRASP are off until ena
4646 4747 **For scripts and agents**4848 49-- [Scripts and agents](docs/13-scripts-and-agents.md): a relay end to end with a key and curl, every management method, and the relay as a configuration file.49+- [Scripts and agents](docs/13-scripts-and-agents.md): a relay end to end with a key and curl, every management method including Git storage inventory, and the relay as a configuration file.5050 - [HTTP reference](docs/14-http-reference.md): every path, method, auth and answer.5151 5252 **For developers**
M docs/10-architecture.md +25 -0
@@ -163,6 +163,31 @@ accepted state as its authorization boundary and passes Git objects through
163163 the object-store seam. It stays separate from the NIP-5A site door, so a site164164 hostname cannot reach relay events or repository data.165165 166+`repository-access.ts` gives one live Durable Object operation the repository167+authority token across awaits. Git HTTP, owner controls, alarms, event168+ingestion and direct lease, adoption and teardown paths use the same admission169+seam; nested work owned by the active token proceeds, while another live170+operation receives a retry response. Async context carries the token, so the171+fence replaces separate Git and control flags without becoming a durable lock.172+It does not coordinate a crashed instance, another R2 client or another object173+using the prefix, and it never authorizes garbage collection.174+175+`git-storage.ts` implements the owner-only `gitstorage` management method. It176+walks one accepted repository's physical R2 objects within explicit budgets,177+compares them with the live dependency inventory and reports physical, live,178+unreferenced and unknown objects by class alongside SQL reservations and their179+difference from listed bytes. An incomplete, over-budget or changed-root walk180+returns an error and no partial report. The manual scan has a 60-second181+per-instance cooldown and does not delete or reclaim storage.182+183+[Tangled knots](https://docs.tangled.org/knot-self-hosting-guide) provide184+useful prior art: AT Protocol identity sits above a filesystem Git host with185+repository maintenance. Bindws instead publishes immutable objects through186+an R2 root. The storage layout and recovery contract determine which old187+objects can be reclaimed; decentralized identity does not require retaining188+every superseded manifest. A collection protocol still needs to protect old189+readers, unpublished writes and retained transaction receipts.190+166191 ## Views167192 168193 `src/views.ts` is a registry of folds. Each view names a trigger, an audience and a fold that returns tags and content; hourly views add a fingerprint so nothing is republished when the inputs did not move. The record is a kind 30078 signed by the identity key on the same strictly increasing clock as the group state, `d` = `bind.ws/view/<name>`, stored and broadcast through `emit` like the roster, taken down when the view is switched off, empty, or no longer public. The rows a run wrote are measured around the fold from the store's write counter and kept as the last 60 runs, which the console and the digest read.
M docs/13-scripts-and-agents.md +6 -0
@@ -67,6 +67,7 @@ await rpc("claim"); // { result: { owner, claimed: true, c
6767 await rpc("applypreset", "outbox"); // one call sets writes, reads, kinds and keep-for6868 await rpc("backfill", ["wss://relay.damus.io", "wss://nos.lol"]);6969 console.log(await rpc("listjobs"));70+console.log(await rpc("gitstorage", "<owner hex pubkey>", "my-project"));7071 ```7172 7273 The bridge takes the same header. `POST /events` answers `{ event_id, accepted, message }`; `POST /query` answers a list of events; `POST /count` answers `{ count }`. The bridge is rate limited per address at four times the per-connection allowance and answers `429` past it.
@@ -137,6 +138,7 @@ The bridge takes the same header. `POST /events` answers `{ event_id, accepted,
137138 **Storage**138139 139140 - `storagestats`: bytes by kind, files, retention.141+- `gitstorage owner identifier`: an owner-only inventory of one accepted GRASP repository. The result compares bounded physical R2 listing with live Git dependencies and reports physical, live, unreferenced and unknown objects by class, SQL reservations and the byte difference. It never deletes data.140142 - `deleteblob sha256`.141143 - `listdumps`, `dumpnow`, `deletedump name`.142144
@@ -164,6 +166,10 @@ The bridge takes the same header. `POST /events` answers `{ event_id, accepted,
164166 165167 - `deleterelay name`: the relay's own name, typed, is the confirmation.166168 169+`gitstorage` is a diagnostic read, not a billing ledger or a deletion grant.170+Its operation counts do not add a tenant charge, and its 60-second cooldown is171+per live Durable Object instance, so eviction or restart resets it.172+167173 ## The configuration file168174 169175 `exportconfig` is a document, and a document can live in a repository. The format is `bind.ws/relay-config/2`: `policy`, `members`, `bans`, `addresses`, `banned_events`, `kinds` and `retention`, each a section, and a section the file leaves out is left alone when it is applied, so a file may carry the rules and nothing about people. The schema is at `https://bind.ws/relay-config.schema.json`; put it in `$schema` and the editor checks the file as you type. JSONC is fine: comments and trailing commas.
M docs/14-http-reference.md +17 -1
@@ -25,7 +25,7 @@ Any request with `Accept: application/nostr+json` answers the NIP-11 document, w
2525 |---|---|---|---|---|2626 | `/` | GET | none | the console, one page for visitors and owner alike | 200 |2727 | `/` | GET with `Accept: application/nostr+json` | none | NIP-11: rules, limits, retention, `self`, `lease` while leased, `succession_pending` while warning | 200 |28-| `/` | POST, `content-type: application/nostr+json+rpc` | NIP-98 | NIP-86 management, `{ result }` or `{ error }` | 200; 400 invalid; 401 bad signature; 403 not allowed; 409 conflict |28+| `/` | POST, `content-type: application/nostr+json+rpc` | NIP-98 | NIP-86 management, `{ result }` or `{ error }` | 200; 400 invalid; 401 bad signature; 403 not allowed; 409 conflict; 429 active relay operation |2929 | `/people` | GET | none | `{ public, self, host, people }`, the members when the directory is public | 200 |3030 | `/terms` | GET | none | the join terms as a page | 200; 404 none set |3131 | `/signer.js` | GET | none | the NIP-46 client bundle, cached a week | 200 |
@@ -97,6 +97,22 @@ unmatched refs are hidden and scheduled for deletion, but immutable Git
9797 objects remain in retained storage. The 128-transaction ceiling can prevent9898 physical cleanup; it does not make those bytes free.9999 100+The owner can inspect one accepted repository through the NIP-86101+`gitstorage` method at the relay root. It performs a bounded, read-only walk102+of the physical R2 prefix and compares it with the live Git dependency set.103+The result reports physical, live, unreferenced and unknown objects by class,104+SQL reservations and `reservationMinusListedBytes`, together with operation105+counts for diagnostics, limits and capture time. These counts are not tenant106+billing meters. Unreferenced does not authorize deletion:107+there is no collector or automatic storage reclamation. A complete report is108+required, so a changed root or any inventory budget exhaustion returns an109+error instead of partial data. The method has a 60-second per-instance110+cooldown; a Durable Object restart or eviction resets that cooldown.111+112+| Method | Parameters | Auth | Answers | Status |113+|---|---|---|---|---|114+| `gitstorage` | repository owner hex pubkey, identifier | NIP-98, owner with the storage action | bounded physical and live inventory, reservations and limits | 200; 400 invalid parameters; 403 inactive, fuel or role; 404 feature or repository unavailable; 413 inventory limit; 429 cooldown or another repository operation; 503 storage unavailable |115+100116 ## Custom domains101117 102118 These NIP-86 methods are owner-only and use the relay's normal NIP-98 RPC
M docs/15-costs-and-margins.md +65 -42
@@ -103,10 +103,12 @@ Prices go up when bitcoin falls. When it rises, do nothing until a line has sat
103103 104104 ## Accounting coverage, checked Sept. 4, 2026105105 106-The following inventory describes the host with ntig 0.2.1 and unmigrated107-format-1 repositories. Installing the library does not migrate roots. Observing an108-expense does not authorize a new customer charge. `/fuel` and the Health tab109-expose aggregate tenant meters, not every operation the host pays for.106+The following inventory describes the host with the current repository107+inventory contract and the owner-only inventory admission path. The pinned ntig 0.3.0108+package supports both formats; installing it does not migrate roots.109+Observing an expense does not authorize a new customer charge. `/fuel` and the110+Health tab expose aggregate tenant meters, not every operation the host pays111+for.110112 111113 | Dimension | Existing source and persistence | Coverage limit |112114 |---|---|---|
@@ -149,21 +151,23 @@ storage. Neither receipt lookup nor checkpointing reclaims old bytes.
149151 Operation counts are host costs, not a new tenant charge; lower read costs150152 alone do not establish a margin after metadata writes and retained storage.151153 152-Git HTTP requests reuse up to 2 MiB and 512 entries of immutable payload in a153-session that ends with the request. Repeated validation and promotion can154-reuse those bytes, while mutable roots and all writes still reach R2 and155-its accounting adapter. New requests start fresh, oversized objects bypass156-the cache, and Git validation still runs. Lower R2 read counts do not imply157-lower CPU cost or a complete request-memory bound. Bulk initial checkpoint158-construction avoids writing intermediate receipt-index nodes; it removes159-no stored data and does not start migration automatically.154+Git HTTP requests reuse bounded immutable payload in a session that ends with155+the request. Repeated validation and promotion can reuse those bytes, while156+mutable roots and all writes still reach R2 and its accounting adapter. New157+requests start fresh, oversized objects bypass the cache, and Git validation158+still runs. Lower R2 read counts do not imply lower CPU cost or a complete159+request-memory bound. Inventory uses its own 16 MiB total-read, 4 MiB object,160+10,000-key, 100-page, 10,000-node and 10,000-receipt budgets,161+with a 1,024-byte key and 8,192-byte cursor limit; it returns no partial report162+when a budget or root-stability check fails.160163 161164 The isolated checkpoint fixture separates physical storage from the objects162165 referenced by the current format. Both workloads use one 214-byte Git pack,163166 260 transactions and an explicit checkpoint after transaction 128. One keeps164167 updating the same ref to the same commit; the other retains 259 new tags.165-These are local measurements with ntig 0.2.1, not production usage or a166-per-repository minimum.168+These local fixtures were first measured with ntig 0.2.1. The ntig 0.3.0169+runtime inventory reproduces their physical, current-format and reservation170+totals exactly. They are not production usage or a per-repository minimum.167171 168172 | Retained Git payload | One fixed ref | Growing tags |169173 |---|---:|---:|
@@ -175,6 +179,13 @@ per-repository minimum.
175179 | Superseded receipt-index nodes | 332,722 bytes | 332,722 bytes |176180 | Physical objects and reserved bytes | 512,431 bytes | 2,147,926 bytes |177181 182+Each runtime scan uses 349 R2 GETs and one LIST. It reads 137,239 bytes for183+fixed refs and 160,972 bytes for growing tags. At the R2 unit rates below,184+those requests represent about $0.00013 per scan before pooled allowances185+and billing-unit rounding, excluding execution and SQL costs. The scan does186+not reclaim bytes or add a tenant fuel rate; its operation counts are187+diagnostics rather than a billing ledger.188+178189 The ledger equals the paginated R2 inventory for every key and size in these179190 quiet fixtures: there is no reservation slack. Each untouched fixture180191 has no Git R2 keys; its first commit uses 631 bytes in three keys. SQLite
@@ -198,40 +209,52 @@ reproduces the inventory, current-format traversal, all historical receipt
198209 lookups and unchanged root checks. The measurements apply to healthy,199210 quiescent fixtures; ambiguous writes can still leave reservation slack.200211 201-`graspBusy`, `graspControls` and the job round's `working` flag fence work202-within a live instance. Async handlers can interleave across awaits. Those203-flags are admission controls, not persisted crash recovery or authority to204-delete data. Repository CAS and persisted authority remain necessary.212+`repository-access.ts` carries one live owner token across awaits. Git,213+non-GET controls, alarms, event ingestion and direct lease, adoption and214+teardown paths use the same admission seam, and nested work owned by that215+token proceeds. Async handlers can still interleave across independent216+instances or R2 clients. The token is admission control, not persisted crash217+recovery or authority to delete data. Repository CAS and persisted authority218+remain necessary.205219 206220 The coordination tests pause actual Git HTTP and alarm requests at an R2207-root read. Competing Git requests return 429 without reading R2. While HTTP208-holds `graspBusy`, signed event writes and HTTP management are refused and209-an alarm defers its storage work. While an alarm holds `graspControls`, a210-signed repository-state event still enters purgatory. This is intentional211-evidence of a narrower admission fence, not proof of exclusive maintenance.212-That event changes SQL authority without an R2 operation in the paused213-fixture; it does not demonstrate an existing Git publication or data-loss bug.214-Both successful and failed R2 reads release their flags. Consuming the215-completed HTTP response performs no more R2 reads in this fixture.221+root read. Competing Git requests return 429 without reading R2. The active222+`repositoryAccess` token admits nested work owned by that operation and223+refuses unrelated controls, event writes and alarms until it releases. This224+is live-instance admission evidence, not proof of crash recovery or exclusive225+maintenance across clients. Both successful and failed R2 reads release the226+token. Consuming the completed HTTP response performs no more R2 reads in this227+fixture.216228 217229 | Entry point | Existing coordination | Limit before collection |218230 |---|---|---|219-| Git advertisement, fetch and push | `grasp` sets `graspBusy` before its first await and rejects either occupied flag | Live-instance admission only; no persisted reader lease |220-| Accepted state and promotion | Run inside the HTTP fence or alarm controls; promotion rechecks candidates after reads | An immutable read session caches bytes but acquires no lock |221-| Non-GET HTTP management | `Relay.fetch` holds `graspControls` while the route runs and refuses `graspBusy` | Other controls and event writes can still interleave |222-| Signed events, imports and pulls | `acceptAny` or `writeGate` refuses `graspBusy` | `graspControls` alone does not refuse event writes |223-| Alarm, PR expiry and pending promotion | Alarm defers during `graspBusy`, otherwise holds `graspControls` | Alarm entry does not refuse an already active control operation |224-| Whole-relay deletion | Management or lease-expiry alarm calls `teardown` | Teardown has no internal lock and deletes the whole slug prefix; it is not a metadata collector |225-| Direct repository helper calls | Production callers are the HTTP handler and `graspTick` | `gitRepository` and `checkpoint()` acquire no host fence themselves; test calls bypass admission |231+| Git advertisement, fetch and push | `repositoryAccess` owns the live token across the Git operation | Live-instance admission only; no persisted reader lease |232+| Accepted state and promotion | Run inside the Git token or the alarm token; promotion rechecks candidates after reads | An immutable read session caches bytes but acquires no cross-instance lock |233+| Non-GET HTTP management | The control token refuses an active Git operation | Nested work owned by the active token proceeds |234+| Signed events, imports and pulls | The event token refuses an unrelated Git or control operation | Independent instances can still interleave |235+| Alarm, PR expiry and pending promotion | The alarm defers and reschedules when another token is active, otherwise it owns the alarm token | Alarm work is not crash recovery |236+| Whole-relay deletion | Management or lease-expiry alarm enters the teardown token | Teardown deletes the whole slug prefix; it is not a metadata collector |237+| Direct repository helper calls | Production callers enter through the repository access seam | Helpers do not become durable locks or deletion authority |226238 227239 The current HTTP handler constructs its response bytes before the read228240 session closes. A future streaming path would need to preserve coordination229-until its last storage read. None of these in-memory flags coordinates an241+until its last storage read. The owner token does not coordinate an230242 independent R2 client, another object ID sharing a prefix, or recovery after231243 the owning instance is replaced. CAS protects root publication, but does not232244 prevent deletion of an object that a reader or unpublished writer still233245 needs. Removing a repository announcement also leaves its WAL and quota234-rows behind; visibility is not a storage-liveness test.246+rows behind; visibility is not a storage-liveness test. The repository helper247+itself has no host admission; production callers enter through248+`repositoryAccess`.249+250+The owner-only `gitstorage` management method makes this distinction visible251+without turning it into a cleanup operation. It walks one repository under252+bounded budgets and reports physical, live, unreferenced and unknown objects253+by class, SQL reservations and the reservation-to-listed-byte difference. A254+complete report is required, and a changed root, budget exhaustion or backend255+failure returns no partial result. The 60-second per-instance cooldown limits256+repeated scans. Unreferenced bytes remain tenant storage until a future,257+separately authorized collection protocol exists.235258 236259 Before metadata collection, a complete inventory needs explicit object,237260 byte and operation budgets and must refuse an incomplete mark. The host
@@ -240,8 +263,8 @@ readers, unpublished writes, administrative deletion and maintenance.
240263 Budget exhaustion, missing or corrupt dependencies and changed roots must241264 prevent a deletion decision. Stable before/after roots alone are insufficient:242265 a reader may hold an older root, or a publisher may have uploaded objects243-without publishing its new root yet. This remains a collection blocker;244-the tests introduce no collector or runtime coordination change.266+without publishing its new root yet. This remains a collection blocker; the267+tests exercise the runtime admission seam but introduce no collector.245268 246269 `npm test -- test/object/grasp-coordination.test.ts` reproduces the paused247270 HTTP/alarm checks. The checkpoint inventory above remains an isolated,
@@ -325,10 +348,10 @@ These are proposed operating boundaries, not new allowances or charges.
325348 326349 ## Proposed maintenance contract327350 328-ntig 0.2.1 has no resumable optimization runner. No maintenance job, budget329-setting, wake schedule, format migration or garbage collection is enabled by330-this contract. Existing ref cleanup, retention and alarms keep their current331-roles. An eventual integration has these boundaries:351+The current ntig 0.3.0 package has no resumable optimization runner. No352+maintenance job, budget setting, wake schedule, format migration or garbage353+collection is enabled by this contract. Existing ref cleanup, retention and354+alarms keep their current roles. An eventual integration has these boundaries:332355 333356 | Stage | Host and backend contract |334357 |---|---|
M docs/22-grasp-01-git-hosting.md +15 -1
@@ -130,7 +130,7 @@ The hard limits are:
130130 | Format 1 | 128 transactions | legacy WAL replay |131131 | Format 2 | 128 unique packs, 2 MiB manifest | explicitly migrated repository metadata |132132 133-ntig 0.2.1 reads both formats, but installing it leaves existing roots alone133+ntig 0.3.0 reads both formats, but installing it leaves existing roots alone134134 and new repositories still use format 1. Bindws exposes no checkpoint action135135 or scheduled migration. The backend's explicit `checkpoint()` upgrade is136136 one-way: every reader and writer must support format 2 before an isolated
@@ -158,6 +158,20 @@ rechecks authority. The session closes on return or error. This bounds the
158158 cache, not total request memory, and does not skip Git validation or pending159159 promotion.160160 161+The owner can request `gitstorage <repository-owner-hex> <identifier>` through162+the management API to compare physical Git objects with quota reservations.163+The report breaks storage down by object class and distinguishes current-root164+references, unreferenced objects and unknown keys. Unreferenced bytes are not165+safe-to-delete bytes: readers and unpublished writes can still need them.166+The scan does not change roots, reservations or stored objects.167+168+Scans run manually with fixed read limits and a 60-second cooldown per live169+relay instance. A restart resets that cooldown. A limit, corrupt dependency or170+changed root returns an error rather than a partial report. Git requests,171+event writes and configuration changes receive a retry response while a scan172+owns the relay. See [HTTP reference](14-http-reference.md) for the method and173+[Costs and margins](15-costs-and-margins.md) for its host costs and limits.174+161175 ## Information document and limits162176 163177 When GRASP is on and reads are open, NIP-11 includes:
M src/landing.ts +1 -1