Request lifecycle
September 19, 2026 · View on GitHub
This document explains how an incoming request moves through the system.
Entry points
Production has two public HTTP entrypoints:
- App origin (
kody.codes) —packages/worker/src/index.ts. Remix, MCP HTTP (/mcp), OAuth, inbound email, and queue consumers. Runtime-owned paths (/@{username}/packages/…, package-invocation API, inline/apps) forward over theRUNTIME_WORKERservice binding tokody-runtime. - Package-app origin (
kody.run) —packages/worker/src/runtime-worker.ts. Zone routes on that script; the origin handler does not see this host in production.
The routing order below is the app-origin handler.
The default fetch handler delegates to OAuthProvider from
@cloudflare/workers-oauth-provider, which means OAuth endpoints and token
infrastructure are available alongside normal app routes.
Before that, GET/HEAD/OPTIONS on /.well-known/oauth-protected-resource
are handled in packages/worker/src/index.ts itself. The OAuth provider
library’s built-in handler for that path advertises resource as the request
origin only; this app’s MCP server is identified by <origin>/mcp. Serving
our own metadata on that URL keeps the RFC 8707 resource value consistent for
clients (e.g. some MCP stacks) that discover metadata from the 401
resource_metadata URL and would otherwise get invalid_target at the token
endpoint. Protected-resource metadata and MCP auth challenges resolve the origin
from the inbound request URL (via getAppBaseUrl) so clients connecting through
kody.codes or a preview/local workers.dev host get matching resource values.
Production refuses non-canonical hosts (see
Non-canonical hosts), so MCP is not served
on the production workers.dev trigger. APP_BASE_URL is only the fallback for
background work with no request URL.
Routing order
Requests are handled in this order:
-
Production canonical-host check (
packages/worker/src/app/canonical-host.ts): if the runtime is production and the request host is not theAPP_BASE_URLhost, not a package-app apex/subdomain (parsePackageAppRequestHost), and not anAPP_LEGACY_HOSTSentry, the Worker returns404{ error: 'not_found' }withCache-Control: no-store.GET /healthis exempt so deploy health checks and the status worker can probe the script on theworkers.devtrigger. Preview and local skip this check. Platform and runtime workers apply the same rule with/__platform/healthand/__runtime/healthas their probe exemptions. -
Package-app host isolation (see Hosted package app origin isolation):
- On the package-app apex, no package code runs:
/redirects to the app origin; legacy/@{username}/packages/*redirects to the owning user's subdomain; everything else is404. - On a per-user package-app subdomain (
{username}.<package-app host>), only/packages/{kodyId}/*for that hostname's username is served (plus the handoff-token exchange on those same paths)./redirects to the app origin; every other path is404. - On the app origin,
/@{username}/packages/*never executes package code: safe methods redirect to the owner's package-app subdomain with a handoff token, other methods get a307to that subdomain. - Production package-app requests fail with
500whenPACKAGE_APP_BASE_URLis missing or is not on a separate registrable domain fromAPP_BASE_URL; package code never executes inline. - In confirmed local, preview, and test runtimes, an unset package-app origin is a no-op and package apps are served inline at step 10 below.
- On the package-app apex, no package code runs:
-
Public OAuth metadata (before
OAuthProvider):- OpenID Connect discovery:
/.well-known/openid-configuration(GET/HEAD/OPTIONS) — includesrevocation_endpointat/oauth/token(same URL RFC 8414 authorization-server metadata advertises) plus matchingrevocation_endpoint_auth_methods_supported. - JWKS:
/.well-known/jwks.json(GET/HEAD/OPTIONS) - Protected resource metadata (base path only):
/.well-known/oauth-protected-resource(GET/HEAD/OPTIONS) - Client ID Metadata Document:
/oauth/client-metadata.json(GET/HEAD/OPTIONS) — Kody-as-client CIMD. Served from the request origin soclient_idmatches the fetch URL. - Official CLI CIMD:
/oauth/cli-client-metadata.json(GET/HEAD/OPTIONS) —@kodycodes/clipresents this URL asclient_id. Loopback redirect ishttp://127.0.0.1:43742/callback.
- OpenID Connect discovery:
-
OAuth authorization endpoints:
/oauth/authorize/oauth/authorize-info/oauth/callback
-
Browser noise endpoint:
/.well-known/appspecific/com.chrome.devtools.json(returns 204)
-
OAuth protected resource metadata endpoint (inside the default handler, for the
/mcpsuffix path only):/.well-known/oauth-protected-resource/mcp
-
MCP endpoint:
/mcp(requires OAuth bearer token). After authentication,packages/worker/src/mcp-auth.tsroutes by protocol era: 2025-era requests go to the sessionfulMCPDurable Object (McpAgent, MCP SDK v1) hosted onkody-platformand reached through theMCP_OBJECTbinding, and2026-07-28envelope requests are served statelessly per request bypackages/worker/src/mcp/stateless-lane.ts(MCP SDK v2, no Durable Object). Both lanes share one tool registration; every authenticated request records a lane data point to theMCP_PROTOCOL_EVENTSAnalytics Engine dataset for dual-lane traffic measurement (see decision 0005). The origin script owns no Durable Object classes (ADR 0034). MCPexecutelooks upKodyFetchGatewayonctx.exportsof the script that ownsMCP— that iskody-platform. OriginPOST /__maintenance/execute-smokeis origin-only: it uses originctx.exportsand returnsscope: "origin-only",proves: "origin-kody-fetch-gateway", andnotMcpExecute: true. A passing smoke does not prove MCP execute health. Authenticated execute evidence is the traffic-backed heartbeat plus the hourlyPOST /__maintenance/mcp-execute-healthfallback (legacy/mcpexecute with a dedicated canary token). The public card is recent for one hour after organic or synthetic success; organic traffic alone is enough. Public health and status GETs do not run that probe. A status-page snapshot older than the one-minute skip window may re-read originGET /health/componentsexecuteEvidence(never execute) so organic last-success is not one cron tick behind the live heartbeat. Persist merges with any newer timestamp written while that fetch was in flight.
-
Public
@usernameingress handled inpackages/worker/src/index.tsbefore the OAuth provider / app router (needsExecutionContextfor background work). Production forwards package-invocation and package-app paths tokody-runtimeviaRUNTIME_WORKER; webhook ingress stays on origin:POST /@{username}/api/package-invocations/:kodyId/:exportName— unadvertised bearer-token drain (see package invocation API)POST /@{username}/webhooks/:packageKodyId/:webhookName/:urlSecret— inbound package webhooks (see Inbound webhooks)- Retired
/@{username}/connectors/...paths return404. Use outbound MCP servers (kody.mcp[...]) for home automation and similar tools.
-
Static assets:
- Served from
ASSETSforGETandHEADwhen available - Matching files under
packages/worker/public/are asset-first at the edge (they do not enter this Worker list) unless listed inassets.run_worker_first
- Served from
-
Hosted package apps served inline on the app origin (
/@{username}/packages/*), only in confirmed non-production runtimes whenPACKAGE_APP_BASE_URLis unset. -
App server routes:
- Everything else is handled by
packages/worker/src/app/handler.ts - Public agent-discovery documents (Worker-first, origin-aware) include
/robots.txt,/sitemap.xml,/auth.md,/.well-known/mcp/server-card.json,/.well-known/api-catalog,/.well-known/agent-skills/index.json, skill markdown under/.well-known/agent-skills/:skillId/SKILL.md,/.well-known/security.txt, and OpenAI Apps domain verification at/.well-known/openai-apps-challenge(unauthenticatedtext/plaintoken). The homepage adds RFC 8288Linkheaders to those documents and serves markdown whenAcceptpreferstext/markdown. DNS-AID (_mcp._agents.<apex>SVCB/HTTPS) is zone DNS, not a Worker route.
Workflow runtime
All server-side Kody runtime contexts expose workflows from kody:runtime.
The helper routes every call to the shared DynamicCallableWorkflow binding;
there is no separate context-specific Workflow class.
workflows.create({ code, workflowName?, runAt, idempotencyKey, params })queues an inline ESM module and later executes it through the same module loader used byexecute. OmittedworkflowNamefalls back toinline-code.workflows.create({ exportName, packageId?, workflowName?, runAt, idempotencyKey, params })queues a saved-package export invocation. Package runtime contexts resolvepackageIdfrompackageContext; ad hoc contexts must pass it explicitly. OmittedworkflowNamefalls back to the export path.- The hub verifies saved-package ownership before queuing export-backed
workflows and records recent workflow rows for
workflowRunList.
App server flow
packages/worker/src/app/handler.ts validates environment variables and
configures session cookie signing (COOKIE_SECRET) before creating the app
router.
Authenticated HTML requests resolve the user row and RBAC roles in one D1
batch, start feature-flag evaluation (global flags + per-user overrides in a
second batch) as soon as the user id is known, and renderAppPage awaits that
already-started promise after page data instead of opening a new D1 wave.
Anonymous callers still receive registry flag defaults without touching D1.
packages/worker/src/app/router.ts maps route patterns from
packages/worker/universal/routes.ts to handler modules (home, auth, account,
session, logout, password reset, health).
Anonymous marketing HTML cache
renderAppPage sets
Cache-Control: public, max-age=60, stale-while-revalidate=300 and
Vary: Cookie for anonymous /, /pricing, /blog, /community,
/onboarding, /docs, and /docs/:slug. Origin fetch stores those
cookie-less GET/HEAD responses in the Cache API (caches.default), keyed on
the canonical origin + pathname + search and a __accept=html marker. Requests
whose Accept prefers text/markdown (prefersMarkdown) skip the store. A
matching HTML request with no kody_session cookie and no Authorization
header is served from that store (X-Kody-Cache: HIT) without running the app
handler. A miss runs the handler and, when the response is 200 text/html
with no Set-Cookie and already carries that Cache-Control, ctx.waitUntil
buffers a clone and stores it only once the body has reached </html>
(isCompleteHtmlDocument); a streamed document that stopped early (render
failure, client abort) is never shared. The stored copy drops Vary: Cookie
because Cloudflare's Cache API does not use Vary as a key (the lookup already
excludes cookie-bearing requests). Hits restore the browser-facing Vary from
the miss (Cookie, plus Accept on negotiated routes). Browser-facing
Cache-Control is unchanged. Cache-Control: no-cache on the request skips the
lookup so e2e can assert fresh HTML. Under WRANGLER_IS_LOCAL_DEV=true
(npm run dev, the Playwright web server) the store is bypassed entirely:
workerd honors stale-while-revalidate and persists caches.default under
.wrangler/state, so a stored page would keep replaying for minutes after the
edit that changed it. The workers-unit suite still exercises the store.
The public package surfaces (/@:username/:kodyId,
/@:username/:kodyId/tree/:ref/*, /@:username/:kodyId/raw/:ref/*,
/community/:id, /community/:id/files/*, /community/:id/raw/*) and their
JSON companions (/profiles/:username/packages/:kodyId.json,
/community/:id.json, .../files.json) are shared too, but with
public, max-age=60 and no stale-while-revalidate: an owner can unpublish or
make a package private and nothing purges shared caches, so a stale public
response is bounded to one minute. They are shared only when the document is a
200 (a 401 or 404 to a stranger stays no-store so making a package
public takes effect at once) and, for JSON, when the request has no session
cookie and the payload carries no viewer state. JSON companions are not stored
in the Cache API (the store is HTML-only). Anonymous /onboarding.json and
/landing-hero-videos.json use the marketing policy (the playlist payload is
also KV-cached with SWR). /docs/:slug.json is publicly cacheable without a
cookie vary (the payload is identical for every visitor). The response stays
no-store when the request carries a kody_session cookie, loadSessionInfo
resolves a session, or the response sets a cookie. Auth, OAuth, account, and
every other HTML path stay no-store.
Request context
handleRequest wraps the router in runWithRequestContext
(packages/worker/src/request-context.ts): an AsyncLocalStorage store, also
keyed by Request, holding a per-request memo and the Server-Timing entries.
memoizePerRequest dedupes lookups a page needs more than once — the package
handler, the streaming community-detail frame, and the files loader all
resolve the same loadPackagePage, and the tree page resolves the same
Artifacts HEAD from two loaders. Callers that hold the Request pass it, since
SSR frames render after the async scope has exited; repo-layer code without a
request falls back to the store. Without any context the helpers just run the
load, so unit tests and jobs are unaffected.
Page Server-Timing
renderAppPage and page JSON companions emit an HTTP Server-Timing header
with request-scoped phases ({ name, durationMs, desc? }, same helper as
execute). Cloudflare may append cfEdge / cfOrigin / cfWorker. App phases
are:
session—loadSessionInfossr— route preload, stylesheet, and starting the HTML streamhighlight— token batch (descishit,worker,miss, orfallback)listings— onboarding featured/chooser loadpackage-page—loadPackagePage(URL resolution, viewer, listing and owner detail), recorded once per request thanks to the memo; nested inside it:resolve-url,auth,listing(public listing + source row on a data-cache miss),artifacts-head(cached default-branch HEAD),owner-packageowner-readme— README read from the owner's published source snapshotfiles-route— tree URL canonicalization for/tree/:refand/filesfiles— tree snapshot read (KV, then the listing pin snapshot)
Loader phases recorded through recordServerTiming land on the request context
and are merged into the header by renderAppPage, the package JSON companions,
and frame responses.
Signed-in /onboarding derives the setup checklist from verification, inbound
MCP OAuth grants (first and second host), a Step 2 access win (memory, execute,
or saved package), and the saved-package meter. The payload also includes
featured community listings so persist-prompt copy can name an already-installed
example, plus a known memory subject or saved-package name when one exists so
Step 3 can show that artifact. The optional first-win email loop is not a
checklist item and does not run Mailbox probes on this page. See
onboarding process.
Durations use Date.now(), which Workers only advance across I/O. The weekly
site-perf collector records these phases; it does not budget them.
Syntax highlighting
Browser pages that show code — markdown bodies on guides, blog posts, and
community READMEs, onboarding MCP config snippets, and JSON dumps on account
jobs/activity — highlight with Shiki on the
kody-highlight worker. Origin loaders POST snippets over the HIGHLIGHT
service binding (POST /highlight) and attach serializable token trees to
loader/API data. The browser only paints those tokens as Remix JSX text and
inline styles — never innerHTML — so untrusted README fences stay inside the
markdown safety model. Light and dark tokens follow prefers-color-scheme via
CSS variables in packages/worker/public/styles.css. Shiki grammars are not
part of the browser bundle; missing tokens render as escaped plaintext in the
same wrapper. Both origin and the highlight worker cache token batches keyed by
highlighter version.
Client-side navigation flow
The browser app intercepts same-origin <a> clicks and same-origin form
submissions (GET/POST) and routes them in-place through the client router.
Normal app navigations stay in-place through the client router instead of
requiring a full document refresh.
Preload-then-commit
SPA navigations use a preload-then-commit model (similar to React Router
data routers): before history.pushState and the route swap, the client router
runs a registered route loader for the destination URL, fetches JSON API
data, and stores it in a single-slot preloaded navigation store. Only after the
loader finishes (or is skipped when no loader matches) does the router commit
the URL change and notify subscribers. Route components consume that payload
synchronously on first render via tryConsumeRouteLoaderData, so the UI updates
once with data already present instead of swapping routes into a loading state.
Because consumption mutates route closure state mid-render, a successful consume
also schedules one follow-up render of the consuming component (flushed in the
same microtask, before paint); values a route derived before the consume call
therefore cannot persist stale. Routes should still consume before deriving
list/detail state — the follow-up render is a safety net, not the primary
ordering contract.
Route loaders are registered in packages/worker/client/routes/index.tsx under
clientRouteLoaders, keyed by routePattern(routes.<name>). The same keying
scheme is used by clientRoutes and document-head.ts, so pathname renames
flow from packages/worker/universal/routes.ts instead of duplicated literal
strings. OAuth authorize/callback are the exception: those shells use
oauthPaths.authorize and oauthPaths.callback because the Cloudflare OAuth
provider wrapper, not routes.ts, owns those pathnames. Loaders still match
with the same Remix route-pattern specificity as clientRoutes. They return a
RouteLoaderRedirect (via routeLoaderRedirect) to abort the SPA navigation
with a full-document redirect (for example, 401 → login). The router performs
the redirect, never the loader itself, so speculative loader runs stay side-
effect free. Loader errors still commit the navigation so the destination route
can fall back to its own fetch; the router marks the destination stale
(markNavigationDataStale) so same-path refreshes — where no href change would
otherwise trigger a refetch — still reload. Hash-only changes commit immediately
without a loader. Back/forward (popstate) and same-path refreshes after form
POST also run loaders before notifying, keeping the previous UI visible until
data is ready.
Intent prefetch
Like React Router's prefetch="intent", the client router speculatively runs
the destination's route loader when the user shows intent to navigate —
mouseover, focusin, or touchstart on a same-origin link with a registered
loader (intent-prefetch.ts). A single latest-wins slot holds the speculative
run; hovering a different link aborts the previous prefetch. When the click
lands, the navigation adopts the in-flight or freshly settled prefetch instead
of starting the loader from scratch; results expire after a short TTL and
failures fall back to a normal loader run. Form POSTs abort any pending prefetch
so pre-mutation data is never shown. Opt a link out with data-prefetch="none".
Rendered lists can also warm many destinations at once (prefetchRouteHrefs).
Onboarding chips share one /onboarding.json payload; docs sidebar slugs do
not, so they pass { independent: true } and run one loader per href.
A thin top-of-viewport navigation progress bar listens for navigationstart
/ navigationend on routerEvents and appears only when a navigation is still
pending after a short delay.
The app shell also mounts scroll restoration. Each history entry's
window.scrollY is stored in sessionStorage keyed by history.state.key (the
same { [key]: y } map React Router uses). A blocking inline script in the SSR
document body restores that Y before first paint on a full document load, so a
refresh does not flash the top of the page. After hydration the restorer keeps
applying the same saved Y until the document is tall enough to reach it, then
keeps pinning that Y through a short settle window so late layout (images,
fonts, scroll anchoring) cannot persist a drifted position. User input ends the
pin early. A persist of the current scrollY does not replace a taller saved Y
while that Y is still unreachable, so a clamped early scrollTo cannot
overwrite the intended position. history.scrollRestoration is manual so the
browser does not fight the restorer. SPA back/forward still restores the saved
position, hash targets scroll into view, and new navigations go to the top after
the destination route commits. Same-document hash links are intercepted like
other same-origin links so restoration can scroll to the target. Preserve the
current scroll for a specific intercepted link or form with
data-prevent-scroll-reset, or for programmatic navigation with
navigate(to, { preventScrollReset: true }).
Full page navigations occur for:
- Explicit browser reloads/new tab loads
- Cross-origin links/forms
- Non-
_selfform targets (for example,_blank) - Explicit code paths that intentionally call
window.location.assign(...)
CORS behavior
packages/worker/src/index.ts wraps the handler with withCors
(getCorsHeaders):
- For
/mcp, anyOriginis reflected so browser-hosted remote MCP clients can connect: allowed methods areGET, HEAD, POST, DELETE, OPTIONS, allowed headers include the MCP protocol headers, andWWW-Authenticate/Mcp-Session-Idare exposed so clients can read the OAuth challenge. - For every other path, CORS headers are only added when
Originexactly matches the request origin; allowed methods areGET, POST, OPTIONSand allowed headers includecontent-typeandauthorization.
This keeps cross-origin behavior narrow while allowing same-origin browser and
API requests, plus the deliberate /mcp exception.
Observability (Sentry and Workers tracing)
The Worker default export is wrapped with Sentry.withSentry from
@sentry/cloudflare (see packages/worker/src/index.ts) so incoming fetch
requests are traced and uncaught errors can be reported when SENTRY_DSN is
configured.
The MCP (MCP / MCP_OBJECT) Durable Object is wrapped with
Sentry.instrumentDurableObjectWithSentry (see
packages/worker/src/mcp/index.ts) because it runs in a separate isolate from
the top-level Worker.
The MCP server flow adds one more Durable Object:
- The MCP client hub Durable Object terminates outbound websocket connections to user-configured MCP servers and proxies JSON-RPC/MCP requests over those sockets.
The runtime capability registry merges synthesized domains from enabled MCP client servers. See MCP client servers.
Shared options are built in packages/worker/src/sentry-options.ts: release
comes from APP_COMMIT_SHA when set (deploy workflows pass it as a var), and
environment defaults from SENTRY_ENVIRONMENT in
packages/worker/wrangler.jsonc per deploy target.
MCP tools emit structured mcp-event logs via
packages/worker/src/mcp/observability.ts. On failures, the same module sends
Sentry events at error severity (with MCP tags and context). Failures the
caller can clear from the message alone — McpCallerError,
failurePhase: 'parse_input', or an explicit callerError payload flag,
including sandbox user-code failures — stay on the structured mcp-event log
line and never reach Sentry; genuine platform failures still do. New "not found
/ bad argument" throw sites in packages/worker/src/mcp/** should use
McpCallerError.
search also publishes exclusive wall-clock tiles (usernameLookupMs,
identityResolutionMs, loadAndRankMs, suffix stamps, waitingItemsMs,
formattingMs) plus exclusiveMs / unaccountedMs so named phases reconcile
against timing.durationMs. Overlapping detail (memory, retrievers, candidate
plugins) stays beside those tiles and is not summed. Each call writes one
privacy-safe point to MCP_SEARCH_EVENTS (see
Usage metering — MCP search duration).
Operators asking "is search slow this hour?" query that dataset; UWD retriever
surface attribution is unchanged and does not measure search latency.
/admin/insights has no search-latency chart. The same page does show the
onboarding funnel (ONBOARDING_FUNNEL_EVENTS): unique users per stage for the
last 7 and 28 days. See
Usage metering — Onboarding funnel.
Authentication and authorization denials are deliberately not
McpCallerError. /mcp rejects anonymous callers at the transport before any
capability runs, so a denial reaching a handler means an internal caller built a
context without a user — a defect worth an issue. Attack visibility for these
lives in the audit log rather than the error stream; see
Security.
Workers native tracing (OpenTelemetry)
packages/worker/wrangler.jsonc also enables
Workers automatic tracing
(observability.traces.enabled, beta). The runtime emits OTel-standard spans
for handler invocations, outbound fetches, and binding calls (D1, KV, R2,
Durable Objects, queues) with no SDK in the bundle; traces appear in the Workers
Observability dashboard next to Workers Logs, and console.* output inside a
span is attributed to that span. Custom spans are available via
tracing.enterSpan() from cloudflare:workers when application-level spans are
worth adding. App-level context rides on two hooks: every metered usage event
emits a kody.usage.{eventType} child span with kody.user_id and entity
attributes (see usage-metering.md), and Sentry error
events carry the signed-in user id (id only, no PII) via Sentry.setUser in the
app auth resolver and the MCP failure reporter.
Production deploys additionally export these traces to Sentry through the
account-level sentry-otlp-traces destination; preview and test deploys inherit
the top-level block without a destination, so their spans stay in the Cloudflare
dashboard. The destination itself (endpoint, auth header, fork provisioning) is
documented in setup-manifest.md, and the
SENTRY_TRACES_SAMPLE_RATE handling (production pins it to 0 to avoid
duplicate SDK traces) in
environment-variables.md.
Billing note: each span is one observability event sharing the Workers Logs
quota (10M events/month included on Workers Paid). Sampling is controlled by
observability.traces.head_sampling_rate (defaults to full sampling, fine at
current traffic).
Browser errors and session replay
The client bundle initializes @sentry/browser from a kody:sentry meta tag
that ssr-document.tsx renders when SENTRY_DSN is configured (the DSN is a
publishable client key), except under WRANGLER_IS_LOCAL_DEV so Vite HMR noise
does not report as production. Capture is errors plus error-only Session
Replay: replaysSessionSampleRate is 0 and replaysOnErrorSampleRate is
1, so nothing is recorded to Sentry unless an error occurs, and replays mask
all text and block all media (packages/worker/client/sentry-client.ts) because
Kody sessions contain personal content. Envelopes are sent through the
same-origin POST /sentry-tunnel route
(packages/worker/src/app/handlers/sentry-tunnel.ts), which only forwards
envelopes whose DSN matches the Worker's own SENTRY_DSN — this keeps the
first-party CSP at connect-src 'self'. Because that DSN is public, the route
is also rate limited per IP before it buffers a body (see
security.md). The CSP allows worker-src blob: for the replay
compression Web Worker.
Source maps
packages/worker/wrangler.jsonc sets
upload_source_maps,
and production deploys pass
--outdir .wrangler/sentry-bundle --upload-source-maps so the bundle + maps are
generated consistently. Wrangler resolves that --outdir against the config
file's directory, so platform and runtime maps land under
packages/platform-worker/.wrangler/sentry-bundle and
packages/runtime-worker/.wrangler/sentry-bundle. Origin maps come from
vite build (dist/ssr plus dist/client). npm run deploy from a laptop is
origin-only.
To symbolicate stack traces in Sentry (not only in Cloudflare), configure
Cloudflare source maps in Sentry:
add GitHub repository variables SENTRY_ORG and SENTRY_PROJECT, a
SENTRY_AUTH_TOKEN secret with release upload scopes, then CI runs
npm run sentry:upload-sourcemaps after deploy using the same release as
APP_COMMIT_SHA. Sibling-only deploys upload the worker bundles that exist and
skip client maps when origin Vite did not run.