Skip to content

refactor(server): extract scripts, data-exchange & server-info route groups (Refs #3502)#3526

Merged
Yeraze merged 1 commit into
mainfrom
fix/3502-batch-b
Jun 17, 2026
Merged

refactor(server): extract scripts, data-exchange & server-info route groups (Refs #3502)#3526
Yeraze merged 1 commit into
mainfrom
fix/3502-batch-b

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Continues the server.ts route-extraction effort (Refs #3502) by moving the next cohesive, low-coupling batch of inline apiRouter.* handlers into src/server/routes/*Routes.ts modules. Behaviour-preserving: handler bodies moved verbatim, middleware and path strings unchanged.

Groups extracted (11 handlers)

Group Handlers New module
scripts /scripts/test, /http/test, /scripts/import, /scripts/export, /scripts/dependencies, /scripts/dependencies/install, DELETE /scripts/:filename (7) scriptRoutes.ts
data exchange /stats, /export, /import (3) dataExchangeRoutes.ts
server info /server-info (1) serverInfoRoutes.ts

The 6 server.ts-local script helpers (getScriptsDirectory, resolveScriptPath, sanitizeMetadataValue, parseScriptMetadata, getLanguageFromExtension, scriptsEndpoint) moved into scriptRoutes.ts. getScriptsDirectory and the public scriptsEndpoint are exported so server.ts can keep the dev-startup log and the app-level /api/scripts mount (which intentionally bypasses CSRF — unchanged). The __dirname-relative dev scripts-dir path was adjusted (../../../../../) to resolve to the same project root from the deeper module location.

All three new routers are mounted at apiRouter.use('/', ...) (verbatim-path style) so every route resolves byte-identically under /api. Removed now-orphaned imports from server.ts (scriptDependencyEnv, normalizeTriggerPatterns, safeFetch, SsrfBlockedError, getDependencyStatus, installDependencies).

Inline handler count

  • Before: 109
  • After: 98

What remains

Heavier-coupled Phase-3 groups still inline: /config (15, config marshalling registry), /channels (11, channel-move helpers), /nodes (17, getEffectivePosition + node enrichment), /admin (17, session-passkey/admin-command), /settings/* (extend settingsRoutes.ts), /system + /version/check (upgrade/Docker/version helpers). Also /debug/ip (couples to app/env and is dev-only) and /messages/send + /poll + /config (deep manager/resolveSourceConnectionConfig coupling) were left for a later batch.

Tests

Added scriptRoutes.test.ts, dataExchangeRoutes.test.ts, serverInfoRoutes.test.ts covering validation and happy paths.

Verification

  • Full Vitest suite: success, 6740 passed, 0 failed, 0 failed suites
  • tsc --noEmit: clean
  • eslint on changed files: 0 errors (only pre-existing no-explicit-any carryover warnings); server.ts error count dropped 18 → 16

🤖 Generated with Claude Code

…groups (Refs #3502)

Continue the server.ts route-extraction effort (#3502) by moving the next
cohesive, low-coupling batch of inline handlers into routes/ modules.

Groups extracted (11 inline handlers, 109 -> 98 remaining):
- scripts (7): /scripts/test, /http/test, /scripts/import, /scripts/export,
  /scripts/dependencies, /scripts/dependencies/install, DELETE /scripts/:filename
  -> new scriptRoutes.ts. The 6 server.ts-local script helpers
  (getScriptsDirectory, resolveScriptPath, sanitizeMetadataValue,
  parseScriptMetadata, getLanguageFromExtension, scriptsEndpoint) moved into
  the module. getScriptsDirectory and the public scriptsEndpoint are exported;
  server.ts re-imports them for the dev-startup log and the app-level
  /api/scripts mount (which intentionally bypasses CSRF, unchanged).
- data exchange (3): /stats, /export, /import -> new dataExchangeRoutes.ts.
- server info (1): /server-info -> new serverInfoRoutes.ts.

All handler bodies moved verbatim; auth/validation middleware
(requirePermission, requireAdmin, express.raw) and path strings unchanged, so
every route resolves byte-identically under /api. Removed now-orphaned imports
(scriptDependencyEnv, normalizeTriggerPatterns, safeFetch, SsrfBlockedError,
getDependencyStatus, installDependencies) from server.ts.

Added scriptRoutes.test.ts, dataExchangeRoutes.test.ts and
serverInfoRoutes.test.ts covering validation and happy paths.

Verification: full Vitest suite green (6740 passed, 0 failed, 0 failed
suites), tsc clean, eslint clean on all changed files (0 new errors; server.ts
error count dropped 18 -> 16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: refactor(server): extract scripts, data-exchange & server-info route groups

Tasks:

  • Gather context and understand request
  • Read changed files
  • Review code quality and best practices
  • Check for bugs/security issues
  • Evaluate test coverage
  • Post final review

Overall Assessment

This is a clean, behaviour-preserving refactor that follows the pattern established by earlier batches in this series. The extraction is mechanically sound and the handler bodies are moved verbatim. The three test files provide good coverage of the validation paths. A few small observations below.


Code Quality & Best Practices

scriptRoutes.ts

  1. Module-level side effect on import (scriptRoutes.ts:16): getEnvironmentConfig() is called at the top level, outside any function. This is a pattern already used elsewhere in the codebase, but it means the config is baked in at module load time and can't be replaced by test mocks applied after the module is first imported. The test file works around this with vi.hoisted / top-level vi.mock, so it's not broken, but it's worth noting.

  2. Duplicate directory-existence check in scriptsEndpoint (scriptRoutes.ts:160-163): getScriptsDirectory() already creates the directory if it doesn't exist (scriptRoutes.ts:35-38), so the existsSync guard inside scriptsEndpoint can never be true. This was present before the refactor so it's pre-existing tech debt, but worth flagging.

  3. interface ParamSpec declared inside a loop body (scriptRoutes.ts:312-315): The interface is re-declared on every iteration of the pattern-matching loop. TypeScript erases interfaces at compile time so this is harmless, but it's unusual and could be hoisted to the module level or removed in favour of an inline type.

  4. Dynamic import() for child_process / util (scriptRoutes.ts:473-475): These are Node built-ins and will always be available. The dynamic imports are carried over from the original code but add unnecessary async overhead; static import at the top of the file would be cleaner.

  5. scriptsEndpoint typed with any (scriptRoutes.ts:156): (_req: any, res: any). The surrounding handlers use the typed Request/Response from the existing imports — this one should too.

dataExchangeRoutes.ts — looks clean. No issues.

serverInfoRoutes.ts — looks clean. No issues.


Potential Bugs

  1. __dirname path depth comment vs. reality (scriptRoutes.ts:27-29): The comment says "project root is three levels up (../../../) vs. two from server.ts" and the code uses ../../../. At runtime __dirname for src/server/routes/scriptRoutes.ts (after compile to dist/server/routes/) resolves to dist/server/routes/, so path.resolve(__dirname, '../../../') → project root. This appears correct, but the comment says "vs. two from server.ts" — server.ts lives in src/server/ (compiled to dist/server/) so it only needs ../../. The description in the PR matches this. ✅ No bug, just worth double-checking the dev path actually resolves to the same data/scripts as it did from server.ts.

  2. AbortController created but signal not always cleared (scriptRoutes.ts:703-704): In the /http/test handler, timeoutId is cleared in the success and fetch-error paths, but the AbortController itself is not checked. If safeFetch throws a non-AbortError synchronously before clearTimeout, the timeout will still fire and abort nothing. This is an existing behaviour but is worth noting.

  3. scripts/exportscripts.length logged, not the actual count written (scriptRoutes.ts:850): After filtering out missing/invalid paths, archive.finalize() is called and then logger.info(\✅ Exported ${scripts.length} script(s)`)` logs the requested count, not the count of files actually added to the archive. Pre-existing behaviour.


Security

  1. Path traversal in DELETE /scripts/:filename (scriptRoutes.ts:888): path.basename(filename) correctly strips path separators, and there's no further startsWith containment check like the one in /scripts/import (scriptRoutes.ts:787-789). For a DELETE, the impact is limited because getScriptsDirectory() + path.join(scriptsDir, sanitizedFilename) can't escape the directory after path.basename, but adding the same defence-in-depth check as import for consistency would be tidy.

  2. req.body spread into process.env-derived object (scriptRoutes.ts:479): scriptEnv starts with { ...process.env } and is then written to with values from req.body (scriptEnv.MESSAGE = testMessage, etc.). Since these keys are fixed literals, there's no injection path, but if mockNode is user-supplied, the destructured mockNode?.nodeNum etc. are still controlled values that go into the environment of a child process. Existing behaviour; the existing prototype-pollution guard at line 431 already covers the PARAM_* case.


Performance

  1. scriptsEndpoint re-reads the directory on every call (scriptRoutes.ts:167): fs.readdirSync is synchronous and blocks the event loop. For a small scripts directory this is fine, but since the endpoint is rate-limited and the directory is user-managed, it's unlikely to be a problem in practice.

  2. fs.openSync / fs.readSync / fs.closeSync inside a map (scriptRoutes.ts:193-197): Low-level sync I/O in a map callback for each script. Acceptable given the small scale of the scripts directory; no action needed.


Test Coverage

Tests are well-structured and cover the key validation and happy paths. A few gaps:

  • DELETE /scripts/:filename — "happy path" (actually deleting a file): Not tested. Only the system-script guard is exercised. Mocking fs.unlinkSync and fs.existsSync would cover the success path and the 404 case.
  • GET /stats — no-sourceId variant: The test at line 40 passes ?sourceId=src-1 but there's no test asserting that omitting sourceId passes undefined through to the db calls (line 53 covers the error path but without a sourceId query param, so it incidentally tests this).
  • scriptsEndpoint export shape: The only test is expect(typeof scriptsEndpoint).toBe('function'). There's no test that exercises the listing behaviour (e.g. with a mocked fs). This is reasonable for a refactor PR that's not changing behaviour.

Minor Nits

  • scriptRoutes.ts:156: scriptsEndpoint uses any types; prefer Request/Response from the existing imports.
  • scriptRoutes.ts:407: for (let i = 0; ...) shadows the outer let i = 0 at line 363. This is valid JS/TS (block scoping), but renaming the inner loop variable would aid readability.
  • dataExchangeRoutes.ts:2: requireAdmin is imported but requirePermission is unused — actually requireAdmin is used for /export and /import and requirePermission is used for /stats, so both imports are needed. ✅

Summary: The refactor achieves its stated goal cleanly. All observations above are either pre-existing behaviours carried over verbatim or minor polish items. No blockers identified.

if (regexPattern.length > 2000) {
continue;
}
const triggerRegex = new RegExp(`^${regexPattern}$`, 'i');
Comment thread src/server/routes/scriptRoutes.ts Dismissed
@Yeraze Yeraze merged commit 35bad22 into main Jun 17, 2026
22 of 23 checks passed
@Yeraze Yeraze deleted the fix/3502-batch-b branch June 17, 2026 17:24
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.

2 participants