Skip to content

feat(publish): custom-domain canonical/OG/sitemap host + tier caps (PR5) - #1709

Merged
2witstudios merged 4 commits into
masterfrom
pu/customdomain-pr5
Jun 24, 2026
Merged

2witstudios merged 4 commits into
masterfrom
pu/customdomain-pr5

Conversation

@2witstudios

@2witstudios 2witstudios commented Jun 24, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Primary published host: Pure `resolvePrimaryPublishedHost()` function picks earliest-registered custom domain (`createdAt` = registration time, schema has no activation timestamp; hostname lexicographic tiebreaker), falling back to `.pagespace.site`. Exported as `@pagespace/lib/canvas/primary-host`.
  • Canonical/OG/JSON-LD host: `publishCanvasPage` and `regeneratePublishedSiteFiles` now load active custom domains and use the primary host for og:url, canonical tags, sitemap ``, and robots.txt `Sitemap:` directive. Response URL (what the UI shows) uses the subdomain (guaranteed write target) — the canonical URLs in the rendered HTML use the primary host.
  • Site file mirroring: `planCustomDomainMirror` gains an `includeSiteFiles` flag; `mirror404ToHosts` and `mirrorDriveToCustomHost` mirror `robots.txt` + `sitemap.xml` to every active custom-domain prefix (alongside 404.html and page artifacts). `copyPublishedSiteFileArtifact` (new) uses `MetadataDirective: 'COPY'` to preserve source content-type; page artifacts keep `MetadataDirective: 'REPLACE'`.
  • Tier caps (atomic): `maxCustomDomains` added to `PlanDefinition.limits` (free=0, pro=1, founder=3, business=10). `POST /api/drives/[driveId]/domains` enforces the cap atomically — `SELECT drives.id FOR UPDATE` + count-check + insert run in a single transaction to prevent over-cap inserts under concurrent load. `GET` returns `limit` for the UI.
  • Mirror cleanup (retryable): `cert/refresh` route calls `clearCustomHost` whenever `nextStatus === 'cert_failed'` (not just `active → cert_failed`), so retries after a failed `clearCustomHost()` still attempt cleanup — the prefix delete is idempotent/no-op when nothing remains. Errors caught+logged so a storage failure does not return 500 when the DB status is already committed.
  • Cert activation ordering: `regeneratePublishedSiteFiles` is called BEFORE `mirrorDriveToCustomHost` on cert activation — so site files embed the custom domain as primary host before being copied.
  • UI: `CustomDomainsCard` shows `X / N` counter, disables Add at cap, shows upgrade nudge for free tier, surfaces 403 plan errors in toast.

Files changed

Layer File What changed
lib (pure) `packages/lib/src/canvas/primary-host.ts` NEW — `resolvePrimaryPublishedHost`
lib (pure) `packages/lib/src/canvas/custom-domain-mirror.ts` `includeSiteFiles` flag in planner
lib `packages/lib/package.json` `canvas/primary-host` export entry
web lib `apps/web/src/lib/canvas/published-storage.ts` `copyPublishedSiteFileArtifact` (MetadataDirective: COPY)
web lib `apps/web/src/lib/canvas/custom-domain-mirror.ts` `getActiveDomainRecords()` export; `isSiteFileKey()` dispatch; pass `includeSiteFiles`
web lib `apps/web/src/lib/canvas/publish-page.ts` Primary host threading; subdomain response URL
web API `apps/web/src/app/api/drives/[driveId]/domains/route.ts` Atomic transaction cap enforcement; `limit` in GET
web API `apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts` Retryable `clearCustomHost`; regen before mirror
web UI `apps/web/src/lib/subscription/plans.ts` `maxCustomDomains` per plan
web UI `apps/web/src/app/dashboard/[driveId]/settings/general/page.tsx` Domain count/limit display; 403 error handling

Test plan

  • `packages/lib` canvas tests: `primary-host.test.ts` (7 pure tests), `custom-domain-mirror.test.ts` (updated)
  • `apps/web` tests: `publish-page.test.ts`, `custom-domain-mirror.test.ts`, `domains/route.test.ts`, `cert/refresh/route.test.ts`, `pages/publish/route.test.ts` — all pass
  • Web typecheck: `bun run --filter 'web' typecheck` exits 0
  • `@pagespace/lib` typecheck: exits 0

