Skip to content

Enforce demo-hub storage guardrails (0291) + workspace share links (0290) - #448

Merged
crs48 merged 10 commits into
mainfrom
claude/0291-demo-hub-runaway-storage-quota-and-eviction-not
Jul 10, 2026
Merged

Enforce demo-hub storage guardrails (0291) + workspace share links (0290)#448
crs48 merged 10 commits into
mainfrom
claude/0291-demo-hub-runaway-storage-quota-and-eviction-not

Conversation

@crs48

@crs48 crs48 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Summary

The demo hub (hub.xnet.fyi) advertised a 10 MB per-user cap and daily eviction but enforced neither — one active user grew past 1 GB, filled the 500 MB Railway volume, and the resulting SQLITE_FULL crash is the most likely cause of the recent 502 outage (explorations 0290/0291, included in this PR).

Demo guardrails (0291)

  • Per-user quota, enforced where the data actually grows: getUsageBytesByDid sums a DID's node_changes bytes (new author_did index); the node relay rejects changes past demoOverrides.quota with QUOTA_EXCEEDED.
  • Backup/File quotas route to the demo overrides (10 MB / 2 MB) in demo mode instead of the 1 GB plan quota.
  • Daily reset: a config.demo-gated interval calls resetAllUserData() (truncate user-content tables + VACUUM) — matches the intended "demo clears daily", which the never-wired inactivity EvictionService could not do for an active user.
  • Disk watchdog: samples the data dir and sheds writes (STORAGE_FULL) at 90% of the volume limit; NodePool defers Yjs persistence while full instead of crashing.
  • createHub({ demo: true }) now resolves demoOverrides — previously only the CLI path did, so programmatic demo hubs silently enforced nothing (caught by live validation).
  • Client: NodeStoreSyncProvider halts outbound sync on the first capacity rejection (no re-flooding), keeps local data, resumes on reconnect, and exposes onSyncBlocked() for UI notices.
  • litestream-entrypoint.sh passes --demo when HUB_MODE=demo/HUB_DEMO=1 so all launch paths agree.

Share-link fixes (0290)

  • Hub now accepts docType: 'workspace' (the client sent it since 0280; every bench share died with 400 INVALID_BODY). Claim union + docRouteFor updated; claimed benches land home.
  • hubApiFetch/claimShareLink name the unreachable hub ("isn't responding — may be down or restarting") instead of surfacing the browser's bare Failed to fetch.

⚠️ Operator action required (not in this PR)

The Railway volume is persistent and already over-full — code alone won't fix the live hub:

  1. Stop/redeploy the hub.xnet.fyi Railway service after truncating its data (delete hub.db* on the volume, or DELETE FROM node_changes; VACUUM;). Demo data is disposable.
  2. Confirm curl https://hub.xnet.fyi/health → 200 and storage.usedBytes is small.
  3. Optionally set DEMO_DISK_LIMIT to the real volume size (default 500 MB).

Validation

  • 485 hub tests, 173 runtime tests, 22 web share-links tests pass; typecheck clean (hub, runtime, web).
  • New integration tests run against real storage (SQLite + memory) — the old eviction test was mock-only, which is how the gap shipped.
  • Live-validated against a booted demo hub: WS change over quota → node-error QUOTA_EXCEEDED; 4 KB backup vs 2 KB demo cap → 413 BLOB_TOO_LARGE; reset interval wiped the room.

Remaining unchecked items in the 0291 doc are production-only validation (a day of hub.xnet.fyi disk headroom) and the optional inactivity-eviction wiring (superseded by the daily reset).

🤖 Generated with Claude Code

xNet Test added 7 commits July 10, 2026 07:26
…ations

Signed-off-by: xNet Test <test@xnet.dev>
Demo mode (--demo) previously advertised its 10 MB per-user cap in the WS
handshake but never enforced it, and the EvictionService was never wired —
one active user filled the 500 MB demo volume (exploration 0291).

- storage: getUsageBytesByDid (per-DID node_changes byte sum, new
  author_did index) + resetAllUserData (truncate user-content tables +
  VACUUM), implemented for both SQLite and memory backends
- node-relay: reject changes past the per-user quota (QUOTA_EXCEEDED) and
  shed writes when the disk is near-full (STORAGE_FULL)
- node-pool: defer doc-state persistence while the disk is full instead of
  dying on SQLITE_FULL (Yjs state has no per-DID attribution to quota)
- server: route Backup/File quota to demoOverrides in demo mode; start a
  daily reset interval + DiskWatchdog when demo; clean both up on stop
- config/types: demoOverrides gains resetInterval + diskLimitBytes
  (DEMO_RESET_INTERVAL / DEMO_DISK_LIMIT env overrides)
- entrypoint: pass --demo through the Litestream launch path
  (HUB_MODE=demo / HUB_DEMO=1) so all launch paths agree

