backend-manager 5.0.201 → 5.0.203

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,17 @@ 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.0.203] - 2026-05-13
18
+ ### Fixed
19
+ - `Settings.resolve()` now surfaces a clear `No schema for <METHOD> request: expected <schema>/<method>.js or <schema>/index.js` error (code 500) when both the method-specific schema (e.g. `delete.js`) and the `index.js` fallback are absent. Previously the raw Node `Cannot find module .../<schema>/index.js` error propagated to consumers, leaking the require stack and surfacing the internal `/workspace/...` deploy path.
20
+ - Schema files that exist but throw (syntax error, runtime error) are now re-thrown directly instead of being silently masked by an unintended fallback to `index.js`. The real error surfaces, making bugs in schema files debuggable.
21
+
22
+ # [5.0.202] - 2026-05-12
23
+ ### Added
24
+ - **`src/defaults/CLAUDE.md`** — new file shipped to consumer projects, marker-wrapped with `# ========== Default Values ==========` / `# ========== Custom Values ==========`. Framework section stays live-synced across `npx mgr setup` while the Custom section is preserved verbatim. Aligns BEM with EM/BXM/UJM, which already ship a consumer-facing default CLAUDE.md.
25
+ - **`src/utils/merge-line-files.js`** — new file. Implements the line-based merge for the marker-wrapped Default/Custom sections (copied verbatim from `electron-manager`'s utility). Reusable across `.env`, `.gitignore`, `CLAUDE.md`, and any other line-based files BEM might ship in the future.
26
+ - **`copyDefaults()` method** in `src/cli/commands/setup.js`. New defaults-shipping mechanism for BEM (which previously had no `src/defaults/` directory at all). Mirrors EM's `copyDefaults` pattern: iterates `src/defaults/**`, renames `_.foo` → `.foo`, routes files matching `MERGEABLE_BASENAMES` (`['.env', '.gitignore', 'CLAUDE.md']`) through `mergeLineBasedFiles`, copies non-mergeable files only if they don't already exist. Wired into `runSetup()` BEFORE `runTests()` so test inspections see the merged state.
27
+
17
28
  # [5.0.201] - 2026-05-08
18
29
  ### Changed
19
30
  - Account deletion confirmation email now includes a "Deletion details:" block with the user's account email, UID, and deletion timestamp (UTC) — matching the pattern used in the data-request download email.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.201",
3
+ "version": "5.0.203",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -107,6 +107,10 @@ class SetupCommand extends BaseCommand {
107
107
  // Clean up leftover trigger files from watch command
108
108
  this.cleanupTriggerFiles();
109
109
 
110
+ // Copy / merge defaults into consumer project root (matches EM/BXM/UJM pattern).
111
+ // Runs BEFORE tests so any test that inspects scaffolded files sees the merged state.
112
+ this.copyDefaults();
113
+
110
114
  // Run all tests
111
115
  await this.runTests();
112
116
 
@@ -147,6 +151,74 @@ class SetupCommand extends BaseCommand {
147
151
  self.default.databaseRulesCore = self.default.databaseRulesWhole.match(bem_allRulesRegex)[0];
148
152
  }
149
153
 
154
+ // Copy default files (src/defaults/**) into the consumer project root.
155
+ // For files in MERGEABLE_BASENAMES, route through `mergeLineBasedFiles` so the
156
+ // framework's section stays live-synced while the consumer's Custom section is
157
+ // preserved verbatim. For non-mergeable files: copy on first setup, skip if exists.
158
+ //
159
+ // Mirrors EM's copyDefaults pattern (src/commands/setup.js in electron-manager).
160
+ // Same marker convention as .env/.gitignore in EM/BXM/UJM:
161
+ // # ========== Default Values ========== (framework-owned)
162
+ // # ========== Custom Values ========== (consumer-owned)
163
+ copyDefaults() {
164
+ const self = this.main;
165
+ const defaultsDir = path.resolve(`${__dirname}/../../defaults`);
166
+
167
+ if (!jetpack.exists(defaultsDir)) {
168
+ // Defaults dir is optional — older BEM versions didn't have one. If missing, skip silently.
169
+ return;
170
+ }
171
+
172
+ const { mergeLineBasedFiles } = require('../../utils/merge-line-files.js');
173
+ // Files routed through the marker-based merge (vs verbatim copy / skip-if-exists).
174
+ // .env / .gitignore aren't currently shipped by BEM but are included here so this
175
+ // matches the EM/BXM/UJM contract if we ever add them.
176
+ const MERGEABLE_BASENAMES = new Set(['.env', '.gitignore', 'CLAUDE.md']);
177
+
178
+ const files = jetpack.find(defaultsDir, { matching: '**/*', recursive: true, files: true, directories: false });
179
+
180
+ for (const src of files) {
181
+ const rel = path.relative(defaultsDir, src);
182
+ const segments = rel.split(path.sep);
183
+
184
+ // Skip "archive" directories — anything under a path segment starting with `_` and
185
+ // followed by a non-`.` character. Matches EM/BXM/UJM convention. The `_.env` /
186
+ // `_.gitignore` files are NOT skipped; their leading `_` strips on copy below.
187
+ if (segments.some((s) => s.startsWith('_') && !s.startsWith('_.'))) {
188
+ continue;
189
+ }
190
+
191
+ // Convert leading `_.` to `.` so dotfiles ship past npm's filter.
192
+ const target = segments.map((part) => part.startsWith('_.') ? part.slice(1) : part).join(path.sep);
193
+ const dest = path.join(self.firebaseProjectPath, target);
194
+ const basename = path.basename(target);
195
+
196
+ if (jetpack.exists(dest)) {
197
+ if (MERGEABLE_BASENAMES.has(basename)) {
198
+ try {
199
+ const existing = jetpack.read(dest, 'utf8');
200
+ const incoming = jetpack.read(src, 'utf8');
201
+ const merged = mergeLineBasedFiles(existing, incoming, basename);
202
+ if (merged !== existing) {
203
+ jetpack.write(dest, merged);
204
+ this.logSuccess(`Merged default → ${target}`);
205
+ }
206
+ } catch (e) {
207
+ this.logWarning(`Failed to merge ${target}: ${e.message}`);
208
+ }
209
+ continue;
210
+ }
211
+
212
+ // Non-mergeable, already exists → preserve consumer's version.
213
+ continue;
214
+ }
215
+
216
+ // First time: copy as-is.
217
+ jetpack.copy(src, dest);
218
+ this.logSuccess(`Copied default → ${target}`);
219
+ }
220
+ }
221
+
150
222
  cleanupTriggerFiles() {
151
223
  const self = this.main;
152
224
  const triggerFile = `${self.firebaseProjectPath}/functions/bem-reload-trigger.js`;
@@ -0,0 +1,73 @@
1
+ # ========== Default Values ==========
2
+ # Backend Manager (BEM) — consumer project
3
+
4
+ > **Auto-managed file.** Everything between `# ========== Default Values ==========` and `# ========== Custom Values ==========` is owned by `backend-manager` and rewritten on every `npx mgr setup`. Put your own project-specific notes BELOW the `Custom Values` marker — that section is preserved verbatim across setups.
5
+
6
+ ## Framework
7
+
8
+ This project consumes **Backend Manager** (BEM) — a comprehensive framework for building modern Firebase Cloud Functions backends. BEM provides a single `Manager.init(exports, {...})` bootstrap that wires built-in functions (`bm_api`, auth events, cron jobs), helper classes (Assistant, User, Analytics, Usage, Middleware, Settings, Utilities, Metadata), payment processor integrations (Stripe / PayPal), Firestore-trigger pipelines, and a deploy/emulator/watch tooling pipeline.
9
+
10
+ **Framework's own docs** (read these for deep-dives; both paths point to the same files, the absolute path works regardless of working directory):
11
+ - Top-level overview: `/Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/CLAUDE.md` (or `node_modules/backend-manager/CLAUDE.md`)
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ cd functions
17
+ npx mgr setup # validate config + scaffold defaults + run checks
18
+ npx mgr emulator # start Firebase emulators (auth/firestore/functions/database/storage)
19
+ npx mgr watch # auto-reload functions on file change
20
+ npx mgr deploy # deploy to Firebase
21
+ npx mgr logs # tail Cloud Functions logs
22
+ ```
23
+
24
+ All `npx mgr <cmd>` aliases — `npx bm <cmd>`, `npx bem <cmd>`, `npx backend-manager <cmd>` work too.
25
+
26
+ ## Where things live
27
+
28
+ - `functions/index.js` — entry point. Must call `Manager.init(exports, { ... })` to register all built-in + custom endpoints.
29
+ - `functions/backend-manager-config.json` — BEM config: brand, projectType (`firebase` or `custom`), feature flags, rate limits, hooks.
30
+ - `functions/.env` — secrets (BACKEND_MANAGER_KEY, third-party API keys). Gitignored.
31
+ - `functions/service-account.json` — Firebase Admin credentials. Gitignored.
32
+ - `functions/routes/<verb>/<path>.js` — custom routes mounted at runtime (e.g. `routes/get/hello.js` → `GET /hello`).
33
+ - `functions/schemas/<name>.js` — schema definitions for `Manager.Settings()` validation.
34
+ - `firebase.json` — Firebase config (hosting, rewrites, emulator ports). Some fields managed by `npx mgr setup`.
35
+ - `.firebaserc` — Firebase project ID alias.
36
+ - `firestore.rules` / `database.rules.json` — security rules. BEM owns a `///---backend-manager---///` block inside each; everything outside is yours.
37
+
38
+ ## Per-context imports
39
+
40
+ ```js
41
+ // functions/index.js — the entire backend bootstrap
42
+ const Manager = require('backend-manager');
43
+ Manager.init(exports, {
44
+ projectType: 'firebase',
45
+ // ...your config
46
+ });
47
+
48
+ // In a custom route (functions/routes/get/hello.js):
49
+ module.exports = async function(Manager, assistant) {
50
+ // assistant.req, assistant.res, assistant.user, etc.
51
+ };
52
+ ```
53
+
54
+ ## Available APIs at runtime
55
+
56
+ After `Manager.init()`, the Manager instance exposes factory methods:
57
+ - `Manager.Assistant({ req, res })` — request handler with user + analytics + utility access
58
+ - `Manager.User(data)` — user property structure + schema
59
+ - `Manager.Analytics({ assistant })` — GA4 event tracking
60
+ - `Manager.Usage()` — rate-limiting
61
+ - `Manager.Middleware(req, res)` — request pipeline
62
+ - `Manager.Settings()` — schema validation against `functions/schemas/*`
63
+ - `Manager.Utilities()` — batch operations + helpers
64
+ - `Manager.Metadata(doc)` — timestamps + tag helpers
65
+ - `Manager.storage({ name })` — local JSON storage (lowdb)
66
+
67
+ Auth events, payment-webhook transitions, and cron jobs are wired automatically — hook into them by exporting from `functions/hooks/<area>/<event>.js`.
68
+
69
+ # ========== Custom Values ==========
70
+
71
+ ## Project-specific notes
72
+
73
+ Add anything specific to THIS project here. Edits below this line are preserved across `npx mgr setup` runs.
@@ -47,19 +47,38 @@ Settings.prototype.resolve = function (assistant, schema, settings, options) {
47
47
  const method = (assistant?.request?.method || '').toLowerCase();
48
48
  const methodFile = `${method}.js`;
49
49
  const schemaFile = options.schema.replace('.js', '');
50
- let schemaPath;
51
50
 
52
- // First try method-specific schema (e.g., test/get.js, test/post.js)
53
51
  const methodSchemaPath = path.resolve(options.dir, `${schemaFile}/${methodFile}`);
52
+ const indexSchemaPath = path.resolve(options.dir, `${schemaFile}/index.js`);
53
+
54
+ // Helper: only fall back when THIS specific file is missing.
55
+ // If the file exists but throws (syntax error, runtime error, etc.) we re-throw
56
+ // so the real problem surfaces instead of being masked by a misleading fallback.
57
+ const isMissingModule = (err, expectedPath) => err
58
+ && err.code === 'MODULE_NOT_FOUND'
59
+ && typeof err.message === 'string'
60
+ && err.message.includes(expectedPath);
54
61
 
55
62
  try {
56
63
  schema = loadSchema(assistant, methodSchemaPath);
57
64
  assistant.log(`Settings.resolve(): Loaded method-specific schema: ${schemaFile}/${methodFile}`);
58
- } catch (e) {
59
- // Fallback to main schema if method-specific doesn't exist
60
- schemaPath = path.resolve(options.dir, `${schemaFile}/index.js`);
61
- schema = loadSchema(assistant, schemaPath);
62
- assistant.log(`Settings.resolve(): Method-specific schema not found, using main schema fallback`);
65
+ } catch (methodErr) {
66
+ if (!isMissingModule(methodErr, methodSchemaPath)) {
67
+ throw methodErr;
68
+ }
69
+
70
+ try {
71
+ schema = loadSchema(assistant, indexSchemaPath);
72
+ assistant.log(`Settings.resolve(): Method-specific schema not found, using main schema fallback`);
73
+ } catch (indexErr) {
74
+ if (!isMissingModule(indexErr, indexSchemaPath)) {
75
+ throw indexErr;
76
+ }
77
+ throw assistant.errorify(
78
+ `No schema for ${method.toUpperCase()} request: expected ${schemaFile}/${methodFile} or ${schemaFile}/index.js`,
79
+ {code: 500},
80
+ );
81
+ }
63
82
  }
64
83
  }
65
84
 
@@ -1377,6 +1377,9 @@
1377
1377
  "edudingy.cfd",
1378
1378
  "edumail.edu.pl",
1379
1379
  "edumail.edu.rs",
1380
+ "edumaili.com",
1381
+ "edumaili.edu.pl",
1382
+ "edumaill.edu.pl",
1380
1383
  "edupolska.edu.pl",
1381
1384
  "edv.to",
1382
1385
  "ee1.pl",
@@ -2461,6 +2464,7 @@
2461
2464
  "javadmin.com",
2462
2465
  "javaemail.com",
2463
2466
  "jbsze.com",
2467
+ "jbsze.ne",
2464
2468
  "jcnorris.com",
2465
2469
  "jdmadventures.com",
2466
2470
  "jdz.ro",
@@ -2909,6 +2913,7 @@
2909
2913
  "maileater.com",
2910
2914
  "mailed.in",
2911
2915
  "mailed.ro",
2916
+ "mailedu.edu.pl",
2912
2917
  "maileimer.de",
2913
2918
  "maileme101.com",
2914
2919
  "mailer.edu.pl",
@@ -3172,6 +3177,7 @@
3172
3177
  "mliok.com",
3173
3178
  "mm.my",
3174
3179
  "mm5.se",
3180
+ "mmaily.com",
3175
3181
  "mnode.me",
3176
3182
  "moakt.cc",
3177
3183
  "moakt.co",
@@ -0,0 +1,211 @@
1
+ // Merge line-based files (.env, .gitignore, CLAUDE.md) on `npx mgr setup`.
2
+ //
3
+ // Convention (matches EM/BXM/UJM):
4
+ //
5
+ // # ========== Default Values ==========
6
+ // # framework-managed; overwritten on every setup
7
+ // KEY1=
8
+ // KEY2="value with spaces"
9
+ //
10
+ // # ========== Custom Values ==========
11
+ // # user's section; preserved verbatim across setups
12
+ // USER_SECRET="my-secret"
13
+ //
14
+ // Behavior:
15
+ // - The Default section is replaced with the new framework's defaults.
16
+ // - Keys that already had values (in either Default or Custom) keep those values
17
+ // in the same section they were in.
18
+ // - Keys the user added to the Default section that are NOT in the new framework's
19
+ // defaults migrate to the Custom section (so framework cleanups don't lose user data).
20
+ // - .env values are normalized to **double-quoted** form on every merge:
21
+ // KEY=raw-value → KEY="raw-value"
22
+ // KEY="already-quoted" → KEY="already-quoted" (left alone)
23
+ // KEY= → KEY= (empty stays empty/unquoted)
24
+ // This protects values containing spaces, #, $, or other shell-meaningful chars.
25
+ // - .gitignore: same logic, line-based instead of key-based (no quoting).
26
+ // - CLAUDE.md: same logic as .gitignore (line-based, no quoting). The markers
27
+ // render as visible H1 headings in markdown — that's intentional UX.
28
+ // - First setup (no existing file): the framework template lands as-is.
29
+
30
+ const DEFAULT_MARKER = '# ========== Default Values ==========';
31
+ const CUSTOM_MARKER = '# ========== Custom Values ==========';
32
+
33
+ function mergeLineBasedFiles(existingContent, newContent, fileName) {
34
+ const isEnvFile = fileName === '.env';
35
+
36
+ const existingLines = existingContent.split('\n');
37
+ const newLines = newContent.split('\n');
38
+
39
+ // Parse existing into default + custom sections.
40
+ const { defaultLines: existingDefault, customLines: existingCustom, existingDefaultKeys, existingCustomKeys }
41
+ = splitSections(existingLines, isEnvFile);
42
+
43
+ // Parse new content. We only use its default section (custom is the user's domain).
44
+ const { defaultLines: newDefault, customLines: newCustom } = splitSections(newLines, isEnvFile);
45
+
46
+ // Build the merged default section: walk new defaults in order, substituting the
47
+ // user's existing value for any key they had set in either section.
48
+ const newDefaultKeys = new Set();
49
+ const mergedDefault = [];
50
+ const emit = (line) => mergedDefault.push(isEnvFile ? normalizeEnvLine(line) : line);
51
+
52
+ for (const line of newDefault) {
53
+ const trimmed = line.trim();
54
+
55
+ if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
56
+ const key = trimmed.split('=')[0].trim();
57
+ if (key) {
58
+ newDefaultKeys.add(key);
59
+ if (existingDefaultKeys.has(key)) {
60
+ emit(findKeyLine(existingDefault, key));
61
+ continue;
62
+ }
63
+ if (existingCustomKeys.has(key)) {
64
+ // Key the user moved to custom — leave it in custom; emit empty default value.
65
+ emit(line);
66
+ continue;
67
+ }
68
+ }
69
+ emit(line);
70
+ } else if (!isEnvFile && trimmed && !trimmed.startsWith('#')) {
71
+ // .gitignore / CLAUDE.md: just keep the new line.
72
+ mergedDefault.push(line);
73
+ } else {
74
+ // Comment / blank.
75
+ mergedDefault.push(line);
76
+ }
77
+ }
78
+
79
+ // User-added stuff in their Default section that the new framework doesn't know about
80
+ // → migrate to Custom so it's preserved without being clobbered next setup.
81
+ const migratedToCustom = [];
82
+ for (const line of existingDefault) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed || trimmed.startsWith('#')) continue;
85
+
86
+ if (isEnvFile) {
87
+ const key = trimmed.split('=')[0].trim();
88
+ if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
89
+ migratedToCustom.push(normalizeEnvLine(line));
90
+ }
91
+ } else {
92
+ // .gitignore / CLAUDE.md: line not in new defaults → migrate.
93
+ const inNew = newLines.some((nl) => nl.trim() === trimmed);
94
+ if (!inNew) {
95
+ migratedToCustom.push(line);
96
+ }
97
+ }
98
+ }
99
+
100
+ // The user's Custom section is preserved verbatim — except .env values get
101
+ // normalized to double-quoted form so the file's quoting style is consistent.
102
+ const finalCustom = isEnvFile
103
+ ? existingCustom.map((line) => normalizeEnvLine(line))
104
+ : existingCustom;
105
+
106
+ const result = [];
107
+ result.push(DEFAULT_MARKER);
108
+ result.push(...mergedDefault);
109
+ // Insert a single blank line before CUSTOM_MARKER, but only if the merged default
110
+ // doesn't already end with one (otherwise we'd accumulate an extra blank line on
111
+ // every merge — breaking idempotency on the first re-run after a fresh `jetpack.copy`).
112
+ if (mergedDefault.length === 0 || mergedDefault[mergedDefault.length - 1].trim() !== '') {
113
+ result.push('');
114
+ }
115
+ result.push(CUSTOM_MARKER);
116
+ if (migratedToCustom.length > 0) {
117
+ result.push(...migratedToCustom);
118
+ }
119
+ result.push(...finalCustom);
120
+
121
+ return result.join('\n');
122
+ }
123
+
124
+ // Normalize a single .env line:
125
+ // - Comments / blanks unchanged
126
+ // - KEY= (empty value) unchanged
127
+ // - KEY="..." (already double-quoted) unchanged
128
+ // - KEY=raw-value → KEY="raw-value" (with embedded " and \ escaped)
129
+ // - KEY='single' → KEY="single" (canonicalize single → double)
130
+ function normalizeEnvLine(line) {
131
+ if (typeof line !== 'string') return line;
132
+
133
+ // Preserve comments and blank lines verbatim.
134
+ const trimmed = line.trim();
135
+ if (!trimmed || trimmed.startsWith('#')) return line;
136
+
137
+ // Capture leading whitespace so we don't lose indentation.
138
+ const leadingMatch = line.match(/^(\s*)/);
139
+ const leading = leadingMatch ? leadingMatch[1] : '';
140
+
141
+ const eqIdx = trimmed.indexOf('=');
142
+ if (eqIdx < 0) return line; // no `=` — not a KEY=VALUE line
143
+
144
+ const key = trimmed.slice(0, eqIdx).trim();
145
+ let value = trimmed.slice(eqIdx + 1);
146
+
147
+ // Strip trailing whitespace + inline comment-after-value (rare; we only strip a # that follows a space).
148
+ // We do NOT strip # inside quoted values. Detect that by checking if value starts with a quote.
149
+ if (value.length === 0) {
150
+ return `${leading}${key}=`;
151
+ }
152
+
153
+ // Already double-quoted? Leave alone (preserves user's exact contents).
154
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
155
+ return `${leading}${key}=${value}`;
156
+ }
157
+
158
+ // Single-quoted → canonicalize to double-quoted.
159
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
160
+ const inner = value.slice(1, -1);
161
+ return `${leading}${key}="${escapeForDoubleQuote(inner)}"`;
162
+ }
163
+
164
+ // Raw value → wrap.
165
+ return `${leading}${key}="${escapeForDoubleQuote(value)}"`;
166
+ }
167
+
168
+ function escapeForDoubleQuote(s) {
169
+ return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
170
+ }
171
+
172
+ function splitSections(lines, isEnvFile) {
173
+ const defaultLines = [];
174
+ const customLines = [];
175
+ const existingDefaultKeys = new Set();
176
+ const existingCustomKeys = new Set();
177
+
178
+ let mode = null; // null | 'default' | 'custom'
179
+ for (const line of lines) {
180
+ const trimmed = line.trim();
181
+ if (trimmed === DEFAULT_MARKER) { mode = 'default'; continue; }
182
+ if (trimmed === CUSTOM_MARKER) { mode = 'custom'; continue; }
183
+
184
+ // Lines before any marker are treated as default (legacy / fresh files).
185
+ if (mode === 'custom') {
186
+ customLines.push(line);
187
+ if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
188
+ const key = trimmed.split('=')[0].trim();
189
+ if (key) existingCustomKeys.add(key);
190
+ }
191
+ } else {
192
+ defaultLines.push(line);
193
+ if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
194
+ const key = trimmed.split('=')[0].trim();
195
+ if (key) existingDefaultKeys.add(key);
196
+ }
197
+ }
198
+ }
199
+
200
+ return { defaultLines, customLines, existingDefaultKeys, existingCustomKeys };
201
+ }
202
+
203
+ function findKeyLine(lines, key) {
204
+ const re = new RegExp(`^\\s*${key}\\s*=`);
205
+ for (const line of lines) {
206
+ if (re.test(line)) return line;
207
+ }
208
+ return `${key}=`;
209
+ }
210
+
211
+ module.exports = { mergeLineBasedFiles, normalizeEnvLine, DEFAULT_MARKER, CUSTOM_MARKER };
@@ -1,32 +0,0 @@
1
- As for the AMEX enrollment, AMEX alerts are not supported on Stripe/Shopify. To enable them, you’ll need a dedicated AMEX MID and a payment orchestrator to route all AMEX transactions through that dedicated MID. Using a dedicated AMEX MID allows for a dispute rate of up to 5%, so routing all AMEX traffic through it will significantly reduce your dispute rate on Stripe.
2
-
3
- 06:44 PM
4
- For Discover,
5
-
6
- In order to enroll in Discover coverage, you will need to do the following:
7
-
8
- First, provide us with details about your business below:
9
- - Merchant Legal Name:
10
- - Merchant DBA Name:
11
- - Merchant Registered Street Address:
12
- - City:
13
- - Country:
14
- - State/Province:
15
- - ZIP/Postal Code:
16
-
17
- Second, you must request from your payment processor’s support for a few pieces of information. To do so, please follow the steps below and send them the email template below.
18
-
19
- ""I am enrolling for Discover Ethoca Alerts via my third-party vendor, Chargeblast. I need my 15-Digit Discover SE number. Can you please provide these pieces of information to me ASAP? Let me know if you need any additional info from me. Please feel free to provide me with these pieces of information piecemeal.”
20
-
21
- 06:44 PM
22
- For AMEX If you’d like to proceed with obtaining a dedicated AMEX MID, we’d be happy to arrange an introduction for you.
23
-
24
- 06:45 PM
25
- Avatar of Zander
26
- Zander
27
- Are we still connected?
28
-
29
- 06:56 PM
30
- Avatar of Zander
31
- Zander
32
- Since this chat has been inactive, I’ll close it for now. If you have more questions, feel free to contact us at any time. Have a great day ahead!
@@ -1,14 +0,0 @@
1
- https://github.com/disposable-email-domains/disposable-email-domains?tab=readme-ov-file
2
- https://www.npmjs.com/package/disposable-domains
3
- https://github.com/tompec/disposable-email-domains
4
-
5
- Two repos:
6
-
7
- Repo Domains Approach
8
- disposable-email-domains/disposable-email-domains 5,359 Curated, conservative, high confidence
9
- ivolo/disposable-email-domains 121,569 Aggressive, aggregated from many sources, more false positives
10
- Our current list has 854 — so even the smaller curated list is 6x larger.
11
-
12
- For our use case (currently only used to skip marketing sync, not blocking signups), I'd recommend the 5,359 curated list — it's comprehensive enough without being overly aggressive. And if we ever do use it for blocking signups, the false positive risk is much lower.
13
-
14
- Want to swap to that one?