mango-cms 0.3.26 → 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.
@@ -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 (401/403/404/502) → surfaced as the resolver's HTTP status.
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.
@@ -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) {
package/package.json CHANGED
@@ -1,107 +1,107 @@
1
1
  {
2
- "name": "mango-cms",
3
- "version": "0.3.26",
4
- "type": "module",
5
- "main": "./index.js",
6
- "exports": {
7
- ".": "./index.js",
8
- "./package.json": "./package.json"
9
- },
10
- "files": [
11
- "index.js",
12
- "cli.js",
13
- "vite.config.js",
14
- "lib/**/*",
15
- "default/**/*",
16
- "!default/node_modules/**"
17
- ],
18
- "author": "Colton Neifert",
19
- "license": "Commercial",
20
- "bin": {
21
- "mango": "cli.js"
22
- },
23
- "scripts": {
24
- "dev": "node cli.js dev",
25
- "start": "node cli.js start",
26
- "build": "node cli.js build"
27
- },
28
- "peerDependencies": {
29
- "algoliasearch": "^4.10.3",
30
- "axios": "^0.21.4",
31
- "vue": "^3.0.0",
32
- "vue-router": "^4.0.0"
33
- },
34
- "devDependencies": {
35
- "@vitejs/plugin-vue": "^4.0.0",
36
- "vite": "^4.0.0"
37
- },
38
- "dependencies": {
39
- "@aws-sdk/client-s3": "^3.423.0",
40
- "@aws-sdk/lib-storage": "^3.423.0",
41
- "@sentry/node": "^7.85.0",
42
- "@sentry/profiling-node": "^1.2.6",
43
- "adm-zip": "^0.5.16",
44
- "algoliasearch": "^4.10.3",
45
- "apollo-server": "^2.19.1",
46
- "apollo-server-express": "^2.19.1",
47
- "archiver": "^7.0.1",
48
- "aws-sdk": "^2.1469.0",
49
- "axios": "^0.21.4",
50
- "cli-progress": "^3.12.0",
51
- "connect-multiparty": "^2.2.0",
52
- "connect-redis": "7",
53
- "cors": "^2.8.5",
54
- "country-state-city": "^3.2.1",
55
- "crypto": "^1.0.1",
56
- "dayjs": "^1.11.6",
57
- "dotenv": "^8.2.0",
58
- "express": "^4.17.1",
59
- "express-session": "^1.19.0",
60
- "fast-csv": "^5.0.5",
61
- "fs-extra": "^11.3.0",
62
- "get-audio-duration": "^3.1.0",
63
- "googleapis": "^109.0.1",
64
- "graphql": "^15.4.0",
65
- "graphql-fields": "^2.0.3",
66
- "graphql-parse-resolve-info": "^4.12.0",
67
- "graphql-subscriptions": "^1.1.0",
68
- "graphql-type-datetime": "^0.2.4",
69
- "graphql-ws": "^5.9.0",
70
- "html-to-text": "^8.0.0",
71
- "http-proxy": "^1.18.1",
72
- "inquirer": "^8.2.4",
73
- "json-fn": "^1.1.1",
74
- "lodash": "^4.17.21",
75
- "mailgun-js": "^0.22.0",
76
- "mailgun.js": "^9.3.0",
77
- "md5": "^2.3.0",
78
- "mime-types": "^2.1.35",
79
- "moment": "^2.29.1",
80
- "mongodb": "^6.8.0",
81
- "multer": "^1.4.2",
82
- "node-lame": "^1.3.2",
83
- "node-zip": "^1.1.1",
84
- "nodemon": "^3.0.0",
85
- "openai": "^5.15.0",
86
- "pack": "^2.2.0",
87
- "parse-html-text-content": "^1.1.1",
88
- "redis": "^4.7.0",
89
- "resend": "^6.3.0",
90
- "sharp": "^0.32.1",
91
- "socket.io": "^4.8.0",
92
- "socket.io-redis": "^6.1.1",
93
- "source-map-support": "^0.5.19",
94
- "ssh2-sftp-client": "^12.0.1",
95
- "stream": "^0.0.2",
96
- "subscriptions-transport-ws": "^0.9.18",
97
- "util": "^0.12.3",
98
- "uuid": "3.4.0",
99
- "web": "^0.0.2"
100
- },
101
- "optionalDependencies": {
102
- "bufferutil": "^4.0.8",
103
- "kerberos": "^2.0.0",
104
- "snappy": "^7.2.2",
105
- "utf-8-validate": "^6.0.4"
106
- }
2
+ "name": "mango-cms",
3
+ "version": "0.3.28",
4
+ "type": "module",
5
+ "main": "./index.js",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "cli.js",
13
+ "vite.config.js",
14
+ "lib/**/*",
15
+ "default/**/*",
16
+ "!default/node_modules/**"
17
+ ],
18
+ "author": "Colton Neifert",
19
+ "license": "Commercial",
20
+ "bin": {
21
+ "mango": "cli.js"
22
+ },
23
+ "scripts": {
24
+ "dev": "node cli.js dev",
25
+ "start": "node cli.js start",
26
+ "build": "node cli.js build"
27
+ },
28
+ "peerDependencies": {
29
+ "algoliasearch": "^4.10.3",
30
+ "axios": "^0.21.4",
31
+ "vue": "^3.0.0",
32
+ "vue-router": "^4.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@vitejs/plugin-vue": "^4.0.0",
36
+ "vite": "^4.0.0"
37
+ },
38
+ "dependencies": {
39
+ "@aws-sdk/client-s3": "^3.423.0",
40
+ "@aws-sdk/lib-storage": "^3.423.0",
41
+ "@sentry/node": "^7.85.0",
42
+ "@sentry/profiling-node": "^1.2.6",
43
+ "adm-zip": "^0.5.16",
44
+ "algoliasearch": "^4.10.3",
45
+ "apollo-server": "^2.19.1",
46
+ "apollo-server-express": "^2.19.1",
47
+ "archiver": "^7.0.1",
48
+ "aws-sdk": "^2.1469.0",
49
+ "axios": "^0.21.4",
50
+ "cli-progress": "^3.12.0",
51
+ "connect-multiparty": "^2.2.0",
52
+ "connect-redis": "7",
53
+ "cors": "^2.8.5",
54
+ "country-state-city": "^3.2.1",
55
+ "crypto": "^1.0.1",
56
+ "dayjs": "^1.11.6",
57
+ "dotenv": "^8.2.0",
58
+ "express": "^4.17.1",
59
+ "express-session": "^1.19.0",
60
+ "fast-csv": "^5.0.5",
61
+ "fs-extra": "^11.3.0",
62
+ "get-audio-duration": "^3.1.0",
63
+ "googleapis": "^109.0.1",
64
+ "graphql": "^15.4.0",
65
+ "graphql-fields": "^2.0.3",
66
+ "graphql-parse-resolve-info": "^4.12.0",
67
+ "graphql-subscriptions": "^1.1.0",
68
+ "graphql-type-datetime": "^0.2.4",
69
+ "graphql-ws": "^5.9.0",
70
+ "html-to-text": "^8.0.0",
71
+ "http-proxy": "^1.18.1",
72
+ "inquirer": "^8.2.4",
73
+ "json-fn": "^1.1.1",
74
+ "lodash": "^4.17.21",
75
+ "mailgun-js": "^0.22.0",
76
+ "mailgun.js": "^9.3.0",
77
+ "md5": "^2.3.0",
78
+ "mime-types": "^2.1.35",
79
+ "moment": "^2.29.1",
80
+ "mongodb": "^6.8.0",
81
+ "multer": "^1.4.2",
82
+ "node-lame": "^1.3.2",
83
+ "node-zip": "^1.1.1",
84
+ "nodemon": "^3.0.0",
85
+ "openai": "^5.15.0",
86
+ "pack": "^2.2.0",
87
+ "parse-html-text-content": "^1.1.1",
88
+ "redis": "^4.7.0",
89
+ "resend": "^6.3.0",
90
+ "sharp": "^0.32.1",
91
+ "socket.io": "^4.8.0",
92
+ "socket.io-redis": "^6.1.1",
93
+ "source-map-support": "^0.5.19",
94
+ "ssh2-sftp-client": "^12.0.1",
95
+ "stream": "^0.0.2",
96
+ "subscriptions-transport-ws": "^0.9.18",
97
+ "util": "^0.12.3",
98
+ "uuid": "3.4.0",
99
+ "web": "^0.0.2"
100
+ },
101
+ "optionalDependencies": {
102
+ "bufferutil": "^4.0.8",
103
+ "kerberos": "^2.0.0",
104
+ "snappy": "^7.2.2",
105
+ "utf-8-validate": "^6.0.4"
106
+ }
107
107
  }