backend-manager 5.2.5 → 5.2.7
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 +40 -0
- package/TODO-CANCEL-EMAIL-MISSING-ORDER-ID.md +159 -0
- package/TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md +15 -0
- package/TODO-WEBHOOK-KEY-UPGRADE.md +138 -0
- package/docs/stripe-webhook-forwarding.md +2 -2
- package/package.json +1 -1
- package/scripts/test-helper-providers.js +162 -0
- package/src/cli/commands/base-command.js +5 -5
- package/src/cli/commands/emulator.js +201 -54
- package/src/cli/commands/test.js +80 -9
- package/src/manager/events/cron/daily/ghostii-auto-publisher.js +2 -2
- package/src/manager/events/firestore/payments-webhooks/analytics.js +2 -2
- package/src/manager/functions/core/actions/api/user/delete.js +1 -1
- package/src/manager/helpers/analytics.js +1 -1
- package/src/manager/libraries/email/generators/newsletter.js +2 -2
- package/src/manager/libraries/email/providers/beehiiv.js +11 -6
- package/src/manager/libraries/email/providers/sendgrid.js +6 -6
- package/src/manager/libraries/email/validation.js +1 -1
- package/src/manager/libraries/infer-contact.js +12 -5
- package/src/manager/routes/general/email/post.js +4 -2
- package/src/manager/routes/marketing/email-preferences/post.js +2 -2
- package/src/manager/routes/payments/dispute-alert/post.js +3 -3
- package/src/manager/routes/payments/intent/processors/test.js +2 -2
- package/src/manager/routes/payments/webhook/post.js +2 -2
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/oauth2/providers/discord.js +1 -1
- package/src/manager/routes/user/oauth2/providers/google.js +1 -1
- package/src/manager/routes/user/signup/post.js +1 -0
- package/src/test/runner.js +7 -0
- package/src/test/utils/http-client.js +1 -0
- package/test/events/payments/journey-payments-cancel.js +4 -4
- package/test/events/payments/journey-payments-failure.js +2 -2
- package/test/events/payments/journey-payments-legacy-product.js +1 -1
- package/test/events/payments/journey-payments-one-time-failure.js +1 -1
- package/test/events/payments/journey-payments-plan-change.js +1 -1
- package/test/events/payments/journey-payments-refund-webhook.js +4 -4
- package/test/events/payments/journey-payments-suspend.js +4 -4
- package/test/events/payments/journey-payments-trial.js +2 -2
- package/test/events/payments/journey-payments-uid-resolution.js +1 -1
- package/test/routes/payments/dispute-alert.js +13 -13
- package/test/routes/payments/webhook.js +3 -3
- package/test/routes/test/usage.js +12 -3
- /package/src/manager/routes/general/email/templates/{download-app-link.js → general/download-app-link.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,46 @@ 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.2.7] - 2026-05-24
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`inferContact` silent-failure logging.** When the AI inference returned an empty result (either the AI call failed and returned null, or `gpt-5-mini` returned a parsed response with all-empty fields, or the response shape was missing `firstName`), the whole flow silently swallowed the failure and the signup's `user.personal.name` got written as `null`/`null`. Confirmed live on Somiibo: signed up `ian.wiedenman.business@gmail.com` twice on the same backend — first signup inferred nothing (empty name written), second signup correctly inferred "Ian Wiedenman". Same email, same code, transient AI hiccup, zero log trail. Added three unconditional diagnostic logs to `src/manager/libraries/infer-contact.js` (AI returned null, AI parsed response had all fields empty, AI response missing firstName) plus a log in `src/manager/routes/user/signup/post.js#inferUserContact` when the helper returns null. Next silent failure will at least leave a breadcrumb.
|
|
22
|
+
|
|
23
|
+
# [5.2.6] - 2026-05-24
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`scripts/test-helper-providers.js`** — small CLI for live-test verification. Run from any consumer's `functions/` dir: `node <bem>/scripts/test-helper-providers.js find <email>` (check SendGrid + Beehiiv state without dashboards) or `purge <email>` (remove from both providers). Used during end-to-end consent-pipeline testing.
|
|
28
|
+
- **Email-template namespacing.** `general/email` route now translates `:` in `settings.id` to a folder separator, so `general:download-app-link` resolves to `templates/general/download-app-link.js`. Existing `templates/download-app-link.js` moved into `templates/general/` to match.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Payment webhook + dispute-alert routes now accept either `BACKEND_MANAGER_WEBHOOK_KEY` (preferred) or `BACKEND_MANAGER_KEY` (legacy).** Phase 1 of the webhook-key migration plan in `TODO-WEBHOOK-KEY-UPGRADE.md` — non-breaking dual-acceptance so consumers can roll over at their own pace. `src/manager/routes/payments/webhook/post.js:28` + `src/manager/routes/payments/dispute-alert/post.js:20` validate against either env var. Test-processor self-fire in `src/manager/routes/payments/intent/processors/test.js:158` now uses `BACKEND_MANAGER_WEBHOOK_KEY` directly. All 13 payment test files (`test/routes/payments/*.js`, `test/events/payments/journey-*.js`) updated to use `BACKEND_MANAGER_WEBHOOK_KEY` for webhook URLs. `src/test/utils/http-client.js` + `src/test/runner.js` thread the new env var through the test context. `docs/stripe-webhook-forwarding.md` reflects the dual-key acceptance. Phase 2 (drop the legacy fallback) is tracked in `TODO-WEBHOOK-KEY-LEGACY-REMOVAL.md`.
|
|
33
|
+
- **`npx mgr emulator` no longer wraps the emulator in a keep-alive subprocess.** Previously spawned a sleep-86400 wrapper via `runWithEmulator`; now spawns the emulator child directly and uses a clean SIGINT handler. Cleaner shutdown, no orphaned subprocesses, no `node-powertools` dep on the boot path. `npx mgr test`'s auto-start path uses the same helper.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Finished the v5.2.3 SendGrid timeout bump that missed 5 sites.** v5.2.3's CHANGELOG claimed "All 9 fetch sites updated" but actually 5 sites in `sendgrid.js` (including `upsertContacts:118` — the hot path used by `Marketing.sync()`) were still using `timeout: 15000`. Confirmed live on Somiibo: a signup with marketing consent granted silently dropped both SendGrid and Beehiiv with `Request timed out` after ~18s. Now every API call in `sendgrid.js` goes through the `SENDGRID_TIMEOUT_MS` (60s) constant. The only remaining literal is the S3 CSV download in `getSegmentContacts:578` (30s — not a SendGrid API call).
|
|
38
|
+
- **`beehiiv.js` timeouts unified under a new `BEEHIIV_TIMEOUT_MS` (60s) constant.** Two hot-path sites (`addSubscriber:71` and the unsub endpoint at `:455`) were still on `timeout: 15000`. Same silent-drop failure mode as SendGrid above. Every Beehiiv API call now uses the constant.
|
|
39
|
+
- **Audited entire BEM codebase for short timeouts. Zero prod-path timeouts under 60s remain.**
|
|
40
|
+
- **Bumped 6 sites from 10-15s → 60s:**
|
|
41
|
+
- `src/manager/libraries/email/validation.js:136` — ZeroBounce mailbox check
|
|
42
|
+
- `src/manager/libraries/email/generators/newsletter.js:520, 549` — parent-server `/newsletter-sources` GET + PUT
|
|
43
|
+
- `src/manager/routes/payments/intent/processors/test.js:163` — test-processor self-fire webhook
|
|
44
|
+
- `src/manager/routes/marketing/email-preferences/post.js:169, 179` — SendGrid ASM suppression POST + DELETE
|
|
45
|
+
- **Bumped 9 sites from 30s → 60s:**
|
|
46
|
+
- `src/manager/libraries/infer-contact.js:46` — PeopleDataLabs AI enrichment
|
|
47
|
+
- `src/manager/libraries/email/providers/sendgrid.js:578` — S3 CSV download for segment exports
|
|
48
|
+
- `src/manager/functions/core/actions/api/user/delete.js:34` + `src/manager/routes/user/delete.js:51` — internal sign-out fan-out
|
|
49
|
+
- `src/manager/events/firestore/payments-webhooks/analytics.js:228, 301` — Facebook + Reddit Conversions API
|
|
50
|
+
- `src/manager/routes/user/oauth2/providers/discord.js:25` + `…/google.js:30` — OAuth token-revoke
|
|
51
|
+
- `src/manager/helpers/analytics.js:331` — itwcw-package-analytics event fire
|
|
52
|
+
- **Bumped 2 sites from 30s → 120s:**
|
|
53
|
+
- `src/manager/events/cron/daily/ghostii-auto-publisher.js:106` — gpt-image-1 hero image generation (regularly 30-60s)
|
|
54
|
+
- `src/manager/events/cron/daily/ghostii-auto-publisher.js:177` — gpt-5+web-search blog post generation (regularly 60-90s)
|
|
55
|
+
- **Audit rule going forward:** any external-API call in BEM prod paths uses **60s minimum** (120s+ for LLM/image generation). Short timeouts are silent-failure machines in serverless code where Cloud Function timeouts already give us a generous outer bound. `_legacy/` files, `src/test/` infra, and `src/cli/` commands keep their existing timeouts — they're not on the request hot path.
|
|
56
|
+
|
|
17
57
|
# [5.2.5] - 2026-05-22
|
|
18
58
|
|
|
19
59
|
### Added
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# TODO: Cancel email missing Order # on non-trial cancels
|
|
2
|
+
|
|
3
|
+
## Symptom
|
|
4
|
+
|
|
5
|
+
Observed 2026-05-23 in `_test.journey-payments-cancel@somiibo.com`'s inbox:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Subject: Your subscription has been cancelled #
|
|
9
|
+
Body: Order #
|
|
10
|
+
Your subscription has been cancelled.
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The `#` after both `Subject` and `Order #` is bare — `order.id` rendered as empty string.
|
|
14
|
+
|
|
15
|
+
The trial-cancel sibling (`_test.journey-payments-trial-cancel@somiibo.com`) on the SAME run got the order ID correctly:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Subject: Your subscription has been cancelled #3794-5306-7041
|
|
19
|
+
Body: Order #3794-5306-7041
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
So this is path-specific, not a template bug.
|
|
23
|
+
|
|
24
|
+
## Root cause (narrowed but not yet proven)
|
|
25
|
+
|
|
26
|
+
Two cancel paths feed the same `subscription-cancelled` transition handler ([src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js:17](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js#L17)):
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
subject: `Your subscription has been cancelled #${order?.id || ''}`,
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Subject + template both render `order.id`. When `order.id` is falsy it falls through to `''`.
|
|
33
|
+
|
|
34
|
+
`order.id` is built in [src/manager/events/firestore/payments-webhooks/on-write.js:241-242](src/manager/events/firestore/payments-webhooks/on-write.js#L241-L242):
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
const order = {
|
|
38
|
+
id: orderId,
|
|
39
|
+
...
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Where `orderId` (line 118) is:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
orderId = library.getOrderId(resource) || passThruOrderId;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`library.getOrderId(resource)` reads the order ID off the processor resource's `meta_data` field. `passThruOrderId` is a PayPal-only fallback from the hosted-page `pass_thru_content`.
|
|
50
|
+
|
|
51
|
+
**Working path** (`journey-payments-trial-cancel`): the trial cancel goes through the `/payments/cancel` endpoint, which already has the `orderId` from `users/{uid}.subscription.payment.orderId`. By the time Stripe/PayPal fires the cancellation webhook back, `meta_data.orderId` on the subscription resource is set, so `library.getOrderId(resource)` returns the order ID. Email shows the ID.
|
|
52
|
+
|
|
53
|
+
**Broken path** (`journey-payments-cancel`): this is the webhook-driven cancel (no `/payments/cancel` endpoint call — `orderDoc.requests.cancellation` is null in the test assertions). The webhook arrives with a subscription resource whose `meta_data.orderId` is empty (or `library.getOrderId(resource)` returns null/empty string). `passThruOrderId` is null because this is Stripe, not PayPal. `orderId` ends up null. `order.id = null`. Email renders `Order #`.
|
|
54
|
+
|
|
55
|
+
## What's still unknown
|
|
56
|
+
|
|
57
|
+
1. **Why is `meta_data.orderId` missing on the Stripe subscription resource at cancel time?** Possibilities:
|
|
58
|
+
- The journey test never set `meta_data` on the subscription (only on the customer or the initial intent).
|
|
59
|
+
- `library.setMetaData` was called during the new-subscription transition but Stripe's subscription resource didn't persist it (Stripe quirk where `metadata` on subscription vs. customer vs. invoice diverge).
|
|
60
|
+
- The `customer.subscription.deleted` event payload doesn't include `metadata` even when it was set — this would be a Stripe-side gotcha.
|
|
61
|
+
2. **Does this affect production?** Possibly — depends on whether real users' Stripe subscription resources have `meta_data.orderId` set. Worth checking one real cancelled subscription in the Stripe dashboard.
|
|
62
|
+
|
|
63
|
+
## Investigation steps
|
|
64
|
+
|
|
65
|
+
1. **Re-run the cancel journey with `TEST_EXTENDED_MODE=true`** and watch the BEM logs:
|
|
66
|
+
```bash
|
|
67
|
+
cd /Users/ian/Developer/Repositories/Somiibo/somiibo-backend/functions
|
|
68
|
+
TEST_EXTENDED_MODE=true npx mgr test events/payments/journey-payments-cancel
|
|
69
|
+
```
|
|
70
|
+
Search the logs around the cancellation webhook for the resolved `orderId` value:
|
|
71
|
+
```bash
|
|
72
|
+
npx mgr logs:read --filter "journey-payments-cancel" --limit 200 | grep -i "orderid\|order id"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. **Inspect the raw webhook payload** stored in `payments-webhooks/{eventId}` for that test run:
|
|
76
|
+
```bash
|
|
77
|
+
npx mgr firestore:query "payments-webhooks" --limit 20 # find the deleted-subscription event
|
|
78
|
+
npx mgr firestore:get "payments-webhooks/<eventId>" # inspect raw.data.object.metadata
|
|
79
|
+
```
|
|
80
|
+
If `raw.data.object.metadata.orderId` is empty → confirms Stripe didn't include it in the event.
|
|
81
|
+
|
|
82
|
+
3. **Check `library.getOrderId(resource)` implementation** for the Stripe library:
|
|
83
|
+
```bash
|
|
84
|
+
grep -n "getOrderId" /Users/ian/Developer/Repositories/ITW-Creative-Works/backend-manager/src/manager/libraries/payment/stripe.js
|
|
85
|
+
```
|
|
86
|
+
Confirm what field path it reads. If it reads only `resource.metadata.orderId`, that's the bug — should also check `resource.metadata.bm_orderId` (some processors have different conventions) or fall through to a Firestore lookup.
|
|
87
|
+
|
|
88
|
+
## Fix options (in order of preference)
|
|
89
|
+
|
|
90
|
+
### Option A — Fall back to userDoc on the transition side (safest)
|
|
91
|
+
|
|
92
|
+
In [subscription-cancelled.js:17](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js#L17), if `order.id` is missing, fall back to `userDoc.subscription.payment.orderId`:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
const orderId = order?.id || userDoc?.subscription?.payment?.orderId || '';
|
|
96
|
+
|
|
97
|
+
sendOrderEmail({
|
|
98
|
+
template: 'core/order/cancelled',
|
|
99
|
+
subject: `Your subscription has been cancelled${orderId ? ` #${orderId}` : ''}`,
|
|
100
|
+
...
|
|
101
|
+
data: {
|
|
102
|
+
order: {
|
|
103
|
+
...order,
|
|
104
|
+
id: orderId,
|
|
105
|
+
...
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is the **safest** fix because it works regardless of whether the bug is in `library.getOrderId`, in Stripe's event payload, or in how `meta_data` was set originally. `userDoc.subscription.payment.orderId` is written when the subscription first activates and reliably has the ID by cancel time.
|
|
112
|
+
|
|
113
|
+
The subject also degrades gracefully — `Your subscription has been cancelled` (no bare `#`) when the ID truly can't be found.
|
|
114
|
+
|
|
115
|
+
### Option B — Fix at the source (`on-write.js`)
|
|
116
|
+
|
|
117
|
+
In [on-write.js:118](src/manager/events/firestore/payments-webhooks/on-write.js#L118), add another fallback after `library.getOrderId`:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
orderId = library.getOrderId(resource)
|
|
121
|
+
|| passThruOrderId
|
|
122
|
+
|| userDoc?.subscription?.payment?.orderId
|
|
123
|
+
|| null;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This fixes it for ALL handlers (not just cancel), and at the cost of a Firestore read of the user doc earlier in `on-write.js` than today. May not need a new read if userDoc is already fetched by this point — confirm by reading the function top-to-bottom.
|
|
127
|
+
|
|
128
|
+
### Option C — Fix `library.getOrderId(resource)` (Stripe library)
|
|
129
|
+
|
|
130
|
+
If investigation step 3 shows the Stripe library is reading the wrong field path, fix it there. Smallest blast radius if the bug is genuinely in one processor's resolver and not a Stripe-event-payload gotcha.
|
|
131
|
+
|
|
132
|
+
## Recommendation
|
|
133
|
+
|
|
134
|
+
Do **A + B together** — A makes the email correct today regardless of root cause; B prevents the same gap from biting any other handler (refund, suspend, etc.). C only if investigation step 3 reveals an actual mis-read.
|
|
135
|
+
|
|
136
|
+
## Regression test
|
|
137
|
+
|
|
138
|
+
Add an assertion to `test/events/payments/journey-payments-cancel.js` that the post-cancellation userDoc still has `subscription.payment.orderId` set AND that the cancel email's `order.id` would render non-empty. Without a regression test, the same bug will silently come back the next time someone refactors `library.getOrderId`.
|
|
139
|
+
|
|
140
|
+
Pseudo-assertion:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
assert.ok(userDoc.subscription?.payment?.orderId, 'userDoc.subscription.payment.orderId should be set after cancellation');
|
|
144
|
+
// And if we capture the email payload (via a test-mode hook in sendOrderEmail), assert order.id !== '' in the rendered data.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
If `sendOrderEmail` doesn't already have a test-mode capture, that's a small enhancement worth doing once — it would let every email-emitting transition assert its rendered payload, not just cancel.
|
|
148
|
+
|
|
149
|
+
## Affected versions
|
|
150
|
+
|
|
151
|
+
Confirmed broken: BEM 5.2.5 (current live on Somiibo as of 2026-05-23). Likely broken in earlier versions too — the `order?.id || ''` template defensive fallback was added explicitly to handle missing IDs, suggesting this gap has been latent for a while. The trial-cancel path masked it because trial cancels go through the endpoint and have `orderId` set in a different code path.
|
|
152
|
+
|
|
153
|
+
## Related files
|
|
154
|
+
|
|
155
|
+
- [src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js](src/manager/events/firestore/payments-webhooks/transitions/subscription/subscription-cancelled.js) — handler with `order?.id || ''` fallback
|
|
156
|
+
- [src/manager/events/firestore/payments-webhooks/on-write.js:241](src/manager/events/firestore/payments-webhooks/on-write.js#L241) — `order` object construction
|
|
157
|
+
- [src/manager/events/firestore/payments-webhooks/on-write.js:118](src/manager/events/firestore/payments-webhooks/on-write.js#L118) — `orderId` resolution from `library.getOrderId` + pass-through fallback
|
|
158
|
+
- [test/events/payments/journey-payments-cancel.js](test/events/payments/journey-payments-cancel.js) — broken-path test (currently passes because it doesn't assert the email payload)
|
|
159
|
+
- [test/events/payments/journey-payments-trial-cancel.js](test/events/payments/journey-payments-trial-cancel.js) — working-path test
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# TODO: Remove legacy BACKEND_MANAGER_KEY acceptance from webhook routes
|
|
2
|
+
|
|
3
|
+
When v5.2.x shipped, the two payment-webhook routes were switched to prefer `BACKEND_MANAGER_WEBHOOK_KEY` but still accept `BACKEND_MANAGER_KEY` as a transitional fallback so existing provider URLs (registered with the old key) keep working until OMEGA re-registers them with the new key.
|
|
4
|
+
|
|
5
|
+
Once every brand's provider URLs (Stripe / PayPal / Chargebee / Chargeblast / Coinbase / etc.) have been re-registered via OMEGA to use `BACKEND_MANAGER_WEBHOOK_KEY`, drop the legacy fallback:
|
|
6
|
+
|
|
7
|
+
## Files to update
|
|
8
|
+
|
|
9
|
+
- `src/manager/routes/payments/webhook/post.js` — line ~28: remove the `|| key === process.env.BACKEND_MANAGER_KEY` half of the validation check.
|
|
10
|
+
- `src/manager/routes/payments/dispute-alert/post.js` — line ~20: same removal. Also update the docstring at line ~11 to drop the "BACKEND_MANAGER_KEY accepted as legacy fallback" note.
|
|
11
|
+
- `docs/stripe-webhook-forwarding.md` — drop the "BACKEND_MANAGER_KEY also accepted as a legacy fallback" parenthetical.
|
|
12
|
+
|
|
13
|
+
## How to verify it's safe
|
|
14
|
+
|
|
15
|
+
Grep production logs (`npx mgr logs:read --fn bm_api --grep "payments/webhook" --limit 1000`) and confirm zero `401 Invalid key` hits over a meaningful window. If there are any, those are providers still on the old URL — re-register them via OMEGA before dropping the fallback.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# TODO: Payment Webhook Key Upgrade — `BACKEND_MANAGER_KEY` → `BACKEND_MANAGER_WEBHOOK_KEY`
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
In BEM v5.2.0 we introduced `BACKEND_MANAGER_WEBHOOK_KEY` as a dedicated env var for third-party webhook authentication, scoped narrowly so it can be rotated independently of the admin key. The marketing routes (`/marketing/webhook`, `/marketing/webhook/forward`) use it correctly.
|
|
6
|
+
|
|
7
|
+
The **payment webhook** (`/payments/webhook`) and **dispute-alert webhook** (`/payments/dispute-alert`) still validate against `BACKEND_MANAGER_KEY` — the general-purpose admin key. This is a security misalignment: a leaked admin key could forge payment webhooks, and rotating the admin key forces every Stripe/PayPal/Chargebee webhook URL to be re-registered.
|
|
8
|
+
|
|
9
|
+
Plan: gradual rollout. Phase 1 (this BEM release) makes the payment routes accept **either** key so existing brand deploys (with only `BACKEND_MANAGER_KEY` set) keep working AND new brands can start using `BACKEND_MANAGER_WEBHOOK_KEY` immediately. Phase 2 (a later BEM release) drops the legacy fallback after every brand has been migrated.
|
|
10
|
+
|
|
11
|
+
## Files needing the dual-key check
|
|
12
|
+
|
|
13
|
+
| File | Line | Current |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| [src/manager/routes/payments/webhook/post.js](src/manager/routes/payments/webhook/post.js) | 28 | `if (!key \|\| key !== process.env.BACKEND_MANAGER_KEY)` |
|
|
16
|
+
| [src/manager/routes/payments/dispute-alert/post.js](src/manager/routes/payments/dispute-alert/post.js) | 20 | `if (!key \|\| key !== process.env.BACKEND_MANAGER_KEY)` |
|
|
17
|
+
| [src/manager/routes/payments/intent/processors/test.js](src/manager/routes/payments/intent/processors/test.js) | 158 | `key=${process.env.BACKEND_MANAGER_KEY}` |
|
|
18
|
+
|
|
19
|
+
## Phase 1 — Dual-key acceptance (this BEM release)
|
|
20
|
+
|
|
21
|
+
### Validation change (webhook + dispute-alert)
|
|
22
|
+
|
|
23
|
+
Replace the single-key check with a dual-key check. Accept the request if `key` matches EITHER `BACKEND_MANAGER_WEBHOOK_KEY` OR the legacy `BACKEND_MANAGER_KEY`.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// Before
|
|
27
|
+
if (!key || key !== process.env.BACKEND_MANAGER_KEY) {
|
|
28
|
+
return assistant.respond('Invalid key', { code: 401 });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// After (Phase 1 — dual-key fallback)
|
|
32
|
+
const validKeys = [
|
|
33
|
+
process.env.BACKEND_MANAGER_WEBHOOK_KEY,
|
|
34
|
+
process.env.BACKEND_MANAGER_KEY, // legacy — remove in Phase 2
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
|
|
37
|
+
if (!key || !validKeys.includes(key)) {
|
|
38
|
+
return assistant.respond('Invalid key', { code: 401 });
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Notes:
|
|
43
|
+
- `.filter(Boolean)` matters — if a brand hasn't set `BACKEND_MANAGER_WEBHOOK_KEY` yet, `validKeys` becomes `[BACKEND_MANAGER_KEY]` (single-element). If a brand HAS set it, both are accepted.
|
|
44
|
+
- If BOTH env vars are unset (misconfigured brand), `validKeys` is `[]` and every request 401s. That's the right behavior — explicit failure beats silent acceptance.
|
|
45
|
+
- Update the route header comments (lines 11 and 27 of dispute-alert, 27 of webhook) to mention both keys.
|
|
46
|
+
|
|
47
|
+
### Test processor self-fire URL
|
|
48
|
+
|
|
49
|
+
Switch [src/manager/routes/payments/intent/processors/test.js:158](src/manager/routes/payments/intent/processors/test.js#L158) to PREFER `BACKEND_MANAGER_WEBHOOK_KEY` and fall back to `BACKEND_MANAGER_KEY`:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const webhookKey = process.env.BACKEND_MANAGER_WEBHOOK_KEY || process.env.BACKEND_MANAGER_KEY;
|
|
53
|
+
const webhookUrl = `${assistant.Manager.project.apiUrl}/backend-manager/payments/webhook?processor=test&key=${webhookKey}`;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This way the test processor exercises the new key when it's set, and falls back to the legacy key on brands that haven't migrated yet.
|
|
57
|
+
|
|
58
|
+
### Test infrastructure updates
|
|
59
|
+
|
|
60
|
+
**`src/test/utils/http-client.js`** — add `backendManagerWebhookKey` field alongside the existing `backendManagerKey`.
|
|
61
|
+
|
|
62
|
+
**`src/test/runner.js`** — thread `backendManagerWebhookKey` through the runner context the same way `backendManagerKey` is threaded today (3 sites).
|
|
63
|
+
|
|
64
|
+
**`src/cli/commands/test.js`** — read `BACKEND_MANAGER_WEBHOOK_KEY` from env (parallel to the existing `BACKEND_MANAGER_KEY` read), pass it through to the runner, fall back to the admin key if unset.
|
|
65
|
+
|
|
66
|
+
**Route + journey tests** — update test fixtures to prefer `BACKEND_MANAGER_WEBHOOK_KEY` when set:
|
|
67
|
+
- `test/routes/payments/webhook.js` — all `${config.backendManagerKey}` interpolations in `payments/webhook?...&key=` URLs
|
|
68
|
+
- `test/routes/payments/dispute-alert.js` — same (13 sites)
|
|
69
|
+
- `test/events/payments/journey-*.js` — only the `payments/webhook?...&key=` URLs (NOT the admin-auth uses elsewhere in journey tests)
|
|
70
|
+
|
|
71
|
+
Use `config.backendManagerWebhookKey || config.backendManagerKey` everywhere so both keys are exercised.
|
|
72
|
+
|
|
73
|
+
### Docs
|
|
74
|
+
|
|
75
|
+
- **`docs/stripe-webhook-forwarding.md`** — line 7 + 18 — note the dual-key acceptance, recommend `BACKEND_MANAGER_WEBHOOK_KEY` for new setups, note legacy `BACKEND_MANAGER_KEY` is still accepted for backwards compatibility.
|
|
76
|
+
- **`CHANGELOG.md`** — entry under the next version. Categorize under `Changed` (NOT `BREAKING` — that's reserved for Phase 2). Spell out: "Payment webhook + dispute-alert routes now accept either `BACKEND_MANAGER_WEBHOOK_KEY` (preferred) or `BACKEND_MANAGER_KEY` (legacy). Set `BACKEND_MANAGER_WEBHOOK_KEY` in every consumer brand's `.env` and run OMEGA payment ensure before Phase 2 ships."
|
|
77
|
+
- **`templates/_.env`** — already declares both keys, no change.
|
|
78
|
+
|
|
79
|
+
### Verification (manual)
|
|
80
|
+
|
|
81
|
+
1. **Local test (no `BACKEND_MANAGER_WEBHOOK_KEY` set)** — confirm legacy fallback still passes:
|
|
82
|
+
```bash
|
|
83
|
+
npx mgr test routes/payments/webhook routes/payments/dispute-alert events/payments
|
|
84
|
+
```
|
|
85
|
+
Expect green.
|
|
86
|
+
|
|
87
|
+
2. **Local test (`BACKEND_MANAGER_WEBHOOK_KEY` set to a different value)** — confirm the new key is accepted AND the legacy key still works:
|
|
88
|
+
```bash
|
|
89
|
+
BACKEND_MANAGER_WEBHOOK_KEY=test-webhook-key npx mgr test routes/payments/webhook routes/payments/dispute-alert events/payments
|
|
90
|
+
```
|
|
91
|
+
Expect green.
|
|
92
|
+
|
|
93
|
+
3. **Live spot-check on one brand (Somiibo)** — deploy this BEM release to Somiibo (whose `.env` currently only has `BACKEND_MANAGER_KEY` set OR has both), trigger a Stripe test event, confirm the webhook doc lands in Firestore.
|
|
94
|
+
|
|
95
|
+
## Phase 2 — Drop legacy fallback (future BEM release)
|
|
96
|
+
|
|
97
|
+
**Do NOT do this until every consumer brand has been migrated and verified.** Tracking checklist for migration readiness:
|
|
98
|
+
|
|
99
|
+
- [ ] Every consumer `.env` has `BACKEND_MANAGER_WEBHOOK_KEY` set (spot-check via OMEGA across all brands)
|
|
100
|
+
- [ ] OMEGA `payment/ensure/{stripe,paypal,chargebee}-webhook.js` updated to use `BACKEND_MANAGER_WEBHOOK_KEY` when constructing webhook URLs (see "OMEGA changes" below)
|
|
101
|
+
- [ ] OMEGA `--service payment` run across every brand to re-register webhook URLs at Stripe/PayPal/Chargebee with the new key
|
|
102
|
+
- [ ] Stale webhook URLs (the old ones with `key=BACKEND_MANAGER_KEY`) deleted from each provider — these will start 401-ing the moment Phase 2 ships
|
|
103
|
+
- [ ] At least one live billing cycle (Stripe charge, refund, dispute) observed working end-to-end on the new key for at least 3 brands
|
|
104
|
+
|
|
105
|
+
When all boxes are checked, Phase 2 ships as a **breaking change**:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
// Phase 2 — single-key (legacy removed)
|
|
109
|
+
if (!key || key !== process.env.BACKEND_MANAGER_WEBHOOK_KEY) {
|
|
110
|
+
return assistant.respond('Invalid key', { code: 401 });
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Phase 2 CHANGELOG entry goes under `BREAKING`.
|
|
115
|
+
|
|
116
|
+
## OMEGA changes (separate repo, needed for Phase 2 prep)
|
|
117
|
+
|
|
118
|
+
Touched files (do NOT do these in Phase 1 — they're for Phase 2 prep):
|
|
119
|
+
|
|
120
|
+
| File | Change |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `/Users/ian/Developer/Repositories/ITW-Creative-Works/omega-manager/src/services/payment/ensure/stripe-webhook.js` | Line 38: swap `BACKEND_MANAGER_KEY` → `BACKEND_MANAGER_WEBHOOK_KEY`. Line 76 check + line 77 message + line 64 doc comment: update env var name. |
|
|
123
|
+
| `/Users/ian/Developer/Repositories/ITW-Creative-Works/omega-manager/src/services/payment/ensure/paypal-webhook.js` | Same change at lines 40, 66, 77, 78, 79. |
|
|
124
|
+
| `/Users/ian/Developer/Repositories/ITW-Creative-Works/omega-manager/src/services/payment/ensure/chargebee-webhook.js` | Same change at lines 34, 45, 57, 58, 59. |
|
|
125
|
+
|
|
126
|
+
**Idempotent re-registration + stale cleanup:** Each ensure file looks up existing webhooks **by URL**. Since the URL will change (the `key=` query param differs), the existing webhook will NOT match and a new one will be created. The OLD webhook (with the admin key) needs to be cleaned up — otherwise the third party will deliver to both and the old one will start failing 401 once Phase 2 ships.
|
|
127
|
+
|
|
128
|
+
Cleanup approach in each ensure file: after creating the new webhook, scan for any other webhook whose URL points at `/payments/webhook?processor={name}` (regardless of key) and delete the stale ones. Match on path prefix, not full URL. Stripe + PayPal + Chargebee all support webhook deletion via the same API methods these files already use for listing.
|
|
129
|
+
|
|
130
|
+
Helper: extract a `pathMatchesOurProcessor(url, processor)` in each ensure file — strip query string, match the path-and-processor segment.
|
|
131
|
+
|
|
132
|
+
## Known constraints / gotchas
|
|
133
|
+
|
|
134
|
+
1. **Phase 1 is non-breaking.** Any consumer can deploy this BEM release without touching their `.env` — the legacy key still works.
|
|
135
|
+
2. **`BACKEND_MANAGER_KEY` continues to grant admin via `assistant.authenticate()`.** That hasn't changed; it's still the admin secret for internal API calls (newsletter, send-email, user/delete, ghostii cron, electron-client, oauth2 state encryption). This TODO is *only* about scoping webhook endpoints to a separate key.
|
|
136
|
+
3. **`UNSUBSCRIBE_HMAC_KEY` is unrelated** and stays separate.
|
|
137
|
+
4. **Admin grant is NOT triggered by the webhook routes.** Middleware auto-runs `assistant.authenticate()` via `Usage.init()`, which reads `data.backendManagerKey` from the request body. The three webhook routes get their key via `?key=` query string (NOT body), and their request bodies are third-party payloads (Stripe events, PayPal events, Chargeblast alerts) that can't contain the real admin secret. So switching the routes' validation does NOT remove any admin-grant the handlers depend on — they write to Firestore via `libraries.admin` (Admin SDK, always full access) anyway.
|
|
138
|
+
5. **Test processor self-fire** must use the same key the BEM endpoint accepts, which is why the test processor URL also gets updated in Phase 1.
|
|
@@ -4,7 +4,7 @@ BEM auto-starts Stripe CLI webhook forwarding when running `npx mgr serve` or `n
|
|
|
4
4
|
|
|
5
5
|
**Requirements:**
|
|
6
6
|
- `STRIPE_SECRET_KEY` set in `functions/.env`
|
|
7
|
-
- `
|
|
7
|
+
- `BACKEND_MANAGER_WEBHOOK_KEY` set in `functions/.env` (`BACKEND_MANAGER_KEY` also accepted as a legacy fallback)
|
|
8
8
|
- [Stripe CLI](https://stripe.com/docs/stripe-cli) installed
|
|
9
9
|
|
|
10
10
|
**Standalone usage:**
|
|
@@ -15,4 +15,4 @@ npx mgr stripe
|
|
|
15
15
|
|
|
16
16
|
If any prerequisite is missing, webhook forwarding is silently skipped with an info message.
|
|
17
17
|
|
|
18
|
-
The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/webhook?processor=stripe&key={
|
|
18
|
+
The forwarding URL is: `http://localhost:{hostingPort}/backend-manager/payments/webhook?processor=stripe&key={BACKEND_MANAGER_WEBHOOK_KEY}`
|
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Test Helper — direct provider lookups + removals via API
|
|
5
|
+
*
|
|
6
|
+
* Lets the live-test checklist verify SendGrid + Beehiiv state without
|
|
7
|
+
* clicking around in dashboards. Reuses the same provider helpers that
|
|
8
|
+
* BEM uses in production (findContact / removeContact).
|
|
9
|
+
*
|
|
10
|
+
* Usage (run from a consumer project's functions/ dir):
|
|
11
|
+
*
|
|
12
|
+
* node ../../../backend-manager/scripts/test-helper-providers.js find user@example.com
|
|
13
|
+
* node ../../../backend-manager/scripts/test-helper-providers.js purge user@example.com
|
|
14
|
+
*
|
|
15
|
+
* Or symlink: `ln -s ../../../backend-manager/scripts/test-helper-providers.js ./prov`
|
|
16
|
+
* then: `node ./prov find user@example.com`
|
|
17
|
+
*
|
|
18
|
+
* - find: prints whether the contact exists in SendGrid + Beehiiv (and the data shape).
|
|
19
|
+
* - purge: removes the contact from BOTH providers (idempotent; safe if absent).
|
|
20
|
+
*
|
|
21
|
+
* The script picks up:
|
|
22
|
+
* - SENDGRID_API_KEY, BEEHIIV_API_KEY from <cwd>/.env (functions/.env)
|
|
23
|
+
* - marketing.sendgrid.listId, marketing.beehiiv.publicationId from <cwd>/backend-manager-config.json
|
|
24
|
+
* - service-account from <cwd>/service-account.json
|
|
25
|
+
*
|
|
26
|
+
* Exit codes: 0 ok, 1 usage error, 2 provider error.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const envPath = path.join(cwd, '.env');
|
|
34
|
+
const configPath = path.join(cwd, 'backend-manager-config.json');
|
|
35
|
+
const serviceAccountPath = path.join(cwd, 'service-account.json');
|
|
36
|
+
|
|
37
|
+
// --- sanity checks ---
|
|
38
|
+
if (!fs.existsSync(envPath)) {
|
|
39
|
+
console.error(`✗ Missing ${envPath} — run from the consumer's functions/ dir`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(configPath)) {
|
|
43
|
+
console.error(`✗ Missing ${configPath} — run from the consumer's functions/ dir`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (!fs.existsSync(serviceAccountPath)) {
|
|
47
|
+
console.error(`✗ Missing ${serviceAccountPath} — run from the consumer's functions/ dir`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- parse args ---
|
|
52
|
+
const [, , cmd, email] = process.argv;
|
|
53
|
+
const VALID_CMDS = ['find', 'purge'];
|
|
54
|
+
|
|
55
|
+
if (!cmd || !VALID_CMDS.includes(cmd) || !email) {
|
|
56
|
+
console.error('Usage:');
|
|
57
|
+
console.error(' node test-helper-providers.js find <email>');
|
|
58
|
+
console.error(' node test-helper-providers.js purge <email>');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- bootstrap env (dotenv style, minimal) ---
|
|
63
|
+
require('dotenv').config({ path: envPath });
|
|
64
|
+
|
|
65
|
+
if (!process.env.SENDGRID_API_KEY) {
|
|
66
|
+
console.error('✗ SENDGRID_API_KEY not set in .env');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
if (!process.env.BEEHIIV_API_KEY) {
|
|
70
|
+
console.error('✗ BEEHIIV_API_KEY not set in .env');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- bootstrap Manager so providers can read Manager.config.marketing.* ---
|
|
75
|
+
// The providers require '../../../index.js' which is the Manager singleton.
|
|
76
|
+
// We need to load BEM from the consumer's node_modules (not the BEM repo's own src)
|
|
77
|
+
// so it picks up the consumer's config + service account.
|
|
78
|
+
let Manager;
|
|
79
|
+
try {
|
|
80
|
+
const BackendManager = require(path.join(cwd, 'node_modules', 'backend-manager'));
|
|
81
|
+
Manager = (new BackendManager()).init({}, { setupFunctionsLegacy: false, log: false });
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('✗ Failed to bootstrap Manager from consumer node_modules:', e.message);
|
|
84
|
+
process.exit(2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- load providers via the BEM Manager.libraries surface (preferred) or direct path ---
|
|
88
|
+
const sendgridProviderPath = path.join(cwd, 'node_modules', 'backend-manager', 'src', 'manager', 'libraries', 'email', 'providers', 'sendgrid.js');
|
|
89
|
+
const beehiivProviderPath = path.join(cwd, 'node_modules', 'backend-manager', 'src', 'manager', 'libraries', 'email', 'providers', 'beehiiv.js');
|
|
90
|
+
|
|
91
|
+
const sendgrid = require(sendgridProviderPath);
|
|
92
|
+
const beehiiv = require(beehiivProviderPath);
|
|
93
|
+
|
|
94
|
+
// --- commands ---
|
|
95
|
+
async function find(email) {
|
|
96
|
+
console.log(`Looking up ${email} on both providers...`);
|
|
97
|
+
|
|
98
|
+
const [sgContact, bhContact] = await Promise.all([
|
|
99
|
+
sendgrid.findContact(email).catch(e => ({ _error: e.message })),
|
|
100
|
+
beehiiv.findContact(email).catch(e => ({ _error: e.message })),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
console.log('\n── SendGrid ──');
|
|
104
|
+
if (sgContact?._error) {
|
|
105
|
+
console.log(` ✗ error: ${sgContact._error}`);
|
|
106
|
+
} else if (!sgContact) {
|
|
107
|
+
console.log(' ⊘ not found');
|
|
108
|
+
} else {
|
|
109
|
+
console.log(` ✓ found — id=${sgContact.id || '?'}, email=${sgContact.email || '?'}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log('\n── Beehiiv ──');
|
|
113
|
+
if (bhContact?._error) {
|
|
114
|
+
console.log(` ✗ error: ${bhContact._error}`);
|
|
115
|
+
} else if (!bhContact) {
|
|
116
|
+
console.log(' ⊘ not found');
|
|
117
|
+
} else {
|
|
118
|
+
console.log(` ✓ found — id=${bhContact.id || '?'}, email=${bhContact.email || '?'}, status=${bhContact.status || '?'}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { sgContact, bhContact };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function purge(email) {
|
|
125
|
+
console.log(`Removing ${email} from both providers...`);
|
|
126
|
+
|
|
127
|
+
const [sgResult, bhResult] = await Promise.all([
|
|
128
|
+
sendgrid.removeContact(email).catch(e => ({ _error: e.message })),
|
|
129
|
+
beehiiv.removeContact(email).catch(e => ({ _error: e.message })),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
console.log('\n── SendGrid ──');
|
|
133
|
+
if (sgResult?._error) {
|
|
134
|
+
console.log(` ✗ error: ${sgResult._error}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(` ✓ ${JSON.stringify(sgResult)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('\n── Beehiiv ──');
|
|
140
|
+
if (bhResult?._error) {
|
|
141
|
+
console.log(` ✗ error: ${bhResult._error}`);
|
|
142
|
+
} else {
|
|
143
|
+
console.log(` ✓ ${JSON.stringify(bhResult)}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { sgResult, bhResult };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- main ---
|
|
150
|
+
(async () => {
|
|
151
|
+
try {
|
|
152
|
+
if (cmd === 'find') {
|
|
153
|
+
await find(email);
|
|
154
|
+
} else if (cmd === 'purge') {
|
|
155
|
+
await purge(email);
|
|
156
|
+
}
|
|
157
|
+
process.exit(0);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error('✗ Unexpected error:', e);
|
|
160
|
+
process.exit(2);
|
|
161
|
+
}
|
|
162
|
+
})();
|
|
@@ -236,7 +236,7 @@ class BaseCommand {
|
|
|
236
236
|
this.log(chalk.gray(' (Stripe webhook forwarding is currently disabled - coming soon!)\n'));
|
|
237
237
|
return null;
|
|
238
238
|
|
|
239
|
-
// Load .env so STRIPE_SECRET_KEY and
|
|
239
|
+
// Load .env so STRIPE_SECRET_KEY and BACKEND_MANAGER_WEBHOOK_KEY are available
|
|
240
240
|
const envPath = path.join(functionsDir, '.env');
|
|
241
241
|
if (jetpack.exists(envPath)) {
|
|
242
242
|
require('dotenv').config({ path: envPath, quiet: true });
|
|
@@ -248,9 +248,9 @@ class BaseCommand {
|
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
// Check for Backend Manager key
|
|
252
|
-
if (!process.env.
|
|
253
|
-
this.log(chalk.gray(' (Stripe webhook forwarding disabled -
|
|
251
|
+
// Check for Backend Manager webhook key
|
|
252
|
+
if (!process.env.BACKEND_MANAGER_WEBHOOK_KEY) {
|
|
253
|
+
this.log(chalk.gray(' (Stripe webhook forwarding disabled - BACKEND_MANAGER_WEBHOOK_KEY not set in .env)\n'));
|
|
254
254
|
return null;
|
|
255
255
|
}
|
|
256
256
|
|
|
@@ -276,7 +276,7 @@ class BaseCommand {
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
const forwardUrl = `http://localhost:${hostingPort}/backend-manager/payments/webhook?processor=stripe&key=${process.env.
|
|
279
|
+
const forwardUrl = `http://localhost:${hostingPort}/backend-manager/payments/webhook?processor=stripe&key=${process.env.BACKEND_MANAGER_WEBHOOK_KEY}`;
|
|
280
280
|
|
|
281
281
|
this.log(chalk.gray(` Stripe webhook forwarding -> localhost:${hostingPort}\n`));
|
|
282
282
|
|