Added publicSiteAccess limit enforcement to the Settings API#27917
Conversation
- gated on the `publicSiteAccess` flag limit from `@tryghost/limit-service`; when it is disabled (set by Ghost(Pro) for trial sites), the Settings BREAD service now marks `is_private` and `password` with `is_read_only: true` on browse, and rejects external attempts to flip `is_private = false` or change the access code on edit - internal context still bypasses the rejection so boot-time enforcement and a future regenerate endpoint can manage the values - the catalogued `@tryghost/limit-service` version is left untouched; Renovate will pull in the version that recognises `publicSiteAccess` separately, so this change is dormant in production until both that bump and the Daisy.js applied-limit land - the boot-time persistence of `is_private = true` and the access-code generator are deliberately scoped out — they belong with the access-code generator follow-up Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughThis PR adds enforcement of the 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Avoids mentioning "trial" in the user-facing error since the publicSiteAccess limit name is deliberately context-agnostic and may apply to non-trial states in future. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Settings BREAD service (merged in #27917) prevents runtime API mutations of is_private and the access code while the publicSiteAccess limit is disabled — but nothing was making the site private in the first place. Without a boot-time pass, visitors could see a public site in the window between boot and the first API write. This adds enforcePublicSiteAccessLimit() between populateDefaults() and SettingsCache.init(). It reads the in-memory settings collection, persists any missing values with internal context, and refetches before the cache reads it. After the first boot the values already match, so steady state is zero writes. The access-code generator is a placeholder (fake-###) — the real format needs a separate design pass and will land in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Settings BREAD service (merged in #27917) prevents runtime API mutations of is_private and the access code while the publicSiteAccess limit is disabled — but nothing was making the site private in the first place. Without a boot-time pass, visitors could see a public site in the window between boot and the first API write. This adds enforcePublicSiteAccessLimit() between populateDefaults() and SettingsCache.init(). It reads the in-memory settings collection, persists any missing values with internal context, and refetches before the cache reads it. After the first boot the values already match, so steady state is zero writes. The access-code generator is a placeholder (fake-###) — the real format needs a separate design pass and will land in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) The BREAD service (#27917) prevents runtime mutations of is_private and the access code while the publicSiteAccess limit is active, but nothing was making the site private in the first place. Without a boot-time pass, visitors could see a public site in the gap between boot and the first API write. This runs before the settings cache is initialized, using findOne/edit on the model directly so the cache sees the final state on first init. After first boot the values already match, so subsequent boots are zero-write no-ops. The access-code generator is a placeholder (fake-###) - the real format needs a separate design pass.
Summary
Adds API-layer enforcement for the new
publicSiteAccessflag limit on@tryghost/limit-service. When Ghost(Pro) applies that limit to a trial site, the Settings BREAD service now:is_privateandpasswordwithis_read_only: trueon browse — the field already flows throughghost/core/core/server/api/endpoints/utils/serializers/output/mappers/settings.js:6and Admin-X'sisSettingReadOnly()helper, so this is just populating it server-side;PUT /settings/attempts to flipis_private = falseor change the access code, returningNoPermissionError(403);Why now, why dormant
The limit-service package was updated to recognise
publicSiteAccessin TryGhost/SDK#918. The catalogued dependency in this repo is intentionally not bumped here — Renovate handles that as its own PR (see #27916, which isolates@tryghost/limit-servicefrom the admin-support group so the bump lands standalone).Until both the Renovate bump AND a Ghost(Pro) Daisy.js applied-limit land,
limitsService.isDisabled('publicSiteAccess')returnsundefined, the new code paths are no-ops, and nothing changes for any existing site.The remaining trial-private-site backend work — boot-time persistence of
is_private = trueand the generated access-code helper — is deliberately scoped out and will follow in a separate PR.Test plan
pnpm test:single test/unit/server/services/settings/settings-bread-service.test.js— 15 passing, including a newpublicSiteAccess limitsuite covering browse marking, edit rejection onis_privateandpassword, no-opis_private = trueedits, internal-context bypass, and the limit-off path.pnpm test:single test/e2e-api/admin/settings.test.js— 28 passing. New cases sinon-stublimits.isDisabled('publicSiteAccess')on the singleton (with an inline comment explaining why) rather than driving the limit throughconfigUtils.set+limits.init(), because this PR doesn't bump the SDK catalog version that recognises the limit name.pnpm lintclean.🤖 Generated with Claude Code