Signed-off-by: xNet Test <test@xnet.dev>
…STORAGE_FULL

When the hub sheds a change for capacity reasons (demo per-user quota or a
full disk — exploration 0291), the sync provider now halts outbound pushes
after the FIRST rejection instead of re-flooding: while the account is over
quota every further change would be rejected identically. Local data stays
intact and un-pushed changes replay on the next reconnect (the demo hub's
daily reset clears the condition server-side). Apps can subscribe via
provider.onSyncBlocked() to surface a storage-full notice.

Signed-off-by: xNet Test <test@xnet.dev>
…k failures

Two 0290 follow-ups:
- The hub's SHARE_DOC_TYPES omitted 'workspace' while the client's Share
  dialog (WorkspaceSwitcher, 0280) sent it — every bench share died with a
  misleading 400 INVALID_BODY. Accept it end-to-end: hub allow-list, the
  claim union, and a docRouteFor case (claimed benches land home; the
  granted node syncs into the workspace switcher).
- hubApiFetch/claimShareLink now catch the bare network TypeError and name
  the unreachable hub ('may be down or restarting') instead of surfacing
  'Failed to fetch' — the exact confusion the hub.xnet.fyi 502 caused.

Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
…ue })

Live validation caught that only the CLI path populated demoOverrides —
createHub({ demo: true }) left them undefined, silently disabling every
demo guardrail. createHub now falls back to env-resolved DEMO_DEFAULTS.
Validated against a booted demo hub: WS node-change over quota returns
node-error QUOTA_EXCEEDED, an oversized backup returns 413 BLOB_TOO_LARGE
at the demo maxBlob, and the reset interval wipes the room.

Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-448 July 10, 2026 14:59 — with GitHub Actions Inactive
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-448 July 10, 2026 15:00 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No UI changes detected in this PR.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #448.

github-actions Bot added a commit that referenced this pull request Jul 10, 2026
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-448 July 10, 2026 15:09 — with GitHub Actions Inactive
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-448 July 10, 2026 15:32 — with GitHub Actions Inactive
@crs48
crs48 merged commit ac59ff2 into main Jul 10, 2026
16 checks passed
@crs48
crs48 deleted the claude/0291-demo-hub-runaway-storage-quota-and-eviction-not branch July 10, 2026 15:40
github-actions Bot added a commit that referenced this pull request Jul 10, 2026
crs48 added a commit that referenced this pull request Jul 10, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @xnetjs/devkit@1.0.0

### Major Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- [#446](#446)
[`10c9f87`](10c9f87)
Thanks [@crs48](https://github.com/crs48)! - Isolate git subprocesses
from inherited repo-location env. When the dev loop (or
its tests) ran while a git hook was active — e.g. husky `pre-push`
running
`pnpm test` — the hook's exported
`GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`
leaked into `git` children and overrode the explicit `cwd`, so
operations
(`config`, `commit`, even `push`) targeted the hook's repo instead of
the
requested worktree. `NodeCommandRunner` now scrubs git's repo-location
env vars
  for `git` invocations so `cwd` is always authoritative; an explicit
  `options.env` entry still wins.
## @xnetjs/cli@0.1.0

### Minor Changes

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`10c9f87`](10c9f87),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/devkit@1.0.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/plugins@0.8.0

### Minor Changes

