backend-manager 5.8.0 → 5.8.2

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/CHANGELOG.md CHANGED
@@ -14,6 +14,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
14
14
  - `Fixed` for any bug fixes.
15
15
  - `Security` in case of vulnerabilities.
16
16
 
17
+ # [5.8.2] - 2026-06-19
18
+
19
+ ### Fixed
20
+ - **Setup crash on fresh projects.** `npx mgr setup` crashed with `Cannot read properties of undefined (reading 'default')` when `.firebaserc`, `firebase.json`, or `backend-manager-config.json` didn't exist. Setup now scaffolds all three from templates before reading from them.
21
+ - **`engines.node` hard crash.** Missing `engines.node` in `package.json` threw an unrecoverable error. Now auto-fixes it using the current Node.js major version.
22
+ - **`dependencies['backend-manager']` crash.** The local-BEM detection crashed when `backend-manager` was listed in `devDependencies` instead of `dependencies`.
23
+
24
+ ### Changed
25
+ - **Unified scaffold pass.** Consolidated config scaffolding, `engines.node` fix, and doc defaults into one `[DEFAULTS]` section with a single file reload afterwards. Eliminates scattered scaffolding steps and conditional re-loads.
26
+
27
+ ### Added
28
+ - **`templates/firebase.json`** — standard Firebase config template (hosting, functions, firestore, database, storage, emulators) scaffolded into new projects by `npx mgr setup`.
29
+
30
+ # [5.8.1] - 2026-06-19
31
+
32
+ ### Fixed
33
+ - **Ghostii 502 timeout fix.** Switched `writeArticle()` from the Firebase Hosting URL (`api.ghostii.ai`) to the raw Cloud Functions URL. Firebase Hosting rewrites have a hard 60-second proxy timeout that caused 502 errors when the AI article pipeline exceeded that limit. The raw URL uses the function's own 5-minute timeout.
34
+
35
+ ### Added
36
+ - **Ghostii call duration logging.** `writeArticle()` now logs the round-trip time of each Ghostii API call (`[ghostii] writeArticle() completed in Xms`).
37
+
17
38
  # [5.8.0] - 2026-06-19
18
39
 
19
40
  ### Added
package/CLAUDE.md CHANGED
@@ -18,7 +18,7 @@ Backend Manager (BEM) is a comprehensive framework for building modern Firebase
18
18
  ### For Consuming Projects
19
19
 
20
20
  1. `npm install backend-manager --save-dev` (inside `functions/`)
21
- 2. `npx mgr setup` — validates config, scaffolds defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provisions Firestore indexes
21
+ 2. `npx mgr setup` — bootstraps a new project (scaffolds `.firebaserc`, `firebase.json`, `backend-manager-config.json`, `engines.node`, CLAUDE.md, CHANGELOG.md, docs/, test/), validates config, provisions Firestore indexes
22
22
  3. `npx mgr emulator` — start Firebase emulators (auth/firestore/functions/database/storage)
23
23
  4. `npx mgr serve` — local serve with Stripe webhook forwarding (if `STRIPE_SECRET_KEY` is set)
24
24
  5. `npx mgr test` — runs framework + project test suites against an emulator. Positional target(s) select which test FILES run, by source + path (multiple space-separated targets compose):
