Repository navigation
feat(webui): guard every Mini App loopback server with the host credential - #262
Closed
Fectivnfy112357 wants to merge 4 commits into
Closed
Fectivnfy112357 wants to merge 4 commits into
Fectivnfy112357 wants to merge 4 commits into
Conversation
This was referenced Oct 26, 2026
…ntial Work in progress on #253. The guard and the credential threading are in place; the test suite and the wiring that hands the credential to the proxy are not. A Mini App's port is derived from its plugin id, so a local process can compute it rather than scan for it. Every `node:http` server the plugin creates is now intercepted at the `http.Server.prototype.emit` seam before the entry module loads, and only a request carrying the Host-issued credential on the exact loopback Host reaches plugin code: anything else is answered 401 with no body, and an `upgrade` or `connect` socket without it is destroyed. The credential is stripped from both `headers` and `rawHeaders` on success, so plugin code never observes it. One reserved health path is answered 204 by the guard itself. The credential is minted once per process generation in the node runtime, rides the runner's init message, and stays on the runtime handle as server-only state. It is absent from `MiniAppPublishedView` and from every renderer contract, so no published view, surface path or client payload can carry it. Readiness stops meaning "the socket accepts": the runtime now requires an anonymous health request to be rejected with 401 and the credentialed one to be answered 204, which is what proves the guard is in the path rather than merely listening. Still to do before #253 can be reviewed: the guard and validator test suites, and the supervisor hand-off of the credential to the proxy that injects it.
…ntial Closes #253. A Mini App's port is derived from its plugin id, so a local process can compute it rather than scan for it. Every `node:http` server the plugin creates is now intercepted at the `http.Server.prototype.emit` seam before the entry module loads, and only a request carrying the Host-issued credential on the exact loopback Host reaches plugin code: anything else is answered 401 with no body, and an `upgrade` or `connect` socket without it is destroyed. One reserved health path is answered 204 by the guard itself, so readiness can prove the guard is in the path rather than merely that a socket accepts. The credential is stripped from both `headers` and `rawHeaders` before plugin code runs, so plugin code never observes it. The credential is minted once per process generation in the node runtime, before the port-attempt loop, rides the runner's init message, and stays on the runtime handle as server-only state. It is absent from `MiniAppPublishedView` and from every renderer contract, so no published view, surface path or client payload can hold it. Handing it to the proxy that injects it belongs to the proxy change, where its consumer exists. Readiness no longer means "the socket accepts": the runtime requires an anonymous health request to be rejected with 401 and the credentialed one to be answered 204, with `READINESS_PROBE_REJECTED` and `READINESS_PROBE_TIMEOUT` as the failure codes. The suite drives a real `http.Server` through the real runner seam rather than a mock: anonymous 401 with an empty body, wrong credential 401, right credential on the wrong Host 401, the plugin handler reached with no credential header visible, the health path answered without reaching the plugin, and an unauthenticated upgrade destroyed while a credentialed one is forwarded. Six of the seven cases fail when the guard install is removed. Verified: pnpm typecheck; pnpm run test:capabilities (236 files, 5818 passed, 18 skipped); pnpm check:source after node scripts/source-inventory.mjs --write.
Readiness now lives in `loopback-readiness.ts` instead of inside `node-runtime.ts`, so it can be exercised against a real server without faking a child process — and so the proxy and ownership work reuse one definition of "the guard answered". The module keeps the rule the guard exists for: an anonymous health request must be rejected with 401 and the credentialed one answered 204, a port that answers wrongly is not retried, and a stalled probe reports READINESS_PROBE_TIMEOUT. Four cases added to the guard suite, all against real servers: - the guarded fixture passes readiness - a wrong credential fails with "expected 204, received 401" - a plain server in a child process that answers 200 passes nothing: it fails with "expected 401, received 200", which is the case readiness exists to catch - a closed port times out instead of reporting ready Verified: pnpm typecheck; pnpm run test:capabilities (236 files, 5822 passed, 18 skipped); pnpm check:source after the inventory write.
The scripted-child harness `node-runtime` was missing: a `ChildProcess` stand-in that runs the real runner in-process, so the readiness probe meets the real guard while the child itself is scripted. With it, the credential stops being construction-only. Two cases: - one preparation mints exactly one credential, the init message carries it, and the runtime handle exposes the same value after readiness passed for it - a second process generation receives a different credential, observed on the init message it was spawned with Together with the guard suite this closes the last unproven acceptance criterion of #253; the credential also stays out of every published view by type. Verified: pnpm typecheck; pnpm run test:capabilities (237 files, 5824 passed, 18 skipped); pnpm check:source after the inventory write.
Fectivnfy112357
force-pushed
the
feat/miniapp-loopback-guard
branch
from
October 26, 2026 08:33
3332247 to
40cadee
Compare
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.
Part of #253. Stacked on #261: base is
feat/miniapp-contract-forward-port, because the reader andcontract changes this branch builds on are still under review there. GitHub retargets this PR to
webuiwhen #261 merges.Change
A Mini App's port is derived from its plugin id (
49152 + sha256(pluginId)), so a local process cancompute it rather than scan for it. This PR puts the Host guard in the Mini App's own process, in
front of every
node:httpserver the plugin creates.hostRunnerpatcheshttp.Server.prototype.emitbefore the entry module loads, so any server theplugin starts inherits it. Only a request carrying the Host-issued credential on the exact loopback
Host reaches plugin code; anything else is answered 401 with no body, and an
upgrade/connectsocket without it is destroyed. The reserved health path
MINIAPP_LOOPBACK_HEALTH_PATHis answered204 by the guard itself.
headersandrawHeaderson success, so plugin code never observesit.
randomBytes(32).toString('base64url'), minted once per process generation inprepareRuntimebefore the port-attempt loop, carried on the init message, and kept on the runtime handle as
server-only state. It is absent from
MiniAppPublishedViewand every renderer contract.to be answered 204 (
READINESS_PROBE_REJECTED,READINESS_PROBE_TIMEOUT), instead of a TCP connect.Handing the credential to the proxy that injects it is deliberately not here:
#255owns the proxy andis the first consumer of that hand-off, so this PR does not add an unread field to the supervisor.
Validation
pnpm typecheck— passedpnpm run test:capabilities— 236 files, 5818 passed, 18 skippedpnpm check:sourceafternode scripts/source-inventory.mjs --write— 5452 files, passedloopback-guard.test.ts(11 cases) drives a realhttp.Serverthrough the real runner seam and exercises readiness against it plus a plain child-process server that
answers 200;
node-runtime-credential.test.ts(2 cases) runs the real runner in-process behind ascripted
ChildProcessand observes the credential the runtime mints and hands downpnpm verifyin full; the browser and release-package steps are left to CIUntested boundaries
connect(HTTP CONNECT) branch is implemented through the same socket-event allowlist asupgradebut is not covered by a case on its own.pnpm verifyin full was not run; the browser and release-package steps are left to CI.Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable.Maintainer handoff
Publication scope or license changes: none.
Shared-source port: not needed — the guard is this repository's own code, written against the shipped
generation's behaviour rather than copied from it.