Conversation
closes https://linear.app/ghost/issue/BER-3477 Marks a member's redeemed gift as `consumed` (with `consumed_at = now`) when they upgrade to a paid subscription, instead of leaving the gift in `redeemed` status until its natural expiry. ## What changed - **Event subscription**: `GiftServiceWrapper` subscribes to `SubscriptionActivatedEvent` and consumes the redeemer's active gift on activation. Failures are caught and logged so an event-handler error never bubbles back into the subscription-activation flow. - **Shared `consume()` method**: extracted from `processConsumed` so both the scheduled batch job and the new event-driven path go through the same logic — re-fetch under `forUpdate`, no-op if not `redeemed` (idempotent), optional `transacting` for composition with an outer transaction.
ref #27530 Emits structured metrics from the Redis cache adapter for every operation: hit, miss, timeout, error, reset, and background refresh (triggered/succeeded/failed). Each metric carries an optional 'feature' dimension (via the new featureName config option) for dashboard filtering. Timed operations include a duration value. The timeout path uses a single-settlement guard to prevent double-counting when a timed-out lookup later rejects. set() accepts a throwOnError option so background refresh only emits 'succeeded' after a write that actually landed.
closes https://linear.app/ghost/issue/ONC-1640/member-with-email-disabled-repeatedly-being-sent-newsletters-for Always set member.email_disabled to true on 607 errors from Mailgun. When a suppressed member was deleted and someone later signed up with the same email, the new member was created with `email_disabled=false` even though the suppression record still existed. The send filter then included them in every newsletter. Mailgun rejected each send with a 607, triggering the suppression handler. But the handler silently swallowed the `ER_DUP_ENTRY` error from trying to re-insert the existing suppression record, never reaching the dispatch that would have set `email_disabled=true`. The cycle repeated indefinitely.
Consolidated the per-action branches in the admin-auth iframe bridge into a single actions table with one shared dispatcher. Each action declares its URL template, and the listener funnels every call through one path: parse payload, look up action, build URL, fetch, post response back. Adding a new action becomes one table entry rather than a new branch in a switch. Pulled identifier validation (24-char hex ObjectID shape) and querystring construction into small helpers at the top of the module. Identifier shape is checked once at dispatch, so URLs built from message-supplied IDs reject malformed input before reaching fetch. Unknown actions are ignored, malformed JSON no longer breaks dispatch, and the parse-failure diagnostic log now includes the bridge name and payload. No behaviour change for the comments UI, which already sends valid ObjectIDs. A small ESLint override under core/frontend/src/admin-auth/** lints the bridge in a browser environment without Node-only rules (no-console, ghost-custom/no-native-error). Credit: Jorian Woltjer
fetchOembedData returns provider-supplied embed html for rich and video oEmbed response types. The fallback path — used when the source isn't on the known-provider allowlist — now drops that html and treats the response as a bookmark candidate, so the caller falls back to a bookmark card. Allowlisted providers (YouTube, Vimeo, Twitter, Spotify, SoundCloud, etc.) continue to use @extractus/oembed-extractor's allowlist via knownProvider, unchanged. photo-type responses are unchanged. Credit: Jorian Woltjer
Password change and password reset destroyed every other active session for the user, but the originating session was preserved unchanged — including its session_id. The originating browser stayed logged in (intended), but the cookie value didn't refresh. Both endpoints now call req.session.regenerate() and re-assign the verified user to the new session, so the originating browser keeps its login with a fresh session_id. Extracted as rotateAndAssignVerifiedUserToSession to share the logic across the two endpoints. Session.destroy is now idempotent: a missing row is treated as success. Needed because req.session.regenerate() calls store.destroy() on a row the model-layer destroy loop has already removed.
no ref This is a types-only change.
…#27561) closes https://linear.app/ghost/issue/BER-3566 We validate that a tier is active at the time a gift is purchased. After purchase, gifts should remain redeemable even if the tier is later archived, since they have already been paid for already. This change allows gifts to be redeemed on an archived tier. Additionally, gift members can currently only continue their subscription on the same tier/cadence so remaining gift time can be carried over as trial days. If that tier has since been archived, this results in a checkout error, as we don't allow paid subscriptions on archived tiers. With this change, the UI renders the standard upgrade flow when the gifted tier is no longer available, letting the member pick from current active tiers. The trade-off is that the remaining gift days can't be preserved in that case; archived tiers are expected to be an edge case in practice. ## What changed Portal: - New `isArchivedTier({member, site})` helper that follows the existing `isActiveOffer` pattern (a tier id missing from `site.products` means the tier has been archived). - The gift "Continue subscription" banner and the gift-specific Continue button on the account page are gated on the tier still being available; otherwise the regular Change button takes over and routes the member through the standard upgrade screen. Backend: - `MemberRepository.create` / `update` previously rejected any product that wasn't `active` with "Cannot use archived Tiers", which also blocked the gift-redemption path (creating a member on the gifted tier). The check is now skipped when `status === 'gift'`. Non-gift paths (comped, paid) keep the archived-tier block. - In `create()`, the products validation block was moved below the status defaulting block so the gift carve-out can read the resolved status.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )