Skip to content

refactor(brands): migrate brand-loader to StorageAdapter#51

Merged
arndvs merged 5 commits into
v2from
ai/feat/backend-evolution-phase0
May 9, 2026
Merged

refactor(brands): migrate brand-loader to StorageAdapter#51
arndvs merged 5 commits into
v2from
ai/feat/backend-evolution-phase0

Conversation

@arndvs
Copy link
Copy Markdown
Owner

@arndvs arndvs commented May 9, 2026

Changes

  • brand-loader.ts: all I/O through getStorageAdapter() instead of direct node:fs calls
  • BrandProfile paths: now store container-relative keys (e.g. brands/acme/font.otf) instead of absolute filesystem paths
  • Logo proxy route: reads through StorageAdapter
  • Generate route: readBrandAsset() uses adapter for product image reads
  • brand-loader tests: rewritten to mock StorageAdapter instead of node:fs
  • storage-adapter tests: fix Dirent type casts for Node 22 types

Verification

  • pnpm typecheck passes
  • 218/218 tests pass
  • No new dependencies

arndvs added 2 commits May 9, 2026 08:43
Replace direct fs calls with StorageAdapter interface so brand loading
works against both local filesystem and Azure Blob Storage.

- brand-loader.ts: all I/O through getStorageAdapter()
- BrandProfile paths now store container-relative keys
- Logo proxy and generate routes use adapter for reads
- Tests rewritten to mock StorageAdapter instead of node:fs
Use Awaited<ReturnType<typeof fs.readdir>> instead of Dirent[] to
match the NonSharedBuffer generic parameter in newer @types/node.
Copilot AI review requested due to automatic review settings May 9, 2026 15: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

This PR refactors brand-related I/O to route through StorageAdapter so the same codepaths can run on local filesystem or Azure Blob Storage, and updates BrandProfile to store container-relative keys instead of absolute filesystem paths.

Changes:

  • Migrates brand-loader to read/validate brand fixtures and assets via getStorageAdapter(), returning container-relative keys in the hydrated profile.
  • Updates the generate pipeline and logo proxy route to read brand assets through StorageAdapter rather than node:fs.
  • Rewrites brand-loader tests to mock StorageAdapter, and adjusts storage-adapter tests for newer Node readdir typings.

Reviewed changes

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

Show a summary per file
File Description
tests/storage-adapter.test.ts Adjusts readdir mock typing to align with updated Node type overloads.
tests/brand-loader.test.ts Reworks tests to mock getStorageAdapter() and validate container-relative keys.
lib/cast/server/pipeline/resolve.ts Updates resolver docs to reflect container-relative keys + adapter-based reads.
lib/cast/server/brand-loader.ts Switches brand fixture reads/existence checks from fs to StorageAdapter; returns keys instead of absolute paths.
lib/cast/schemas.ts Updates BrandProfile docs to describe container-relative key fields.
app/api/generate/route.ts Reads product-image (“products” source) assets via StorageAdapter.
app/api/brands/[slug]/logos/[id]/route.ts Reads logo bytes via StorageAdapter instead of direct filesystem reads.

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

Comment thread lib/cast/server/brand-loader.ts
Comment thread lib/cast/server/brand-loader.ts
Comment thread lib/cast/server/brand-loader.ts
Comment thread lib/cast/server/brand-loader.ts
Reject item.file values containing .., ., backslashes, or absolute
paths before building container-relative keys. Surfaces violations
as BrandInvalidError (400) instead of an unexpected PathTraversalError.

Addresses Copilot review on PR #51:
- brand-loader.ts — "item.file contains backslashes or traversal segments"
- brand-loader.ts — "backgrounds.json item.file is not constrained"
@arndvs
Copy link
Copy Markdown
Owner Author

arndvs commented May 9, 2026

Copilot Review — Round 1 Summary