🤖 Generated with Claude Code

https://claude.ai/code/session_01FWbcqFFxPncD7Xpu6XRdSY

- Add resolvePrimaryPublishedHost() pure fn: earliest-activated custom
  domain wins; hostname lexicographic tiebreaker; pagespace.site fallback
- Export ActiveDomainRecord type + canvas/primary-host subpath from lib
- Thread primary host through publishCanvasPage (og:url, canonical,
  JSON-LD) and regeneratePublishedSiteFiles (sitemap <loc>, robots.txt
  Sitemap: directive)
- Add includeSiteFiles flag to planCustomDomainMirror so robots.txt +
  sitemap.xml are mirrored to every active custom-domain prefix alongside
  page artifacts and 404.html
- Export getActiveDomainRecords() from web custom-domain-mirror so
  publish-page.ts can load active domains without duplicating the query
- Add maxCustomDomains per plan (free=0, pro=1, founder=3, business=10)
  to PlanDefinition.limits
- Enforce tier cap in POST /api/drives/[driveId]/domains: 403 when
  limit=0 (plan not available) or count >= limit
- Return limit from GET /api/drives/[driveId]/domains so the UI can
  surface it without a second round-trip
- Wire clearCustomHost in cert/refresh route when active→cert_failed so
  stale content is purged on deactivation
- Update CustomDomainsCard: show X/N counter, disable Add when at cap,
  show upgrade nudge for free tier, surface 403 tier errors in toast

Tests: pure unit tests for primary-host; updated custom-domain-mirror
tests; updated publish-page + route tests with getActiveDomainRecords
mock; updated cert/refresh tests; new domains route tests for tier caps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWbcqFFxPncD7Xpu6XRdSY
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds custom-domain caps, a deterministic primary host resolver, site-file mirroring for custom-domain hosts, updated published-site URL handling, and certificate-refresh cleanup for active domains that fail.

Changes

Custom Domain Limits, Primary Host & Site File Mirroring

Layer / File(s) Summary
Plan limits and primary host
apps/web/src/lib/subscription/plans.ts, packages/lib/src/canvas/primary-host.ts, packages/lib/package.json, packages/lib/src/canvas/__tests__/primary-host.test.ts
maxCustomDomains is added to plan limits for all tiers. resolvePrimaryPublishedHost selects a primary host from active domain records with deterministic sorting and is exported through packages/lib, with tests covering fallback, ordering, and tie-breaking.
Site-file mirroring
packages/lib/src/canvas/custom-domain-mirror.ts, apps/web/src/lib/canvas/custom-domain-mirror.ts, apps/web/src/lib/canvas/published-storage.ts, packages/lib/src/canvas/__tests__/custom-domain-mirror.test.ts, apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
includeSiteFiles is added to the mirror planner, robots.txt and sitemap.xml are copied with a site-file copy helper, and the app-layer mirror module exposes active-domain records. Tests cover the planner, active-domain lookup, and the expanded mirroring behavior.
Published URLs and site files
apps/web/src/lib/canvas/publish-page.ts, apps/web/src/lib/canvas/__tests__/publish-page.test.ts, apps/web/src/app/api/pages/[pageId]/publish/__tests__/route.test.ts
publishCanvasPage now builds canonical metadata URLs from the primary host while returning subdomain-based response URLs. regeneratePublishedSiteFiles uses the same host for sitemap and robots origins, and tests cover active-domain and fallback-host paths.
Domain cap enforcement
apps/web/src/app/api/drives/[driveId]/domains/route.ts, apps/web/src/app/api/drives/[driveId]/domains/__tests__/route.test.ts, apps/web/src/app/dashboard/[driveId]/settings/general/page.tsx
The domains API now returns and enforces per-drive custom-domain limits from the owner’s subscription tier. The settings UI consumes the limit to show availability, cap, and empty-state changes, and the route tests cover the new GET and POST behavior.
Cert refresh cleanup
apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts, apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/__tests__/route.test.ts
The cert refresh route now calls clearCustomHost when an active domain moves to cert_failed, and the tests verify the cleanup path and the non-cleanup cases.

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant DomainsRoute
  participant DB
  participant CustomDomainsCard

  Dashboard->>DomainsRoute: GET /api/drives/:driveId/domains
  DomainsRoute->>DB: fetch domains and plan limit
  DB-->>DomainsRoute: domains, limit
  DomainsRoute-->>Dashboard: { domains, limit }
  Dashboard->>CustomDomainsCard: render with limit
  CustomDomainsCard->>CustomDomainsCard: show availability or cap state
Loading
sequenceDiagram
  participant publishCanvasPage
  participant getActiveDomainRecords
  participant resolvePrimaryPublishedHost
  participant regeneratePublishedSiteFiles

  publishCanvasPage->>getActiveDomainRecords: driveId
  getActiveDomainRecords-->>publishCanvasPage: ActiveDomainRecord[]
  publishCanvasPage->>resolvePrimaryPublishedHost: subdomain, publishHost, activeDomains
  resolvePrimaryPublishedHost-->>publishCanvasPage: primaryHost
  regeneratePublishedSiteFiles->>getActiveDomainRecords: driveId
  getActiveDomainRecords-->>regeneratePublishedSiteFiles: ActiveDomainRecord[]
  regeneratePublishedSiteFiles->>resolvePrimaryPublishedHost: subdomain, publishHost, activeDomains
  resolvePrimaryPublishedHost-->>regeneratePublishedSiteFiles: primaryHost
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • 2witstudios/PageSpace#1679: Also changes apps/web/src/lib/canvas/publish-page.ts around primary-host URL handling and published-page metadata, which is directly related to the URL-selection changes here.

Poem

🐇 I hop by hosts both old and new,
With sitemaps bright and robots too.
When domain caps say “just one more,”
I nibble routes and mirrorோர்—
Then clear the haze when certs go dim,
And bounce on with a tidy grin.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly captures the main scope: custom-domain host selection for published metadata/files and plan-based custom-domain caps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/customdomain-pr5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/lib/canvas/custom-domain-mirror.ts (1)

218-225: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Activation backfill copies stale host-dependent artifacts.

After this PR, page canonicals/OG URLs and site files depend on the active-domain set at render time. mirrorDriveToCustomHost() still just copies the existing subdomain artifacts when a cert flips to active, so a newly activated domain can serve pages, robots.txt, and sitemap.xml that still point at *.pagespace.site until some later publish/regeneration happens. The activation path needs regeneration against the new active-domain set before mirroring.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/lib/canvas/custom-domain-mirror.ts` around lines 218 - 225, The
activation path in mirrorDriveToCustomHost currently reuses stale subdomain
artifacts instead of regenerating them for the newly active host. Update the
flow around planCustomDomainMirror and the copying logic so that when a cert
becomes active, pages, robots.txt, and sitemap.xml are regenerated against the
new active-domain set before copying/mirroring site files and root assets.
Ensure the host-sensitive render step is tied to the activation path, not
deferred to a later publish.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/web/src/app/api/drives/`[driveId]/domains/[domainId]/cert/refresh/route.ts:
- Around line 83-92: The custom-host purge in the cert status transition path is
fired-and-forgotten, so the route can return before `clearCustomHost()`
finishes. Update the `refresh` route handling for the `domain.status ===
'active' && nextStatus === 'cert_failed'` branch to either `await
clearCustomHost(domain.hostname)` before continuing or move the cleanup into a
supported `after()`-style post-response hook, and keep the existing
`loggers.api.warn` error handling around `clearCustomHost()`.

In `@apps/web/src/app/api/drives/`[driveId]/domains/route.ts:
- Around line 102-123: The cap check in the domains POST handler is race-prone
because getMaxCustomDomainsForDrive and the customDomains count are read before
db.insert() in the same flow, allowing concurrent requests to bypass the limit.
Update the route’s create path to enforce the limit atomically for each drive,
ideally by wrapping the check-and-insert logic in a transaction with a row-level
lock or another DB-backed guard, so the existing count cannot change between the
guard and the insert. Reference the route handler logic around the existing
maxAllowed/existingCount check and the db.insert(customDomains) call when making
the fix.

