Skip to content

feat(hub): add the Hub HTTP and WebSocket host and its image - #481

Open
oxwen11 wants to merge 2 commits into
feat/hub-storefrom
feat/hub-host
Open

oxwen11 wants to merge 2 commits into
feat/hub-storefrom
feat/hub-host

Conversation

@oxwen11

@oxwen11 oxwen11 commented Oct 7, 2026

Copy link
Copy Markdown
Owner

Requirement

Hub Phase 1, slice 3 (RFC #466, sections 3, 5, 6, 8): a runnable Hub that can be deployed (Dokploy) and verified end to end without the daemon connector.

Stacked: base is feat/hub-store (#480); it also carries the contract commit from #479, so those two merge first and this retargets to main.

Expected behavior

packages/hub gains src/server.ts (startHub), src/main.ts (env config), a tsdown bundle and packages/hub/Dockerfile.

  • GET /healthz; everything else needs the Hub token (HUB_TOKEN, at least 32 chars, constant-time compare).
  • POST /enrollment-tokens: mints a 256-bit token, returns it once; 429 at ten unused.
  • POST /events { environmentId, type, payload, id? }: committed before the 202; 404 for an unknown or revoked target (records nothing); 413 above HUB_MAX_EVENT_BYTES (default 1 MiB, Content-Length and a streaming cap); duplicate id returns 200 duplicate.
  • DELETE /environments/:id: revokes (persisted first), closes the socket with 4403.
  • WSS /daemon: bearer token + hub.hello; first valid hello enrolls (or authenticates the known credential), replies hub.welcome, delivers held events oldest first, resends unacked events every 30 s, records hub.event.ack, 30 s ping with two missed closing. Bad token or UUID conflict closes 4403; a second live socket closes 4409; 10 s hello deadline, 64 KiB frame bound, at most 32 pending upgrades.
  • Single process per database: the store now opens with locking_mode=EXCLUSIVE, so a second Hub on the same file fails (no stale lock file).

Changes and risks

New code only; nothing else imports it. Host write: $HUB_HOME/hub.sqlite, documented in docs/host-persistence.md (new "Pie Hub data" section), per the persistence gate.

Security notes: the Hub token and every credential compare via SHA-256 digests with timingSafeEqual; WebSocket auth is a header, never a query parameter; no body, token or peer error text is logged; payloads are cleared at a terminal outcome. Not implemented (RFC section 5 lists it): the per-source limit of five Hub-token/enrollment failures per minute and bounded concurrent webhook handling with 429. Tokens are 256-bit random and HUB_TOKEN must be 32+ characters, so brute force is not realistic, but the limiter should land before Hub is exposed beyond a trusted operator. Retry is a fixed 30 s, not exponential backoff with jitter. TLS is the reverse proxy's job.

Verification

Tested revision e42d9294.

  • vitest in packages/hub: 24 passed, no type errors, repeated 5 times without a flake (found and fixed one flake caused by a silent test client being closed for missed pings). Covers auth failures, enroll/welcome/reconnect, 4403 and 4409, held delivery with resend and ack, immediate push, 404/413/400 recording nothing, revoke, ping timeout, malformed hello, unauthenticated upgrade.
  • Typecheck, build, oxlint packages/hub (plugin built), oxfmt clean.
  • Built bundle smoke-run under Node 24: /healthz, token mint, 0600 files. Docker image built (pie-hub:test), run with a named volume: healthy, files 0600, data directory survives a container restart.
  • Not run: Bun, CI, a real reverse proxy and TLS (next: Dokploy deployment and a daemon-simulating WSS client against the public URL, to be reported on this PR).

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.

2 participants