feat(sunset): wind down the hosted service behind SUNSET_MODE - #1801
Conversation
# Conflicts: # surfsense_local/frontend/src/components/ui/icons.tsx # surfsense_local/frontend/src/features/updates/update-settings.test.tsx # surfsense_local/frontend/src/features/updates/update-settings.tsx # surfsense_web/lib/auth-utils.ts
|
@CREDO23 is attempting to deploy a commit to the Rohan Verma's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Supersedes #1777, which had drifted far enough from
devthat its diff no longer rendered. Same work, rebuilt on the current base, with three bugs fixed. Credit for the implementation is @AnishSarkar22's.What it does
One flag,
SUNSET_MODE, read per request on both sides, turning the hosted service export-only at T-0. Unset means off, so self-hosted installs are unaffected — that half is a launch gate, and the tests assert it path by path and method by method.SunsetWriteBlockMiddlewarerefuses writes with410. Reads are never touched, which is what keeps export working without anyone enumerating the routes that mutate. Auth stays open except/auth/register, because export lives behind a session. Licence routes and the Stripe webhook stay open, because that business outlives the wind-down.proxy.tssends app routes to/sunsetwhile the portal stays reachable. Deliberately notNEXT_PUBLIC_*: those are inlined at build time, and the flip has to be a restart rather than a redeploy, the same way the backend reads its half./healthreportssunsetand now accepts1,true,yes,on. The previous comparison was== "true", so contract 4's documentedSUNSET_MODE=1read as false — the switch would have looked thrown and done nothing.Also:
purge_hosted_accounts.pyfor T+30, looping the existingerase_accountso blobs go with the rows; both runbooks; the flag documented in three.env.examplefiles and both compose files.Fixed relative to #1777
The scraper API and MCP were blocked at T-0. Scraper calls are
POST /api/v1/workspaces/{id}/scrapers/{platform}/{verb}, which matched no allowlist prefix, so flipping the flag would have410d the one product meant to survive the sunset and become the paid plugin at T+7. The workspace id sits mid-path, so a prefix cannot express it:Covered by a test; the previous suite mentioned scrapers nowhere.
/downloadsand/pluginswere dropped from the public route list. ExtractingPUBLIC_ROUTE_PREFIXESintolib/public-routes.tsmoved a copy that predated those two entries. With the flag off that breaks both pages for anonymous visitors —ZeroProviderstops short-circuiting, the context fetch 401s, andhandleUnauthorizedsends them to/login. With the flag on,/downloadsredirects to/sunset, whose whole purpose is to send people to/downloads. The extracted list is now byte-identical to the one ondev.UpdateButtonthrew on click. It calledbridge(), whichdevhad renamed toupdatesBridge. Both sides merged cleanly as text, so only the test caught it.The middleware-order comment is also corrected:
add_middlewareinserts at index 0, so the block runs inside CORS, not outside it — which is what lets a refused write carry the headers the browser needs to read it.Checks
Backend sunset and licence suites pass, 186 tests.
sunset-redirectand the desktop update tests pass. The desktop suite has 5 pre-existing failures, all of which also fail on a cleandevcheckout (which has 6).High-level PR Summary
This PR implements a coordinated wind-down mechanism for the hosted service behind a single
SUNSET_MODEflag. When enabled, the backend refuses writes with 410 Gone while keeping exports and authentication working, and the web app redirects all application routes to/sunsetwhile preserving the portal pages (licensing, pricing, downloads). The flag is read per-request on both sides so the switch is a restart rather than a redeploy, and it defaults to off so self-hosted installations are completely unaffected. The implementation includes a purge script for T+30 that loops the existing account deletion function to properly clean up blobs and knowledge stores, comprehensive test coverage asserting the launch gate path-by-path, and operational runbooks for both the sunset and purge stages. Three bugs from the original PR are fixed: the scraper API and MCP are now properly allowlisted,/downloadsand/pluginsare restored to the public route list, andUpdateButtoncalls the correctly-renamed bridge function.⏱️ Estimated Review Time: 30-90 minutes
💡 Review Order Suggestion
plans/community-local/00d-pivot-plan.mdplans/community-local/sunset-runbook.mdplans/community-local/purge-runbook.mdsurfsense_backend/app/sunset.pysurfsense_web/lib/sunset.tssurfsense_web/lib/public-routes.tssurfsense_backend/app/app.pysurfsense_web/proxy.tssurfsense_backend/scripts/purge_hosted_accounts.pysurfsense_backend/tests/unit/test_sunset_flag.pysurfsense_backend/tests/unit/test_sunset_write_block.pysurfsense_web/tests/unit/sunset-redirect.test.tssurfsense_backend/.env.examplesurfsense_web/.env.exampledocker/.env.exampledocker/docker-compose.ymldocker/docker-compose.dev.ymlsurfsense_web/lib/auth-utils.tssurfsense_local/frontend/src/features/updates/update-settings.tsxsurfsense_local/frontend/src/features/updates/update-settings.test.tsxsurfsense_local/frontend/src/features/dashboard/dashboard-page.tsx