- [#420](#420)
[`dd3b1cb`](dd3b1cb)
Thanks [@crs48](https://github.com/crs48)! - Single-shell layout
primitives (exploration 0284): `createDefaultTree()` and
`DEFAULT_WORKSPACE_ID` join the workspace layout API — the one canonical
tree (a sectioned sidebar in the rail, the full left dock, tabs on) that
replaces the quiet/calm/bench preset trichotomy. Purely additive:
`createPresetTree` and the preset ids remain for the devtools seed and
portable-workspace round-trips.

- [#439](#439)
[`677856e`](677856e)
Thanks [@crs48](https://github.com/crs48)! - Secure the
browser↔local-model bridge (exploration 0289).
- **`@xnetjs/devkit` (breaking):** the agent bridge daemon now
**requires a
per-launch pairing token** (`Authorization: Bearer <token>`,
constant-time
compared) on its data endpoints (`/v1/chat/completions`, `/run`) and
validates
the `Host` header to reject DNS-rebinding requests. `BridgeServerConfig`
gains
`pairingToken?`, `BridgeServerHandle` exposes `pairingToken`, and a
token is
auto-generated when none is supplied — so a client that previously
called the
data endpoints with no auth now gets `401`. `/health` stays
unauthenticated so
detection still works before pairing. New `openAiChatAgent` lets the
bridge
front a raw OpenAI-compatible model server (Ollama/LM Studio) through
the same
    authenticated door.
- **`@xnetjs/plugins`:** `ConnectorEnv` gains `appOrigin` and the
local-server
setup hint now names the exact `OLLAMA_ORIGINS=<origin>` line (never a
wildcard); new `localServerSetupHint` export; the MCP HTTP transport now
validates the `Host` header (defense-in-depth, no change for legitimate
    callers). Additive.
- **`@xnetjs/cli`:** `xnet bridge serve` prints the pairing code and
gains
`--token` (pin the code) and `--upstream` / `--upstream-model` (front a
raw
    local model). Additive.

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/abuse@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/runtime@0.2.0

### Minor Changes

- [#448](#448)
[`853d849`](853d849)
Thanks [@crs48](https://github.com/crs48)! - `NodeStoreSyncProvider` now
handles hub capacity rejections gracefully: on the first
`QUOTA_EXCEEDED` (over the hub's per-user cap) or `STORAGE_FULL` (hub
disk full) rejection it pauses outbound sync instead of re-flooding the
hub, keeps local data intact, and resumes on the next reconnect.
Subscribe to the new `onSyncBlocked(listener)` API (with
`SyncBlockedReason`/`SyncBlockedListener` types) to surface a "storage
full" notice in your app.

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/storage@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/abuse@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/crypto@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/core@0.8.0
## @xnetjs/data@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/storage@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/data-bridge@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/history@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/identity@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/react@0.8.0

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`853d849`](853d849),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/runtime@0.2.0
  - @xnetjs/history@0.8.0
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/sync@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/storage@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/sqlite@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/sync@0.8.0

### Patch Changes

- Updated dependencies []:
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
  - @xnetjs/core@0.8.0
## @xnetjs/core@0.8.0


## @xnetjs/sqlite@0.8.0


## xnet-cloud@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
  - @xnetjs/cloud@0.0.1
## @xnetjs/brain@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/vectors@0.0.1
## @xnetjs/comms@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/dashboard@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/react@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/social@0.0.12
## @xnetjs/labs@0.0.12

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/licenses@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/crypto@0.8.0
## @xnetjs/maps@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## @xnetjs/meetings@0.0.5

### Patch Changes

- Updated dependencies
[[`dd3b1cb`](dd3b1cb),
[`677856e`](677856e)]:
  - @xnetjs/plugins@0.8.0
  - @xnetjs/data@0.8.0
## @xnetjs/server@0.0.11

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data-bridge@0.8.0
  - @xnetjs/data@0.8.0
  - @xnetjs/identity@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/social@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
  - @xnetjs/crypto@0.8.0
## @xnetjs/unreal@0.0.12

### Patch Changes

- Updated dependencies []:
  - @xnetjs/data@0.8.0
## xnet-desktop@0.8.0

Desktop shell release riding the @xnetjs/core 0.8.0 train.
Desktop-specific changes are not tracked here; see the core packages'
changelogs for what shipped.
crs48 added a commit that referenced this pull request Jul 10, 2026
Extends URL share-links to two more node kinds, reusing the exact
pipeline pages/databases already use (and that #448/#452 hardened this
week).

## Channels
- Hub accepts `docType: 'channel'` (`SHARE_DOC_TYPES`).
- Share entry points: a **Share button in the channel header**
(`ChannelView`) and a **Share action in the ChatsPanel** channel
context-menu/kebab.
- A claimed channel routes to `/channel/$channelId` (both `docRouteFor`
and the `/share` web-fallback map).
- **Authorization:** `ChatMessage@` is added to the comment-schema
allowlist, so a **comment-role** channel share means "can post messages,
can't edit the channel" — `write` still required to edit the channel
node itself. `Channel@` is deliberately *not* in the allowlist.

## Workspaces
- Per-row **Share button in the workspace (bench) switcher**, alongside
the pre-existing `Workspace: Share…` command. Preset workspaces aren't
shareable (guarded by `isPresetWorkspaceId`).
- `EditorHeader` maps `channel` tabs to the `channel` docType too, so a
focused channel shows Share in the editor chrome.

## Verification
- **Channel flow verified end-to-end in a real browser** against a live
hub: created a channel → **Share → New link** minted a hub link → a
second identity claimed it via the API (200, `docType: channel`, role
granted) → the in-browser `/share` claim routed to
`/channel/$channelId`.
- Hub tests assert **all eight** `ShareDocType`s round-trip
create→claim, and that a comment grantee may post `ChatMessage` but not
`Channel`/arbitrary schemas.
- Web unit tests cover `docRouteFor('channel')`; typecheck + lint clean
across hub and web.
- The workspace-switcher Share button reuses the identical
`ShareDialog`/`setShareFor` path the existing `workspace.share` command
already ships (typecheck-verified).

Also fixes a stale source-guard test that still pointed at
`use-boot-sequence.ts` after #452 moved the URL-stripping logic to
`boot/hub-session.ts`.

No publishable package changed (hub + apps/web are private) → no
changeset; changelog fragment included.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant