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.
- package/dist/index.js +228 -3
- package/dist/index.js.map +1 -1
- package/dist/solo/contract.d.ts +104 -0
- package/dist/solo/contract.js +248 -0
- package/dist/solo/contract.js.map +1 -0
- package/dist/solo/hub-manager.d.ts +85 -0
- package/dist/solo/hub-manager.js +381 -0
- package/dist/solo/hub-manager.js.map +1 -0
- package/dist/solo/panel-manager.d.ts +129 -0
- package/dist/solo/panel-manager.js +715 -0
- package/dist/solo/panel-manager.js.map +1 -0
- package/dist/solo/solo-auth.d.ts +25 -0
- package/dist/solo/solo-auth.js +83 -0
- package/dist/solo/solo-auth.js.map +1 -0
- package/dist/solo/solo-server.d.ts +105 -0
- package/dist/solo/solo-server.js +399 -0
- package/dist/solo/solo-server.js.map +1 -0
- package/dist/solo/ws-proxy.d.ts +55 -0
- package/dist/solo/ws-proxy.js +228 -0
- package/dist/solo/ws-proxy.js.map +1 -0
- package/package.json +1 -1
|
@@ -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>;
|