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 +22 -0
- package/CLAUDE.md +2 -2
- package/PROGRESS.md +15 -5
- package/docs/ghostii.md +8 -5
- package/package.json +1 -1
- package/src/cli/commands/setup.js +98 -24
- package/src/manager/events/cron/daily/ghostii-auto-publisher.js +7 -5
- package/src/manager/libraries/email/data/disposable-domains.json +22 -0
- package/templates/firebase.json +66 -0
- package/test/helpers/content/ghostii-auto-publisher.js +8 -1
- package/test/helpers/content/ghostii-feed-integration.js +12 -19
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` —
|
|
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` |
|
|
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:**
|
|
6
|
-
* **Current Phase:**
|
|
7
|
-
* **Priority:**
|
|
8
|
-
* **Last Updated:** 2026-06-
|
|
9
|
-
* **Notes:**
|
|
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-
|
|
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-
|
|
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-
|
|
58
|
+
Collection: `ghostii-sources` (consumer project Firestore)
|
|
59
59
|
|
|
60
60
|
```js
|
|
61
|
-
ghostii-
|
|
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
|
@@ -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
|
-
//
|
|
74
|
-
|
|
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
|
-
//
|
|
97
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
|
|
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-
|
|
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
|
|
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 >=
|
|
382
|
-
`at least
|
|
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
|