mango-cms 0.3.31 → 0.3.33
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 +103 -36
- package/package.json +1 -1
package/lib/devProxyGateway.js
CHANGED
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
|
|
28
28
|
export const DEFAULT_STAGING_PREFIX = 'staging.'
|
|
29
29
|
|
|
30
|
+
// HAP-1188 (Option B): the container shell points its iframe at
|
|
31
|
+
// `<branch>--<baseStagingHost>` so the branch travels in the Host header on the
|
|
32
|
+
// iframe's document AND every one of its sub-resource + HMR-WS requests — sticky
|
|
33
|
+
// per-branch with zero cookie pollution. This is the separator between the
|
|
34
|
+
// branch prefix and the base staging host. The front builds it from
|
|
35
|
+
// VITE_VIBE_SHELL_HOST_TEMPLATE="{branch}--staging-vibe.<site>".
|
|
36
|
+
export const DEFAULT_BRANCH_HOST_SEPARATOR = '--'
|
|
37
|
+
|
|
30
38
|
// Header / cookie the editing surface (and the orchestrator) use to carry the
|
|
31
39
|
// SELECTED BRANCH with a staging request (HAP-1164). The branch — not the session
|
|
32
40
|
// identity — is what the resolver demuxes on, so it must travel with every
|
|
@@ -120,37 +128,84 @@ export function hasBranchSignal(req) {
|
|
|
120
128
|
return false
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
131
|
+
// Internal: match an already-normalized (port-stripped, trimmed, lowercased)
|
|
132
|
+
// hostname as a staging host, returning { hostname, slug } | null. This is the
|
|
133
|
+
// pre-HAP-1188 matcher, factored out so the branch-in-hostname peeling below can
|
|
134
|
+
// reuse it on the host's remainder.
|
|
125
135
|
//
|
|
126
136
|
// Matches in this order:
|
|
127
|
-
// 1. exact match against opts.stagingDomain
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
// Returns null when no match, or when the slug would be empty. Port suffixes
|
|
134
|
-
// (":80") and case are normalized away so the matcher is stable across
|
|
135
|
-
// browsers, curl, and the Vite HMR upgrade.
|
|
136
|
-
export function parseStagingHost(hostHeader, opts = {}) {
|
|
137
|
-
if (!hostHeader) return null
|
|
138
|
-
const prefix = String(opts.stagingHostPrefix || DEFAULT_STAGING_PREFIX).toLowerCase()
|
|
139
|
-
const hostname = String(hostHeader).split(':')[0].trim().toLowerCase()
|
|
137
|
+
// 1. exact match against `configured` (opts.stagingDomain, when set) — slug is
|
|
138
|
+
// the part after the staging prefix, or the full hostname if it has none.
|
|
139
|
+
// 2. any hostname starting with `prefix` (defaults to "staging.") — slug is the
|
|
140
|
+
// part after that prefix.
|
|
141
|
+
function matchStagingBase(hostname, prefix, configured) {
|
|
140
142
|
if (!hostname) return null
|
|
141
|
-
|
|
142
|
-
const configured = opts.stagingDomain ? String(opts.stagingDomain).toLowerCase() : null
|
|
143
143
|
if (configured && hostname === configured) {
|
|
144
144
|
const slug = hostname.startsWith(prefix) ? hostname.slice(prefix.length) : hostname
|
|
145
145
|
if (!slug) return null
|
|
146
146
|
return { hostname, slug }
|
|
147
147
|
}
|
|
148
|
-
|
|
149
148
|
if (hostname.startsWith(prefix)) {
|
|
150
149
|
const slug = hostname.slice(prefix.length)
|
|
151
150
|
if (!slug) return null
|
|
152
151
|
return { hostname, slug }
|
|
153
152
|
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Pure: does this request's Host header look like a staging request, what site
|
|
157
|
+
// slug does it resolve to, and — when the container shell encodes the branch in
|
|
158
|
+
// the hostname (HAP-1188, Option B) — which branch?
|
|
159
|
+
//
|
|
160
|
+
// Returns null when no match, or when the slug would be empty. Port suffixes
|
|
161
|
+
// (":80") and case are normalized away so the matcher is stable across
|
|
162
|
+
// browsers, curl, and the Vite HMR upgrade.
|
|
163
|
+
//
|
|
164
|
+
// Branch-in-hostname (HAP-1188): a host of the form `<branch>--<baseStagingHost>`
|
|
165
|
+
// resolves to the same { hostname, slug } as `<baseStagingHost>` PLUS a `branch`
|
|
166
|
+
// field. The gateway treats that host-branch as a deliberate branch signal, so
|
|
167
|
+
// the iframe's document, modules, AND HMR WebSocket all demux to the one branch's
|
|
168
|
+
// dev server — no `?branch=` on sub-resources, no origin-wide cookie. The shell
|
|
169
|
+
// host (`<baseStagingHost>`, no `--` prefix) carries no branch and is unaffected.
|
|
170
|
+
export function parseStagingHost(hostHeader, opts = {}) {
|
|
171
|
+
if (!hostHeader) return null
|
|
172
|
+
const prefix = String(opts.stagingHostPrefix || DEFAULT_STAGING_PREFIX).toLowerCase()
|
|
173
|
+
const sep = String(opts.branchHostSeparator ?? DEFAULT_BRANCH_HOST_SEPARATOR)
|
|
174
|
+
const raw = String(hostHeader).split(':')[0].trim()
|
|
175
|
+
if (!raw) return null
|
|
176
|
+
const hostname = raw.toLowerCase()
|
|
177
|
+
const configured = opts.stagingDomain ? String(opts.stagingDomain).toLowerCase() : null
|
|
178
|
+
|
|
179
|
+
// Fast path: the shell host and every pre-HAP-1188 staging host (no branch
|
|
180
|
+
// prefix) match directly. This also means a plain host is never misread as a
|
|
181
|
+
// branch-prefixed one.
|
|
182
|
+
const direct = matchStagingBase(hostname, prefix, configured)
|
|
183
|
+
if (direct) return direct
|
|
184
|
+
|
|
185
|
+
// Branch-in-hostname: peel a leading `<branch>--` and match the REMAINDER as
|
|
186
|
+
// an ordinary staging host. Scan separator positions left→right and accept
|
|
187
|
+
// the first split whose remainder is a valid base host — so a branch whose
|
|
188
|
+
// sanitized form itself contains `--` (e.g. two non-git chars in a row) still
|
|
189
|
+
// resolves to the real base host rather than splitting in the middle.
|
|
190
|
+
if (sep) {
|
|
191
|
+
let from = 0
|
|
192
|
+
let idx
|
|
193
|
+
while ((idx = hostname.indexOf(sep, from)) > 0) {
|
|
194
|
+
const base = matchStagingBase(hostname.slice(idx + sep.length), prefix, configured)
|
|
195
|
+
if (base) {
|
|
196
|
+
// Slice the ORIGINAL-case host for the branch (toLowerCase keeps
|
|
197
|
+
// length, so `idx` is valid in both). Browsers usually lowercase
|
|
198
|
+
// the whole Host, but don't assume it. Percent-decode because the
|
|
199
|
+
// front builds the prefix with encodeURIComponent(sanitizeBranch()).
|
|
200
|
+
const rawBranch = raw.slice(0, idx)
|
|
201
|
+
let branch = rawBranch
|
|
202
|
+
try { branch = decodeURIComponent(rawBranch) } catch { /* keep raw */ }
|
|
203
|
+
branch = branch.trim()
|
|
204
|
+
return branch ? { ...base, branch } : base
|
|
205
|
+
}
|
|
206
|
+
from = idx + sep.length
|
|
207
|
+
}
|
|
208
|
+
}
|
|
154
209
|
|
|
155
210
|
return null
|
|
156
211
|
}
|
|
@@ -170,7 +225,7 @@ export function parseStagingHost(hostHeader, opts = {}) {
|
|
|
170
225
|
// no_workspace (404) — resolver returned nothing for (user, slug)
|
|
171
226
|
// resolver_error (502) — resolver threw / returned a bad shape
|
|
172
227
|
// forbidden (403) — resolver explicitly refused (e.g. wrong site)
|
|
173
|
-
export async function resolveStagingTarget({ req, slug, resolve }) {
|
|
228
|
+
export async function resolveStagingTarget({ req, slug, resolve, branch }) {
|
|
174
229
|
if (typeof resolve !== 'function') {
|
|
175
230
|
return { error: 'resolver_error', status: 502, message: 'No staging resolver configured' }
|
|
176
231
|
}
|
|
@@ -184,13 +239,19 @@ export async function resolveStagingTarget({ req, slug, resolve }) {
|
|
|
184
239
|
}
|
|
185
240
|
}
|
|
186
241
|
|
|
187
|
-
// The SELECTED BRANCH (HAP-1164)
|
|
188
|
-
//
|
|
189
|
-
|
|
242
|
+
// The SELECTED BRANCH (HAP-1164) is what the resolver demuxes on. It travels
|
|
243
|
+
// either in the Host header (HAP-1188 branch-in-hostname, passed in here as
|
|
244
|
+
// `branch` by the gateway) or on the request itself (x-vibe-branch / ?branch=
|
|
245
|
+
// / vibe_branch cookie). An explicit host-branch wins — it's the most
|
|
246
|
+
// deliberate signal and is what keeps the iframe's sub-resources sticky.
|
|
247
|
+
// Absent → resolver falls back to the configured default.
|
|
248
|
+
const selectedBranch = (branch != null && String(branch).trim())
|
|
249
|
+
? String(branch).trim()
|
|
250
|
+
: parseBranchFromReq(req)
|
|
190
251
|
|
|
191
252
|
let result
|
|
192
253
|
try {
|
|
193
|
-
result = await resolve({ slug, branch, cookieHeader, authHeader, req })
|
|
254
|
+
result = await resolve({ slug, branch: selectedBranch, cookieHeader, authHeader, req })
|
|
194
255
|
} catch (err) {
|
|
195
256
|
return { error: 'resolver_error', status: 502, message: `Vibe staging resolver error: ${err?.message || err}` }
|
|
196
257
|
}
|
|
@@ -424,12 +485,15 @@ export function createStagingGatewayServer({
|
|
|
424
485
|
sendError(res, 404, `Unknown host: ${req?.headers?.host || '(missing Host header)'}`)
|
|
425
486
|
return
|
|
426
487
|
}
|
|
427
|
-
// HAP-1175: default route is the shared fallback front. Only
|
|
428
|
-
// that carry a
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
|
|
488
|
+
// HAP-1175 / HAP-1188: default route is the shared fallback front. Only
|
|
489
|
+
// requests that carry a DELIBERATE branch signal attempt per-branch
|
|
490
|
+
// resolution — the branch in the Host header (HAP-1188, `<branch>--<host>`),
|
|
491
|
+
// a vibe_branch cookie, an x-vibe-branch header, or a ?branch= URL param.
|
|
492
|
+
// A returning browser with stale session cookies but no branch signal —
|
|
493
|
+
// the case that re-broke staging in HAP-1175 — passes straight through to
|
|
494
|
+
// the fallback. The shell host has no host-branch, so it lands here too.
|
|
495
|
+
const hostBranch = parsed.branch || ''
|
|
496
|
+
if (!hostBranch && !hasBranchSignal(req)) {
|
|
433
497
|
if (fallbackTarget) {
|
|
434
498
|
proxy.web(req, res, { target: fallbackTarget })
|
|
435
499
|
return
|
|
@@ -437,7 +501,7 @@ export function createStagingGatewayServer({
|
|
|
437
501
|
sendError(res, 401, 'Vibe staging: no branch signal and no fallback configured')
|
|
438
502
|
return
|
|
439
503
|
}
|
|
440
|
-
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
504
|
+
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve, branch: hostBranch })
|
|
441
505
|
if (outcome.error) {
|
|
442
506
|
// HAP-1175: even with a branch signal, a failed resolve falls back
|
|
443
507
|
// to the shared front rather than 401-ing the page load. Hard errors
|
|
@@ -458,11 +522,14 @@ export function createStagingGatewayServer({
|
|
|
458
522
|
try { socket.destroy() } catch { /* noop */ }
|
|
459
523
|
return
|
|
460
524
|
}
|
|
461
|
-
// HAP-1175: same default-to-fallback rule as handleRequest.
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
|
|
525
|
+
// HAP-1175 / HAP-1188: same default-to-fallback rule as handleRequest.
|
|
526
|
+
// The shell's own HMR upgrade rides without a branch signal → fallback.
|
|
527
|
+
// The iframe's HMR WebSocket carries the branch in the Host header
|
|
528
|
+
// (`<branch>--<host>`), so it resolves to the SAME per-branch dev server
|
|
529
|
+
// as the iframe's document — this is what closes the dual-Vue / white-
|
|
530
|
+
// screen gap (HAP-1177 class) that `?branch=`-on-document-only left open.
|
|
531
|
+
const hostBranch = parsed.branch || ''
|
|
532
|
+
if (!hostBranch && !hasBranchSignal(req)) {
|
|
466
533
|
if (fallbackTarget) {
|
|
467
534
|
proxy.ws(req, socket, head, { target: fallbackTarget })
|
|
468
535
|
return
|
|
@@ -470,7 +537,7 @@ export function createStagingGatewayServer({
|
|
|
470
537
|
try { socket.destroy() } catch { /* noop */ }
|
|
471
538
|
return
|
|
472
539
|
}
|
|
473
|
-
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
|
|
540
|
+
const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve, branch: hostBranch })
|
|
474
541
|
if (outcome.error) {
|
|
475
542
|
// Failed resolve on an upgrade: prefer the fallback over destroying
|
|
476
543
|
// the socket so HMR keeps working through the shared front.
|