@@ -64,7 +64,7 @@ Every feature ships with tests at EVERY surface it exposes — logic (`test/rout
64
64
 
65
65
  | Command | Description |
66
66
  |---|---|
67
- | `setup` | Validate config, scaffold defaults (CLAUDE.md, CHANGELOG.md, docs/, test/), provision Firestore indexes |
67
+ | `setup` | Bootstrap new projects (scaffolds config files + doc defaults), validate config, provision Firestore indexes |
68
68
  | `emulator` | Start Firebase emulators (auth/firestore/functions/database/storage) |
69
69
  | `serve` | Local Firebase serve (with auto Stripe webhook forwarding if keys set) |
70
70
  | `watch` | Auto-reload functions on file change |
package/PROGRESS.md CHANGED
@@ -2,13 +2,23 @@
2
2
  > Agents and maintainers should update this file regularly to reflect the current state of the project.
3
3
 
4
4
  ## 🎯 Current Focus
5
- * **Goal:** RSS/Atom feed-based article generation for the Ghostii system
6
- * **Current Phase:** Implementation complete, pending deploy
5
+ * **Goal:** Fix setup crash on fresh projects missing config files
6
+ * **Current Phase:** Phase 6 — in progress (code done, needs docs + publish)
7
7
  * **Priority:** High
8
- * **Last Updated:** 2026-06-18 6:45 PM PDT
9
- * **Notes:** All code + tests complete. Found and fixed humanizer bug — `injectBurstiness` was destroying markdown links by splitting on commas inside `[text](url)` syntax. Fixed with protect-and-restore pattern. Also fixed 403 links treated as "working" in verification step. Ghostii backend needs deploy.
8
+ * **Last Updated:** 2026-06-18 11:30 PM PDT
9
+ * **Notes:** Setup now fully bootstraps a bare project (just package.json + .nvmrc). Scaffolds configs, engines.node, everything stops cleanly at service-account with a Firebase console link. Still needs docs + publish.
10
10
 
11
11
  ## 📌 Active Task List
12
+ * [ ] Phase 6: Setup scaffolds essential configs for fresh projects
13
+ * [x] Task 6.1: Add `templates/firebase.json` standard template
14
+ * [x] Task 6.2: Add `scaffoldConfigs()` + `resolveProjectId()` to setup.js (runs before config resolution)
15
+ * [x] Task 6.3: Auto-fix `engines.node` instead of throwing
16
+ * [x] Task 6.4: Fix `dependencies['backend-manager']` crash when BEM is in devDependencies
17
+ * [x] Task 6.5: Verify fix on truly bare project (dailyembers-backend — 13/14 pass, only service-account expected)
18
+ * [x] Task 6.6: Optimize — consolidate scattered scaffolding into one `[DEFAULTS]` pass with `loadFiles()` DRY extraction
19
+ * [x] Task 6.7: Regression test on existing project (ultimate-jekyll-backend — all pass)
20
+ * [x] Task 6.8: Update docs (CLAUDE.md, CHANGELOG.md)
21
+ * [ ] Task 6.9: Publish new BEM version
12
22
  * [ ] Phase 5: Ghostii feed-based article system
13
23
  * [x] Task 5.1: Create `feed-parser.js` (RSS 2.0, Atom 1.0, JSON Feed parser + article extractor)
14
24
  * [x] Task 5.2: Add `fast-xml-parser` dependency to BEM
@@ -28,7 +38,8 @@
28
38
  * [x] Task 5.16: Fix humanizer stripping links — `injectBurstiness` was breaking `[text](url)` syntax; added protect-and-restore pattern + tests
29
39
  * [x] Task 5.17: Fix 403 links treated as "working" in link verification step
30
40
  * [x] Task 5.18: Add detective-level `[LINKS]` diagnostic logging to article pipeline
31
- * [ ] Task 5.19: Deploy Ghostii backend
41
+ * [x] Task 5.19: Ship BEM v5.8.0 to npm
42
+ * [x] Task 5.20: Ship Ghostii-backend v1.0.5 + deploy to Firebase
32
43
  * [ ] Task 5.17: Publish BEM with feed support
33
44
  * [ ] Task 5.12: Publish BEM with feed support
34
45
  * [ ] Task 5.13: Configure consumer project(s) with `$feed:` sources
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.8.0",
3
+ "version": "5.8.2",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -70,14 +70,8 @@ class SetupCommand extends BaseCommand {
70
70
  // prints it on a hard failure; we print it here on success).
71
71
  self.setupSummary = new ui.Summary().start();
72
72
 
73
- // Load files
74
- self.package = loadJSON(`${self.firebaseProjectPath}/functions/package.json`);
75
- self.firebaseJSON = loadJSON(`${self.firebaseProjectPath}/firebase.json`);
76
- self.firebaseRC = loadJSON(`${self.firebaseProjectPath}/.firebaserc`);
77
- self.remoteconfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/remoteconfig.template.json`);
78
- self.projectPackage = loadJSON(`${self.firebaseProjectPath}/package.json`);
79
- self.bemConfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/backend-manager-config.json`);
80
- self.gitignore = jetpack.read(`${self.firebaseProjectPath}/.gitignore`) || '';
73
+ // Initial load — returns {} for missing files so scaffold checks can run.
74
+ this.loadFiles();
81
75
 
82
76
  // Check if package exists
83
77
  if (!hasContent(self.package)) {
@@ -93,12 +87,23 @@ class SetupCommand extends BaseCommand {
93
87
  process.exit(1);
94
88
  }
95
89
 
96
- // Load the rules files
97
- this.getRulesFile();
90
+ // One unified scaffold pass: config files, package.json fixes, doc defaults.
91
+ // Everything that creates/fixes files goes here, BEFORE any code reads from
92
+ // them. One reload afterwards picks up the final state.
93
+ ui.section('Defaults');
94
+ this.scaffoldConfigs();
95
+ this.scaffoldPackageJson();
96
+ this.copyDefaults();
97
+ this.loadFiles();
98
+
99
+ // Clean up leftover trigger files + stale log files from older BEM versions
100
+ this.cleanupGeneratedArtifacts();
98
101
 
102
+ // Load the rules files (reads from BEM's own templates/, not consumer files)
103
+ this.getRulesFile();
99
104
  self.default.rulesVersionRegex = new RegExp(`///---version=${self.default.version}---///`);
