backend-manager 5.8.1 → 5.8.3

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,28 @@ 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.3] - 2026-06-19
18
+
19
+ ### Fixed
20
+ - **Ghostii feed tracking timestamps.** `trackFeedItem` now uses BEM's standard `metadata.{created,updated}` with `timestamp` (ISO) and `timestampUNIX` (seconds) instead of `FieldValue.serverTimestamp()`, which failed in the emulator context.
21
+
22
+ ### Changed
23
+ - **Firestore collection rename.** `ghostii-feed-items` → `ghostii-sources` for clarity and consistency with the OMEGA naming convention.
24
+ - **Extended feed test coverage.** Added 5 marketing/social feeds (Social Media Examiner, Hootsuite, Sprout Social, Buffer, Digiday) to the real-feed integration test. Threshold bumped from 2/3 to 5/7.
25
+
26
+ # [5.8.2] - 2026-06-19
27
+
28
+ ### Fixed
29
+ - **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.
30
+ - **`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.
31
+ - **`dependencies['backend-manager']` crash.** The local-BEM detection crashed when `backend-manager` was listed in `devDependencies` instead of `dependencies`.
32
+
33
+ ### Changed
34
+ - **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.
35
+
36
+ ### Added
37
+ - **`templates/firebase.json`** — standard Firebase config template (hosting, functions, firestore, database, storage, emulators) scaffolded into new projects by `npx mgr setup`.
38
+
17
39
  # [5.8.1] - 2026-06-19
18
40
 
19
41
  ### Fixed
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:** Shipped
7
- * **Priority:** Complete
8
- * **Last Updated:** 2026-06-18 7:30 PM PDT
9
- * **Notes:** BEM v5.8.0 published to npm. Ghostii-backend v1.0.5 deployed to Firebase. All code, tests, and docs shipped.
5
+ * **Goal:** Ghostii feed sources cleanup + Somiibo consumer config
6
+ * **Current Phase:** Fixes applied + tested, pending commit + publish
7
+ * **Priority:** High
8
+ * **Last Updated:** 2026-06-19 3:05 AM PDT
9
+ * **Notes:** Renamed Firestore collection `ghostii-feed-items` `ghostii-sources`. Fixed `FieldValue.serverTimestamp()` → BEM metadata pattern. Added 5 marketing/social feeds to extended tests. All tests passing (20/20 standard, 9/9 extended). Somiibo consumer has feeds configured + `ghostii-sources.md` evaluation doc. Needs BEM publish + consumer deploy.
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
package/docs/ghostii.md CHANGED
@@ -46,27 +46,30 @@ Each `ghostii[]` entry has a `sources` array. The cron picks one at random per a
46
46
  ### Feed source flow
47
47
 
48
48
  1. Fetch and parse the RSS/Atom/JSON feed via `feed-parser.parseFeed()`
49
- 2. Query `ghostii-feed-items` in Firestore for already-processed items
49
+ 2. Query `ghostii-sources` in Firestore for already-processed items
50
50
  3. Filter to unprocessed items, pick the newest
51
51
  4. Extract full article content from the item URL via `feed-parser.extractArticleContent()`
52
52
  5. Pass extracted text as `sourceContent` to the Ghostii API (separate from the `description` prompt)
53
- 6. After publish, write a tracking doc to `ghostii-feed-items/{hash}`
53
+ 6. After publish, write a tracking doc to `ghostii-sources/{hash}`
54
54
  7. On failure (feed unreachable, unparseable, exhausted): fall back to `$app` behavior
55
55
 
56
56
  ### Feed item tracking
57
57
 
58
- Collection: `ghostii-feed-items` (consumer project Firestore)
58
+ Collection: `ghostii-sources` (consumer project Firestore)
59
59
 
60
60
  ```js
61
- ghostii-feed-items/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
61
+ ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
62
62
  feedUrl: 'https://...',
63
63
  itemId: 'guid-or-url',
64
64
  itemUrl: 'https://...',
65
65
  itemTitle: '...',
66
- processedAt: Timestamp,
67
66
  brandId: '...',
68
67
  postUrl: '...',
69
68
  postSlug: '...',
69
+ metadata: {
70
+ created: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
71
+ updated: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
72
+ },
70
73
  metadata: { created: Timestamp },
71
74
  }
72
75
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.8.1",
3
+ "version": "5.8.3",
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
 
@@ -12,7 +12,7 @@
12
12
  * 'https://...' — fetch URL content as prompt seed
13
13
  * '<text>' — use directly as prompt seed
14
14
  *
15
- * Feed items are tracked in Firestore (`ghostii-feed-items`) so the same
15
+ * Feed items are tracked in Firestore (`ghostii-sources`) so the same
16
16
  * article is never processed twice. When a feed is unreachable or exhausted,
17
17
  * the entry falls back to $app behavior.
18
18
  */
@@ -381,7 +381,7 @@ async function getProcessedItemIds(admin, feedUrl) {
381
381
  }
382
382
 
383
383
  const snapshot = await admin.firestore()
384
- .collection('ghostii-feed-items')
384
+ .collection('ghostii-sources')
385
385
  .where('feedUrl', '==', feedUrl)
386
386
  .select('itemId', 'itemUrl')
387
387
  .get();
