mango-cms 0.3.27 → 0.3.28
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/lib/devProxyGateway.js +35 -1
- package/lib/staging-gateway.js +25 -0
- package/package.json +1 -1
package/lib/devProxyGateway.js
CHANGED
|
@@ -282,9 +282,18 @@ export function createHttpResolver({
|
|
|
282
282
|
* - Matching staging host → resolve via Mango → http-proxy to the workspace port.
|
|
283
283
|
* - WebSocket upgrade (Vite HMR) → same resolution → proxy.ws to the same port.
|
|
284
284
|
* - Non-staging host → 404 with a clear message (no misroute).
|
|
285
|
-
* - Resolver errors (
|
|
285
|
+
* - Resolver errors (403/404/502) → surfaced as the resolver's HTTP status.
|
|
286
286
|
* - Upgrade-time resolver failure → socket destroyed (Vite retries the WS).
|
|
287
287
|
*
|
|
288
|
+
* Page-load auth (HAP-1173): a top-level browser navigation (`GET /`) carries
|
|
289
|
+
* neither Cookie nor Authorization — the Vibe front's session is a localStorage
|
|
290
|
+
* token attached only to explicit fetch/XHR. If the gateway gated those on auth
|
|
291
|
+
* the editing surface couldn't load at all. So when `fallbackPort` is configured
|
|
292
|
+
* AND the request has no Cookie + no Authorization, the gateway proxies the
|
|
293
|
+
* request to that port (the shared front) instead of 401-ing. Branch selection
|
|
294
|
+
* happens client-side after the SPA loads; subsequent authenticated calls carry
|
|
295
|
+
* `x-vibe-branch` and resolve to the per-branch workspace as before.
|
|
296
|
+
*
|
|
288
297
|
* Returns { server, proxy, close() } — caller owns server.listen(port, host).
|
|
289
298
|
*/
|
|
290
299
|
export function createStagingGatewayServer({
|
|
@@ -295,6 +304,7 @@ export function createStagingGatewayServer({
|
|
|
295
304
|
backendPort,
|
|
296
305
|
resolverPath,
|
|
297
306
|
timeoutMs,
|
|
307
|
+
fallbackPort,
|
|
298
308
|
onError,
|
|
299
309
|
} = {}) {
|
|
300
310
|
if (!http || typeof http.createServer !== 'function') {
|
|
@@ -306,9 +316,16 @@ export function createStagingGatewayServer({
|
|
|
306
316
|
if (!Number.isInteger(backendPort)) {
|
|
307
317
|
throw new Error('createStagingGatewayServer: backendPort (integer) is required')
|
|
308
318
|
}
|
|
319
|
+
if (fallbackPort != null && (!Number.isInteger(fallbackPort) || fallbackPort <= 0 || fallbackPort > 65535)) {
|
|
320
|
+
throw new Error('createStagingGatewayServer: fallbackPort, if set, must be an integer in 1..65535')
|
|
321
|
+
}
|
|
309
322
|
|
|
310
323
|
const resolve = createHttpResolver({ backendPort, resolverPath, timeoutMs, http })
|
|
311
324
|
const proxy = httpProxy.createProxyServer({ ws: true, xfwd: true })
|
|
325
|
+
const fallbackTarget = Number.isInteger(fallbackPort) ? `http://127.0.0.1:${fallbackPort}` : null
|
|
326
|
+
function isUnauthenticated(req) {
|
|
327
|
+
return !(req?.headers?.cookie) && !(req?.headers?.authorization)
|
|
328
|
+
}
|
|
312
329
|
|
|
313
330
|
const log = (where, err, req) => {
|
|
314
331
|
if (typeof onError === 'function') onError(where, err, req)
|
|
@@ -342,6 +359,15 @@ export function createStagingGatewayServer({
|
|
|
342
359
|
sendError(res, 404, `Unknown host: ${req?.headers?.host || '(missing Host header)'}`)
|
|
343
360
|
return
|
|
344
361
|
}
|
|
362
|
+
// HAP-1173: a raw browser navigation carries no Cookie + no Authorization
|
|
363
|
+
// (the front's session is a localStorage token attached only to fetch/XHR).
|
|
364
|
+
// Without a fallback the SPA shell can't load → editing surface is dead.
|
|
365
|
+
// With one configured, serve page + assets from the shared front; the
|
|
366
|
+
// branch picker fires client-side and later authenticated calls demux.
|
|
367
|
+
if (fallbackTarget && isUnauthenticated(req)) {
|
|
368
|
+
proxy.web(req, res, { target: fallbackTarget })
|
|
369
|
+
return
|
|
370
|
+
}
|
|
345
371
|
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
346
372
|
if (outcome.error) {
|
|
347
373
|
sendError(res, outcome.status || 502, outcome.message || outcome.error)
|
|
@@ -356,6 +382,14 @@ export function createStagingGatewayServer({
|
|
|
356
382
|
try { socket.destroy() } catch { /* noop */ }
|
|
357
383
|
return
|
|
358
384
|
}
|
|
385
|
+
// Page-load HMR upgrade rides without auth on the very first connect
|
|
386
|
+
// (same reason as handleRequest above). Send it to the fallback front so
|
|
387
|
+
// Vite HMR works for the un-routed shell; once the user picks a branch,
|
|
388
|
+
// the next upgrade carries `x-vibe-branch` and routes per-branch.
|
|
389
|
+
if (fallbackTarget && isUnauthenticated(req)) {
|
|
390
|
+
proxy.ws(req, socket, head, { target: fallbackTarget })
|
|
391
|
+
return
|
|
392
|
+
}
|
|
359
393
|
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
360
394
|
if (outcome.error) {
|
|
361
395
|
// Don't write an HTTP response onto an upgrade socket — Vite retries.
|
package/lib/staging-gateway.js
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* STAGING_HOST_PREFIX (default "staging.") Prefix fallback for slug demux.
|
|
19
19
|
* RESOLVER_PATH (default /system/vibe/staging-resolve)
|
|
20
20
|
* RESOLVER_TIMEOUT_MS (default 2000, int)
|
|
21
|
+
* FALLBACK_PORT (default 7121, int) Where to send requests with no Cookie
|
|
22
|
+
* and no Authorization header — i.e. a
|
|
23
|
+
* browser page-load (HAP-1173). Usually
|
|
24
|
+
* the site's shared front. Set to 0 to
|
|
25
|
+
* disable and 401 those requests instead.
|
|
21
26
|
*
|
|
22
27
|
* Usage (typical droplet pm2):
|
|
23
28
|
* BACKEND_PORT=7122 LISTEN_PORT=7123 \
|
|
@@ -40,6 +45,19 @@ function intEnv(name, fallback) {
|
|
|
40
45
|
return n
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
function fallbackPortEnv() {
|
|
49
|
+
// Allow `FALLBACK_PORT=0` to opt out (page-loads → 401, the pre-1173 behavior).
|
|
50
|
+
const raw = process.env.FALLBACK_PORT
|
|
51
|
+
if (raw === undefined) return 7121
|
|
52
|
+
if (raw === '' || raw === '0') return null
|
|
53
|
+
const n = parseInt(raw, 10)
|
|
54
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
55
|
+
console.error(`[staging-gateway] FALLBACK_PORT must be a port in 1..65535 or 0/empty (got "${raw}")`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
return n
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
const BACKEND_PORT = intEnv('BACKEND_PORT', null)
|
|
44
62
|
if (BACKEND_PORT === null) {
|
|
45
63
|
console.error('[staging-gateway] BACKEND_PORT (int) is required')
|
|
@@ -52,6 +70,7 @@ const STAGING_DOMAIN = process.env.STAGING_DOMAIN || ''
|
|
|
52
70
|
const STAGING_HOST_PREFIX = process.env.STAGING_HOST_PREFIX || undefined
|
|
53
71
|
const RESOLVER_PATH = process.env.RESOLVER_PATH || undefined
|
|
54
72
|
const RESOLVER_TIMEOUT_MS = intEnv('RESOLVER_TIMEOUT_MS', 2000)
|
|
73
|
+
const FALLBACK_PORT = fallbackPortEnv()
|
|
55
74
|
|
|
56
75
|
const { server, close } = createStagingGatewayServer({
|
|
57
76
|
http,
|
|
@@ -61,6 +80,7 @@ const { server, close } = createStagingGatewayServer({
|
|
|
61
80
|
backendPort: BACKEND_PORT,
|
|
62
81
|
resolverPath: RESOLVER_PATH,
|
|
63
82
|
timeoutMs: RESOLVER_TIMEOUT_MS,
|
|
83
|
+
fallbackPort: FALLBACK_PORT,
|
|
64
84
|
onError: (where, err) => {
|
|
65
85
|
console.error(`[staging-gateway] ${where} error: ${err?.message || err}`)
|
|
66
86
|
},
|
|
@@ -70,6 +90,11 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
|
|
|
70
90
|
const target = STAGING_DOMAIN || `${STAGING_HOST_PREFIX || 'staging.'}*`
|
|
71
91
|
console.log(`[staging-gateway] listening on ${LISTEN_HOST}:${LISTEN_PORT}`)
|
|
72
92
|
console.log(`[staging-gateway] ${target} -> per-branch Vibe workspace (resolved via 127.0.0.1:${BACKEND_PORT})`)
|
|
93
|
+
if (FALLBACK_PORT) {
|
|
94
|
+
console.log(`[staging-gateway] page-load (no Cookie + no Authorization) -> 127.0.0.1:${FALLBACK_PORT}`)
|
|
95
|
+
} else {
|
|
96
|
+
console.log('[staging-gateway] page-load fallback disabled (no Cookie + no Authorization → 401)')
|
|
97
|
+
}
|
|
73
98
|
})
|
|
74
99
|
|
|
75
100
|
function shutdown(signal) {
|