ftown-bridge 0.19.19 → 0.19.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ /**
2
+ * ftown Solo — frozen contract v2 for the single-port LAN deployment mode.
3
+ * REVISION 2 — gauntlet round 1 findings applied (see SOLO_CONTRACT_REVISION).
4
+ *
5
+ * PRODUCT DEFINITION (do not renegotiate in module code):
6
+ * `ftown-bridge --solo` binds ONE HTTP port on the LAN and serves:
7
+ * - /api/solo/* → solo bootstrap/token endpoints (bridge-owned)
8
+ * - /api/* → the existing local API (unchanged)
9
+ * - /healthz → liveness of front + hub + panel
10
+ * - /hub/* → managed Centrifugo child, proxied, WS UPGRADES ONLY
11
+ * - anything else → the panel: a managed Next.js STANDALONE child process
12
+ *
13
+ * The panel is NOT a static export: the ui app is output:'standalone' with an
14
+ * auth()-gated server page, so static export is impossible without re-
15
+ * architecting it. Solo instead fetches the published standalone bundle,
16
+ * spawns it on a private port like the hub, and the front reverse-proxies
17
+ * everything not bridge-owned to it. UI auth is dual-mode: hosted builds use
18
+ * NextAuth; solo builds authenticate with the access key against /api/solo/*.
19
+ *
20
+ * No external account service, no docker, no second public port. A tunnel
21
+ * provider may point at this SAME port; nothing may assume loopback-only
22
+ * access. This file is TYPES + CONSTANTS + TABLES ONLY — no logic. Every
23
+ * cross-module value must appear here; a name absent from this file does not
24
+ * exist.
25
+ */
26
+ /** Contract revision — bump on any material change (re-gauntlet required). */
27
+ export const SOLO_CONTRACT_REVISION = 4;
28
+ // ---------- Ports ----------
29
+ /** Default public LAN port (--port overrides). Children NEVER use fixed
30
+ * ports: hub and panel bind 127.0.0.1:0 (OS-assigned ephemeral), eliminating
31
+ * child port collisions entirely. */
32
+ export const DEFAULT_SOLO_PORT = 8040;
33
+ // ---------- Health probes (pinned) ----------
34
+ //
35
+ // hub: GET http://127.0.0.1:<hubPort>/health (native centrifugo endpoint)
36
+ // panel: HEAD http://127.0.0.1:<panelPort>/ (any status <500 = up)
37
+ // Poll interval 5s, probe timeout 1s; 'up' requires one success, 'down' after
38
+ // one failure following a prior success or after grace period (10s) at boot.
39
+ // ---------- Lifecycle (integrator, frozen) ----------
40
+ //
41
+ // L1. Boot: parse args → rotate-key short-circuit (S11) → create SoloConfig
42
+ // (children get port 0) → front.listen(port) → print URLs/banner → async
43
+ // ensure(hub) + ensure(panel).
44
+ // L2. Shutdown (SIGINT/SIGTERM identical): front.stopListening() (stop accept,
45
+ // destroy open WS upgrades after 5s grace) → panel.kill() → hub.kill()
46
+ // (children get SIGTERM then SIGKILL after 3s) → flush config files.
47
+ // Orphaned children must be detectable+reaped on next boot (stale pidfile
48
+ // under dataDir/solo/ checked before spawn).
49
+ // L3. EADDRINUSE on the front port: fail fast with a clear error naming the
50
+ // port. Never auto-retry, never pick another port silently.
51
+ // L4. --rotate-key alone: regenerate key, persist hash (0600), print new
52
+ // banner, exit 0 WITHOUT starting any listener. With --solo: rotate, then
53
+ // continue normal boot. Bundles/dataDir untouched either way.
54
+ // ---------- Identity & crypto constants ----------
55
+ /** Access key entropy: 32 random bytes, hex-encoded (64 chars). */
56
+ export const ACCESS_KEY_BYTES = 32;
57
+ /** Hub JWT time-to-live, seconds. Refresh via POST /api/solo/token. */
58
+ export const HUB_JWT_TTL_SECONDS = 12 * 60 * 60;
59
+ /** Fixed solo subject. Solo mode is single-user by definition. */
60
+ export const SOLO_USER_ID = 'solo';
61
+ /**
62
+ * Audience claim REQUIRED in every hub JWT and configured as token_audience
63
+ * in the hub config. Minting without aud, or configuring a different value,
64
+ * makes every WS handshake fail with an audience mismatch.
65
+ */
66
+ export const HUB_JWT_AUDIENCE = 'ftown:centrifugo';
67
+ /** Centrifugo release to download (pinned; checksums embedded below). */
68
+ export const CENTRIFUGO_VERSION = 'v5.4.9';
69
+ /**
70
+ * Embedded sha256 digests for the pinned centrifugo release assets, sourced
71
+ * from the release's official checksums.txt at prep time (never fetched at
72
+ * install time). Keyed by platform triple derived from process.platform/arch:
73
+ * darwin-arm64, darwin-amd64, linux-amd64, linux-arm64.
74
+ */
75
+ export const CENTRIFUGO_SHA256 = {
76
+ 'darwin-arm64': 'b0bef645acffe29ae9eb07fd98e93ac14d9c1cdd26b568b6cf9b8f20c6f653f4',
77
+ 'darwin-amd64': 'ab221e476f8e9abd69f9943c2d3e7fefc232b90c5c23ccff60b89cae82f3fd50',
78
+ 'linux-amd64': '75d2fac2dcea005bb3cb1b4636b3825d98c97709c94d10755c892dbe1c9956c2',
79
+ 'linux-arm64': 'ba6df455ee0064399dd13652575fbbefa3d00bbe647d0555cb3669c1060821e5',
80
+ };
81
+ /**
82
+ * Panel (UI standalone) bundle source. Published as a GitHub release asset of
83
+ * THIS repository named `ftown-ui-standalone-<version>.tar.gz`, where version
84
+ * matches the ui package version. Integrity: the release workflow publishes a
85
+ * `<asset>.sha256` sidecar fetched over HTTPS from the same release — TLS to
86
+ * github.com is the trust root for first-party artifacts (third-party
87
+ * centrifugo gets EMBEDDED digests above instead).
88
+ */
89
+ export const PANEL_BUNDLE_URL_TEMPLATE = 'https://github.com/fmktech/ftown/releases/download/ui-v<version>/ftown-ui-standalone-<version>.tar.gz';
90
+ // ---------- Endpoint & routing table ----------
91
+ //
92
+ // | Method | Path | Auth | Body/Result |
93
+ // |--------|-------------------------------|-----------------|--------------------|
94
+ // | GET | /api/solo/bootstrap | Bearer key | SoloBootstrap |
95
+ // | POST | /api/solo/token | Bearer key | SoloTokenResponse |
96
+ // | GET | /healthz | none | SoloHealth |
97
+ // | GET | /hub/connection/websocket | upgrade ONLY | proxied to hub |
98
+ // | * | /hub/* (anything else) | rejected | 404 |
99
+ // | * | /api/* (all other) | per local API | existing bridge API|
100
+ // | | — SOLO GUARD SUB: see S18 | | |
101
+ // | * | /* (everything else) | none | proxied to panel |
102
+ // | * | /* (everything else) | none | proxied to panel |
103
+ //
104
+ // ROUTING PRECEDENCE (exact match first, then longest-prefix): solo endpoints
105
+ // → /hub allowlist → /api/* to the EXISTING local API handler (the front and
106
+ // local-api-server compose; the bridge API is never shadowed by the panel) →
107
+ // everything else to the panel child.
108
+ //
109
+ // Errors: {"error": string} with 401 (bad key), 429 (rate limited), 404, 502
110
+ // (private child down) — matching local-api-server conventions.
111
+ //
112
+ // Pre-panel placeholder: while the panel child is not yet healthy, GET /
113
+ // (and only /) is served by the FRONT itself as a minimal inline HTML page
114
+ // ("Starting ftown Solo…", auto-refresh meta tag). This is the only HTML the
115
+ // front ever generates.
116
+ // ---------- Hub config keys (written by hub-manager, frozen values) ----------
117
+ //
118
+ // token_hmac_secret_key = hubSecret
119
+ // token_audience = HUB_JWT_AUDIENCE
120
+ // allowed_origins = [] // allow all — bearer+JWT gated; the
121
+ // // panel is served from arbitrary
122
+ // // LAN IPs/tunnel domains by design
123
+ // websocket_compression = false // proxy must not negotiate deflate
124
+ // client.allowed = false // no anonymous connections
125
+ // health = true
126
+ // Admin API, server API: disabled. Hub listens on 127.0.0.1:hubPort with its
127
+ // DEFAULT paths (/connection/websocket) — the proxy strips the /hub prefix,
128
+ // so no centrifugo path options are needed.
129
+ // ---------- Proxy rules (ws-proxy.ts, frozen) ----------
130
+ //
131
+ // P1. Only ^/hub/connection/websocket$ (after stripping the /hub prefix) is
132
+ // forwarded; plain HTTP under /hub/* never reaches the hub (404 at front).
133
+ // P2. Target hardcoded to 127.0.0.1:<hubPort>. Never configurable.
134
+ // P3. Strip inbound hop-by-hop headers: connection, keep-alive, proxy-authenticate,
135
+ // proxy-authorization, te, trailer, transfer-encoding, upgrade (re-set for
136
+ // upgrades), sec-websocket-* (let the 'ws' upstream handshake recompute
137
+ // Sec-WebSocket-Accept), permessage-deflate extension offer dropped.
138
+ // P4. Rewrite Host to 127.0.0.1:<hubPort>; set X-Forwarded-Proto from rule S-scheme.
139
+ // P5. Forward protocol-level ping/pong untouched (centrifugo ping 10s/pong 5s).
140
+ // ---------- Security invariants (implementation + tests enforce) ----------
141
+ //
142
+ // S1. Access key comparison constant-time; raw key never persisted and never
143
+ // written by the bridge to any file/log EXCEPT the one-time startup
144
+ // banner (deliberate UX delivery; documented exception).
145
+ // S2. /api/solo/* require Bearer key before any handler logic. /hub/* serves
146
+ // upgrades only (P1). The panel child is unauthenticated BY THE FRONT —
147
+ // the panel itself redirects to its /local key screen when bootstrap has
148
+ // not run (dual-mode auth); this is accepted and covered by the UI brief.
149
+ // S3. Rate limiting (solo-server-owned, NO XFF parsing in v1):
150
+ // - key failures: >=10 failures/60s per source IP → 429 + Retry-After: 60
151
+ // - global per-IP backstop on /api/solo/*: >240 req/min → 429
152
+ // Source IP = socket peer address via an INJECTED peerAddress(req) seam
153
+ // (default: socket.remoteAddress) so offline tests can simulate distinct
154
+ // peers. 256-bit key space makes online guessing non-viable; per-IP is
155
+ // acceptable behind tunnels (shared loopback source) because the UI
156
+ // caches the key — self-lockout requires repeated genuine auth failures.
157
+ // S4. Archive extraction rejects absolute entry paths, ".." segments, and
158
+ // symlink/hardlink entries (zip-slip); extraction target must resolve
159
+ // inside its dataDir subdirectory (realpath containment).
160
+ // S5. See proxy rules P1-P5 (hardcoded target, header policy, prefix allowlist).
161
+ // S6. Centrifugo downloads verified against CENTRIFUGO_SHA256 (embedded).
162
+ // Panel bundle verified against its published .sha256 sidecar over HTTPS.
163
+ // Digest mismatch aborts install with a clear error; nothing executes.
164
+ // S7. accessKeyHash + hubSecret persist 0600 under dataDir.
165
+ // S8. No cookies for solo auth anywhere (Bearer + fragment only) → CSRF-immune.
166
+ // The panel build must not mount SessionProvider (see UI module brief).
167
+ // S9. Plain HTTP on LAN is an accepted residual risk: a passive LAN observer
168
+ // captures the Bearer key from any authenticated call, and an active MITM
169
+ // additionally controls the unauthenticated panel HTML (key harvesting) —
170
+ // this MUST be stated prominently in the solo README. Tunnel providers
171
+ // terminate TLS. Front logs one console warning when binding a
172
+ // non-loopback interface.
173
+ // S10. Hub JWTs: alg fixed HS256, claims sub=SOLO_USER_ID, aud=HUB_JWT_AUDIENCE,
174
+ // iat, exp. Nothing parses or accepts other algorithms/claims shapes.
175
+ // S11. Key rotation is OUT OF HTTP SCOPE in v1: `ftown-bridge --solo --rotate-key`
176
+ // regenerates offline (new banner print). No rotation endpoint exists.
177
+ // S12. Boot sequence (frozen): front LISTENS FIRST → prints URLs immediately →
178
+ // async ensure(hub binary→config→spawn→health) and ensure(panel bundle→
179
+ // spawn→health). /healthz reflects live state; pre-panel GET / gets the
180
+ // placeholder page. URLs never depend on children being ready.
181
+ // S13. Inbound X-Forwarded-* are consumed only for scheme derivation (loopback
182
+ // peer required) and never relayed to children except that single field.
183
+ // S14. Every /api/solo/* response sets Cache-Control: no-store (live 12h JWTs
184
+ // in bodies). The pre-panel placeholder page is BYTE-STATIC — zero
185
+ // request-derived bytes — and also no-store. Panel-proxy responses that
186
+ // arrive without cache headers get a no-store passthrough guard.
187
+ // S15. No secret (raw key, accessKeyHash, hubSecret, JWT) ever appears in any
188
+ // process argv or environment variable: hubSecret reaches centrifugo
189
+ // EXCLUSIVELY via its 0600 config file path; children get only -c <path>.
190
+ // S16. The front and all managers never log: Authorization headers, presented
191
+ // keys, JWTs, or /hub request URLs including query strings.
192
+ // S17. Extraction hardening beyond S4: only regular-file and directory entries
193
+ // are extracted (explicit type allowlist); per-entry uncompressed cap,
194
+ // total uncompressed cap, and entry-count cap abort the install on
195
+ // exceed (decompression-bomb defense).
196
+ // S18. SOLO VS HOSTED API GUARDS: in solo mode the local API's loopback-Host
197
+ // guard is substituted by the injected peerAddress seam (any source is
198
+ // fine — Bearer is mandatory) and its Origin check is DROPPED (Bearer-
199
+ // only, no cookies → CSRF-immune). Hosted mode keeps both guards
200
+ // byte-for-byte unchanged. Constant-time Bearer verification is
201
+ // mandatory in both modes.
202
+ // S19. centrifugoUrl host derivation validates the request Host against the
203
+ // socket's local address:port (injectable allowlist seam for tunnels);
204
+ // absolute-form request lines are rejected 400. Host reflection into the
205
+ // bootstrap body must not be steerable by a third party.
206
+ // S20. Routing and ws-prefix stripping consume ONE parsed representation of
207
+ // req.url; golden tests pin //hub, percent-encoded slashes, case
208
+ // variation, trailing segments, and unicode against the allowlist.
209
+ // S21. PANEL_SOLO build-surface minimization: the solo panel build contains NO
210
+ // mutating server endpoints (route handlers/server actions) and performs
211
+ // no request-derived outbound fetches; the build asserts absence of
212
+ // middleware and /api/auth artifacts from the standalone output. Next.js
213
+ // stays current on security patches — post-tunnel this surface faces the
214
+ // internet directly.
215
+ // ---------- Module ownership (disjoint files; integrator touches index.ts) --
216
+ //
217
+ // solo/solo-auth.ts(+test) — key gen/hash/constant-time verify (S1);
218
+ // hub JWT mint/verify (S10)
219
+ // solo/hub-manager.ts(+test) — binary ensure (S6), config write (frozen keys),
220
+ // spawn/health/stop lifecycle
221
+ // solo/panel-manager.ts(+test)— bundle fetch/verify/extract (S4,S6), standalone
222
+ // spawn/health/stop lifecycle
223
+ // solo/ws-proxy.ts(+test) — P1-P5 HTTP + upgrade proxying
224
+ // solo/solo-server.ts(+test) — front server: routing table, auth gate (S2,S3),
225
+ // scheme derivation, placeholder page (S12),
226
+ // composition of the three managers + proxy
227
+ // ui/src/lib/auth-mode.tsx — dual-mode auth context (NextAuth | solo key):
228
+ // signInState, signOut, tokenRefresher callbacks
229
+ // ui/src/hooks/useCentrifugo.ts — EXTEND (owned file this build): optional
230
+ // tokenRefresher param replacing hardcoded
231
+ // /api/auth/token; onUnauthorized callback
232
+ // replacing '/login' redirect
233
+ // ui/src/app/local/page.tsx (+ ui/src/components/local/*) — #k fragment
234
+ // capture → localStorage, bootstrap call, states:
235
+ // no-key form / bad-key / starting(healthz poll) /
236
+ // ready(renders Dashboard)
237
+ // ui/src/components/DashboardClient.tsx — EXTEND: signOut via auth-mode ctx;
238
+ // providers.tsx — conditional SessionProvider
239
+ // ui solo build flag — PANEL_SOLO=1 at build time: root app/page.tsx
240
+ // becomes a redirect to /local (marketing landing
241
+ // and middleware are hosted-only); NextAuth pages
242
+ // (/login etc.) excluded from the solo build; nav
243
+ // links to /dashboard|/devices hidden in solo mode
244
+ // (their hosted APIs are unreachable through the
245
+ // proxy — accepted, documented in the UI brief)
246
+ // integrator (index.ts) — --solo/--port/--rotate-key flags, lifecycle L1-L4,
247
+ // console URL banner (S1 exception)
248
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/solo/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Solo hub manager — downloads, verifies, configures, spawns and stops the
3
+ * managed Centrifugo child (contract ownership: solo/hub-manager.ts(+test)).
4
+ *
5
+ * Dependency policy (deliberate): node builtins + node:child_process + the
6
+ * SYSTEM `tar` binary only — no new npm deps. tar(1) ships with macOS and
7
+ * Linux, the only platforms resolvePlatformTriple() supports, so archive
8
+ * listing/extraction needs no bundled extraction library.
9
+ *
10
+ * Invariants owned here: S6 (embedded CENTRIFUGO_SHA256 verification before
11
+ * anything is extracted), S7 (0600 config + pidfile), S15 (child argv is
12
+ * exactly [binPath, '-c', configPath]; no secrets in argv/env), S16 (logs
13
+ * never echo secrets — stderr tails are sanitized), L2 (stale pidfile reap
14
+ * under dataDir/solo/ on boot).
15
+ */
16
+ import type { SpawnOptions } from 'node:child_process';
17
+ export type PlatformTriple = 'darwin-arm64' | 'darwin-amd64' | 'linux-amd64' | 'linux-arm64';
18
+ /** Minimal fetch seam so downloads and health probes run offline in tests. */
19
+ export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
20
+ /** Structural view of the child we spawn (satisfied by node's ChildProcess). */
21
+ export interface HubChildProcess {
22
+ pid: number | undefined;
23
+ stdout: NodeJS.ReadableStream | null;
24
+ stderr: NodeJS.ReadableStream | null;
25
+ kill(signal?: NodeJS.Signals | number): boolean;
26
+ once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
27
+ on(event: 'error', listener: (error: Error) => void): unknown;
28
+ }
29
+ export type SpawnLike = (command: string, args: readonly string[], options: SpawnOptions) => HubChildProcess;
30
+ export declare class UnsupportedPlatformError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ export declare class ChecksumError extends Error {
34
+ constructor(message: string);
35
+ }
36
+ export declare class ArchiveSafetyError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ export declare class HubStartError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ export declare function resolvePlatformTriple(platform?: string, arch?: string): PlatformTriple;
43
+ export declare function assetUrl(version: string, triple: PlatformTriple): string;
44
+ export interface EnsureHubBinaryOptions {
45
+ dataDir: string;
46
+ version?: string;
47
+ fetchImpl?: FetchLike;
48
+ /**
49
+ * Test seam overriding the embedded digest map. Production callers MUST
50
+ * leave it unset so verification stays pinned to contract CENTRIFUGO_SHA256.
51
+ */
52
+ digests?: Readonly<Record<string, string>>;
53
+ }
54
+ /**
55
+ * Validates tar entries against the extraction allowlist (S4/S17):
56
+ * names must be relative without '..' segments; entry types must be regular
57
+ * file ('-') or directory ('d') — symlinks, hardlinks, devices are refused.
58
+ */
59
+ export declare function assertSafeTarEntries(names: readonly string[], verboseLines: readonly string[]): void;
60
+ export declare function ensureHubBinary(opts: EnsureHubBinaryOptions): Promise<string>;
61
+ export declare function writeHubConfig(configPath: string, opts: {
62
+ port: number;
63
+ secret: string;
64
+ }): Promise<void>;
65
+ export interface StartHubOptions {
66
+ configPath: string;
67
+ binPath: string;
68
+ dataDir: string;
69
+ fetchImpl?: FetchLike;
70
+ spawnImpl?: SpawnLike;
71
+ /** Overrides http://127.0.0.1:<port> as the base for /health probes (tests). */
72
+ healthBaseUrl?: string;
73
+ healthIntervalMs?: number;
74
+ healthTryTimeoutMs?: number;
75
+ healthDeadlineMs?: number;
76
+ }
77
+ export interface RunningHub {
78
+ child: HubChildProcess;
79
+ pid: number | undefined;
80
+ port: number;
81
+ }
82
+ export declare function hubPidFilePath(dataDir: string): string;
83
+ export declare function startHub(opts: StartHubOptions): Promise<RunningHub>;
84
+ /** SIGTERM → 3s grace → SIGKILL; always unlinks the pidfile. Returns whether a live process was stopped. */
85
+ export declare function stopHub(dataDir: string): Promise<boolean>;