Enforce demo-hub storage guardrails (0291) + workspace share links (0290) - #448
Merged
crs48 merged 10 commits intoJul 10, 2026
Merged
Conversation
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>
Signed-off-by: xNet Test <test@xnet.dev>
Contributor
🖼️ UI changes in this PRNo UI changes detected in this PR. |
Contributor
|
Preview removed for PR #448. |
Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
crs48
deleted the
claude/0291-demo-hub-runaway-storage-quota-and-eviction-not
branch
July 10, 2026 15:40
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.
This was referenced Jul 10, 2026
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
getUsageBytesByDidsums a DID'snode_changesbytes (newauthor_didindex); the node relay rejects changes pastdemoOverrides.quotawithQUOTA_EXCEEDED.config.demo-gated interval callsresetAllUserData()(truncate user-content tables +VACUUM) — matches the intended "demo clears daily", which the never-wired inactivityEvictionServicecould not do for an active user.STORAGE_FULL) at 90% of the volume limit;NodePooldefers Yjs persistence while full instead of crashing.createHub({ demo: true })now resolvesdemoOverrides— previously only the CLI path did, so programmatic demo hubs silently enforced nothing (caught by live validation).NodeStoreSyncProviderhalts outbound sync on the first capacity rejection (no re-flooding), keeps local data, resumes on reconnect, and exposesonSyncBlocked()for UI notices.litestream-entrypoint.shpasses--demowhenHUB_MODE=demo/HUB_DEMO=1so all launch paths agree.Share-link fixes (0290)
docType: 'workspace'(the client sent it since 0280; every bench share died with400 INVALID_BODY). Claim union +docRouteForupdated; claimed benches land home.hubApiFetch/claimShareLinkname the unreachable hub ("isn't responding — may be down or restarting") instead of surfacing the browser's bareFailed to fetch.The Railway volume is persistent and already over-full — code alone won't fix the live hub:
hub.xnet.fyiRailway service after truncating its data (deletehub.db*on the volume, orDELETE FROM node_changes; VACUUM;). Demo data is disposable.curl https://hub.xnet.fyi/health→ 200 andstorage.usedBytesis small.DEMO_DISK_LIMITto the real volume size (default 500 MB).Validation
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.fyidisk headroom) and the optional inactivity-eviction wiring (superseded by the daily reset).🤖 Generated with Claude Code