32127e54 Dropped apache-arrow from the Elasticsearch client
no ref
Ghost pulls apache-arrow in through @tryghost/logging and @tryghost/metrics,
which both depend on @tryghost/elasticsearch and so on the 8.x Elasticsearch
client. That client requires apache-arrow at module scope, but the only things
that touch it are the ES|QL helpers `toArrowTable` and `toArrowReader` — neither
of which Ghost calls, and neither of which @tryghost/elasticsearch can reach: it
only ever indexes documents.
The 9.x client already fixed this, deferring the require behind a `loadArrow()`
try/catch. We can't take 9.x, because it needs Elasticsearch 9 and our instance
isn't there yet, so the patch backports that: the module-scope require becomes a
lazy `loadArrow()` throwing 9.x's own "Package apache-arrow is required for Arrow
functionality" message, and the two helpers call it on entry. The `.d.ts` import
becomes `import type`, as 9.x has it.
Removing the dependency is the readPackage hook in .pnpmfile.mjs, not the patch —
pnpm resolves from the registry manifest and applies patches afterwards, so a
manifest edit inside the patch changes nothing about what gets installed. That's
the same tool this repo already uses for consolidate's peers, knex's sqlite3 and
the typescript peers on viem/ox/abitype. It deletes the dependency rather than
re-declaring it as the optional peer 9.x has, because autoInstallPeers is on by
default and installs optional peers anyway — and doing so peer-forks
@tryghost/logging and everything above it (elasticsearch, metrics, job-manager,
prometheus-metrics, domain-events, knex-migrator) into `(apache-arrow@21.1.0)`
variants for no benefit.
So the installed package.json still names apache-arrow as a dependency that
isn't there. The try/catch is what makes that safe.
That takes 15 packages and ~15MB out of the install — apache-arrow itself plus
flatbuffers, json-bignum, the command-line-args/usage tree and @types/node 24 —
and the phantom `@75lb/nature` peer disappears from eight packages' transitive
peer lists with them. A process that configures the Elasticsearch log transport
loads 145 fewer modules and measures ~3.3MB less RSS, averaged over three runs
each way with a forced GC.
Both packaging paths were already generic over `patchedDependencies` after the
juice patch landed, so the new patch rides along: pack.mjs copies and validates
every patch the trimmed workspace names, and docker/ghost-dev copies the whole
patches dir. The release archive gets no .pnpmfile.mjs, but its lockfile is
seeded from this one and pnpm reuses the seeded snapshot rather than
re-resolving, so arrow stays out there too — verified by reproducing that step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013AjXHCDS7EaJmRKmtggwfU 3e2a398a Dropped apache-arrow from the Elasticsearch client
no ref
Ghost pulls apache-arrow in through @tryghost/logging and @tryghost/metrics,
which both depend on @tryghost/elasticsearch and so on the 8.x Elasticsearch
client. That client requires apache-arrow at module scope, but the only things
that touch it are the ES|QL helpers `toArrowTable` and `toArrowReader` — neither
of which Ghost calls, and neither of which @tryghost/elasticsearch can reach: it
only ever indexes documents.
The 9.x client already fixed this, deferring the require behind a `loadArrow()`
try/catch. We can't take 9.x, because it needs Elasticsearch 9 and our instance
isn't there yet, so the patch backports that: the module-scope require becomes a
lazy `loadArrow()` throwing 9.x's own "Package apache-arrow is required for Arrow
functionality" message, and the two helpers call it on entry. The `.d.ts` import
becomes `import type`, as 9.x has it.
Removing the dependency is the readPackage hook in .pnpmfile.mjs, not the patch —
pnpm resolves from the registry manifest and applies patches afterwards, so a
manifest edit inside the patch changes nothing about what gets installed. That's
the same tool this repo already uses for consolidate's peers, knex's sqlite3 and
the typescript peers on viem/ox/abitype. It deletes the dependency rather than
re-declaring it as the optional peer 9.x has, because autoInstallPeers is on by
default and installs optional peers anyway — and doing so peer-forks
@tryghost/logging and everything above it (elasticsearch, metrics, job-manager,
prometheus-metrics, domain-events, knex-migrator) into `(apache-arrow@21.1.0)`
variants for no benefit.
So the installed package.json still names apache-arrow as a dependency that
isn't there. The try/catch is what makes that safe.
That takes 15 packages and ~15MB out of the install — apache-arrow itself plus
flatbuffers, json-bignum, the command-line-args/usage tree and @types/node 24 —
and the phantom `@75lb/nature` peer disappears from eight packages' transitive
peer lists with them. A process that configures the Elasticsearch log transport
loads 145 fewer modules and measures ~3.3MB less RSS, averaged over three runs
each way with a forced GC.
Both packaging paths were already generic over `patchedDependencies` after the
juice patch landed, so the new patch rides along: pack.mjs copies and validates
every patch the trimmed workspace names, and docker/ghost-dev copies the whole
patches dir. The release archive gets no .pnpmfile.mjs, but its lockfile is
seeded from this one and pnpm reuses the seeded snapshot rather than
re-resolving, so arrow stays out there too — verified by reproducing that step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013AjXHCDS7EaJmRKmtggwfU cf659fa7 Updated email snapshots for juice 11 dropping dead CSS
no ref
juice 9 left every rule it had already inlined, and every rule that matched
nothing at all, in the residual style block. juice 11 emits only what it cannot
inline. The email snapshots recorded the old behaviour, so six of them failed on
rules that never applied to anything.
Nine selectors go from the style block of the emails that never contained the
cards they style - kg-nft-link, kg-twitter-link, the three kg-cta-* link rules,
post-title-link, post-title-link-left, view-online-link and kg-audio-link. None
of those classes appears on any element in the snapshots that lost them, so this
is dead weight rather than styling: 1,165 bytes of it in every email, which is
1,165 bytes further from Gmail's clipping threshold.
Nothing about what renders changes, and the diff shows it: no deletion touches a
tag, an inline style attribute, an href or a class, and the only content that did
not exist before is seven content-length headers, each smaller than before. The
rules juice genuinely cannot inline are untouched - :hover, :active and both
media queries survive byte-identically, checked directly against juice 9. Where
one of these selectors does match an element, the inline style juice writes is
byte-identical between the two versions, `!important` and duplicated
client-fallback declarations included.
Verified against MySQL 8.0.46, the version CI runs: the integration lane is green
(463 passed) and the e2e lane green but for two members.test.js snapshots about
member attribution, which pass in isolation, passed in an earlier full-lane run,
and belong to a file this branch does not touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWgjERCX2GuTpX9rLXHBPR 18c05503 Updated email snapshots for juice 11 dropping dead CSS
no ref
juice 9 left every rule it had already inlined, and every rule that matched
nothing at all, in the residual style block. juice 11 emits only what it cannot
inline. The email snapshots recorded the old behaviour, so six of them failed on
rules that never applied to anything.
Nine selectors go from the style block of the emails that never contained the
cards they style - kg-nft-link, kg-twitter-link, the three kg-cta-* link rules,
post-title-link, post-title-link-left, view-online-link and kg-audio-link. None
of those classes appears on any element in the snapshots that lost them, so this
is dead weight rather than styling: 1,165 bytes of it in every email, which is
1,165 bytes further from Gmail's clipping threshold.
Nothing about what renders changes, and the diff shows it: no deletion touches a
tag, an inline style attribute, an href or a class, and the only content that did
not exist before is seven content-length headers, each smaller than before. The
rules juice genuinely cannot inline are untouched - :hover, :active and both
media queries survive byte-identically, checked directly against juice 9. Where
one of these selectors does match an element, the inline style juice writes is
byte-identical between the two versions, `!important` and duplicated
client-fallback declarations included.
Verified against MySQL 8.0.46, the version CI runs: the integration lane is green
(463 passed) and the e2e lane green but for two members.test.js snapshots about
member attribution, which pass in isolation, passed in an earlier full-lane run,
and belong to a file this branch does not touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWgjERCX2GuTpX9rLXHBPR 584acf90 Dropped apache-arrow from the Elasticsearch client
no ref
Ghost pulls apache-arrow in through @tryghost/logging and @tryghost/metrics,
which both depend on @tryghost/elasticsearch and so on the 8.x Elasticsearch
client. That client requires apache-arrow at module scope, but the only things
that touch it are the ES|QL helpers `toArrowTable` and `toArrowReader` — neither
of which Ghost calls, and neither of which @tryghost/elasticsearch can reach: it
only ever indexes documents.
The 9.x client already fixed this, deferring the require behind a `loadArrow()`
try/catch. We can't take 9.x, because it needs Elasticsearch 9 and our instance
isn't there yet, so the patch backports that: the module-scope require becomes a
lazy `loadArrow()` throwing 9.x's own "Package apache-arrow is required for Arrow
functionality" message, and the two helpers call it on entry. The `.d.ts` import
becomes `import type`, as 9.x has it.
Removing the dependency is the readPackage hook in .pnpmfile.mjs, not the patch —
pnpm resolves from the registry manifest and applies patches afterwards, so a
manifest edit inside the patch changes nothing about what gets installed. That's
the same tool this repo already uses for consolidate's peers, knex's sqlite3 and
the typescript peers on viem/ox/abitype. It deletes the dependency rather than
re-declaring it as the optional peer 9.x has, because autoInstallPeers is on by
default and installs optional peers anyway — and doing so peer-forks
@tryghost/logging and everything above it (elasticsearch, metrics, job-manager,
prometheus-metrics, domain-events, knex-migrator) into `(apache-arrow@21.1.0)`
variants for no benefit.
So the installed package.json still names apache-arrow as a dependency that
isn't there. The try/catch is what makes that safe.
That takes 15 packages and ~15MB out of the install — apache-arrow itself plus
flatbuffers, json-bignum, the command-line-args/usage tree and @types/node 24 —
and the phantom `@75lb/nature` peer disappears from eight packages' transitive
peer lists with them. A process that configures the Elasticsearch log transport
loads 145 fewer modules and measures ~3.3MB less RSS, averaged over three runs
each way with a forced GC.
Both packaging paths were already generic over `patchedDependencies` after the
juice patch landed, so the new patch rides along: pack.mjs copies and validates
every patch the trimmed workspace names, and docker/ghost-dev copies the whole
patches dir. The release archive gets no .pnpmfile.mjs, but its lockfile is
seeded from this one and pnpm reuses the seeded snapshot rather than
re-resolving, so arrow stays out there too — verified by reproducing that step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013AjXHCDS7EaJmRKmtggwfU ba523372 Added a freeze helper that makes config read-only once loaded
no ref
Config is fully loaded by the time loadNconf() returns, and nothing in Ghost
writes to it afterwards, but nconf doesn't know that. Every config.get()
walks all nine stores, and for an object-valued key it collects a hit from
each one and deep-merges them - on every single call. There are 353 get call
sites in core, some on per-request paths.
freeze() makes the instance read-only and memoises get() by key. Because each
cached value is whatever nconf itself returned for that key, a frozen lookup
can't disagree with an unfrozen one, and with writes rejected a cache entry
can't go stale - so there's no invalidation to get wrong. Measured against the
real config: get('database') 3526ns -> 15ns, getSiteUrl() 1276ns -> 63ns.
The mutators throw rather than no-op. nconf's own readOnly flag would have
been the obvious lever, but Provider._execute *skips* read-only stores for a
destructive action and returns undefined, which turns a config write into a
silent failure instead of a loud one.
loadNconf freezes as its last step rather than leaving it to boot, so a write
during boot fails loudly instead of quietly working. Nothing in the tree does
that today - the last two runtime writes were the asset hash, moved into the
asset hash service - so this holds the line rather than changing behaviour.
It's skipped under test, where the suites rewrite config between cases on
purpose, and optimization.freezeConfig turns it off entirely.
A keyless get() is left uncached. nconf's env store holds the whole
environment, so the merged tree materialises it - on a boot here, 139 of 194
top-level keys came from env rather than config - and caching that would mean
a long-lived object holding every environment variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s9ZdgxjVXMr7YTh4UtmtH ba523372 Added a freeze helper that makes config read-only once loaded
no ref
Config is fully loaded by the time loadNconf() returns, and nothing in Ghost
writes to it afterwards, but nconf doesn't know that. Every config.get()
walks all nine stores, and for an object-valued key it collects a hit from
each one and deep-merges them - on every single call. There are 353 get call
sites in core, some on per-request paths.
freeze() makes the instance read-only and memoises get() by key. Because each
cached value is whatever nconf itself returned for that key, a frozen lookup
can't disagree with an unfrozen one, and with writes rejected a cache entry
can't go stale - so there's no invalidation to get wrong. Measured against the
real config: get('database') 3526ns -> 15ns, getSiteUrl() 1276ns -> 63ns.
The mutators throw rather than no-op. nconf's own readOnly flag would have
been the obvious lever, but Provider._execute *skips* read-only stores for a
destructive action and returns undefined, which turns a config write into a
silent failure instead of a loud one.
loadNconf freezes as its last step rather than leaving it to boot, so a write
during boot fails loudly instead of quietly working. Nothing in the tree does
that today - the last two runtime writes were the asset hash, moved into the
asset hash service - so this holds the line rather than changing behaviour.
It's skipped under test, where the suites rewrite config between cases on
purpose, and optimization.freezeConfig turns it off entirely.
A keyless get() is left uncached. nconf's env store holds the whole
environment, so the merged tree materialises it - on a boot here, 139 of 194
top-level keys came from env rather than config - and caching that would mean
a long-lived object holding every environment variable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s9ZdgxjVXMr7YTh4UtmtH c8c3b7b4 Added a freeze helper that makes config read-only after boot
no ref
Config is fully loaded by the time loadNconf() returns, and nothing in Ghost
writes to it once booted, but nconf doesn't know that. Every config.get()
walks all nine stores, and for an object-valued key it collects a hit from
each one and deep-merges them - on every single call. There are 353 get call
sites in core, some on per-request paths.
freeze() makes the instance read-only and memoises get() by key. Because each
cached value is whatever nconf itself returned for that key, a frozen lookup
can't disagree with an unfrozen one, and with writes rejected a cache entry
can't go stale - so there's no invalidation to get wrong. Measured against the
real config: get('database') 3526ns -> 15ns, getSiteUrl() 1276ns -> 63ns.
The mutators throw rather than no-op. nconf's own readOnly flag would have
been the obvious lever, but Provider._execute *skips* read-only stores for a
destructive action and returns undefined, which turns a config write into a
silent failure instead of a loud one.
Boot freezes after the app is mounted rather than at load, so anything that
still wants to write during startup keeps working and only runtime writes are
rejected. It's skipped under test, where the suites rewrite config between
cases on purpose, and optimization.freezeConfig turns it off entirely.
freeze() also reports shadowed keys. nconf reads the whole environment, so a
stray env var can collide with a config namespace, and because lookups resolve
per key path the collision only half-applies:
storage=/mnt/data ghost run
config.get('storage') -> '/mnt/data'
config.get('storage:active') -> 'LocalImagesStorage' (still the default)
Ghost boots fine in that state today and quietly ignores half of what the
operator asked for, so boot now warns about it. This is a diagnostic only -
freezing caches what nconf returns, so it changes no lookup's result.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011s9ZdgxjVXMr7YTh4UtmtH 73a5dd19 Dropped apache-arrow from the Elasticsearch client
no ref
Ghost pulls apache-arrow in through @tryghost/logging and @tryghost/metrics,
which both depend on @tryghost/elasticsearch and so on the 8.x Elasticsearch
client. That client requires apache-arrow at module scope, but the only things
that touch it are the ES|QL helpers `toArrowTable` and `toArrowReader` — neither
of which Ghost calls, and neither of which @tryghost/elasticsearch can reach: it
only ever indexes documents.
The 9.x client already fixed this, deferring the require behind a `loadArrow()`
try/catch. We can't take 9.x, because it needs Elasticsearch 9 and our instance
isn't there yet, so the patch backports that: the module-scope require becomes a
lazy `loadArrow()` throwing 9.x's own "Package apache-arrow is required for Arrow
functionality" message, and the two helpers call it on entry. The `.d.ts` import
becomes `import type`, as 9.x has it.
Removing the dependency is the readPackage hook in .pnpmfile.mjs, not the patch —
pnpm resolves from the registry manifest and applies patches afterwards, so a
manifest edit inside the patch changes nothing about what gets installed. That's
the same tool this repo already uses for consolidate's peers, knex's sqlite3 and
the typescript peers on viem/ox/abitype. It deletes the dependency rather than
re-declaring it as the optional peer 9.x has, because autoInstallPeers is on by
default and installs optional peers anyway — and doing so peer-forks
@tryghost/logging and everything above it (elasticsearch, metrics, job-manager,
prometheus-metrics, domain-events, knex-migrator) into `(apache-arrow@21.1.0)`
variants for no benefit.
So the installed package.json still names apache-arrow as a dependency that
isn't there. The try/catch is what makes that safe.
That takes 15 packages and ~15MB out of the install — apache-arrow itself plus
flatbuffers, json-bignum, the command-line-args/usage tree and @types/node 24 —
and the phantom `@75lb/nature` peer disappears from eight packages' transitive
peer lists with them. A process that configures the Elasticsearch log transport
loads 145 fewer modules and measures ~3.3MB less RSS, averaged over three runs
each way with a forced GC.
Both packaging paths were already generic over `patchedDependencies` after the
juice patch landed, so the new patch rides along: pack.mjs copies and validates
every patch the trimmed workspace names, and docker/ghost-dev copies the whole
patches dir. The release archive gets no .pnpmfile.mjs, but its lockfile is
seeded from this one and pnpm reuses the seeded snapshot rather than
re-resolving, so arrow stays out there too — verified by reproducing that step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013AjXHCDS7EaJmRKmtggwfU dbb229a9 Updated email snapshots for juice 11 dropping dead CSS
no ref
juice 9 left every rule it had already inlined, and every rule that matched
nothing at all, in the residual style block. juice 11 emits only what it cannot
inline. The email snapshots recorded the old behaviour, so six of them failed on
rules that never applied to anything.
Nine selectors go from the style block of the emails that never contained the
cards they style - kg-nft-link, kg-twitter-link, the three kg-cta-* link rules,
post-title-link, post-title-link-left, view-online-link and kg-audio-link. None
of those classes appears on any element in the snapshots that lost them, so this
is dead weight rather than styling: 1,165 bytes of it in every email, which is
1,165 bytes further from Gmail's clipping threshold.
Nothing about what renders changes, and the diff shows it: no deletion touches a
tag, an inline style attribute, an href or a class, and the only content that did
not exist before is seven content-length headers, each smaller than before. The
rules juice genuinely cannot inline are untouched - :hover, :active and both
media queries survive byte-identically, checked directly against juice 9. Where
one of these selectors does match an element, the inline style juice writes is
byte-identical between the two versions, `!important` and duplicated
client-fallback declarations included.
Verified against MySQL 8.0.46, the version CI runs: the integration lane is green
(463 passed) and the e2e lane green but for two members.test.js snapshots about
member attribution, which pass in isolation, passed in an earlier full-lane run,
and belong to a file this branch does not touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWgjERCX2GuTpX9rLXHBPR 8cbcc170 Restored the lockfile hashes my sandbox dropped
no ref
Installing juice and cheerio re-resolved three entries that have nothing to do
with either, because this sandbox reaches npm but gets a 403 for
codeload.github.com and unofficial-builds.nodejs.org. pnpm did what it should
on an unreachable host and fell back, so the damage was quiet and in the
lockfile rather than in an error.
google-caja-bower and keymaster went from codeload tarballs to git+https
clones. Both still pin the same commit, but the tarball URL and its sha512 go
away in that form, which is the binding pnpm's supply-chain pass checks on
every install - the specs are a branch and a bare repo URL, so that hash is
what pins the bytes rather than the ref. node lost its two linux-musl variants
outright, which would leave Alpine with no runtime to resolve.
Restored all three from the pre-change lockfile, so they are byte-identical to
what was there before. Everything still differing is cheerio, juice, their
dropped transitives or the patch: a diff of the packages and snapshots sections
reports 16 removals, 3 additions and no in-place modifications, and the
importers section differs in exactly the three specifier lines this branch
means to change. `pnpm install --frozen-lockfile` re-verifies all 4375 entries
against the supply-chain policies and passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWgjERCX2GuTpX9rLXHBPR 9a1d8695 Removed the duplicate npm copy of kg-default-cards
no ref
ghost/core links the workspace @tryghost/kg-default-cards, but it also
depends on @tryghost/mg-clean-html, whose own dependency on
@tryghost/mg-utils asks for the npm release of the same package. Both
copies were installed side by side, and the published one brought its own
published dependency tree with it, so the lockfile also carried duplicate
kg-utils, kg-markdown-html-renderer and url-utils entries — five packages
in all, in the production tree.
Beyond the dedupe this makes content import render cards with the same
code Ghost renders them with. The two were already the same release
(10.3.5) and their builds differ only in formatting — line wrapping,
trailing commas — from a different tsc printer, and the package exports
the same 20 cards either way. Verified with the package's own suite, the
content-import unit tests, and cleanHTML() end to end resolving through
to koenig/kg-default-cards. No new build requirement: ghost/core already
declares the workspace package, so its build output was already needed. d523683b Removed the duplicate npm copy of kg-default-cards
no ref
ghost/core links the workspace @tryghost/kg-default-cards, but it also
depends on @tryghost/mg-clean-html, whose own dependency on
@tryghost/mg-utils asks for the npm release of the same package. Both
copies were installed side by side, and the published one brought its own
published dependency tree with it, so the lockfile also carried duplicate
kg-utils, kg-markdown-html-renderer and url-utils entries — five packages
in all, in the production tree.
Point that one edge at the workspace package. It is scoped to mg-utils
because that is the only dependent; a bare override would silently capture
future ones too.
Beyond the dedupe this makes content import render cards with the same
code Ghost renders them with. The two were already the same release
(10.3.5) and their builds differ only in formatting — line wrapping,
trailing commas — from a different tsc printer, and the package exports
the same 20 cards either way. Verified with the package's own suite, the
content-import unit tests, and cleanHTML() end to end resolving through
to koenig/kg-default-cards. No new build requirement: ghost/core already
declares the workspace package, so its build output was already needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFn7f6or3xRZEcaUzzgeJX 059b4769 Updated undici to a single 8.10.0 copy via a pnpm override
no ref
ghost/core dragged three copies of undici into its tree, and all three
ship in the production image: jsdom pulls 8.x directly, cheerio 1.x (via
@tryghost/url-utils) pulls 7.x, and @elastic/transport (via
@tryghost/logging) pulls 6.x. Nothing declares undici itself, so there
was no version to reconcile in a package.json - an override is the only
lever.
Both consumers being moved across a major only use API that is unchanged
in 8.x: @elastic/transport builds a Pool with buildConnector and reads
statusCode/headers/body off pool.request, and cheerio composes a Client
with interceptors.redirect and errors.ResponseError. Smoke-tested each
against a local server on its original major and on 8.10.0 - same
parsed output, same status/body, same RequestAbortedError mapping for an
aborted request. undici 8 wants node >=22.19.0, which is below the
^22.23.1 || ^24.20.0 the monorepo already requires.
The pin also supersedes the `undici@<6.24.0` advisory override, which is
unreachable now that nothing resolves below 8.x, so it has been removed
rather than left to look like it still does something.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFn7f6or3xRZEcaUzzgeJX