@@ -413,18 +413,20 @@ async function getProcessedItemIds(admin, feedUrl) {
413
413
  */
414
414
  async function trackFeedItem(admin, { feedUrl, item, brandId, postUrl, postSlug }) {
415
415
  const docId = feedItemHash(feedUrl, item.id || item.url);
416
+ const nowISO = new Date().toISOString();
417
+ const nowUNIX = Math.round(Date.now() / 1000);
416
418
 
417
- await admin.firestore().doc(`ghostii-feed-items/${docId}`).set({
419
+ await admin.firestore().doc(`ghostii-sources/${docId}`).set({
418
420
  feedUrl: feedUrl,
419
421
  itemId: item.id || item.url,
420
422
  itemUrl: item.url,
421
423
  itemTitle: item.title,
422
- processedAt: admin.firestore.FieldValue.serverTimestamp(),
423
424
  brandId: brandId,
424
425
  postUrl: postUrl || null,
425
426
  postSlug: postSlug || null,
426
427
  metadata: {
427
- created: admin.firestore.FieldValue.serverTimestamp(),
428
+ created: { timestamp: nowISO, timestampUNIX: nowUNIX },
429
+ updated: { timestamp: nowISO, timestampUNIX: nowUNIX },
428
430
  },
429
431
  });
430
432
  }
@@ -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
+ }
@@ -176,7 +176,7 @@ module.exports = {
176
176
  postSlug: 'test-article',
177
177
  });
178
178
 
179
- const doc = await admin.firestore().doc(`ghostii-feed-items/${docId}`).get();
179
+ const doc = await admin.firestore().doc(`ghostii-sources/${docId}`).get();
180
180
  assert.ok(doc.exists, 'tracking doc created');
181
181
 
182
182
  const data = doc.data();
@@ -188,6 +188,13 @@ module.exports = {
188
188
  assert.equal(data.postUrl, 'https://test-brand.com/blog/test-article');
189
189
  assert.equal(data.postSlug, 'test-article');
190
190
  assert.ok(data.metadata, 'has metadata object');
191
+ assert.ok(data.metadata.created, 'has metadata.created');
192
+ assert.ok(data.metadata.updated, 'has metadata.updated');
193
+ assert.equal(typeof data.metadata.created.timestamp, 'string', 'created.timestamp is ISO string');
194
+ assert.equal(typeof data.metadata.created.timestampUNIX, 'number', 'created.timestampUNIX is number');
195
+ assert.equal(typeof data.metadata.updated.timestamp, 'string', 'updated.timestamp is ISO string');
196
+ assert.equal(typeof data.metadata.updated.timestampUNIX, 'number', 'updated.timestampUNIX is number');
197
+ assert.ok(!data.processedAt, 'no legacy processedAt field');
191
198
  },
192
199
  },
193
200
 
@@ -26,22 +26,15 @@ const TEMP_DIR = path.join(BEM_ROOT, '.temp', 'ghostii-feed', `run-${new Date().
26
26
 
27
27
  // --- Real feed URLs for extended tests ---
28
28
  // Chosen for stability and confirmed to work with wonderful-fetch.
29
+ // Includes generic tech feeds + marketing/social feeds used by OMEGA consumers.
29
30
  const REAL_FEEDS = [
30
- {
31
- name: 'Ars Technica',
32
- url: 'https://feeds.arstechnica.com/arstechnica/technology-lab',
33
- format: 'rss',
34
- },
35
- {
36
- name: 'TechCrunch',
37
- url: 'https://techcrunch.com/feed/',
38
- format: 'rss',
39
- },
40
- {
41
- name: 'Politico',
42
- url: 'https://rss.politico.com/politics-news.xml',
43
- format: 'rss',
44
- },
31
+ { name: 'Ars Technica', url: 'https://feeds.arstechnica.com/arstechnica/technology-lab', format: 'rss' },
32
+ { name: 'TechCrunch', url: 'https://techcrunch.com/feed/', format: 'rss' },
33
+ { name: 'Social Media Examiner', url: 'https://www.socialmediaexaminer.com/feed/', format: 'rss' },
34
+ { name: 'Hootsuite Blog', url: 'https://blog.hootsuite.com/feed/', format: 'rss' },
35
+ { name: 'Sprout Social Insights', url: 'https://sproutsocial.com/insights/feed/', format: 'rss' },
36
+ { name: 'Buffer Resources', url: 'https://buffer.com/resources/feed/', format: 'rss' },
37
+ { name: 'Digiday', url: 'https://digiday.com/feed/', format: 'rss' },
45
38
  ];
46
39
 
47
40
  /**
@@ -333,7 +326,7 @@ module.exports = {
333
326
 
334
327
  // Verify the doc was stored with the expected hash ID
335
328
  const expectedDocId = feedItemHash(realFeedUrl, firstItem.id || firstItem.url);
336
- const doc = await admin.firestore().doc(`ghostii-feed-items/${expectedDocId}`).get();
329
+ const doc = await admin.firestore().doc(`ghostii-sources/${expectedDocId}`).get();
337
330
 
338
331
  assert.ok(doc.exists, 'tracking doc exists at expected hash-based ID');
339
332
 
@@ -375,11 +368,11 @@ module.exports = {
375
368
  saveArtifact(`feed-${feed.name.toLowerCase().replace(/\s+/g, '-')}-raw.xml`, text);
376
369
  }
377
370
 
378
- // At least 2 of 3 feeds should parse successfully
371
+ // At least 5 of 7 feeds should parse successfully
379
372
  const successful = results.filter((r) => r.items > 0);
380
373
  assert.ok(
381
- successful.length >= 2,
382
- `at least 2 of ${REAL_FEEDS.length} real feeds parsed successfully: ${JSON.stringify(results)}`,
374
+ successful.length >= 5,
375
+ `at least 5 of ${REAL_FEEDS.length} real feeds parsed successfully: ${JSON.stringify(results)}`,
383
376
  );
384
377
 
385
378
  // Save summary