mango-cms 0.3.23 → 0.3.26

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/cli.js CHANGED
@@ -879,6 +879,48 @@ program
879
879
  }
880
880
  });
881
881
 
882
+ program
883
+ .command('launch')
884
+ .description('One-shot server deploy: git pull, install, build the site, and deploy backend + UI')
885
+ .action(async () => {
886
+ try {
887
+ const cwd = process.cwd();
888
+ const useYarn = fs.existsSync(path.join(cwd, 'yarn.lock'));
889
+ const pm = useYarn ? 'yarn' : 'npm';
890
+ const run = (label, cmd) => {
891
+ console.log(`\n→ ${label}\n $ ${cmd}`);
892
+ execSync(cmd, { stdio: 'inherit', cwd });
893
+ };
894
+
895
+ console.log('šŸš€ Launching Mango deployment...');
896
+
897
+ // 1. Pull the latest code for this project.
898
+ run('Pulling latest code', 'git pull');
899
+
900
+ // 2. Install dependencies (picks up any version bumps, incl. mango-cms).
901
+ run('Installing dependencies', useYarn ? 'yarn install' : 'npm install');
902
+
903
+ // 3. Build the public front-end site (-> dist), if a build script exists.
904
+ const pkgPath = path.join(cwd, 'package.json');
905
+ const hasBuildScript = fs.existsSync(pkgPath) && JSON.parse(fs.readFileSync(pkgPath, 'utf8'))?.scripts?.build;
906
+ if (hasBuildScript) {
907
+ run('Building site', useYarn ? 'yarn build' : 'npm run build');
908
+ } else {
909
+ console.log('\n→ Skipping site build (no "build" script found)');
910
+ }
911
+
912
+ // 4. Deploy backend + UI + web server config via the freshly installed mango.
913
+ // `mango deploy` builds the backend, (re)starts PM2 for {db}-mango, {db}
914
+ // and {db}-ui, and writes the nginx/apache blocks for all three domains.
915
+ run('Deploying backend + UI', 'npx mango deploy');
916
+
917
+ console.log('\n✨ Launch complete! Backend, site, and UI are deployed.');
918
+ } catch (error) {
919
+ console.error('\nLaunch failed:', error.message);
920
+ process.exit(1);
921
+ }
922
+ });
923
+
882
924
  program
883
925
  .command('build')
884
926
  .description('Build the Mango CMS for production')
package/lib/dev-proxy.js CHANGED
@@ -6,12 +6,21 @@
6
6
  * Runs on port 80, routes requests by hostname to the correct local dev server.
7
7
  * Supports WebSocket upgrade for Vite HMR and Socket.io.
8
8
  *
9
- * Usage: node lib/dev-proxy.js <frontDomain> <apiDomain> <frontPort> <backendPort> [pidPath] [uiDomain] [uiPort]
9
+ * Usage: node lib/dev-proxy.js <frontDomain> <apiDomain> <frontPort> <backendPort> [pidPath] [uiDomain] [uiPort] [stagingDomain]
10
+ *
11
+ * When [stagingDomain] is provided (or any request comes in with a "staging."
12
+ * Host header), the proxy enables the identity-aware Vibe staging route
13
+ * (HAP-1155): the per-request user identity is resolved by forwarding the
14
+ * Cookie / Authorization header to Mango, the vibeWorkspaces registry is
15
+ * consulted for that (userId, slug) pair, and the request — including the Vite
16
+ * HMR WebSocket upgrade — is proxied to that user's dev server port.
17
+ * See lib/devProxyGateway.js for the demux details.
10
18
  */
11
19
 
12
20
  import http from 'http';
13
21
  import httpProxy from 'http-proxy';
14
22
  import fs from 'fs';
23
+ import { parseStagingHost, resolveStagingTarget, createHttpResolver } from './devProxyGateway.js';
15
24
 
16
25
  const frontDomain = process.argv[2];
17
26
  const apiDomain = process.argv[3];
@@ -20,9 +29,10 @@ const backendPort = parseInt(process.argv[5], 10);
20
29
  const pidPath = process.argv[6];
21
30
  const uiDomain = process.argv[7];
22
31
  const uiPort = process.argv[8] ? parseInt(process.argv[8], 10) : null;
32
+ const stagingDomain = process.argv[9] || null;
23
33
 
24
34
  if (!frontDomain || !apiDomain || !frontendPort || !backendPort) {
25
- console.error('Usage: node lib/dev-proxy.js <frontDomain> <apiDomain> <frontPort> <backendPort> [pidPath] [uiDomain] [uiPort]');
35
+ console.error('Usage: node lib/dev-proxy.js <frontDomain> <apiDomain> <frontPort> <backendPort> [pidPath] [uiDomain] [uiPort] [stagingDomain]');
26
36
  process.exit(1);
27
37
  }
28
38
 
@@ -39,6 +49,11 @@ proxy.on('error', (err, req, res) => {
39
49
  }
40
50
  });
41
51
 