In `@apps/web/src/lib/canvas/custom-domain-mirror.ts`:
- Around line 26-32: The primary-host selection in getActiveDomainRecords is
using customDomains.createdAt, but that is the row creation time rather than the
time a domain became active. Update the data model and selection flow so
resolvePrimaryPublishedHost orders by a persisted activation timestamp (or
change the documented rule to match the existing field), and make sure
getActiveDomainRecords returns that activation field instead of createdAt for
active domains.
- Around line 109-115: Split the site-file mirroring flow in
custom-domain-mirror so robots.txt and sitemap.xml do not use
copyPublishedArtifact, since that helper applies HTML metadata via
MetadataDirective and ContentType. Update the mirroring logic around the
Promise.allSettled copy loop to route 404.html through copyPublishedArtifact as
before, but handle site files with a separate helper that preserves their
correct content types (text/plain for robots.txt and application/xml for
sitemap.xml). Keep the existing logging in the error path, and use the relevant
symbols copyPublishedArtifact and the mirroring loop in custom-domain-mirror.ts
to locate the change.

In `@apps/web/src/lib/canvas/publish-page.ts`:
- Around line 217-226: The publish response is now using the primary custom
domain even though only the subdomain artifact is guaranteed to be written in
publish-page.ts via resolvePrimaryPublishedHost and the later mirroring step is
best-effort. Update the logic around publishedUrl/result.url so it either waits
for the primary-host mirror to succeed before returning that host, or falls back
to the subdomain URL whenever the custom-domain copy is not confirmed. Use the
existing primaryHost, subdomain, and publishedUrl flow to keep the response
aligned with the actually persisted artifact.

In `@packages/lib/src/canvas/primary-host.ts`:
- Around line 23-25: The primary host selection is currently based on
`createdAt` in `ActiveDomainRecord`, which can pick the wrong canonical host
when activation order differs from record creation. Update `ActiveDomainRecord`
and `getActiveDomainRecords()` in `primary-host.ts` to carry the actual
activation timestamp instead of creation time, then sort by that activation
field so the earliest-activated custom domain is chosen consistently for
`canonical`, `og:url`, sitemap, and robots origins.

---

Outside diff comments:
In `@apps/web/src/lib/canvas/custom-domain-mirror.ts`:
- Around line 218-225: The activation path in mirrorDriveToCustomHost currently
reuses stale subdomain artifacts instead of regenerating them for the newly
active host. Update the flow around planCustomDomainMirror and the copying logic
so that when a cert becomes active, pages, robots.txt, and sitemap.xml are
regenerated against the new active-domain set before copying/mirroring site
files and root assets. Ensure the host-sensitive render step is tied to the
activation path, not deferred to a later publish.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02de92f2-d3e8-4e8d-90f7-faddd2e1f23c

📥 Commits

Reviewing files that changed from the base of the PR and between 29ac103 and 74d6af5.

📒 Files selected for processing (16)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts
  • apps/web/src/app/api/drives/[driveId]/domains/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/route.ts
  • apps/web/src/app/api/pages/[pageId]/publish/__tests__/route.test.ts
  • apps/web/src/app/dashboard/[driveId]/settings/general/page.tsx
  • apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
  • apps/web/src/lib/canvas/__tests__/publish-page.test.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • apps/web/src/lib/canvas/publish-page.ts
  • apps/web/src/lib/subscription/plans.ts
  • packages/lib/package.json
  • packages/lib/src/canvas/__tests__/custom-domain-mirror.test.ts
  • packages/lib/src/canvas/__tests__/primary-host.test.ts
  • packages/lib/src/canvas/custom-domain-mirror.ts
  • packages/lib/src/canvas/primary-host.ts

Comment thread apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts Outdated
Comment thread apps/web/src/app/api/drives/[driveId]/domains/route.ts Outdated
Comment thread apps/web/src/lib/canvas/custom-domain-mirror.ts
Comment thread apps/web/src/lib/canvas/custom-domain-mirror.ts
Comment thread apps/web/src/lib/canvas/publish-page.ts
Comment thread packages/lib/src/canvas/primary-host.ts
- Await clearCustomHost in cert/refresh (was fire-and-forget, should surface errors)
- Regenerate site files before mirrorDriveToCustomHost on cert activation so
  sitemap/robots embed the custom domain as primary host before copying