# Thread Tier Action
1 BrandNotFoundError replaces BrandIncompleteError for missing brand.json Reply-only Intentional semantic change — blob storage has no empty dirs. BrandIncompleteError still fires for missing siblings.
2 listBrandSlugs uses full-recursive listFiles HITL-deferrable → #52 Filed issue for listPrefixes StorageAdapter method. Not blocking.
3 Product item.file not validated for path traversal Fixed in dcb934e Added validateItemFile() — rejects .., backslashes, absolute paths → BrandInvalidError. 3 new tests.
4 Background item.file same issue Fixed in dcb934e Same validateItemFile() applied to backgrounds loop.

All 4 threads resolved. Requesting re-review.

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 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread lib/cast/server/brand-loader.ts
Comment thread tests/brand-loader.test.ts Outdated
Addresses Copilot review on PR #51 (round 2):
- brand-loader.ts:validateItemFile — reject backslashes upfront instead of
  silently splitting on them; container keys are forward-slash only
- brand-loader.test.ts — rename traversal-via-backslash test, add pure
  backslash test case (products\\can.png)
@arndvs
Copy link
Copy Markdown
Owner Author

arndvs commented May 9, 2026

PR #51 — Copilot review addressed (round 2)

Pre-flight: round 2/3 | CI: no checks configured | pending review: no
Triage: Auto 2 | Confirm 0 | HITL-deferrable 0 | HITL-blocking 0
Comments fixed: 2 / 2
Issues filed: 0
Threads resolved: 2
Threads left open: 0
Commits: 1
Review re-requested: yes

Commits:
0b0c69e fix(brands): reject any backslash in item.file container keys

Details:

# Thread Score Action
5 validateItemFile silently splits backslashes instead of rejecting 100 = [50 +20 specific +25 mechanical +15 ≤1 file +15 test coverage → clamped] Fixed — reject any \ upfront; split on / only
6 Test "backslashes" also contains .. traversal 90 = [50 +20 specific +25 mechanical +15 ≤1 file −20 modifies test] Fixed — renamed test, added pure-backslash case (products\can.png)

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 7 out of 7 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

app/api/brands/[slug]/logos/[id]/route.ts:62

  • The error handling after storage.readFile() assumes a Node-style errno with err.code. With the AzureBlobAdapter, missing blobs typically throw an Azure RestError (e.g. statusCode === 404 / code === "BlobNotFound"), so this route may throw instead of returning a 404. Consider normalizing StorageAdapter not-found errors to { code: "ENOENT" } across backends (preferred), or update this catch block to also treat Azure 404-style errors as "not found" (or preflight with fileExists() and map false to 404).
    buf = await storage.readFile("inputs", variant.path)
  } catch (err) {
    const code = (err as NodeJS.ErrnoException).code
    if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
      return NextResponse.json(

Comment thread lib/cast/server/brand-loader.ts
Comment thread lib/cast/server/brand-loader.ts
Comment thread lib/cast/server/brand-loader.ts Outdated
…y brand.json

- validateItemFile: reject inputs that split to zero segments (defense-in-depth)
- listBrandSlugs: only list slugs whose brands/<slug>/brand.json key exists,
  consistent with loadBrandProfile semantics
- Add tests: empty-segments rejection, partial-brand exclusion (224 pass)
@arndvs
Copy link
Copy Markdown
Owner Author

arndvs commented May 9, 2026

Round 3 — Copilot Review Summary

3 threads triaged → 2 fixed, 1 deferred

# File Disposition Action
7 brand-loader.ts (readJson) HITL-deferrable → #53 isENOENT() won't catch Azure RestError. Fix belongs in StorageAdapter (Slice 4).
8 brand-loader.ts (validateItemFile) Fixed in 8105584 Added segments.length === 0 guard for inputs like "/" or "///".
9 brand-loader.ts (listBrandSlugs) Fixed in 8105584 Filter slugs to only those with brands/<slug>/brand.json key.

Tests: 224 passing (+2 new: empty-segments rejection, partial-brand exclusion)
Typecheck: clean

Cumulative across 3 rounds: 9 threads → 6 fixed, 1 reply-only (intentional semantic change), 2 deferred (#52 listPrefixes, #53 error normalization).

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 7 out of 7 changed files in this pull request and generated no new comments.

@arndvs arndvs merged commit d2c443c into v2 May 9, 2026
4 checks passed
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