Changed verification flow to support webhook mode behind labs flag#26778
Changed verification flow to support webhook mode behind labs flag#26778
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a webhook-based escalation path for host email verification and centralizes email sending into a shared helper. Introduces VerificationWebhookService that posts JSON payloads with optional HMAC-SHA256 signing, timeout, and retries. VerificationTrigger now computes an effective threshold and method, prefers sending a verification webhook when enabled, catches webhook errors, and falls back to the legacy email flow. Unit and e2e tests were added/updated to cover webhook and lab-state behavior. 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
ghost/core/core/server/services/verification/verification-webhook-service.ts
Show resolved
Hide resolved
b1ed383 to
6a50ce8
Compare
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
120c513 to
0786920
Compare
🤖 Velo CI Failure AnalysisClassification: 🟠 SOFT FAIL
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
ghost/core/core/server/services/verification/verification-webhook-service.ts (1)
41-94: Consider adding error handling for the HTTP request.The
sendVerificationWebhookmethod awaits the request without error handling. If the webhook endpoint is unavailable or returns an error, this will propagate up and could potentially interfere with the verification flow completion (e.g., prevent settingemail_verification_required).The email path in
sendVerificationEmailalso doesn't have explicit error handling, so this may be intentional for consistency. However, the webhook retry logic (5 retries) could cause delays if the endpoint is down.Consider whether:
- The current behavior (letting errors propagate) is acceptable, OR
- A try/catch with logging would be more appropriate to ensure the verification flag is always set
♻️ Optional: Add error handling with logging
this.#logging.info(`Triggering verification webhook to "${webhookUrl}"`); - await this.#request(webhookUrl, requestOptions); + try { + await this.#request(webhookUrl, requestOptions); + } catch (error) { + this.#logging.error?.(`Failed to send verification webhook: ${error.message || error}`); + throw error; // Re-throw to maintain existing behavior if needed + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/core/server/services/verification/verification-webhook-service.ts` around lines 41 - 94, sendVerificationWebhook currently awaits this.#request without handling failures, which can block verification flow; wrap the await this.#request(webhookUrl, requestOptions) call in a try/catch, log failures using this.#logging.error including webhookUrl, requestOptions.headers['X-Ghost-Request-Timestamp'] (or timestamp) and the caught error, and do not rethrow so verification completion isn't blocked; optionally mirror the same pattern used by sendVerificationEmail for consistency or make behavior configurable via a flag if propagation should be preserved in some environments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@ghost/core/core/server/services/verification/verification-webhook-service.ts`:
- Around line 41-94: sendVerificationWebhook currently awaits this.#request
without handling failures, which can block verification flow; wrap the await
this.#request(webhookUrl, requestOptions) call in a try/catch, log failures
using this.#logging.error including webhookUrl,
requestOptions.headers['X-Ghost-Request-Timestamp'] (or timestamp) and the
caught error, and do not rethrow so verification completion isn't blocked;
optionally mirror the same pattern used by sendVerificationEmail for consistency
or make behavior configurable via a flag if propagation should be preserved in
some environments.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 79161b5d-4cf8-4d0e-bf37-3ddcdef18c5d
📒 Files selected for processing (6)
ghost/core/core/server/services/members/service.jsghost/core/core/server/services/verification-trigger.jsghost/core/core/server/services/verification/verification-webhook-service.tsghost/core/test/e2e-api/admin/members.test.jsghost/core/test/legacy/api/admin/members-importer.test.jsghost/core/test/unit/server/services/verification-trigger.test.js
d0a9f98 to
fb0eb46
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ghost/core/core/server/services/verification-trigger.js`:
- Around line 206-216: The site is marked as requiring verification before the
webhook is sent, which can leave the site stuck if webhook delivery fails;
change the flow so you only set email_verification_required after a confirmed
successful webhook send (or alternately gate the webhook branch on a positive
"webhook configured" check). Specifically, in verification-trigger.js, call
this._shouldUseWebhookFlow() and if true, await
this._sendVerificationWebhook({amountTriggered: amount, threshold: threshold ??
amount, method: method ?? source ?? 'import'}) and only when that promise
resolves successfully then call this._Settings.edit(...) to set key:
'email_verification_required'; on webhook failure do not change the setting and
surface/log the error so the legacy email path remains available; you can also
add an explicit "webhook configured" check used to decide whether to attempt the
webhook branch before touching the setting.
In
`@ghost/core/core/server/services/verification/verification-webhook-service.ts`:
- Around line 53-64: The code sets webhookType =
this.#config.get('hostSettings:emailVerification:webhookType') ||
'not_set_in_config', which masks a missing/empty config and prevents the
subsequent guard in verification-webhook-service.ts from bailing out; change the
logic in the webhookType retrieval (or the guard) so a missing or empty
webhookType is treated as unconfigured (e.g., remove the 'not_set_in_config'
default and/or check for falsy/empty string in the existing if (!webhookType ||
typeof webhookType !== 'string') return) so the service returns early instead of
emitting type: "not_set_in_config".
- Around line 89-102: The requestOptions object in
verification-webhook-service.ts is missing an explicit HTTP method; update the
requestOptions declaration (the one passed to this.#request) to include method:
'POST' so the webhook is sent as a POST with the provided body; ensure the
method property is added alongside body, headers, timeout, and retry before
calling this.#request(webhookUrl, requestOptions).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 879dcc8d-042e-4d33-82a0-5654f0eed488
📒 Files selected for processing (6)
ghost/core/core/server/services/members/service.jsghost/core/core/server/services/verification-trigger.jsghost/core/core/server/services/verification/verification-webhook-service.tsghost/core/test/e2e-api/admin/members.test.jsghost/core/test/legacy/api/admin/members-importer.test.jsghost/core/test/unit/server/services/verification-trigger.test.js
ghost/core/core/server/services/verification/verification-webhook-service.ts
Outdated
Show resolved
Hide resolved
ghost/core/core/server/services/verification/verification-webhook-service.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (2)
ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts (2)
68-105: Good coverage of the happy path and signature verification.The test correctly captures the request parameters, validates the payload structure, and recomputes the HMAC signature using the captured timestamp—ensuring the signature logic is verified end-to-end.
For completeness, consider adding:
- A test for request failure (verify the error is thrown/propagated).
- A test for empty
webhookSecret(verify noX-Ghost-Signatureheader is set).,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts` around lines 68 - 105, Add two additional unit tests for createService(). First, add a test that simulates the request function throwing an error (e.g., request rejects) and assert that sendVerificationWebhook propagates/throws that error (reference: createService and sendVerificationWebhook). Second, add a test where the webhookSecret is empty/undefined in the service config, call sendVerificationWebhook and assert that the request headers do not include 'X-Ghost-Signature' (reference: requestOptions.headers and the signature generation flow in sendVerificationWebhook). Ensure both tests reuse the existing pattern of injecting a custom request implementation and validating requestUrl, requestOptions, and exceptions as appropriate.
33-66: Consider adding a parallel test for missingwebhookUrl.This test covers the
webhookTypemissing branch, but the source code also has an early return whenwebhookUrlis missing or empty. Adding a similar test for that branch would improve coverage symmetry.📝 Example test to add
it('returns false when webhookUrl is missing', async function () { let warnCalled = false; const service = createService({ config: { get: (key: string) => { if (key === 'hostSettings:emailVerification:webhookUrl') { return ''; } if (key === 'hostSettings:emailVerification:webhookType') { return 'member_import_limit_breach'; } return null; } }, logging: { info: () => {}, warn: () => { warnCalled = true; }, error: () => {} } }); const result = await service.sendVerificationWebhook({ amountTriggered: 1001, threshold: 1000, method: 'import' }); assert.equal(result, false); assert.equal(warnCalled, true); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts` around lines 33 - 66, Add a parallel unit test that mirrors the existing "returns false when webhookType is missing" case but exercises the early-return branch when the webhookUrl is empty: create a service via createService with config.get returning '' for 'hostSettings:emailVerification:webhookUrl' and a valid 'hostSettings:emailVerification:webhookType' (e.g. 'member_import_limit_breach'), stub logging.warn to set a flag, call sendVerificationWebhook with the same payload, and assert the result is false and that warn was called.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts`:
- Around line 68-105: Add two additional unit tests for createService(). First,
add a test that simulates the request function throwing an error (e.g., request
rejects) and assert that sendVerificationWebhook propagates/throws that error
(reference: createService and sendVerificationWebhook). Second, add a test where
the webhookSecret is empty/undefined in the service config, call
sendVerificationWebhook and assert that the request headers do not include
'X-Ghost-Signature' (reference: requestOptions.headers and the signature
generation flow in sendVerificationWebhook). Ensure both tests reuse the
existing pattern of injecting a custom request implementation and validating
requestUrl, requestOptions, and exceptions as appropriate.
- Around line 33-66: Add a parallel unit test that mirrors the existing "returns
false when webhookType is missing" case but exercises the early-return branch
when the webhookUrl is empty: create a service via createService with config.get
returning '' for 'hostSettings:emailVerification:webhookUrl' and a valid
'hostSettings:emailVerification:webhookType' (e.g.
'member_import_limit_breach'), stub logging.warn to set a flag, call
sendVerificationWebhook with the same payload, and assert the result is false
and that warn was called.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 2b5cd5eb-31d4-4510-8a88-00e1d56cc78f
📒 Files selected for processing (4)
ghost/core/core/server/services/verification-trigger.jsghost/core/core/server/services/verification/verification-webhook-service.tsghost/core/test/unit/server/services/verification-trigger.test.jsghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts
✅ Files skipped from review due to trivial changes (1)
- ghost/core/test/unit/server/services/verification-trigger.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
- ghost/core/core/server/services/verification-trigger.js
- ghost/core/core/server/services/verification/verification-webhook-service.ts
f81eaf8 to
0016a1a
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ghost/core/core/server/services/verification-trigger.js`:
- Around line 249-266: The code assumes _sendVerificationWebhook resolves false
on failure but that method can throw transport errors; wrap the call to
this._sendVerificationWebhook inside a try/catch so any exception is treated as
a non-delivery (set webhookWasSent = false), optionally log the caught error,
and then continue to call _startLegacyEmailVerificationProcess when
webhookWasSent is falsy; keep the existing flow that still marks verification
required and returns via _finishTrigger only when the webhook truly succeeded
(i.e., webhookWasSent === true), referencing the methods _shouldUseWebhookFlow,
_sendVerificationWebhook, _markVerificationRequired, _finishTrigger, and
_startLegacyEmailVerificationProcess.
In
`@ghost/core/core/server/services/verification/verification-webhook-service.ts`:
- Around line 103-110: The logs currently emit the raw webhookUrl (used in the
verification send flow) which can leak credentials; update the code around the
send in the verification webhook method to stop logging the full URL: replace
direct uses of webhookUrl in this.#logging.info and this.#logging.error with a
sanitized value (e.g. parse the URL and log only the origin/hostname or a masked
version that strips query, path, and credentials) by adding a small helper like
sanitizeWebhookUrl(...) and use that sanitized string when calling
this.#logging.info and this.#logging.error while leaving the actual call to
this.#request(webhookUrl, requestOptions) unchanged so delivery still uses the
full URL.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6b873e0c-f665-479b-8b99-b1fe3e915c5f
📒 Files selected for processing (7)
ghost/core/core/server/services/members/service.jsghost/core/core/server/services/verification-trigger.jsghost/core/core/server/services/verification/verification-webhook-service.tsghost/core/test/e2e-api/admin/members.test.jsghost/core/test/legacy/api/admin/members-importer.test.jsghost/core/test/unit/server/services/verification-trigger.test.jsghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts
✅ Files skipped from review due to trivial changes (2)
- ghost/core/test/unit/server/services/verification-trigger.test.js
- ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- ghost/core/test/legacy/api/admin/members-importer.test.js
ghost/core/core/server/services/verification/verification-webhook-service.ts
Outdated
Show resolved
Hide resolved
0c453cb to
eba9d0d
Compare
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ghost/core/test/e2e-api/admin/members.test.js (1)
894-961:⚠️ Potential issue | 🟠 MajorAlways restore test-global state in
finally.These two cases mutate global config/settings and create members, but cleanup only runs on the happy path. If anything fails before Line 959 or Line 1057, later tests inherit the modified
hostSettings,email_verification_required, and extra members.♻️ Suggested structure
- configUtils.set('hostSettings:emailVerification', { - ... - }); - - ... - - // state cleanup - await agent.delete(`/members/${memberPassVerification.id}`); - await agent.delete(`/members/${memberFailVerification.id}`); - - await configUtils.restore(); - settingsCache.set('email_verification_required', {value: false}); + let memberPassVerification; + let memberFailVerification; + + try { + configUtils.set('hostSettings:emailVerification', { + ... + }); + + ... + + memberPassVerification = passBody.members[0]; + ... + memberFailVerification = failBody.members[0]; + ... + } finally { + if (memberPassVerification) { + await agent.delete(`/members/${memberPassVerification.id}`); + } + + if (memberFailVerification) { + await agent.delete(`/members/${memberFailVerification.id}`); + } + + await configUtils.restore(); + settingsCache.set('email_verification_required', {value: false}); + }Also applies to: 963-1059
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/test/e2e-api/admin/members.test.js` around lines 894 - 961, The test mutates global state and creates members without guaranteeing cleanup; wrap the main test steps in a try/finally so configUtils.set(...), mockManager.mockLabsDisabled('verificationFlow'), and created members (the responses stored in memberPassVerification and memberFailVerification from agent.post(`/members/`)) are always reverted: in finally call configUtils.restore(), reset settingsCache.set('email_verification_required', {value: false}) and delete any created members via agent.delete(`/members/${id}`) if those variables are populated; keep assertions (e.g., mockManager.assert.sentEmail) inside the try block and ensure DomainEvents.allSettled() is awaited before cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@ghost/core/test/e2e-api/admin/members.test.js`:
- Around line 894-961: The test mutates global state and creates members without
guaranteeing cleanup; wrap the main test steps in a try/finally so
configUtils.set(...), mockManager.mockLabsDisabled('verificationFlow'), and
created members (the responses stored in memberPassVerification and
memberFailVerification from agent.post(`/members/`)) are always reverted: in
finally call configUtils.restore(), reset
settingsCache.set('email_verification_required', {value: false}) and delete any
created members via agent.delete(`/members/${id}`) if those variables are
populated; keep assertions (e.g., mockManager.assert.sentEmail) inside the try
block and ensure DomainEvents.allSettled() is awaited before cleanup.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e58083a6-713b-4966-85da-04805321bae8
📒 Files selected for processing (7)
ghost/core/core/server/services/members/service.jsghost/core/core/server/services/verification-trigger.jsghost/core/core/server/services/verification/verification-webhook-service.tsghost/core/test/e2e-api/admin/members.test.jsghost/core/test/legacy/api/admin/members-importer.test.jsghost/core/test/unit/server/services/verification-trigger.test.jsghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- ghost/core/test/legacy/api/admin/members-importer.test.js
- ghost/core/test/unit/server/services/verification/verification-webhook-service.test.ts
- ghost/core/test/unit/server/services/verification-trigger.test.js
ba27776 to
f6bb300
Compare
fe1687e to
591fbc7
Compare
closes [GVA-729](https://linear.app/ghost/issue/GVA-729/add-feature-flag-verification-flow) Added the private Labs flag �0verificationFlow�0 to Ghost core allowlists and exposed it in Admin X private Labs settings so it can be toggled safely during rollout, with the admin config API snapshot updated to reflect the new flag in public config responses.
closes [GVA-730](https://linear.app/ghost/issue/GVA-730/add-webhook-changes-behind-feature-flag) This adds the webhook-based verification escalation flow behind the verificationFlow flag while keeping the existing email path in parallel until rollout is complete. The new path sends signed verification payloads with explicit POST requests, treats missing webhook config as unconfigured, keeps the temporary legacy fallback isolated for easy removal at GA, and validates both flows with unit and acceptance coverage using placeholder webhook fixtures rather than real integration details.
…` in DB The email verification test wrote `email_verification_required = true` to the database via `Settings.edit`, but cleanup only reset the in-memory settings cache — leaving the stale `true` value in the DB. If the cache refreshed from the DB during the subsequent webhook verification test, `_isVerificationRequired()` would return `true`, causing the verification flow to skip entirely and the test to fail. Replaced `settingsCache.set()` with `models.Settings.edit()` in both test `finally` blocks so the flag is properly reset in both the DB and cache, and added `nock.cleanAll()` to the webhook test cleanup to prevent interceptor leakage.
`_markVerificationRequired` relied on Bookshelf's `onUpdated` model hook to propagate the DB write to the settings cache. When the DB already held the same value (e.g. `true` from a previous verification cycle), `hasChanged()` returned false, the save was skipped, and the cache was never updated — leaving stale state that silently prevented subsequent verification flows. The fix adds a `setVerificationRequired` callback that explicitly updates the cache after the DB write, so the cache always reflects the intended state regardless of `hasChanged()`. Also guards `initVerificationTrigger` with a singleton check to prevent duplicate `MemberCreatedEvent` subscriptions across repeated `init()` calls, and moves test cleanup to `afterEach` using `settingsCache.set` — the standard test utility.
The `verificationFlow` labs flag appeared twice in the config API snapshot, causing a mismatch with the actual API response.
9be9ef0 to
30056cc
Compare
…26778) closes [GVA-730](https://linear.app/ghost/issue/GVA-730/add-webhook-changes-behind-feature-flag) The `verificationFlow` feature flag now keeps the existing escalation email path as default while enabling a parallel webhook path through `VerificationTrigger` and `verification-webhook-service.ts`. This wires both strategies in `members/service.js` and adds tests to assert webhook payload behavior without regressing the legacy flow.
closes GVA-730
The
verificationFlowfeature flag now keeps the existing escalation email path as default while enabling a parallel webhook path throughVerificationTriggerandverification-webhook-service.ts. This wires both strategies inmembers/service.jsand adds tests to assert webhook payload behavior without regressing the legacy flow.