- Wrap POST /domains count-check + insert in db.transaction with drive-row
  SELECT FOR UPDATE to prevent concurrent over-cap inserts
- Add copyPublishedSiteFileArtifact (MetadataDirective=COPY) for site files so
  robots.txt/sitemap.xml preserve their correct content-types when mirrored
- Response URL in publishCanvasPage now uses subdomain (guaranteed write target)
  instead of primary custom domain (best-effort mirror) for reliability
- Clarify ActiveDomainRecord.createdAt is earliest-registered not earliest-activated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWbcqFFxPncD7Xpu6XRdSY
@2witstudios

Copy link
Copy Markdown
Owner Author

Outside-diff comment (activation backfill uses stale host-dependent artifacts): Fixed in 657200b. In cert/refresh/route.ts, the activation path now calls await regeneratePublishedSiteFiles(driveId) first — this regenerates robots.txt/sitemap.xml with the newly-active custom domain as the primary host. Only then does it fire-and-forget mirrorDriveToCustomHost to copy all artifacts (including the freshly-generated site files) to the custom-host prefix. This ensures no custom-domain host ever serves site files that still point at *.pagespace.site.

…ost-commit

clearCustomHost is now awaited (so response waits for cleanup) but errors are
caught+logged rather than thrown. DB status is already committed before this
call, so a storage failure should not make the response appear as a 500 error
to the caller. Added test: clearCustomHost throw → still returns 200+cert_failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWbcqFFxPncD7Xpu6XRdSY

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/web/src/app/api/drives/`[driveId]/domains/[domainId]/cert/refresh/route.ts:
- Around line 87-88: The cleanup in the cert refresh flow is only triggered on
the first active-to-cert_failed transition, so retries can skip purging stale
mirrored artifacts after a failed clearCustomHost() call. Update the route
handler logic in the cert refresh path to run the purge whenever nextStatus is
cert_failed, regardless of the current domain.status, and keep the
clearCustomHost() call as the idempotent cleanup step so repeated retries safely
no-op when nothing remains.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fba41135-defd-4fdb-bdc4-02cab3d47e57

📥 Commits

Reviewing files that changed from the base of the PR and between 74d6af5 and 657200b.

📒 Files selected for processing (10)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts
  • apps/web/src/app/api/drives/[driveId]/domains/__tests__/route.test.ts
  • apps/web/src/app/api/drives/[driveId]/domains/route.ts
  • apps/web/src/lib/canvas/__tests__/custom-domain-mirror.test.ts
  • apps/web/src/lib/canvas/__tests__/publish-page.test.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • apps/web/src/lib/canvas/publish-page.ts
  • apps/web/src/lib/canvas/published-storage.ts
  • packages/lib/src/canvas/primary-host.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/tests/route.test.ts
  • apps/web/src/lib/canvas/publish-page.ts
  • apps/web/src/app/api/drives/[driveId]/domains/route.ts
  • packages/lib/src/canvas/primary-host.ts
  • apps/web/src/lib/canvas/custom-domain-mirror.ts
  • apps/web/src/app/api/drives/[driveId]/domains/tests/route.test.ts
  • apps/web/src/lib/canvas/tests/custom-domain-mirror.test.ts

Comment thread apps/web/src/app/api/drives/[driveId]/domains/[domainId]/cert/refresh/route.ts Outdated
…s, not just active→cert_failed

Retryability fix: the previous condition only purged mirrored artifacts on the
first active→cert_failed transition. If clearCustomHost() threw, the DB was
already cert_failed, so retries saw a non-active status and skipped cleanup,
leaving stale prefix artifacts indefinitely.

Widening to nextStatus === 'cert_failed' is safe because clearCustomHost()
deletes everything under published/<host>/ which is a no-op when the prefix
is already empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FWbcqFFxPncD7Xpu6XRdSY
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant