From 81cb22e49ee60f511f1098dea0ed0e4205f9b90e Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 12:02:41 -0500 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=90=9B=20Stopped=20automation=20run?= =?UTF-8?q?s=20when=20member=20unsubscribes=20(#29959)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes https://linear.app/ghost/issue/NY-1527 Before this change, if a member unsubscribed from automation emails, `send_email` steps would be no-ops. Every step would run, but emails wouldn't be sent. After this change, the automation is stopped. ## Manual test In addition to automated tests, I also did a manual test. 1. I set up an automation with 3 emails separated by 1 day waits. (I configured things so that 1 day got shortened to 15 seconds, for testing.) 2. I signed up and verified that I got the first email. 3. I unsubscribed. 4. After ~15 seconds, I got no email. And the data in the database looked correct. Screencast (no audio): https://github.com/user-attachments/assets/6f1e6512-093d-4bfe-bfc3-24a353bc632f --- .../core/server/services/automations/poll.ts | 10 ++-------- .../server/services/automations/poll.test.ts | 20 ++++++++----------- 2 files changed, 10 insertions(+), 20 deletions(-) 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/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 () { From 5d50c277dfc05d0cef18e696121dc9cf1d1c87b4 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 17 Aug 2026 12:20:14 -0500 Subject: [PATCH 02/19] Removed stale ignoreDeps from Renovate config (#30013) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Most of the `ignoreDeps` entries in `.github/renovate.json5` dated from 2019–2020 and their reasons no longer hold, so they were either silently holding back updates or doing nothing. This prunes the list to entries with a live reason and refreshes the kept comments. No dependency versions change in this PR — Renovate will open the usual PRs for anything newly unignored. --- .github/renovate.json5 | 65 +++++++++--------------------------------- 1 file changed, 13 insertions(+), 52 deletions(-) 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" From aa25788fb317225a87d61ab95c987508a5fa691e Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 13:13:40 -0500 Subject: [PATCH 03/19] Renamed Mailgun provider ID database column (#30025) ref https://github.com/TryGhost/Ghost/pull/29182 This change should have no user impact. This renames `email_batches.provider_id` to `email_batches.mailgun_message_id`. Two benefits: - Clarity (several of us have been confused by this name) - Matches the `automated_email_recipients` table (see https://github.com/TryGhost/Ghost/pull/29182) This also changes its usage on the frontend. Normally I'd be more careful about this change--after all, it's a breaking API change--but it only affects a debug view. --- .../app/components/posts/debug.hbs | 6 ++-- .../ember-admin/app/components/posts/debug.js | 2 +- .../output/mappers/email-batches.js | 2 +- .../server/data/migrations/utils/schema.js | 8 +++-- ...-22-48-rename-email-batches-provider-id.js | 8 +++++ .../core/core/server/data/schema/commands.js | 7 ++-- ghost/core/core/server/data/schema/schema.js | 2 +- .../importers/email-batches-importer.js | 2 +- .../email-service/batch-sending-service.js | 2 +- .../email-service/email-event-processor.js | 12 +++---- .../admin/__snapshots__/emails.test.js.snap | 8 ++--- ghost/core/test/e2e-api/admin/emails.test.js | 2 +- .../test/e2e-api/admin/max-limit-cap.test.js | 2 +- .../email-service/batch-sending.test.js | 20 +++++------ .../newsletter-email-event-storage.test.js | 30 ++++++++-------- .../resume-interrupted-sends.test.js | 4 +-- .../mailgun-email-suppression-list.test.js | 12 +++---- .../utils/serializers/output/mapper.test.js | 4 +-- .../unit/server/data/schema/commands.test.js | 36 +++++++++++++++++++ .../unit/server/data/schema/integrity.test.js | 2 +- .../batch-sending-service.test.js | 8 ++--- .../test/utils/fixtures/data-generator.js | 2 +- 22 files changed, 115 insertions(+), 66 deletions(-) create mode 100644 ghost/core/core/server/data/migrations/versions/6.58/2026-08-11-21-22-48-rename-email-batches-provider-id.js 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/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/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..3e290e9da6f 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', 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..f05a040e280 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 @@ -27,7 +27,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/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/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/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..1f96805d3ce 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 = '205297f193a7d60b95e8ceeab3b80f4f'; const currentFixturesHash = 'd4c9e4fabcec3365c42d37642216021b'; const currentSettingsHash = 'f57245b22af55ea5fc1e5e20913de9bb'; const currentRoutesHash = 'd8c25fa01bf6d22a2bcb05ba0de70dc1'; 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/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 } From 66c0bf78028fc4094772c2e316fec11f7304dd6d Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Mon, 17 Aug 2026 14:15:41 -0400 Subject: [PATCH 04/19] Updated metascraper + jsdom dependencies to latest (#30026) no ref - update jsdom to v30 - update metascraper to latest Updating metascraper to latest ensures we pull in an updated version of `re2` - the latest version of re2 fixes an issue where the shipped binaries weren't `stripped`, meaning the binary was ~20mb larger than it needed to be. Updates jsdom alongside it to ensure that we aren't pulling in two different jsdom versions. --- .changeset/six-parents-shout.md | 11 + ghost/core/package.json | 22 +- koenig/kg-default-nodes/package.json | 2 +- koenig/kg-html-to-lexical/package.json | 3 + koenig/kg-lexical-html-renderer/package.json | 3 + pnpm-lock.yaml | 491 ++++++++++--------- pnpm-workspace.yaml | 6 +- 7 files changed, 286 insertions(+), 252 deletions(-) create mode 100644 .changeset/six-parents-shout.md 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/ghost/core/package.json b/ghost/core/package.json index 45d8d3da5b2..3e19d783374 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -199,16 +199,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 +255,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/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/pnpm-lock.yaml b/pnpm-lock.yaml index 154a1e5cad7..6f13343de7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,8 +196,8 @@ catalogs: 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 @@ -334,8 +334,8 @@ catalogs: 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 @@ -608,7 +608,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 +681,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 +693,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: @@ -907,7 +907,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 +928,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 +959,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 +1044,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 +1065,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 +1090,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: @@ -1196,7 +1196,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 +1220,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: @@ -1672,7 +1672,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 +1687,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 +1874,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,7 +1907,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@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: @@ -2020,7 +2020,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 +2038,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 +2103,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: @@ -2505,7 +2505,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 +2537,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 +2691,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 +2821,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 +2855,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 +2864,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 +2879,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 +2888,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 +2915,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 +2946,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 +2961,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 +2973,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 +3009,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 +3034,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 +3064,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 +3110,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 +3140,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 +3150,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 +3168,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 +3198,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 +3208,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 +3229,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 +3290,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 +3354,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 +3381,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 +3415,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 +3619,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 +3688,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 +3724,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 +3754,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,7 +3784,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/scheduling-base: dependencies: @@ -3824,7 +3824,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/sso-base: dependencies: @@ -3861,7 +3861,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,7 +3898,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/admin-api-schema: dependencies: @@ -3938,7 +3938,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,7 +3972,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/i18n: dependencies: @@ -4003,7 +4003,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: @@ -4036,7 +4036,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/parse-email-address: dependencies: @@ -4070,7 +4070,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 +4092,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 +4157,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 +5033,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 +5048,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 @@ -6556,8 +6556,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': @@ -9553,8 +9553,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==} @@ -11862,8 +11862,8 @@ 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} ci-info@2.0.0: @@ -15460,8 +15460,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 +16233,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 +16323,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: @@ -17243,44 +17243,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 +17692,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 +17787,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 @@ -18342,10 +18345,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'} @@ -19659,13 +19658,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==} @@ -21377,8 +21376,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 +21648,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 +21662,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 +22341,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 +22685,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 +23940,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 +23949,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 @@ -25806,10 +25813,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 +25828,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' @@ -29277,12 +29284,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 +29996,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 +30009,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 +30023,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 +30040,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 +30057,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 +30075,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,7 +30096,7 @@ 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@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)) 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) @@ -30172,7 +30179,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@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)) '@vitest/utils@3.2.4': dependencies: @@ -32787,7 +32794,7 @@ snapshots: chrome-trace-event@1.0.4: {} - chrono-node@2.9.1: {} + chrono-node@2.10.1: {} ci-info@2.0.0: {} @@ -37903,7 +37910,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 +38235,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 +39275,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 +39291,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 +39363,7 @@ snapshots: jsonpointer@5.0.1: {} - jsonrepair@3.14.0: {} + jsonrepair@3.15.0: {} jsonwebtoken@8.5.1: dependencies: @@ -40467,85 +40474,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 +41073,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 +41098,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 +41172,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 +41270,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 +41280,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: @@ -41949,8 +41959,6 @@ snapshots: p-map@7.0.6: {} - p-reflect@2.1.0: {} - p-timeout@3.2.0: dependencies: p-finally: 1.0.0 @@ -43360,16 +43368,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: @@ -45626,7 +45633,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 +45930,7 @@ snapshots: undici-types@7.18.2: {} - undici-types@7.28.0: {} + undici-types@8.10.0: {} undici-types@8.3.0: {} @@ -45931,6 +45938,8 @@ snapshots: undici@7.28.0: {} + undici@8.10.0: {} + unherit@1.1.3: dependencies: inherits: 2.0.4 @@ -46169,12 +46178,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 +46544,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 +46577,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 +46609,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 +46641,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 +46902,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..3c1e9157682 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 @@ -100,7 +100,7 @@ catalog: '@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 @@ -143,7 +143,7 @@ catalog: glob: 13.0.6 globals: 17.7.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 From 696657ac2c94f698c07d93ddfc89903721f43f7d Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 13:50:06 -0500 Subject: [PATCH 05/19] TypeScriptified data generator "luck" utility (#30028) towards https://linear.app/ghost/issue/NY-1522 --- ghost/core/core/server/data/seeders/utils/random.js | 13 ------------- ghost/core/core/server/data/seeders/utils/random.ts | 11 +++++++++++ 2 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 ghost/core/core/server/data/seeders/utils/random.js create mode 100644 ghost/core/core/server/data/seeders/utils/random.ts 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; From 45118e97f4dfb951711317adec55fd9dabb6779b Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Mon, 17 Aug 2026 13:58:01 -0500 Subject: [PATCH 06/19] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20redirecting=20outb?= =?UTF-8?q?ound=20requests=20crashing=20Ghost=20(#30027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://github.com/TryGhost/Ghost/issues/30001 Got 13 can terminate the Ghost process when a redirect clears the active request while a body write callback is still outstanding. The stale callback then dereferences `request.destroyed` after the request has gone away. --- .changeset/young-spiders-request.md | 5 + .../server/services/oembed/oembed-service.js | 10 +- ghost/core/package.json | 4 +- .../services/oembed/oembed-service.test.js | 38 +++ pnpm-lock.yaml | 282 +++++++----------- pnpm-workspace.yaml | 4 +- 6 files changed, 156 insertions(+), 187 deletions(-) create mode 100644 .changeset/young-spiders-request.md 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/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 3e19d783374..776857907b0 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -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", @@ -177,7 +177,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", 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/pnpm-lock.yaml b/pnpm-lock.yaml index 6f13343de7a..a9fe75a3d32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,6 +183,9 @@ catalogs: '@tryghost/nql-lang': specifier: 0.6.6 version: 0.6.6 + '@tryghost/request': + specifier: 4.0.0 + version: 4.0.0 '@tryghost/string': specifier: 0.3.5 version: 0.3.5 @@ -330,6 +333,9 @@ 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 @@ -492,7 +498,7 @@ catalogs: overrides: knex-migrator>knex: 2.4.2 '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0 + '@tryghost/logging': 5.3.3 jackspeak: 4.2.3 moment: 2.30.1 moment-timezone: 0.5.45 @@ -2126,8 +2132,8 @@ importers: specifier: 'catalog:' version: 2.3.5(supports-color@10.2.2) '@tryghost/logging': - specifier: 5.3.0 - version: 5.3.0(supports-color@10.2.2) + specifier: 5.3.3 + version: 5.3.3(supports-color@10.2.2) '@tryghost/test-data': specifier: workspace:* version: link:../packages/testing/test-data @@ -2288,8 +2294,8 @@ 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.3 + version: 5.3.3(supports-color@10.2.2) '@tryghost/metrics': specifier: 1.0.43 version: 1.0.43(supports-color@10.2.2) @@ -2333,8 +2339,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.0 '@tryghost/root-utils': specifier: 2.3.2 version: 2.3.2 @@ -2471,8 +2477,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) @@ -3789,8 +3795,8 @@ importers: packages/adapters/scheduling-base: dependencies: '@tryghost/logging': - specifier: 5.3.0 - version: 5.3.0(supports-color@10.2.2) + specifier: 5.3.3 + version: 5.3.3(supports-color@10.2.2) devDependencies: '@internal/cfg-eslint': specifier: 'workspace:' @@ -8430,13 +8436,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 +8788,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'} @@ -9112,8 +9110,8 @@ packages: resolution: {integrity: sha512-1Q1CzpDuWNRrejD4sbSZF4CAJ9RTuZWLuQH1BjcC1U6+5sTY3cbnRv4w6mmxGHbUxWJ0kwbP10UM+ODheGaL7A==} 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.13': + resolution: {integrity: sha512-AjmgbmMutgUC6j7qaZtHkDme3qA1yatdyBDRhHaKnHH17v8ogZ6RVTTSrxKumuecQ3yfWb4ZItYMmPe4q0/+VA==} '@tryghost/color-utils@0.2.20': resolution: {integrity: sha512-0KLCQDX7TbJGKNihs+OB1+3hEvhdP4YXOKpdbqJlUkvEFdY+Gl1FlI4WL8wnn0v6oc1aOjvrKmva5BUT0pWtIg==} @@ -9151,8 +9149,8 @@ packages: '@tryghost/elasticsearch@3.0.29': resolution: {integrity: sha512-WGfoGbPXzX7SYPKeD7naYyoF8Lc7KruQPixFSJGLciRQ3wkrZQANQIguD4nA71upp0dhsT4D4aWP296VfJxuPw==} - '@tryghost/elasticsearch@5.4.2': - resolution: {integrity: sha512-AcLyDeULrWHOiBnftSmI5Enj/QTZSCZ7LkEVAKmIByQ7Zy3kEqRaTTJMLF3dSxqw4cJTrr11ADs72fwxfN2gdw==} + '@tryghost/elasticsearch@5.4.4': + resolution: {integrity: sha512-mcekvvTQaa57Vhs5l7WkrLRyekmr+oPVdgxZNYLDj4E1QcRjcYr6tnB06YK7LsLfF8tzZi+/MRdcQeGgd91rqw==} '@tryghost/email-mock-receiver@2.1.0': resolution: {integrity: sha512-bgEVM5eRnN53rnqWJ8ZkjuGCzMwwt5Ch29TTXRp3qSGTJruUW44DtN/mjB36DDti0hq4unJV9+D1qIIKxdbong==} @@ -9178,8 +9176,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.8': + resolution: {integrity: sha512-KyZbeTDNMu2HTQmjpLq1rvYbt6+29IZMCE2vBp9jcFoU+mkysbBxZaAa8dRKLBOsbl9rUpoLe98hh56B1ProBA==} '@tryghost/image-transform@1.4.17': resolution: {integrity: sha512-+WipAbQ6gTOB9hbhmTDHW7tzaB1lPIRszoN+z1qV+vlI+cR/4qO70Ux+HkSYT9gt4WpxRZJZCBCk3+x4Nm0LhQ==} @@ -9193,8 +9191,8 @@ 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/logging@5.3.3': + resolution: {integrity: sha512-Cd6kgGNf61DzZ/Wl5ty6ItcKyP5kWtE2dTuJGjkLFUGxTJZsDVAmayrpnloe9svy9poP3WUybHW7L5Z1f0nQmg==} '@tryghost/metrics@1.0.43': resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} @@ -9241,8 +9239,8 @@ packages: '@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.7': + resolution: {integrity: sha512-M+gTgVB5m2hqYiRLiVgA5O7ACD1Mrr8cngQTNTAqova5C8GKivLKucwAiYxRDGNWPOoqpJNtQwjlz9lM4ndGag==} '@tryghost/prometheus-metrics@1.0.8': resolution: {integrity: sha512-E1LRRwLgiWIN/P20uHgpTK+JVYYQ3+BarKCBszB6qmK5IBANJvQHI3LQV0wDWefVwFyl5FI6AqxLpOOpquvahA==} @@ -9260,11 +9258,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.0': + resolution: {integrity: sha512-PwLUaQ7O7/cOGBOBS+8263aE77NnCHbPrqecLn9r0JpNHpBhASk/5jdHYil9DYLUISgVwkzkqUoTy+qM8htqjA==} '@tryghost/root-utils@0.3.38': resolution: {integrity: sha512-ARn8wC6qv867lCr7BZ+IS8S/88K5go0j6HgQFhP27rKje4b40PsxH/P3rO4Ez2NzF/Do8ywFrTWHoLCSsCazXQ==} @@ -9275,9 +9270,6 @@ 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==} @@ -9309,9 +9301,6 @@ 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==} @@ -9327,20 +9316,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.7': + resolution: {integrity: sha512-M8Z3oKWRFZJadea63b6ApaB0FqcEIKzQoz4cdXh0RV1Vb40ShY9kIHDKDhkpEVOT/aLjbFzs7A5sBgUPq/fEqw==} + '@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.7': + resolution: {integrity: sha512-POS9bmBWFETNBBXujWsO1TWCI34Te0SL3PcH8Gm0257Hin1BsA/YwfJO7vGS7krF7Rfs3Gk/17bdxAFgzJE5tQ==} '@tryghost/webhook-mock-receiver@2.1.0': resolution: {integrity: sha512-Cka5SW4igfgbGv33fFyEjkgt1HCA3lNhKCL88ThsDCw3/9ROxKQLcHMB+S7SNcewIDfsS5kI2INHtD+1Tbp/BA==} @@ -11638,10 +11624,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'} @@ -11866,6 +11848,10 @@ packages: 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==} @@ -14563,14 +14549,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 +14602,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 +14922,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==} @@ -16942,6 +16920,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==} @@ -18277,14 +18259,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'} @@ -20135,10 +20109,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'} @@ -27638,9 +27608,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': {} @@ -28151,10 +28119,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': @@ -28559,7 +28523,7 @@ snapshots: - supports-color - tedious - '@tryghost/bunyan-rotating-filestream@0.0.11': + '@tryghost/bunyan-rotating-filestream@0.0.13': dependencies: long-timeout: 0.1.1 @@ -28611,7 +28575,7 @@ snapshots: '@tryghost/domain-events@3.3.5(supports-color@10.2.2)': dependencies: - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/logging': 5.3.3(supports-color@10.2.2) transitivePeerDependencies: - '@75lb/nature' - supports-color @@ -28624,10 +28588,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/elasticsearch@5.4.2(supports-color@10.2.2)': + '@tryghost/elasticsearch@5.4.4(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.7(supports-color@10.2.2) split2: 4.2.0 transitivePeerDependencies: - '@75lb/nature' @@ -28679,10 +28643,10 @@ snapshots: '@tryghost/http-cache-utils@0.1.25': {} - '@tryghost/http-stream@2.3.5': + '@tryghost/http-stream@2.3.8': dependencies: '@tryghost/errors': 3.3.5 - '@tryghost/request': 3.3.5 + '@tryghost/request': 4.0.0 '@tryghost/image-transform@1.4.17(@types/node@22.20.1)': dependencies: @@ -28706,7 +28670,7 @@ snapshots: dependencies: '@breejs/later': 4.2.0 '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/logging': 5.3.3(supports-color@10.2.2) bree: 6.5.0(supports-color@10.2.2) cron-validate: 1.4.5 fastq: 1.20.1 @@ -28722,15 +28686,15 @@ snapshots: lodash: 4.18.1 luxon: 3.7.2 - '@tryghost/logging@5.3.0(supports-color@10.2.2)': + '@tryghost/logging@5.3.3(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.13 + '@tryghost/elasticsearch': 5.4.4(supports-color@10.2.2) + '@tryghost/http-stream': 2.3.8 + '@tryghost/pretty-stream': 2.3.7 + '@tryghost/root-utils': 2.3.7 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 @@ -28838,7 +28802,7 @@ snapshots: lodash: 4.18.1 prettyjson: 1.2.5 - '@tryghost/pretty-stream@2.3.5': + '@tryghost/pretty-stream@2.3.7': dependencies: date-format: 4.0.14 lodash: 4.18.1 @@ -28846,7 +28810,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.3(supports-color@10.2.2) express: 4.22.1(supports-color@10.2.2) prom-client: 15.1.3 stoppable: 1.1.0 @@ -28862,22 +28826,13 @@ snapshots: '@tryghost/referrer-parser@0.1.21': {} - '@tryghost/request@3.3.4': + '@tryghost/request@4.0.0': dependencies: '@tryghost/errors': 3.3.5 - '@tryghost/validator': 3.2.4 - '@tryghost/version': 2.3.4 + '@tryghost/validator': 3.2.7 + '@tryghost/version': 2.3.7 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 - cacheable-lookup: 7.0.0 - got: 14.6.6 + got: 15.1.0 lodash: 4.18.1 '@tryghost/root-utils@0.3.38': @@ -28895,11 +28850,6 @@ snapshots: caller: 1.1.0 find-root: 1.1.0 - '@tryghost/root-utils@2.3.4': - dependencies: - caller: 1.1.0 - find-root: 1.1.0 - '@tryghost/root-utils@2.3.5': dependencies: caller: 1.1.0 @@ -28918,7 +28868,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.3(supports-color@10.2.2) transitivePeerDependencies: - '@75lb/nature' - supports-color @@ -28941,8 +28891,6 @@ snapshots: '@tryghost/tpl@2.3.1': {} - '@tryghost/tpl@2.3.4': {} - '@tryghost/tpl@2.3.5': {} '@tryghost/tpl@2.3.7': {} @@ -28975,18 +28923,18 @@ snapshots: 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/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.7': dependencies: '@tryghost/errors': 3.3.5 - '@tryghost/tpl': 2.3.5 + '@tryghost/tpl': 2.3.7 lodash: 4.18.1 moment-timezone: 0.5.45 validator: 13.15.35 @@ -28996,14 +28944,9 @@ snapshots: '@tryghost/root-utils': 2.3.2 semver: 7.8.5 - '@tryghost/version@2.3.4': - dependencies: - '@tryghost/root-utils': 2.3.4 - semver: 7.8.5 - - '@tryghost/version@2.3.5': + '@tryghost/version@2.3.7': dependencies: - '@tryghost/root-utils': 2.3.5 + '@tryghost/root-utils': 2.3.7 semver: 7.8.5 '@tryghost/webhook-mock-receiver@2.1.0': @@ -32498,16 +32441,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 @@ -32796,6 +32729,8 @@ snapshots: chrono-node@2.10.1: {} + chunk-data@0.1.0: {} + ci-info@2.0.0: {} ci-info@3.9.0: {} @@ -37068,10 +37003,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 @@ -37140,6 +37071,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 @@ -37328,7 +37265,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: @@ -37567,34 +37505,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: {} @@ -37610,7 +37534,7 @@ snapshots: '@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/logging': 5.3.3(supports-color@10.2.2) '@tryghost/nql': 0.13.1(supports-color@10.2.2) '@tryghost/pretty-cli': 3.3.1 '@tryghost/server': 3.1.1(supports-color@10.2.2) @@ -39485,7 +39409,7 @@ snapshots: dependencies: '@tryghost/database-info': 0.3.35 '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.0(supports-color@10.2.2) + '@tryghost/logging': 5.3.3(supports-color@10.2.2) '@tryghost/promise': 0.3.20 commander: 5.1.0 compare-ver: 2.0.2 @@ -40035,6 +39959,8 @@ snapshots: lowercase-keys@3.0.0: {} + lowercase-keys@4.0.1: {} + lru-cache@10.4.3: {} lru-cache@11.5.1: {} @@ -41905,10 +41831,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: {} @@ -44002,10 +43924,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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3c1e9157682..9de100eb86c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -88,10 +88,11 @@ catalog: '@tryghost/errors': 3.3.5 '@tryghost/helpers': 1.1.106 '@tryghost/limit-service': 1.5.6 - '@tryghost/logging': 5.3.0 + '@tryghost/logging': 5.3.3 '@tryghost/mongo-knex': 0.10.1 '@tryghost/nql': 0.13.1 '@tryghost/nql-lang': 0.6.6 + '@tryghost/request': 4.0.0 '@tryghost/string': 0.3.5 '@tryghost/timezone-data': 1.0.0 '@tryghost/tpl': 2.3.5 @@ -142,6 +143,7 @@ catalog: fs-extra: 11.3.6 glob: 13.0.6 globals: 17.7.0 + got: 15.1.0 js-yaml: 4.3.0 jsdom: 30.0.1 jsonc-parser: 3.3.1 From 1637e97aae82398aac868437d385117adbb0d829 Mon Sep 17 00:00:00 2001 From: Jannis Fedoruk-Betschki Date: Mon, 17 Aug 2026 21:48:57 +0200 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20periodic=20host=20?= =?UTF-8?q?limits=20in=20Admin=20(#29837)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Admin's limit service loaded limits without the `subscription` argument that @tryghost/limit-service requires for periodic limits, so any configured maxPeriodic limit threw an IncorrectUsageError in the browser. Loading stops at the first limit that can't be built, so a site with an emails limit ended up with no limits registered at all, and the rejection surfaced in the editor's publish flow. - passes the subscription through, resolved from hostSettings the same way the server does - adds a client-side count query for emails, since the default one counts recipients via knex - skips the emails check for authors and contributors, who can't browse emails, matching how the members limit is already handled - tolerates a limit that can't be built instead of leaving Admin with none --- apps/ember-admin/app/services/limit.js | 44 ++++++++++- apps/ember-admin/app/utils/publish-options.js | 9 ++- .../acceptance/editor/publish-flow-test.js | 44 +++++++++++ .../tests/unit/services/limit-test.js | 79 +++++++++++++++++++ 4 files changed, 173 insertions(+), 3 deletions(-) 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..1d216f90b89 100644 --- a/apps/ember-admin/app/utils/publish-options.js +++ b/apps/ember-admin/app/utils/publish-options.js @@ -402,9 +402,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.'; } 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); + }); + }); }); From c0254ce6f2893ecfa2edfb84cda90969b81f3b2d Mon Sep 17 00:00:00 2001 From: Jannis Fedoruk-Betschki Date: Mon, 17 Aug 2026 21:49:14 +0200 Subject: [PATCH 08/19] =?UTF-8?q?=F0=9F=8E=A8=20Changed=20the=20upgrade=20?= =?UTF-8?q?destination=20on=20host=20limits=20to=20be=20configurable=20(#2?= =?UTF-8?q?9843)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no ref Every limit dialogue sent people to Ghost(Pro)'s billing app at a hardcoded '/pro', so a managed host running its own billing had no way to redirect them. Adds hostSettings.billing.upgradeUrl, read through a shared helper alongside the existing upgradeUrl settings for the pre-launch and trial banners. Each surface keeps its own fallback so Ghost(Pro) lands where it always has: the publish-limit modal still opens checkout, and the inline publish messages still point at the billing root. hostSettings holds an href, so the React hook drops a leading '#' before handing it to updateRoute(), which reads that as an internal route. --- apps/admin-x-framework/src/api/config.ts | 12 +++++++ .../overview/components/overview-kpis.tsx | 5 ++- .../advanced/integrations.acceptance.test.tsx | 19 ++++++++-- .../settings/advanced/integrations.tsx | 8 ++++- .../integrations/add-integration-modal.tsx | 6 ++-- .../newsletters/add-newsletter-modal.tsx | 6 ++-- .../newsletters/newsletter-detail-modal.tsx | 4 ++- .../settings/general/user-detail-modal.tsx | 4 ++- .../settings/membership/analytics.tsx | 4 ++- .../stripe/stripe-connect-modal.tsx | 6 ++-- .../components/settings/membership/tiers.tsx | 4 ++- .../components/settings/site/change-theme.tsx | 6 ++-- .../settings/site/design-and-theme-modal.tsx | 6 ++-- .../components/settings/site/theme-modal.tsx | 7 ++-- .../site/theme/advanced-theme-settings.tsx | 4 ++- .../app/hooks/use-upgrade-route.test.tsx | 33 +++++++++++++++++ .../settings/app/hooks/use-upgrade-route.ts | 8 +++++ .../editor/modals/publish-flow/confirm.js | 13 ++++++- .../modals/limits/publish-limit.hbs | 4 +-- .../components/modals/limits/publish-limit.js | 8 +++++ apps/ember-admin/app/utils/publish-options.js | 6 +++- apps/ember-admin/app/utils/upgrade-url.js | 23 ++++++++++++ .../tests/unit/utils/upgrade-url-test.js | 36 +++++++++++++++++++ 23 files changed, 206 insertions(+), 26 deletions(-) create mode 100644 apps/admin/src/settings/app/hooks/use-upgrade-route.test.tsx create mode 100644 apps/admin/src/settings/app/hooks/use-upgrade-route.ts create mode 100644 apps/ember-admin/app/utils/upgrade-url.js create mode 100644 apps/ember-admin/tests/unit/utils/upgrade-url-test.js 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/utils/publish-options.js b/apps/ember-admin/app/utils/publish-options.js index 1d216f90b89..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; @@ -429,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/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'); + }); +}); From a4223936e105d0e675d51d8911178def55d875cd Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 15:01:52 -0500 Subject: [PATCH 09/19] TypeScriptify data generator table importer base class (#30029) towards https://linear.app/ghost/issue/NY-1522 --- .../seeders/importers/benefits-importer.js | 2 +- .../importers/comment-reports-importer.js | 2 +- .../seeders/importers/comments-importer.js | 2 +- .../importers/email-batches-importer.js | 2 +- .../email-recipient-failures-importer.js | 2 +- .../importers/email-recipients-importer.js | 2 +- .../data/seeders/importers/emails-importer.js | 2 +- .../data/seeders/importers/labels-importer.js | 2 +- .../members-click-events-importer.js | 2 +- .../members-created-events-importer.js | 2 +- .../importers/members-feedback-importer.js | 2 +- .../seeders/importers/members-importer.js | 2 +- .../importers/members-labels-importer.js | 2 +- .../members-login-events-importer.js | 2 +- .../importers/members-newsletters-importer.js | 2 +- ...mbers-paid-subscription-events-importer.js | 2 +- .../importers/members-products-importer.js | 2 +- .../members-status-events-importer.js | 2 +- .../members-stripe-customers-importer.js | 2 +- ...stripe-customers-subscriptions-importer.js | 2 +- .../members-subscribe-events-importer.js | 2 +- ...rs-subscription-created-events-importer.js | 2 +- .../seeders/importers/newsletters-importer.js | 2 +- .../importers/offer-redemptions-importer.js | 2 +- .../data/seeders/importers/offers-importer.js | 2 +- .../importers/posts-authors-importer.js | 2 +- .../data/seeders/importers/posts-importer.js | 2 +- .../importers/posts-products-importer.js | 2 +- .../seeders/importers/posts-tags-importer.js | 2 +- .../importers/products-benefits-importer.js | 2 +- .../seeders/importers/products-importer.js | 2 +- .../recommendation-click-events-importer.js | 2 +- ...ecommendation-subscribe-events-importer.js | 2 +- .../importers/recommendations-importer.js | 2 +- .../seeders/importers/redirects-importer.js | 2 +- .../seeders/importers/roles-users-importer.js | 2 +- .../importers/stripe-prices-importer.js | 2 +- .../importers/stripe-products-importer.js | 2 +- .../{table-importer.js => table-importer.ts} | 120 +++++++++--------- .../data/seeders/importers/tags-importer.js | 2 +- .../data/seeders/importers/users-importer.js | 2 +- .../importers/web-mentions-importer.js | 2 +- .../data/seeders/utils/json-importer.js | 2 +- ghost/core/package.json | 1 - pnpm-lock.yaml | 8 -- 45 files changed, 99 insertions(+), 114 deletions(-) rename ghost/core/core/server/data/seeders/importers/{table-importer.js => table-importer.ts} (61%) 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 f05a040e280..8b47708a426 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'); 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/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 61% 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..0b0772d48be 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,25 +64,21 @@ 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); @@ -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/package.json b/ghost/core/package.json index 776857907b0..a54e12f4a51 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9fe75a3d32..481c39067ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2422,9 +2422,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 @@ -12573,9 +12570,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==} @@ -33532,8 +33526,6 @@ snapshots: csstype@3.2.3: {} - csv-writer@1.6.0: {} - custom-error-instance@2.1.1: {} cyclist@1.0.2: {} From 155709f8200527e9e712a5a6c6888e5ff470f3f0 Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Mon, 17 Aug 2026 16:08:17 -0400 Subject: [PATCH 10/19] Updated nql/nql-lang deps to latest (#30032) no ref - bump mongo-knex/nql/nql-lang to latest - override nql/nql-lang to catalog versions to deduplicate --- pnpm-lock.yaml | 100 +++++++++++++++----------------------------- pnpm-workspace.yaml | 8 ++-- 2 files changed, 38 insertions(+), 70 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 481c39067ce..415bb346714 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,14 +175,8 @@ catalogs: specifier: 1.5.6 version: 1.5.6 '@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.0 version: 4.0.0 @@ -499,6 +493,8 @@ overrides: knex-migrator>knex: 2.4.2 '@tryghost/errors': 3.3.5 '@tryghost/logging': 5.3.3 + '@tryghost/nql': 0.13.3 + '@tryghost/nql-lang': 0.7.0 jackspeak: 4.2.3 moment: 2.30.1 moment-timezone: 0.5.45 @@ -773,11 +769,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 @@ -1174,8 +1170,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 @@ -1322,8 +1318,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 @@ -2301,7 +2297,7 @@ importers: version: 1.0.43(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 @@ -2315,11 +2311,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 @@ -4020,8 +4016,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 @@ -9194,11 +9190,8 @@ packages: '@tryghost/metrics@1.0.43': resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} - '@tryghost/mongo-knex@0.10.1': - resolution: {integrity: sha512-6LRA4zVpNvCrVrA9hOhs8N4iylAiafGssiKuD8yML/13xg2PvKw6dzOZj6hg0CKQwG7tVp8TA+7Nw3iDOzy/jQ==} - - '@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==} @@ -9215,17 +9208,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==} @@ -28455,7 +28442,7 @@ snapshots: 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/nql': 0.13.3(supports-color@10.2.2) '@tryghost/tpl': 2.3.7 transitivePeerDependencies: - supports-color @@ -28705,14 +28692,7 @@ snapshots: transitivePeerDependencies: - 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 @@ -28752,29 +28732,15 @@ snapshots: - debug - supports-color - '@tryghost/nql-lang@0.6.6': + '@tryghost/nql-lang@0.7.0': 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)': - 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: @@ -37527,7 +37493,7 @@ snapshots: '@tryghost/debug': 2.3.1(supports-color@10.2.2) '@tryghost/errors': 3.3.5 '@tryghost/logging': 5.3.3(supports-color@10.2.2) - '@tryghost/nql': 0.13.1(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) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9de100eb86c..262d170ba91 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -89,9 +89,9 @@ catalog: '@tryghost/helpers': 1.1.106 '@tryghost/limit-service': 1.5.6 '@tryghost/logging': 5.3.3 - '@tryghost/mongo-knex': 0.10.1 - '@tryghost/nql': 0.13.1 - '@tryghost/nql-lang': 0.6.6 + '@tryghost/mongo-knex': 0.11.1 + '@tryghost/nql': 0.13.3 + '@tryghost/nql-lang': 0.7.0 '@tryghost/request': 4.0.0 '@tryghost/string': 0.3.5 '@tryghost/timezone-data': 1.0.0 @@ -231,6 +231,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:' From ac87f8e966a5e021e15a0335e0afe1d28a4d5a53 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 15:13:34 -0500 Subject: [PATCH 11/19] Fixed stack overflow error in email batches data seeder (#30022) towards https://linear.app/ghost/issue/NY-1522 This change should have no user impact, but makes it possible to create thousands of email batches. Co-Authored-By: Chris Raible --- .../server/data/seeders/importers/email-batches-importer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 8b47708a426..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 @@ -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() { From 3db4c02d1868f8b6d533c05c01c418ae182f2f81 Mon Sep 17 00:00:00 2001 From: Austin Burdine Date: Mon, 17 Aug 2026 16:30:54 -0400 Subject: [PATCH 12/19] Updated @tryghost/metrics to latest, dedupe framework versions (#30031) no ref - update @tryghost/metrics to latest - update framework packages to deduplicate versions --- .changeset/open-shirts-arrive.md | 6 + .../email-analytics-service-wrapper.ts | 7 +- .../server/services/email-analytics/index.ts | 5 +- ghost/core/package.json | 2 +- pnpm-lock.yaml | 302 ++++++++---------- pnpm-workspace.yaml | 15 +- 6 files changed, 159 insertions(+), 178 deletions(-) create mode 100644 .changeset/open-shirts-arrive.md 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/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/package.json b/ghost/core/package.json index a54e12f4a51..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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 415bb346714..e84611d14fb 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,23 +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.11.1 version: 0.11.1 '@tryghost/request': - specifier: 4.0.0 - version: 4.0.0 + specifier: 4.0.1 + version: 4.0.1 '@tryghost/string': specifier: 0.3.5 version: 0.3.5 @@ -187,8 +190,8 @@ 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 @@ -491,8 +494,8 @@ catalogs: overrides: knex-migrator>knex: 2.4.2 - '@tryghost/errors': 3.3.5 - '@tryghost/logging': 5.3.3 + '@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 @@ -1140,7 +1143,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 @@ -1631,7 +1634,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 @@ -1915,7 +1918,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) react: specifier: 'catalog:' version: 18.3.1 @@ -1985,7 +1988,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 @@ -2126,10 +2129,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.3 - version: 5.3.3(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 @@ -2225,7 +2228,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 @@ -2243,16 +2246,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 @@ -2290,11 +2293,11 @@ importers: specifier: 'catalog:' version: 1.5.6 '@tryghost/logging': - specifier: 5.3.3 - version: 5.3.3(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.11.1(supports-color@10.2.2) @@ -2336,7 +2339,7 @@ importers: version: 0.1.21 '@tryghost/request': specifier: 'catalog:' - version: 4.0.0 + version: 4.0.1 '@tryghost/root-utils': specifier: 2.3.2 version: 2.3.2 @@ -2351,7 +2354,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 @@ -3788,8 +3791,8 @@ importers: packages/adapters/scheduling-base: dependencies: '@tryghost/logging': - specifier: 5.3.3 - version: 5.3.3(supports-color@10.2.2) + specifier: 5.3.4 + version: 5.3.4(supports-color@10.2.2) devDependencies: '@internal/cfg-eslint': specifier: 'workspace:' @@ -3828,8 +3831,8 @@ importers: 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:* @@ -3902,8 +3905,8 @@ importers: 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 @@ -3977,7 +3980,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) i18next: specifier: 23.16.8 version: 23.16.8 @@ -5230,10 +5233,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'} @@ -5242,10 +5241,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.*} @@ -9099,12 +9094,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.13': - resolution: {integrity: sha512-AjmgbmMutgUC6j7qaZtHkDme3qA1yatdyBDRhHaKnHH17v8ogZ6RVTTSrxKumuecQ3yfWb4ZItYMmPe4q0/+VA==} + '@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==} @@ -9136,14 +9131,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.4': - resolution: {integrity: sha512-mcekvvTQaa57Vhs5l7WkrLRyekmr+oPVdgxZNYLDj4E1QcRjcYr6tnB06YK7LsLfF8tzZi+/MRdcQeGgd91rqw==} + '@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==} @@ -9152,8 +9147,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==} @@ -9169,8 +9164,8 @@ packages: '@tryghost/http-cache-utils@0.1.25': resolution: {integrity: sha512-OJAD1ESvV+F81w6IOTKCX0JLGIiJqNn87HVFMskpUC3IUGpqcDP1pCM79d72khxLWy5oRQDYu8xIqicHJ9ST1Q==} - '@tryghost/http-stream@2.3.8': - resolution: {integrity: sha512-KyZbeTDNMu2HTQmjpLq1rvYbt6+29IZMCE2vBp9jcFoU+mkysbBxZaAa8dRKLBOsbl9rUpoLe98hh56B1ProBA==} + '@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==} @@ -9184,11 +9179,11 @@ packages: '@tryghost/limit-service@1.5.6': resolution: {integrity: sha512-wR+Hm2v5k2FjOUhLhfPZ0p0acCfaTQoSEhuw96uw8c15CLZtm/HKsLnxA5d3AXwYmjRo5gkYskP8UYxrCKNcoQ==} - '@tryghost/logging@5.3.3': - resolution: {integrity: sha512-Cd6kgGNf61DzZ/Wl5ty6ItcKyP5kWtE2dTuJGjkLFUGxTJZsDVAmayrpnloe9svy9poP3WUybHW7L5Z1f0nQmg==} + '@tryghost/logging@5.3.4': + resolution: {integrity: sha512-7ZRknXmKLFK+WW2xwQ1UJ392bh/yDaqyMCJCPvCI//UwVUkhsRfOAVYCYaMn5VWYDLYEflk1fOL8aYhatmeVEQ==} - '@tryghost/metrics@1.0.43': - resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} + '@tryghost/metrics@3.4.2': + resolution: {integrity: sha512-20ldmzt6yHK7835yuU/PJDfZXaHlLl9Y3QfKM2tHUXJH0t6VdTEx1CSEnMjdSgxKG2z5+5uK84FoFD67toUVrQ==} '@tryghost/mongo-knex@0.11.1': resolution: {integrity: sha512-W5m2aU4WW14fMQoyj5oY6vvHTGzMfz1WEVGK8YTY0zWszJ/kUPo5qHXY9ivoxQthmFoBUpSbNuP0qRAfE6UrNQ==} @@ -9220,11 +9215,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.7': - resolution: {integrity: sha512-M+gTgVB5m2hqYiRLiVgA5O7ACD1Mrr8cngQTNTAqova5C8GKivLKucwAiYxRDGNWPOoqpJNtQwjlz9lM4ndGag==} + '@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==} @@ -9242,8 +9234,8 @@ packages: resolution: {integrity: sha512-Uz7bj8IadLah1j9GgieGQ0VGytdOG6mIlYdbpNu1r8/o9pjR2IS4bt88tvwpGSPNb47P9NrGntGn+Le1W/90jg==} engines: {node: '>=16.0.0'} - '@tryghost/request@4.0.0': - resolution: {integrity: sha512-PwLUaQ7O7/cOGBOBS+8263aE77NnCHbPrqecLn9r0JpNHpBhASk/5jdHYil9DYLUISgVwkzkqUoTy+qM8htqjA==} + '@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==} @@ -9260,6 +9252,9 @@ packages: '@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==} @@ -9291,6 +9286,9 @@ packages: '@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==} @@ -9303,14 +9301,14 @@ packages: '@tryghost/validator@3.2.5': resolution: {integrity: sha512-uIpY/QFGjsVgTjT2Hzps4iTxyS3Shjh5g3MpB1QCG6yXm7KWinTfuh8ps2ZNgxKqDjCPEQH6gTWpREugTlLHaw==} - '@tryghost/validator@3.2.7': - resolution: {integrity: sha512-M8Z3oKWRFZJadea63b6ApaB0FqcEIKzQoz4cdXh0RV1Vb40ShY9kIHDKDhkpEVOT/aLjbFzs7A5sBgUPq/fEqw==} + '@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.7': - resolution: {integrity: sha512-POS9bmBWFETNBBXujWsO1TWCI34Te0SL3PcH8Gm0257Hin1BsA/YwfJO7vGS7krF7Rfs3Gk/17bdxAFgzJE5tQ==} + '@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==} @@ -20318,9 +20316,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==} @@ -24056,13 +24051,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) @@ -24085,17 +24073,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) @@ -28415,7 +28392,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 @@ -28425,7 +28402,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 @@ -28441,7 +28418,7 @@ 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/errors': 3.3.8 '@tryghost/nql': 0.13.3(supports-color@10.2.2) '@tryghost/tpl': 2.3.7 transitivePeerDependencies: @@ -28467,7 +28444,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 @@ -28490,7 +28467,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) @@ -28504,7 +28481,7 @@ snapshots: - supports-color - tedious - '@tryghost/bunyan-rotating-filestream@0.0.13': + '@tryghost/bunyan-rotating-filestream@0.0.14': dependencies: long-timeout: 0.1.1 @@ -28554,25 +28531,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.3(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.4(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.7(supports-color@10.2.2) + '@tryghost/debug': 2.3.8(supports-color@10.2.2) split2: 4.2.0 transitivePeerDependencies: - '@75lb/nature' @@ -28601,7 +28577,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: @@ -28624,14 +28600,14 @@ snapshots: '@tryghost/http-cache-utils@0.1.25': {} - '@tryghost/http-stream@2.3.8': + '@tryghost/http-stream@2.3.9': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/request': 4.0.0 + '@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) @@ -28642,7 +28618,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 @@ -28650,8 +28626,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.3(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 @@ -28663,17 +28639,17 @@ 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.3(supports-color@10.2.2)': + '@tryghost/logging@5.3.4(supports-color@10.2.2)': dependencies: - '@tryghost/bunyan-rotating-filestream': 0.0.13 - '@tryghost/elasticsearch': 5.4.4(supports-color@10.2.2) - '@tryghost/http-stream': 2.3.8 - '@tryghost/pretty-stream': 2.3.7 - '@tryghost/root-utils': 2.3.7 + '@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.4.0 gelf-stream: 1.1.1 @@ -28683,13 +28659,14 @@ 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.11.1(supports-color@10.2.2)': @@ -28710,7 +28687,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 @@ -28723,7 +28700,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) @@ -28756,13 +28733,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.7': + '@tryghost/pretty-stream@2.3.8': dependencies: date-format: 4.0.14 lodash: 4.18.1 @@ -28770,7 +28741,7 @@ snapshots: '@tryghost/prometheus-metrics@1.0.8(supports-color@10.2.2)': dependencies: - '@tryghost/logging': 5.3.3(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 @@ -28786,11 +28757,11 @@ snapshots: '@tryghost/referrer-parser@0.1.21': {} - '@tryghost/request@4.0.0': + '@tryghost/request@4.0.1': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/validator': 3.2.7 - '@tryghost/version': 2.3.7 + '@tryghost/errors': 3.3.8 + '@tryghost/validator': 3.2.8 + '@tryghost/version': 2.3.8 cacheable-lookup: 7.0.0 got: 15.1.0 lodash: 4.18.1 @@ -28820,6 +28791,11 @@ snapshots: caller: 1.1.0 find-root: 1.1.0 + '@tryghost/root-utils@2.3.8': + dependencies: + caller: 1.1.0 + find-root: 1.1.0 + '@tryghost/security@1.0.6': dependencies: '@tryghost/string': 0.2.21 @@ -28828,7 +28804,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.3(supports-color@10.2.2) + '@tryghost/logging': 5.3.4(supports-color@10.2.2) transitivePeerDependencies: - '@75lb/nature' - supports-color @@ -28855,6 +28831,8 @@ snapshots: '@tryghost/tpl@2.3.7': {} + '@tryghost/tpl@2.3.8': {} + '@tryghost/url-utils@5.2.6': dependencies: cheerio: 1.2.0 @@ -28877,7 +28855,7 @@ 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 @@ -28885,16 +28863,16 @@ snapshots: '@tryghost/validator@3.2.5': dependencies: - '@tryghost/errors': 3.3.5 + '@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.7': + '@tryghost/validator@3.2.8': dependencies: - '@tryghost/errors': 3.3.5 - '@tryghost/tpl': 2.3.7 + '@tryghost/errors': 3.3.8 + '@tryghost/tpl': 2.3.8 lodash: 4.18.1 moment-timezone: 0.5.45 validator: 13.15.35 @@ -28904,9 +28882,9 @@ snapshots: '@tryghost/root-utils': 2.3.2 semver: 7.8.5 - '@tryghost/version@2.3.7': + '@tryghost/version@2.3.8': dependencies: - '@tryghost/root-utils': 2.3.7 + '@tryghost/root-utils': 2.3.8 semver: 7.8.5 '@tryghost/webhook-mock-receiver@2.1.0': @@ -28915,7 +28893,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: @@ -28926,7 +28904,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: @@ -31600,7 +31578,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 @@ -33165,7 +33143,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: {} @@ -33970,7 +33948,7 @@ snapshots: dtrace-provider@0.8.8: dependencies: - nan: 2.27.0 + nan: 2.28.0 optional: true dunder-proto@1.0.1: @@ -37131,7 +37109,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: @@ -37491,8 +37469,8 @@ 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.3(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) @@ -39366,8 +39344,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.3(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 @@ -44189,8 +44167,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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 262d170ba91..cd7b12283da 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -80,22 +80,23 @@ 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.3 + '@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.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 From dc96d09b6fd72c4bdc899fb48c55401470d7887b Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 15:37:53 -0500 Subject: [PATCH 13/19] Fixed stack overflow error in table importer (#30021) towards https://linear.app/ghost/issue/NY-1522 This change should have no user impact, and fixes an error with the data importer. Co-Authored-By: Chris Raible --- .../core/core/server/data/seeders/importers/table-importer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/core/core/server/data/seeders/importers/table-importer.ts b/ghost/core/core/server/data/seeders/importers/table-importer.ts index 0b0772d48be..0b3fbe7b523 100644 --- a/ghost/core/core/server/data/seeders/importers/table-importer.ts +++ b/ghost/core/core/server/data/seeders/importers/table-importer.ts @@ -82,8 +82,8 @@ export abstract class TableImporter< } const generatedData = await this.#generateData(currentAmount); - if (generatedData.length > 0) { - data.push(...generatedData); + for (const generatedModel of generatedData) { + data.push(generatedModel); } } From fbd2b66d906588917610ac43d142e8b115aaafcd Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 16:06:09 -0500 Subject: [PATCH 14/19] Tightened object lookup in data generator sorter (#30034) no ref This helps fix a hypothetical bug in the topological sorter. If you supply a dependency that doesn't exist, this will now error instead of silently passing. Noticed this while looking at `topologicalSort`. `reset:data` still works after this change. --- .../core/core/server/data/seeders/utils/topological-sort.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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); From b658b767f08ba81e0c6f540ed70e45268880185b Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 16:06:51 -0500 Subject: [PATCH 15/19] Update dependency parse-email-address to v0.0.4 (#30036) ref https://github.com/TryGhost/Ghost/pull/29979 --- packages/parse-email-address/package.json | 8 +++++--- pnpm-lock.yaml | 21 ++++++++++++--------- pnpm-workspace.yaml | 1 + 3 files changed, 18 insertions(+), 12 deletions(-) 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 e84611d14fb..aa246113d70 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -375,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 @@ -4043,8 +4046,8 @@ importers: 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:* @@ -18348,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==} @@ -29977,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@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)) + 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: @@ -30060,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@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)) + 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: @@ -41865,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: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cd7b12283da..08164178746 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -212,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: From 4f028d8e21ab98764f5e9cf3837781f0618a7fef Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 16:10:53 -0500 Subject: [PATCH 16/19] Added index to prep for automation run analytics query (#29950) towards https://linear.app/ghost/issue/NY-1523 In [an upcoming change][0], we'll be trying to find the newest automation run per automation. This adds an index to make that query faster. **I was lazy and did not test the performance of this migration.** [0]: https://linear.app/ghost/issue/NY-1523 --- ...ion-runs-automation-id-created-at-index.js | 28 +++++++++++++++++++ ghost/core/core/server/data/schema/schema.js | 5 +++- .../unit/server/data/schema/integrity.test.js | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 ghost/core/core/server/data/migrations/versions/6.58/2026-08-13-16-07-33-add-automation-runs-automation-id-created-at-index.js 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/schema.js b/ghost/core/core/server/data/schema/schema.js index 3e290e9da6f..ae9b0db9690 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -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/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index 1f96805d3ce..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 = '205297f193a7d60b95e8ceeab3b80f4f'; + const currentSchemaHash = 'bbded7a3841350d2d8c5856bc01c3461'; const currentFixturesHash = 'd4c9e4fabcec3365c42d37642216021b'; const currentSettingsHash = 'f57245b22af55ea5fc1e5e20913de9bb'; const currentRoutesHash = 'd8c25fa01bf6d22a2bcb05ba0de70dc1'; From accdee750405cb0d163e142d9208cffe5d314034 Mon Sep 17 00:00:00 2001 From: "tryghost-renovate[bot]" <269725441+tryghost-renovate[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:14:54 -0400 Subject: [PATCH 17/19] Update Docker image digests (#29990) Co-authored-by: tryghost-renovate[bot] <269725441+tryghost-renovate[bot]@users.noreply.github.com> --- .github/workflows/tinybird.yml | 2 +- compose.dev.analytics.yaml | 2 +- compose.dev.yaml | 2 +- docker/dev-gateway/Dockerfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/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 From 022293fac8c4e8cf889a7e1b4b4703650a74fb28 Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 16:20:09 -0500 Subject: [PATCH 18/19] Added automations records to data generator (#30033) closes https://linear.app/ghost/issue/NY-1522 This change should have no user impact. It adds various automations records, like `automations` and `automation_actions`, to our data generator. The code was primarily vibe-coded and isn't great, but it worked in our testing. Co-Authored-By: Chris Raible --- .../server/data/seeders/data-generator.js | 4 +- .../automation-action-edges-importer.ts | 52 +++++ .../automation-action-revisions-importer.ts | 132 +++++++++++ .../importers/automation-actions-importer.ts | 70 ++++++ .../automation-run-steps-importer.ts | 148 +++++++++++++ .../importers/automation-runs-importer.ts | 95 ++++++++ .../seeders/importers/automations-importer.ts | 60 +++++ .../server/data/seeders/importers/index.js | 6 + .../data/seeders/data-generator.test.js | 209 ++++++++++++++++++ 9 files changed, 775 insertions(+), 1 deletion(-) create mode 100644 ghost/core/core/server/data/seeders/importers/automation-action-edges-importer.ts create mode 100644 ghost/core/core/server/data/seeders/importers/automation-action-revisions-importer.ts create mode 100644 ghost/core/core/server/data/seeders/importers/automation-actions-importer.ts create mode 100644 ghost/core/core/server/data/seeders/importers/automation-run-steps-importer.ts create mode 100644 ghost/core/core/server/data/seeders/importers/automation-runs-importer.ts create mode 100644 ghost/core/core/server/data/seeders/importers/automations-importer.ts 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/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/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); From ce0841435c44a24bd444277cc587e3a18adc052d Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Mon, 17 Aug 2026 16:39:47 -0500 Subject: [PATCH 19/19] Added "last run created at" stat to automation browse endpoint (#29951) closes https://linear.app/ghost/issue/NY-1523 *I recommend reviewing this one commit at a time.* This adds `stats.last_run_created_at` to each automation, which we'll display in the UI soon. In addition to unit and E2E tests, I also verified that the data appeared by checking the Network tab of the Firefox devtools: ![devtools screenshot](https://github.com/user-attachments/assets/61db3645-ef0d-48f5-be8c-5336af2021dc) --- .../automations/automations-repository.ts | 8 +++- .../database-automations-repository.ts | 37 +++++++++++++--- .../__snapshots__/automations.test.js.snap | 8 +++- .../test/e2e-api/admin/automations.test.js | 43 ++++++++++++++++++- .../automations-repository.test.ts | 28 ++++++++++-- 5 files changed, 111 insertions(+), 13 deletions(-) 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/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/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/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'])