100
105
 
101
- // Set project info
106
+ // Resolve project info — safe now, scaffoldConfigs guarantees these exist.
102
107
  self.projectId = self.firebaseRC.projects.default;
103
108
  self.projectUrl = `https://console.firebase.google.com/project/${self.projectId}`;
104
109
  self.apiUrl = `https://api.${(self.bemConfigJSON.brand?.url || '').replace(/^https?:\/\//, '')}`;
@@ -110,24 +115,15 @@ class SetupCommand extends BaseCommand {
110
115
  ui.field('Project', self.projectId, { pad: 9 });
111
116
  ui.field('API', self.apiUrl, { pad: 9, valueColor: chalk.cyan });
112
117
 
113
- if (!self.package || !self.package.engines || !self.package.engines.node) {
114
- throw new Error('Missing <engines.node> in package.json');
115
- }
116
-
117
- // Clean up leftover trigger files + stale log files from older BEM versions
118
- this.cleanupGeneratedArtifacts();
119
-
120
- // Copy / merge defaults into consumer project root (matches EM/BXM/UJM pattern).
121
- // Runs BEFORE tests so any test that inspects scaffolded files sees the merged state.
122
- ui.section('Defaults');
123
- this.copyDefaults();
124
-
125
118
  // Run all tests
126
119
  ui.section('Checks');
127
120
  await this.runTests();
128
121
 
129
122
  // Warn if using local backend-manager
130
- if (self.package.dependencies['backend-manager'].includes('file:')) {
123
+ const bemDep = self.package.dependencies?.['backend-manager']
124
+ || self.package.devDependencies?.['backend-manager']
125
+ || '';
126
+ if (bemDep.includes('file:')) {
131
127
  ui.section('Notices');
132
128
  ui.status('warn', `Using the local ${chalk.bold('backend-manager')} source (file: dependency)`, { level: 2 });
133
129
  }
@@ -245,6 +241,84 @@ class SetupCommand extends BaseCommand {
245
241
  }
246
242
  }
247
243
 
244
+ loadFiles() {
245
+ const self = this.main;
246
+ self.package = loadJSON(`${self.firebaseProjectPath}/functions/package.json`);
247
+ self.firebaseJSON = loadJSON(`${self.firebaseProjectPath}/firebase.json`);
248
+ self.firebaseRC = loadJSON(`${self.firebaseProjectPath}/.firebaserc`);
249
+ self.remoteconfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/remoteconfig.template.json`);
250
+ self.projectPackage = loadJSON(`${self.firebaseProjectPath}/package.json`);
251
+ self.bemConfigJSON = loadJSON(`${self.firebaseProjectPath}/functions/backend-manager-config.json`);
252
+ self.gitignore = jetpack.read(`${self.firebaseProjectPath}/.gitignore`) || '';
253
+ }
254
+
255
+ scaffoldPackageJson() {
256
+ const self = this.main;
257
+ const ui = this.ui;
258
+
259
+ if (!self.package.engines || !self.package.engines.node) {
260
+ const nodeVer = String(parseInt(process.versions.node, 10));
261
+ self.package.engines = self.package.engines || {};
262
+ self.package.engines.node = nodeVer;
263
+ jetpack.write(`${self.firebaseProjectPath}/functions/package.json`, JSON.stringify(self.package, null, 2));
264
+ ui.status('add', `Added ${chalk.cyan('engines.node')} = ${chalk.bold(nodeVer)} to package.json`, { level: 2 });
265
+ }
266
+ }
267
+
268
+ scaffoldConfigs() {
269
+ const self = this.main;
270
+ const ui = this.ui;
271
+ const templatesDir = path.resolve(`${__dirname}/../../../templates`);
272
+ let touched = 0;
273
+
274
+ // .firebaserc — resolve project ID from service account or env
275
+ const firebasercPath = `${self.firebaseProjectPath}/.firebaserc`;
276
+ if (!hasContent(self.firebaseRC)) {
277
+ const projectId = this.resolveProjectId();
278
+ jetpack.write(firebasercPath, JSON.stringify({ projects: { default: projectId } }, null, 2) + '\n');
279
+ ui.status('add', `Created ${chalk.cyan('.firebaserc')} (project: ${chalk.bold(projectId)})`, { level: 2 });
280
+ touched++;
281
+ }
282
+
283
+ // firebase.json
284
+ const firebaseJsonPath = `${self.firebaseProjectPath}/firebase.json`;
285
+ if (!hasContent(self.firebaseJSON)) {
286
+ const templatePath = path.join(templatesDir, 'firebase.json');
287
+ jetpack.copy(templatePath, firebaseJsonPath);
288
+ ui.status('add', `Created ${chalk.cyan('firebase.json')}`, { level: 2 });
289
+ touched++;
290
+ }
291
+
292
+ // backend-manager-config.json
293
+ const bemConfigPath = `${self.firebaseProjectPath}/functions/backend-manager-config.json`;
294
+ if (!hasContent(self.bemConfigJSON)) {
295
+ const templatePath = path.join(templatesDir, 'backend-manager-config.json');
296
+ jetpack.copy(templatePath, bemConfigPath);
297
+ ui.status('add', `Created ${chalk.cyan('functions/backend-manager-config.json')}`, { level: 2 });
298
+ touched++;
299
+ }
300
+
301
+ return touched;
302
+ }
303
+
304
+ resolveProjectId() {
305
+ const self = this.main;
306
+ const saPath = `${self.firebaseProjectPath}/functions/service-account.json`;
307
+
308
+ if (jetpack.exists(saPath)) {
309
+ try {
310
+ const sa = JSON.parse(jetpack.read(saPath));
311
+ if (sa.project_id) {
312
+ return sa.project_id;
313
+ }
314
+ } catch (e) {
315
+ // Fall through
316
+ }
317
+ }
318
+
319
+ return process.env.GCLOUD_PROJECT || 'demo-project';
320
+ }
321
+
248
322
  cleanupGeneratedArtifacts() {
249
323
  const self = this.main;
250
324
 
@@ -37,7 +37,7 @@ const powertools = require('node-powertools');
37
37
  * where `json` is the structured block array ([{ name, content }]) that
38
38
  * blocksToPost() consumes to build BEM's post shape.
39
39
  */
40
- function writeArticle({ brand, description, links, sourceContent, overrides }) {
40
+ async function writeArticle({ brand, description, links, sourceContent, overrides }) {
41
41
  const o = overrides || {};
42
42
 
43
43
  const body = {
@@ -60,13 +60,24 @@ function writeArticle({ brand, description, links, sourceContent, overrides }) {
60
60
  body.sourceContent = sourceContent;
61
61
  }
62
62
 
63
- return fetch('https://api.ghostii.ai/write/article', {
63
+ const start = Date.now();
64
+
65
+ // Original URL: https://api.ghostii.ai/write/article
66
+ // Firebase Hosting rewrites have a hard 60s proxy timeout that causes 502s
67
+ // on slow AI pipelines. The raw Cloud Functions URL uses the function's own
68
+ // 5-minute timeout instead.
69
+ const result = await fetch('https://us-central1-ghostii.cloudfunctions.net/writeGlobal/write/article', {
64
70
  method: 'post',
65
71
  timeout: 180000,
66
72
  tries: 1,
67
73
  response: 'json',
68
74
  body: body,
69
75
  });
76
+
77
+ const duration = Date.now() - start;
78
+ console.log(`[ghostii] writeArticle() completed in ${duration}ms (${(duration / 1000).toFixed(1)}s)`);
79
+
80
+ return result;
70
81
  }
71
82
 
72
83
  /**
@@ -237,6 +237,7 @@
237
237
  "4warding.org",
238
238
  "50set.ru",
239
239
  "51baidu.tech",
240
+ "521883.xyz",
240
241
  "52mails.com",
241
242
  "55hosting.net",
242
243
  "5ghgfhfghfgh.tk",
@@ -252,6 +253,7 @@
252
253
  "5y.loseyourip.com",
253
254
  "5ymail.com",
254
255
  "60minutemail.com",
256
+ "628868.xyz",
255
257
  "66-77.cc",
256
258
  "668fmail.com",
257
259
  "672643.net",
@@ -508,6 +510,7 @@
508
510
  "alienware13.com",
509
511
  "aligamel.com",
510
512
  "alightmotion.cloud",
513
+ "alightmotion.id",
511
514
  "aligroup.uk",
512
515
  "alilot.com",
513
516
  "alin.cc.cd",
@@ -568,6 +571,7 @@
568
571
  "amail.com",
569
572
  "amail1.com",
570
573
  "amail4.me",
574
+ "amazinggift.life",
571
575
  "amazingly.online",
572
576
  "amazon-aws.org",
573
577
  "amberwe.us",
@@ -1399,6 +1403,7 @@
1399
1403
  "code-gmail.com",
1400
1404
  "codeandscotch.com",
1401
1405
  "codeguard.net",
1406
+ "codei.site",
1402
1407
  "coderdir.com",
1403
1408
  "codestar.site",
1404
1409
  "codesuc.top",
@@ -1483,6 +1488,7 @@
1483
1488
  "crossmailjet.com",
1484
1489
  "crossroadsmail.com",
1485
1490
  "crunchcompass.com",
1491
+ "cruncoau.asia",
1486
1492
  "crusthost.com",
1487
1493
  "cryptoavalonsolhub.cloud",
1488
1494
  "cryptogmail.com",
@@ -3441,6 +3447,7 @@
3441
3447
  "jav8.de",
3442
3448
  "javadmin.com",
3443
3449
  "javaemail.com",
3450
+ "javaka.live",
3444
3451
  "javindra.cfd",
3445
3452
  "jbsze.com",
3446
3453
  "jbsze.ne",
@@ -3539,6 +3546,7 @@
3539
3546
  "kagi.be",
3540
3547
  "kakadua.net",
3541
3548
  "kakao-mail.com",
3549
+ "kakaomail.kr",
3542
3550
  "kakator.com",
3543
3551
  "kakaveo3.io.vn",
3544
3552
  "kakslsie.store",
@@ -4042,6 +4050,7 @@
4042
4050
  "maildrop.ml",
4043
4051
  "maildu.de",
4044
4052
  "maildx.com",
4053
+ "maildy.site",
4045
4054
  "maileater.com",
4046
4055
  "mailed.in",
4047
4056
  "mailed.ro",
@@ -5031,6 +5040,8 @@
5031
5040
  "otpku.com",
5032
5041
  "ouchcloud.com",
5033
5042
  "ouk.me",
5043
+ "oupul8b1k.bond",
5044
+ "oupul8b1k.cyou",
5034
5045
  "ourklips.com",
5035
5046
  "ourl.me",
5036
5047
  "ourpreviewdomain.com",
@@ -5045,6 +5056,8 @@
5045
5056
  "owleyes.ch",
5046
5057
  "owlpic.com",
5047
5058
  "ownsyou.de",
5059
+ "owo-mailteam.bond",
5060
+ "owo-mailteam.cyou",
5048
5061
  "owube.com",
5049
5062
  "oxfo.edu.pl",
5050
5063
  "oxmail.homes",
@@ -5638,12 +5651,14 @@
5638
5651
  "satisfyme.club",
5639
5652
  "satukosong.com",
5640
5653
  "saturrshop.uk",
5654
+ "saungadaid.pro",
5641
5655
  "sausen.com",
5642
5656
  "savests.com",
5643
5657
  "say0.com",
5644
5658
  "saynotospams.com",
5645
5659
  "sayverfox.com",
5646
5660
  "scatmail.com",
5661
+ "scatterteam.com",
5647
5662
  "scay.net",
5648
5663
  "schachrol.com",
5649
5664
  "schafmail.de",
@@ -5764,6 +5779,7 @@
5764
5779
  "shuelder.com",
5765
5780
  "shut.name",
5766
5781
  "shut.ws",
5782
+ "shxwonew.xyz",
5767
5783
  "siap-sepuh.com",
5768
5784
  "siberpay.com",
5769
5785
  "sicmg.com",
@@ -7310,8 +7326,12 @@
7310
7326
  "yopmail.fr",
7311
7327
  "yopmail.gq",
7312
7328
  "yopmail.kro.kr",
7329
+ "yopmail.loseyourip.com",
7313
7330
  "yopmail.net",
7331
+ "yopmail.ooguy.com",
7314
7332
  "yopmail.pp.ua",
7333
+ "yopmail.theworkpc.com",
7334
+ "yopmail.webredirect.org",
7315
7335
  "yoptruc.fr.nf",
7316
7336
  "yordanmail.cf",
7317
7337
  "yorushika.one",
@@ -7345,6 +7365,7 @@
7345
7365
  "ytpayy.com",
7346
7366
  "yugasandrika.com",
7347
7367
  "yui.it",
7368
+ "yujiehanjiao.cc",
7348
7369
  "yuki.ren",
7349
7370
  "yun.pics",
7350
7371
  "yuoia.com",
@@ -7468,6 +7489,7 @@
7468
7489
  "zzzzzzzzzzzzzz-2092.dynv6.net",
7469
7490
  "zzzzzzzzzzzzzz-277.dynv6.net",
7470
7491
  "zzzzzzzzzzzzzz-3294.dynv6.net",
7492
+ "zzzzzzzzzzzzzz-3330.dynv6.net",
7471
7493
  "zzzzzzzzzzzzzz-3535.dynv6.net",
7472
7494
  "zzzzzzzzzzzzzz-3928.dynv6.net",
7473
7495
  "zzzzzzzzzzzzzz-3940.dynv6.net",
@@ -0,0 +1,66 @@
1
+ {
2
+ "hosting": {
3
+ "public": "public",
4
+ "ignore": [
5
+ "firebase.json",
6
+ "**/.*",
7
+ "**/node_modules/**"
8
+ ],
9
+ "rewrites": [
10
+ {
11
+ "source": "{/backend-manager,/backend-manager/**,/mcp,/mcp/**,/.well-known/oauth-protected-resource,/.well-known/oauth-authorization-server,/authorize,/token,/register}",
12
+ "function": "bm_api"
13
+ }
14
+ ]
15
+ },
16
+ "functions": [
17
+ {
18
+ "source": "functions",
19
+ "codebase": "default",
20
+ "ignore": [
21
+ "node_modules",
22
+ ".git",
23
+ "firebase-debug.log",
24
+ "firebase-debug.*.log"
25
+ ]
26
+ }
27
+ ],
28
+ "firestore": {
29
+ "rules": "firestore.rules",
30
+ "indexes": "firestore.indexes.json"
31
+ },
32
+ "database": {
33
+ "rules": "database.rules.json"
34
+ },
35
+ "storage": {
36
+ "rules": "storage.rules"
37
+ },
38
+ "emulators": {
39
+ "auth": {
40
+ "port": 9099
41
+ },
42
+ "functions": {
43
+ "port": 5001
44
+ },
45
+ "firestore": {
46
+ "port": 8080
47
+ },
48
+ "database": {
49
+ "port": 9000
50
+ },
51
+ "hosting": {
52
+ "port": 5002
53
+ },
54
+ "storage": {
55
+ "port": 9199
56
+ },
57
+ "ui": {
58
+ "enabled": true,
59
+ "port": 4050
60
+ },
61
+ "pubsub": {
62
+ "port": 8085
63
+ },
64
+ "singleProjectMode": true
65
+ }
66
+ }