diff --git a/.changeset/open-shirts-arrive.md b/.changeset/open-shirts-arrive.md new file mode 100644 index 00000000000..bb7ecca6cad --- /dev/null +++ b/.changeset/open-shirts-arrive.md @@ -0,0 +1,6 @@ +--- +"@tryghost/adapter-base-scheduling": patch +"@tryghost/adapter-base-sso": patch +--- + +Update framework dependencies diff --git a/.changeset/six-parents-shout.md b/.changeset/six-parents-shout.md new file mode 100644 index 00000000000..6c070752718 --- /dev/null +++ b/.changeset/six-parents-shout.md @@ -0,0 +1,11 @@ +--- +"@tryghost/kg-default-nodes": minor +"@tryghost/kg-html-to-lexical": minor +"@tryghost/kg-lexical-html-renderer": minor +"@tryghost/kg-clean-basic-html": patch +"@tryghost/kg-default-cards": patch +"@tryghost/kg-unsplash-selector": patch +"@tryghost/koenig-lexical": patch +--- + +Update jsdom to 30 & node engines to match jsdom's diff --git a/.changeset/young-spiders-request.md b/.changeset/young-spiders-request.md new file mode 100644 index 00000000000..22c87141a74 --- /dev/null +++ b/.changeset/young-spiders-request.md @@ -0,0 +1,5 @@ +--- +"@tryghost/adapter-base-scheduling": patch +--- + +Updated logging requests to avoid process crashes on redirects. diff --git a/.github/renovate.json5 b/.github/renovate.json5 index cec3982c13c..efb3cb1d336 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -124,64 +124,25 @@ }, "ignoreDeps": [ // https://github.com/TryGhost/Ghost/commit/2b9e494dfcb95c40f596ccf54ec3151c25d53601 - // `got` 10.x has a Node 10 bug that makes it pretty much unusable for now - "got", - // https://github.com/TryGhost/Ghost/commit/2b9e494dfcb95c40f596ccf54ec3151c25d53601 - // `intl-messageformat` 6.0.0 introduced a breaking change in terms of - // escaping that would be pretty difficult to fix for now + // Theme i18n ({{t}} helper) is on intl-messageformat 5.x. 6.0.0 changed + // message escaping from `\` to ICU apostrophe quoting, which changes the + // output of existing theme locale strings; needs a deliberate migration. "intl-messageformat", - // https://github.com/TryGhost/Ghost/commit/b2fa84c7ff9bf8e21b0791f268f57e92759a87b1 - // no reason given - "moment", - // https://github.com/TryGhost/Ghost/pull/10672 - // https://github.com/TryGhost/Ghost/issues/10870 - "moment-timezone", - // https://github.com/TryGhost/Admin/pull/1111/files - // Ignored because of a mobiledoc-kit issue but that's now in koenig, can probably be cleaned up - "simple-dom", - // https://github.com/TryGhost/Admin/pull/1111/files - // https://github.com/TryGhost/Ghost/pull/10672 - // These have been ignored since forever - "ember-drag-drop", - "normalize.css", - "validator", - - // https://github.com/TryGhost/Ghost/commit/7ebf2891b7470a1c2ffeddefb2fe5e7a57319df3 - // Changed how modules are loaded, caused a weird error during render - "@embroider/macros", - // https://github.com/TryGhost/Ghost/commit/a10ad3767f60ed2c8e56feb49e7bf83d9618b2ab - // Caused linespacing issues in the editor, but now it's used in different places - // So not sure if it's relevant - soon we will finish switching to react-codemirror + // codemirror 6 is a rewrite of the 5.x API. @tryghost/kg-simplemde (the + // Koenig markdown card) is built on 5.x and is not being ported. Also + // covers the `codemirror@<5.58.2` pnpm override key. "codemirror", - // https://github.com/TryGhost/Ghost/commit/3236891b80988924fbbdb625d30cb64a7bf2afd1 - // ember-cli-code-coverage@2.0.0 broke our code coverage - "ember-cli-code-coverage", - // https://github.com/TryGhost/Ghost/commit/1382e34e42a513c201cb957b7f843369a2ce1b63 - // ember-cli-terser@4.0.2 has a regression that breaks our sourcemaps - "ember-cli-terser", - - // https://linear.app/ghost/issue/PLA-56 - // Deprecated (per-method lodash packages are EOL); last real release is - // 4.5.0 (2019). The flagged "4.18.0 [SECURITY]" bump does not exist, so - // Renovate can't remediate it. It's transitive-only — we never call - // `_.template` ourselves — and only reachable via frozen Ember build - // tooling (broccoli-templater, sourcemap-validator) plus old @tryghost/* - // packages, so it can't be removed from the tree here. Ignore it to stop - // the unactionable dashboard noise. - "lodash.template", - // https://linear.app/ghost/issue/PLA-78 // https://github.com/TryGhost/Ghost/pull/28180 - // knex 2.5.x breaks migrations: 2.5.0's password-masking mutates the - // shared `config.get('database')` object, which we hand to BOTH knex() - // and knex-migrator. With core ahead of knex-migrator's pinned knex - // (2.4.2), the password is stripped before knex-migrator connects → - // "Access denied (using password: NO)". 2.x is EOL; the real move is - // knex 3.x, gated on publishing a knex-3 knex-migrator - // (TryGhost/knex-migrator#86, unreleased), bumped in lockstep with core. - // Ignore until we do that coordinated upgrade. + // ghost/core is pinned to knex 2.4.2 and knex-migrator's knex is pinned + // back to the same version via the `knex-migrator>knex` pnpm override + // (knex-migrator 5.4.x bundles knex 3.x). Migrations are built with + // core's query builder and executed by knex-migrator's, so the two must + // move together; 2.5.x also strips the password from the shared + // `config.get('database')` object before knex-migrator connects. Ignore + // until core moves to knex 3.x in one coordinated upgrade. "knex", // Same PLA-78 pin, but Renovate treats the pnpm override key // `knex-migrator>knex` as its own dependency name, so the plain "knex" diff --git a/.github/workflows/tinybird.yml b/.github/workflows/tinybird.yml index cdb48a7e6b0..6e3c414e49b 100644 --- a/.github/workflows/tinybird.yml +++ b/.github/workflows/tinybird.yml @@ -58,7 +58,7 @@ jobs: working-directory: ghost/core/core/server/data/tinybird services: tinybird: - image: tinybirdco/tinybird-local:latest@sha256:0865fb2f8aef0b8f0cd3ffe53a7a9832787b2d995fd0d34ddaa6563f1638feed + image: tinybirdco/tinybird-local:latest@sha256:cf9de0516700eea366161181e74cae943f1a7310e6f2cc4f5762cb1777ed3fb3 ports: - 7181:7181 outputs: diff --git a/apps/admin-x-framework/src/api/config.ts b/apps/admin-x-framework/src/api/config.ts index ca68d98e3cc..e034666eb9e 100644 --- a/apps/admin-x-framework/src/api/config.ts +++ b/apps/admin-x-framework/src/api/config.ts @@ -80,6 +80,9 @@ export type Config = { billing?: { enabled?: boolean url?: string + // Destination for the upgrade button shown when a host limit is reached. + // Managed hosting providers can override it; falls back to Ghost's default. + upgradeUrl?: string // eg. '#/pro/billing/plans' or an absolute billing URL // Copy and branding for the sidebar trial banner. // Managed hosting providers can override these; each falls back to Ghost's default. upgradeBanner?: { @@ -148,3 +151,12 @@ export const hasSendingDomain = (config: Config) => { export const sendingDomain = (config: Config) => { return config?.hostSettings?.managedEmail?.sendingDomain; }; + +export const DEFAULT_UPGRADE_ROUTE = '/pro'; + +// hostSettings stores an href; navigation takes a route, so a hash one drops its '#' +export const upgradeRoute = (config?: Config) => { + const configured = config?.hostSettings?.billing?.upgradeUrl; + + return configured ? configured.replace(/^#/, '') : DEFAULT_UPGRADE_ROUTE; +}; diff --git a/apps/admin/src/analytics/views/stats/overview/components/overview-kpis.tsx b/apps/admin/src/analytics/views/stats/overview/components/overview-kpis.tsx index 286ec26becb..ef26653d10c 100644 --- a/apps/admin/src/analytics/views/stats/overview/components/overview-kpis.tsx +++ b/apps/admin/src/analytics/views/stats/overview/components/overview-kpis.tsx @@ -7,6 +7,8 @@ import {centsToDollars} from '@tryghost/shade/app'; import {getPeriodText} from '@/shared/analytics/chart-helpers'; import {useAppContext} from '@tryghost/admin-x-framework'; import {useAnalytics} from '@/analytics/providers/analytics-context'; +import {useAnalyticsData} from '@/shared/analytics/use-analytics-data'; +import {upgradeRoute} from '@tryghost/admin-x-framework/api/config'; import {useLimiter} from '@/analytics/hooks/use-limiter'; import {useNavigate} from '@tryghost/admin-x-framework'; @@ -134,6 +136,7 @@ const OverviewKPIs:React.FC = ({ const navigate = useNavigate(); const {appSettings} = useAppContext(); const limiter = useLimiter(); + const {config} = useAnalyticsData(); const isWebAnalyticsLimited = limiter.isLimited('limitAnalytics'); const areaChartClassName = '-mb-3 h-[10vw] max-h-[200px] min-h-[100px] hover:cursor-pointer!'; @@ -199,7 +202,7 @@ const OverviewKPIs:React.FC = ({ navigate('/pro', {crossApp: true})}> + } diff --git a/apps/admin/src/settings/advanced/integrations.acceptance.test.tsx b/apps/admin/src/settings/advanced/integrations.acceptance.test.tsx index 9b2e6b88284..dd267b53afd 100644 --- a/apps/admin/src/settings/advanced/integrations.acceptance.test.tsx +++ b/apps/admin/src/settings/advanced/integrations.acceptance.test.tsx @@ -45,10 +45,14 @@ function apiKey(secret: string) { }; } -function limitedConfig() { +const customIntegrationsLimit = {customIntegrations: {disabled: true, error: "Your plan does not support custom integrations"}}; + +function limitedConfig(upgradeUrl?: string) { const response = configResponse(); response.config.labs = {...response.config.labs, transistor: true}; - response.config.hostSettings = {limits: {customIntegrations: {disabled: true, error: "Your plan does not support custom integrations"}}}; + response.config.hostSettings = upgradeUrl ? + {limits: customIntegrationsLimit, billing: {upgradeUrl}} : + {limits: customIntegrationsLimit}; return response; } @@ -279,7 +283,16 @@ describe("Advanced integrations", () => { } await section.getByTestId("zapier-integration").getByRole("button", {name: "Upgrade"}).click(); - expect(JSON.parse(document.body.dataset.externalNavigate!)).toMatchObject({route: "pro"}); + expect(JSON.parse(document.body.dataset.externalNavigate!)).toMatchObject({route: "/pro"}); + }); + + it("sends the upgrade CTA to a host's own billing app when one is configured", async () => { + fakeSettingsScreens(); + await renderAdminApp("/settings/integrations", {boot: {browseConfig: {response: limitedConfig("#/pro/billing/plans")}}}); + + const section = settingsScreen.section("integrations"); + await section.getByTestId("zapier-integration").getByRole("button", {name: "Upgrade"}).click(); + expect(JSON.parse(document.body.dataset.externalNavigate!)).toMatchObject({route: "/pro/billing/plans"}); }); it("saves FirstPromoter configuration and warns before discarding later changes", async () => { diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx index 2ffba14db5c..ebc50fb01ff 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations.tsx @@ -13,6 +13,8 @@ import {useConfirmation} from '@/settings/app/components/providers/confirmation- import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {DEFAULT_UPGRADE_ROUTE} from '@tryghost/admin-x-framework/api/config'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {withErrorBoundary} from '@/settings/app/components/error-boundary'; interface IntegrationItemProps { @@ -23,6 +25,7 @@ interface IntegrationItemProps { onDelete?: () => void; active?: boolean; disabled?: boolean; + upgradeRoute?: string; testId?: string; custom?: boolean; } @@ -45,6 +48,7 @@ const IntegrationItem: React.FC = ({ onDelete, active, disabled, + upgradeRoute = DEFAULT_UPGRADE_ROUTE, testId, custom = false }) => { @@ -55,7 +59,7 @@ const IntegrationItem: React.FC = ({ e?.stopPropagation(); if (disabled) { - updateRoute({route: 'pro', isExternal: true}); + updateRoute({route: upgradeRoute, isExternal: true}); } else { action(); } @@ -93,6 +97,7 @@ const IntegrationItem: React.FC = ({ const BuiltInIntegrations: React.FC = () => { const {config} = useGlobalData(); + const upgradeRoute = useUpgradeRoute(); const {updateRoute} = useSettingsNavigation(); const openModal = (modal: string) => { @@ -190,6 +195,7 @@ const BuiltInIntegrations: React.FC = () => { icon={item.icon} testId={item.testId} title={item.title} + upgradeRoute={upgradeRoute} /> ))} diff --git a/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx b/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx index 2c717137192..d5245bfda35 100644 --- a/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/advanced/integrations/add-integration-modal.tsx @@ -3,12 +3,14 @@ import {Field, FieldError, FieldGroup, FieldLabel, Input} from '@tryghost/shade/ import {HostLimitError, useLimiter} from '@/settings/app/hooks/use-limiter'; import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {SettingsModal} from '@tryghost/shade/patterns'; import {useCreateIntegration} from '@tryghost/admin-x-framework/api/integrations'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; function AddIntegrationModal() { const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const [name, setName] = useState(''); const [errors, setErrors] = useState({name: ''}); const {mutateAsync: createIntegration} = useCreateIntegration(); @@ -22,13 +24,13 @@ function AddIntegrationModal() { if (error instanceof HostLimitError) { showLimit({ prompt: error.message || `Your current plan doesn't support more custom integrations.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); updateRoute('integrations'); } }); } - }, [limiter, showLimit, updateRoute]); + }, [limiter, showLimit, updateRoute, upgradeRoute]); return { const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const returnRoute = useFeatureFlag('automations') ? 'emails' : 'newsletters'; const handleError = useHandleError(); const {showLimit} = useConfirmation(); @@ -77,11 +79,11 @@ const AddNewsletterModal: React.FC = () => { if (limitError) { showLimit({ prompt: limitError.message || `Your current plan doesn't support more newsletters.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); updateRoute(returnRoute); } - }, [limitError, returnRoute, updateRoute, showLimit]); + }, [limitError, returnRoute, updateRoute, showLimit, upgradeRoute]); if (isCheckingLimit || limitError) { return null; diff --git a/apps/admin/src/settings/app/components/settings/email/newsletters/newsletter-detail-modal.tsx b/apps/admin/src/settings/app/components/settings/email/newsletters/newsletter-detail-modal.tsx index 62b67678fea..978771f0eec 100644 --- a/apps/admin/src/settings/app/components/settings/email/newsletters/newsletter-detail-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/email/newsletters/newsletter-detail-modal.tsx @@ -16,6 +16,7 @@ import {PreviewModalContent} from '@/settings/app/components/settings/preview-mo import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useParams} from '@tryghost/admin-x-framework'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {getSettingValue, getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {hasSendingDomain, isManagedEmail, sendingDomain} from '@tryghost/admin-x-framework/api/config'; import {renderReplyToEmail, renderSenderEmail} from '@/settings/app/utils/newsletter-emails'; @@ -97,6 +98,7 @@ const Sidebar: React.FC<{ }> = ({newsletter, onlyOne, updateNewsletter, validate, errors, clearError}) => { type FontOption = {value: string; label: string; className?: string}; const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const {mutateAsync: editNewsletter} = useEditNewsletter(); const limiter = useLimiter(); const {settings, config, siteData} = useGlobalData(); @@ -177,7 +179,7 @@ const Sidebar: React.FC<{ if (error instanceof HostLimitError) { showLimit({ prompt: error.message || `Your current plan doesn't support more newsletters.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } else { diff --git a/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx b/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx index ee71145a161..a22c04a94cf 100644 --- a/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/general/user-detail-modal.tsx @@ -15,6 +15,7 @@ import {LucideIcon} from '@tryghost/shade/utils'; import {Pencil, Trash2} from 'lucide-react'; import {useLocation, useParams} from '@tryghost/admin-x-framework'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {SOCIAL_PLATFORM_CONFIGS, SOCIAL_PLATFORM_KEYS, getSocialValidationError} from '@/settings/app/utils/social-urls/index'; import {SettingsModal} from '@tryghost/shade/patterns'; import {Text} from '@tryghost/shade/primitives'; @@ -75,6 +76,7 @@ export interface UserDetailProps { const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDeleting: boolean) => void}> = ({user, onDeletingUserChange}) => { const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const location = useLocation(); const getTabFromPath = (path: string): string => { @@ -174,7 +176,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel showLimit({ formSheet: true, prompt: error.message || `Your current plan doesn't support more users.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } else { diff --git a/apps/admin/src/settings/app/components/settings/membership/analytics.tsx b/apps/admin/src/settings/app/components/settings/membership/analytics.tsx index 51237ac3daf..d09e5b03cbf 100644 --- a/apps/admin/src/settings/app/components/settings/membership/analytics.tsx +++ b/apps/admin/src/settings/app/components/settings/membership/analytics.tsx @@ -6,6 +6,7 @@ import {HostLimitError, useLimiter} from '@/settings/app/hooks/use-limiter'; import {SettingGroupContent} from '@tryghost/shade/patterns'; import {getSettingValues, isSettingReadOnly} from '@tryghost/admin-x-framework/api/settings'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {withErrorBoundary} from '@/settings/app/components/error-boundary'; const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -27,6 +28,7 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => { const [isWebAnalyticsLimited, setIsWebAnalyticsLimited] = useState(false); const limiter = useLimiter(); const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); useEffect(() => { if (limiter?.isLimited('limitAnalytics')) { @@ -72,7 +74,7 @@ const Analytics: React.FC<{ keywords: string[] }> = ({keywords}) => {
- Web analytics is available on the Publisher plan and above. updateRoute({route: '/pro', isExternal: true})}>Upgrade now → + Web analytics is available on the Publisher plan and above. updateRoute({route: upgradeRoute, isExternal: true})}>Upgrade now →
diff --git a/apps/admin/src/settings/app/components/settings/membership/stripe/stripe-connect-modal.tsx b/apps/admin/src/settings/app/components/settings/membership/stripe/stripe-connect-modal.tsx index 64d0d50f149..f8dd8740182 100644 --- a/apps/admin/src/settings/app/components/settings/membership/stripe/stripe-connect-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/membership/stripe/stripe-connect-modal.tsx @@ -21,6 +21,7 @@ import {useConfirmation} from '@/settings/app/components/providers/confirmation- import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; const RETRY_PRODUCT_SAVE_POLL_LENGTH = 1000; const RETRY_PRODUCT_SAVE_MAX_POLL = 15 * RETRY_PRODUCT_SAVE_POLL_LENGTH; @@ -257,6 +258,7 @@ const StripeConnectModal: React.FC = () => { const {config, settings} = useGlobalData(); const stripeConnectAccountId = getSettingValue(settings, 'stripe_connect_account_id'); const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const [step, setStep] = useState<'start' | 'connect'>('start'); const limiter = useLimiter(); const {showLimit} = useConfirmation(); @@ -278,7 +280,7 @@ const StripeConnectModal: React.FC = () => { updateRoute('tiers'); showLimit({ prompt: error.message || `Your current plan doesn't support Stripe Connect.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); } } @@ -286,7 +288,7 @@ const StripeConnectModal: React.FC = () => { }; checkLimit(); - }, [limiter, updateRoute, stripeEnabled, hasStripeConnectLimit, showLimit]); + }, [limiter, updateRoute, stripeEnabled, hasStripeConnectLimit, showLimit, upgradeRoute]); const startFlow = () => { setStep('connect'); diff --git a/apps/admin/src/settings/app/components/settings/membership/tiers.tsx b/apps/admin/src/settings/app/components/settings/membership/tiers.tsx index 81655e51fb0..570ef57437c 100644 --- a/apps/admin/src/settings/app/components/settings/membership/tiers.tsx +++ b/apps/admin/src/settings/app/components/settings/membership/tiers.tsx @@ -16,6 +16,7 @@ import {useConfirmation} from '@/settings/app/components/providers/confirmation- import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {withErrorBoundary} from '@/settings/app/components/error-boundary'; const StripeConnectedButton: React.FC<{className?: string; onClick: () => void;}> = ({className, onClick}) => { @@ -43,6 +44,7 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => { const archivedTiers = getArchivedTiers(tiers || []); const defaultPaidTierCurrency = activeTiers.find(tier => tier.type === 'paid' && tier.currency)?.currency || 'USD'; const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const limiter = useLimiter(); const {showLimit} = useConfirmation(); const handleError = useHandleError(); @@ -105,7 +107,7 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => { if (error instanceof HostLimitError) { showLimit({ prompt: error.message || `Your current plan doesn't support Stripe Connect.`, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } diff --git a/apps/admin/src/settings/app/components/settings/site/change-theme.tsx b/apps/admin/src/settings/app/components/settings/site/change-theme.tsx index 85bfc8bb2f5..eae76f4eabd 100644 --- a/apps/admin/src/settings/app/components/settings/site/change-theme.tsx +++ b/apps/admin/src/settings/app/components/settings/site/change-theme.tsx @@ -9,6 +9,7 @@ import {downloadFile, getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {useCheckThemeLimitError} from '@/settings/app/hooks/use-check-theme-limit-error'; import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {withErrorBoundary} from '@/settings/app/components/error-boundary'; const ChangeTheme: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -16,6 +17,7 @@ const ChangeTheme: React.FC<{ keywords: string[] }> = ({keywords}) => { const [isCheckingLimit, setIsCheckingLimit] = useState(false); const {checkThemeLimitError} = useCheckThemeLimitError(); const {route, updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const {showLimit} = useConfirmation(); const {data: themesData} = useBrowseThemes(); const activeTheme = themesData?.themes.find((theme: Theme) => theme.active); @@ -40,7 +42,7 @@ const ChangeTheme: React.FC<{ keywords: string[] }> = ({keywords}) => { if (themeLimitError) { showLimit({ prompt: themeLimitError, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); } else { updateRoute('design/change-theme'); @@ -57,7 +59,7 @@ const ChangeTheme: React.FC<{ keywords: string[] }> = ({keywords}) => { if (limitError) { showLimit({ prompt: limitError, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } diff --git a/apps/admin/src/settings/app/components/settings/site/design-and-theme-modal.tsx b/apps/admin/src/settings/app/components/settings/site/design-and-theme-modal.tsx index 2197cd5c789..25a08a14ce9 100644 --- a/apps/admin/src/settings/app/components/settings/site/design-and-theme-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/site/design-and-theme-modal.tsx @@ -4,11 +4,13 @@ import React, {useCallback, useEffect, useState} from 'react'; import ThemeCodeEditorModal from './theme/theme-code-editor-modal'; import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; import {parseEditingThemeRoute} from './theme/theme-editor-utils'; import {useCheckThemeLimitError} from '@/settings/app/hooks/use-check-theme-limit-error'; const DesignAndThemeModal: React.FC = () => { const {route, updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const currentPath = route; const [themeChangeError, setThemeChangeError] = useState(null); const [isCheckingLimit, setIsCheckingLimit] = useState(false); @@ -24,9 +26,9 @@ const DesignAndThemeModal: React.FC = () => { const showThemeLimitModal = useCallback((error: string) => { showLimit({ prompt: error, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); - }, [showLimit, updateRoute]); + }, [showLimit, updateRoute, upgradeRoute]); useEffect(() => { const checkIfThemeChangeAllowed = async () => { diff --git a/apps/admin/src/settings/app/components/settings/site/theme-modal.tsx b/apps/admin/src/settings/app/components/settings/site/theme-modal.tsx index 9c533f2a154..9a7815ea523 100644 --- a/apps/admin/src/settings/app/components/settings/site/theme-modal.tsx +++ b/apps/admin/src/settings/app/components/settings/site/theme-modal.tsx @@ -15,6 +15,7 @@ import {useCheckThemeLimitError} from '@/settings/app/hooks/use-check-theme-limi import {type ConfirmationHandle, useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; interface ThemeToolbarProps { selectedTheme: OfficialTheme|null; @@ -52,6 +53,7 @@ const ThemeToolbar: React.FC = ({ themes }) => { const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const {mutateAsync: uploadTheme} = useUploadTheme(); const {checkThemeLimitError, isThemeLimited} = useCheckThemeLimitError(); const handleError = useHandleError(); @@ -239,7 +241,7 @@ const ThemeToolbar: React.FC = ({ showLimit({ title: 'Upgrade to enable custom themes', prompt: uploadConfig.error || <>Your current plan only supports official themes. You can install them from the Ghost theme marketplace., - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); } }; @@ -303,6 +305,7 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) = const [installedFromMarketplace, setInstalledFromMarketplace] = useState(false); const [isMounted, setIsMounted] = useState(false); const {updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const {data: {themes} = {}} = useBrowseThemes(); const {mutateAsync: installTheme} = useInstallTheme(); @@ -406,7 +409,7 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) = if (limitError) { showLimit({ prompt: limitError, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } diff --git a/apps/admin/src/settings/app/components/settings/site/theme/advanced-theme-settings.tsx b/apps/admin/src/settings/app/components/settings/site/theme/advanced-theme-settings.tsx index f0e1c8a4b8e..d2cc106175f 100644 --- a/apps/admin/src/settings/app/components/settings/site/theme/advanced-theme-settings.tsx +++ b/apps/admin/src/settings/app/components/settings/site/theme/advanced-theme-settings.tsx @@ -13,6 +13,7 @@ import {useCheckThemeLimitError} from '@/settings/app/hooks/use-check-theme-limi import {useConfirmation} from '@/settings/app/components/providers/confirmation-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useSettingsNavigation} from '@/settings/app/hooks/use-settings-navigation'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; interface ThemeActionProps { theme: Theme; @@ -58,6 +59,7 @@ const ThemeActions: React.FC = ({ const {refreshActiveThemeData} = useCustomFonts(); const handleError = useHandleError(); const {route, updateRoute} = useSettingsNavigation(); + const upgradeRoute = useUpgradeRoute(); const {checkThemeLimitError} = useCheckThemeLimitError(); const {confirm, showLimit} = useConfirmation(); @@ -133,7 +135,7 @@ const ThemeActions: React.FC = ({ if (limitError) { showLimit({ prompt: limitError, - onOk: () => updateRoute({route: '/pro', isExternal: true}) + onOk: () => updateRoute({route: upgradeRoute, isExternal: true}) }); return; } diff --git a/apps/admin/src/settings/app/hooks/use-upgrade-route.test.tsx b/apps/admin/src/settings/app/hooks/use-upgrade-route.test.tsx new file mode 100644 index 00000000000..204b1af725c --- /dev/null +++ b/apps/admin/src/settings/app/hooks/use-upgrade-route.test.tsx @@ -0,0 +1,33 @@ +import {renderHook} from '@testing-library/react'; +import {useUpgradeRoute} from '@/settings/app/hooks/use-upgrade-route'; + +const mockConfig = vi.fn(); + +vi.mock('@/settings/app/components/providers/global-data-provider', () => ({ + useGlobalData: () => ({config: mockConfig()}) +})); + +describe('useUpgradeRoute', () => { + const routeFor = (billing?: Record) => { + mockConfig.mockReturnValue({hostSettings: billing ? {billing} : {}}); + return renderHook(() => useUpgradeRoute()).result.current; + }; + + it('sends people to Ghost(Pro) billing when the host has not configured anything', () => { + expect(routeFor()).toBe('/pro'); + expect(routeFor({})).toBe('/pro'); + }); + + // hostSettings holds an href, updateRoute takes a route + it('turns a hash href into a route', () => { + expect(routeFor({upgradeUrl: '#/pro/billing/plans'})).toBe('/pro/billing/plans'); + }); + + it('leaves an absolute billing URL alone', () => { + expect(routeFor({upgradeUrl: 'https://billing.example.com/upgrade'})).toBe('https://billing.example.com/upgrade'); + }); + + it('leaves a route without a hash alone', () => { + expect(routeFor({upgradeUrl: '/billing'})).toBe('/billing'); + }); +}); diff --git a/apps/admin/src/settings/app/hooks/use-upgrade-route.ts b/apps/admin/src/settings/app/hooks/use-upgrade-route.ts new file mode 100644 index 00000000000..25e76816f2d --- /dev/null +++ b/apps/admin/src/settings/app/hooks/use-upgrade-route.ts @@ -0,0 +1,8 @@ +import {upgradeRoute} from '@tryghost/admin-x-framework/api/config'; +import {useGlobalData} from '@/settings/app/components/providers/global-data-provider'; + +export function useUpgradeRoute() { + const {config} = useGlobalData(); + + return upgradeRoute(config); +} diff --git a/apps/ember-admin/app/components/editor/modals/publish-flow/confirm.js b/apps/ember-admin/app/components/editor/modals/publish-flow/confirm.js index d5273731419..87c4873f98c 100644 --- a/apps/ember-admin/app/components/editor/modals/publish-flow/confirm.js +++ b/apps/ember-admin/app/components/editor/modals/publish-flow/confirm.js @@ -1,12 +1,17 @@ import Component from '@glimmer/component'; +import Ember from 'ember'; +import getUpgradeUrl from 'ghost-admin/utils/upgrade-url'; import moment from 'moment-timezone'; import {htmlSafe} from '@ember/template'; +import {inject} from 'ghost-admin/decorators/inject'; import {isArray} from '@ember/array'; import {isServerUnreachableError} from 'ghost-admin/services/ajax'; import {inject as service} from '@ember/service'; import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; +const {Handlebars} = Ember; + function isString(str) { return toString.call(str) === '[object String]'; } @@ -16,8 +21,14 @@ export default class PublishFlowOptions extends Component { @service feature; @service router; + @inject config; + @tracked errorMessage; + get upgradeUrl() { + return getUpgradeUrl(this.config); + } + // store any derived state from PublishOptions on creation so the copy // doesn't change whilst the post is saving willEmail = this.args.publishOptions.willEmail; @@ -109,7 +120,7 @@ export default class PublishFlowOptions extends Component { if (isServerUnreachableError(e)) { errorMessage = 'Unable to connect, please check your internet connection and try again.'; } else if (payloadError?.type === 'HostLimitError') { - errorMessage = htmlSafe(payloadError.context.replace(/please upgrade/i, '$&')); + errorMessage = htmlSafe(payloadError.context.replace(/please upgrade/i, `$&`)); } else if (e && isString(e)) { errorMessage = e; } else if (e && isArray(e)) { diff --git a/apps/ember-admin/app/components/modals/limits/publish-limit.hbs b/apps/ember-admin/app/components/modals/limits/publish-limit.hbs index 23a1abdf71e..cfab8569903 100644 --- a/apps/ember-admin/app/components/modals/limits/publish-limit.hbs +++ b/apps/ember-admin/app/components/modals/limits/publish-limit.hbs @@ -15,8 +15,8 @@ Cancel - + Upgrade - + \ No newline at end of file diff --git a/apps/ember-admin/app/components/modals/limits/publish-limit.js b/apps/ember-admin/app/components/modals/limits/publish-limit.js index 14b6f0d822c..d746dfd3e8a 100644 --- a/apps/ember-admin/app/components/modals/limits/publish-limit.js +++ b/apps/ember-admin/app/components/modals/limits/publish-limit.js @@ -1,6 +1,14 @@ import Component from '@glimmer/component'; +import getUpgradeUrl from 'ghost-admin/utils/upgrade-url'; +import {inject} from 'ghost-admin/decorators/inject'; export default class PublishLimitModal extends Component { + @inject config; + + get upgradeUrl() { + return getUpgradeUrl(this.config, '#/pro?action=checkout'); + } + get headerMessage() { if (this.args.data.code === 'EMAIL_VERIFICATION_NEEDED') { return 'Hold up, we\'re missing some details'; diff --git a/apps/ember-admin/app/components/posts/debug.hbs b/apps/ember-admin/app/components/posts/debug.hbs index 92ce4d1aa94..71572cf33de 100644 --- a/apps/ember-admin/app/components/posts/debug.hbs +++ b/apps/ember-admin/app/components/posts/debug.hbs @@ -209,13 +209,13 @@
- {{#if (or batch.providerId batch.errorStatusCode batch.errorMessage)}} + {{#if (or batch.mailgunMessageId batch.errorStatusCode batch.errorMessage)}}
- {{#if batch.providerId}} + {{#if batch.mailgunMessageId}}
Provider id: - {{batch.providerId}} + {{batch.mailgunMessageId}}
{{/if}} diff --git a/apps/ember-admin/app/components/posts/debug.js b/apps/ember-admin/app/components/posts/debug.js index a6c283f25ac..436d59b9801 100644 --- a/apps/ember-admin/app/components/posts/debug.js +++ b/apps/ember-admin/app/components/posts/debug.js @@ -90,7 +90,7 @@ export default class Debug extends Component { statusClass: batch.status, createdAt: batch.created_at ? moment(batch.created_at).format('DD MMM, YYYY, HH:mm:ss') : '', segment: batch.member_segment || '', - providerId: batch.provider_id || null, + mailgunMessageId: batch.mailgun_message_id || null, errorMessage: batch.error_message || '', errorStatusCode: batch.error_status_code || '', recipientCount: batch.count?.recipients || 0 diff --git a/apps/ember-admin/app/services/limit.js b/apps/ember-admin/app/services/limit.js index cf550098ce3..57a1fdb3d9e 100644 --- a/apps/ember-admin/app/services/limit.js +++ b/apps/ember-admin/app/services/limit.js @@ -57,8 +57,20 @@ export default class LimitsService extends Service { helpLink = 'https://ghost.org/help/'; } + let subscription; + + // A subscription without a start can't anchor a period, so it's treated as + // absent rather than built into one that throws on the way to the count query + if (this.config.hostSettings?.subscription?.start) { + subscription = { + startDate: this.config.hostSettings.subscription.start, + interval: 'month' + }; + } + this.limiter.loadLimits({ - limits: this.decorateWithCountQueries(limits), + limits: this.decorateWithCountQueries(this.usableLimits(limits, subscription)), + subscription, helpLink, errors: { HostLimitError, @@ -67,6 +79,23 @@ export default class LimitsService extends Service { }); } + // Periodic limits need a subscription to build. Registration stops at the first + // limit that throws, so passing one through would drop every limit after it + usableLimits(limits, subscription) { + if (subscription) { + return limits; + } + + return Object.fromEntries(Object.entries(limits).filter(([name, limit]) => { + if (limit && Object.prototype.hasOwnProperty.call(limit, 'maxPeriodic')) { + console.warn(`Skipping ${name} limit: periodic limits need hostSettings.subscription`); // eslint-disable-line no-console + return false; + } + + return true; + })); + } + reload() { this.loadLimits(); } @@ -84,6 +113,10 @@ export default class LimitsService extends Service { limits.newsletters.currentCountQuery = bind(this, this.getNewslettersCount); } + if (limits.emails) { + limits.emails.currentCountQuery = bind(this, this.getEmailsCount); + } + return limits; } @@ -108,4 +141,13 @@ export default class LimitsService extends Service { const activeNewsletters = await this.store.query('newsletter', {filter: 'status:active', limit: 'all'}); return activeNewsletters.length; } + + // Periodic limits pass the period start as the second argument. The default + // emails query counts recipients via knex, which doesn't exist in the browser + async getEmailsCount(_db, startDate) { + const since = new Date(startDate).toISOString(); + const emails = await this.store.query('email', {filter: `created_at:>='${since}'`, fields: 'id,email_count', limit: 'all'}); + + return emails.reduce((total, email) => total + (email.emailCount ?? 0), 0); + } } diff --git a/apps/ember-admin/app/utils/publish-options.js b/apps/ember-admin/app/utils/publish-options.js index f7197d19085..0b00b54a380 100644 --- a/apps/ember-admin/app/utils/publish-options.js +++ b/apps/ember-admin/app/utils/publish-options.js @@ -1,10 +1,14 @@ +import Ember from 'ember'; import moment from 'moment-timezone'; +import upgradeUrl from 'ghost-admin/utils/upgrade-url'; import {action} from '@ember/object'; import {getPublicPreviewWarning} from 'ghost-admin/utils/public-preview-warning'; import {htmlSafe} from '@ember/template'; import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; +const {Handlebars} = Ember; + export default class PublishOptions { // passed in services config = null; @@ -402,9 +406,14 @@ export default class PublishOptions { await this.settings.reload(); try { - if (this.limit.limiter && this.limit.limiter.isLimited('emails')) { + // authors and contributors cannot browse emails so we can't count them at this stage + if (this.limit.limiter?.isLimited('emails') && !this.user.isAuthorOrContributor) { await this.limit.limiter.errorIfWouldGoOverLimit('emails'); - } else if (this.settings.emailVerificationRequired) { + } + + // checked separately so a site under its email limit still shows a + // verification hold, in the order the server applies them + if (this.settings.emailVerificationRequired) { this.emailDisabledError = this.config.hostSettings?.emailVerification?.emailSendingDisabledMessage || 'Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org.'; } @@ -424,7 +433,7 @@ export default class PublishOptions { await this.limit.limiter.errorIfIsOverLimit('members'); } } catch (e) { - const linkedMessage = htmlSafe(e.message.replace(/please upgrade/i, '$&')); + const linkedMessage = htmlSafe(e.message.replace(/please upgrade/i, `$&`)); this.publishDisabledError = linkedMessage; } } diff --git a/apps/ember-admin/app/utils/upgrade-url.js b/apps/ember-admin/app/utils/upgrade-url.js new file mode 100644 index 00000000000..85b7f13701d --- /dev/null +++ b/apps/ember-admin/app/utils/upgrade-url.js @@ -0,0 +1,23 @@ +const DEFAULT_UPGRADE_URL = '#/pro'; +const ALLOWED_SCHEMES = ['http:', 'https:']; + +// Mirrors upgradeRoute() in @tryghost/admin-x-framework/api/config, which Ember +// can't import. Returns an href, so in-app values are anchored to the admin hash +// and other schemes are rejected the way navigateTo() rejects them on the React side +export default function upgradeUrl(config, fallback = DEFAULT_UPGRADE_URL) { + const configured = config?.hostSettings?.billing?.upgradeUrl; + + if (!configured) { + return fallback; + } + + if (/^[a-z]+:/i.test(configured)) { + try { + return ALLOWED_SCHEMES.includes(new URL(configured).protocol) ? configured : fallback; + } catch (e) { + return fallback; + } + } + + return configured.startsWith('#') ? configured : `#/${configured.replace(/^\/+/, '')}`; +} diff --git a/apps/ember-admin/tests/acceptance/editor/publish-flow-test.js b/apps/ember-admin/tests/acceptance/editor/publish-flow-test.js index 82bbc517666..c81bc4c3a40 100644 --- a/apps/ember-admin/tests/acceptance/editor/publish-flow-test.js +++ b/apps/ember-admin/tests/acceptance/editor/publish-flow-test.js @@ -686,6 +686,50 @@ describe('Acceptance: Publish flow', function () { .to.have.trimmed.text('Your plan supports up to 1,000 members. Please upgrade to reenable publishing.'); }); + // the email limit registering must not mask an account-review hold: the + // server applies both, and this used to short-circuit on the limit passing + it('shows the verification hold while under a configured email limit', async function () { + const config = this.server.db.configs.find(1); + config.hostSettings = { + subscription: {start: '2026-01-01T00:00:00.000Z'}, + limits: { + emails: { + maxPeriodic: 1000, + error: 'Your plan supports up to {{max}} emails. Please upgrade to keep sending.' + } + } + }; + this.server.db.configs.update(1, config); + + this.server.db.settings.update({key: 'email_verification_required'}, {value: true}); + + // the periodic limit counts sent emails through this endpoint + const emailRequests = []; + this.server.get('/emails/', function (schema, request) { + emailRequests.push(request.queryParams); + return {emails: [], meta: {pagination: {total: 0}}}; + }); + + await loginAsRole('Administrator', this.server); + const post = this.server.create('post', {status: 'draft'}); + + await visit(`/editor/post/${post.id}`); + await click('[data-test-button="publish-flow"]'); + await click('[data-test-setting="publish-type"] [data-test-setting-title]'); + + // without this the verification hold alone would satisfy the assertion + // below, even with the limit never registered + // the adapter turns limit: 'all' into paged requests, so this asserts what + // reaches the endpoint rather than what getEmailsCount asked for + const emailCountQuery = emailRequests.find(query => query.fields === 'id,email_count'); + expect(emailCountQuery, 'email count query').to.exist; + expect(emailCountQuery.filter).to.match(/^created_at:>='/); + + expect(find('[data-test-publish-type-error="email-disabled"]'), 'email disabled error').to.exist; + expect(find('[data-test-publish-type-error="email-disabled"]'), 'email disabled error') + .to.have.trimmed.text('Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org.'); + }); + it('(as editor) handles over-member limits', async function () { // set members limit const config = this.server.db.configs.find(1); diff --git a/apps/ember-admin/tests/unit/services/limit-test.js b/apps/ember-admin/tests/unit/services/limit-test.js index 222f51cf7fc..0e3b3fc8787 100644 --- a/apps/ember-admin/tests/unit/services/limit-test.js +++ b/apps/ember-admin/tests/unit/services/limit-test.js @@ -1,3 +1,4 @@ +import sinon from 'sinon'; import {describe, it} from 'mocha'; import {expect} from 'chai'; import {setupTest} from 'ember-mocha'; @@ -11,7 +12,85 @@ describe('Unit | Service | limit', function () { limitService = this.owner.lookup('service:limit'); }); + afterEach(function () { + sinon.restore(); + }); + it('exists', function () { expect(limitService).to.be.ok; }); + + describe('loadLimits', function () { + it('registers a periodic limit when a subscription start is configured', function () { + limitService.config.hostSettings = { + subscription: {start: '2026-01-01T00:00:00.000Z'}, + limits: {emails: {maxPeriodic: 100}} + }; + + limitService.loadLimits(); + + expect(limitService.limiter.isLimited('emails')).to.be.true; + }); + + // Without a subscription the limit service throws, which aborts the loop + // registering limits and leaves Admin with none of them + it('skips a periodic limit rather than throwing when no subscription is configured', function () { + limitService.config.hostSettings = { + limits: {emails: {maxPeriodic: 100}} + }; + + expect(() => limitService.loadLimits()).to.not.throw(); + expect(limitService.limiter.isLimited('emails')).to.be.false; + }); + + // A subscription with no start can't anchor a period; building one anyway + // throws on the missing start date and leaves the count query without one + it('skips a periodic limit when the subscription has no start date', function () { + limitService.config.hostSettings = { + subscription: {}, + limits: {emails: {maxPeriodic: 100}} + }; + + expect(() => limitService.loadLimits()).to.not.throw(); + expect(limitService.limiter.isLimited('emails')).to.be.false; + }); + + it('still registers limits declared after an unusable periodic limit', function () { + limitService.config.hostSettings = { + limits: { + emails: {maxPeriodic: 100}, + customThemes: {allowlist: ['casper']} + } + }; + + limitService.loadLimits(); + + expect(limitService.limiter.isLimited('emails')).to.be.false; + expect(limitService.limiter.isLimited('customThemes')).to.be.true; + }); + }); + + describe('getEmailsCount', function () { + it('sums recipients of emails sent since the start of the period', async function () { + const query = sinon.stub().resolves([ + {emailCount: 30}, + {emailCount: 12} + ]); + limitService.set('store', {query}); + + const count = await limitService.getEmailsCount(undefined, '2026-01-01T00:00:00.000Z'); + + expect(count).to.equal(42); + expect(query.firstCall.args[0]).to.equal('email'); + expect(query.firstCall.args[1].filter).to.equal(`created_at:>='2026-01-01T00:00:00.000Z'`); + // emails carry their full html and plaintext bodies, which this has no use for + expect(query.firstCall.args[1].fields).to.equal('id,email_count'); + }); + + it('counts nothing when no emails were sent in the period', async function () { + limitService.set('store', {query: sinon.stub().resolves([])}); + + expect(await limitService.getEmailsCount(undefined, '2026-01-01T00:00:00.000Z')).to.equal(0); + }); + }); }); diff --git a/apps/ember-admin/tests/unit/utils/upgrade-url-test.js b/apps/ember-admin/tests/unit/utils/upgrade-url-test.js new file mode 100644 index 00000000000..066def7f19e --- /dev/null +++ b/apps/ember-admin/tests/unit/utils/upgrade-url-test.js @@ -0,0 +1,36 @@ +import upgradeUrl from 'ghost-admin/utils/upgrade-url'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; + +describe('Unit | Utility | upgrade-url', function () { + const configWith = url => ({hostSettings: {billing: {upgradeUrl: url}}}); + + it('falls back to Ghost(Pro) billing when nothing is configured', function () { + expect(upgradeUrl({})).to.equal('#/pro'); + expect(upgradeUrl(configWith(undefined))).to.equal('#/pro'); + }); + + // the publish-limit modal linked straight into checkout before this was configurable + it('uses the caller\'s fallback when one is given', function () { + expect(upgradeUrl({}, '#/pro?action=checkout')).to.equal('#/pro?action=checkout'); + }); + + it('keeps a configured hash href as-is', function () { + expect(upgradeUrl(configWith('#/pro/billing/plans'))).to.equal('#/pro/billing/plans'); + }); + + it('keeps a configured billing URL as-is', function () { + expect(upgradeUrl(configWith('https://billing.example.com'))).to.equal('https://billing.example.com'); + }); + + // an unanchored value would resolve against whatever page it is rendered on + it('anchors a value that is neither a hash nor a URL', function () { + expect(upgradeUrl(configWith('pro/billing'))).to.equal('#/pro/billing'); + expect(upgradeUrl(configWith('/pro/billing'))).to.equal('#/pro/billing'); + }); + + it('rejects a scheme that is not http(s)', function () { + expect(upgradeUrl(configWith('javascript:alert(1)'))).to.equal('#/pro'); + expect(upgradeUrl(configWith('data:text/html,hi'))).to.equal('#/pro'); + }); +}); diff --git a/compose.dev.analytics.yaml b/compose.dev.analytics.yaml index ffcae8f2caf..869aa0c6bfa 100644 --- a/compose.dev.analytics.yaml +++ b/compose.dev.analytics.yaml @@ -31,7 +31,7 @@ services: condition: service_completed_successfully tinybird-local: - image: tinybirdco/tinybird-local:latest@sha256:0865fb2f8aef0b8f0cd3ffe53a7a9832787b2d995fd0d34ddaa6563f1638feed + image: tinybirdco/tinybird-local:latest@sha256:cf9de0516700eea366161181e74cae943f1a7310e6f2cc4f5762cb1777ed3fb3 container_name: ghost-dev-tinybird platform: linux/amd64 stop_grace_period: 2s diff --git a/compose.dev.yaml b/compose.dev.yaml index 04f247047ef..a0781b2c0e8 100644 --- a/compose.dev.yaml +++ b/compose.dev.yaml @@ -144,7 +144,7 @@ services: condition: service_healthy stripe: - image: stripe/stripe-cli:latest@sha256:4b7f2994f827ad134185daf180a8b70d7340f24d3614e1d70d2d09bf59b77980 + image: stripe/stripe-cli:latest@sha256:e35a010404b9b527249283bad6b1360430f4eb22a2f7726dc1d8735509a5fbda container_name: ghost-dev-stripe entrypoint: ["/entrypoint.sh"] profiles: ["stripe"] diff --git a/docker/dev-gateway/Dockerfile b/docker/dev-gateway/Dockerfile index 2ef35fc12a5..f782d7dc7bd 100644 --- a/docker/dev-gateway/Dockerfile +++ b/docker/dev-gateway/Dockerfile @@ -1,4 +1,4 @@ -FROM caddy:2.11.4-builder-alpine@sha256:8e89605351333ad2cc2f3bcc95275a2ccc427f88914050e86a5fde0fd77a63c4 AS builder +FROM caddy:2.11.4-builder-alpine@sha256:2b9994510fadb5dfa5257a5357cbe26a2c4a3298f8cc675796a6570218280ce7 AS builder RUN xcaddy build --with github.com/caddyserver/transform-encoder@ba4124974830222da7f12a091cf11ddf4d49363f diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js index b79f537cb0e..1fb353095ff 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/email-batches.js @@ -2,7 +2,7 @@ const _ = require('lodash'); const batchFields = [ 'id', - 'provider_id', + 'mailgun_message_id', 'status', 'member_segment', 'created_at', diff --git a/ghost/core/core/server/data/migrations/utils/schema.js b/ghost/core/core/server/data/migrations/utils/schema.js index 298fe27dc98..e986e07f69a 100644 --- a/ghost/core/core/server/data/migrations/utils/schema.js +++ b/ghost/core/core/server/data/migrations/utils/schema.js @@ -210,17 +210,19 @@ function createAddIndexMigration(table, columns, options = {}) { * @param {string} table * @param {string} from * @param {string} to + * @param {object} [options] + * @param {'instant'|'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only * * @returns {Migration} */ -function createRenameColumnMigration(table, from, to) { +function createRenameColumnMigration(table, from, to, options = {}) { return createNonTransactionalMigration( async function up(knex) { const hasColumn = await knex.schema.hasColumn(table, to); if (hasColumn) { logging.warn(`Renaming ${table}.${from} to ${table}.${to} column - skipping as column ${table}.${to} already exists`); } else { - await commands.renameColumn(table, from, to, knex); + await commands.renameColumn(table, from, to, knex, options); } }, async function down(knex) { @@ -228,7 +230,7 @@ function createRenameColumnMigration(table, from, to) { if (hasColumn) { logging.warn(`Renaming ${table}.${to} to ${table}.${from} column - skipping as column ${table}.${from} already exists`); } else { - await commands.renameColumn(table, to, from, knex); + await commands.renameColumn(table, to, from, knex, options); } } ); diff --git a/ghost/core/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js b/ghost/core/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js new file mode 100644 index 00000000000..1756a7806ee --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js @@ -0,0 +1,8 @@ +const {createRenameColumnMigration} = require('../../utils'); + +module.exports = createRenameColumnMigration( + 'email_batches', + 'provider_id', + 'mailgun_message_id', + {algorithm: 'instant'} +); diff --git a/ghost/core/core/server/data/migrations/versions/6.58/2026-08-13-16-07-33-add-automation-runs-automation-id-created-at-index.js b/ghost/core/core/server/data/migrations/versions/6.58/2026-08-13-16-07-33-add-automation-runs-automation-id-created-at-index.js new file mode 100644 index 00000000000..a66cf357942 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/6.58/2026-08-13-16-07-33-add-automation-runs-automation-id-created-at-index.js @@ -0,0 +1,28 @@ +const {createNonTransactionalMigration} = require('../../utils'); +const {addIndex, dropIndex, getIndexes} = require('../../../schema/commands'); + +const table = 'automation_runs'; +const compositeIndexColumns = ['automation_id', 'created_at']; +const foreignKeyIndexColumns = ['automation_id']; +const foreignKeyIndexName = 'automation_runs_automation_id_index'; + +module.exports = createNonTransactionalMigration( + async function up(knex) { + await addIndex(table, compositeIndexColumns, knex); + + // If this migration was rolled back (see `down`), we need to drop the + // index it created. + const indexes = await getIndexes(table, knex); + if (indexes.includes(foreignKeyIndexName)) { + await dropIndex(table, foreignKeyIndexColumns, knex); + } + }, + async function down(knex) { + // Before this whole migration, MySQL had an implicit index on + // `automation_id` because it was referenced by foreign keys. Adding the + // composite index in `up` removes that, so we need to add it back if we + // reverse the migration. + await addIndex(table, foreignKeyIndexColumns, knex); + await dropIndex(table, compositeIndexColumns, knex); + } +); \ No newline at end of file diff --git a/ghost/core/core/server/data/schema/commands.js b/ghost/core/core/server/data/schema/commands.js index 5997494afe6..b2075a55dc5 100644 --- a/ghost/core/core/server/data/schema/commands.js +++ b/ghost/core/core/server/data/schema/commands.js @@ -183,13 +183,16 @@ async function dropColumn(tableName, column, transaction = db.knex, columnSpec = * @param {string} from * @param {string} to * @param {import('knex').Knex.Transaction} [transaction] + * @param {object} [options] + * @param {'instant'|'inplace'|'copy'|'auto'} [options.algorithm] - MySQL only */ -async function renameColumn(tableName, from, to, transaction = db.knex) { +async function renameColumn(tableName, from, to, transaction = db.knex, options = {}) { logging.info(`Renaming column '${from}' to '${to}' in table '${tableName}'`); if (DatabaseInfo.isMySQL(transaction)) { // The knex helper does a lot of interesting things with foreign keys that are slow on bigger MySQL clusters - return await transaction.raw(`ALTER TABLE \`${tableName}\` RENAME COLUMN \`${from}\` TO \`${to}\`;`); + const algorithm = options.algorithm && options.algorithm !== 'auto' ? `, algorithm=${options.algorithm}` : ''; + return await transaction.raw(`ALTER TABLE \`${tableName}\` RENAME COLUMN \`${from}\` TO \`${to}\`${algorithm};`); } return await transaction.schema.table(tableName, function (table) { diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index 986a91865a0..ae9b0db9690 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -974,7 +974,7 @@ module.exports = { email_batches: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, email_id: {type: 'string', maxlength: 24, nullable: false, references: 'emails.id'}, - provider_id: {type: 'string', maxlength: 255, nullable: true}, + mailgun_message_id: {type: 'string', maxlength: 255, nullable: true}, fallback_sending_domain: {type: 'boolean', nullable: false, defaultTo: false}, status: { type: 'string', @@ -1331,7 +1331,10 @@ module.exports = { updated_at: {type: 'dateTime', nullable: false}, automation_id: {type: 'string', maxlength: 24, nullable: false, references: 'automations.id', restrictDelete: true}, member_id: {type: 'string', maxlength: 24, nullable: true, references: 'members.id', setNullDelete: true, index: true}, - member_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}} + member_email: {type: 'string', maxlength: 191, nullable: false, validations: {isEmail: true}}, + '@@INDEXES@@': [ + ['automation_id', 'created_at'] + ] }, automation_run_steps: { id: {type: 'string', maxlength: 24, nullable: false, primary: true}, diff --git a/ghost/core/core/server/data/seeders/data-generator.js b/ghost/core/core/server/data/seeders/data-generator.js index 39429223f66..f5a93ea3b62 100644 --- a/ghost/core/core/server/data/seeders/data-generator.js +++ b/ghost/core/core/server/data/seeders/data-generator.js @@ -61,7 +61,9 @@ class DataGenerator { const referencedTable = data.references.split('.')[0]; // Some nullable relationships point to tables the generator does not populate. // Generated redirects belong to posts, so automation revisions are not a seed dependency. - const isUnsupportedOptionalDependency = referencedTable === 'subscriptions' || (referencedTable === 'automation_action_revisions' && data.nullable); + const isUnsupportedOptionalDependency = referencedTable === 'subscriptions' + || (referencedTable === 'automation_action_revisions' && data.nullable) + || (table.name === 'automation_action_revisions' && referencedTable === 'email_design_settings' && data.nullable); if (!acc.includes(referencedTable) && !isUnsupportedOptionalDependency) { acc.push(referencedTable); } diff --git a/ghost/core/core/server/data/seeders/importers/automation-action-edges-importer.ts b/ghost/core/core/server/data/seeders/importers/automation-action-edges-importer.ts new file mode 100644 index 00000000000..ba9925228a7 --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automation-action-edges-importer.ts @@ -0,0 +1,52 @@ +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; + +type AutomationAction = { + id: string; + automation_id: string; + created_at: string; +}; + +type AutomationActionEdge = { + source_action_id: string; + target_action_id: string; +}; + +export class AutomationActionEdgesImporter extends TableImporter { + static table = 'automation_action_edges'; + static dependencies = ['automation_actions']; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationActionEdgesImporter.table, knex, transaction); + } + + async import(): Promise { + const actions = await this.transaction + .select('id', 'automation_id', 'created_at') + .from('automation_actions') + .orderBy('automation_id') + .orderBy('created_at') + .orderBy('id'); + + const actionsByAutomation = new Map(); + for (const action of actions) { + const automationActions = actionsByAutomation.get(action.automation_id) ?? []; + automationActions.push(action); + actionsByAutomation.set(action.automation_id, automationActions); + } + + const edges: AutomationActionEdge[] = []; + for (const automationActions of actionsByAutomation.values()) { + for (let index = 1; index < automationActions.length; index += 1) { + edges.push({ + source_action_id: automationActions[index - 1].id, + target_action_id: automationActions[index].id + }); + } + } + + if (edges.length > 0) { + await this.batchInsert(edges); + } + } +} diff --git a/ghost/core/core/server/data/seeders/importers/automation-action-revisions-importer.ts b/ghost/core/core/server/data/seeders/importers/automation-action-revisions-importer.ts new file mode 100644 index 00000000000..31bc492df27 --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automation-action-revisions-importer.ts @@ -0,0 +1,132 @@ +import {faker} from '@faker-js/faker'; +import errors from '@tryghost/errors'; +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; +// @ts-expect-error This module currently lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; +import {DEFAULT_EMAIL_DESIGN_SETTING_SLUG} from '../../../services/member-welcome-emails/constants'; + +type AutomationAction = { + id: string; + type: 'wait' | 'send_email'; + created_at: string; +}; + +type AutomationActionRevision = { + id: string; + created_at: string; + action_id: string; + wait_hours: number | null; + email_subject: string | null; + email_lexical: string | null; + email_design_setting_id: string | null; + email_sent_count: number | null; + email_opened_count: number | null; + email_clicked_count: number | null; +}; + +export class AutomationActionRevisionsImporter extends TableImporter { + static table = 'automation_action_revisions'; + static dependencies = ['automation_actions']; + + #action?: AutomationAction; + #emailDesignSettingId?: string; + #revisionIndex = 0; + + defaultQuantity = 16; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationActionRevisionsImporter.table, knex, transaction); + } + + async import(quantity = this.defaultQuantity): Promise { + const actions = await this.transaction.select('id', 'type', 'created_at').from('automation_actions'); + if (actions.length === 0) { + return; + } + + const emailDesignSetting = await this.transaction('email_design_settings') + .select('id') + .where('slug', DEFAULT_EMAIL_DESIGN_SETTING_SLUG) + .first<{id: string}>(); + if (!emailDesignSetting) { + throw new errors.InternalServerError({message: `Missing email design setting: ${DEFAULT_EMAIL_DESIGN_SETTING_SLUG}`}); + } + this.#emailDesignSettingId = emailDesignSetting.id; + + await this.importForEach(actions, quantity / actions.length); + } + + setReferencedModel(action: AutomationAction): void { + this.#action = action; + this.#revisionIndex = 0; + } + + generate(): AutomationActionRevision { + if (!this.#action) { + throw new errors.IncorrectUsageError({message: 'Cannot generate automation action revision without an action'}); + } + + const createdAt = dateToDatabaseString.parse(this.#action.created_at); + createdAt.setSeconds(createdAt.getSeconds() + this.#revisionIndex); + this.#revisionIndex += 1; + + const common = { + id: this.fastFakeObjectId(), + created_at: dateToDatabaseString(createdAt), + action_id: this.#action.id, + email_sent_count: null, + email_opened_count: null, + email_clicked_count: null + }; + + switch (this.#action.type) { + case 'wait': + return { + ...common, + wait_hours: faker.number.int({min: 1, max: 30}) * 24, + email_subject: null, + email_lexical: null, + email_design_setting_id: null + }; + case 'send_email': + if (!this.#emailDesignSettingId) { + throw new errors.InternalServerError({message: `Missing email design setting: ${DEFAULT_EMAIL_DESIGN_SETTING_SLUG}`}); + } + const emailBody = faker.lorem.sentence(); + return { + ...common, + wait_hours: null, + email_subject: faker.lorem.sentence(), + email_design_setting_id: this.#emailDesignSettingId, + email_lexical: JSON.stringify({ + root: { + children: [{ + children: [{ + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: emailBody, + type: 'text', + version: 1 + }], + direction: null, + format: '', + indent: 0, + type: 'paragraph', + version: 1 + }], + direction: null, + format: '', + indent: 0, + type: 'root', + version: 1 + } + }) + }; + default: + throw new errors.IncorrectUsageError({message: `Unknown automation action type: ${this.#action.type}`}); + } + } +} diff --git a/ghost/core/core/server/data/seeders/importers/automation-actions-importer.ts b/ghost/core/core/server/data/seeders/importers/automation-actions-importer.ts new file mode 100644 index 00000000000..1a431f2786f --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automation-actions-importer.ts @@ -0,0 +1,70 @@ +import {faker} from '@faker-js/faker'; +import errors from '@tryghost/errors'; +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; +// @ts-expect-error This module currently lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; + +type Automation = { + id: string; + created_at: string; +}; + +type AutomationAction = { + id: string; + created_at: string; + updated_at: string; + deleted_at: null; + automation_id: string; + type: 'wait' | 'send_email'; +}; + +export class AutomationActionsImporter extends TableImporter { + static table = 'automation_actions'; + static dependencies = ['automations']; + + #automation?: Automation; + #actionIndex = 0; + + defaultQuantity = 8; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationActionsImporter.table, knex, transaction); + } + + async import(quantity = this.defaultQuantity): Promise { + const automations = await this.transaction.select('id', 'created_at').from('automations'); + if (automations.length === 0) { + return; + } + + await this.importForEach(automations, quantity / automations.length); + } + + setReferencedModel(automation: Automation): void { + this.#automation = automation; + this.#actionIndex = 0; + } + + generate(): AutomationAction { + if (!this.#automation) { + throw new errors.IncorrectUsageError({message: 'Cannot generate automation action without an automation'}); + } + + const createdAt = faker.date.between({ + from: dateToDatabaseString.parse(this.#automation.created_at), + to: new Date() + }); + const type = this.#actionIndex % 2 === 0 ? 'wait' : 'send_email'; + this.#actionIndex += 1; + + return { + id: this.fastFakeObjectId(), + created_at: dateToDatabaseString(createdAt), + updated_at: dateToDatabaseString(createdAt), + deleted_at: null, + automation_id: this.#automation.id, + type + }; + } +} diff --git a/ghost/core/core/server/data/seeders/importers/automation-run-steps-importer.ts b/ghost/core/core/server/data/seeders/importers/automation-run-steps-importer.ts new file mode 100644 index 00000000000..ad94d00366b --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automation-run-steps-importer.ts @@ -0,0 +1,148 @@ +import {faker} from '@faker-js/faker'; +import errors from '@tryghost/errors'; +import {clamp} from 'lodash'; +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; +// @ts-expect-error This module currently lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; + +type AutomationRun = { + id: string; + automation_id: string; + created_at: string; +}; + +type ActionRevision = { + action_id: string; + automation_id: string; + revision_id: string; + revision_created_at: string; +}; + +type ActionEdge = { + source_action_id: string; + target_action_id: string; +}; + +type AutomationRunStep = { + id: string; + created_at: string; + updated_at: string; + automation_run_id: string; + automation_action_revision_id: string; + ready_at: string; + step_attempts: number; + started_at: string | null; + finished_at: string | null; + status: 'pending' | 'finished' | 'failed'; + locked_by: null; + locked_at: null; +}; + +export class AutomationRunStepsImporter extends TableImporter { + static table = 'automation_run_steps'; + static dependencies = ['automation_runs', 'automation_action_revisions', 'automation_action_edges']; + + defaultQuantity = 40; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationRunStepsImporter.table, knex, transaction); + } + + async import(quantity = this.defaultQuantity): Promise { + const runs = await this.transaction.select('id', 'automation_id', 'created_at').from('automation_runs'); + if (runs.length === 0) { + return; + } + + const actionRevisions = await this.transaction('automation_actions as action') + .select( + 'action.id as action_id', + 'action.automation_id as automation_id', + 'revision.id as revision_id', + 'revision.created_at as revision_created_at' + ) + .innerJoin('automation_action_revisions as revision', 'revision.action_id', 'action.id') as ActionRevision[]; + const edges = await this.transaction.select('source_action_id', 'target_action_id').from('automation_action_edges'); + + const latestRevisionByAction = new Map(); + for (const revision of actionRevisions) { + const latest = latestRevisionByAction.get(revision.action_id); + if (!latest || revision.revision_created_at > latest.revision_created_at || (revision.revision_created_at === latest.revision_created_at && revision.revision_id > latest.revision_id)) { + latestRevisionByAction.set(revision.action_id, revision); + } + } + + const targetActionIds = new Set(edges.map(edge => edge.target_action_id)); + const nextActionBySource = new Map(edges.map(edge => [edge.source_action_id, edge.target_action_id])); + const actionCountByAutomation = new Map(); + const actionPathByAutomation = new Map(); + + for (const revision of latestRevisionByAction.values()) { + actionCountByAutomation.set(revision.automation_id, (actionCountByAutomation.get(revision.automation_id) ?? 0) + 1); + if (targetActionIds.has(revision.action_id)) { + continue; + } + + const path: ActionRevision[] = []; + let current: ActionRevision | undefined = revision; + while (current) { + path.push(current); + const nextActionId = nextActionBySource.get(current.action_id); + current = nextActionId ? latestRevisionByAction.get(nextActionId) : undefined; + } + actionPathByAutomation.set(revision.automation_id, path); + } + + const steps: AutomationRunStep[] = []; + const stepsPerRun = quantity / runs.length; + const fullPathGeneratedForAutomation = new Set(); + let fractionalCarry = 0; + + for (const run of runs) { + const actionPath = actionPathByAutomation.get(run.automation_id) ?? []; + const actionCount = actionCountByAutomation.get(run.automation_id) ?? 0; + if (actionPath.length === 0) { + throw new errors.InternalServerError({message: `Missing action path for automation run: ${run.id}`}); + } + if (actionPath.length !== actionCount) { + throw new errors.InternalServerError({message: `Incomplete action path for automation run: ${run.id}`}); + } + fractionalCarry += stepsPerRun % 1; + const extraStep = fractionalCarry >= 1 ? 1 : 0; + fractionalCarry -= extraStep; + const requestedStepCount = clamp(Math.floor(stepsPerRun) + extraStep, 1, actionCount); + const runStepCount = fullPathGeneratedForAutomation.has(run.automation_id) ? requestedStepCount : actionCount; + fullPathGeneratedForAutomation.add(run.automation_id); + const runCreatedAt = dateToDatabaseString.parse(run.created_at); + const lastStepStatus = faker.helpers.arrayElement(['pending', 'finished', 'failed'] as const); + + for (let index = 0; index < runStepCount; index += 1) { + const createdAt = new Date(runCreatedAt); + createdAt.setHours(createdAt.getHours() + index); + const createdAtString = dateToDatabaseString(createdAt); + const status = index === runStepCount - 1 ? lastStepStatus : 'finished'; + const isTerminal = status !== 'pending'; + + steps.push({ + id: this.fastFakeObjectId(), + created_at: createdAtString, + updated_at: createdAtString, + automation_run_id: run.id, + automation_action_revision_id: actionPath[index].revision_id, + ready_at: createdAtString, + step_attempts: isTerminal ? 1 : 0, + started_at: isTerminal ? createdAtString : null, + finished_at: isTerminal ? createdAtString : null, + status, + locked_by: null, + locked_at: null + }); + } + } + + if (steps.length > 0) { + await this.batchInsert(steps); + } + } +} diff --git a/ghost/core/core/server/data/seeders/importers/automation-runs-importer.ts b/ghost/core/core/server/data/seeders/importers/automation-runs-importer.ts new file mode 100644 index 00000000000..bccef290ae2 --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automation-runs-importer.ts @@ -0,0 +1,95 @@ +import {faker} from '@faker-js/faker'; +import errors from '@tryghost/errors'; +import assert from 'node:assert/strict'; +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; +import {parseEmailAddress} from '@tryghost/parse-email-address'; +// @ts-expect-error This module currently lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; + +type Automation = { + id: string; + created_at: string; +}; + +type Member = { + id: string; + email: string; + created_at: string; +}; + +type AutomationRun = { + id: string; + created_at: string; + updated_at: string; + automation_id: string; + member_id: string; + member_email: string; +}; + +const assertExampleEmailDomain = (email: string) => { + const {domain} = parseEmailAddress(email) ?? {}; + assert(domain, 'Refusing to seed an automation run for a member with no email'); + assert( + domain === 'example.com' || + domain === 'example.net' || + domain === 'example.org' || + domain === 'example.edu' || + domain.endsWith('.example'), + `Refusing to seed an automation run for non-example email: ${email}` + ); +}; + +export class AutomationRunsImporter extends TableImporter { + static table = 'automation_runs'; + static dependencies = ['automations', 'members']; + + #automation?: Automation; + #members: Member[] = []; + + defaultQuantity = 20; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationRunsImporter.table, knex, transaction); + } + + async import(quantity = this.defaultQuantity): Promise { + const automations = await this.transaction.select('id', 'created_at').from('automations'); + this.#members = await this.transaction.select('id', 'email', 'created_at').from('members'); + + if (automations.length === 0 || this.#members.length === 0) { + return; + } + + await this.importForEach(automations, quantity / automations.length); + } + + setReferencedModel(automation: Automation): void { + this.#automation = automation; + } + + generate(): AutomationRun { + if (!this.#automation) { + throw new errors.IncorrectUsageError({message: 'Cannot generate automation run without an automation'}); + } + + const member = faker.helpers.arrayElement(this.#members); + const automationCreatedAt = dateToDatabaseString.parse(this.#automation.created_at); + const memberCreatedAt = dateToDatabaseString.parse(member.created_at); + const createdAt = faker.date.between({ + from: new Date(Math.max(automationCreatedAt.valueOf(), memberCreatedAt.valueOf())), + to: new Date() + }); + + assertExampleEmailDomain(member.email); + + return { + id: this.fastFakeObjectId(), + created_at: dateToDatabaseString(createdAt), + updated_at: dateToDatabaseString(createdAt), + automation_id: this.#automation.id, + member_id: member.id, + member_email: member.email + }; + } +} diff --git a/ghost/core/core/server/data/seeders/importers/automations-importer.ts b/ghost/core/core/server/data/seeders/importers/automations-importer.ts new file mode 100644 index 00000000000..e2b6d908cc3 --- /dev/null +++ b/ghost/core/core/server/data/seeders/importers/automations-importer.ts @@ -0,0 +1,60 @@ +import {faker} from '@faker-js/faker'; +// @ts-expect-error This module currently lacks type definitions. +import {slugify} from '@tryghost/string'; +import type {Knex} from 'knex'; +import {TableImporter} from './table-importer'; +// @ts-expect-error This module currently lacks type definitions. +import {blogStartDate} from '../utils/blog-info'; +// @ts-expect-error This module currently lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; +import {MEMBER_WELCOME_EMAIL_SLUGS} from '../../../services/member-welcome-emails/constants'; + +type Automation = { + id: string; + status: 'active' | 'inactive'; + name: string; + slug: string; + created_at: string; + updated_at: string; +}; + +const defaultAutomations = [{ + name: 'Free member welcome flow', + slug: MEMBER_WELCOME_EMAIL_SLUGS.free +}, { + name: 'Paid member welcome flow', + slug: MEMBER_WELCOME_EMAIL_SLUGS.paid +}]; + +export class AutomationsImporter extends TableImporter { + static table = 'automations'; + static dependencies: string[] = []; + + #generated = 0; + + defaultQuantity = 2; + + constructor(knex: Knex, transaction: Knex.Transaction) { + super(AutomationsImporter.table, knex, transaction); + } + + generate(): Automation { + const id = this.fastFakeObjectId(); + const defaultAutomation = defaultAutomations[this.#generated]; + const randomName = `${faker.word.adjective()} ${faker.word.noun()} flow`; + const name = defaultAutomation?.name ?? `${randomName} ${id}`; + const slug = defaultAutomation?.slug ?? `${slugify(randomName)}-${id}`; + const createdAt = faker.date.between({from: blogStartDate, to: new Date()}); + + this.#generated += 1; + + return { + id, + status: faker.helpers.arrayElement(['active', 'inactive']), + name, + slug, + created_at: dateToDatabaseString(createdAt), + updated_at: dateToDatabaseString(createdAt) + }; + } +} diff --git a/ghost/core/core/server/data/seeders/importers/benefits-importer.js b/ghost/core/core/server/data/seeders/importers/benefits-importer.js index e1ddd84d9ac..cb62878ff8c 100644 --- a/ghost/core/core/server/data/seeders/importers/benefits-importer.js +++ b/ghost/core/core/server/data/seeders/importers/benefits-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); const {blogStartDate} = require('../utils/blog-info'); diff --git a/ghost/core/core/server/data/seeders/importers/comment-reports-importer.js b/ghost/core/core/server/data/seeders/importers/comment-reports-importer.js index a0ebf823d27..d4f493f10fe 100644 --- a/ghost/core/core/server/data/seeders/importers/comment-reports-importer.js +++ b/ghost/core/core/server/data/seeders/importers/comment-reports-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/comments-importer.js b/ghost/core/core/server/data/seeders/importers/comments-importer.js index 58406b2b80c..302e50f30dc 100644 --- a/ghost/core/core/server/data/seeders/importers/comments-importer.js +++ b/ghost/core/core/server/data/seeders/importers/comments-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {luck} = require('../utils/random'); const generateEvents = require('../utils/event-generator'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/email-batches-importer.js b/ghost/core/core/server/data/seeders/importers/email-batches-importer.js index a41496d6a39..592c5629e8a 100644 --- a/ghost/core/core/server/data/seeders/importers/email-batches-importer.js +++ b/ghost/core/core/server/data/seeders/importers/email-batches-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const dateToDatabaseString = require('../utils/database-date'); @@ -14,9 +14,10 @@ class EmailBatchesImporter extends TableImporter { const emails = await this.transaction.select('id', 'created_at', 'email_count').from('emails'); // 1 batch per 1000 recipients - await this.importForEach(emails, quantity ?? (() => { + const amount = typeof quantity === 'number' ? Math.ceil(quantity / emails.length) : () => { return Math.ceil(this.model.email_count / 1000); - })); + }; + await this.importForEach(emails, amount); } generate() { @@ -27,7 +28,7 @@ class EmailBatchesImporter extends TableImporter { return { id: this.fastFakeObjectId(), email_id: this.model.id, - provider_id: `${new Date().toISOString().split('.')[0].replace(/[^0-9]/g, '')}.${faker.string.hexadecimal({length: 16, prefix: '', casing: 'lower'})}@m.example.com`, + mailgun_message_id: `${new Date().toISOString().split('.')[0].replace(/[^0-9]/g, '')}.${faker.string.hexadecimal({length: 16, prefix: '', casing: 'lower'})}@m.example.com`, status: 'submitted', // TODO: introduce failures created_at: this.model.created_at, updated_at: dateToDatabaseString(dateToDatabaseString.randomBetween(emailSentDate, latestUpdatedDate)) diff --git a/ghost/core/core/server/data/seeders/importers/email-recipient-failures-importer.js b/ghost/core/core/server/data/seeders/importers/email-recipient-failures-importer.js index 1d80b752dd5..c28152012ae 100644 --- a/ghost/core/core/server/data/seeders/importers/email-recipient-failures-importer.js +++ b/ghost/core/core/server/data/seeders/importers/email-recipient-failures-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); class EmailRecipientFailuresImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/email-recipients-importer.js b/ghost/core/core/server/data/seeders/importers/email-recipients-importer.js index 88020253b55..bbb1673e4e7 100644 --- a/ghost/core/core/server/data/seeders/importers/email-recipients-importer.js +++ b/ghost/core/core/server/data/seeders/importers/email-recipients-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const generateEvents = require('../utils/event-generator'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/emails-importer.js b/ghost/core/core/server/data/seeders/importers/emails-importer.js index 0465997c513..56c2d4d779c 100644 --- a/ghost/core/core/server/data/seeders/importers/emails-importer.js +++ b/ghost/core/core/server/data/seeders/importers/emails-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const generateEvents = require('../utils/event-generator'); const {luck} = require('../utils/random'); diff --git a/ghost/core/core/server/data/seeders/importers/index.js b/ghost/core/core/server/data/seeders/importers/index.js index 87f8bb0150f..b6a6abd1569 100644 --- a/ghost/core/core/server/data/seeders/importers/index.js +++ b/ghost/core/core/server/data/seeders/importers/index.js @@ -1,4 +1,10 @@ module.exports = [ + require('./automations-importer').AutomationsImporter, + require('./automation-actions-importer').AutomationActionsImporter, + require('./automation-action-revisions-importer').AutomationActionRevisionsImporter, + require('./automation-action-edges-importer').AutomationActionEdgesImporter, + require('./automation-runs-importer').AutomationRunsImporter, + require('./automation-run-steps-importer').AutomationRunStepsImporter, require('./newsletters-importer'), require('./posts-importer'), require('./users-importer'), diff --git a/ghost/core/core/server/data/seeders/importers/labels-importer.js b/ghost/core/core/server/data/seeders/importers/labels-importer.js index 6c3cd93e620..f0dd07fef63 100644 --- a/ghost/core/core/server/data/seeders/importers/labels-importer.js +++ b/ghost/core/core/server/data/seeders/importers/labels-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); const {blogStartDate} = require('../utils/blog-info'); diff --git a/ghost/core/core/server/data/seeders/importers/members-click-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-click-events-importer.js index 41e8c7e3bd2..4ce60711afb 100644 --- a/ghost/core/core/server/data/seeders/importers/members-click-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-click-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-created-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-created-events-importer.js index d6980e1e04a..bf3850fb94f 100644 --- a/ghost/core/core/server/data/seeders/importers/members-created-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-created-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-feedback-importer.js b/ghost/core/core/server/data/seeders/importers/members-feedback-importer.js index 82f0f86f22c..b413b3a92e3 100644 --- a/ghost/core/core/server/data/seeders/importers/members-feedback-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-feedback-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-importer.js b/ghost/core/core/server/data/seeders/importers/members-importer.js index 325338b5cbf..566d2e78a46 100644 --- a/ghost/core/core/server/data/seeders/importers/members-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {faker: americanFaker} = require('@faker-js/faker/locale/en_US'); const {blogStartDate: startTime} = require('../utils/blog-info'); diff --git a/ghost/core/core/server/data/seeders/importers/members-labels-importer.js b/ghost/core/core/server/data/seeders/importers/members-labels-importer.js index c97a61f11c3..306f8ca6a04 100644 --- a/ghost/core/core/server/data/seeders/importers/members-labels-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-labels-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); diff --git a/ghost/core/core/server/data/seeders/importers/members-login-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-login-events-importer.js index 4064621868e..d9a5b386645 100644 --- a/ghost/core/core/server/data/seeders/importers/members-login-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-login-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {luck} = require('../utils/random'); const generateEvents = require('../utils/event-generator'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-newsletters-importer.js b/ghost/core/core/server/data/seeders/importers/members-newsletters-importer.js index 2f97b1c8b33..1364a4acead 100644 --- a/ghost/core/core/server/data/seeders/importers/members-newsletters-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-newsletters-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class MembersNewslettersImporter extends TableImporter { static table = 'members_newsletters'; diff --git a/ghost/core/core/server/data/seeders/importers/members-paid-subscription-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-paid-subscription-events-importer.js index e0e2bf9093c..b0a3f5ae006 100644 --- a/ghost/core/core/server/data/seeders/importers/members-paid-subscription-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-paid-subscription-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class MembersPaidSubscriptionEventsImporter extends TableImporter { static table = 'members_paid_subscription_events'; diff --git a/ghost/core/core/server/data/seeders/importers/members-products-importer.js b/ghost/core/core/server/data/seeders/importers/members-products-importer.js index 24ff43fe998..65469d0aad8 100644 --- a/ghost/core/core/server/data/seeders/importers/members-products-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-products-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-status-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-status-events-importer.js index d6aca5a43f9..27e8e4d3d71 100644 --- a/ghost/core/core/server/data/seeders/importers/members-status-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-status-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); class MembersStatusEventsImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/members-stripe-customers-importer.js b/ghost/core/core/server/data/seeders/importers/members-stripe-customers-importer.js index ae2769766b0..480786cb821 100644 --- a/ghost/core/core/server/data/seeders/importers/members-stripe-customers-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-stripe-customers-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); class MembersStripeCustomersImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js b/ghost/core/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js index 6b04349a915..375a5fa5464 100644 --- a/ghost/core/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-stripe-customers-subscriptions-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); const generateEvents = require('../utils/event-generator'); const {luck} = require('../utils/random'); diff --git a/ghost/core/core/server/data/seeders/importers/members-subscribe-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-subscribe-events-importer.js index 259fa6934c1..fdf352cacaf 100644 --- a/ghost/core/core/server/data/seeders/importers/members-subscribe-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-subscribe-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/members-subscription-created-events-importer.js b/ghost/core/core/server/data/seeders/importers/members-subscription-created-events-importer.js index f5072f9da9f..8583edb4230 100644 --- a/ghost/core/core/server/data/seeders/importers/members-subscription-created-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/members-subscription-created-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/newsletters-importer.js b/ghost/core/core/server/data/seeders/importers/newsletters-importer.js index 792de9c0d5c..17141e8af39 100644 --- a/ghost/core/core/server/data/seeders/importers/newsletters-importer.js +++ b/ghost/core/core/server/data/seeders/importers/newsletters-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {blogStartDate} = require('../utils/blog-info'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); diff --git a/ghost/core/core/server/data/seeders/importers/offer-redemptions-importer.js b/ghost/core/core/server/data/seeders/importers/offer-redemptions-importer.js index 6ae8fcb843f..c1dd84867c7 100644 --- a/ghost/core/core/server/data/seeders/importers/offer-redemptions-importer.js +++ b/ghost/core/core/server/data/seeders/importers/offer-redemptions-importer.js @@ -1,6 +1,6 @@ const {faker} = require('@faker-js/faker'); const errors = require('@tryghost/errors'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); class OfferRedemptionsImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/offers-importer.js b/ghost/core/core/server/data/seeders/importers/offers-importer.js index d4969b6b821..bce624ca194 100644 --- a/ghost/core/core/server/data/seeders/importers/offers-importer.js +++ b/ghost/core/core/server/data/seeders/importers/offers-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {slugify} = require('@tryghost/string'); const {blogStartDate} = require('../utils/blog-info'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/importers/posts-authors-importer.js b/ghost/core/core/server/data/seeders/importers/posts-authors-importer.js index 001e93c05c7..2a29d25eb65 100644 --- a/ghost/core/core/server/data/seeders/importers/posts-authors-importer.js +++ b/ghost/core/core/server/data/seeders/importers/posts-authors-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class PostsAuthorsImporter extends TableImporter { static table = 'posts_authors'; diff --git a/ghost/core/core/server/data/seeders/importers/posts-importer.js b/ghost/core/core/server/data/seeders/importers/posts-importer.js index 2de87922b07..80d2af45215 100644 --- a/ghost/core/core/server/data/seeders/importers/posts-importer.js +++ b/ghost/core/core/server/data/seeders/importers/posts-importer.js @@ -1,7 +1,7 @@ const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); const {luck} = require('../utils/random'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); class PostsImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/posts-products-importer.js b/ghost/core/core/server/data/seeders/importers/posts-products-importer.js index 9b4767ee3f4..52c5878c390 100644 --- a/ghost/core/core/server/data/seeders/importers/posts-products-importer.js +++ b/ghost/core/core/server/data/seeders/importers/posts-products-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class PostsProductsImporter extends TableImporter { static table = 'posts_products'; diff --git a/ghost/core/core/server/data/seeders/importers/posts-tags-importer.js b/ghost/core/core/server/data/seeders/importers/posts-tags-importer.js index 1294c3b87d1..1079cbdffb8 100644 --- a/ghost/core/core/server/data/seeders/importers/posts-tags-importer.js +++ b/ghost/core/core/server/data/seeders/importers/posts-tags-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class PostsTagsImporter extends TableImporter { static table = 'posts_tags'; diff --git a/ghost/core/core/server/data/seeders/importers/products-benefits-importer.js b/ghost/core/core/server/data/seeders/importers/products-benefits-importer.js index 580b18a8484..ed3f0172dc6 100644 --- a/ghost/core/core/server/data/seeders/importers/products-benefits-importer.js +++ b/ghost/core/core/server/data/seeders/importers/products-benefits-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class ProductsBenefitsImporter extends TableImporter { static table = 'products_benefits'; diff --git a/ghost/core/core/server/data/seeders/importers/products-importer.js b/ghost/core/core/server/data/seeders/importers/products-importer.js index 98111d737a0..696d459dcdb 100644 --- a/ghost/core/core/server/data/seeders/importers/products-importer.js +++ b/ghost/core/core/server/data/seeders/importers/products-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); const {blogStartDate} = require('../utils/blog-info'); diff --git a/ghost/core/core/server/data/seeders/importers/recommendation-click-events-importer.js b/ghost/core/core/server/data/seeders/importers/recommendation-click-events-importer.js index c8d9b6d2dae..c3bd786a29c 100644 --- a/ghost/core/core/server/data/seeders/importers/recommendation-click-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/recommendation-click-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); diff --git a/ghost/core/core/server/data/seeders/importers/recommendation-subscribe-events-importer.js b/ghost/core/core/server/data/seeders/importers/recommendation-subscribe-events-importer.js index 7984291f00e..207bca03640 100644 --- a/ghost/core/core/server/data/seeders/importers/recommendation-subscribe-events-importer.js +++ b/ghost/core/core/server/data/seeders/importers/recommendation-subscribe-events-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {luck} = require('../utils/random'); diff --git a/ghost/core/core/server/data/seeders/importers/recommendations-importer.js b/ghost/core/core/server/data/seeders/importers/recommendations-importer.js index 3cb1af00c30..558303ee309 100644 --- a/ghost/core/core/server/data/seeders/importers/recommendations-importer.js +++ b/ghost/core/core/server/data/seeders/importers/recommendations-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); function capitalize(str) { diff --git a/ghost/core/core/server/data/seeders/importers/redirects-importer.js b/ghost/core/core/server/data/seeders/importers/redirects-importer.js index 67b644a4de6..8ca8c06acac 100644 --- a/ghost/core/core/server/data/seeders/importers/redirects-importer.js +++ b/ghost/core/core/server/data/seeders/importers/redirects-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); diff --git a/ghost/core/core/server/data/seeders/importers/roles-users-importer.js b/ghost/core/core/server/data/seeders/importers/roles-users-importer.js index d0b50515020..451e5657645 100644 --- a/ghost/core/core/server/data/seeders/importers/roles-users-importer.js +++ b/ghost/core/core/server/data/seeders/importers/roles-users-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); class RolesUsersImporter extends TableImporter { static table = 'roles_users'; diff --git a/ghost/core/core/server/data/seeders/importers/stripe-prices-importer.js b/ghost/core/core/server/data/seeders/importers/stripe-prices-importer.js index 4670f961f1c..9764c3ab76d 100644 --- a/ghost/core/core/server/data/seeders/importers/stripe-prices-importer.js +++ b/ghost/core/core/server/data/seeders/importers/stripe-prices-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {blogStartDate} = require('../utils/blog-info'); const sixWeeksLater = new Date(blogStartDate); diff --git a/ghost/core/core/server/data/seeders/importers/stripe-products-importer.js b/ghost/core/core/server/data/seeders/importers/stripe-products-importer.js index b6c97cd1a35..545c5a8e149 100644 --- a/ghost/core/core/server/data/seeders/importers/stripe-products-importer.js +++ b/ghost/core/core/server/data/seeders/importers/stripe-products-importer.js @@ -1,5 +1,5 @@ const {faker} = require('@faker-js/faker'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {blogStartDate} = require('../utils/blog-info'); const sixWeeksLater = new Date(blogStartDate); diff --git a/ghost/core/core/server/data/seeders/importers/table-importer.js b/ghost/core/core/server/data/seeders/importers/table-importer.ts similarity index 60% rename from ghost/core/core/server/data/seeders/importers/table-importer.js rename to ghost/core/core/server/data/seeders/importers/table-importer.ts index cab459d7ba9..0b3fbe7b523 100644 --- a/ghost/core/core/server/data/seeders/importers/table-importer.js +++ b/ghost/core/core/server/data/seeders/importers/table-importer.ts @@ -1,32 +1,35 @@ -const debug = require('@tryghost/debug')('TableImporter'); -const dateToDatabaseString = require('../utils/database-date'); -const path = require('path'); -const createCsvWriter = require('csv-writer').createObjectCsvWriter; -const fs = require('fs'); -const {luck} = require('../utils/random'); -const os = require('os'); -const crypto = require('crypto'); -const logging = require('@tryghost/logging'); -const errors = require('@tryghost/errors'); - -class TableImporter { - /** - * @type {object|undefined} model Referenced model when generating data - */ - model; - - /** - * @type {number|undefined} defaultQuantity Default number of records to import - */ - defaultQuantity; - - /** - * Transaction and knex need to be separate since we're using the batchInsert helper - * @param {string} name Name of the table to be generated - * @param {import('knex/types').Knex} knex Database connection - * @param {import('knex/types').Knex.Transaction} transaction Transaction to be used for import - */ - constructor(name, knex, transaction) { +import debugFactory from '@tryghost/debug'; +// @ts-expect-error This module lacks type definitions. +import dateToDatabaseString from '../utils/database-date'; +import path from 'node:path'; +import fs from 'node:fs'; +import papaparse from 'papaparse'; +import {luck} from '../utils/random'; +import os from 'node:os'; +import crypto from 'node:crypto'; +import logging from '@tryghost/logging'; +import errors from '@tryghost/errors'; +import type {Knex} from 'knex'; +import type {Promisable} from 'type-fest'; + +type GeneratedModel = Record; + +const debug = debugFactory('TableImporter'); + +export abstract class TableImporter< + T extends GeneratedModel = GeneratedModel, + TReferenced extends GeneratedModel = GeneratedModel +> { + protected name: string; + protected knex: Knex; + protected transaction: Knex.Transaction; + /** Referenced model when generating data. */ + protected model?: TReferenced; + /** Default number of records to import. */ + protected defaultQuantity?: number; + + /** Transaction and knex stay separate because batchInsert needs both. */ + constructor(name: string, knex: Knex, transaction: Knex.Transaction) { this.name = name; this.knex = knex; this.transaction = transaction; @@ -38,8 +41,8 @@ class TableImporter { return `00000000` + crypto.randomBytes(8).toString('hex'); } - async #generateData(amount = this.defaultQuantity) { - let data = []; + async #generateData(amount = this.defaultQuantity ?? 0): Promise { + const data: T[] = []; for (let i = 0; i < amount; i++) { const model = await this.generate(); @@ -51,7 +54,7 @@ class TableImporter { return data; } - async import(amount = this.defaultQuantity) { + async import(amount = this.defaultQuantity): Promise { const generateNow = Date.now(); const data = await this.#generateData(amount); debug(`${this.name} generated ${data.length} records in ${Date.now() - generateNow}ms`); @@ -61,30 +64,26 @@ class TableImporter { } } - /** - * @param {Array} models List of models to reference - * @param {Number|function} amount Number of records to import per model - */ - async importForEach(models = [], amount) { - const data = []; + async importForEach(models: TReferenced[] = [], amount: number | (() => number) = 0): Promise { + const data: T[] = []; debug (`Generating data for ${models.length} models x ${amount} for ${this.name}`); const now = Date.now(); let settingReferenceModel = 0; for (const model of models) { - let s = Date.now(); + const s = Date.now(); this.setReferencedModel(model); settingReferenceModel += Date.now() - s; let currentAmount = (typeof amount === 'function') ? amount() : amount; if (!Number.isInteger(currentAmount)) { - currentAmount = Math.floor(currentAmount) + luck((currentAmount % 1) * 100); + currentAmount = Math.floor(currentAmount) + Number(luck((currentAmount % 1) * 100)); } const generatedData = await this.#generateData(currentAmount); - if (generatedData.length > 0) { - data.push(...generatedData); + for (const generatedModel of generatedData) { + data.push(generatedModel); } } @@ -95,7 +94,7 @@ class TableImporter { } } - async batchInsert(data) { + async batchInsert(data: T[]): Promise { // Write to CSV file const rootFolder = os.tmpdir(); const filePath = path.join(rootFolder, `${this.name}.csv`); @@ -108,12 +107,7 @@ class TableImporter { // Ignore: file doesn't exist } - const csvWriter = createCsvWriter({ - path: filePath, - header: Object.keys(data[0]).map((key) => { - return {id: key, title: key}; - }) - }); + const columns = Object.keys(data[0]); // Loop the data in chunks of 50.000 items const batchSize = 50000; @@ -123,20 +117,25 @@ class TableImporter { const slicedData = data.slice(i, i + batchSize); // Map data to what MySQL expects in the CSV for values like booleans, null and dates - for (let j = 0; j < slicedData.length; j++) { - const obj = slicedData[j]; + for (const obj of slicedData) { + const mutableObj: GeneratedModel = obj; - for (const [key, value] of Object.entries(obj)) { + for (const [key, value] of Object.entries(mutableObj)) { if (typeof value === 'boolean') { - obj[key] = value ? 1 : 0; + mutableObj[key] = value ? 1 : 0; } else if (value instanceof Date) { - obj[key] = dateToDatabaseString(value); + mutableObj[key] = dateToDatabaseString(value); } else if (value === null) { - obj[key] = '\\N'; + mutableObj[key] = '\\N'; } } } - await csvWriter.writeRecords(slicedData); + const csv = papaparse.unparse(slicedData, { + columns, + header: i === 0, + newline: '\n' + }); + await fs.promises.appendFile(filePath, `${i === 0 ? '' : '\n'}${csv}`); } debug(`${this.name} saved CSV import file in ${Date.now() - now}ms`); @@ -153,7 +152,7 @@ class TableImporter { logging.warn(`CSV import warning: expected ${data.length} imported rows, got ${result.affectedRows}.`); } } else { - await this.knex.batchInsert(this.name, data).transacting(this.transaction); + await this.knex.batchInsert(this.name, data as GeneratedModel[]).transacting(this.transaction); } debug(`${this.name} imported ${data.length} records in ${Date.now() - now}ms`); @@ -162,26 +161,21 @@ class TableImporter { /** * Finalise the imported data, e.g. adding summary records based on a table's dependents */ - async finalise() { + async finalise(): Promise { // No-op by default } /** * Sets the model which newly generated data will reference - * @param {Object} model Model to reference when generating data */ - setReferencedModel(model) { + setReferencedModel(model: TReferenced): void { this.model = model; } /** * Generates the data for a single model to be imported - * @returns {Object|null} Data to import, optional */ - generate() { + generate(): Promisable { // Should never be called - return false; } } - -module.exports = TableImporter; diff --git a/ghost/core/core/server/data/seeders/importers/tags-importer.js b/ghost/core/core/server/data/seeders/importers/tags-importer.js index 3b3f95f4f6b..19d6effd920 100644 --- a/ghost/core/core/server/data/seeders/importers/tags-importer.js +++ b/ghost/core/core/server/data/seeders/importers/tags-importer.js @@ -1,6 +1,6 @@ const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const dateToDatabaseString = require('../utils/database-date'); class TagsImporter extends TableImporter { diff --git a/ghost/core/core/server/data/seeders/importers/users-importer.js b/ghost/core/core/server/data/seeders/importers/users-importer.js index f7924842fdd..efec11fb770 100644 --- a/ghost/core/core/server/data/seeders/importers/users-importer.js +++ b/ghost/core/core/server/data/seeders/importers/users-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const {slugify} = require('@tryghost/string'); const security = require('@tryghost/security'); diff --git a/ghost/core/core/server/data/seeders/importers/web-mentions-importer.js b/ghost/core/core/server/data/seeders/importers/web-mentions-importer.js index 87187655b18..182724a4452 100644 --- a/ghost/core/core/server/data/seeders/importers/web-mentions-importer.js +++ b/ghost/core/core/server/data/seeders/importers/web-mentions-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('./table-importer'); +const {TableImporter} = require('./table-importer'); const {faker} = require('@faker-js/faker'); const dateToDatabaseString = require('../utils/database-date'); diff --git a/ghost/core/core/server/data/seeders/utils/json-importer.js b/ghost/core/core/server/data/seeders/utils/json-importer.js index dc4626e9907..c9d075d8bf2 100644 --- a/ghost/core/core/server/data/seeders/utils/json-importer.js +++ b/ghost/core/core/server/data/seeders/utils/json-importer.js @@ -1,4 +1,4 @@ -const TableImporter = require('../importers/table-importer'); +const {TableImporter} = require('../importers/table-importer'); class JsonImporter extends TableImporter { constructor(knex, transaction) { diff --git a/ghost/core/core/server/data/seeders/utils/random.js b/ghost/core/core/server/data/seeders/utils/random.js deleted file mode 100644 index 06b6a1d1cf1..00000000000 --- a/ghost/core/core/server/data/seeders/utils/random.js +++ /dev/null @@ -1,13 +0,0 @@ -const {faker} = require('@faker-js/faker'); - -/** - * Adds another degree of randomness into some decisions - * @param {number} lowerThan Only this % of people will achieve this luck - * @returns {boolean} Whether this person is lucky enough for the condition - */ -const luck = lowerThan => faker.number.int({ - min: 1, - max: 100 -}) <= lowerThan; - -module.exports = {luck}; diff --git a/ghost/core/core/server/data/seeders/utils/random.ts b/ghost/core/core/server/data/seeders/utils/random.ts new file mode 100644 index 00000000000..357209a3524 --- /dev/null +++ b/ghost/core/core/server/data/seeders/utils/random.ts @@ -0,0 +1,11 @@ +import {faker} from '@faker-js/faker'; + +/** + * Adds another degree of randomness into some decisions + * @param lowerThan Only this % of people will achieve this luck + * @returns Whether this person is lucky enough for the condition + */ +export const luck = (lowerThan: number): boolean => faker.number.int({ + min: 1, + max: 100 +}) <= lowerThan; diff --git a/ghost/core/core/server/data/seeders/utils/topological-sort.ts b/ghost/core/core/server/data/seeders/utils/topological-sort.ts index 1669c25fd7c..0a161ed212d 100644 --- a/ghost/core/core/server/data/seeders/utils/topological-sort.ts +++ b/ghost/core/core/server/data/seeders/utils/topological-sort.ts @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import type {ReadonlyDeep} from 'type-fest'; type TopologicalSortable = ReadonlyDeep<{ @@ -26,9 +27,7 @@ export function topologicalSort(objects: Readonly } const object = objectsByName.get(name); - if (!object) { - return; - } + assert(object, `Expected an object with name ${name}`); visited.add(name); diff --git a/ghost/core/core/server/services/automations/automations-repository.ts b/ghost/core/core/server/services/automations/automations-repository.ts index 1d9051e44af..fca54f96902 100644 --- a/ghost/core/core/server/services/automations/automations-repository.ts +++ b/ghost/core/core/server/services/automations/automations-repository.ts @@ -65,6 +65,12 @@ export interface AutomationSummary { updated_at: string; } +export interface AutomationBrowseResult extends AutomationSummary { + stats: { + last_run_created_at: Date | null; + }; +} + export interface Automation extends AutomationSummary { actions: AutomationAction[]; edges: AutomationEdge[]; @@ -136,7 +142,7 @@ export type AutomationStepTerminalStatus = | 'member unsubscribed'; export interface AutomationsRepository { - browse(): Promise>; + browse(): Promise>; getById(id: string): Promise; getAutomationActionLinks(automationId: string, actionId: string): Promise; edit(id: string, data: EditAutomationData): Promise; diff --git a/ghost/core/core/server/services/automations/database-automations-repository.ts b/ghost/core/core/server/services/automations/database-automations-repository.ts index db184a2529d..48f9b9e2340 100644 --- a/ghost/core/core/server/services/automations/database-automations-repository.ts +++ b/ghost/core/core/server/services/automations/database-automations-repository.ts @@ -11,6 +11,7 @@ import {DEFAULT_EMAIL_DESIGN_SETTING_SLUG, MEMBER_WELCOME_EMAIL_SLUGS} from '../ import type { AutomatedEmailEvents, Automation, + AutomationBrowseResult, AutomationAction, AutomationEdge, AutomationEmailStats, @@ -52,6 +53,9 @@ interface AutomationRow { updated_at: DatabaseDate; } +interface AutomationBrowseRow extends AutomationRow { + last_run_created_at: DatabaseDate | null; +} interface ActionRow { id: string; @@ -145,12 +149,12 @@ export function createDatabaseAutomationsRepository({ fakeWaitHoursMultiplier: number | null; }): AutomationsRepository { return { - async browse(): Promise> { + async browse(): Promise> { return await knex.transaction(async (trx) => { await ensureDefaultAutomations(trx); const rows = await loadAutomations(trx); return { - data: rows.map(row => buildAutomationSummary(row)), + data: rows.map(row => buildAutomationBrowseResult(row)), meta: { pagination: buildPagination(rows.length) } @@ -1013,10 +1017,24 @@ async function loadAutomationBySlug(trx: Knex.Transaction, slug: string): Promis return row ?? null; } -async function loadAutomations(trx: Knex.Transaction): Promise { +async function loadAutomations(trx: Knex.Transaction): Promise { + const latestRunDates = trx('automation_runs') + .select('automation_id') + .max({last_run_created_at: 'created_at'}) + .groupBy('automation_id') + .as('latest_run_dates'); return await trx('automations') - .select('id', 'slug', 'name', 'status', 'created_at', 'updated_at') - .orderBy('name'); + .select( + 'automations.id', + 'automations.slug', + 'automations.name', + 'automations.status', + 'automations.created_at', + 'automations.updated_at', + 'latest_run_dates.last_run_created_at' + ) + .leftJoin(latestRunDates, 'automations.id', 'latest_run_dates.automation_id') + .orderBy('automations.name'); } async function updateAutomation(trx: Knex.Transaction, automation: AutomationRow): Promise { @@ -1355,6 +1373,15 @@ function buildAutomationSummary(automation: AutomationRow): AutomationSummary { }; } +function buildAutomationBrowseResult(automation: AutomationBrowseRow): AutomationBrowseResult { + return { + ...buildAutomationSummary(automation), + stats: { + last_run_created_at: automation.last_run_created_at ? fromDatabaseDate(automation.last_run_created_at) : null + } + }; +} + function serializeDate(date: DatabaseDate) { const normalizedDate = fromDatabaseDate(date); normalizedDate.setMilliseconds(0); diff --git a/ghost/core/core/server/services/automations/poll.ts b/ghost/core/core/server/services/automations/poll.ts index dea7b3836e4..29e40def1df 100644 --- a/ghost/core/core/server/services/automations/poll.ts +++ b/ghost/core/core/server/services/automations/poll.ts @@ -177,14 +177,8 @@ const processStep = async ({ break; case 'send_email': { if (!hasUpdatesAndAnnouncementsEnabled(member)) { - logging.info({ - system: { - event: 'automations.poll.skipped_unsubscribed_member', - member_id: step.member_id, - step_id: step.id - } - }, `[AUTOMATIONS] Member ${step.member_id} for step ${step.id} has unsubscribed from emails. Fast-finishing this step`); - break; + await automationsApi.markStepTerminal(step, 'member unsubscribed'); + return null; } memberWelcomeEmailService.init(); const trackClicks = labs.isSet('automationAnalytics') && Boolean(settingsCache.get('email_track_clicks')); diff --git a/ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.ts b/ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.ts index 864e682df1e..515dac30b83 100644 --- a/ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.ts +++ b/ghost/core/core/server/services/email-analytics/email-analytics-service-wrapper.ts @@ -3,8 +3,7 @@ import logging from '@tryghost/logging'; import type {ConfigInstance} from '../../../shared/config/loader'; // @ts-expect-error This module lacks type definitions. import type DomainEvents from '@tryghost/domain-events'; -// @ts-expect-error This module lacks type definitions. -import type Metrics from '@tryghost/metrics'; +import type {GhostMetrics} from '@tryghost/metrics'; import {EmailAnalyticsService, type CursorSeed, type EmailAnalyticsFetchResult, type JobNames} from './email-analytics-service'; import type {BatchEventProcessor} from './batch-event-processor'; import type {Queries} from './lib/queries'; @@ -13,7 +12,7 @@ import {fetchMailgunEvents} from './fetch-mailgun-events'; export class EmailAnalyticsServiceWrapper { #logName: string; #config?: Pick; - #metrics?: Pick; + #metrics?: Pick; #service?: EmailAnalyticsService; #fetching = false; #restoredSchedule = false; @@ -46,7 +45,7 @@ export class EmailAnalyticsServiceWrapper { jobNames: JobNames; cursorSeed: CursorSeed; createEventProcessor: () => BatchEventProcessor; - metrics: Pick; + metrics: Pick; settingsCache: {get: (key: string) => unknown}; }>): void { if (this.#service) { diff --git a/ghost/core/core/server/services/email-analytics/index.ts b/ghost/core/core/server/services/email-analytics/index.ts index 746d96da076..da1d98d530d 100644 --- a/ghost/core/core/server/services/email-analytics/index.ts +++ b/ghost/core/core/server/services/email-analytics/index.ts @@ -1,8 +1,7 @@ import type {Knex} from 'knex'; import type {PrometheusClient} from '@tryghost/prometheus-metrics'; import type {ConfigInstance} from '../../../shared/config/loader'; -// @ts-expect-error This module lacks type definitions. -import type Metrics from '@tryghost/metrics'; +import type {GhostMetrics} from '@tryghost/metrics'; // @ts-expect-error This module lacks type definitions. import type SettingsCache from '../../../shared/settings-cache'; import {EmailAnalyticsServiceWrapper} from './email-analytics-service-wrapper'; @@ -61,7 +60,7 @@ export const init = ({ EmailRecipientFailure: EmailRecipientFailure; EmailSpamComplaintEvent: EmailSpamComplaintEvent; }; - metrics: Pick; + metrics: Pick; prometheusClient: Pick | null; settingsCache: Pick; }) => { diff --git a/ghost/core/core/server/services/email-service/batch-sending-service.js b/ghost/core/core/server/services/email-service/batch-sending-service.js index 3968f88b8d6..cee0eda8827 100644 --- a/ghost/core/core/server/services/email-service/batch-sending-service.js +++ b/ghost/core/core/server/services/email-service/batch-sending-service.js @@ -611,7 +611,7 @@ class BatchSendingService { async () => { await batch.save({ status: 'submitted', - provider_id: response.id, + mailgun_message_id: response.id, // reset error fields when sending succeeds error_status_code: null, error_message: null, diff --git a/ghost/core/core/server/services/email-service/email-event-processor.js b/ghost/core/core/server/services/email-service/email-event-processor.js index 9049edf78b6..53f5bad9adb 100644 --- a/ghost/core/core/server/services/email-service/email-event-processor.js +++ b/ghost/core/core/server/services/email-service/email-event-processor.js @@ -50,7 +50,7 @@ class EmailEventProcessor { this.#db = db; this.#eventStorage = eventStorage; this.#prometheusClient = prometheusClient; - // Avoid having to query email_batch by provider_id for every event + // Avoid having to query email_batch by mailgun_message_id for every event this.providerIdEmailIdMap = {}; if (this.#prometheusClient) { @@ -211,7 +211,7 @@ class EmailEventProcessor { return; } - // With the provider_id and email address we can look for the EmailRecipient + // With the mailgun_message_id and email address we can look for the EmailRecipient const emailId = emailIdentification.emailId ?? await this.getEmailId(emailIdentification.providerId); if (!emailId) { // Invalid @@ -269,7 +269,7 @@ class EmailEventProcessor { const {emailId} = await this.#db.knex('email_batches') .select('email_id as emailId') - .where('provider_id', providerId) + .where('mailgun_message_id', providerId) .first() || {}; if (!emailId) { @@ -301,11 +301,11 @@ class EmailEventProcessor { if (providerIds.length > 0) { const providerIdMapping = await this.#db.knex('email_batches') - .select('provider_id', 'email_id') - .whereIn('provider_id', providerIds); + .select('mailgun_message_id', 'email_id') + .whereIn('mailgun_message_id', providerIds); for (const row of providerIdMapping) { - this.providerIdEmailIdMap[row.provider_id] = row.email_id; + this.providerIdEmailIdMap[row.mailgun_message_id] = row.email_id; } } diff --git a/ghost/core/core/server/services/oembed/oembed-service.js b/ghost/core/core/server/services/oembed/oembed-service.js index df63634993c..29cb45077c0 100644 --- a/ghost/core/core/server/services/oembed/oembed-service.js +++ b/ghost/core/core/server/services/oembed/oembed-service.js @@ -52,6 +52,10 @@ const GZIP_MAGIC = [0x1f, 0x8b]; const SVG_EXTENSIONS = new Set(['.svg', '.svgz']); const SVG_SNIFF_BYTES = 1024; +const toBuffer = (bytes) => { + return Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength); +}; + const shouldRasterize = (buffer, ext) => { if (SVG_EXTENSIONS.has(ext.toLowerCase())) { return true; @@ -179,8 +183,8 @@ class OEmbedService { * @returns {Promise} - Promise resolving to the image buffer */ async fetchImageBuffer(imageUrl) { - const buffer = await this.externalRequest(imageUrl).buffer(); - return buffer; + const bytes = await this.externalRequest(imageUrl).buffer(); + return toBuffer(bytes); } /** @@ -306,6 +310,8 @@ class OEmbedService { ...options }); + body = toBuffer(body); + try { // Detect page encoding which might not be utf-8 // and decode content diff --git a/ghost/core/package.json b/ghost/core/package.json index 45d8d3da5b2..6028b28d8cc 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -117,7 +117,7 @@ "@tryghost/kg-lexical-html-renderer": "workspace:*", "@tryghost/limit-service": "catalog:", "@tryghost/logging": "catalog:", - "@tryghost/metrics": "1.0.43", + "@tryghost/metrics": "catalog:", "@tryghost/mongo-knex": "catalog:", "@tryghost/mongo-utils": "0.6.5", "@tryghost/mw-error-handler": "1.0.13", @@ -131,7 +131,7 @@ "@tryghost/prometheus-metrics": "1.0.8", "@tryghost/promise": "2.3.2", "@tryghost/referrer-parser": "0.1.21", - "@tryghost/request": "3.3.4", + "@tryghost/request": "catalog:", "@tryghost/root-utils": "2.3.2", "@tryghost/security": "1.0.6", "@tryghost/social-urls": "0.1.63", @@ -159,7 +159,6 @@ "cors": "2.8.6", "countries-and-timezones": "3.9.0", "csso": "5.0.5", - "csv-writer": "1.6.0", "dequal": "catalog:", "dompurify": "catalog:", "downsize-cjs": "1.1.2", @@ -177,7 +176,7 @@ "fs-extra": "catalog:", "ghost-storage-base": "workspace:*", "glob": "catalog:", - "got": "13.0.0", + "got": "catalog:", "gscan": "6.4.2", "handlebars": "4.7.9", "heic-convert": "2.1.0", @@ -199,16 +198,16 @@ "lodash": "catalog:", "luxon": "3.7.2", "mailgun.js": "10.4.0", - "metascraper": "5.51.1", - "metascraper-amazon": "5.51.2", - "metascraper-author": "5.51.1", - "metascraper-description": "5.51.1", - "metascraper-image": "5.51.1", - "metascraper-logo": "5.51.1", - "metascraper-logo-favicon": "5.51.1", - "metascraper-publisher": "5.51.1", - "metascraper-title": "5.51.1", - "metascraper-url": "5.51.1", + "metascraper": "5.55.2", + "metascraper-amazon": "5.55.2", + "metascraper-author": "5.55.2", + "metascraper-description": "5.55.2", + "metascraper-image": "5.55.2", + "metascraper-logo": "5.55.2", + "metascraper-logo-favicon": "5.55.2", + "metascraper-publisher": "5.55.2", + "metascraper-title": "5.55.2", + "metascraper-url": "5.55.2", "mime-types": "2.1.35", "mingo": "catalog:", "moment": "2.24.0", @@ -255,7 +254,7 @@ "@types/express": "catalog:", "@types/fs-extra": "11.0.4", "@types/js-yaml": "4.0.9", - "@types/jsdom": "28.0.3", + "@types/jsdom": "catalog:", "@types/jsonwebtoken": "9.0.10", "@types/lodash": "4.17.25", "@types/lodash-es": "4.17.12", diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap index f73e8d29bf8..e37f7111460 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/automations.test.js.snap @@ -8,6 +8,9 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Free member welcome flow", "slug": "member-welcome-email-free", + "stats": Object { + "last_run_created_at": null, + }, "status": "active", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, }, @@ -16,6 +19,9 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "name": "Paid member welcome flow", "slug": "member-welcome-email-paid", + "stats": Object { + "last_run_created_at": null, + }, "status": "active", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, }, @@ -37,7 +43,7 @@ exports[`Automations API browse returns automations sourced from the database 2: Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "510", + "content-length": "584", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap index 757d969fc6a..58f38e15c4a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/emails.test.js.snap @@ -94,8 +94,8 @@ Object { "error_message": null, "error_status_code": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "mailgun_message_id": Any, "member_segment": null, - "provider_id": Any, "status": "submitted", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, }, @@ -117,7 +117,7 @@ exports[`Emails API Can browse email batches 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "365", + "content-length": "372", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -138,8 +138,8 @@ Object { "error_message": null, "error_status_code": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "mailgun_message_id": Any, "member_segment": null, - "provider_id": Any, "status": "submitted", "updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, }, @@ -161,7 +161,7 @@ exports[`Emails API Can browse email batches with recipient count 2: [headers] 1 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "390", + "content-length": "397", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/automations.test.js b/ghost/core/test/e2e-api/admin/automations.test.js index 49fbf9bb92a..6a8ea0f8a30 100644 --- a/ghost/core/test/e2e-api/admin/automations.test.js +++ b/ghost/core/test/e2e-api/admin/automations.test.js @@ -15,14 +15,21 @@ const {anyContentVersion, anyEtag, anyErrorId, anyISODateTime, anyObjectId} = ma const {cacheInvalidateHeaderNotSet} = assertions; const hashRedirectDestination = url => createHash('sha256').update(url).digest(); -const matchAutomationSummary = () => ({ +const matchAutomationBase = () => ({ id: anyObjectId, created_at: anyISODateTime, updated_at: anyISODateTime }); +const matchAutomationSummary = () => ({ + ...matchAutomationBase(), + stats: { + last_run_created_at: null + } +}); + const matchAutomation = () => ({ - ...matchAutomationSummary(), + ...matchAutomationBase(), actions: [{ id: anyObjectId }, { @@ -106,6 +113,17 @@ describe('Automations API', function () { }); describe('browse', function () { + async function createAutomationRun(automationId, createdAt) { + await models.Base.knex('automation_runs').insert({ + id: ObjectId().toHexString(), + automation_id: automationId, + member_id: null, + member_email: 'member@example.com', + created_at: createdAt, + updated_at: createdAt + }); + } + async function deleteActionsForAutomationIds(automationIds) { const actionIds = await models.Base.knex('automation_actions') .whereIn('automation_id', automationIds) @@ -171,6 +189,27 @@ describe('Automations API', function () { }); }); + it('returns null last run timestamps for automations without runs', async function () { + const {body} = await agent.get('automations').expectStatus(200); + + assert.deepEqual(body.automations.map(automation => automation.stats.last_run_created_at), [null, null]); + }); + + it('returns latest automation run timestamp', async function () { + const {body: beforeBody} = await agent.get('automations').expectStatus(200); + const automationId = beforeBody.automations[0].id; + const olderRunCreatedAt = new Date('2026-01-01T00:00:00.000Z'); + const latestRunCreatedAt = new Date('2026-01-02T00:00:00.000Z'); + + await createAutomationRun(automationId, olderRunCreatedAt); + await createAutomationRun(automationId, latestRunCreatedAt); + + const {body} = await agent.get('automations').expectStatus(200); + const automation = body.automations.find(candidate => candidate.id === automationId); + + assert.equal(automation.stats.last_run_created_at, latestRunCreatedAt.toISOString()); + }); + it('upserts the default free and paid automations', async function () { const existingAutomations = await models.Base.knex('automations') .select('id') diff --git a/ghost/core/test/e2e-api/admin/emails.test.js b/ghost/core/test/e2e-api/admin/emails.test.js index 6a0d3d78d19..f275c9c23fc 100644 --- a/ghost/core/test/e2e-api/admin/emails.test.js +++ b/ghost/core/test/e2e-api/admin/emails.test.js @@ -21,7 +21,7 @@ const matchEmailNewsletter = { const matchBatch = { id: anyObjectId, - provider_id: anyString, + mailgun_message_id: anyString, created_at: anyISODateTime, updated_at: anyISODateTime }; diff --git a/ghost/core/test/e2e-api/admin/max-limit-cap.test.js b/ghost/core/test/e2e-api/admin/max-limit-cap.test.js index 92d45d96c0b..0e000e2110f 100644 --- a/ghost/core/test/e2e-api/admin/max-limit-cap.test.js +++ b/ghost/core/test/e2e-api/admin/max-limit-cap.test.js @@ -69,7 +69,7 @@ describe('Admin API - Max Limit Cap', function () { batches.push({ id: ObjectId().toHexString(), email_id: emailId, - provider_id: `test-batch-${i}-${Date.now()}`, + mailgun_message_id: `test-batch-${i}-${Date.now()}`, status: 'submitted', member_segment: null, created_at: new Date(), diff --git a/ghost/core/test/integration/services/email-service/batch-sending.test.js b/ghost/core/test/integration/services/email-service/batch-sending.test.js index 36828683305..0f6019eeb45 100644 --- a/ghost/core/test/integration/services/email-service/batch-sending.test.js +++ b/ghost/core/test/integration/services/email-service/batch-sending.test.js @@ -27,8 +27,8 @@ let stubbedSend; let frontendAgent; function sortBatches(a, b) { - const aId = a.get('provider_id'); - const bId = b.get('provider_id'); + const aId = a.get('mailgun_message_id'); + const bId = b.get('mailgun_message_id'); if (aId === null) { return 1; } @@ -68,7 +68,7 @@ async function testEmailBatches(settings, email_recipient_filter, expectedBatche const firstBatch = remainingBatches[index]; remainingBatches.splice(index, 1); - assert.equal(firstBatch.get('provider_id'), 'stubbed-email-id'); + assert.equal(firstBatch.get('mailgun_message_id'), 'stubbed-email-id'); assert.equal(firstBatch.get('status'), 'submitted'); assert.equal(firstBatch.get('member_segment'), expectedBatch.segment); assert.equal(firstBatch.get('error_status_code'), null); @@ -163,7 +163,7 @@ describe('Batch sending tests', function () { // Check all batches are in send state for (const batch of batches.models) { - assert.equal(batch.get('provider_id'), 'stubbed-email-id'); + assert.equal(batch.get('mailgun_message_id'), 'stubbed-email-id'); assert.equal(batch.get('status'), 'submitted'); assert.equal(batch.get('member_segment'), null); @@ -471,7 +471,7 @@ describe('Batch sending tests', function () { count += 1; if (count === 4) { - assert.equal(batch.get('provider_id'), null); + assert.equal(batch.get('mailgun_message_id'), null); assert.equal(batch.get('status'), 'failed'); assert.equal(batch.get('error_status_code'), 500); assert.equal(batch.get('error_message'), 'Internal server error: Something went wrong'); @@ -479,7 +479,7 @@ describe('Batch sending tests', function () { assert.equal(errorData.error.status, 500); assert.deepEqual(errorData.messageData.to.length, 1); } else { - assert.equal(batch.get('provider_id'), 'stubbed-email-id-' + count); + assert.equal(batch.get('mailgun_message_id'), 'stubbed-email-id-' + count); assert.equal(batch.get('status'), 'submitted'); assert.equal(batch.get('error_status_code'), null); assert.equal(batch.get('error_message'), null); @@ -519,7 +519,7 @@ describe('Batch sending tests', function () { await emailModel.refresh(); batches = await models.EmailBatch.findAll({filter: `email_id:'${emailModel.id}'`}); - // sort batches by provider_id (nullable) because findAll doesn't have order option + // sort batches by mailgun_message_id (nullable) because findAll doesn't have order option batches.models.sort(sortBatches); assert.equal(emailModel.get('status'), 'submitted'); @@ -528,7 +528,7 @@ describe('Batch sending tests', function () { // Did we keep the batches? batches = await models.EmailBatch.findAll({filter: `email_id:'${emailModel.id}'`}); - // sort batches by provider_id (nullable) because findAll doesn't have order option + // sort batches by mailgun_message_id (nullable) because findAll doesn't have order option batches.models.sort(sortBatches); assert.equal(batches.models.length, 4); @@ -536,7 +536,7 @@ describe('Batch sending tests', function () { // Check all batches are in send state for (const batch of batches.models) { - assert(!!batch.get('provider_id')); + assert(!!batch.get('mailgun_message_id')); assert.equal(batch.get('status'), 'submitted'); assert.equal(batch.get('member_segment'), null); @@ -627,7 +627,7 @@ describe('Batch sending tests', function () { // Check all batches are in send state for (const batch of batches.models) { - assert.equal(batch.get('provider_id'), 'stubbed-email-id'); + assert.equal(batch.get('mailgun_message_id'), 'stubbed-email-id'); assert.equal(batch.get('status'), 'submitted'); assert.equal(batch.get('member_segment'), null); diff --git a/ghost/core/test/integration/services/email-service/newsletter-email-event-storage.test.js b/ghost/core/test/integration/services/email-service/newsletter-email-event-storage.test.js index c434be268e6..f5bb70f762e 100644 --- a/ghost/core/test/integration/services/email-service/newsletter-email-event-storage.test.js +++ b/ghost/core/test/integration/services/email-service/newsletter-email-event-storage.test.js @@ -66,7 +66,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ @@ -111,7 +111,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); // Reset @@ -159,7 +159,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ @@ -207,7 +207,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ @@ -303,7 +303,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 4); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ @@ -397,7 +397,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2001, 0, 1); events = [{ @@ -487,7 +487,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 1); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ @@ -579,7 +579,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); // Reset @@ -696,7 +696,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2001, 0, 1); events = [{ @@ -798,7 +798,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2001, 0, 2); events = [{ @@ -900,7 +900,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2001, 0, 3); events = [{ @@ -1002,7 +1002,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 1); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const eventsURI = '/members/events/?' + encodeURIComponent( `filter=type:-[comment_event,aggregated_click_event]+data.member_id:'${memberId}'` @@ -1055,7 +1055,7 @@ processingModes.forEach(({name, batchProcessing}) => { assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); // Initialise member with 2 newsletters @@ -1117,7 +1117,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailBatch = fixtureManager.get('email_batches', 0); const emailRecipient = fixtureManager.get('email_recipients', 0); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); // Member subscribed to 2 newsletters @@ -1164,7 +1164,7 @@ processingModes.forEach(({name, batchProcessing}) => { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); events = [{ diff --git a/ghost/core/test/integration/services/email-service/resume-interrupted-sends.test.js b/ghost/core/test/integration/services/email-service/resume-interrupted-sends.test.js index 619065a93cf..b871ed951e2 100644 --- a/ghost/core/test/integration/services/email-service/resume-interrupted-sends.test.js +++ b/ghost/core/test/integration/services/email-service/resume-interrupted-sends.test.js @@ -59,7 +59,7 @@ describe('Resume interrupted sends', function () { // 2. Mutate the DB to simulate a crash mid-send: one batch never made it to Mailgun, // the other did; the parent email row is stuck in `submitting`. const [batchA, batchB] = batches; - await batchB.save({status: 'pending', provider_id: null}, {patch: true, autoRefresh: false}); + await batchB.save({status: 'pending', mailgun_message_id: null}, {patch: true, autoRefresh: false}); await emailModel.save({status: 'submitting'}, {patch: true, autoRefresh: false}); // 3. Reset the Mailgun stub so we only count calls produced by the resume. @@ -98,7 +98,7 @@ describe('Resume interrupted sends', function () { const [batchA, batchB] = batches; // batchA: stays submitted (already accepted by Mailgun on the original run) - // batchB: flipped to submitting (orphan from crash) — provider_id intentionally preserved + // batchB: flipped to submitting (orphan from crash) — mailgun_message_id intentionally preserved // so the breadcrumb in the runbook still cross-references against Mailgun. await batchB.save({status: 'submitting'}, {patch: true, autoRefresh: false}); await emailModel.save({status: 'submitting'}, {patch: true, autoRefresh: false}); diff --git a/ghost/core/test/integration/services/mailgun-email-suppression-list.test.js b/ghost/core/test/integration/services/mailgun-email-suppression-list.test.js index f50ca9d3181..ee629bdd889 100644 --- a/ghost/core/test/integration/services/mailgun-email-suppression-list.test.js +++ b/ghost/core/test/integration/services/mailgun-email-suppression-list.test.js @@ -43,7 +43,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 0); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const recipient = emailRecipient.member_email; @@ -71,7 +71,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 1); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const recipient = emailRecipient.member_email; @@ -99,7 +99,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 2); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const recipient = emailRecipient.member_email; @@ -127,7 +127,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 3); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const recipient = emailRecipient.member_email; @@ -156,7 +156,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 4); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const timestamp = new Date(2000, 0, 1); const {body: {members: [member]}} = await agent.get(`/members/${memberId}`); @@ -193,7 +193,7 @@ describe('MailgunEmailSuppressionList', function () { const emailRecipient = fixtureManager.get('email_recipients', 5); assert(emailRecipient.batch_id === emailBatch.id); const memberId = emailRecipient.member_id; - const providerId = emailBatch.provider_id; + const providerId = emailBatch.mailgun_message_id; const recipient = emailRecipient.member_email; const timestamp = new Date(2000, 0, 1); diff --git a/ghost/core/test/unit/api/canary/utils/serializers/output/mapper.test.js b/ghost/core/test/unit/api/canary/utils/serializers/output/mapper.test.js index 3833b27f1b0..d81b0d9ae28 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/output/mapper.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/output/mapper.test.js @@ -412,7 +412,7 @@ describe('Unit: utils/serializers/output/mappers', function () { const model = createJsonModel({ id: 'id1', - provider_id: 'provider_id1', + mailgun_message_id: 'provider_id1', status: 'status1', member_segment: 'member_segment1', created_at: 'created_at1', @@ -430,7 +430,7 @@ describe('Unit: utils/serializers/output/mappers', function () { const mapped = mappers.emailBatches(model, frame); assert.deepEqual(mapped, { id: 'id1', - provider_id: 'provider_id1', + mailgun_message_id: 'provider_id1', status: 'status1', member_segment: 'member_segment1', created_at: 'created_at1', diff --git a/ghost/core/test/unit/server/data/schema/commands.test.js b/ghost/core/test/unit/server/data/schema/commands.test.js index 0ab8a7d6c6f..a98f4f6e6b3 100644 --- a/ghost/core/test/unit/server/data/schema/commands.test.js +++ b/ghost/core/test/unit/server/data/schema/commands.test.js @@ -139,4 +139,40 @@ describe('schema commands', function () { assert.deepEqual(builderViews, ['my_view']); }); }); + + describe('renameColumn', function () { + it('uses requested algorithm on MySQL', async function () { + const rawStatements = []; + const fakeKnex = { + client: {config: {client: 'mysql2'}}, + raw: (sql) => { + rawStatements.push(sql); + return Promise.resolve(); + } + }; + + await commands.renameColumn('email_batches', 'provider_id', 'mailgun_message_id', fakeKnex, {algorithm: 'instant'}); + + assert.deepEqual(rawStatements, [ + 'ALTER TABLE `email_batches` RENAME COLUMN `provider_id` TO `mailgun_message_id`, algorithm=instant;' + ]); + }); + + it('does not force an algorithm when none is requested', async function () { + const rawStatements = []; + const fakeKnex = { + client: {config: {client: 'mysql2'}}, + raw: (sql) => { + rawStatements.push(sql); + return Promise.resolve(); + } + }; + + await commands.renameColumn('table', 'old_column', 'new_column', fakeKnex); + + assert.deepEqual(rawStatements, [ + 'ALTER TABLE `table` RENAME COLUMN `old_column` TO `new_column`;' + ]); + }); + }); }); diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index 90389f27162..19505941905 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -35,7 +35,7 @@ const parseYaml = require('../../../../../core/server/services/route-settings/ya */ describe('DB version integrity', function () { // Only these variables should need updating - const currentSchemaHash = '3771a4cd7362a5f9311018812438f21b'; + const currentSchemaHash = 'bbded7a3841350d2d8c5856bc01c3461'; const currentFixturesHash = 'd4c9e4fabcec3365c42d37642216021b'; const currentSettingsHash = 'f57245b22af55ea5fc1e5e20913de9bb'; const currentRoutesHash = 'd8c25fa01bf6d22a2bcb05ba0de70dc1'; diff --git a/ghost/core/test/unit/server/data/seeders/data-generator.test.js b/ghost/core/test/unit/server/data/seeders/data-generator.test.js index af64f99dd97..0a8a2de21f9 100644 --- a/ghost/core/test/unit/server/data/seeders/data-generator.test.js +++ b/ghost/core/test/unit/server/data/seeders/data-generator.test.js @@ -7,6 +7,8 @@ const ProductsImporter = importers.find(i => i.table === 'products'); const OfferRedemptionsImporter = importers.find(i => i.table === 'offer_redemptions'); const StripeProductsImporter = importers.find(i => i.table === 'stripe_products'); const StripePricesImporter = importers.find(i => i.table === 'stripe_prices'); +const AutomationsImporter = importers.find(i => i.table === 'automations'); +const AutomationRunStepsImporter = importers.find(i => i.table === 'automation_run_steps'); const generateEvents = require('../../../../../core/server/data/seeders/utils/event-generator'); const dateToDatabaseString = require('../../../../../core/server/data/seeders/utils/database-date'); @@ -84,6 +86,12 @@ describe('Data Generator', function () { } }); } + + await db('email_design_settings').insert({ + id: '000000000000000000000001', + slug: 'default-automated-email', + created_at: new Date() + }); }); afterEach(async function () { @@ -112,6 +120,177 @@ describe('Data Generator', function () { await dataGenerator.importData(); }); + it('Can import automation actions and revisions', async function () { + const dataGenerator = new DataGenerator({ + knex: db, + schema, + schemaTables, + logger: { + info: () => { }, + ok: () => { }, + warn: () => { } + }, + tables: [{ + name: 'automations', + quantity: 2 + }, { + name: 'automation_actions', + quantity: 8 + }, { + name: 'automation_action_revisions', + quantity: 16 + }, { + name: 'automation_action_edges', + quantity: 1 + }] + }); + await dataGenerator.importData(); + + const actions = await db.select('id', 'automation_id', 'created_at', 'type').from('automation_actions'); + const revisions = await db.select('action_id', 'wait_hours', 'email_subject', 'email_lexical', 'email_design_setting_id').from('automation_action_revisions'); + const edges = await db.select('source_action_id', 'target_action_id').from('automation_action_edges'); + + assert.equal(actions.length, 8); + assert.equal(revisions.length, 16); + assert.equal(edges.length, 6); + + for (const action of actions) { + const actionRevisions = revisions.filter(revision => revision.action_id === action.id); + assert.equal(actionRevisions.length, 2); + + if (action.type === 'wait') { + assert.ok(actionRevisions.every(revision => revision.wait_hours !== null)); + assert.ok(actionRevisions.every(revision => revision.wait_hours % 24 === 0)); + assert.ok(actionRevisions.every(revision => revision.email_subject === null)); + assert.ok(actionRevisions.every(revision => revision.email_lexical === null)); + assert.ok(actionRevisions.every(revision => revision.email_design_setting_id === null)); + } else { + assert.ok(actionRevisions.every(revision => revision.wait_hours === null)); + assert.ok(actionRevisions.every(revision => revision.email_subject !== null)); + assert.ok(actionRevisions.every(revision => revision.email_lexical !== null)); + assert.ok(actionRevisions.every(revision => revision.email_design_setting_id === '000000000000000000000001')); + } + } + + const actionsByAutomation = Map.groupBy(actions, action => action.automation_id); + const expectedEdges = [...actionsByAutomation.values()].flatMap((automationActions) => { + const sortedActions = automationActions.toSorted((first, second) => { + return first.created_at.localeCompare(second.created_at) || first.id.localeCompare(second.id); + }); + return sortedActions.slice(1).map((action, index) => ({ + source_action_id: sortedActions[index].id, + target_action_id: action.id + })); + }); + + const bySourceAndTarget = (first, second) => { + return first.source_action_id.localeCompare(second.source_action_id) || first.target_action_id.localeCompare(second.target_action_id); + }; + assert.deepEqual(edges.toSorted(bySourceAndTarget), expectedEdges.toSorted(bySourceAndTarget)); + }); + + it('Can import automation runs and run steps', async function () { + const dataGenerator = new DataGenerator({ + knex: db, + schema, + schemaTables, + logger: { + info: () => { }, + ok: () => { }, + warn: () => { } + }, + tables: [{ + name: 'automations', + quantity: 2 + }, { + name: 'members', + quantity: 4 + }, { + name: 'automation_actions', + quantity: 8 + }, { + name: 'automation_action_revisions', + quantity: 16 + }, { + name: 'automation_action_edges' + }, { + name: 'automation_runs', + quantity: 6 + }, { + name: 'automation_run_steps', + quantity: 12 + }] + }); + await dataGenerator.importData(); + + const members = await db.select('id', 'email').from('members'); + const runs = await db.select('id', 'automation_id', 'member_id', 'member_email').from('automation_runs'); + const steps = await db.select('automation_run_id', 'automation_action_revision_id', 'status', 'step_attempts', 'started_at', 'finished_at').from('automation_run_steps'); + const revisions = await db.select('id', 'action_id', 'created_at').from('automation_action_revisions'); + const actions = await db.select('id', 'automation_id').from('automation_actions'); + const edges = await db.select('source_action_id', 'target_action_id').from('automation_action_edges'); + + assert.equal(runs.length, 6); + assert.ok(steps.length >= 12); + + const membersById = new Map(members.map(member => [member.id, member])); + const actionsById = new Map(actions.map(action => [action.id, action])); + const actionCountsByAutomation = new Map([...Map.groupBy(actions, action => action.automation_id)].map(([automationId, automationActions]) => [automationId, automationActions.length])); + const revisionsById = new Map(revisions.map(revision => [revision.id, revision])); + const latestRevisionIds = new Set([...Map.groupBy(revisions, revision => revision.action_id).values()].map((actionRevisions) => { + return actionRevisions.toSorted((first, second) => second.created_at.localeCompare(first.created_at) || second.id.localeCompare(first.id))[0].id; + })); + const edgeKeys = new Set(edges.map(edge => `${edge.source_action_id}:${edge.target_action_id}`)); + + for (const run of runs) { + assert.equal(run.member_email, membersById.get(run.member_id).email); + + const runSteps = steps.filter(step => step.automation_run_id === run.id); + assert.ok(runSteps.length > 0); + assert.ok(runSteps.slice(0, -1).every(step => step.status === 'finished')); + assert.ok(['pending', 'finished', 'failed'].includes(runSteps.at(-1).status)); + + for (const step of runSteps) { + if (step.status === 'pending') { + assert.equal(step.step_attempts, 0); + assert.equal(step.started_at, null); + assert.equal(step.finished_at, null); + } else { + assert.equal(step.step_attempts, 1); + assert.notEqual(step.started_at, null); + assert.notEqual(step.finished_at, null); + } + } + + const stepActions = runSteps.map((step) => { + assert.ok(latestRevisionIds.has(step.automation_action_revision_id)); + const revision = revisionsById.get(step.automation_action_revision_id); + return actionsById.get(revision.action_id); + }); + + assert.ok(stepActions.every(action => action.automation_id === run.automation_id)); + assert.ok(edgeKeys.has(`${stepActions[0].id}:${stepActions[1].id}`)); + } + + for (const [automationId, actionCount] of actionCountsByAutomation) { + const automationRuns = runs.filter(run => run.automation_id === automationId); + assert.ok(automationRuns.some((run) => { + return steps.filter(step => step.automation_run_id === run.id).length === actionCount; + })); + } + + await db('automation_run_steps').del(); + const transaction = await db.transaction(); + const runStepsImporter = new AutomationRunStepsImporter(db, transaction); + await runStepsImporter.import(0); + await transaction.commit(); + + const minimumSteps = await db.select('automation_run_id').from('automation_run_steps'); + const fullPathExtraSteps = [...actionCountsByAutomation.values()].reduce((total, actionCount) => total + actionCount - 1, 0); + assert.equal(minimumSteps.length, runs.length + fullPathExtraSteps); + assert.equal(new Set(minimumSteps.map(step => step.automation_run_id)).size, runs.length); + }); + it('Can import explicit offer redemptions', async function () { const dataGenerator = new DataGenerator({ knex: db, @@ -198,6 +377,15 @@ describe('Importer', function () { table.date('created_at'); table.date('updated_at'); }); + + await db.schema.createTable('automations', function (table) { + table.string('id'); + table.string('status'); + table.string('name').unique(); + table.string('slug').unique(); + table.dateTime('created_at'); + table.dateTime('updated_at'); + }); }); afterEach(async function () { @@ -230,6 +418,27 @@ describe('Importer', function () { assert.equal(results.length, 4); }); + it('Should import automations', async function () { + const transaction = await db.transaction(); + const automationsImporter = new AutomationsImporter(db, transaction); + await automationsImporter.import(3); + await transaction.commit(); + + const automations = await db.select('id', 'status', 'name', 'slug').from('automations'); + + assert.equal(automations.length, 3); + assert.deepEqual(automations.slice(0, 2).map(({name, slug}) => ({name, slug})), [{ + name: 'Free member welcome flow', + slug: 'member-welcome-email-free' + }, { + name: 'Paid member welcome flow', + slug: 'member-welcome-email-paid' + }]); + assert.equal(new Set(automations.map(automation => automation.name)).size, 3); + assert.equal(new Set(automations.map(automation => automation.slug)).size, 3); + assert.ok(automations.every(automation => ['active', 'inactive'].includes(automation.status))); + }); + it('Should update products to reference price ids', async function () { const transaction = await db.transaction(); const productsImporter = new ProductsImporter(db, transaction); diff --git a/ghost/core/test/unit/server/services/automations/automations-repository.test.ts b/ghost/core/test/unit/server/services/automations/automations-repository.test.ts index 2597c181d12..4726d0378fb 100644 --- a/ghost/core/test/unit/server/services/automations/automations-repository.test.ts +++ b/ghost/core/test/unit/server/services/automations/automations-repository.test.ts @@ -458,12 +458,11 @@ describe('automations repository', function () { return result; }; - const insertRun = async (automationId: string) => { - const now = toDatabaseDate(new Date()); + const insertRun = async (automationId: string, createdAt = new Date()) => { const run = { id: ObjectId().toHexString(), - created_at: now, - updated_at: now, + created_at: toDatabaseDate(createdAt), + updated_at: toDatabaseDate(createdAt), automation_id: automationId, member_id: ObjectId().toHexString(), member_email: 'member@example.com' @@ -660,6 +659,27 @@ describe('automations repository', function () { ]); }); + it('returns null for "last run created at" if the automation has no runs', async function () { + const result = await repo.browse(); + + assert(result.data.every(automation => automation.stats.last_run_created_at === null)); + }); + + it('returns the newest run creation time for the automation', async function () { + const automationId = (await getAutomationBySlug('member-welcome-email-free')).id; + const latestRunCreatedAt = new Date('2026-01-02T00:00:00.000Z'); + const olderRunCreatedAt = new Date('2026-01-01T00:00:00.000Z'); + + await insertRun(automationId, latestRunCreatedAt); + await insertRun(automationId, olderRunCreatedAt); + + const browseResult = await repo.browse(); + const automation = browseResult.data.find(candidate => candidate.id === automationId); + assert(automation); + + assert.deepEqual(automation.stats.last_run_created_at, latestRunCreatedAt); + }); + it('creates missing default free and paid automations', async function () { const automationIds = await knex('automations') .whereIn('slug', ['member-welcome-email-free', 'member-welcome-email-paid']) diff --git a/ghost/core/test/unit/server/services/automations/poll.test.ts b/ghost/core/test/unit/server/services/automations/poll.test.ts index b94ac419bc5..04682f5132f 100644 --- a/ghost/core/test/unit/server/services/automations/poll.test.ts +++ b/ghost/core/test/unit/server/services/automations/poll.test.ts @@ -265,11 +265,9 @@ describe('automations poll', function () { sinon.assert.calledOnceWithExactly(automationsApi.markStepTerminal, step, 'member changed status'); }); - it('skips sending email if the member unsubscribed from updates & announcements', async function () { - const nextReadyAt = new Date(Date.now() + 60 * 1000); + it('ends the automation run if the member unsubscribed from updates & announcements', async function () { const step = buildEmailStep(); automationsApi.fetchAndLockSteps.resolves({steps: [step], nextStepReadyAt: null}); - automationsApi.finishStepAndEnqueueNext.resolves(nextReadyAt); Member.findOne.resolves(buildMember({enable_updates_and_announcements: false})); await poll(options); @@ -277,9 +275,9 @@ describe('automations poll', function () { sinon.assert.notCalled(memberWelcomeEmailService.init); sinon.assert.notCalled(memberWelcomeEmailService.api.sendAutomationEmail); sinon.assert.notCalled(automationsApi.recordEmailSent); - sinon.assert.notCalled(automationsApi.markStepTerminal); - sinon.assert.calledOnceWithExactly(automationsApi.finishStepAndEnqueueNext, step); - sinon.assert.calledOnceWithExactly(options.enqueueAnotherPollAt, nextReadyAt); + sinon.assert.calledOnceWithExactly(automationsApi.markStepTerminal, step, 'member unsubscribed'); + sinon.assert.notCalled(automationsApi.finishStepAndEnqueueNext); + sinon.assert.notCalled(options.enqueueAnotherPollAt); }); it('sends email if updates & announcements is unset and the member has newsletter subscriptions', async function () { @@ -297,11 +295,9 @@ describe('automations poll', function () { sinon.assert.calledOnceWithExactly(automationsApi.finishStepAndEnqueueNext, step); }); - it('skips sending email if updates & announcements is unset and the member has no newsletter subscriptions', async function () { - const nextReadyAt = new Date(Date.now() + 60 * 1000); + it('ends the automation run if updates & announcements is unset and the member has no newsletter subscriptions', async function () { const step = buildEmailStep(); automationsApi.fetchAndLockSteps.resolves({steps: [step], nextStepReadyAt: null}); - automationsApi.finishStepAndEnqueueNext.resolves(nextReadyAt); Member.findOne.resolves(buildMember({ enable_updates_and_announcements: null, newsletters: [] @@ -312,9 +308,9 @@ describe('automations poll', function () { sinon.assert.notCalled(memberWelcomeEmailService.init); sinon.assert.notCalled(memberWelcomeEmailService.api.sendAutomationEmail); sinon.assert.notCalled(automationsApi.recordEmailSent); - sinon.assert.notCalled(automationsApi.markStepTerminal); - sinon.assert.calledOnceWithExactly(automationsApi.finishStepAndEnqueueNext, step); - sinon.assert.calledOnceWithExactly(options.enqueueAnotherPollAt, nextReadyAt); + sinon.assert.calledOnceWithExactly(automationsApi.markStepTerminal, step, 'member unsubscribed'); + sinon.assert.notCalled(automationsApi.finishStepAndEnqueueNext); + sinon.assert.notCalled(options.enqueueAnotherPollAt); }); it('gift members run through paid automations', async function () { diff --git a/ghost/core/test/unit/server/services/email-service/batch-sending-service.test.js b/ghost/core/test/unit/server/services/email-service/batch-sending-service.test.js index 027b65a6586..1817c5678e8 100644 --- a/ghost/core/test/unit/server/services/email-service/batch-sending-service.test.js +++ b/ghost/core/test/unit/server/services/email-service/batch-sending-service.test.js @@ -1248,7 +1248,7 @@ describe('Batch Sending Service', function () { sinon.assert.calledOnce(findOne); const batch = await findOne.firstCall.returnValue; assert.equal(batch.get('status'), 'submitted'); - assert.equal(batch.get('provider_id'), 'providerid@example.com'); + assert.equal(batch.get('mailgun_message_id'), 'providerid@example.com'); const {members} = sendingService.send.firstCall.args[0]; assert.equal(members.length, 2); @@ -1289,7 +1289,7 @@ describe('Batch Sending Service', function () { sinon.assert.calledOnce(findOne); const batch = await findOne.firstCall.returnValue; assert.equal(batch.get('status'), 'submitted'); - assert.equal(batch.get('provider_id'), 'providerid@example.com'); + assert.equal(batch.get('mailgun_message_id'), 'providerid@example.com'); const {members} = sendingService.send.firstCall.args[0]; assert.equal(members.length, 2); @@ -1332,7 +1332,7 @@ describe('Batch Sending Service', function () { const batch = await findOne.firstCall.returnValue; assert.equal(batch.get('status'), 'submitted'); - assert.equal(batch.get('provider_id'), 'providerid@example.com'); + assert.equal(batch.get('mailgun_message_id'), 'providerid@example.com'); assert.equal(batch.get('fallback_sending_domain'), useFallback); }); }); @@ -1537,7 +1537,7 @@ describe('Batch Sending Service', function () { sinon.assert.calledOnce(findOne); const batch = await findOne.firstCall.returnValue; assert.equal(batch.get('status'), 'submitted'); - assert.equal(batch.get('provider_id'), 'providerid@example.com'); + assert.equal(batch.get('mailgun_message_id'), 'providerid@example.com'); const {members} = sendingService.send.firstCall.args[0]; assert.equal(members.length, 2); diff --git a/ghost/core/test/unit/server/services/oembed/oembed-service.test.js b/ghost/core/test/unit/server/services/oembed/oembed-service.test.js index e1e33c4f09e..6a723ecf531 100644 --- a/ghost/core/test/unit/server/services/oembed/oembed-service.test.js +++ b/ghost/core/test/unit/server/services/oembed/oembed-service.test.js @@ -124,6 +124,28 @@ describe('oembed-service', function () { }); }); + describe('fetchPageHtml', function () { + it('decodes Uint8Array response bodies from Got 15', async function () { + const externalRequest = sinon.stub().resolves({ + headers: { + 'content-type': 'text/html; charset=utf-8' + }, + body: new Uint8Array(Buffer.from('Hello')), + url: 'https://www.example.com/' + }); + const service = new OembedService({ + config: {get() { + return true; + }}, + externalRequest + }); + + const response = await service.fetchPageHtml('https://www.example.com'); + + assert.equal(response.body, 'Hello'); + }); + }); + describe('fetchOembedData', function () { const pageHtml = ``; @@ -622,6 +644,22 @@ describe('oembed-service', function () { describe('processImageFromUrl', function () { const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; + it('normalizes Uint8Array image responses from Got 15 to Buffer', async function () { + const bytes = new Uint8Array(Buffer.from('img-bytes')); + const service = new OembedService({ + externalRequest() { + return { + buffer: async () => bytes + }; + } + }); + + const result = await service.fetchImageBuffer('https://example.com/sample.png'); + + assert.ok(Buffer.isBuffer(result)); + assert.deepEqual(result, Buffer.from('img-bytes')); + }); + it('returns null without fetching when the image URL is missing', async function () { const externalRequest = sinon.stub(); const service = new OembedService({ diff --git a/ghost/core/test/utils/fixtures/data-generator.js b/ghost/core/test/utils/fixtures/data-generator.js index 02054ffd9a5..9df2ee972ed 100644 --- a/ghost/core/test/utils/fixtures/data-generator.js +++ b/ghost/core/test/utils/fixtures/data-generator.js @@ -1312,7 +1312,7 @@ DataGenerator.Content = { { id: ObjectId().toHexString(), email_id: null, // emails[0] relation added later - provider_id: 'email1@testing.mailgun.net', + mailgun_message_id: 'email1@testing.mailgun.net', status: 'submitted', fallback_sending_domain: false } diff --git a/koenig/kg-default-nodes/package.json b/koenig/kg-default-nodes/package.json index 8f9725a7827..48f0d37c082 100644 --- a/koenig/kg-default-nodes/package.json +++ b/koenig/kg-default-nodes/package.json @@ -10,7 +10,7 @@ "author": "Ghost Foundation", "license": "MIT", "engines": { - "node": "^22.13.1 || ^24.0.0" + "node": "^22.22.2 || ^24.15.0" }, "main": "build/cjs/index.js", "module": "build/esm/index.js", diff --git a/koenig/kg-html-to-lexical/package.json b/koenig/kg-html-to-lexical/package.json index e1379c9a807..5c77a136773 100644 --- a/koenig/kg-html-to-lexical/package.json +++ b/koenig/kg-html-to-lexical/package.json @@ -9,6 +9,9 @@ }, "author": "Ghost Foundation", "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0" + }, "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/esm/index.d.ts", diff --git a/koenig/kg-lexical-html-renderer/package.json b/koenig/kg-lexical-html-renderer/package.json index f5c2b627763..42d785d42c6 100644 --- a/koenig/kg-lexical-html-renderer/package.json +++ b/koenig/kg-lexical-html-renderer/package.json @@ -9,6 +9,9 @@ }, "author": "Ghost Foundation", "license": "MIT", + "engines": { + "node": "^22.22.2 || ^24.15.0" + }, "main": "build/cjs/index.js", "module": "build/esm/index.js", "types": "build/esm/index.d.ts", diff --git a/packages/parse-email-address/package.json b/packages/parse-email-address/package.json index 2b2a1608b07..c10e7b04a61 100644 --- a/packages/parse-email-address/package.json +++ b/packages/parse-email-address/package.json @@ -39,19 +39,21 @@ "@internal/cfg-typescript": "workspace:*", "@internal/cfg-vitest": "workspace:*", "@types/node": "catalog:", + "@typescript/native": "catalog:", "@vitest/coverage-v8": "catalog:", "eslint": "catalog:", - "@typescript/native": "catalog:", "typescript": "catalog:", "vitest": "catalog:" }, "dependencies": { - "parse-email-address": "0.0.2" + "parse-email-address": "catalog:" }, "nx": { "targets": { "build": { - "outputs": ["{projectRoot}/build"] + "outputs": [ + "{projectRoot}/build" + ] } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 154a1e5cad7..aa246113d70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,8 +154,8 @@ catalogs: specifier: 3.3.5 version: 3.3.5 '@tryghost/brute-knex': - specifier: 3.2.1 - version: 3.2.1 + specifier: 3.2.2 + version: 3.2.2 '@tryghost/color-utils': specifier: 0.2.20 version: 0.2.20 @@ -163,26 +163,26 @@ catalogs: specifier: 1.0.11 version: 1.0.11 '@tryghost/debug': - specifier: 2.3.5 - version: 2.3.5 + specifier: 2.3.8 + version: 2.3.8 '@tryghost/domain-events': - specifier: 3.3.5 - version: 3.3.5 + specifier: 3.3.9 + version: 3.3.9 '@tryghost/helpers': specifier: 1.1.106 version: 1.1.106 '@tryghost/limit-service': specifier: 1.5.6 version: 1.5.6 + '@tryghost/metrics': + specifier: 3.4.2 + version: 3.4.2 '@tryghost/mongo-knex': - specifier: 0.10.1 - version: 0.10.1 - '@tryghost/nql': - specifier: 0.13.1 - version: 0.13.1 - '@tryghost/nql-lang': - specifier: 0.6.6 - version: 0.6.6 + specifier: 0.11.1 + version: 0.11.1 + '@tryghost/request': + specifier: 4.0.1 + version: 4.0.1 '@tryghost/string': specifier: 0.3.5 version: 0.3.5 @@ -190,14 +190,14 @@ catalogs: specifier: 1.0.0 version: 1.0.0 '@tryghost/tpl': - specifier: 2.3.5 - version: 2.3.5 + specifier: 2.3.8 + version: 2.3.8 '@types/express': specifier: 4.17.25 version: 4.17.25 '@types/jsdom': - specifier: 28.0.3 - version: 28.0.3 + specifier: 30.0.0 + version: 30.0.0 '@types/lodash': specifier: 4.17.25 version: 4.17.25 @@ -330,12 +330,15 @@ catalogs: globals: specifier: 17.7.0 version: 17.7.0 + got: + specifier: 15.1.0 + version: 15.1.0 js-yaml: specifier: 4.3.0 version: 4.3.0 jsdom: - specifier: 29.1.1 - version: 29.1.1 + specifier: 30.0.1 + version: 30.0.1 jsonc-parser: specifier: 3.3.1 version: 3.3.1 @@ -372,6 +375,9 @@ catalogs: node-html-markdown: specifier: 2.0.0 version: 2.0.0 + parse-email-address: + specifier: 0.0.4 + version: 0.0.4 postcss: specifier: 8.5.26 version: 8.5.26 @@ -491,8 +497,10 @@ catalogs: overrides: knex-migrator>knex: 2.4.2 - '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0 + '@tryghost/errors': 3.3.8 + '@tryghost/logging': 5.3.4 + '@tryghost/nql': 0.13.3 + '@tryghost/nql-lang': 0.7.0 jackspeak: 4.2.3 moment: 2.30.1 moment-timezone: 0.5.45 @@ -608,7 +616,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/activitypub: dependencies: @@ -681,7 +689,7 @@ importers: version: 9.39.4(jiti@2.7.0)(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) tailwindcss: specifier: 'catalog:' version: 4.2.2 @@ -693,7 +701,7 @@ importers: version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/admin: dependencies: @@ -767,11 +775,11 @@ importers: specifier: 'catalog:' version: 1.5.6 '@tryghost/nql': - specifier: 'catalog:' - version: 0.13.1(supports-color@10.2.2) + specifier: 0.13.3 + version: 0.13.3(supports-color@10.2.2) '@tryghost/nql-lang': - specifier: 'catalog:' - version: 0.6.6 + specifier: 0.7.0 + version: 0.7.0 '@tryghost/nql-string': specifier: workspace:* version: link:../../packages/nql-string @@ -907,7 +915,7 @@ importers: version: 7.0.0(@typescript/typescript6@6.0.2)(jest@29.7.0(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(supports-color@10.2.2)) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) msw: specifier: 'catalog:' version: 2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2) @@ -928,7 +936,7 @@ importers: version: 8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest-browser-react: specifier: 'catalog:' version: 2.2.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vitest@4.1.10) @@ -959,13 +967,13 @@ importers: version: 17.7.0 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) vite: specifier: 'catalog:' version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/admin-x-framework: dependencies: @@ -1044,7 +1052,7 @@ importers: version: 13.0.6 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) msw: specifier: 'catalog:' version: 2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2) @@ -1065,7 +1073,7 @@ importers: version: 4.5.0(@typescript/typescript6@6.0.2)(rollup@4.62.2)(supports-color@10.2.2)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/announcement-bar: dependencies: @@ -1090,13 +1098,13 @@ importers: version: 9.39.4(jiti@2.7.0)(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) vite: specifier: 'catalog:' version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/comments-ui: dependencies: @@ -1138,7 +1146,7 @@ importers: version: 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))(@tiptap/pm@2.27.2)(react-dom@17.0.2(react@17.0.2))(react@17.0.2) '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) react: specifier: catalog:react17 version: 17.0.2 @@ -1168,8 +1176,8 @@ importers: specifier: workspace:* version: link:../../packages/i18n '@tryghost/nql': - specifier: 'catalog:' - version: 0.13.1(supports-color@10.2.2) + specifier: 0.13.3 + version: 0.13.3(supports-color@10.2.2) '@types/react': specifier: catalog:react17 version: 17.0.93 @@ -1196,7 +1204,7 @@ importers: version: 3.18.3(tailwindcss@3.4.19(tsx@4.23.12)(yaml@2.9.0)) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) moment: specifier: 2.30.1 version: 2.30.1 @@ -1220,7 +1228,7 @@ importers: version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/ember-admin: dependencies: @@ -1316,8 +1324,8 @@ importers: specifier: 'catalog:' version: 1.5.6 '@tryghost/nql': - specifier: 'catalog:' - version: 0.13.1(supports-color@10.2.2) + specifier: 0.13.3 + version: 0.13.3(supports-color@10.2.2) '@tryghost/nql-string': specifier: workspace:* version: link:../../packages/nql-string @@ -1629,7 +1637,7 @@ importers: dependencies: '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) devDependencies: '@doist/react-interpolate': specifier: 2.2.4 @@ -1672,7 +1680,7 @@ importers: version: 9.39.4(jiti@2.7.0)(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) react: specifier: catalog:react17 version: 17.0.2 @@ -1687,7 +1695,7 @@ importers: version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/shade: dependencies: @@ -1874,7 +1882,7 @@ importers: version: 13.0.6 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) postcss: specifier: 'catalog:' version: 8.5.26 @@ -1907,13 +1915,13 @@ importers: version: 4.5.0(@typescript/typescript6@6.0.2)(rollup@4.62.2)(supports-color@10.2.2)(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) apps/signup-form: dependencies: '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) react: specifier: 'catalog:' version: 18.3.1 @@ -1983,7 +1991,7 @@ importers: dependencies: '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) '@tryghost/i18n': specifier: workspace:* version: link:../../packages/i18n @@ -2020,7 +2028,7 @@ importers: version: 9.39.4(jiti@2.7.0)(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) nock: specifier: 'catalog:' version: 14.0.17 @@ -2038,7 +2046,7 @@ importers: version: 8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) configs/eslint: dependencies: @@ -2103,13 +2111,13 @@ importers: version: 4.5.0(@typescript/typescript6@6.0.2)(rollup@4.62.2)(supports-color@10.2.2)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) configs/vitest: dependencies: vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) e2e: devDependencies: @@ -2124,10 +2132,10 @@ importers: version: 1.61.1 '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) '@tryghost/logging': - specifier: 5.3.0 - version: 5.3.0(supports-color@10.2.2) + specifier: 5.3.4 + version: 5.3.4(supports-color@10.2.2) '@tryghost/test-data': specifier: workspace:* version: link:../packages/testing/test-data @@ -2223,7 +2231,7 @@ importers: version: 2.3.8(supports-color@10.2.2) '@tryghost/brute-knex': specifier: 'catalog:' - version: 3.2.1(better-sqlite3@12.11.1)(express@4.22.2(supports-color@10.2.2))(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2) + version: 3.2.2(better-sqlite3@12.11.1)(express@4.22.2(supports-color@10.2.2))(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2) '@tryghost/color-utils': specifier: 'catalog:' version: 0.2.20 @@ -2241,16 +2249,16 @@ importers: version: 2.3.2 '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) '@tryghost/domain-events': specifier: 'catalog:' - version: 3.3.5(supports-color@10.2.2) + version: 3.3.9(supports-color@10.2.2) '@tryghost/email-mock-receiver': specifier: 2.1.0 version: 2.1.0 '@tryghost/errors': - specifier: 3.3.5 - version: 3.3.5 + specifier: 3.3.8 + version: 3.3.8 '@tryghost/helpers': specifier: 'catalog:' version: 1.1.106 @@ -2288,14 +2296,14 @@ importers: specifier: 'catalog:' version: 1.5.6 '@tryghost/logging': - specifier: 5.3.0 - version: 5.3.0(supports-color@10.2.2) + specifier: 5.3.4 + version: 5.3.4(supports-color@10.2.2) '@tryghost/metrics': - specifier: 1.0.43 - version: 1.0.43(supports-color@10.2.2) + specifier: 'catalog:' + version: 3.4.2(supports-color@10.2.2) '@tryghost/mongo-knex': specifier: 'catalog:' - version: 0.10.1(supports-color@10.2.2) + version: 0.11.1(supports-color@10.2.2) '@tryghost/mongo-utils': specifier: 0.6.5 version: 0.6.5 @@ -2309,11 +2317,11 @@ importers: specifier: 2.3.1 version: 2.3.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) '@tryghost/nql': - specifier: 'catalog:' - version: 0.13.1(supports-color@10.2.2) + specifier: 0.13.3 + version: 0.13.3(supports-color@10.2.2) '@tryghost/nql-lang': - specifier: 'catalog:' - version: 0.6.6 + specifier: 0.7.0 + version: 0.7.0 '@tryghost/nql-string': specifier: workspace:* version: link:../../packages/nql-string @@ -2333,8 +2341,8 @@ importers: specifier: 0.1.21 version: 0.1.21 '@tryghost/request': - specifier: 3.3.4 - version: 3.3.4 + specifier: 'catalog:' + version: 4.0.1 '@tryghost/root-utils': specifier: 2.3.2 version: 2.3.2 @@ -2349,7 +2357,7 @@ importers: version: 0.3.5 '@tryghost/tpl': specifier: 'catalog:' - version: 2.3.5 + version: 2.3.8 '@tryghost/url-utils': specifier: 5.2.6 version: 5.2.6 @@ -2416,9 +2424,6 @@ importers: csso: specifier: 5.0.5 version: 5.0.5 - csv-writer: - specifier: 1.6.0 - version: 1.6.0 dequal: specifier: 'catalog:' version: 2.0.3 @@ -2471,8 +2476,8 @@ importers: specifier: 'catalog:' version: 13.0.6 got: - specifier: 13.0.0 - version: 13.0.0 + specifier: 'catalog:' + version: 15.1.0 gscan: specifier: 6.4.2 version: 6.4.2(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(supports-color@10.2.2) @@ -2505,7 +2510,7 @@ importers: version: 4.3.0 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) jsonc-parser: specifier: 'catalog:' version: 3.3.1 @@ -2537,35 +2542,35 @@ importers: specifier: 10.4.0 version: 10.4.0(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) metascraper: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-amazon: - specifier: 5.51.2 - version: 5.51.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-author: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-description: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-image: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-logo: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-logo-favicon: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-publisher: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-title: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) metascraper-url: - specifier: 5.51.1 - version: 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + specifier: 5.55.2 + version: 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) mime-types: specifier: 2.1.35 version: 2.1.35 @@ -2691,8 +2696,8 @@ importers: specifier: 4.0.9 version: 4.0.9 '@types/jsdom': - specifier: 28.0.3 - version: 28.0.3 + specifier: 'catalog:' + version: 30.0.0 '@types/jsonwebtoken': specifier: 9.0.10 version: 9.0.10 @@ -2821,7 +2826,7 @@ importers: version: 13.12.0 vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) optionalDependencies: better-sqlite3: specifier: 'catalog:' @@ -2855,7 +2860,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-clean-basic-html: devDependencies: @@ -2864,7 +2869,7 @@ importers: version: 9.39.5 '@types/jsdom': specifier: 'catalog:' - version: 28.0.3 + version: 30.0.0 '@types/node': specifier: 'catalog:' version: 22.20.1 @@ -2879,7 +2884,7 @@ importers: version: 3.5.0(@babel/core@7.29.7(supports-color@10.2.2))(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) typescript: specifier: 'catalog:' version: '@typescript/typescript6@6.0.2' @@ -2888,7 +2893,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-converters: devDependencies: @@ -2915,7 +2920,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-default-cards: dependencies: @@ -2946,7 +2951,7 @@ importers: version: 9.39.5 '@types/jsdom': specifier: 'catalog:' - version: 28.0.3 + version: 30.0.0 '@types/node': specifier: 'catalog:' version: 22.20.1 @@ -2961,7 +2966,7 @@ importers: version: 3.5.0(@babel/core@7.29.7(supports-color@10.2.2))(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) simple-dom: specifier: 1.4.0 version: 1.4.0 @@ -2973,7 +2978,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-default-nodes: dependencies: @@ -3009,7 +3014,7 @@ importers: version: 4.0.0 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) lexical: specifier: 0.13.1 version: 0.13.1 @@ -3034,7 +3039,7 @@ importers: version: 0.6.1(prettier@3.9.6) '@types/jsdom': specifier: 'catalog:' - version: 28.0.3 + version: 30.0.0 '@types/lodash': specifier: 'catalog:' version: 4.17.25 @@ -3064,7 +3069,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-default-transforms: dependencies: @@ -3110,7 +3115,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-html-to-lexical: dependencies: @@ -3140,7 +3145,7 @@ importers: version: link:../kg-default-transforms jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) lexical: specifier: 0.13.1 version: 0.13.1 @@ -3150,7 +3155,7 @@ importers: version: 9.39.5 '@types/jsdom': specifier: 'catalog:' - version: 28.0.3 + version: 30.0.0 '@typescript/native': specifier: 'catalog:' version: typescript@7.0.2 @@ -3168,7 +3173,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-lexical-html-renderer: dependencies: @@ -3198,7 +3203,7 @@ importers: version: link:../kg-utils jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) lexical: specifier: 0.13.1 version: 0.13.1 @@ -3208,7 +3213,7 @@ importers: version: 9.39.5 '@types/jsdom': specifier: 'catalog:' - version: 28.0.3 + version: 30.0.0 '@typescript/native': specifier: 'catalog:' version: typescript@7.0.2 @@ -3229,7 +3234,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-markdown-html-renderer: dependencies: @@ -3290,7 +3295,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-simplemde: dependencies: @@ -3354,7 +3359,7 @@ importers: version: 3.18.3(tailwindcss@3.4.19(tsx@4.23.12)(yaml@2.9.0)) jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) postcss: specifier: 'catalog:' version: 8.5.26 @@ -3381,7 +3386,7 @@ importers: version: 5.2.0(@typescript/typescript6@6.0.2)(rollup@4.62.2)(supports-color@10.2.2)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/kg-utils: dependencies: @@ -3415,7 +3420,7 @@ importers: version: 8.67.0(@typescript/typescript6@6.0.2)(eslint@9.39.4(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) koenig/koenig-lexical: devDependencies: @@ -3619,7 +3624,7 @@ importers: version: 10.7.3 jsdom: specifier: 'catalog:' - version: 29.1.1(@noble/hashes@1.8.0) + version: 30.0.1(@noble/hashes@1.8.0) lexical: specifier: 0.13.1 version: 0.13.1 @@ -3688,7 +3693,7 @@ importers: version: 5.2.0(@typescript/typescript6@6.0.2)(rollup@4.62.2)(supports-color@10.2.2)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) y-websocket: specifier: 3.0.0 version: 3.0.0(yjs@13.6.31) @@ -3724,7 +3729,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/adapters/redirects-base: devDependencies: @@ -3754,7 +3759,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/adapters/route-settings-base: devDependencies: @@ -3784,13 +3789,13 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/adapters/scheduling-base: dependencies: '@tryghost/logging': - specifier: 5.3.0 - version: 5.3.0(supports-color@10.2.2) + specifier: 5.3.4 + version: 5.3.4(supports-color@10.2.2) devDependencies: '@internal/cfg-eslint': specifier: 'workspace:' @@ -3824,13 +3829,13 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/adapters/sso-base: dependencies: '@tryghost/errors': - specifier: 3.3.5 - version: 3.3.5 + specifier: 3.3.8 + version: 3.3.8 devDependencies: '@internal/cfg-eslint': specifier: workspace:* @@ -3861,7 +3866,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/adapters/storage-base: dependencies: @@ -3898,13 +3903,13 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/admin-api-schema: dependencies: '@tryghost/errors': - specifier: 3.3.5 - version: 3.3.5 + specifier: 3.3.8 + version: 3.3.8 ajv: specifier: 'catalog:' version: 8.20.0 @@ -3938,7 +3943,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/custom-field-types: dependencies: @@ -3972,13 +3977,13 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/i18n: dependencies: '@tryghost/debug': specifier: 'catalog:' - version: 2.3.5(supports-color@10.2.2) + version: 2.3.8(supports-color@10.2.2) i18next: specifier: 23.16.8 version: 23.16.8 @@ -4003,7 +4008,7 @@ importers: version: 9.4.0(supports-color@10.2.2) vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/nql-string: devDependencies: @@ -4017,8 +4022,8 @@ importers: specifier: workspace:* version: link:../../configs/vitest '@tryghost/nql': - specifier: 'catalog:' - version: 0.13.1(supports-color@10.2.2) + specifier: 0.13.3 + version: 0.13.3(supports-color@10.2.2) '@types/node': specifier: 'catalog:' version: 22.20.1 @@ -4036,13 +4041,13 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/parse-email-address: dependencies: parse-email-address: - specifier: 0.0.2 - version: 0.0.2 + specifier: 'catalog:' + version: 0.0.4 devDependencies: '@internal/cfg-eslint': specifier: workspace:* @@ -4070,7 +4075,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) packages/testing/test-data: dependencies: @@ -4092,7 +4097,7 @@ importers: version: '@typescript/typescript6@6.0.2' vitest: specifier: 'catalog:' - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) scripts: dependencies: @@ -4157,20 +4162,13 @@ packages: '@apm-js-collab/tracing-hooks@0.10.1': resolution: {integrity: sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA==} - '@asamuzakjp/css-color@5.1.11': - resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/dom-selector@7.1.1': - resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - - '@asamuzakjp/generational-cache@1.0.1': - resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} - engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} - '@asamuzakjp/nwsapi@2.3.9': - resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -5040,8 +5038,8 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} - '@csstools/color-helpers@6.0.2': - resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} engines: {node: '>=20.19.0'} '@csstools/convert-colors@1.4.0': @@ -5055,8 +5053,15 @@ packages: '@csstools/css-parser-algorithms': ^4.0.0 '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@4.1.8': - resolution: {integrity: sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==} + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} engines: {node: '>=20.19.0'} peerDependencies: '@csstools/css-parser-algorithms': ^4.0.0 @@ -5231,10 +5236,6 @@ packages: react: '>16.8.0' react-dom: '>16.8.0' - '@elastic/elasticsearch@8.13.1': - resolution: {integrity: sha512-2G4Vu6OHw4+XTrp7AGIcOEezpPEoVrWg2JTK1v/exEKSLYquZkUdd+m4yOL3/UZ6bTj7hmXwrmYzW76BnLCkJQ==} - engines: {node: '>=18'} - '@elastic/elasticsearch@8.19.2': resolution: {integrity: sha512-LMJCju/+AZkDlJArd/MYABWTDrHi4U7j3qGTKi1hYC7+67SaiYmYFItiueACQVrj2j3ECPMcguZ+7WrZeB+Z5g==} engines: {node: '>=20'} @@ -5243,10 +5244,6 @@ packages: resolution: {integrity: sha512-xo2lPBAJEt81fQRAKa9T/gUq1SPGBHpSnVUXhoSpL996fPZRAfQwFA4BZtEUQL1p8Dezodd3ZN8Wwno+mYyKuw==} engines: {node: '>=18'} - '@elastic/transport@8.4.1': - resolution: {integrity: sha512-/SXVuVnuU5b4dq8OFY4izG+dmGla185PcoqgK6+AJMpmOeY1QYVNbWtCwvSvoAANN5D/wV+EBU8+x7Vf9EphbA==} - engines: {node: '>=16'} - '@ember-data/adapter@3.24.0': resolution: {integrity: sha512-vdDvvHF2QyAfLLvJPeREAVUr3M5LsjaZDJJ6Ernf8gJDZBwYvTcYGQEk39GeCzd1Qhw8F7tUTz1DGZXQQRDCrw==} engines: {node: 10.* || >= 12.*} @@ -6556,8 +6553,8 @@ packages: peerDependencies: rollup: '>=2' - '@metascraper/helpers@5.51.1': - resolution: {integrity: sha512-69omtPC3Fap69balcMuJk2tN6nduKlP/VkZMXPw+UKCldDFJmKmTRi5AzvBE4/yxPAgFSpwP0Lt9YQmDoYCBLg==} + '@metascraper/helpers@5.55.2': + resolution: {integrity: sha512-3hVAj5mtgDvH6QekXc52Pod1VxzAzDelzY1HCgxIe4J+lInhQKWmcFVRpXrpwWvJTnpN5OueFyekQnqeyhDL+Q==} engines: {node: '>= 22.13'} '@miragejs/pretender-node-polyfill@0.1.2': @@ -8430,13 +8427,9 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} - '@sindresorhus/is@5.6.0': - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} + '@sindresorhus/is@8.1.0': + resolution: {integrity: sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==} + engines: {node: '>=22'} '@sindresorhus/merge-streams@4.0.0': resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} @@ -8786,10 +8779,6 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@szmarczak/http-timer@5.0.1': - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} - '@tabby_ai/hijri-converter@1.0.5': resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} engines: {node: '>=16.0.0'} @@ -9108,12 +9097,12 @@ packages: '@tryghost/bookshelf-transaction-events@2.3.7': resolution: {integrity: sha512-m9SP1f999C+pO0HU6wzEVhGAIijXWcv4A4EFooHwTqyYszJTXxSwyzyw3u2dXMaMLHLKO9+iFotlaOBPLZN65g==} - '@tryghost/brute-knex@3.2.1': - resolution: {integrity: sha512-1Q1CzpDuWNRrejD4sbSZF4CAJ9RTuZWLuQH1BjcC1U6+5sTY3cbnRv4w6mmxGHbUxWJ0kwbP10UM+ODheGaL7A==} + '@tryghost/brute-knex@3.2.2': + resolution: {integrity: sha512-wdhqZV5klysUO+0ZthzxkaWiSxqKKoJm7rkqby3llt5FHZTU5yiaJ55yraio9knm5MHqfDd+IFspNTKEjAnsIw==} engines: {node: '>=20.20.0'} - '@tryghost/bunyan-rotating-filestream@0.0.11': - resolution: {integrity: sha512-mXYF/qvL2E13yZBNubkAKL8peFcdqb8JVQSyKbGZn6GOsamD1WQ/OcXz2N4KKcf/MIs0LO84VPE3owa1Ko8PRA==} + '@tryghost/bunyan-rotating-filestream@0.0.14': + resolution: {integrity: sha512-ECV9pTYcuCQjzzGwEpsWYDHC6RCAJXafAjAjKg1FN8J52uP8ekQ4J7dAaXBdd76b3ktfsNo90Ul1eGd+5yeT2w==} '@tryghost/color-utils@0.2.20': resolution: {integrity: sha512-0KLCQDX7TbJGKNihs+OB1+3hEvhdP4YXOKpdbqJlUkvEFdY+Gl1FlI4WL8wnn0v6oc1aOjvrKmva5BUT0pWtIg==} @@ -9145,14 +9134,14 @@ packages: '@tryghost/debug@2.3.7': resolution: {integrity: sha512-qS8QrBLNdDTu1DBgx/RwnNeF/7abDvT1iRZ+bDJ95TIj6gKcHbQN2gQs0rLhZLsnxQs2aC4arb0QjHRy8JyazA==} - '@tryghost/domain-events@3.3.5': - resolution: {integrity: sha512-mflHTxYQxZ8j7veZqrLQfhqiBbn17NpvCKX+8gzAYvX3jOAJTZOY1KgsJ6GRDuLrxMrAuT80enuhIjFxfWGcKw==} + '@tryghost/debug@2.3.8': + resolution: {integrity: sha512-7c9/8cCRjmt0AuFjEt+5ALHzoffa8QdqSYaTRXn0Y5PcflStLvv19jF7yUTdvo4JFn4gTvpNosg1QxEohfFgmQ==} - '@tryghost/elasticsearch@3.0.29': - resolution: {integrity: sha512-WGfoGbPXzX7SYPKeD7naYyoF8Lc7KruQPixFSJGLciRQ3wkrZQANQIguD4nA71upp0dhsT4D4aWP296VfJxuPw==} + '@tryghost/domain-events@3.3.9': + resolution: {integrity: sha512-Lw8//qojB7ljRdEXvAK8Jtv0RHKpzZLNdh6kulBtxDKNE67ei8ZoTT/yLR8aKmGzdQ6PJnsOalYPvVJeaXJrqg==} - '@tryghost/elasticsearch@5.4.2': - resolution: {integrity: sha512-AcLyDeULrWHOiBnftSmI5Enj/QTZSCZ7LkEVAKmIByQ7Zy3kEqRaTTJMLF3dSxqw4cJTrr11ADs72fwxfN2gdw==} + '@tryghost/elasticsearch@5.4.5': + resolution: {integrity: sha512-8sjw9VQhfjUzIH2Vu1Gl/a4CP+gsohhc9Qw9tIkQFeMH6LMEKDzYik3gkOlaWj+MyT5BUkizU2tKPdlm1jTEKQ==} '@tryghost/email-mock-receiver@2.1.0': resolution: {integrity: sha512-bgEVM5eRnN53rnqWJ8ZkjuGCzMwwt5Ch29TTXRp3qSGTJruUW44DtN/mjB36DDti0hq4unJV9+D1qIIKxdbong==} @@ -9161,8 +9150,8 @@ packages: resolution: {integrity: sha512-py/Fi3jbr+UiUped74m2VRhNEJQrFSgPEhQu/FSHkPjUPWruIokwJoO4TDedklGcMJ+0RKr3YiigcuMFxXjf7A==} engines: {node: 12.* || >= 14.*} - '@tryghost/errors@3.3.5': - resolution: {integrity: sha512-iaBF5/raLVj0LEL/tuQ+K0cxiYaqH3MoNt3xQPVcV7HMVReSge3Z/1eisUJcWisEsbh0WUNSg0ZG32gsM+r5dw==} + '@tryghost/errors@3.3.8': + resolution: {integrity: sha512-11AaQCyPC8w8Fh9zmAfXIUCDO8q6i0tHsGl65XnZCzqOd5G6HOXgf+rt+cjhLhslBxdcp9qX1nForZ0jrct31Q==} '@tryghost/express-test@2.1.0': resolution: {integrity: sha512-gY2AeFzBLvDtDCdc5rE54crDeEC9TzVboViUTmP1Vwbt35BA3O177Ox2j6vb/LQ9HCQ1tp3DOa0hbNYZFaaJAQ==} @@ -9178,8 +9167,8 @@ packages: '@tryghost/http-cache-utils@0.1.25': resolution: {integrity: sha512-OJAD1ESvV+F81w6IOTKCX0JLGIiJqNn87HVFMskpUC3IUGpqcDP1pCM79d72khxLWy5oRQDYu8xIqicHJ9ST1Q==} - '@tryghost/http-stream@2.3.5': - resolution: {integrity: sha512-w+CKY7/rhaMitdKtVrVWRI90r9jNtTcaNCT4IeraC4ftPmkc7ZpNCz8gKZVpZxTvbNhQ0xrJ0pCNW8+21j3mHw==} + '@tryghost/http-stream@2.3.9': + resolution: {integrity: sha512-ffxWIJ8bGBZS2d/NkFfOgqWEkd1LoqT9AIEqdBTvlDm3jDMqu585IbIluTddrnZIIGgKJNi5+lyFfjBj5z1CAg==} '@tryghost/image-transform@1.4.17': resolution: {integrity: sha512-+WipAbQ6gTOB9hbhmTDHW7tzaB1lPIRszoN+z1qV+vlI+cR/4qO70Ux+HkSYT9gt4WpxRZJZCBCk3+x4Nm0LhQ==} @@ -9193,17 +9182,14 @@ packages: '@tryghost/limit-service@1.5.6': resolution: {integrity: sha512-wR+Hm2v5k2FjOUhLhfPZ0p0acCfaTQoSEhuw96uw8c15CLZtm/HKsLnxA5d3AXwYmjRo5gkYskP8UYxrCKNcoQ==} - '@tryghost/logging@5.3.0': - resolution: {integrity: sha512-zbLsBWyKnX2opCP4ygyS7CldoBgHT3/qUyDkRcn9wtfoYl/tv/QxYnuLW1JTvwHdinT/lNH7CaynkXPaXw8I6g==} - - '@tryghost/metrics@1.0.43': - resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} + '@tryghost/logging@5.3.4': + resolution: {integrity: sha512-7ZRknXmKLFK+WW2xwQ1UJ392bh/yDaqyMCJCPvCI//UwVUkhsRfOAVYCYaMn5VWYDLYEflk1fOL8aYhatmeVEQ==} - '@tryghost/mongo-knex@0.10.1': - resolution: {integrity: sha512-6LRA4zVpNvCrVrA9hOhs8N4iylAiafGssiKuD8yML/13xg2PvKw6dzOZj6hg0CKQwG7tVp8TA+7Nw3iDOzy/jQ==} + '@tryghost/metrics@3.4.2': + resolution: {integrity: sha512-20ldmzt6yHK7835yuU/PJDfZXaHlLl9Y3QfKM2tHUXJH0t6VdTEx1CSEnMjdSgxKG2z5+5uK84FoFD67toUVrQ==} - '@tryghost/mongo-knex@0.11.0': - resolution: {integrity: sha512-OWNRZLAqgRDgW3FFFhsZksiDaUMcjgBQVAWM+PzZEB3JpamdqLq+60MWrbkDiI5WynrFHtYI/5rAOxDaz7HY/g==} + '@tryghost/mongo-knex@0.11.1': + resolution: {integrity: sha512-W5m2aU4WW14fMQoyj5oY6vvHTGzMfz1WEVGK8YTY0zWszJ/kUPo5qHXY9ivoxQthmFoBUpSbNuP0qRAfE6UrNQ==} '@tryghost/mongo-utils@0.6.5': resolution: {integrity: sha512-fMEfdlVaVkr7SJwVxBxVDfUQ+x4DVF4PMet698PLzabqSnGsaWcruBaQlOuNvtf8ITNXKFUAqZMdmSUTIAHU+Q==} @@ -9220,17 +9206,11 @@ packages: '@tryghost/nodemailer@2.3.1': resolution: {integrity: sha512-sIek+jnYoHWRbsvXgjA+v1sBiiXS7Gh75iW2fl/jo+ED1H7B0i+svSMTfKwLpDRlGrUTMP2/swvr8EBb59kC4w==} - '@tryghost/nql-lang@0.6.6': - resolution: {integrity: sha512-ZWEUN92fVnxavf+matwASvi7pom0i1jhAtNCuoCN9f3ShtyrVm92m1SfvY+x/XuEvbefXAzCGzSEMcaQ4cqoXw==} - - '@tryghost/nql-lang@0.6.7': - resolution: {integrity: sha512-quholb52aCqHWCskYp1ZC2hY/E+BD8XmZtgsrp1TcTHm0si9O3DBV9NGX2GB1GEE1V+QHFL24gdp0j98TrhFIQ==} + '@tryghost/nql-lang@0.7.0': + resolution: {integrity: sha512-S3P66JRL7kiKhYR7VnYC5S5xnkKqS+A5aW60nCGi/lraAhk/G5+xmPisztQiJWbHxM1odIagt2GpDMbKCDXT2w==} - '@tryghost/nql@0.13.1': - resolution: {integrity: sha512-TQRDur1miWd2ORa3XOhtw8Na8AUj/8h+epNMDgiutOyAH/RTw+d+ksLndMnm/Z9MhnKg7kFy7398peKZ7nF8bQ==} - - '@tryghost/nql@0.13.2': - resolution: {integrity: sha512-nlG3xMNJYUO5MvOtnSaYltFKRaSeBTalIAUbIyeGplY4YXlIPVH/KUzCIpM8HyJ5uncR8bXkUey0a29RqCFaeA==} + '@tryghost/nql@0.13.3': + resolution: {integrity: sha512-nnX7l8tr6GuPoQML1wDn86JYQpdZ9kFLhJ75JVfF1iTYEWB/WQpJ2tCGRBxXr160jM5L/hMe2dfnZr9EZRMksg==} '@tryghost/pretty-cli@3.3.1': resolution: {integrity: sha512-P7/GffeBG0grjmFxMWEeVFONL6HGt1mWUZvI5G36mhlxC/AXSWCSS6+qiQU91ZfenTwBrP6LZRC9Pyt0gqfzLg==} @@ -9238,11 +9218,8 @@ packages: '@tryghost/pretty-cli@3.3.2': resolution: {integrity: sha512-JOWoEwrdF+flxZBWq+ikww5BRvU95jfUzUnx25pkrSNJEdTYWzJj3bsTGt7LravWCN41M9y4+XvYwEp6cfawpg==} - '@tryghost/pretty-stream@0.2.5': - resolution: {integrity: sha512-2bc5mk+7BAOt/mrcbjTsmUQhTKqysbIDOUUThSemPVbnij6487DhaRbnYWZW2IGKfAucrEO3h6TTm3kfioKJSw==} - - '@tryghost/pretty-stream@2.3.5': - resolution: {integrity: sha512-vXZyqLzgCWyfuqxdaXITxUyazacgb83nBMjClbbp43rnHVisx1iuOotZCiXjnb/6EoLmIWAVdFujHNE+bMomXw==} + '@tryghost/pretty-stream@2.3.8': + resolution: {integrity: sha512-yd95fnXZ9ssS4nfMzzaPCnD+v2GR0c2cQonX1X9+njH3gBln+nyEUlOhQZWGArqq393YeITeC0B2ctPNBFuQRA==} '@tryghost/prometheus-metrics@1.0.8': resolution: {integrity: sha512-E1LRRwLgiWIN/P20uHgpTK+JVYYQ3+BarKCBszB6qmK5IBANJvQHI3LQV0wDWefVwFyl5FI6AqxLpOOpquvahA==} @@ -9260,11 +9237,8 @@ packages: resolution: {integrity: sha512-Uz7bj8IadLah1j9GgieGQ0VGytdOG6mIlYdbpNu1r8/o9pjR2IS4bt88tvwpGSPNb47P9NrGntGn+Le1W/90jg==} engines: {node: '>=16.0.0'} - '@tryghost/request@3.3.4': - resolution: {integrity: sha512-EjNHLmBPWsQmnrgb0VTiqrNRH/E+y9cG1jQ06gTSbwKS1d5om6Fmf9mCF7Fwysmdspt3tbpw/gx/T7PhS+W+Kw==} - - '@tryghost/request@3.3.5': - resolution: {integrity: sha512-yQchJt93YQsFdRrXlTu6A37ffux/C7aRCXQqzn0luBw8gRsQzCxTa28mLrjGlRJQiwIuuf77tZ2dCnPkUNxOgg==} + '@tryghost/request@4.0.1': + resolution: {integrity: sha512-AZdig94vsNuG6PORGe5Ygm5+tuAOQikND7znHiwMASWxP34Kt1pd8QNuKvCyGDtlD7hwanX8/Pc4IP7RCtganA==} '@tryghost/root-utils@0.3.38': resolution: {integrity: sha512-ARn8wC6qv867lCr7BZ+IS8S/88K5go0j6HgQFhP27rKje4b40PsxH/P3rO4Ez2NzF/Do8ywFrTWHoLCSsCazXQ==} @@ -9275,15 +9249,15 @@ packages: '@tryghost/root-utils@2.3.2': resolution: {integrity: sha512-tUyMS4xJUceE62JjZIjWAXCeoVlbNHWF2dIy6xJkaYgeHw4oZ5YoKMu9Mzqa7TPHIapU+BxwdWV0HvMj9dZGlA==} - '@tryghost/root-utils@2.3.4': - resolution: {integrity: sha512-0ScfhRAXdaCWHlQ9J3N0sZjscLb5amMjPS07grCcOxQpeLgBOVSfLPNT6bBVRUOm3MIQnoDdGoPlZhJJnIbE/g==} - '@tryghost/root-utils@2.3.5': resolution: {integrity: sha512-7OnoEPEAAaT9LFgR0WNxHUeehC1VSits1k3KuW5FpqitE6NznieiSK9Cpo8v0489Xq7d0Xbkc9qsDytWCCRGCg==} '@tryghost/root-utils@2.3.7': resolution: {integrity: sha512-8HR0W95it+s+ERkRI3syWHDLyYcinBFexpVlmLBhMMf8yEcL7r6tio6H3sUAj09L/YE8Kk5uGhvjAZAT3TU77Q==} + '@tryghost/root-utils@2.3.8': + resolution: {integrity: sha512-n12HsmW0mms6hlfYYSOcsJ9um6jifBvBFXJak2yPBySSmoTZpCgPezG9JlTXtPjG+4VmfvKaOE7NxlbEJyegiw==} + '@tryghost/security@1.0.6': resolution: {integrity: sha512-h4FiUK4ndHezlXeuRzfwTZrX/a8ZdCS/FRqmZWCHcMUiBH6lewHv8eIjsPemN6e6Gn9jtsWZsKnuYM2mD0meSQ==} @@ -9309,15 +9283,15 @@ packages: '@tryghost/tpl@2.3.1': resolution: {integrity: sha512-qqa2SvhnBVKFYN+G4hfj2cYZuZXINBDyJ9cTzNBtev/FRNUKniyGarPKAkyb6ZBtvp3Nqlmyvrqe+SFoqHcfrQ==} - '@tryghost/tpl@2.3.4': - resolution: {integrity: sha512-/sPmwPuTcu1noY5PGwTwDmlE8wZuttl2r1IORMF5vxRKYUC7iWQnyKNstXtYfgh3bgBTYcq0kkfoPdz3B4GEKQ==} - '@tryghost/tpl@2.3.5': resolution: {integrity: sha512-wexPVuAfF+k/Ysy1uTLTHPznQVsKe5kImlvhKd3IQU1eA4ZYGwymtD7GaSvJxHvyaS6zRB8FXzbgXl2dpAUyVg==} '@tryghost/tpl@2.3.7': resolution: {integrity: sha512-TIDQF9tj4MaQKrIxNB8CxpElShAwLfyByozggIBnuzIVkGudX07gy/o9oWGRYslRselrvdByf+NpYonQ9j3Y0Q==} + '@tryghost/tpl@2.3.8': + resolution: {integrity: sha512-WkStVunfpAzfZsGx9Hy4hi+CF5ZYNzwBkxY6+JAeQs8O4npWfVpMLQCEP+CaHpFkuJRhVVtH63Y4KbRw97cH6g==} + '@tryghost/url-utils@5.2.6': resolution: {integrity: sha512-3TQRcseZW/rq3UsSMP94n6WgKYZJqufreNdv9wJT5P4Fm5bNQ04rEHvL8i1AggXWa2kUPmwt2ed7KzNY6xx7zA==} @@ -9327,20 +9301,17 @@ packages: '@tryghost/validator@0.2.22': resolution: {integrity: sha512-dmobNVEKXMi3K4OdAXLBFwa78hVgy8cYvCPJgfV4h3NtYIyRHqwARr3upT3/ASpVBpKGVBu7XexfSv+tibzsuQ==} - '@tryghost/validator@3.2.4': - resolution: {integrity: sha512-voyNSOxIhS5ltyFGqy+xxp6dRgsMkgfxE/6T7S8q4II+WCkU0W9mXu+NP9tpv5frazN7ZlRE0/RHVKzxXC6lbQ==} - '@tryghost/validator@3.2.5': resolution: {integrity: sha512-uIpY/QFGjsVgTjT2Hzps4iTxyS3Shjh5g3MpB1QCG6yXm7KWinTfuh8ps2ZNgxKqDjCPEQH6gTWpREugTlLHaw==} + '@tryghost/validator@3.2.8': + resolution: {integrity: sha512-aw5c77K9vl8/dh8AEqoG7bf3YE0gtqKJSasJeu5j56VbaVHhexAh92FXK5BP8pyOfQwqz1ZREuWsfwiDUl5YGQ==} + '@tryghost/version@2.3.2': resolution: {integrity: sha512-tBW8gJKLJTUsrGdt/mM1k28Vy4DRTQF7tfZPUf9DQ0KkTgFc+5Z2WipGa50dOaicGysM15imOq7AK7IfXddCHw==} - '@tryghost/version@2.3.4': - resolution: {integrity: sha512-uHCFpPnM/1eS3VbUxKNOj0rUkvUQYIsxZuIfVEJmsBTvm3OAGvajPaqM/hq1T4pN+vAdz/j90tEr1hoNHoLQ5g==} - - '@tryghost/version@2.3.5': - resolution: {integrity: sha512-IXL8KQNHYDtZn0QMzHTlzCPijHFDSyQgsqY9WGmjAkOdRmDNB/RcCe3XEk6/n2W9CxhNW6yE1e47bH4mY9VBlg==} + '@tryghost/version@2.3.8': + resolution: {integrity: sha512-25Aa+J9AvnTLhcu3ph4Klw/nt45h2EUtmfGpSLN6TQykreV20bGvBLzUHlHhnL1kuNpuQaR9qSCvMNU9yYzXmA==} '@tryghost/webhook-mock-receiver@2.1.0': resolution: {integrity: sha512-Cka5SW4igfgbGv33fFyEjkgt1HCA3lNhKCL88ThsDCw3/9ROxKQLcHMB+S7SNcewIDfsS5kI2INHtD+1Tbp/BA==} @@ -9553,8 +9524,8 @@ packages: '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - '@types/jsdom@28.0.3': - resolution: {integrity: sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ==} + '@types/jsdom@30.0.0': + resolution: {integrity: sha512-uAHGxujGE0cDaKGdK28zgDotFtNA7MKq5DXl8LrfdxdCI8VHcg15oJz+amHTChPNI5JpgEPQWc2xFdrw3em/nQ==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -11638,10 +11609,6 @@ packages: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} - cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} - cacheable-request@13.0.19: resolution: {integrity: sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==} engines: {node: '>=18'} @@ -11862,10 +11829,14 @@ packages: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} - chrono-node@2.9.1: - resolution: {integrity: sha512-nqP8Zp11efCYQIESXPxeDM8ikzN5BDb3Zzou+a66fZq+X2hzKFdsNLQE2/uBAh//BZEMbaMo1eTnagK7hOenAg==} + chrono-node@2.10.1: + resolution: {integrity: sha512-tPOsBzYVAK5zth+CiHCgp5NGeqRc4mMD8FPthQ5IxkgjPWWxIFdM5odnmfw//IRAzl6C++Xy8olQW7wn5qBR1g==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + chunk-data@0.1.0: + resolution: {integrity: sha512-zFyPtyC0SZ6Zu79b9sOYtXZcgrsXe0RpePrzRyj52hYVFG1+Rk6rBqjjOEk+GNQwc3PIX+86teQMok970pod1g==} + engines: {node: '>=20'} + ci-info@2.0.0: resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} @@ -12587,9 +12558,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - csv-writer@1.6.0: - resolution: {integrity: sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==} - custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} @@ -14563,14 +14531,6 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - - form-data-encoder@4.1.0: - resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} - engines: {node: '>= 18'} - form-data@3.0.5: resolution: {integrity: sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==} engines: {node: '>= 6'} @@ -14624,6 +14584,10 @@ packages: resolution: {integrity: sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==} engines: {node: '>=14.14'} + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + fs-extra@4.0.3: resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} @@ -14940,13 +14904,9 @@ packages: resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} engines: {node: '>=10.19.0'} - got@13.0.0: - resolution: {integrity: sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==} - engines: {node: '>=16'} - - got@14.6.6: - resolution: {integrity: sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==} - engines: {node: '>=20'} + got@15.1.0: + resolution: {integrity: sha512-DG+DAAkRtno+oDr/GBsliAkhN9+zczOPM5qXk3efDZY3qvyRnZU+NwQlb/IOY6cSnxxTR9z3aHCcShJyjb0hJA==} + engines: {node: '>=22'} graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -15460,8 +15420,8 @@ packages: resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} engines: {node: '>=12.0.0'} - install-artifact-from-github@1.6.0: - resolution: {integrity: sha512-wKsuzN8fy8QK7iEUqyWTQmvZ1QFGPn1xyl3/1iIIDthDjS7Hn9HoPwHlNakZirWbCsbad0lZMkr6Xfbpe1pUzw==} + install-artifact-from-github@1.7.0: + resolution: {integrity: sha512-qAb91yAKVF9rFY4rVP21ZtYUyCScxAFt9udwzVWNLBE1pQcdQeB2gd1HlNPcQNYCzCDvJ/QJQPuWQ6aTmSlU8g==} engines: {node: '>=18'} hasBin: true @@ -16233,11 +16193,11 @@ packages: canvas: optional: true - jsdom@29.1.1: - resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} peerDependencies: - canvas: ^3.0.0 + canvas: ^3.2.3 peerDependenciesMeta: canvas: optional: true @@ -16323,8 +16283,8 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} - jsonrepair@3.14.0: - resolution: {integrity: sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==} + jsonrepair@3.15.0: + resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} hasBin: true jsonwebtoken@8.5.1: @@ -16942,6 +16902,10 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lowercase-keys@4.0.1: + resolution: {integrity: sha512-wI9Nui/L8VfADa/cr/7NQruaASk1k23/Uh1khQ02BCVYiiy8F4AhOGnQzJy3Fl/c44GnYSbZHv8g7EcG3kJ1Qg==} + engines: {node: '>=20'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -17243,44 +17207,44 @@ packages: resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} engines: {node: '>=18.0.0'} - metascraper-amazon@5.51.2: - resolution: {integrity: sha512-m4I310TMLCfm/mV2YiKxin+BVYTuV/rVhndjjYV0Phen8DYowf+PqSD+ZjF4fwkxQYZ1DlPCAaT8A0xikPLSWg==} + metascraper-amazon@5.55.2: + resolution: {integrity: sha512-6z3fdZDQ1wACEdsjdzX19Grk6T9eQPQ3yfHCCYGFHUFA6w0eT7J+kkP3VRb4oENFA8AtaNYVGR29XcCLh+GOmw==} engines: {node: '>= 22'} - metascraper-author@5.51.1: - resolution: {integrity: sha512-wXhGsUQ4HDQoYeEYyLqAd1OzfMH+EdpN0lR3bQNF09JdvCwD8gacVEIyMogO1SQ0DjvecvIEL3wJgVFlMI6q0Q==} + metascraper-author@5.55.2: + resolution: {integrity: sha512-UGj3sovGA8M/JxgKpUFeljfo8vE0U0Zf7okT5X3oSWdhE9k6mCIRsfSWzIX2ZPNFUpTONgEgNKPcjr2UvXqrGg==} engines: {node: '>= 22'} - metascraper-description@5.51.1: - resolution: {integrity: sha512-N7ki5J46p5b7fdw/+55i8kMnjQ1tJ2vHa/RTeJ9DidK46kUzReQV9S6r2J9fqZwUEGB6sMQjyX2SkXqmeOdhtQ==} + metascraper-description@5.55.2: + resolution: {integrity: sha512-lV10KxGQ05QAquF+Uie2TfuXh3Ma7SGU7LIByaIwH3LhkR9PFz5zCbHwg3lDtQubDB/s8BRAOUMsDq2WToPBmQ==} engines: {node: '>= 22'} - metascraper-image@5.51.1: - resolution: {integrity: sha512-oRiCb6KB7VrfSMQCHcon70BkoJP2B53d0sEjTwFrsVwV4uSbZHtlTRyIZ00tXZ07wbYJeTst/PjUywCtbIMEfA==} + metascraper-image@5.55.2: + resolution: {integrity: sha512-lzznTrLmIO2I+NlmPqnYpRgE+YoPlxwgsSZUBk7q30Ir/kfQL+qjcblesjlYs36rzgaAPBx70+LKSdxJyEh+ew==} engines: {node: '>= 22'} - metascraper-logo-favicon@5.51.1: - resolution: {integrity: sha512-i7pNj740f0rWs8ni3AIt4umo4sRi6Q2sR6OFQtqRdnUTZzS0hGtrkAMkyICLfX7RoUek+e7fROhmd9a5YtZ4HA==} + metascraper-logo-favicon@5.55.2: + resolution: {integrity: sha512-KdTjf5yo//t5XOMSBYGEudLMuHrMpx10HXuXpvn8FhH3QApQruDMrdvf9mrtTRlBfHb93nms6BQcPnSjkdwH8w==} engines: {node: '>= 22'} - metascraper-logo@5.51.1: - resolution: {integrity: sha512-Oy1T3lFbn+g+q77e6HLIek44feIYjPm8YPKB3+5vP18+vJK1vK0dncfvOJz4GRDykTyHHzepAuE3lvl45hUFOA==} + metascraper-logo@5.55.2: + resolution: {integrity: sha512-ilLuulh+YbAeD72kFN0rHa7p1LTjnMlQfJAJslLtdD1hw2Vu4GEc3lPQvQupUyOPCoqNrnWz6vECv27MvBce0A==} engines: {node: '>= 22'} - metascraper-publisher@5.51.1: - resolution: {integrity: sha512-WWoUr+b731k9HOXxokWEiTHGxyvKwCn1R73h7++6TNmHcPSJWfTtg7JuS/7FDNSbIbHW6Afpi43yBkhGm4y1Vg==} + metascraper-publisher@5.55.2: + resolution: {integrity: sha512-8KkurmP+9qq2DqScLtjKMrX6SCMfOg2uSdEpYffj2/dNSnLBbGdO5Q5t25pjjtWyY4VLd3mI/tWzMUty4xwLzQ==} engines: {node: '>= 22'} - metascraper-title@5.51.1: - resolution: {integrity: sha512-aXsjULWwxjJwL9JpQL5Ihx1QYGLb3ApuRLTk8DDzHvlSliQ9mpU7wRpZDmt9XJIOJ5RgNqTLobSd+7/BINOsow==} + metascraper-title@5.55.2: + resolution: {integrity: sha512-swMLD+utq1ejyGMMUXyrpFw8oBbnzgMh/iWn9xIRqtSKH9GrA2HS1gx6mWx9sYDCH3+XTPtaj85XF+DZoeEgjA==} engines: {node: '>= 22'} - metascraper-url@5.51.1: - resolution: {integrity: sha512-WmY1AdM4uRJu1l4XfC5A84JdfYGX7I0+NQQSNkHuib4x484Fzqvl6OehBPleP4qjdDl9k0I7uZ8ivY6kFzOABQ==} + metascraper-url@5.55.2: + resolution: {integrity: sha512-DBYPjGnYM3hkUgz5ZY5d8s9CKiA9q7/2MhlcZMuszGnoOWq0IMbzuysSjngSbFHHbf+NwEGORUvZPQlxw6NafA==} engines: {node: '>= 22'} - metascraper@5.51.1: - resolution: {integrity: sha512-7ZmGn0EBv0oNxwaaUuxeUge/GkpCfuuuMV6/0aXgfU4WQ60yJj+/u13A2tqSe0otQq640YkEXua90uKsAwlWdA==} + metascraper@5.55.2: + resolution: {integrity: sha512-ePRV1iObxmRs6yKLZuMTfT2yz8psgZYQuSR9Kpjsr+bbjfsc3SUErPXczEwpemmgi3HA/vllq7Pk4hhvKTxCzA==} engines: {node: '>= 22'} methods@1.1.2: @@ -17692,6 +17656,9 @@ packages: nan@2.27.0: resolution: {integrity: sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==} + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + nanoclone@0.2.1: resolution: {integrity: sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==} @@ -17784,8 +17751,8 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-gyp@13.0.0: - resolution: {integrity: sha512-FYYyBDWdc+kzoyPd5PqHUgM9DGs1C/Z4jxBZAOnA2GRUVXPivKRREq5q+VVPXVr9aGVqGMaMqyFHbviy/yb7Hg==} + node-gyp@13.0.1: + resolution: {integrity: sha512-piOr0S10qy5THB+q5BdqkoOx65XL/tjTMUAit3vciPNp+snTOBnGunWH1Rz7XZUxf2T9uFrfT/Ty4+aC3yPeyg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} hasBin: true @@ -18274,14 +18241,6 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - - p-cancelable@4.0.1: - resolution: {integrity: sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==} - engines: {node: '>=14.16'} - p-defer@3.0.0: resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==} engines: {node: '>=8'} @@ -18342,10 +18301,6 @@ packages: resolution: {integrity: sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==} engines: {node: '>=18'} - p-reflect@2.1.0: - resolution: {integrity: sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg==} - engines: {node: '>=8'} - p-timeout@3.2.0: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} @@ -18396,9 +18351,9 @@ packages: resolution: {integrity: sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==} engines: {node: '>= 0.10'} - parse-email-address@0.0.2: - resolution: {integrity: sha512-Ul8NS2MQ6PG7xdaa2SCNbkUjKcm0TTySXWquqDT8u+vldPVHZNhhrwfhG0KqN8caA96ul34Ptu6JtX7JYtK8CA==} - engines: {node: '>=20'} + parse-email-address@0.0.4: + resolution: {integrity: sha512-bfgHub0chtuU8VhH8OHm8wpS9k5ecR18XdK1ZuvyYCMqejwZr0y40CC9COvlFnm2qsdVyRfCZ+zukkv7KOAfyw==} + engines: {node: '>=22'} parse-entities@1.2.2: resolution: {integrity: sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==} @@ -19659,13 +19614,13 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - re2@1.25.0: - resolution: {integrity: sha512-mtxKjWS+VYIt2ijgt6ohEdwzNlGPom1whyaEKJD40cBc/wqkO1vJoOyK539Qb8Xa9m4GA6hiPGDIbW/d3egSRQ==} + re2@1.26.1: + resolution: {integrity: sha512-oi79a4h6EO3PAwNsDMWgeCcsRGQEUa52DIgOiFTZGDEZocEXG9h+oXy0qZqndo47huUeJuVWSoOJIEhOupqOcg==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - reachable-url@1.8.3: - resolution: {integrity: sha512-qQnVOlFlTHBgFmOC5siHh17fcR9Zu7eNIBO95bJw3+Ln90raTY45h5mmHVmkKsukbA5z6ds/Me631VuQKAf2cw==} - engines: {node: '>=8'} + reachable-url@2.1.2: + resolution: {integrity: sha512-1iOZpL6fFut9f6frWmP6WwDIni+OOOwwl9jiHqOfoQ6MER1e3n0hJidIWSbgWzAonBlFnVymlEEHmHVDLdt09A==} + engines: {node: '>=14'} react-colorful@5.7.0: resolution: {integrity: sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==} @@ -20136,10 +20091,6 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} - responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} - responselike@4.0.2: resolution: {integrity: sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==} engines: {node: '>=20'} @@ -20368,9 +20319,6 @@ packages: resolution: {integrity: sha512-cZFz5XcvYzdHUCOwwrrSFPA9KZdukGENO7ukOgxpcScZaM8zRMIyR6LRySdgoX0nD7NH+XY85F1pQ7n89CZliw==} hasBin: true - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - secure-json-parse@3.0.2: resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==} @@ -21377,8 +21325,8 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} - tough-cookie@6.0.1: - resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} engines: {node: '>=16'} tr46@0.0.3: @@ -21649,8 +21597,8 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici-types@7.28.0: - resolution: {integrity: sha512-LJAfY+2w6HGeT8d8J1wNQsUGUEGio6NWWpwdwurQe4f6oojzCFuGLizl1KSve4irsTxyLly1QhEeE6iapdaIvQ==} + undici-types@8.10.0: + resolution: {integrity: sha512-ibvdovq3nCFs8Msrd95BW+zUOq+aOVbT+wpHUoPWhztbHEoPc6oof51iFDB6Es8lTKvNvVW9jNSAB8dwrKTMGg==} undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} @@ -21663,6 +21611,10 @@ packages: resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} engines: {node: '>=20.18.1'} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + unherit@1.1.3: resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} @@ -22338,6 +22290,10 @@ packages: resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -22678,25 +22634,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@asamuzakjp/css-color@5.1.11': + '@asamuzakjp/css-color@6.0.7': dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) - '@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 - '@asamuzakjp/dom-selector@7.1.1': + '@asamuzakjp/dom-selector@8.3.2': dependencies: - '@asamuzakjp/generational-cache': 1.0.1 - '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - - '@asamuzakjp/generational-cache@1.0.1': {} - - '@asamuzakjp/nwsapi@2.3.9': {} + lru-cache: 11.5.2 '@aws-crypto/sha256-browser@5.2.0': dependencies: @@ -23938,7 +23889,7 @@ snapshots: '@colors/colors@1.5.0': optional: true - '@csstools/color-helpers@6.0.2': {} + '@csstools/color-helpers@6.1.0': {} '@csstools/convert-colors@1.4.0': {} @@ -23947,10 +23898,15 @@ snapshots: '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 6.0.2 - '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 @@ -24098,13 +24054,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@elastic/elasticsearch@8.13.1(supports-color@10.2.2)': - dependencies: - '@elastic/transport': 8.4.1(supports-color@10.2.2) - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - '@elastic/elasticsearch@8.19.2(supports-color@10.2.2)': dependencies: '@elastic/transport': 8.10.1(supports-color@10.2.2) @@ -24127,17 +24076,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@elastic/transport@8.4.1(supports-color@10.2.2)': - dependencies: - debug: 4.4.3(supports-color@10.2.2) - hpagent: 1.2.0 - ms: 2.1.3 - secure-json-parse: 2.7.0 - tslib: 2.8.1 - undici: 6.27.0 - transitivePeerDependencies: - - supports-color - '@ember-data/adapter@3.24.0(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@ember-data/private-build-infra': 3.24.0(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) @@ -25806,10 +25744,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@metascraper/helpers@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2))': + '@metascraper/helpers@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2))': dependencies: audio-extensions: 0.0.0 - chrono-node: 2.9.1 + chrono-node: 2.10.1 condense-whitespace: 3.0.0 data-uri-utils: 1.0.13 debug-logfmt: 1.4.13(debug@4.4.3(supports-color@10.2.2)) @@ -25821,17 +25759,17 @@ snapshots: is-uri: 1.2.14 iso-639-3: 3.0.1 isostring: 0.0.1 - jsdom: 29.1.1(@noble/hashes@1.8.0) - jsonrepair: 3.14.0 + jsdom: 30.0.1(@noble/hashes@1.8.0) + jsonrepair: 3.15.0 lodash: 4.18.1 memoize-one: 6.0.0 microsoft-capitalize: 1.0.8 mime: 4.1.0 normalize-url: 9.0.1 - re2: 1.25.0 + re2: 1.26.1 smartquotes: 2.3.2 tldts: 7.4.4 - url-regex-safe: 4.0.0(re2@1.25.0) + url-regex-safe: 4.0.0(re2@1.26.1) video-extensions: 2.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -27631,9 +27569,7 @@ snapshots: '@sindresorhus/is@4.6.0': {} - '@sindresorhus/is@5.6.0': {} - - '@sindresorhus/is@7.2.0': {} + '@sindresorhus/is@8.1.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -28144,10 +28080,6 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@szmarczak/http-timer@5.0.1': - dependencies: - defer-to-connect: 2.0.1 - '@tabby_ai/hijri-converter@1.0.5': {} '@tailwindcss/node@4.2.2': @@ -28463,7 +28395,7 @@ snapshots: '@tryghost/api-framework@3.3.5(supports-color@10.2.2)': dependencies: '@tryghost/debug': 2.3.5(supports-color@10.2.2) - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 '@tryghost/promise': 2.3.5 '@tryghost/tpl': 2.3.5 '@tryghost/validator': 3.2.5 @@ -28473,7 +28405,7 @@ snapshots: '@tryghost/bookshelf-collision@2.3.7': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 lodash: 4.18.1 moment-timezone: 0.5.45 @@ -28489,8 +28421,8 @@ snapshots: '@tryghost/bookshelf-filter@2.3.7(supports-color@10.2.2)': dependencies: '@tryghost/debug': 2.3.7(supports-color@10.2.2) - '@tryghost/errors': 3.3.5 - '@tryghost/nql': 0.13.2(supports-color@10.2.2) + '@tryghost/errors': 3.3.8 + '@tryghost/nql': 0.13.3(supports-color@10.2.2) '@tryghost/tpl': 2.3.7 transitivePeerDependencies: - supports-color @@ -28515,7 +28447,7 @@ snapshots: '@tryghost/bookshelf-pagination@2.4.1': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 '@tryghost/tpl': 2.3.7 lodash: 4.18.1 @@ -28538,7 +28470,7 @@ snapshots: '@tryghost/bookshelf-transaction-events@2.3.7': {} - '@tryghost/brute-knex@3.2.1(better-sqlite3@12.11.1)(express@4.22.2(supports-color@10.2.2))(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2)': + '@tryghost/brute-knex@3.2.2(better-sqlite3@12.11.1)(express@4.22.2(supports-color@10.2.2))(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2)': dependencies: express-brute: 1.0.1(express@4.22.2(supports-color@10.2.2)) knex: 2.4.2(better-sqlite3@12.11.1)(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2) @@ -28552,7 +28484,7 @@ snapshots: - supports-color - tedious - '@tryghost/bunyan-rotating-filestream@0.0.11': + '@tryghost/bunyan-rotating-filestream@0.0.14': dependencies: long-timeout: 0.1.1 @@ -28602,25 +28534,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/domain-events@3.3.5(supports-color@10.2.2)': + '@tryghost/debug@2.3.8(supports-color@10.2.2)': dependencies: - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/root-utils': 2.3.8 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - - '@75lb/nature' - supports-color - '@tryghost/elasticsearch@3.0.29(supports-color@10.2.2)': + '@tryghost/domain-events@3.3.9(supports-color@10.2.2)': dependencies: - '@elastic/elasticsearch': 8.13.1(supports-color@10.2.2) - '@tryghost/debug': 0.1.40(supports-color@10.2.2) - split2: 4.2.0 + '@tryghost/logging': 5.3.4(supports-color@10.2.2) transitivePeerDependencies: + - '@75lb/nature' - supports-color - '@tryghost/elasticsearch@5.4.2(supports-color@10.2.2)': + '@tryghost/elasticsearch@5.4.5(supports-color@10.2.2)': dependencies: '@elastic/elasticsearch': 8.19.2(supports-color@10.2.2) - '@tryghost/debug': 2.3.5(supports-color@10.2.2) + '@tryghost/debug': 2.3.8(supports-color@10.2.2) split2: 4.2.0 transitivePeerDependencies: - '@75lb/nature' @@ -28649,7 +28580,7 @@ snapshots: - webpack-cli - webpack-command - '@tryghost/errors@3.3.5': {} + '@tryghost/errors@3.3.8': {} '@tryghost/express-test@2.1.0(express@4.22.2(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: @@ -28672,14 +28603,14 @@ snapshots: '@tryghost/http-cache-utils@0.1.25': {} - '@tryghost/http-stream@2.3.5': + '@tryghost/http-stream@2.3.9': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/request': 3.3.5 + '@tryghost/errors': 3.3.8 + '@tryghost/request': 4.0.1 '@tryghost/image-transform@1.4.17(@types/node@22.20.1)': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 fs-extra: 11.3.6 optionalDependencies: sharp: 0.35.3(@types/node@22.20.1) @@ -28690,7 +28621,7 @@ snapshots: dependencies: '@jest/expect': 30.3.0(supports-color@10.2.2) '@jest/expect-utils': 30.3.0 - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 jest-snapshot: 30.3.0(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -28698,8 +28629,8 @@ snapshots: '@tryghost/job-manager@1.0.9(supports-color@10.2.2)': dependencies: '@breejs/later': 4.2.0 - '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/errors': 3.3.8 + '@tryghost/logging': 5.3.4(supports-color@10.2.2) bree: 6.5.0(supports-color@10.2.2) cron-validate: 1.4.5 fastq: 1.20.1 @@ -28711,19 +28642,19 @@ snapshots: '@tryghost/limit-service@1.5.6': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 lodash: 4.18.1 luxon: 3.7.2 - '@tryghost/logging@5.3.0(supports-color@10.2.2)': + '@tryghost/logging@5.3.4(supports-color@10.2.2)': dependencies: - '@tryghost/bunyan-rotating-filestream': 0.0.11 - '@tryghost/elasticsearch': 5.4.2(supports-color@10.2.2) - '@tryghost/http-stream': 2.3.5 - '@tryghost/pretty-stream': 2.3.5 - '@tryghost/root-utils': 2.3.5 + '@tryghost/bunyan-rotating-filestream': 0.0.14 + '@tryghost/elasticsearch': 5.4.5(supports-color@10.2.2) + '@tryghost/http-stream': 2.3.9 + '@tryghost/pretty-stream': 2.3.8 + '@tryghost/root-utils': 2.3.8 bunyan: 1.8.15 - fs-extra: 11.3.6 + fs-extra: 11.4.0 gelf-stream: 1.1.1 json-stringify-safe: 5.0.1 lodash: 4.18.1 @@ -28731,23 +28662,17 @@ snapshots: - '@75lb/nature' - supports-color - '@tryghost/metrics@1.0.43(supports-color@10.2.2)': + '@tryghost/metrics@3.4.2(supports-color@10.2.2)': dependencies: - '@tryghost/elasticsearch': 3.0.29(supports-color@10.2.2) - '@tryghost/pretty-stream': 0.2.5 - '@tryghost/root-utils': 0.3.38 + '@tryghost/elasticsearch': 5.4.5(supports-color@10.2.2) + '@tryghost/pretty-stream': 2.3.8 + '@tryghost/root-utils': 2.3.8 json-stringify-safe: 5.0.1 transitivePeerDependencies: + - '@75lb/nature' - supports-color - '@tryghost/mongo-knex@0.10.1(supports-color@10.2.2)': - dependencies: - debug: 4.4.3(supports-color@10.2.2) - lodash: 4.18.1 - transitivePeerDependencies: - - supports-color - - '@tryghost/mongo-knex@0.11.0(supports-color@10.2.2)': + '@tryghost/mongo-knex@0.11.1(supports-color@10.2.2)': dependencies: debug: 4.4.3(supports-color@10.2.2) lodash: 4.18.1 @@ -28765,7 +28690,7 @@ snapshots: '@tryghost/mw-error-handler@1.0.13(supports-color@10.2.2)': dependencies: '@tryghost/debug': 0.1.40(supports-color@10.2.2) - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 '@tryghost/http-cache-utils': 0.1.25 '@tryghost/tpl': 0.1.40 lodash: 4.18.1 @@ -28778,7 +28703,7 @@ snapshots: '@tryghost/nodemailer@2.3.1(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: '@aws-sdk/client-sesv2': 3.1073.0 - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 '@tryghost/tpl': 2.3.1 nodemailer: 9.0.1 nodemailer-mailgun-transport: 2.1.5(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2) @@ -28787,29 +28712,15 @@ snapshots: - debug - supports-color - '@tryghost/nql-lang@0.6.6': - dependencies: - date-fns: 2.30.0 - - '@tryghost/nql-lang@0.6.7': - dependencies: - date-fns: 2.30.0 - - '@tryghost/nql@0.13.1(supports-color@10.2.2)': + '@tryghost/nql-lang@0.7.0': dependencies: - '@tryghost/mongo-knex': 0.10.1(supports-color@10.2.2) - '@tryghost/mongo-utils': 0.6.5 - '@tryghost/nql-lang': 0.6.6 - lodash: 4.18.1 - mingo: 2.5.3 - transitivePeerDependencies: - - supports-color + luxon: 3.7.2 - '@tryghost/nql@0.13.2(supports-color@10.2.2)': + '@tryghost/nql@0.13.3(supports-color@10.2.2)': dependencies: - '@tryghost/mongo-knex': 0.11.0(supports-color@10.2.2) + '@tryghost/mongo-knex': 0.11.1(supports-color@10.2.2) '@tryghost/mongo-utils': 0.6.6 - '@tryghost/nql-lang': 0.6.7 + '@tryghost/nql-lang': 0.7.0 lodash: 4.18.1 mingo: 2.5.3 transitivePeerDependencies: @@ -28825,13 +28736,7 @@ snapshots: chalk: 5.6.2 sywac: 1.3.0 - '@tryghost/pretty-stream@0.2.5': - dependencies: - date-format: 4.0.14 - lodash: 4.18.1 - prettyjson: 1.2.5 - - '@tryghost/pretty-stream@2.3.5': + '@tryghost/pretty-stream@2.3.8': dependencies: date-format: 4.0.14 lodash: 4.18.1 @@ -28839,7 +28744,7 @@ snapshots: '@tryghost/prometheus-metrics@1.0.8(supports-color@10.2.2)': dependencies: - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/logging': 5.3.4(supports-color@10.2.2) express: 4.22.1(supports-color@10.2.2) prom-client: 15.1.3 stoppable: 1.1.0 @@ -28855,22 +28760,13 @@ snapshots: '@tryghost/referrer-parser@0.1.21': {} - '@tryghost/request@3.3.4': + '@tryghost/request@4.0.1': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/validator': 3.2.4 - '@tryghost/version': 2.3.4 - cacheable-lookup: 7.0.0 - got: 14.6.6 - lodash: 4.18.1 - - '@tryghost/request@3.3.5': - dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/validator': 3.2.5 - '@tryghost/version': 2.3.5 + '@tryghost/errors': 3.3.8 + '@tryghost/validator': 3.2.8 + '@tryghost/version': 2.3.8 cacheable-lookup: 7.0.0 - got: 14.6.6 + got: 15.1.0 lodash: 4.18.1 '@tryghost/root-utils@0.3.38': @@ -28888,17 +28784,17 @@ snapshots: caller: 1.1.0 find-root: 1.1.0 - '@tryghost/root-utils@2.3.4': + '@tryghost/root-utils@2.3.5': dependencies: caller: 1.1.0 find-root: 1.1.0 - '@tryghost/root-utils@2.3.5': + '@tryghost/root-utils@2.3.7': dependencies: caller: 1.1.0 find-root: 1.1.0 - '@tryghost/root-utils@2.3.7': + '@tryghost/root-utils@2.3.8': dependencies: caller: 1.1.0 find-root: 1.1.0 @@ -28911,7 +28807,7 @@ snapshots: '@tryghost/server@3.1.1(supports-color@10.2.2)': dependencies: '@tryghost/debug': 2.3.1(supports-color@10.2.2) - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/logging': 5.3.4(supports-color@10.2.2) transitivePeerDependencies: - '@75lb/nature' - supports-color @@ -28934,12 +28830,12 @@ snapshots: '@tryghost/tpl@2.3.1': {} - '@tryghost/tpl@2.3.4': {} - '@tryghost/tpl@2.3.5': {} '@tryghost/tpl@2.3.7': {} + '@tryghost/tpl@2.3.8': {} + '@tryghost/url-utils@5.2.6': dependencies: cheerio: 1.2.0 @@ -28962,24 +28858,24 @@ snapshots: '@tryghost/validator@0.2.22': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 '@tryghost/tpl': 0.1.40 lodash: 4.18.1 moment-timezone: 0.5.45 validator: 7.2.0 - '@tryghost/validator@3.2.4': + '@tryghost/validator@3.2.5': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/tpl': 2.3.4 + '@tryghost/errors': 3.3.8 + '@tryghost/tpl': 2.3.5 lodash: 4.18.1 moment-timezone: 0.5.45 validator: 13.15.35 - '@tryghost/validator@3.2.5': + '@tryghost/validator@3.2.8': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/tpl': 2.3.5 + '@tryghost/errors': 3.3.8 + '@tryghost/tpl': 2.3.8 lodash: 4.18.1 moment-timezone: 0.5.45 validator: 13.15.35 @@ -28989,14 +28885,9 @@ snapshots: '@tryghost/root-utils': 2.3.2 semver: 7.8.5 - '@tryghost/version@2.3.4': + '@tryghost/version@2.3.8': dependencies: - '@tryghost/root-utils': 2.3.4 - semver: 7.8.5 - - '@tryghost/version@2.3.5': - dependencies: - '@tryghost/root-utils': 2.3.5 + '@tryghost/root-utils': 2.3.8 semver: 7.8.5 '@tryghost/webhook-mock-receiver@2.1.0': @@ -29005,7 +28896,7 @@ snapshots: '@tryghost/zip@3.5.0(supports-color@10.2.2)': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 archiver: 8.0.0 extract-zip: 2.0.1(supports-color@10.2.2) transitivePeerDependencies: @@ -29016,7 +28907,7 @@ snapshots: '@tryghost/zip@3.5.1(supports-color@10.2.2)': dependencies: - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 archiver: 8.0.0 extract-zip: 2.0.1(supports-color@10.2.2) transitivePeerDependencies: @@ -29277,12 +29168,12 @@ snapshots: '@types/js-yaml@4.0.9': {} - '@types/jsdom@28.0.3': + '@types/jsdom@30.0.0': dependencies: '@types/node': 26.0.0 '@types/tough-cookie': 4.0.5 parse5: 8.0.1 - undici-types: 7.28.0 + undici-types: 8.10.0 '@types/json-schema@7.0.15': {} @@ -29989,7 +29880,7 @@ snapshots: '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) playwright: 1.61.1 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -30002,7 +29893,7 @@ snapshots: '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) playwright: 1.61.1 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -30016,7 +29907,7 @@ snapshots: '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) playwright: 1.61.1 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw @@ -30033,7 +29924,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -30050,7 +29941,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -30068,7 +29959,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -30089,9 +29980,9 @@ snapshots: obug: 2.1.3 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) '@vitest/expect@3.2.4': dependencies: @@ -30172,7 +30063,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/utils@3.2.4': dependencies: @@ -31690,7 +31581,7 @@ snapshots: bookshelf-relations@2.8.0(bookshelf@1.2.0(knex@2.4.2(better-sqlite3@12.11.1)(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2)))(supports-color@10.2.2): dependencies: '@tryghost/debug': 0.1.40(supports-color@10.2.2) - '@tryghost/errors': 3.3.5 + '@tryghost/errors': 3.3.8 bluebird: 3.7.2 bookshelf: 1.2.0(knex@2.4.2(better-sqlite3@12.11.1)(mysql2@3.22.5(@types/node@22.20.1))(supports-color@10.2.2)) lodash: 4.18.1 @@ -32491,16 +32382,6 @@ snapshots: cacheable-lookup@7.0.0: {} - cacheable-request@10.2.14: - dependencies: - '@types/http-cache-semantics': 4.2.0 - get-stream: 6.0.1 - http-cache-semantics: 4.2.0 - keyv: 4.5.4 - mimic-response: 4.0.0 - normalize-url: 8.1.1 - responselike: 3.0.0 - cacheable-request@13.0.19: dependencies: '@types/http-cache-semantics': 4.2.0 @@ -32787,7 +32668,9 @@ snapshots: chrome-trace-event@1.0.4: {} - chrono-node@2.9.1: {} + chrono-node@2.10.1: {} + + chunk-data@0.1.0: {} ci-info@2.0.0: {} @@ -33263,7 +33146,7 @@ snapshots: cpu-features@0.0.10: dependencies: buildcheck: 0.0.7 - nan: 2.27.0 + nan: 2.28.0 optional: true crc-32@1.2.2: {} @@ -33590,8 +33473,6 @@ snapshots: csstype@3.2.3: {} - csv-writer@1.6.0: {} - custom-error-instance@2.1.1: {} cyclist@1.0.2: {} @@ -34070,7 +33951,7 @@ snapshots: dtrace-provider@0.8.8: dependencies: - nan: 2.27.0 + nan: 2.28.0 optional: true dunder-proto@1.0.1: @@ -37061,10 +36942,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data-encoder@2.1.4: {} - - form-data-encoder@4.1.0: {} - form-data@3.0.5: dependencies: asynckit: 0.4.0 @@ -37133,6 +37010,12 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@4.0.3: dependencies: graceful-fs: 4.2.11 @@ -37229,7 +37112,7 @@ snapshots: fsevents@1.2.13: dependencies: bindings: 1.5.0 - nan: 2.27.0 + nan: 2.28.0 optional: true fsevents@2.3.2: @@ -37321,7 +37204,8 @@ snapshots: dependencies: pump: 3.0.4 - get-stream@6.0.1: {} + get-stream@6.0.1: + optional: true get-stream@9.0.1: dependencies: @@ -37560,34 +37444,20 @@ snapshots: p-cancelable: 2.1.1 responselike: 2.0.1 - got@13.0.0: - dependencies: - '@sindresorhus/is': 5.6.0 - '@szmarczak/http-timer': 5.0.1 - cacheable-lookup: 7.0.0 - cacheable-request: 10.2.14 - decompress-response: 6.0.0 - form-data-encoder: 2.1.4 - get-stream: 6.0.1 - http2-wrapper: 2.2.1 - lowercase-keys: 3.0.0 - p-cancelable: 3.0.0 - responselike: 3.0.0 - - got@14.6.6: + got@15.1.0: dependencies: - '@sindresorhus/is': 7.2.0 + '@sindresorhus/is': 8.1.0 byte-counter: 0.1.0 cacheable-lookup: 7.0.0 cacheable-request: 13.0.19 + chunk-data: 0.1.0 decompress-response: 10.0.0 - form-data-encoder: 4.1.0 http2-wrapper: 2.2.1 keyv: 5.6.0 - lowercase-keys: 3.0.0 - p-cancelable: 4.0.1 + lowercase-keys: 4.0.1 responselike: 4.0.2 - type-fest: 4.41.0 + type-fest: 5.8.0 + uint8array-extras: 1.5.0 graceful-fs@4.2.11: {} @@ -37602,9 +37472,9 @@ snapshots: '@sentry/node': 10.65.0(@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1))(supports-color@10.2.2) '@tryghost/config': 2.3.1 '@tryghost/debug': 2.3.1(supports-color@10.2.2) - '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0(supports-color@10.2.2) - '@tryghost/nql': 0.13.1(supports-color@10.2.2) + '@tryghost/errors': 3.3.8 + '@tryghost/logging': 5.3.4(supports-color@10.2.2) + '@tryghost/nql': 0.13.3(supports-color@10.2.2) '@tryghost/pretty-cli': 3.3.1 '@tryghost/server': 3.1.1(supports-color@10.2.2) '@tryghost/zip': 3.5.0(supports-color@10.2.2) @@ -37903,7 +37773,7 @@ snapshots: '@vitest/expect': 4.1.10 jest: 29.7.0(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(supports-color@10.2.2) jest-snapshot: 30.3.0(supports-color@10.2.2) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) html2canvas-pro@2.2.1: dependencies: @@ -38228,7 +38098,7 @@ snapshots: transitivePeerDependencies: - '@types/node' - install-artifact-from-github@1.6.0: {} + install-artifact-from-github@1.7.0: {} internal-slot@1.1.0: dependencies: @@ -39268,10 +39138,10 @@ snapshots: - supports-color - utf-8-validate - jsdom@29.1.1(@noble/hashes@1.8.0): + jsdom@30.0.1(@noble/hashes@1.8.0): dependencies: - '@asamuzakjp/css-color': 5.1.11 - '@asamuzakjp/dom-selector': 7.1.1 + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 '@bramus/specificity': 2.4.2 '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) @@ -39284,12 +39154,12 @@ snapshots: parse5: 8.0.1 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 6.0.1 - undici: 7.28.0 + tough-cookie: 6.0.2 + undici: 8.10.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 5.0.0 - whatwg-url: 16.0.1(@noble/hashes@1.8.0) + whatwg-url: 17.1.0(@noble/hashes@1.8.0) xml-name-validator: 5.0.0 transitivePeerDependencies: - '@noble/hashes' @@ -39356,7 +39226,7 @@ snapshots: jsonpointer@5.0.1: {} - jsonrepair@3.14.0: {} + jsonrepair@3.15.0: {} jsonwebtoken@8.5.1: dependencies: @@ -39477,8 +39347,8 @@ snapshots: knex-migrator@5.4.1(@types/node@22.20.1)(supports-color@10.2.2): dependencies: '@tryghost/database-info': 0.3.35 - '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/errors': 3.3.8 + '@tryghost/logging': 5.3.4(supports-color@10.2.2) '@tryghost/promise': 0.3.20 commander: 5.1.0 compare-ver: 2.0.2 @@ -40028,6 +39898,8 @@ snapshots: lowercase-keys@3.0.0: {} + lowercase-keys@4.0.1: {} + lru-cache@10.4.3: {} lru-cache@11.5.1: {} @@ -40467,85 +40339,85 @@ snapshots: meriyah@6.1.4: {} - metascraper-amazon@5.51.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-amazon@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-author@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-author@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-description@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-description@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-image@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-image@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-logo-favicon@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-logo-favicon@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: '@keyvhq/memoize': 2.2.5 - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) lodash: 4.18.1 - reachable-url: 1.8.3 + reachable-url: 2.1.2 transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-logo@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-logo@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) lodash: 4.18.1 transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-publisher@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-publisher@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-title@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-title@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper-url@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper-url@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) transitivePeerDependencies: - '@noble/hashes' - canvas - debug - metascraper@5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): + metascraper@5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)): dependencies: - '@metascraper/helpers': 5.51.1(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) + '@metascraper/helpers': 5.55.2(@noble/hashes@1.8.0)(debug@4.4.3(supports-color@10.2.2)) cheerio: 1.2.0 debug-logfmt: 1.4.13(debug@4.4.3(supports-color@10.2.2)) whoops: 5.1.2 @@ -41066,7 +40938,7 @@ snapshots: rettime: 0.11.11 statuses: 2.0.2 strict-event-emitter: 0.5.1 - tough-cookie: 6.0.1 + tough-cookie: 6.0.2 type-fest: 5.8.0 until-async: 3.0.2 yargs: 17.7.3 @@ -41091,7 +40963,7 @@ snapshots: rettime: 0.11.11 statuses: 2.0.2 strict-event-emitter: 0.5.1 - tough-cookie: 6.0.1 + tough-cookie: 6.0.2 type-fest: 5.8.0 until-async: 3.0.2 yargs: 17.7.3 @@ -41165,7 +41037,10 @@ snapshots: dependencies: lru.min: 1.1.4 - nan@2.27.0: {} + nan@2.27.0: + optional: true + + nan@2.28.0: {} nanoclone@0.2.1: {} @@ -41260,7 +41135,7 @@ snapshots: node-forge@1.4.0: {} - node-gyp@13.0.0: + node-gyp@13.0.1: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.3 @@ -41270,7 +41145,7 @@ snapshots: semver: 7.8.5 tar: 7.5.16 tinyglobby: 0.2.17 - undici: 6.27.0 + undici: 8.10.0 which: 7.0.0 node-html-markdown@2.0.0: @@ -41895,10 +41770,6 @@ snapshots: p-cancelable@2.1.1: {} - p-cancelable@3.0.0: {} - - p-cancelable@4.0.1: {} - p-defer@3.0.0: {} p-finally@1.0.0: {} @@ -41949,8 +41820,6 @@ snapshots: p-map@7.0.6: {} - p-reflect@2.1.0: {} - p-timeout@3.2.0: dependencies: p-finally: 1.0.0 @@ -41999,7 +41868,7 @@ snapshots: pbkdf2: 3.1.6 safe-buffer: 5.2.1 - parse-email-address@0.0.2: {} + parse-email-address@0.0.4: {} parse-entities@1.2.2: dependencies: @@ -43360,16 +43229,15 @@ snapshots: strip-json-comments: 2.0.1 optional: true - re2@1.25.0: + re2@1.26.1: dependencies: - install-artifact-from-github: 1.6.0 - nan: 2.27.0 - node-gyp: 13.0.0 + install-artifact-from-github: 1.7.0 + nan: 2.28.0 + node-gyp: 13.0.1 - reachable-url@1.8.3: + reachable-url@2.1.2: dependencies: got: 11.8.6 - p-reflect: 2.1.0 react-colorful@5.7.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -43995,10 +43863,6 @@ snapshots: dependencies: lowercase-keys: 2.0.0 - responselike@3.0.0: - dependencies: - lowercase-keys: 3.0.0 - responselike@4.0.2: dependencies: lowercase-keys: 3.0.0 @@ -44306,8 +44170,6 @@ snapshots: ee-types: 2.2.1 glob: 7.2.3 - secure-json-parse@2.7.0: {} - secure-json-parse@3.0.2: {} secure-keys@1.0.0: {} @@ -45626,7 +45488,7 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 - tough-cookie@6.0.1: + tough-cookie@6.0.2: dependencies: tldts: 7.4.4 @@ -45923,7 +45785,7 @@ snapshots: undici-types@7.18.2: {} - undici-types@7.28.0: {} + undici-types@8.10.0: {} undici-types@8.3.0: {} @@ -45931,6 +45793,8 @@ snapshots: undici@7.28.0: {} + undici@8.10.0: {} + unherit@1.1.3: dependencies: inherits: 2.0.4 @@ -46169,12 +46033,12 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - url-regex-safe@4.0.0(re2@1.25.0): + url-regex-safe@4.0.0(re2@1.26.1): dependencies: ip-regex: 4.3.0 tlds: 1.261.0 optionalDependencies: - re2: 1.25.0 + re2: 1.26.1 url@0.11.4: dependencies: @@ -46535,12 +46399,12 @@ snapshots: dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) optionalDependencies: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) @@ -46568,11 +46432,11 @@ snapshots: '@vitest/browser-playwright': 4.1.10(msw@2.14.6(@types/node@22.20.1)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(vite@8.1.3(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) '@vitest/ui': 4.1.10(vitest@4.1.10) - jsdom: 29.1.1(@noble/hashes@1.8.0) + jsdom: 30.0.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) @@ -46600,11 +46464,11 @@ snapshots: '@vitest/browser-playwright': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@1.21.7)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) '@vitest/ui': 4.1.10(vitest@4.1.10) - jsdom: 29.1.1(@noble/hashes@1.8.0) + jsdom: 30.0.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@30.0.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 '@vitest/mocker': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)) @@ -46632,7 +46496,7 @@ snapshots: '@vitest/browser-playwright': 4.1.10(msw@2.14.6(@types/node@26.0.0)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(vite@8.1.3(@types/node@26.0.0)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) '@vitest/ui': 4.1.10(vitest@4.1.10) - jsdom: 29.1.1(@noble/hashes@1.8.0) + jsdom: 30.0.1(@noble/hashes@1.8.0) transitivePeerDependencies: - msw @@ -46893,6 +46757,14 @@ snapshots: transitivePeerDependencies: - '@noble/hashes' + whatwg-url@17.1.0(@noble/hashes@1.8.0): + dependencies: + '@exodus/bytes': 1.15.1(@noble/hashes@1.8.0) + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 19d13e8181d..08164178746 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -36,7 +36,7 @@ allowBuilds: 'msw@2.14.6': true 'nx@23.1.1': true 'protobufjs@7.6.4': true - 're2@1.25.0': true + 're2@1.26.1': true 'sharp@0.35.3': true ssh2: false @@ -80,27 +80,29 @@ catalog: '@testing-library/jest-dom': 6.9.1 '@testing-library/react': 14.3.1 '@tryghost/api-framework': 3.3.5 - '@tryghost/brute-knex': 3.2.1 + '@tryghost/brute-knex': 3.2.2 '@tryghost/color-utils': 0.2.20 '@tryghost/custom-fonts': 1.0.11 - '@tryghost/debug': 2.3.5 - '@tryghost/domain-events': 3.3.5 - '@tryghost/errors': 3.3.5 + '@tryghost/debug': 2.3.8 + '@tryghost/domain-events': 3.3.9 + '@tryghost/errors': 3.3.8 '@tryghost/helpers': 1.1.106 '@tryghost/limit-service': 1.5.6 - '@tryghost/logging': 5.3.0 - '@tryghost/mongo-knex': 0.10.1 - '@tryghost/nql': 0.13.1 - '@tryghost/nql-lang': 0.6.6 + '@tryghost/logging': 5.3.4 + '@tryghost/metrics': 3.4.2 + '@tryghost/mongo-knex': 0.11.1 + '@tryghost/nql': 0.13.3 + '@tryghost/nql-lang': 0.7.0 + '@tryghost/request': 4.0.1 '@tryghost/string': 0.3.5 '@tryghost/timezone-data': 1.0.0 - '@tryghost/tpl': 2.3.5 + '@tryghost/tpl': 2.3.8 '@types/express': 4.17.25 '@types/html-minifier': ^4.0.6 '@types/lodash': 4.17.25 '@types/luxon': 3.7.4 '@types/jest': 29.5.14 - '@types/jsdom': 28.0.3 + '@types/jsdom': 30.0.0 '@types/nconf': 0.10.7 '@types/node': 22.20.1 '@types/react': 18.3.31 @@ -142,8 +144,9 @@ catalog: fs-extra: 11.3.6 glob: 13.0.6 globals: 17.7.0 + got: 15.1.0 js-yaml: 4.3.0 - jsdom: 29.1.1 + jsdom: 30.0.1 jsonc-parser: 3.3.1 knex-migrator: 5.4.1 lodash: 4.18.1 @@ -209,6 +212,7 @@ catalog: remark-cli: 12.0.1 remark-validate-links: 13.1.0 markdownlint-cli2: 0.23.2 + parse-email-address: 0.0.4 catalogs: react17: @@ -229,6 +233,8 @@ overrides: 'knex-migrator>knex': 2.4.2 '@tryghost/errors': 'catalog:' '@tryghost/logging': 'catalog:' + '@tryghost/nql': 'catalog:' + '@tryghost/nql-lang': 'catalog:' jackspeak: 4.2.3 moment: 'catalog:' moment-timezone: 'catalog:'