52
+ // Production resolver for the staging route: hits a small Mango endpoint that
53
+ // authorizes the member (cookie/auth) and looks up vibeWorkspaces by the selected
54
+ // (site, branch) — the branch travels with the request (HAP-1164).
55
+ const stagingResolver = createHttpResolver({ backendPort, http });
56
+
42
57
  function getTarget(host) {
43
58
  const hostname = host?.split(':')[0];
44
59
  if (hostname === apiDomain) {
@@ -53,18 +68,70 @@ function getTarget(host) {
53
68
  return null;
54
69
  }
55
70
 
71
+ // Identify the staging request (if any) BEFORE the simple host-route table —
72
+ // staging.<frontDomain> would otherwise fall through to 404.
73
+ function parseStaging(host) {
74
+ return parseStagingHost(host, { stagingDomain });
75
+ }
76
+
77
+ function sendError(res, status, message) {
78
+ if (!res || res.headersSent) return;
79
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
80
+ res.end(message);
81
+ }
82
+
83
+ async function handleStagingRequest(req, res, parsed) {
84
+ const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve: stagingResolver });
85
+ if (outcome.error) {
86
+ sendError(res, outcome.status || 502, outcome.message || outcome.error);
87
+ return;
88
+ }
89
+ proxy.web(req, res, { target: outcome.target });
90
+ }
91
+
92
+ async function handleStagingUpgrade(req, socket, head, parsed) {
93
+ const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve: stagingResolver });
94
+ if (outcome.error) {
95
+ // Don't write an HTTP error onto an upgrade socket — just close it.
96
+ // The browser surfaces this as a WS connect failure (Vite will retry).
97
+ socket.destroy();
98
+ return;
99
+ }
100
+ proxy.ws(req, socket, head, { target: outcome.target });
101
+ }
102
+
56
103
  const server = http.createServer((req, res) => {
57
- const target = getTarget(req.headers.host);
104
+ const host = req.headers.host;
105
+ const staging = parseStaging(host);
106
+ if (staging) {
107
+ handleStagingRequest(req, res, staging).catch((err) => {
108
+ console.error(`Staging request error for ${host}${req.url}:`, err.message);
109
+ sendError(res, 502, `Staging error: ${err.message}`);
110
+ });
111
+ return;
112
+ }
113
+
114
+ const target = getTarget(host);
58
115
  if (!target) {
59
116
  res.writeHead(404);
60
- res.end(`Unknown host: ${req.headers.host}`);
117
+ res.end(`Unknown host: ${host}`);
61
118
  return;
62
119
  }
63
120
  proxy.web(req, res, { target });
64
121
  });
65
122
 
