Skip to content

Slice 8: Server Module Boundary Hardening#55

Open
arndvs wants to merge 5 commits into
v2from
ai/refactor/server-module-boundary
Open

Slice 8: Server Module Boundary Hardening#55
arndvs wants to merge 5 commits into
v2from
ai/refactor/server-module-boundary

Conversation

@arndvs
Copy link
Copy Markdown
Owner

@arndvs arndvs commented May 9, 2026

Summary

Harden the server module boundary so route handlers are thin pass-throughs with no direct filesystem I/O.

Changes

Route refactoring — eliminate all node:fs/promises imports from app/api/:

  • detected-assets/route.ts → uses detectAssetFiles() via StorageAdapter instead of fs.access()
  • upload/route.ts → uses saveAssetFile() via StorageAdapter instead of fs.mkdir/unlink/writeFile()
  • outputs/[...path]/route.ts → uses readOutputFile() via StorageAdapter instead of fs.readFile()

New server functions in lib/cast/server/storage.ts:

  • detectAssetFiles(slugs) — delegates to existing findLocalAsset()
  • saveAssetFile(productSlug, ext, bytes) — delete-then-write via adapter
  • readOutputFile(...segments) — validates path segments then reads via adapter

Barrel exportlib/cast/server/index.ts:

  • Re-exports the entire public API surface
  • Documents the import boundary rules
  • This becomes the Fastify service API surface when the backend migrates off Next.js

Acceptance Criteria

  • Clean import graph: app/api/*lib/cast/server/* only
  • No direct fs, Azure, or Qdrant calls in route handlers
  • Barrel export covers entire server surface
  • 239/239 tests passing, typecheck clean

Related

  • Backend evolution plan: Slice 8
  • Unblocks Slice 14b (MCP Server stdio)

arndvs added 2 commits May 9, 2026 13:43
Move filesystem operations behind StorageAdapter:
- detected-assets: use detectAssetFiles() instead of fs.access()
- upload: use saveAssetFile() instead of fs.mkdir/unlink/writeFile()
- outputs: use readOutputFile() instead of fs.readFile()

readOutputFile validates individual path segments (absolute, ..,
null bytes) before delegating to the adapter, preserving the same
rejection behavior as the former safeJoin call in the route handler.

Acceptance: no node:fs/promises imports remain in app/api/.
Create lib/cast/server/index.ts that re-exports the entire public
API surface. Documents the import boundary:
  app/api/* → lib/cast/server/* only
  No direct fs, Azure, or Qdrant calls in route handlers

This barrel becomes the Fastify service API surface when the backend
migrates off Next.js API routes.
Copilot AI review requested due to automatic review settings May 9, 2026 20:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the server/module boundary by moving filesystem I/O out of Next.js route handlers and into lib/cast/server storage helpers backed by StorageAdapter, plus adds a server barrel export intended to define the long-term service API surface.

Changes:

  • Refactors API routes to remove direct node:fs/promises usage and delegate to StorageAdapter-backed helpers.
  • Adds new storage helpers: asset detection, asset upload save/replace, and outputs file reads with segment validation.
  • Introduces lib/cast/server/index.ts as a barrel export documenting import boundary rules.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
lib/cast/server/storage.ts Adds detectAssetFiles, saveAssetFile, and readOutputFile helpers that delegate to StorageAdapter.
lib/cast/server/index.ts New barrel export defining/documenting the intended public server API surface.
app/api/upload/route.ts Replaces direct FS write/delete operations with saveAssetFile().
app/api/outputs/[...path]/route.ts Replaces direct FS reads with readOutputFile() while keeping .png whitelist behavior in the route.
app/api/detected-assets/route.ts Replaces per-extension FS probing with detectAssetFiles().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/cast/server/storage.ts
Comment thread lib/cast/server/storage.ts Outdated
Comment thread lib/cast/server/index.ts Outdated
Comment thread lib/cast/server/index.ts Outdated
arndvs added 2 commits May 9, 2026 13:56
…ents

saveAssetFile: narrow ext param to AssetExt union type with runtime
check, preventing arbitrary extensions from reaching the storage layer.

readOutputFile: split segments on / and \ before validation so embedded
separators cannot smuggle traversal components past per-segment checks.
Pre-check rejects absolute and empty raw segments before normalization.
Replace `export *` from storage-adapter with explicit named exports,
hiding _resetStorageAdapter and LocalFsAdapter test seams from the
public API surface.

Replace eager re-export of azure-blob-adapter with type-only export so
@azure/storage-blob stays lazy-loaded via dynamic import().
@arndvs arndvs requested a review from Copilot May 9, 2026 20:57
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Comment thread lib/cast/server/storage.ts
Comment thread lib/cast/server/storage.ts
Comment thread app/api/outputs/[...path]/route.ts
Addresses Copilot review on PR #55 (round 2):
- outputs/[...path]/route.ts — stale reference to safeJoin replaced
  with readOutputFile's segment validation description.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread lib/cast/server/index.ts
Comment on lines +10 to +13
* ✅ lib/cast/server/* → lib/cast/* (server uses shared schemas/types)
* ❌ app/api/* → node:fs/promises (no direct I/O in route handlers)
* ❌ app/api/* → @azure/* (no direct Azure calls in routes)
* ❌ components/ → lib/cast/server/* (client code never imports server)
Comment on lines +157 to +163
// Normalize: split on both / and \ so embedded separators can't smuggle
// traversal components past the per-segment check.
const parts = segments.flatMap((s) => s.split(/[/\\]/)).filter(Boolean)
for (const part of parts) {
if (part === "." || part === ".." || part.includes("\0")) {
throw new PathTraversalError(`invalid output path segment: "${part}"`)
}
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