mango-cms 0.3.28 → 0.3.30
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 +113 -28
- package/lib/staging-gateway.js +8 -6
- package/package.json +1 -1
- package/vite.config.js +12 -0
package/lib/devProxyGateway.js
CHANGED
|
@@ -34,13 +34,31 @@ export const DEFAULT_STAGING_PREFIX = 'staging.'
|
|
|
34
34
|
export const BRANCH_HEADER = 'x-vibe-branch'
|
|
35
35
|
export const BRANCH_COOKIE = 'vibe_branch'
|
|
36
36
|
|
|
37
|
-
// Pure: pull the selected branch out of a request
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// Pure: pull the selected branch out of a request. Precedence (high → low):
|
|
38
|
+
// 1. x-vibe-branch header (set by fetch/XHR after the picker)
|
|
39
|
+
// 2. ?branch=<name> URL search param (deliberate URL navigation, HAP-1175)
|
|
40
|
+
// 3. vibe_branch cookie (set by the picker after selection)
|
|
41
|
+
// Returns '' when none is present so the resolver can fall back to the site's
|
|
42
|
+
// configured default branch.
|
|
40
43
|
export function parseBranchFromReq(req) {
|
|
41
44
|
const headerVal = req?.headers?.[BRANCH_HEADER]
|
|
42
45
|
if (headerVal) return String(Array.isArray(headerVal) ? headerVal[0] : headerVal).trim()
|
|
43
46
|
|
|
47
|
+
const url = req?.url || ''
|
|
48
|
+
const q = url.indexOf('?')
|
|
49
|
+
if (q !== -1) {
|
|
50
|
+
for (const pair of url.slice(q + 1).split('&')) {
|
|
51
|
+
const eq = pair.indexOf('=')
|
|
52
|
+
const key = eq === -1 ? pair : pair.slice(0, eq)
|
|
53
|
+
if (key === 'branch') {
|
|
54
|
+
const val = eq === -1 ? '' : pair.slice(eq + 1)
|
|
55
|
+
if (val) {
|
|
56
|
+
try { return decodeURIComponent(val.trim()) } catch { return val.trim() }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
const cookieHeader = req?.headers?.cookie || ''
|
|
45
63
|
for (const pair of String(cookieHeader).split(';')) {
|
|
46
64
|
const idx = pair.indexOf('=')
|
|
@@ -56,6 +74,52 @@ export function parseBranchFromReq(req) {
|
|
|
56
74
|
return ''
|
|
57
75
|
}
|
|
58
76
|
|
|
77
|
+
// Pure: does this request carry a DELIBERATE branch signal (HAP-1175)?
|
|
78
|
+
//
|
|
79
|
+
// The gateway uses this — not "is there any Cookie/Authorization header" — to
|
|
80
|
+
// decide whether to attempt per-branch resolution. Random session cookies left
|
|
81
|
+
// over from prior visits do NOT count; only signals the app sets on purpose:
|
|
82
|
+
//
|
|
83
|
+
// 1. the `x-vibe-branch` request header (set by fetch/XHR after the picker)
|
|
84
|
+
// 2. a non-empty `?branch=` URL search param (deliberate URL navigation)
|
|
85
|
+
// 3. a non-empty `vibe_branch` cookie (set by the picker after selection)
|
|
86
|
+
//
|
|
87
|
+
// Returns true iff at least one of those carries a non-empty value. A request
|
|
88
|
+
// with only unrelated cookies (e.g. `foo=bar`, `connect.sid=...`) returns false
|
|
89
|
+
// so the gateway defaults it to the shared fallback front, not the resolver.
|
|
90
|
+
export function hasBranchSignal(req) {
|
|
91
|
+
const headerVal = req?.headers?.[BRANCH_HEADER]
|
|
92
|
+
if (headerVal && String(Array.isArray(headerVal) ? headerVal[0] : headerVal).trim()) {
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const url = req?.url || ''
|
|
97
|
+
const q = url.indexOf('?')
|
|
98
|
+
if (q !== -1) {
|
|
99
|
+
for (const pair of url.slice(q + 1).split('&')) {
|
|
100
|
+
const eq = pair.indexOf('=')
|
|
101
|
+
const key = eq === -1 ? pair : pair.slice(0, eq)
|
|
102
|
+
if (key === 'branch') {
|
|
103
|
+
const val = eq === -1 ? '' : pair.slice(eq + 1)
|
|
104
|
+
if (val) return true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cookieHeader = req?.headers?.cookie || ''
|
|
110
|
+
if (cookieHeader) {
|
|
111
|
+
for (const pair of String(cookieHeader).split(';')) {
|
|
112
|
+
const idx = pair.indexOf('=')
|
|
113
|
+
if (idx === -1) continue
|
|
114
|
+
if (pair.slice(0, idx).trim() === BRANCH_COOKIE) {
|
|
115
|
+
if (pair.slice(idx + 1).trim()) return true
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
59
123
|
// Pure: does this request's Host header look like a staging request, and what
|
|
60
124
|
// site slug does it resolve to?
|
|
61
125
|
//
|
|
@@ -285,14 +349,18 @@ export function createHttpResolver({
|
|
|
285
349
|
* - Resolver errors (403/404/502) → surfaced as the resolver's HTTP status.
|
|
286
350
|
* - Upgrade-time resolver failure → socket destroyed (Vite retries the WS).
|
|
287
351
|
*
|
|
288
|
-
* Page-load auth (HAP-1173): a top-level browser navigation
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* the
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* `
|
|
352
|
+
* Page-load auth (HAP-1175, supersedes HAP-1173): a top-level browser navigation
|
|
353
|
+
* MUST never 401, regardless of what cookies it carries. Real browsers attach
|
|
354
|
+
* stale session cookies from prior visits on every request, so a "has Cookie →
|
|
355
|
+
* try the resolver" gate locks out every returning user (the original HAP-1173
|
|
356
|
+
* fix only solved the cookie-less case). The new contract: the gateway defaults
|
|
357
|
+
* ALL requests to `fallbackPort` (the shared front) and only resolves a per-
|
|
358
|
+
* branch workspace when the request carries a DELIBERATE branch signal — the
|
|
359
|
+
* `vibe_branch` cookie the picker sets after selection, the `x-vibe-branch`
|
|
360
|
+
* header on fetch/XHR, or a `?branch=` URL search param. If a signaled resolve
|
|
361
|
+
* fails (no auth, no workspace, resolver error), the request STILL falls back
|
|
362
|
+
* to the shared front rather than 401-ing. Hard errors only surface in
|
|
363
|
+
* "lockdown" mode where no `fallbackPort` is configured.
|
|
296
364
|
*
|
|
297
365
|
* Returns { server, proxy, close() } — caller owns server.listen(port, host).
|
|
298
366
|
*/
|
|
@@ -323,9 +391,6 @@ export function createStagingGatewayServer({
|
|
|
323
391
|
const resolve = createHttpResolver({ backendPort, resolverPath, timeoutMs, http })
|
|
324
392
|
const proxy = httpProxy.createProxyServer({ ws: true, xfwd: true })
|
|
325
393
|
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
|
-
}
|
|
329
394
|
|
|
330
395
|
const log = (where, err, req) => {
|
|
331
396
|
if (typeof onError === 'function') onError(where, err, req)
|
|
@@ -359,17 +424,28 @@ export function createStagingGatewayServer({
|
|
|
359
424
|
sendError(res, 404, `Unknown host: ${req?.headers?.host || '(missing Host header)'}`)
|
|
360
425
|
return
|
|
361
426
|
}
|
|
362
|
-
// HAP-
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
if (
|
|
368
|
-
|
|
427
|
+
// HAP-1175: default route is the shared fallback front. Only requests
|
|
428
|
+
// that carry a deliberate branch signal (vibe_branch cookie, x-vibe-branch
|
|
429
|
+
// header, or ?branch= URL param) attempt per-branch resolution. A returning
|
|
430
|
+
// browser with stale session cookies but no branch signal — the case that
|
|
431
|
+
// re-broke staging in HAP-1175 — passes straight through here.
|
|
432
|
+
if (!hasBranchSignal(req)) {
|
|
433
|
+
if (fallbackTarget) {
|
|
434
|
+
proxy.web(req, res, { target: fallbackTarget })
|
|
435
|
+
return
|
|
436
|
+
}
|
|
437
|
+
sendError(res, 401, 'Vibe staging: no branch signal and no fallback configured')
|
|
369
438
|
return
|
|
370
439
|
}
|
|
371
440
|
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
372
441
|
if (outcome.error) {
|
|
442
|
+
// HAP-1175: even with a branch signal, a failed resolve falls back
|
|
443
|
+
// to the shared front rather than 401-ing the page load. Hard errors
|
|
444
|
+
// surface only in lockdown mode (no fallback configured).
|
|
445
|
+
if (fallbackTarget) {
|
|
446
|
+
proxy.web(req, res, { target: fallbackTarget })
|
|
447
|
+
return
|
|
448
|
+
}
|
|
373
449
|
sendError(res, outcome.status || 502, outcome.message || outcome.error)
|
|
374
450
|
return
|
|
375
451
|
}
|
|
@@ -382,17 +458,26 @@ export function createStagingGatewayServer({
|
|
|
382
458
|
try { socket.destroy() } catch { /* noop */ }
|
|
383
459
|
return
|
|
384
460
|
}
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
if (
|
|
390
|
-
|
|
461
|
+
// HAP-1175: same default-to-fallback rule as handleRequest. The Vite
|
|
462
|
+
// HMR upgrade for the shared shell rides without a branch signal; only
|
|
463
|
+
// after the picker fires does the next upgrade carry x-vibe-branch and
|
|
464
|
+
// resolve per-branch.
|
|
465
|
+
if (!hasBranchSignal(req)) {
|
|
466
|
+
if (fallbackTarget) {
|
|
467
|
+
proxy.ws(req, socket, head, { target: fallbackTarget })
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
try { socket.destroy() } catch { /* noop */ }
|
|
391
471
|
return
|
|
392
472
|
}
|
|
393
473
|
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
394
474
|
if (outcome.error) {
|
|
395
|
-
//
|
|
475
|
+
// Failed resolve on an upgrade: prefer the fallback over destroying
|
|
476
|
+
// the socket so HMR keeps working through the shared front.
|
|
477
|
+
if (fallbackTarget) {
|
|
478
|
+
proxy.ws(req, socket, head, { target: fallbackTarget })
|
|
479
|
+
return
|
|
480
|
+
}
|
|
396
481
|
try { socket.destroy() } catch { /* noop */ }
|
|
397
482
|
return
|
|
398
483
|
}
|
package/lib/staging-gateway.js
CHANGED
|
@@ -18,10 +18,12 @@
|
|
|
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
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* FALLBACK_PORT (default 7121, int) Where to send any request without a
|
|
22
|
+
* deliberate branch signal (vibe_branch
|
|
23
|
+
* cookie, x-vibe-branch header, or
|
|
24
|
+
* ?branch= URL param), or any signaled
|
|
25
|
+
* request whose resolution fails. The
|
|
26
|
+
* shared front (HAP-1175). Set to 0 to
|
|
25
27
|
* disable and 401 those requests instead.
|
|
26
28
|
*
|
|
27
29
|
* Usage (typical droplet pm2):
|
|
@@ -91,9 +93,9 @@ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
|
|
|
91
93
|
console.log(`[staging-gateway] listening on ${LISTEN_HOST}:${LISTEN_PORT}`)
|
|
92
94
|
console.log(`[staging-gateway] ${target} -> per-branch Vibe workspace (resolved via 127.0.0.1:${BACKEND_PORT})`)
|
|
93
95
|
if (FALLBACK_PORT) {
|
|
94
|
-
console.log(`[staging-gateway]
|
|
96
|
+
console.log(`[staging-gateway] no branch signal / failed resolve -> 127.0.0.1:${FALLBACK_PORT}`)
|
|
95
97
|
} else {
|
|
96
|
-
console.log('[staging-gateway]
|
|
98
|
+
console.log('[staging-gateway] fallback disabled (no branch signal → 401, failed resolve → resolver status)')
|
|
97
99
|
}
|
|
98
100
|
})
|
|
99
101
|
|
package/package.json
CHANGED
package/vite.config.js
CHANGED
|
@@ -34,6 +34,17 @@ export default defineConfig(({ mode }) => {
|
|
|
34
34
|
|
|
35
35
|
const buildPath = legacyMode ? mangoRoot : userProjectRoot
|
|
36
36
|
|
|
37
|
+
// HAP-1180: honour an explicit per-worktree Vite cache dir. Every per-branch
|
|
38
|
+
// vibe dev watcher (`vite build --watch`) is spawned with cwd = the SHARED
|
|
39
|
+
// node_modules/mango-cms, so without an override they all share one default
|
|
40
|
+
// `.vite` optimize cache and contend — the cross-branch half of the
|
|
41
|
+
// leaked-watcher → white-screen failure (HAP-1179). The dev-server wrapper
|
|
42
|
+
// (generations-vibe mango/helpers/vibeDevServer.mjs) exports
|
|
43
|
+
// MANGO_VITE_CACHE_DIR=<worktree>/.vite-cache (absolute); read it here so each
|
|
44
|
+
// worktree's build keeps an isolated cache. No-op when the env var is unset.
|
|
45
|
+
const cacheDir = process.env.MANGO_VITE_CACHE_DIR || process.env.VITE_CACHE_DIR || undefined
|
|
46
|
+
if (cacheDir) console.log('Vite cacheDir (per-worktree):', cacheDir)
|
|
47
|
+
|
|
37
48
|
// Auto-detect dependencies to externalize (avoids bundling native modules, etc.)
|
|
38
49
|
const getDeps = (pkgPath) => {
|
|
39
50
|
if (!fs.existsSync(pkgPath)) return []
|
|
@@ -59,6 +70,7 @@ export default defineConfig(({ mode }) => {
|
|
|
59
70
|
})
|
|
60
71
|
|
|
61
72
|
return {
|
|
73
|
+
...(cacheDir ? { cacheDir } : {}),
|
|
62
74
|
build: {
|
|
63
75
|
ssr: path.resolve(mangoRoot, 'src/main.js'),
|
|
64
76
|
outDir: path.resolve(buildPath, 'build'),
|