66
123
  server.on('upgrade', (req, socket, head) => {
67
- const target = getTarget(req.headers.host);
124
+ const host = req.headers.host;
125
+ const staging = parseStaging(host);
126
+ if (staging) {
127
+ handleStagingUpgrade(req, socket, head, staging).catch((err) => {
128
+ console.error(`Staging upgrade error for ${host}${req.url}:`, err.message);
129
+ socket.destroy();
130
+ });
131
+ return;
132
+ }
133
+
134
+ const target = getTarget(host);
68
135
  if (target) {
69
136
  proxy.ws(req, socket, head, { target });
70
137
  } else {
@@ -79,6 +146,11 @@ server.listen(80, '0.0.0.0', () => {
79
146
  if (uiDomain && uiPort) {
80
147
  console.log(` ${uiDomain} -> http://127.0.0.1:${uiPort} (ui)`);
81
148
  }
149
+ if (stagingDomain) {
150
+ console.log(` ${stagingDomain} -> per-user Vibe workspace (identity-aware)`);
151
+ } else {
152
+ console.log(` staging.* -> per-user Vibe workspace (identity-aware)`);
153
+ }
82
154
  });
83
155
 
84
156
  // Write PID for cleanup
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Pure helpers for the dev-proxy's identity-aware staging route (HAP-1155).
3
+ *
4
+ * The dev-proxy already host-routes the site's own front / api / ui domains 1:1
5
+ * (see lib/dev-proxy.js). This module adds the multi-user staging route designed
6
+ * in HAP-1096 §5 / R1: ONE shared staging hostname per site that demuxes by the
7
+ * authenticated session identity into the right user's Vibe workspace port.
8
+ *
9
+ * Split into three pure, dependency-injected pieces so the proxy can stay
10
+ * standalone-runnable AND the demux logic stays unit-testable with plain node:
11
+ *
12
+ * parseStagingHost(host, opts) -> { hostname, slug } | null
13
+ * resolveStagingTarget({req,slug,resolve}) -> { port, target } | { error, status, message }
14
+ * createHttpResolver({ backendPort }) -> resolve({ slug, cookieHeader, authHeader })
15
+ *
16
+ * The proxy never owns session state. The HTTP resolver forwards the original
17
+ * Cookie / Authorization headers to Mango on the backend port — Mango identifies
18
+ * the member (`req.session.memberId` for the session cookie, member by
19
+ * password-hash for the Authorization header — same path getMember() uses
20
+ * everywhere else) and authorizes them, but the workspace is keyed by the
21
+ * SELECTED BRANCH (HAP-1164), looked up as `vibeWorkspaces` by (site, branch).
22
+ * The branch travels with the request (x-vibe-branch header / vibe_branch cookie);
23
+ * two members on the same branch resolve to the same port. The endpoint that does
24
+ * the lookup is owned by the provisioning service (HAP-1154); the contract is
25
+ * documented on createHttpResolver below.
26
+ */
27
+
28
+ export const DEFAULT_STAGING_PREFIX = 'staging.'
29
+
30
+ // Header / cookie the editing surface (and the orchestrator) use to carry the
31
+ // SELECTED BRANCH with a staging request (HAP-1164). The branch — not the session
32
+ // identity — is what the resolver demuxes on, so it must travel with every
33
+ // request. Header wins over cookie when both are present.
34
+ export const BRANCH_HEADER = 'x-vibe-branch'
35
+ export const BRANCH_COOKIE = 'vibe_branch'
36
+
37
+ // Pure: pull the selected branch out of a request's headers (header first, then
38
+ // the `vibe_branch` cookie). Returns '' when none is present so the resolver can
39
+ // fall back to the site's configured default branch.
40
+ export function parseBranchFromReq(req) {
41
+ const headerVal = req?.headers?.[BRANCH_HEADER]
42
+ if (headerVal) return String(Array.isArray(headerVal) ? headerVal[0] : headerVal).trim()
43
+
44
+ const cookieHeader = req?.headers?.cookie || ''
45
+ for (const pair of String(cookieHeader).split(';')) {
46
+ const idx = pair.indexOf('=')
47
+ if (idx === -1) continue
48
+ if (pair.slice(0, idx).trim() === BRANCH_COOKIE) {
49
+ try {
50
+ return decodeURIComponent(pair.slice(idx + 1).trim())
51
+ } catch {
52
+ return pair.slice(idx + 1).trim()
53
+ }
54
+ }
55
+ }
56
+ return ''
57
+ }
58
+
59
+ // Pure: does this request's Host header look like a staging request, and what
60
+ // site slug does it resolve to?
61
+ //
62
+ // Matches in this order:
63
+ // 1. exact match against opts.stagingDomain (when configured) — slug is the
64
+ // part of the hostname after the staging prefix, or the full hostname if
65
+ // it doesn't have one.
66
+ // 2. any hostname starting with opts.stagingHostPrefix (defaults to
67
+ // "staging.") — slug is the part after that prefix.
68
+ //
69
+ // Returns null when no match, or when the slug would be empty. Port suffixes
70
+ // (":80") and case are normalized away so the matcher is stable across
71
+ // browsers, curl, and the Vite HMR upgrade.
72
+ export function parseStagingHost(hostHeader, opts = {}) {
73
+ if (!hostHeader) return null
74
+ const prefix = String(opts.stagingHostPrefix || DEFAULT_STAGING_PREFIX).toLowerCase()
75
+ const hostname = String(hostHeader).split(':')[0].trim().toLowerCase()
76
+ if (!hostname) return null
77
+
78
+ const configured = opts.stagingDomain ? String(opts.stagingDomain).toLowerCase() : null
79
+ if (configured && hostname === configured) {
80
+ const slug = hostname.startsWith(prefix) ? hostname.slice(prefix.length) : hostname
81
+ if (!slug) return null
82
+ return { hostname, slug }
83
+ }
84
+
85
+ if (hostname.startsWith(prefix)) {
86
+ const slug = hostname.slice(prefix.length)
87
+ if (!slug) return null
88
+ return { hostname, slug }
89
+ }
90
+
91
+ return null
92
+ }
93
+
94
+ // Async: turn a parsed staging request into a concrete proxy target.
95
+ //
96
+ // Delegates the (identity + workspace) lookup to an injected `resolve` function
97
+ // — the proxy never owns session state. `resolve` receives the Cookie and
98
+ // Authorization headers from the original request so the same auth that the
99
+ // browser sent rides through (this is also why HMR lands on the right port:
100
+ // the WS upgrade ships the same cookie, so a second call to resolveStagingTarget
101
+ // from the upgrade handler picks the same port).
102
+ //
103
+ // The error contract is explicit so the proxy can return a clear response
104
+ // instead of misrouting — every failure path names its category:
105
+ // unauthenticated (401) — no Cookie and no Authorization header sent
106
+ // no_workspace (404) — resolver returned nothing for (user, slug)
107
+ // resolver_error (502) — resolver threw / returned a bad shape
108
+ // forbidden (403) — resolver explicitly refused (e.g. wrong site)
109
+ export async function resolveStagingTarget({ req, slug, resolve }) {
110
+ if (typeof resolve !== 'function') {
111
+ return { error: 'resolver_error', status: 502, message: 'No staging resolver configured' }
112
+ }
113
+ const cookieHeader = req?.headers?.cookie || ''
114
+ const authHeader = req?.headers?.authorization || ''
115
+ if (!cookieHeader && !authHeader) {
116
+ return {
117
+ error: 'unauthenticated',
118
+ status: 401,
119
+ message: 'Vibe staging requires an authenticated session (no Cookie / Authorization header)',
120
+ }
121
+ }
122
+
123
+ // The SELECTED BRANCH (HAP-1164) travels with the request and is what the
124
+ // resolver demuxes on. Absent → resolver falls back to the configured default.
125
+ const branch = parseBranchFromReq(req)
126
+
127
+ let result
128
+ try {
129
+ result = await resolve({ slug, branch, cookieHeader, authHeader, req })
130
+ } catch (err) {
131
+ return { error: 'resolver_error', status: 502, message: `Vibe staging resolver error: ${err?.message || err}` }
132
+ }
133
+
134
+ if (!result) {
135
+ return { error: 'no_workspace', status: 404, message: `No Vibe workspace for slug "${slug}"` }
136
+ }
137
+ if (result.error) {
138
+ // resolver may pre-shape an error response (e.g. unauthenticated/forbidden)
139
+ return result
140
+ }
141
+ if (!Number.isInteger(result.port) || result.port <= 0 || result.port > 65535) {
142
+ return {
143
+ error: 'resolver_error',
144
+ status: 502,
145
+ message: `Vibe staging resolver returned invalid port ${JSON.stringify(result.port)}`,
146
+ }
147
+ }
148
+ return { port: result.port, target: `http://127.0.0.1:${result.port}` }
149
+ }
150
+
151
+ /**
152
+ * Production resolver: calls Mango (running on the backend port colocated with
153
+ * this proxy) at a small system endpoint that does identity + workspace lookup.
154
+ *
155
+ * GET <resolverPath>?slug=<slug>[&branch=<branch>]
156
+ * forwards: Cookie, Authorization
157
+ *
158
+ * 200 { port: <int> } workspace found, proxy to that port
159
+ * 401 no member could be identified
160
+ * 403 member identified, but not authorized for slug
161
+ * 404 no vibeWorkspaces record for (site, branch)
162
+ * 5xx / network error surfaced as resolver_error
163
+ *
164
+ * The endpoint implementation lives with the provisioning service (HAP-1154)
165
+ * where the session helpers and vibeWorkspacesService are already wired up.
166
+ * Keeping it in Mango means the proxy never needs Redis / Mongo credentials.
167
+ *
168
+ * `http` is injected so the resolver is unit-testable without binding a real
169
+ * socket. Defaults to node's built-in http module via the dev-proxy entry.
170
+ */
171
+ export function createHttpResolver({
172
+ backendPort,
173
+ resolverPath = '/system/vibe/staging-resolve',
174
+ timeoutMs = 2000,
175
+ http,
176
+ } = {}) {
177
+ if (!Number.isInteger(backendPort)) {
178
+ throw new Error('createHttpResolver: backendPort (integer) is required')
179
+ }
180
+ if (!http || typeof http.request !== 'function') {
181
+ throw new Error('createHttpResolver: http module with request() is required')
182
+ }
183
+
184
+ return function resolve({ slug, branch, cookieHeader, authHeader }) {
185
+ return new Promise((resolvePromise) => {
186
+ let path = `${resolverPath}?slug=${encodeURIComponent(slug)}`
187
+ if (branch) path += `&branch=${encodeURIComponent(branch)}`
188
+ const headers = {}
189
+ if (cookieHeader) headers.cookie = cookieHeader
190
+ if (authHeader) headers.authorization = authHeader
191
+
192
+ const request = http.request(
193
+ { host: '127.0.0.1', port: backendPort, method: 'GET', path, headers },
194
+ (res) => {
195
+ let body = ''
196
+ res.setEncoding('utf8')
197
+ res.on('data', (chunk) => {
198
+ body += chunk
199
+ })
200
+ res.on('end', () => {
201
+ const status = res.statusCode
202
+ if (status === 401) {
203
+ return resolvePromise({
204
+ error: 'unauthenticated',
205
+ status: 401,
206
+ message: 'Vibe staging: unauthenticated',
207
+ })
208
+ }
209
+ if (status === 403) {
210
+ return resolvePromise({
211
+ error: 'forbidden',
212
+ status: 403,
213
+ message: 'Vibe staging: forbidden for this slug',
214
+ })
215
+ }
216
+ if (status === 404) {
217
+ return resolvePromise({
218
+ error: 'no_workspace',
219
+ status: 404,
220
+ message: `Vibe staging: no workspace for "${slug}"`,
221
+ })
222
+ }
223
+ if (status < 200 || status >= 300) {
224
+ return resolvePromise({
225
+ error: 'resolver_error',
226
+ status: 502,
227
+ message: `Vibe staging resolver returned ${status}: ${body.slice(0, 200)}`,
228
+ })
229
+ }
230
+ let parsed
231
+ try {
232
+ parsed = JSON.parse(body)
233
+ } catch (err) {
234
+ return resolvePromise({
235
+ error: 'resolver_error',
236
+ status: 502,
237
+ message: `Vibe staging resolver returned non-JSON: ${err.message}`,
238
+ })
239
+ }
240
+ if (!Number.isInteger(parsed?.port)) {
241
+ return resolvePromise({
242
+ error: 'resolver_error',
243
+ status: 502,
244
+ message: 'Vibe staging resolver returned no integer port',
245
+ })
246
+ }
247
+ return resolvePromise({ port: parsed.port })
248
+ })
249
+ },
250
+ )
251
+
252
+ const onError = (err) => {
253
+ resolvePromise({
254
+ error: 'resolver_error',
255
+ status: 502,
256
+ message: `Vibe staging resolver request failed: ${err?.message || err}`,
257
+ })
258
+ }
259
+
260
+ request.setTimeout(timeoutMs, () => {
261
+ request.destroy(new Error(`timed out after ${timeoutMs}ms`))
262
+ })
263
+ request.on('error', onError)
264
+ request.end()
265
+ })
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Standalone staging-gateway server factory (HAP-1165).
271
+ *
272
+ * Wraps parseStagingHost + resolveStagingTarget + createHttpResolver into a
273
+ * runnable HTTP server fronted by nginx on the droplet. Listens on a configured
274
+ * loopback port; nginx proxy_passes `staging-vibe.<site>` to it instead of to
275
+ * the shared `:7121` front, so per-branch isolation actually happens.
276
+ *
277
+ * Pure DI: `http` and `httpProxy` are injected so the module itself has no
278
+ * runtime imports and stays unit-testable with plain node fakes. The thin
279
+ * CLI entry `lib/staging-gateway.js` wires the real modules.
280
+ *
281
+ * Behavior:
282
+ * - Matching staging host → resolve via Mango → http-proxy to the workspace port.
283
+ * - WebSocket upgrade (Vite HMR) → same resolution → proxy.ws to the same port.
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.
286
+ * - Upgrade-time resolver failure → socket destroyed (Vite retries the WS).
287
+ *
288
+ * Returns { server, proxy, close() } — caller owns server.listen(port, host).
289
+ */
290
+ export function createStagingGatewayServer({
291
+ http,
292
+ httpProxy,
293
+ stagingDomain,
294
+ stagingHostPrefix,
295
+ backendPort,
296
+ resolverPath,
297
+ timeoutMs,
298
+ onError,
299
+ } = {}) {
300
+ if (!http || typeof http.createServer !== 'function') {
301
+ throw new Error('createStagingGatewayServer: http module with createServer() is required')
302
+ }
303
+ if (!httpProxy || typeof httpProxy.createProxyServer !== 'function') {
304
+ throw new Error('createStagingGatewayServer: httpProxy module with createProxyServer() is required')
305
+ }
306
+ if (!Number.isInteger(backendPort)) {
307
+ throw new Error('createStagingGatewayServer: backendPort (integer) is required')
308
+ }
309
+
310
+ const resolve = createHttpResolver({ backendPort, resolverPath, timeoutMs, http })
311
+ const proxy = httpProxy.createProxyServer({ ws: true, xfwd: true })
312
+
313
+ const log = (where, err, req) => {
314
+ if (typeof onError === 'function') onError(where, err, req)
315
+ }
316
+
317
+ proxy.on('error', (err, req, res) => {
318
+ log('proxy', err, req)
319
+ if (res && typeof res.writeHead === 'function' && !res.headersSent) {
320
+ res.writeHead(502, { 'Content-Type': 'text/plain; charset=utf-8' })
321
+ res.end(`Bad gateway: ${err?.message || err}`)
322
+ } else if (res && typeof res.destroy === 'function') {
323
+ // Upgrade path: `res` is a Socket — close it, browser retries.
324
+ try { res.destroy() } catch { /* noop */ }
325
+ }
326
+ })
327
+
328
+ const parseHostOpts = { stagingDomain, stagingHostPrefix }
329
+ function parseHost(req) {
330
+ return parseStagingHost(req?.headers?.host, parseHostOpts)
331
+ }
332
+
333
+ function sendError(res, status, message) {
334
+ if (!res || res.headersSent) return
335
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' })
336
+ res.end(message)
337
+ }
338
+
339
+ async function handleRequest(req, res) {
340
+ const parsed = parseHost(req)
341
+ if (!parsed) {
342
+ sendError(res, 404, `Unknown host: ${req?.headers?.host || '(missing Host header)'}`)
343
+ return
344
+ }
345
+ const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
346
+ if (outcome.error) {
347
+ sendError(res, outcome.status || 502, outcome.message || outcome.error)
348
+ return
349
+ }
350
+ proxy.web(req, res, { target: outcome.target })
351
+ }
352
+
353
+ async function handleUpgrade(req, socket, head) {
354
+ const parsed = parseHost(req)
355
+ if (!parsed) {
356
+ try { socket.destroy() } catch { /* noop */ }
357
+ return
358
+ }
359
+ const outcome = await resolveStagingTarget({ req, slug: parsed.slug, resolve })
360
+ if (outcome.error) {
361
+ // Don't write an HTTP response onto an upgrade socket — Vite retries.
362
+ try { socket.destroy() } catch { /* noop */ }
363
+ return
364
+ }
365
+ proxy.ws(req, socket, head, { target: outcome.target })
366
+ }
367
+
368
+ const server = http.createServer((req, res) => {
369
+ handleRequest(req, res).catch((err) => {
370
+ log('request', err, req)
371
+ sendError(res, 502, `Staging gateway error: ${err?.message || err}`)
372
+ })
373
+ })
374
+
375
+ server.on('upgrade', (req, socket, head) => {
376
+ handleUpgrade(req, socket, head).catch((err) => {
377
+ log('upgrade', err, req)
378
+ try { socket.destroy() } catch { /* noop */ }
379
+ })
380
+ })
381
+
382
+ return {
383
+ server,
384
+ proxy,
385
+ close() {
386
+ return new Promise((res) => {
387
+ try { proxy.close(() => {}) } catch { /* noop */ }
388
+ server.close(() => res())
389
+ })
390
+ },
391
+ }
392
+ }
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Mango Vibe Staging Gateway (HAP-1165)
5
+ *
6
+ * Standalone server that fronts the multi-user staging editing surface. nginx
7
+ * proxy_passes the staging hostname (e.g. `staging-vibe.generations.org`) to
8
+ * this process on a loopback port; the gateway demuxes by selected branch
9
+ * (HAP-1164) into the right Vibe workspace's dev-server port, including the
10
+ * Vite HMR WebSocket upgrade.
11
+ *
12
+ * Configuration (env vars):
13
+ * BACKEND_PORT (required, int) Mango port that owns /system/vibe/staging-resolve.
14
+ * LISTEN_PORT (default 7123, int) Loopback port to bind.
15
+ * LISTEN_HOST (default 127.0.0.1) Bind host — keep loopback so nginx fronts it.
16
+ * STAGING_DOMAIN (optional) Exact staging hostname; when set, only that host
17
+ * (and `staging.*` prefix matches) are routed.
18
+ * STAGING_HOST_PREFIX (default "staging.") Prefix fallback for slug demux.
19
+ * RESOLVER_PATH (default /system/vibe/staging-resolve)
20
+ * RESOLVER_TIMEOUT_MS (default 2000, int)
21
+ *
22
+ * Usage (typical droplet pm2):
23
+ * BACKEND_PORT=7122 LISTEN_PORT=7123 \
24
+ * STAGING_DOMAIN=staging-vibe.generations.org \
25
+ * node node_modules/mango-cms/lib/staging-gateway.js
26
+ */
27
+
28
+ import http from 'http'
29
+ import httpProxy from 'http-proxy'
30
+ import { createStagingGatewayServer } from './devProxyGateway.js'
31
+
32
+ function intEnv(name, fallback) {
33
+ const raw = process.env[name]
34
+ if (raw === undefined || raw === '') return fallback
35
+ const n = parseInt(raw, 10)
36
+ if (!Number.isInteger(n) || n <= 0) {
37
+ console.error(`[staging-gateway] ${name} must be a positive integer (got "${raw}")`)
38
+ process.exit(1)
39
+ }
40
+ return n
41
+ }
42
+
43
+ const BACKEND_PORT = intEnv('BACKEND_PORT', null)
44
+ if (BACKEND_PORT === null) {
45
+ console.error('[staging-gateway] BACKEND_PORT (int) is required')
46
+ process.exit(1)
47
+ }
48
+
49
+ const LISTEN_PORT = intEnv('LISTEN_PORT', 7123)
50
+ const LISTEN_HOST = process.env.LISTEN_HOST || '127.0.0.1'
51
+ const STAGING_DOMAIN = process.env.STAGING_DOMAIN || ''
52
+ const STAGING_HOST_PREFIX = process.env.STAGING_HOST_PREFIX || undefined
53
+ const RESOLVER_PATH = process.env.RESOLVER_PATH || undefined
54
+ const RESOLVER_TIMEOUT_MS = intEnv('RESOLVER_TIMEOUT_MS', 2000)
55
+
56
+ const { server, close } = createStagingGatewayServer({
57
+ http,
58
+ httpProxy,
59
+ stagingDomain: STAGING_DOMAIN || undefined,
60
+ stagingHostPrefix: STAGING_HOST_PREFIX,
61
+ backendPort: BACKEND_PORT,
62
+ resolverPath: RESOLVER_PATH,
63
+ timeoutMs: RESOLVER_TIMEOUT_MS,
64
+ onError: (where, err) => {
65
+ console.error(`[staging-gateway] ${where} error: ${err?.message || err}`)
66
+ },
67
+ })
68
+
69
+ server.listen(LISTEN_PORT, LISTEN_HOST, () => {
70
+ const target = STAGING_DOMAIN || `${STAGING_HOST_PREFIX || 'staging.'}*`
71
+ console.log(`[staging-gateway] listening on ${LISTEN_HOST}:${LISTEN_PORT}`)
72
+ console.log(`[staging-gateway] ${target} -> per-branch Vibe workspace (resolved via 127.0.0.1:${BACKEND_PORT})`)
73
+ })
74
+
75
+ function shutdown(signal) {
76
+ console.log(`[staging-gateway] ${signal} — closing server`)
77
+ close().then(() => process.exit(0)).catch(() => process.exit(1))
78
+ setTimeout(() => process.exit(1), 5000).unref()
79
+ }
80
+ process.on('SIGTERM', () => shutdown('SIGTERM'))
81
+ process.on('SIGINT', () => shutdown('SIGINT'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mango-cms",
3
- "version": "0.3.23",
3
+ "version": "0.3.26",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -1 +0,0 @@
1
- [{"name":"members","ssrModel":null,"titleName":"Members","singular":"member","titleSingular":"Member","humanName":" Members","humanSingular":" Member","fields":[{"name":"title","relationship":false,"instanceOf":null,"options":null,"humanName":"title","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":true,"customField":false,"global":true,"fields":null},{"name":"author","relationship":"members","instanceOf":"relationship","options":null,"humanName":"author","type":"Member","inputType":null,"computed":true,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"editId","relationship":false,"instanceOf":null,"options":null,"humanName":"edit Id","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"created","relationship":false,"instanceOf":null,"options":null,"humanName":"created","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"updated","relationship":false,"instanceOf":null,"options":null,"humanName":"updated","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"slug","relationship":false,"instanceOf":null,"options":null,"humanName":"slug","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"startDate","relationship":false,"instanceOf":null,"options":null,"humanName":"start Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"endDate","relationship":false,"instanceOf":null,"options":null,"humanName":"end Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"email","relationship":false,"instanceOf":null,"options":null,"humanName":"email","type":"String","inputType":"String","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"password","relationship":false,"instanceOf":null,"options":null,"humanName":"password","type":"MembersPassword","inputType":"String","computed":false,"complex":true,"translate":true,"required":true,"customField":false,"global":false,"fields":[{"name":"salt","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"salt","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"hash","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"hash","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"apiKey","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"api Key","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"roles","relationship":false,"instanceOf":"select","options":["admin","member","contributor","editor"],"humanName":"roles","type":"[String]","inputType":"[String]","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"firstName","relationship":false,"instanceOf":null,"options":null,"humanName":"first Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"lastName","relationship":false,"instanceOf":null,"options":null,"humanName":"last Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"examples","ssrModel":null,"titleName":"Examples","singular":"example","titleSingular":"Example","humanName":" Examples","humanSingular":" Example","fields":[{"name":"title","relationship":false,"instanceOf":null,"options":null,"humanName":"title","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":true,"customField":false,"global":true,"fields":null},{"name":"author","relationship":"members","instanceOf":"relationship","options":null,"humanName":"author","type":"Member","inputType":null,"computed":true,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"editId","relationship":false,"instanceOf":null,"options":null,"humanName":"edit Id","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"created","relationship":false,"instanceOf":null,"options":null,"humanName":"created","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"updated","relationship":false,"instanceOf":null,"options":null,"humanName":"updated","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"slug","relationship":false,"instanceOf":null,"options":null,"humanName":"slug","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"startDate","relationship":false,"instanceOf":null,"options":null,"humanName":"start Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"endDate","relationship":false,"instanceOf":null,"options":null,"humanName":"end Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"someData","relationship":false,"instanceOf":null,"options":null,"humanName":"some Data","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"anotherField","relationship":false,"instanceOf":null,"options":null,"humanName":"another Field","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"anArrayOfInts","array":true,"relationship":false,"instanceOf":null,"options":null,"humanName":"an Array Of Ints","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"image","relationship":false,"instanceOf":"image","options":null,"humanName":"image","type":"ExamplesImage","inputType":"String","computed":false,"complex":true,"translate":true,"required":false,"customField":false,"global":false,"fields":[{"name":"width","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"width","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"height","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"height","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"url","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"url","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"exampleRelationship","relationship":"examples","instanceOf":"relationship","options":null,"humanName":"example Relationship","type":"[Example]","inputType":"[String]","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"someLegitData","relationship":false,"instanceOf":null,"options":null,"humanName":"some Legit Data","type":"ExamplesSomeLegitData","inputType":"CreateExamplesSomeLegitDataInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"coordinates","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"coordinates","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"productOfXY","relationship":false,"instanceOf":null,"options":null,"humanName":"product Of X Y","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"vimeo","relationship":false,"instanceOf":null,"options":null,"humanName":"vimeo","type":"ExamplesVimeo","inputType":"Int","computed":false,"complex":true,"translate":true,"required":false,"customField":false,"global":false,"fields":[{"name":"id","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"id","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"url","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"url","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]},{"name":"items","ssrModel":null,"titleName":"Items","singular":"item","titleSingular":"Item","humanName":" Items","humanSingular":" Item","library":"Mango-stand","fields":[{"name":"title","relationship":false,"instanceOf":null,"options":null,"humanName":"title","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":true,"customField":false,"global":true,"fields":null},{"name":"author","relationship":"members","instanceOf":"relationship","options":null,"humanName":"author","type":"Member","inputType":null,"computed":true,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"editId","relationship":false,"instanceOf":null,"options":null,"humanName":"edit Id","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"created","relationship":false,"instanceOf":null,"options":null,"humanName":"created","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"updated","relationship":false,"instanceOf":null,"options":null,"humanName":"updated","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"slug","relationship":false,"instanceOf":null,"options":null,"humanName":"slug","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"startDate","relationship":false,"instanceOf":null,"options":null,"humanName":"start Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"endDate","relationship":false,"instanceOf":null,"options":null,"humanName":"end Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"price","relationship":false,"instanceOf":null,"options":null,"humanName":"price","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"stock","relationship":false,"instanceOf":null,"options":null,"humanName":"stock","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"description","relationship":false,"instanceOf":null,"options":null,"humanName":"description","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"carts","ssrModel":null,"titleName":"Carts","singular":"cart","titleSingular":"Cart","humanName":" Carts","humanSingular":" Cart","library":"Mango-stand","fields":[{"name":"title","relationship":false,"instanceOf":null,"options":null,"humanName":"title","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"author","relationship":"members","instanceOf":"relationship","options":null,"humanName":"author","type":"Member","inputType":null,"computed":true,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"editId","relationship":false,"instanceOf":null,"options":null,"humanName":"edit Id","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"created","relationship":false,"instanceOf":null,"options":null,"humanName":"created","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"updated","relationship":false,"instanceOf":null,"options":null,"humanName":"updated","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"slug","relationship":false,"instanceOf":null,"options":null,"humanName":"slug","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"startDate","relationship":false,"instanceOf":null,"options":null,"humanName":"start Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"endDate","relationship":false,"instanceOf":null,"options":null,"humanName":"end Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"transactionId","relationship":false,"instanceOf":null,"options":null,"humanName":"transaction Id","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"finalized","relationship":false,"instanceOf":null,"options":null,"humanName":"finalized","type":"Boolean","inputType":"Boolean","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"completed","relationship":false,"instanceOf":null,"options":null,"humanName":"completed","type":"Boolean","inputType":"Boolean","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"completedDate","relationship":false,"instanceOf":null,"options":null,"humanName":"completed Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"cartItems","array":true,"relationship":false,"instanceOf":null,"options":null,"humanName":"cart Items","type":"CartsCartItems","inputType":"CreateCartsCartItemsInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"quantity","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"quantity","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"discount","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"discount","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"item","array":false,"relationship":"items","instanceOf":"relationship","options":null,"humanName":"item","type":"Item","inputType":"String","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"coupons","relationship":"discounts","instanceOf":"relationship","options":null,"humanName":"coupons","type":"[Discount]","inputType":"[String]","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"customerId","relationship":false,"instanceOf":null,"options":null,"humanName":"customer Id","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"billing","relationship":false,"instanceOf":null,"options":null,"humanName":"billing","type":"CartsBilling","inputType":"CreateCartsBillingInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"paymentId","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"payment Id","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"firstName","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"first Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"lastName","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"last Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"phoneNumber","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"phone Number","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"address","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"address","type":"CartsBillingAddress","inputType":"CreateCartsBillingAddressInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"venue","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"venue","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"address","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"address","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"city","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"city","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"state","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"state","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"zip","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"zip","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"country","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"country","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"human","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"human","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"coordinates","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"coordinates","type":"CartsBillingAddressCoordinates","inputType":null,"computed":true,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"lat","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"lat","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"lng","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"lng","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]},{"name":"payment","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"payment","type":"CartsBillingPayment","inputType":"CreateCartsBillingPaymentInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"creditCard","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"credit Card","type":"CartsBillingPaymentCreditCard","inputType":"CreateCartsBillingPaymentCreditCardInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"cardNumber","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"card Number","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"cardType","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"card Type","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"expirationDate","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"expiration Date","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"bankAccount","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"bank Account","type":"CartsBillingPaymentBankAccount","inputType":"CreateCartsBillingPaymentBankAccountInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"accountNumber","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"account Number","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"accountType","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"account Type","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"echeckType","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"echeck Type","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"nameOnAccount","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"name On Account","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"routingNumber","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"routing Number","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]}]},{"name":"shipping","relationship":false,"instanceOf":null,"options":null,"humanName":"shipping","type":"CartsShipping","inputType":"CreateCartsShippingInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"method","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"method","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"price","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"price","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"addressId","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"address Id","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"firstName","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"first Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"lastName","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"last Name","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"phoneNumber","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"phone Number","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"address","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"address","type":"CartsShippingAddress","inputType":"CreateCartsShippingAddressInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"venue","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"venue","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"address","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"address","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"city","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"city","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"state","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"state","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"zip","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"zip","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"country","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"country","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"human","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"human","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"coordinates","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"coordinates","type":"CartsShippingAddressCoordinates","inputType":null,"computed":true,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"lat","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"lat","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"lng","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"lng","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]},{"name":"free","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"free","type":"Boolean","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"price","relationship":false,"instanceOf":null,"options":null,"humanName":"price","type":"CartsPrice","inputType":null,"computed":true,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"subtotal","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"subtotal","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"discount","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"discount","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"total","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"total","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]},{"name":"discounts","ssrModel":null,"titleName":"Discounts","singular":"discount","titleSingular":"Discount","humanName":" Discounts","humanSingular":" Discount","library":"Mango-stand","fields":[{"name":"title","relationship":false,"instanceOf":null,"options":null,"humanName":"title","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":true,"customField":false,"global":true,"fields":null},{"name":"author","relationship":"members","instanceOf":"relationship","options":null,"humanName":"author","type":"Member","inputType":null,"computed":true,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"editId","relationship":false,"instanceOf":null,"options":null,"humanName":"edit Id","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"created","relationship":false,"instanceOf":null,"options":null,"humanName":"created","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"updated","relationship":false,"instanceOf":null,"options":null,"humanName":"updated","type":"Float","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"slug","relationship":false,"instanceOf":null,"options":null,"humanName":"slug","type":"String","inputType":null,"computed":true,"complex":false,"translate":false,"required":false,"customField":false,"global":true,"fields":null},{"name":"startDate","relationship":false,"instanceOf":null,"options":null,"humanName":"start Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"endDate","relationship":false,"instanceOf":null,"options":null,"humanName":"end Date","type":"DateTime","inputType":"DateTime","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":true,"fields":null},{"name":"type","relationship":false,"instanceOf":"select","options":["coupon","sale"],"humanName":"type","type":"String","inputType":"String","computed":false,"complex":false,"translate":true,"required":true,"customField":false,"global":false,"fields":null},{"name":"code","relationship":false,"instanceOf":null,"options":null,"humanName":"code","type":"String","inputType":"String","computed":false,"complex":false,"translate":true,"required":true,"customField":false,"global":false,"fields":null},{"name":"limitations","relationship":false,"instanceOf":null,"options":null,"humanName":"limitations","type":"DiscountsLimitations","inputType":"CreateDiscountsLimitationsInput","computed":false,"complex":true,"translate":false,"required":false,"customField":true,"global":false,"fields":[{"name":"items","array":false,"relationship":"items","instanceOf":"relationship","options":null,"humanName":"items","type":"[Item]","inputType":"[String]","computed":false,"complex":false,"translate":true,"required":false,"customField":false,"global":false,"fields":null},{"name":"perCustomer","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"per Customer","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"totalUses","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"total Uses","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"perOrder","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"per Order","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"quantity","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"quantity","type":"Int","inputType":"Int","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]},{"name":"freeShipping","relationship":false,"instanceOf":null,"options":null,"humanName":"free Shipping","type":"Boolean","inputType":"Boolean","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"discount","relationship":false,"instanceOf":null,"options":null,"humanName":"discount","type":"DiscountsDiscount","inputType":"String","computed":false,"complex":true,"translate":true,"required":false,"customField":false,"example":"$12.50 or 50% etc.","global":false,"fields":[{"name":"type","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"type","type":"String","inputType":"String","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null},{"name":"value","array":false,"relationship":false,"instanceOf":null,"options":null,"humanName":"value","type":"Float","inputType":"Float","computed":false,"complex":false,"translate":false,"required":false,"customField":false,"global":false,"fields":null}]}]}]
@@ -1,14 +0,0 @@
1
- {
2
- "account/login": [
3
- "post"
4
- ],
5
- "account/resetPassword": [
6
- "post"
7
- ],
8
- "account/sendResetInstructions": [
9
- "post"
10
- ],
11
- "test": [
12
- "get"
13
- ]
14
- }