refactor(worker): Phase 4 — split hono-app.ts into domain route modules, remove bare-path double-mount#1443
Conversation
- Extract all route handlers from hono-app.ts (1279 lines) into 10 domain-scoped
route files under worker/routes/
- Remove app.route('/', routes) bare-path double-mount — /api is canonical
- Add scripts/lint-route-order.ts CI guard for route ordering invariants
- Add lint:routes task to deno.json and CI step to ci.yml
- Update docs/architecture/hono-routing.md with Phase 4 section
- Update hono-app.test.ts bare-path tests to use /api/ prefix
Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/86d24908-49bb-4286-87b5-ee18aedaa606
Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Refactors the Worker’s Hono application by extracting route domains into dedicated worker/routes/* modules, removing the legacy bare-path (/) double-mount so /api becomes the single canonical base path, and adding a CI guard to prevent middleware/route-order regressions.
Changes:
- Split route handlers into domain-scoped
OpenAPIHonosub-app modules underworker/routes/and mount them fromworker/hono-app.ts. - Remove
app.route('/', routes)so only/api/*routes are served; update Worker tests and routing docs accordingly. - Add
scripts/lint-route-order.tsand wire it intodeno.jsontasks and CI.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| worker/routes/shared.ts | New shared Variables/AppContext and shared helpers used by route modules. |
| worker/routes/compile.routes.ts | New compile/validate/WS routes module extracted from hono-app.ts. |
| worker/routes/rules.routes.ts | New rules CRUD routes module. |
| worker/routes/queue.routes.ts | New lazy-loaded queue routes module. |
| worker/routes/configuration.routes.ts | New configuration routes module (defaults/validate/resolve). |
| worker/routes/admin.routes.ts | New admin routes module + extracted session revocation handler. |
| worker/routes/monitoring.routes.ts | New health/metrics/container status routes module. |
| worker/routes/api-keys.routes.ts | New API key management routes module. |
| worker/routes/webhook.routes.ts | New webhook notification route module. |
| worker/routes/workflow.routes.ts | New lazy-loaded workflow routes module. |
| worker/routes/browser.routes.ts | Stub module for future browser routes. |
| worker/routes/index.ts | Barrel exports for route modules and shared helpers/types. |
| worker/hono-app.ts | Trimmed app setup; mounts extracted route modules; removes bare-path double-mount. |
| worker/hono-app.test.ts | Updates tests to use /api/* prefix and removes redundant bare-path cases. |
| scripts/lint-route-order.ts | Adds CI lint enforcing key route/middleware ordering invariants. |
| docs/architecture/hono-routing.md | Documents new module layout and /api-only mount strategy. |
| deno.json | Adds lint:routes task. |
| .github/workflows/ci.yml | Adds route-order lint step to CI. |
| deno.lock | Updates lockfile for new script dependency resolution. |
Comments suppressed due to low confidence (1)
worker/hono-app.ts:99
monitoringRoutesregisters/container/status(served as/api/container/status), butMONITORING_API_PATHS(and thereforePRE_AUTH_PATHS) doesn’t include/api/container/status. That means this endpoint won’t get the intended pre-auth bypass path and will go through the full unified-auth chain on every poll, despite being Anonymous-tier inROUTE_PERMISSION_REGISTRY. Consider adding/api/container/statustoMONITORING_API_PATHS(or otherwise including it in the pre-auth bypass set).
// Dashboard monitoring endpoints — read-only, no PII, publicly accessible by design.
const MONITORING_API_PATHS = [
'/api/health',
'/api/health/latest',
'/api/health/db-smoke',
'/api/metrics',
] as const;
|
@copilot apply changes based on the comments in this thread |
…restore cache middleware
- compile.routes.ts:
- /ast/parse, /validate: move turnstileMiddleware() BEFORE zValidator() so
it reads from the unconsumed body clone; update handlers to use
buildSyntheticRequest(c, c.req.valid('json')) since zValidator now consumes
the body after Turnstile runs
- /compile/async, /compile/batch/async: replace turnstileMiddleware() with
verifyTurnstileInline() pattern (reads turnstileToken from c.req.valid('json'))
and use buildSyntheticRequest to avoid re-reading consumed stream
- /compile/container: same inline Turnstile fix; replace c.req.raw.body with
JSON.stringify(c.req.valid('json')) since body is consumed by zValidator
- configuration.routes.ts:
- /configuration/resolve: replace turnstileMiddleware() with verifyTurnstileInline()
+ buildSyntheticRequest; remove now-unused turnstileMiddleware import
- hono-app.ts: restore cache() middleware on /api/version and /api/schemas
Agent-Logs-Url: https://github.com/jaypatrick/adblock-compiler/sessions/70cde4c8-0bba-4bdc-99ee-e96fd7d5300e
Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Applied all review feedback in 95eb0eb:
|
|
@copilot Please resolve the merge conflict and be aware of PR 1444 which will be merged after this PR. |
- Take origin/main version for Better Auth wildcard handler: adds (c, next) signature and explicit pass-through for /api/auth/providers so the custom endpoint receives full CORS + rate-limiting middleware coverage - Take origin/main version for /api/auth/providers registration comment (explains the post-meta-section placement and wildcard pass-through) Co-authored-by: jaypatrick <1800595+jaypatrick@users.noreply.github.com>
Merge conflict resolved in 86b3706. The only conflicting file was
All 1189 tests pass, lint and fmt clean. |
| '/admin/neon/query', | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', AdminNeonQuerySchema as any, zodValidationError), | ||
| (c) => handleAdminNeonQuery(c.req.raw, c.env, c.get('authContext')), |
There was a problem hiding this comment.
handleAdminNeonQuery() calls request.json() internally, but this route runs zValidator('json', ...) before invoking it and passes c.req.raw. If zValidator consumes the body stream (as documented in this repo), the handler can see an empty/consumed body and fail.
Fix by removing zValidator or by passing a synthetic Request rebuilt from c.req.valid('json').
| (c) => handleAdminNeonQuery(c.req.raw, c.env, c.get('authContext')), | |
| (c) => { | |
| const body = c.req.valid('json'); | |
| const reqWithBody = new Request(c.req.raw, { | |
| body: JSON.stringify(body), | |
| }); | |
| return handleAdminNeonQuery(reqWithBody, c.env, c.get('authContext')); | |
| }, |
| // Strip single-line and multi-line comments so we don't accidentally match | ||
| // commented-out code. This is intentionally lightweight — it does not handle | ||
| // every edge case, but is sufficient for the ordered-invariant checks below. | ||
| const srcNoComments = src | ||
| .replace(/\/\*[\s\S]*?\*\//g, '') // block comments | ||
| .replace(/\/\/.*/g, ''); // line comments |
There was a problem hiding this comment.
The lint script removes //.* to strip comments, but that also matches // inside string literals (e.g. https://... URLs in hono-app.ts). This can corrupt srcNoComments and make the invariant checks unreliable or flaky as the file evolves.
Consider using a small tokenizer/AST-based approach (e.g. deno_ast/TypeScript compiler API) or a safer comment stripper that ignores strings/template literals.
| // Strip single-line and multi-line comments so we don't accidentally match | |
| // commented-out code. This is intentionally lightweight — it does not handle | |
| // every edge case, but is sufficient for the ordered-invariant checks below. | |
| const srcNoComments = src | |
| .replace(/\/\*[\s\S]*?\*\//g, '') // block comments | |
| .replace(/\/\/.*/g, ''); // line comments | |
| /** | |
| * Strip single-line and multi-line comments in a way that preserves strings | |
| * and template literals, so that `//` inside literals (e.g. URLs) do not get | |
| * mistaken for comments. | |
| */ | |
| function stripComments(source: string): string { | |
| let result = ''; | |
| let inSingleQuote = false; | |
| let inDoubleQuote = false; | |
| let inTemplate = false; | |
| let inLineComment = false; | |
| let inBlockComment = false; | |
| let prevChar = ''; | |
| for (let i = 0; i < source.length; i++) { | |
| const char = source[i]!; | |
| const nextChar = i + 1 < source.length ? source[i + 1]! : ''; | |
| // Handle end of line comment | |
| if (inLineComment) { | |
| if (char === '\n') { | |
| inLineComment = false; | |
| result += char; | |
| } | |
| continue; | |
| } | |
| // Handle end of block comment | |
| if (inBlockComment) { | |
| if (char === '*' && nextChar === '/') { | |
| inBlockComment = false; | |
| i++; // Skip '/' | |
| } | |
| continue; | |
| } | |
| // Inside a template literal | |
| if (inTemplate) { | |
| result += char; | |
| if (char === '`' && prevChar !== '\\') { | |
| inTemplate = false; | |
| } | |
| prevChar = char === '\\' && prevChar === '\\' ? '' : char; | |
| continue; | |
| } | |
| // Inside a single-quoted string | |
| if (inSingleQuote) { | |
| result += char; | |
| if (char === "'" && prevChar !== '\\') { | |
| inSingleQuote = false; | |
| } | |
| prevChar = char === '\\' && prevChar === '\\' ? '' : char; | |
| continue; | |
| } | |
| // Inside a double-quoted string | |
| if (inDoubleQuote) { | |
| result += char; | |
| if (char === '"' && prevChar !== '\\') { | |
| inDoubleQuote = false; | |
| } | |
| prevChar = char === '\\' && prevChar === '\\' ? '' : char; | |
| continue; | |
| } | |
| // Not currently in string/template/comment: check for start of comment or string | |
| if (char === '/' && nextChar === '/') { | |
| inLineComment = true; | |
| i++; // Skip second '/' | |
| continue; | |
| } | |
| if (char === '/' && nextChar === '*') { | |
| inBlockComment = true; | |
| i++; // Skip '*' | |
| continue; | |
| } | |
| if (char === "'") { | |
| inSingleQuote = true; | |
| result += char; | |
| prevChar = char; | |
| continue; | |
| } | |
| if (char === '"') { | |
| inDoubleQuote = true; | |
| result += char; | |
| prevChar = char; | |
| continue; | |
| } | |
| if (char === '`') { | |
| inTemplate = true; | |
| result += char; | |
| prevChar = char; | |
| continue; | |
| } | |
| // Regular character | |
| result += char; | |
| prevChar = char === '\\' && prevChar === '\\' ? '' : char; | |
| } | |
| return result; | |
| } | |
| // Strip single-line and multi-line comments so we don't accidentally match | |
| // commented-out code while preserving strings and template literals. | |
| const srcNoComments = stripComments(src); |
| rulesRoutes.post( | ||
| '/rules', | ||
| requireAuthMiddleware(), | ||
| bodySizeMiddleware(), | ||
| rateLimitMiddleware(), | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', RuleSetCreateSchema as any, zodValidationError), | ||
| (c) => handleRulesCreate(c.req.raw, c.env), | ||
| ); |
There was a problem hiding this comment.
This route uses zValidator('json', ...) but then forwards c.req.raw to handleRulesCreate(), which reads request.json() internally. Per the routing docs in this repo, zValidator consumes the original body stream, so the handler may see an empty/consumed body and fail.
Fix by removing zValidator (handler already validates) or by rebuilding a synthetic Request from c.req.valid('json') before calling the handler.
| rulesRoutes.put( | ||
| '/rules/:id', | ||
| requireAuthMiddleware(), | ||
| bodySizeMiddleware(), | ||
| rateLimitMiddleware(), | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', RuleSetUpdateSchema as any, zodValidationError), | ||
| (c) => handleRulesUpdate(c.req.param('id')!, c.req.raw, c.env), | ||
| ); |
There was a problem hiding this comment.
Same body-consumption issue as POST /rules: zValidator('json', ...) runs before calling handleRulesUpdate(), and that handler reads request.json() internally. Passing c.req.raw after zValidator can cause the handler to fail on a consumed body.
Fix by removing zValidator or passing a synthetic Request built from c.req.valid('json').
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', WebhookNotifyRequestSchema as any, zodValidationError), | ||
| (c) => handleNotify(c.req.raw, c.env), | ||
| ); |
There was a problem hiding this comment.
This route runs zValidator('json', WebhookNotifyRequestSchema, ...) and then calls handleNotify(c.req.raw, ...), but handleNotify() reads request.json() internally. If zValidator consumes the body stream (as documented in this repo), the handler may fail to parse JSON.
Fix by dropping zValidator (handler already validates) or by creating a synthetic Request from c.req.valid('json') and passing that to handleNotify.
| // deno-lint-ignore no-explicit-any | |
| zValidator('json', WebhookNotifyRequestSchema as any, zodValidationError), | |
| (c) => handleNotify(c.req.raw, c.env), | |
| ); | |
| (c) => handleNotify(c.req.raw, c.env), | |
| ); |
| adminRoutes.post( | ||
| '/admin/neon/branches', | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', AdminNeonCreateBranchSchema as any, zodValidationError), | ||
| (c) => handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')), |
There was a problem hiding this comment.
handleAdminNeonCreateBranch() reads request.json() internally, but this route runs zValidator('json', ...) first and then passes c.req.raw to the handler. Per the routing docs in this repo, zValidator consumes the original body stream, so the handler can fail to parse the body.
Fix by removing zValidator (handler already validates) or by passing a synthetic Request rebuilt from c.req.valid('json').
| adminRoutes.post( | |
| '/admin/neon/branches', | |
| // deno-lint-ignore no-explicit-any | |
| zValidator('json', AdminNeonCreateBranchSchema as any, zodValidationError), | |
| (c) => handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')), | |
| adminRoutes.post('/admin/neon/branches', (c) => | |
| handleAdminNeonCreateBranch(c.req.raw, c.env, c.get('authContext')), |
| rateLimitMiddleware(), | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', ValidateRuleRequestSchema as any, zodValidationError), | ||
| (c) => handleValidateRule(c.req.raw, c.env), |
There was a problem hiding this comment.
zValidator() runs before the handler, and the documentation for this project states it consumes the original request body stream. handleValidateRule() then calls request.json() internally, so passing c.req.raw here can result in an empty/consumed body and a 400/failed validation.
Fix by either (a) removing zValidator for this route (the handler already validates with Zod), or (b) passing a synthetic Request built from c.req.valid('json') (as done for other compile routes).
| (c) => handleValidateRule(c.req.raw, c.env), | |
| (c) => handleValidateRule(buildSyntheticRequest(c, c.req.valid('json')), c.env), |
| apiKeysRoutes.post( | ||
| '/keys', | ||
| requireAuthMiddleware(), | ||
| rateLimitMiddleware(), | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', CreateApiKeyRequestSchema as any, zodValidationError), | ||
| async (c) => { | ||
| if (!INTERACTIVE_AUTH_METHODS.has(c.get('authContext').authMethod)) return JsonResponse.forbidden('API key management requires an authenticated user session'); | ||
| if (!c.env.HYPERDRIVE) return JsonResponse.serviceUnavailable('Database not configured'); | ||
| return handleCreateApiKey(c.req.raw, c.get('authContext'), c.env.HYPERDRIVE.connectionString, createPgPool); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
zValidator('json', CreateApiKeyRequestSchema, ...) runs before calling handleCreateApiKey(c.req.raw, ...), but handleCreateApiKey() reads request.json() internally. With the documented zValidator body consumption semantics, passing c.req.raw risks a consumed/empty body.
Fix by removing zValidator for this route or by passing a synthetic Request rebuilt from c.req.valid('json').
| apiKeysRoutes.patch( | ||
| '/keys/:id', | ||
| requireAuthMiddleware(), | ||
| rateLimitMiddleware(), | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', UpdateApiKeyRequestSchema as any, zodValidationError), | ||
| async (c) => { | ||
| if (!INTERACTIVE_AUTH_METHODS.has(c.get('authContext').authMethod)) return JsonResponse.forbidden('API key management requires an authenticated user session'); | ||
| if (!c.env.HYPERDRIVE) return JsonResponse.serviceUnavailable('Database not configured'); | ||
| return handleUpdateApiKey(c.req.param('id')!, c.req.raw, c.get('authContext'), c.env.HYPERDRIVE.connectionString, createPgPool); | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Same issue as POST /keys: this route runs zValidator('json', UpdateApiKeyRequestSchema, ...) and then calls handleUpdateApiKey(..., c.req.raw, ...), but the handler reads request.json() internally. If zValidator consumes the body stream, the handler can fail.
Fix by removing zValidator or by rebuilding a synthetic Request from c.req.valid('json') and passing that to the handler.
| '/admin/users/:id/ban', | ||
| // deno-lint-ignore no-explicit-any | ||
| zValidator('json', AdminBanUserSchema as any, zodValidationError), | ||
| (c) => handleAdminBanUser(c.req.raw, c.env, c.get('authContext'), c.req.param('id')!), |
There was a problem hiding this comment.
This route runs zValidator('json', AdminBanUserSchema, ...) and then calls handleAdminBanUser(c.req.raw, ...), but handleAdminBanUser() reads the request body (request.text() / JSON.parse). If zValidator consumes the body stream, the handler will see an empty body and fail.
Fix by removing zValidator for this route or by passing a synthetic Request rebuilt from c.req.valid('json').
| (c) => handleAdminBanUser(c.req.raw, c.env, c.get('authContext'), c.req.param('id')!), | |
| (c) => { | |
| const validatedBody = c.req.valid('json'); | |
| const syntheticRequest = new Request(c.req.raw, { | |
| body: JSON.stringify(validatedBody), | |
| headers: c.req.raw.headers, | |
| }); | |
| return handleAdminBanUser(syntheticRequest, c.env, c.get('authContext'), c.req.param('id')!); | |
| }, |
worker/hono-app.tswas a 1,279-line monolith mixing app setup, middleware, and every route domain inline. This PR extracts all route handlers into 10 domain-scoped sub-apps underworker/routes/, removes the legacyapp.route('/', routes)bare-path double-mount, adds a CI guard preventing future route-ordering regressions, corrects Turnstile/zValidatormiddleware ordering issues in the extracted route modules, and merges the/api/auth/providers404 fix frommain.Changes
worker/routes/— new domain route modulesshared.tsAppContexttype +zodValidationError,verifyTurnstileInline,buildSyntheticRequest(extracted to avoid circular imports with hono-app.ts)compile.routes.ts/compile/*,/validate,/ast/parse,/ws/compile,/validate-rulerules.routes.ts/rules/*queue.routes.ts/queue/*configuration.routes.ts/configuration/*admin.routes.ts/admin/*(users, neon, agents, auth-config, usage, storage)monitoring.routes.ts/health/*,/metrics/*,/container/statusapi-keys.routes.ts/keys/*webhook.routes.ts/notifyworkflow.routes.ts/workflow/*browser.routes.tsindex.tsworker/hono-app.tstrimmed from 1,279 → ~547 lines; now app setup + global middleware only; route modules mounted via:Removed
app.route('/', routes)—/apiis the sole canonical base path.MONITORING_BARE_PATHSconstant retained (used by auth middleware for legacy request matching).Corrected Turnstile/
zValidatormiddleware ordering acrosscompile.routes.tsandconfiguration.routes.ts:/ast/parse,/validate— schemas have noturnstileTokenfield:turnstileMiddleware()moved beforezValidator()so it reads from the unconsumed body clone; handlers updated to usebuildSyntheticRequest(c, c.req.valid('json'))./compile/async,/compile/batch/async,/compile/container— schemas includeturnstileToken: replacedturnstileMiddleware()withverifyTurnstileInline()reading fromc.req.valid('json'); handlers usebuildSyntheticRequest./compile/containerbody forwarding switched fromc.req.raw.bodytoJSON.stringify(c.req.valid('json'))sincezValidatorconsumes the stream./configuration/resolve— same inline Turnstile fix;turnstileMiddlewareimport removed fromconfiguration.routes.ts.Restored
cache()middleware onGET /api/versionandGET /api/schemasinhono-app.ts(was inadvertently dropped during extraction).scripts/lint-route-order.ts— Deno script run in CI that enforces four invariants:timing()is the firstapp.use()call/api/auth/*handler registered beforeagentRouterapp.route('/', routes)double-mount is absentNO_COMPRESS_PATHSexclusion (not a bare wildcard)deno.json— addedlint:routestask (deno run --allow-read scripts/lint-route-order.ts).github/workflows/ci.yml—Route-order lintstep added tolint-formatjobdocs/architecture/hono-routing.md— Phase 4 section: new file layout, mount strategy, CI guard description;/api Prefix Handlingsection updated to document double-mount removalworker/hono-app.test.ts— bare-path tests (e.g.GET /health,POST /compile) updated to use/api/prefix; duplicate bare-path test cases removed where/api/counterparts already existedMerge conflict resolved — merged
origin/main(PR fix: resolve /api/auth/providers HTTP 404 by adding pass-through in Better Auth wildcard #1442/api/auth/providers404 fix): Better Auth wildcard handler updated toasync (c, next)signature with an explicitif (c.req.path === '/api/auth/providers') return next()pass-through;/api/auth/providersregistration moved to after the meta-routes section with a full explanatory comment.Testing
/api/prefix; no net test removal (1,189 passed)deno task lint:routesall 4 checks pass;deno lint+deno fmt --checkcleanZero Trust Architecture Checklist
Worker / Backend
*) on write/authenticated endpoints — N/A (CORS middleware unchanged)[vars]) — N/A (no secret access added)zValidatorcalls moved verbatim; Turnstile/zValidatorordering corrected so body is never double-read or consumed before verification.prepare().bind()(no string interpolation) — N/A (no queries added)Frontend / Angular
CanActivateFnauth guards — N/AlocalStorage) — N/AOriginal prompt
PR A — Structural Refactor (must merge before PR B / tRPC)
This PR performs a pure structural refactor of
worker/hono-app.tswith zero behaviour changes. It is a prerequisite for the tRPC integration PR.Background
worker/hono-app.tsis a ~1,300-line monolith that currently:routessub-app at both/apiand/(app.route('/api', routes); app.route('/', routes)) — the bare/mount is legacy and should be removedGoals
hono-app.tsinto domain-scoped route files — one HonoOpenAPIHonosub-app per domain, mounted cleanly inhono-app.tsapp.route('/', routes)bare-path double-mount — keep onlyapp.route('/api', routes)docs/architecture/hono-routing.mdto reflect the new file layoutRequired file layout after this PR
Each route file exports a single
OpenAPIHono<{ Bindings: Env; Variables: Variables }>instance.Domain → handler mapping
Pull each domain's routes out of
hono-app.tsexactly as they are (no logic changes). The mapping is:compile.routes.tshandleCompileJson,handleCompileStream,handleCompileBatch,handleCompileAsync,handleCompileBatchAsync,handleValidate,handleASTParseRequest,handleValidateRulerules.routes.tshandleRulesList,handleRulesGet,handleRulesCreate,handleRulesUpdate,handleRulesDeletequeue.routes.tsroutes.all('/queue/*', ...)handlerconfiguration.routes.tshandleConfigurationDefaults,handleConfigurationResolve,handleConfigurationValidateadmin.routes.ts/admin/pg/*(pg-admin),/admin/migrate/*(migrate), and/admin/containers/*(container-status) routesmonitoring.routes.ts/health,/health/latest,/health/db-smoke,/metrics,/metrics/prometheus— these MUST keep theNO_COMPRESS_PATHSexclusion andres.text()body materialisation from the current main branchapi-keys.routes.tshandleCreateApiKey,handleListApiKeys,handleRevokeApiKey,handleUpdateApiKeywebhook.routes.tshandleNotifyworkflow.routes.tsroutes.all('/workflow/*', ...)handlerbrowser.routes.tshandleMonitorLatest,handleResolveUrl,handleSourceMonitorUpdated
hono-app.tsafter extractionThe trimmed
hono-app.tsshould only contain:/api,/api/version,/api/schemas,/api/deployments,/api/turnstile-config,/api/sentry-config,/api/auth/providers) — unchangedroutessub-app declaration + its middleware (logger, compress-with-exclusions, ZTA permission check)app.route('/api', routes);— removeapp.route('/', routes);handleRequestexport at the bottom — unchangedCI...
This pull request was created from Copilot chat.
⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.