Skip to content

Codex/media upload validation snapshot#48

Merged
cobraprojects merged 2 commits into
mainfrom
codex/media-upload-validation-snapshot
May 27, 2026
Merged

Codex/media upload validation snapshot#48
cobraprojects merged 2 commits into
mainfrom
codex/media-upload-validation-snapshot

Conversation

@cobraprojects

@cobraprojects cobraprojects commented May 26, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Added image upload support for blog posts with automatic thumbnail generation
    • Integrated media storage with form-based validation for image files (size and type constraints)
    • Added database migrations and configuration for media management
  • Documentation

    • Updated setup guides for media installation and configuration

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds comprehensive media (image) upload functionality to blog post creation and editing across Next.js, Nuxt, and SvelteKit applications. It introduces database media tables, form schema validation with file constraints, admin UI components for image selection and preview, server-side transaction-wrapped post operations with media handling, form failure recovery via cookie flashing (Nuxt), CLI scaffolding commands, and extensive test coverage for upload workflows.

Changes

Media Upload & Post Operations

Layer / File(s) Summary
Media table migrations and Post model media wrapping
apps/*/server/db/migrations/2026_05_26_000100_create_media_table.ts, apps/*/server/models/Post.ts
Adds media table with UUID, polymorphic linkage, file metadata, and indexes across all three blog applications; wraps Post model with defineMediaModel to configure images collection (public disk, single file, 2MB max) and thumb conversion (960×540 WebP, quality 82).
Post form schema with image field
apps/blog-next/lib/schemas/blog.ts, apps/blog-nuxt/shared/schemas/blog.ts, apps/blog-sveltekit/src/lib/schemas/blog.ts
Defines postForm schema with required title/body, optional excerpt/categoryId, status enum, tagIds array, and optional image file field (image type, 2MB max).
Server post query and transaction logic
apps/*/server/lib/blog.ts
Refactors getPublishedPostBySlug to explicitly return undefined when not published; refactors getAdminPostById to use firstWhere and load relations on demand; wraps createPost/updatePost in DB.transaction with optional image media collection handling and structured { data, error } returns.

Admin Form Processing & UI

Layer / File(s) Summary
Next.js admin actions with validation
apps/blog-next/app/admin/actions.ts
createPostAction and updatePostAction now validate formData against postForm, map validated fields (including tagIds.join(',')), conditionally include image when present, and return submission.fail() on media errors.
Nuxt & SvelteKit admin routes with validation
apps/blog-nuxt/server/routes/admin/posts/create.post.ts, apps/blog-nuxt/server/routes/admin/posts/[id]/update.post.ts, apps/blog-sveltekit/src/routes/admin/posts/new/+page.server.ts, apps/blog-sveltekit/src/routes/admin/posts/[id]/edit/+page.server.ts
Routes now use readFormData, validate against postForm, map validated data, conditionally include image, and return submission.fail() with errors.image on media errors.
PostForm React component (Next.js)
apps/blog-next/app/admin/posts/post-form.jsx
Client component managing form state (title, excerpt, body, status, categoryId, tagIds), blur validation, image file input, category/tag selection, image preview, and action submission.
Admin post pages
apps/blog-next/app/admin/posts/[id]/edit/page.tsx, apps/blog-next/app/admin/posts/new/page.tsx, Nuxt/SvelteKit admin routes
Pages now use PostForm component (Next.js) or updated form templates (Nuxt/SvelteKit) with image upload inputs, CSRF tokens, and multipart encoding.
CSRF token generation
apps/blog-sveltekit/src/routes/admin/posts/+page.server.ts, apps/blog-sveltekit/src/routes/admin/posts/new/+page.server.ts, apps/blog-sveltekit/src/routes/admin/posts/[id]/edit/+page.server.ts
Admin page load functions now accept request and generate CSRF tokens for form submission.

Public Post Image Display

Layer / File(s) Summary
Post detail pages with image rendering
apps/blog-next/app/posts/[slug]/page.tsx, apps/blog-nuxt/server/api/blog/posts/[slug].get.ts, apps/blog-nuxt/app/pages/posts/[slug].vue, apps/blog-sveltekit/src/routes/posts/[slug]/+page.server.ts, apps/blog-sveltekit/src/routes/posts/[slug]/+page.svelte
Pages and API handlers now fetch and conditionally render post thumbnail images from post.getFirstMediaUrl('images', 'thumb').
Admin post API endpoints
apps/blog-nuxt/server/api/admin/posts/[id].get.ts
Endpoint now augments returned post data with imageUrl thumbnail URL.

Form Infrastructure & Validation

Layer / File(s) Summary
Nuxt form failure flashing plugin
packages/adapter-nuxt/src/runtime/plugins/forms.ts
New Nitro plugin that converts form validation failures into 303 redirects with cookie-flashed error details and injects error HTML into returned pages.
Form contracts async validation
packages/forms/src/contracts.ts
Adds async request body resolution (preferring Node raw-body symbol/field), supports h3-style cached raw bodies, and properly handles multipart form data.
Form client validation & submission
packages/forms/src/internal/client.ts
Client now captures live browser FormData during submit, validates that instead of tracked state, and aligns submitted values with validated input.
File field handling
packages/forms/src/sensitiveInput.ts, packages/validation/src/contracts-support.ts, packages/validation/src/contracts-types.ts
File fields are now treated as sensitive; WebFileLike extends Blob; empty files (zero-byte with empty name) in optional file fields are coerced to undefined.

Media Validation & Errors

Layer / File(s) Summary
Structured media validation errors
packages/media/src/model/adder.ts
Introduces MediaAddError, MediaAddErrorCode, and MediaAddValidationException for size/MIME/extension validation; toMediaCollection now returns MediaAddResult with { data, error } structure.
MediaItem URL/path resolution
packages/media/src/model/item.ts
MediaItem.getPath() now derives paths from resolved URLs via new toUrlPath helper.
Media model TypeScript refinements
packages/media/src/defineMediaModel.ts
Updates type surface to handle loaded entities and eager-load path resolution; refines static method overloads including with().
Media adder exports
packages/media/src/index.ts
Exports new MediaAddResult type.

CLI Media Commands

Layer / File(s) Summary
Media migration generation
packages/cli/src/media-migrations.ts
New module defining normalizeMediaMigrationName, renderMediaTableMigration, createMediaTableMigration, and runMediaTableCommand for scaffolding media tables.
CLI media install & table commands
packages/cli/src/cli.ts
Adds holo install media (scaffolds config/media.ts, migrations, updates package.json) and holo media:table (generates migration).
Project scaffolding
packages/cli/src/project/scaffold.ts, packages/cli/src/project/scaffold/dependencies.ts
New installMediaIntoProject helper and upsertMediaPackageDependency for adding/managing @holo-js/media dependency.
CLI types & parsing
packages/cli/src/parsing.ts, packages/cli/src/project.ts, packages/cli/src/project/shared.ts, packages/cli/src/types.ts
Adds SUPPORTED_INSTALL_TARGETS entry for media, exports MediaInstallResult type, MEDIA_CONFIG_FILE_NAMES constant, and installMediaIntoProject helper.

Tests & Documentation

Layer / File(s) Summary
Test fixtures and helpers
apps/blog-next/tests/run.mjs, apps/blog-nuxt/tests/run.mjs, apps/blog-sveltekit/tests/run.mjs
Add PNG pixel and oversized image fixtures; helpers query uploaded media from DB, verify file existence, assert public image endpoints respond; test authenticated admin post flows with image upload validation and success assertions.
Form & media validation tests
packages/forms/tests/client.test.ts, packages/forms/tests/contracts.test.ts, packages/forms/tests/security.test.ts, packages/media/tests/media.test.ts, packages/validation/tests/contracts.test.ts
Comprehensive coverage for async validation staleness, h3 raw-body handling, file field coercion, media error structures, and oversized file rejection.
CLI tests
packages/cli/tests/cli.test.ts
Tests for holo install media idempotency, config/migration/package creation, and holo media:table migration generation.
Adapter tests
packages/adapter-nuxt/tests/module.test.ts, packages/adapter-nuxt/tests/plugin.test.ts, packages/adapter-nuxt/tests/setup.test.ts
Tests forms plugin beforeResponse redirect/cookie behavior, renderResponse error injection, edge cases, and adapter setup.
Auth flow CSRF
tests/example-app-auth-flow.mjs
Refactors CSRF token generation and injection for consistent handling across form submission scenarios.
Documentation
apps/docs/docs/index.md, apps/docs/docs/installation.md, apps/docs/docs/database/commands.md, apps/docs/docs/media.md
Documents holo install media workflow, npx holo media:table command, and media configuration setup.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • cobraprojects/holo-js#46: Overlaps in admin post action updates for createPostAction/updatePostAction in the same file with different logic changes.

🐰 With a hop and a bound, new media files abound,
Images for posts, validation so sound,
CSRF tokens guard, and errors are clear,
Admin forms flourish throughout the whole sphere! ✨📸

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/media-upload-validation-snapshot

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (5)
apps/blog-nuxt/app/pages/admin/posts/[id]/edit.vue (1)

18-18: ⚡ Quick win

Use descriptive alt text for the current image preview.

At Line 18, alt="" hides useful context from assistive tech. A short descriptive alt (e.g., current post image) improves edit-page accessibility with minimal change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-nuxt/app/pages/admin/posts/`[id]/edit.vue at line 18, Replace the
empty alt attribute on the image preview so assistive tech gets a descriptive
label: update the <img> that renders when data?.imageUrl to provide a meaningful
alt like “Current post image” or build a dynamic label using the post title
(e.g., include data.title if present) so the preview conveys context to screen
readers.
apps/blog-next/tests/run.mjs (1)

45-45: ⚡ Quick win

Avoid importing Next internal compiled Flight helpers in tests

apps/blog-next/tests/run.mjs:45 hard-codes encodeReply via next/dist/compiled/react-server-dom-webpack/client.node.js. Next doesn’t provide a documented/public supported import path for encodeReply, so this is prone to break on routine Next upgrades.

Prefer testing server actions through Next’s runtime behavior (e.g., issuing the same request/form submission your test is validating) instead of importing encodeReply. If keeping this import, wrap it in a try/catch and throw a clear “unsupported Next internal path” error to make failures actionable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-next/tests/run.mjs` at line 45, The test is directly importing Next
internal helper encodeReply from
next/dist/compiled/react-server-dom-webpack/client.node.js which is unsupported
and brittle; update apps/blog-next/tests/run.mjs to avoid that import by
exercising server actions via Next’s runtime (simulate the same HTTP
request/form submission your test validates) instead of calling encodeReply, or
if you must keep it wrap the
require('next/dist/compiled/react-server-dom-webpack/client.node.js') in a
try/catch and rethrow a clear error like "unsupported Next internal path:
encodeReply" so failures are actionable; locate the usage of encodeReply in
run.mjs and replace with runtime request logic or guarded import.
apps/blog-nuxt/app/pages/posts/[slug].vue (1)

14-14: 💤 Low value

Consider moving inline styles to scoped CSS.

The inline styles could be extracted to the scoped <style> block for better maintainability.

♻️ Proposed refactor
-    <img v-if="data?.imageUrl" :src="data.imageUrl" alt="" style="width: 100%; border-radius: 1rem; background: `#111827`;">
+    <img v-if="data?.imageUrl" :src="data.imageUrl" alt="" class="cover-image">

Then in the <style scoped> section:

<style scoped>
.stack { display: grid; gap: 1rem; }
.panel { padding: 1.25rem; border-radius: 1rem; background: `#111827`; line-height: 1.8; }
.cover-image { width: 100%; border-radius: 1rem; background: `#111827`; }
</style>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-nuxt/app/pages/posts/`[slug].vue at line 14, Remove the inline
style on the image element that currently uses style="width: 100%;
border-radius: 1rem; background: `#111827`;" and add a semantic class (e.g.,
"cover-image") to the <img v-if="data?.imageUrl" :src="data.imageUrl" alt="">;
then move those style rules into the component's <style scoped> block (define
.cover-image { width: 100%; border-radius: 1rem; background: `#111827`; }) and, if
applicable, extract other inline styles used in this template into scoped
classes like .stack and .panel to replace inline styling for maintainability.
apps/blog-nuxt/server/api/admin/posts/[id].get.ts (1)

11-11: Confirm media lookup behavior: getFirstMediaUrl exists and returns null (not throw) for missing media/URL generation.

data.post.getFirstMediaUrl('images', 'thumb') is provided by packages/media and is typed as Promise<string | null>. Its implementation returns item?.getUrl(conversionName) ?? null, and MediaItem.getUrl() already catches Storage URL errors and returns null. The remaining gap is only potential upstream DB/query failures inside getFirstMedia()/query.first()—wrap with try/catch and fall back to null if the admin endpoint should never fail due to media.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-nuxt/server/api/admin/posts/`[id].get.ts at line 11, The call to
data.post.getFirstMediaUrl('images','thumb') can surface DB/query errors even
though it normally returns string|null, so update the admin GET handler to guard
against failures by wrapping the getFirstMediaUrl call (and upstream
getFirstMedia()/query.first calls) in a try/catch and on any exception set
imageUrl to null; locate the media lookup in the handler where imageUrl is
assigned and change it to catch errors from getFirstMediaUrl (or its internals)
and return null as a safe fallback so the admin endpoint never fails due to
media lookup.
apps/blog-sveltekit/src/routes/admin/posts/[id]/edit/+page.server.ts (1)

46-75: ⚖️ Poor tradeoff

Consider preserving post updates even when image upload fails.

If the image upload fails (lines 46-51), the early return prevents post.update() from executing, so all text/category/tag changes are lost. Users may expect their non-image edits to be saved even if the image fails.

One option is to save post data first, then attempt the image upload outside the critical path, or allow partial success.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-sveltekit/src/routes/admin/posts/`[id]/edit/+page.server.ts around
lines 46 - 75, The code currently aborts all updates when post.addMedia fails;
instead, perform the post.update and post.tags().sync first (keep
uniqueSlug(Post, ...) usage and published_at logic), then attempt the image
upload with post.addMedia(data.image).toMediaCollection('images') afterwards; if
the upload returns an error, do not return early—log or capture the image error
and return the updated post with an image-specific error payload (use
submission.fail/fail only to report the image failure while still preserving and
returning the saved post data), so text/category/tag changes persist even if
image upload fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/blog-nuxt/app/pages/posts/`[slug].vue:
- Line 14: The img tag currently uses an empty alt (alt="") which hides
meaningful cover images from screen readers; update the <img> in
posts/[slug].vue to set alt to a descriptive value (e.g. use data.imageAlt if
present, else fallback to data.title or a generic "Post cover image") and ensure
your post data model includes an optional imageAlt/imageAltText field so editors
can supply descriptive text; adjust any types/interfaces that define the post
payload to include this optional field and update any serializers or fixtures
that produce data.imageUrl to also provide imageAlt when available.

In `@apps/blog-sveltekit/src/routes/admin/posts/new/`+page.server.ts:
- Around line 36-69: The transaction currently returns {data:null,error} when
post.addMedia().toMediaCollection fails, which doesn't trigger a rollback;
instead, throw the error inside the DB.transaction callback (i.e., replace the
early return in the post.addMedia() error branch with throwing the error) so the
transaction will rollback, and wrap the DB.transaction(...) call in a try/catch
around the callsite that then uses submission.fail(...) and fail(...) (as
currently used) to convert the thrown error into the proper response; reference
DB.transaction, post.addMedia().toMediaCollection, Post.create, submission.fail,
and fail to locate where to change control flow.

In `@apps/blog-sveltekit/src/routes/posts/`[slug]/+page.svelte:
- Line 11: Replace the empty alt on the post image with a meaningful description
derived from the post data: when rendering the image in +page.svelte (the {`#if`
data.imageUrl} block that uses data.imageUrl), set the alt to something like the
post title or a short summary (e.g., use data.title or data.summary) so screen
readers receive descriptive text rather than an empty string.

In `@apps/blog-sveltekit/tests/run.mjs`:
- Around line 331-333: The test currently takes the first token from the
combined Set-Cookie header (csrfCookie) which can be a different cookie when
multiple cookies are set; instead, extract csrfName from the HTML (csrfName
variable) first and then iterate the full list of Set-Cookie header entries (use
response.headers.getAll / response.headers.raw()['set-cookie'] or equivalent) to
find the cookie string whose name equals csrfName, then take the token before
the first semicolon for that matching entry and assign it to csrfCookie; replace
the current single-split logic that sets csrfCookie and keep csrfName extraction
as-is.

In `@apps/docs/docs/media.md`:
- Around line 24-25: The docs use a bare "holo install media" command which is
inconsistent with the rest of the page's "npx holo ..." usage and can confuse
users without a global install; update the literal command string "holo install
media" to the executable form "npx holo install media" so all install examples
use the same npx prefix and avoid command-not-found issues.

In `@packages/adapter-nuxt/src/runtime/plugins/forms.ts`:
- Around line 150-153: serializeFormFailureCookie currently JSON-serializes the
entire FormFailurePayload (which may include user-submitted fields) and stores
it in a cookie; change it to only persist the errors subset by extracting
payload.errors (e.g., const safe = { errors: payload.errors }) and
JSON.stringify/encodeURIComponent that instead, keeping the same cookie name and
attributes; apply the same change to the other occurrence that uses
FormFailurePayload (mentioned around line 256) so only errors are stored.

In `@packages/cli/tests/cli.test.ts`:
- Around line 8578-8580: Add cleanup to unmock the module mocked with
vi.doMock('../src/media-migrations') to prevent cross-test contamination: in the
test's finally cleanup block (the same place other mocks are undone) call
vi.unmock('../src/media-migrations') or otherwise restore that module mock
(e.g., vi.resetModules()/vi.restoreAllMocks() if used consistently) so the
mocked runMediaTableCommand does not leak into later tests.

In `@packages/validation/src/contracts-types.ts`:
- Around line 74-77: The exported WebFileLike currently extends Blob but tests
and isWebFileLike expect plain structural objects (e.g., { size: 100 } or {
name, type, size }); change the WebFileLike declaration in contracts-types.ts to
a structural interface that does not extend Blob and includes the optional
properties used by isWebFileLike (name?: string, lastModified?: number, type?:
string, size?: number) so it matches runtime checks in isWebFileLike and the
test inputs; update the exported type only (keep the name WebFileLike) so
consumers and type-checking align with the function isWebFileLike and existing
tests.

---

Nitpick comments:
In `@apps/blog-next/tests/run.mjs`:
- Line 45: The test is directly importing Next internal helper encodeReply from
next/dist/compiled/react-server-dom-webpack/client.node.js which is unsupported
and brittle; update apps/blog-next/tests/run.mjs to avoid that import by
exercising server actions via Next’s runtime (simulate the same HTTP
request/form submission your test validates) instead of calling encodeReply, or
if you must keep it wrap the
require('next/dist/compiled/react-server-dom-webpack/client.node.js') in a
try/catch and rethrow a clear error like "unsupported Next internal path:
encodeReply" so failures are actionable; locate the usage of encodeReply in
run.mjs and replace with runtime request logic or guarded import.

In `@apps/blog-nuxt/app/pages/admin/posts/`[id]/edit.vue:
- Line 18: Replace the empty alt attribute on the image preview so assistive
tech gets a descriptive label: update the <img> that renders when data?.imageUrl
to provide a meaningful alt like “Current post image” or build a dynamic label
using the post title (e.g., include data.title if present) so the preview
conveys context to screen readers.

In `@apps/blog-nuxt/app/pages/posts/`[slug].vue:
- Line 14: Remove the inline style on the image element that currently uses
style="width: 100%; border-radius: 1rem; background: `#111827`;" and add a
semantic class (e.g., "cover-image") to the <img v-if="data?.imageUrl"
:src="data.imageUrl" alt="">; then move those style rules into the component's
<style scoped> block (define .cover-image { width: 100%; border-radius: 1rem;
background: `#111827`; }) and, if applicable, extract other inline styles used in
this template into scoped classes like .stack and .panel to replace inline
styling for maintainability.

In `@apps/blog-nuxt/server/api/admin/posts/`[id].get.ts:
- Line 11: The call to data.post.getFirstMediaUrl('images','thumb') can surface
DB/query errors even though it normally returns string|null, so update the admin
GET handler to guard against failures by wrapping the getFirstMediaUrl call (and
upstream getFirstMedia()/query.first calls) in a try/catch and on any exception
set imageUrl to null; locate the media lookup in the handler where imageUrl is
assigned and change it to catch errors from getFirstMediaUrl (or its internals)
and return null as a safe fallback so the admin endpoint never fails due to
media lookup.

In `@apps/blog-sveltekit/src/routes/admin/posts/`[id]/edit/+page.server.ts:
- Around line 46-75: The code currently aborts all updates when post.addMedia
fails; instead, perform the post.update and post.tags().sync first (keep
uniqueSlug(Post, ...) usage and published_at logic), then attempt the image
upload with post.addMedia(data.image).toMediaCollection('images') afterwards; if
the upload returns an error, do not return early—log or capture the image error
and return the updated post with an image-specific error payload (use
submission.fail/fail only to report the image failure while still preserving and
returning the saved post data), so text/category/tag changes persist even if
image upload fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0bc6af2c-3c25-4455-a5f6-3c9ff012ca61

📥 Commits

Reviewing files that changed from the base of the PR and between bf9cb6e and 8f7588e.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (78)
  • apps/blog-next/app/admin/actions.ts
  • apps/blog-next/app/admin/posts/[id]/edit/page.tsx
  • apps/blog-next/app/admin/posts/new/page.tsx
  • apps/blog-next/app/admin/posts/post-form.jsx
  • apps/blog-next/app/posts/[slug]/page.tsx
  • apps/blog-next/lib/schemas/blog.ts
  • apps/blog-next/next.config.ts
  • apps/blog-next/server/db/migrations/2026_05_26_000100_create_media_table.ts
  • apps/blog-next/server/lib/blog.ts
  • apps/blog-next/server/models/Post.ts
  • apps/blog-next/tests/run.mjs
  • apps/blog-nuxt/app/pages/admin/categories/index.vue
  • apps/blog-nuxt/app/pages/admin/posts/[id]/edit.vue
  • apps/blog-nuxt/app/pages/admin/posts/index.vue
  • apps/blog-nuxt/app/pages/admin/posts/new.vue
  • apps/blog-nuxt/app/pages/admin/tags/index.vue
  • apps/blog-nuxt/app/pages/categories/[slug].vue
  • apps/blog-nuxt/app/pages/index.vue
  • apps/blog-nuxt/app/pages/posts/[slug].vue
  • apps/blog-nuxt/app/pages/posts/index.vue
  • apps/blog-nuxt/app/pages/tags/[slug].vue
  • apps/blog-nuxt/package.json
  • apps/blog-nuxt/server/api/admin/posts/[id].get.ts
  • apps/blog-nuxt/server/api/blog/posts/[slug].get.ts
  • apps/blog-nuxt/server/db/migrations/2026_05_26_000100_create_media_table.ts
  • apps/blog-nuxt/server/lib/blog.ts
  • apps/blog-nuxt/server/models/Post.ts
  • apps/blog-nuxt/server/routes/admin/posts/[id]/update.post.ts
  • apps/blog-nuxt/server/routes/admin/posts/create.post.ts
  • apps/blog-nuxt/shared/schemas/blog.ts
  • apps/blog-nuxt/tests/run.mjs
  • apps/blog-sveltekit/server/db/migrations/2026_05_26_000100_create_media_table.ts
  • apps/blog-sveltekit/server/models/Post.ts
  • apps/blog-sveltekit/src/lib/schemas/blog.ts
  • apps/blog-sveltekit/src/lib/server/blog.ts
  • apps/blog-sveltekit/src/routes/admin/posts/+page.server.ts
  • apps/blog-sveltekit/src/routes/admin/posts/+page.svelte
  • apps/blog-sveltekit/src/routes/admin/posts/[id]/edit/+page.server.ts
  • apps/blog-sveltekit/src/routes/admin/posts/[id]/edit/+page.svelte
  • apps/blog-sveltekit/src/routes/admin/posts/new/+page.server.ts
  • apps/blog-sveltekit/src/routes/admin/posts/new/+page.svelte
  • apps/blog-sveltekit/src/routes/posts/[slug]/+page.server.ts
  • apps/blog-sveltekit/src/routes/posts/[slug]/+page.svelte
  • apps/blog-sveltekit/tests/blog-logic.mjs
  • apps/blog-sveltekit/tests/run.mjs
  • apps/docs/docs/database/commands.md
  • apps/docs/docs/index.md
  • apps/docs/docs/installation.md
  • apps/docs/docs/media.md
  • packages/adapter-nuxt/src/module.ts
  • packages/adapter-nuxt/src/runtime/plugins/forms.ts
  • packages/adapter-nuxt/tests/module.test.ts
  • packages/adapter-nuxt/tests/plugin.test.ts
  • packages/adapter-nuxt/tests/setup.test.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/media-migrations.ts
  • packages/cli/src/parsing.ts
  • packages/cli/src/project.ts
  • packages/cli/src/project/scaffold.ts
  • packages/cli/src/project/scaffold/dependencies.ts
  • packages/cli/src/project/shared.ts
  • packages/cli/src/types.ts
  • packages/cli/tests/cli.test.ts
  • packages/forms/src/contracts.ts
  • packages/forms/src/internal/client.ts
  • packages/forms/src/sensitiveInput.ts
  • packages/forms/tests/client.test.ts
  • packages/forms/tests/contracts.test.ts
  • packages/forms/tests/security.test.ts
  • packages/media/src/defineMediaModel.ts
  • packages/media/src/index.ts
  • packages/media/src/model/adder.ts
  • packages/media/src/model/item.ts
  • packages/media/tests/media.test.ts
  • packages/validation/src/contracts-support.ts
  • packages/validation/src/contracts-types.ts
  • packages/validation/tests/contracts.test.ts
  • tests/example-app-auth-flow.mjs

<article class="stack">
<NuxtLink to="/posts">Back to posts</NuxtLink>
<h1>{{ data?.title }}</h1>
<img v-if="data?.imageUrl" :src="data.imageUrl" alt="" style="width: 100%; border-radius: 1rem; background: #111827;">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add meaningful alt text for the post cover image.

The image uses an empty alt="" attribute, which tells screen readers to skip it. Post cover images are content, not decoration, and should have descriptive alt text for accessibility. Consider adding an altText or imageAlt field to the post data, or at minimum use the post title as fallback.

♿ Proposed fix for accessibility
-    <img v-if="data?.imageUrl" :src="data.imageUrl" alt="" style="width: 100%; border-radius: 1rem; background: `#111827`;">
+    <img v-if="data?.imageUrl" :src="data.imageUrl" :alt="data.imageAlt || data.title" style="width: 100%; border-radius: 1rem; background: `#111827`;">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-nuxt/app/pages/posts/`[slug].vue at line 14, The img tag currently
uses an empty alt (alt="") which hides meaningful cover images from screen
readers; update the <img> in posts/[slug].vue to set alt to a descriptive value
(e.g. use data.imageAlt if present, else fallback to data.title or a generic
"Post cover image") and ensure your post data model includes an optional
imageAlt/imageAltText field so editors can supply descriptive text; adjust any
types/interfaces that define the post payload to include this optional field and
update any serializers or fixtures that produce data.imageUrl to also provide
imageAlt when available.

Comment on lines +36 to 69
const result = await DB.transaction(async () => {
const post = await Post.create({
user_id: await ensureAuthorId(),
category_id: data.categoryId ? Number(data.categoryId) : null,
title: data.title.trim(),
slug: await uniqueSlug(Post, data.title),
excerpt: data.excerpt?.trim() || null,
body: data.body.trim(),
status: data.status,
published_at: data.status === 'published' ? new Date() : null,
})

if (data.tagIds.length > 0) {
await post.tags().attach(data.tagIds)
}

if (data.image?.size) {
const { error } = await post.addMedia(data.image).toMediaCollection('images')
if (error) {
return { data: null, error }
}
}

if (data.tagIds.length > 0) {
await post.tags().attach(data.tagIds)
return { data: post, error: null }
})
if (result.error) {
const failure = submission.fail({
status: result.error.status,
errors: {
image: [result.error.message],
},
})
return fail(failure.status, failure)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Transaction commits even when image upload fails, causing inconsistent state.

When addMedia().toMediaCollection() fails, the code returns { data: null, error } from the transaction callback. However, this does not throw an exception, so the transaction commits normally. The post and tags are persisted, but the action returns fail(), leading the user to believe the operation failed entirely.

To rollback on image failure, throw the error instead of returning it:

Proposed fix
       if (data.image?.size) {
         const { error } = await post.addMedia(data.image).toMediaCollection('images')
         if (error) {
-          return { data: null, error }
+          throw error
         }
       }
 
-      return { data: post, error: null }
+      return { data: post }
     })
-    if (result.error) {
+  } catch (err) {
+    if (err?.status && err?.message) {
       const failure = submission.fail({
-        status: result.error.status,
+        status: err.status,
         errors: {
-          image: [result.error.message],
+          image: [err.message],
         },
       })
       return fail(failure.status, failure)
     }
+    throw err
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-sveltekit/src/routes/admin/posts/new/`+page.server.ts around lines
36 - 69, The transaction currently returns {data:null,error} when
post.addMedia().toMediaCollection fails, which doesn't trigger a rollback;
instead, throw the error inside the DB.transaction callback (i.e., replace the
early return in the post.addMedia() error branch with throwing the error) so the
transaction will rollback, and wrap the DB.transaction(...) call in a try/catch
around the callsite that then uses submission.fail(...) and fail(...) (as
currently used) to convert the thrown error into the proper response; reference
DB.transaction, post.addMedia().toMediaCollection, Post.create, submission.fail,
and fail to locate where to change control flow.

<article class="stack">
<a href="/posts">Back to posts</a>
<h1>{data.post.title}</h1>
{#if data.imageUrl}<img src={data.imageUrl} alt="" style="width: 100%; border-radius: 1rem; background: #111827;">{/if}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use meaningful alt text for the post image.

Line 11 sets alt="", which treats this image as decorative for screen readers. If this image conveys post content, provide a descriptive alt (for example, the post title).

Suggested fix
-{`#if` data.imageUrl}<img src={data.imageUrl} alt="" style="width: 100%; border-radius: 1rem; background: `#111827`;">{/if}
+{`#if` data.imageUrl}<img src={data.imageUrl} alt={data.post.title} style="width: 100%; border-radius: 1rem; background: `#111827`;">{/if}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-sveltekit/src/routes/posts/`[slug]/+page.svelte at line 11, Replace
the empty alt on the post image with a meaningful description derived from the
post data: when rendering the image in +page.svelte (the {`#if` data.imageUrl}
block that uses data.imageUrl), set the alt to something like the post title or
a short summary (e.g., use data.title or data.summary) so screen readers receive
descriptive text rather than an empty string.

Comment on lines +331 to +333
const setCookie = response.headers.get('set-cookie') ?? ''
const csrfCookie = setCookie.split(';', 1)[0]
const csrfName = html.match(/<input[^>]+name="([^"]+)"[^>]+value="([^"]+)"/)?.[1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Extract the CSRF cookie by name, not by first Set-Cookie entry.

Line 332 currently grabs the first cookie token, which can be a non-CSRF cookie when multiple cookies are set. That can make this test intermittently fail.

Suggested fix
-  const setCookie = response.headers.get('set-cookie') ?? ''
-  const csrfCookie = setCookie.split(';', 1)[0]
+  const setCookie = response.headers.get('set-cookie') ?? ''
+  const csrfCookie = setCookie.match(/(?:^|,\s*)(XSRF-TOKEN=[^;,\s]+)/)?.[1] ?? ''
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const setCookie = response.headers.get('set-cookie') ?? ''
const csrfCookie = setCookie.split(';', 1)[0]
const csrfName = html.match(/<input[^>]+name="([^"]+)"[^>]+value="([^"]+)"/)?.[1]
const setCookie = response.headers.get('set-cookie') ?? ''
const csrfCookie = setCookie.match(/(?:^|,\s*)(XSRF-TOKEN=[^;,\s]+)/)?.[1] ?? ''
const csrfName = html.match(/<input[^>]+name="([^"]+)"[^>]+value="([^"]+)"/)?.[1]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/blog-sveltekit/tests/run.mjs` around lines 331 - 333, The test currently
takes the first token from the combined Set-Cookie header (csrfCookie) which can
be a different cookie when multiple cookies are set; instead, extract csrfName
from the HTML (csrfName variable) first and then iterate the full list of
Set-Cookie header entries (use response.headers.getAll /
response.headers.raw()['set-cookie'] or equivalent) to find the cookie string
whose name equals csrfName, then take the token before the first semicolon for
that matching entry and assign it to csrfCookie; replace the current
single-split logic that sets csrfCookie and keep csrfName extraction as-is.

Comment thread apps/docs/docs/media.md
Comment on lines +24 to +25
`holo install media` creates the `media` table migration. For projects that already installed the
package manually, create only the table migration:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the same executable form for install commands.

Line 24 currently says holo install media, but the rest of this page uses npx holo .... Keeping this consistent avoids “command not found” confusion for users without a global install.

Suggested docs fix
-`holo install media` creates the `media` table migration. For projects that already installed the
+`npx holo install media` creates the `media` table migration. For projects that already installed the
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`holo install media` creates the `media` table migration. For projects that already installed the
package manually, create only the table migration:
`npx holo install media` creates the `media` table migration. For projects that already installed the
package manually, create only the table migration:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/docs/media.md` around lines 24 - 25, The docs use a bare "holo
install media" command which is inconsistent with the rest of the page's "npx
holo ..." usage and can confuse users without a global install; update the
literal command string "holo install media" to the executable form "npx holo
install media" so all install examples use the same npx prefix and avoid
command-not-found issues.

Comment on lines +150 to +153
function serializeFormFailureCookie(payload: FormFailurePayload): string {
const encoded = encodeURIComponent(JSON.stringify(payload))
return `${FORM_FAILURE_COOKIE}=${encoded}; Path=/; Max-Age=60; HttpOnly; SameSite=Lax`
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Flash only the error subset, not the entire failure payload.

The cookie currently serializes all failure fields, which can include extra user-submitted data and unnecessarily increases cookie/header size. You only render errors, so persist just that subset.

Suggested hardening patch
 type FormFailurePayload = {
   ok: false
   status: number
   valid: false
   errors: Record<string, unknown>
 } & Record<string, unknown>
+
+type FlashedFormFailure = {
+  errors: Record<string, unknown>
+}

-function serializeFormFailureCookie(payload: FormFailurePayload): string {
-  const encoded = encodeURIComponent(JSON.stringify(payload))
+function serializeFormFailureCookie(payload: FormFailurePayload): string {
+  const flashed: FlashedFormFailure = { errors: payload.errors }
+  const encoded = encodeURIComponent(JSON.stringify(flashed))
   return `${FORM_FAILURE_COOKIE}=${encoded}; Path=/; Max-Age=60; HttpOnly; SameSite=Lax`
 }

-function readFlashedFailure(event: NitroEvent): FormFailurePayload | undefined {
+function readFlashedFailure(event: NitroEvent): FlashedFormFailure | undefined {
   const value = readCookie(event, FORM_FAILURE_COOKIE)
   if (!value) {
     return undefined
   }

   try {
-    const decoded = JSON.parse(decodeURIComponent(value)) as unknown
-    return isFormFailurePayload(decoded) ? decoded : undefined
+    const decoded = JSON.parse(decodeURIComponent(value)) as unknown
+    if (!decoded || typeof decoded !== 'object') {
+      return undefined
+    }
+    const errors = (decoded as { errors?: unknown }).errors
+    return errors && typeof errors === 'object'
+      ? { errors: errors as Record<string, unknown> }
+      : undefined
   } catch {
     return undefined
   }
 }

Also applies to: 256-256

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/adapter-nuxt/src/runtime/plugins/forms.ts` around lines 150 - 153,
serializeFormFailureCookie currently JSON-serializes the entire
FormFailurePayload (which may include user-submitted fields) and stores it in a
cookie; change it to only persist the errors subset by extracting payload.errors
(e.g., const safe = { errors: payload.errors }) and
JSON.stringify/encodeURIComponent that instead, keeping the same cookie name and
attributes; apply the same change to the other occurrence that uses
FormFailurePayload (mentioned around line 256) so only errors are stored.

Comment on lines +8578 to +8580
vi.doMock('../src/media-migrations', () => ({
runMediaTableCommand: mediaTable,
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unmock media-migrations in test cleanup to avoid cross-test contamination.

Line 8578 adds vi.doMock('../src/media-migrations', ...), but this mock is not unmocked in the finally cleanup block (unlike the other module mocks). That can leak mocked behavior into later tests.

Suggested fix
     } finally {
       vi.doUnmock('../src/queue')
       vi.doUnmock('../src/queue-migrations')
       vi.doUnmock('../src/cache-migrations')
+      vi.doUnmock('../src/media-migrations')
       vi.doUnmock('../src/runtime')
       vi.doUnmock('../src/generators')
       vi.doUnmock('../src/broadcast')
       vi.doUnmock('../src/project/scaffold')
       vi.doUnmock('../src/dev')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/tests/cli.test.ts` around lines 8578 - 8580, Add cleanup to
unmock the module mocked with vi.doMock('../src/media-migrations') to prevent
cross-test contamination: in the test's finally cleanup block (the same place
other mocks are undone) call vi.unmock('../src/media-migrations') or otherwise
restore that module mock (e.g., vi.resetModules()/vi.restoreAllMocks() if used
consistently) so the mocked runMediaTableCommand does not leak into later tests.

Comment on lines +74 to 77
export interface WebFileLike extends Blob {
readonly name?: string
readonly type?: string
readonly size?: number
readonly lastModified?: number
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align WebFileLike’s exported type with what isWebFileLike()/tests actually accept

isWebFileLike() accepts plain structural objects with name/type/size (tests validate inputs like { size: 100 } and { name, type, size }), but packages/validation/src/contracts-types.ts currently exports WebFileLike extends Blob, which over-promises Blob methods for values that may not implement them.

One low-impact fix if structural inputs are still intentional
-export interface WebFileLike extends Blob {
+export interface WebFileLike {
   readonly name?: string
+  readonly type?: string
+  readonly size?: number
   readonly lastModified?: number
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface WebFileLike extends Blob {
readonly name?: string
readonly type?: string
readonly size?: number
readonly lastModified?: number
}
export interface WebFileLike {
readonly name?: string
readonly type?: string
readonly size?: number
readonly lastModified?: number
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/validation/src/contracts-types.ts` around lines 74 - 77, The
exported WebFileLike currently extends Blob but tests and isWebFileLike expect
plain structural objects (e.g., { size: 100 } or { name, type, size }); change
the WebFileLike declaration in contracts-types.ts to a structural interface that
does not extend Blob and includes the optional properties used by isWebFileLike
(name?: string, lastModified?: number, type?: string, size?: number) so it
matches runtime checks in isWebFileLike and the test inputs; update the exported
type only (keep the name WebFileLike) so consumers and type-checking align with
the function isWebFileLike and existing tests.

@cobraprojects
cobraprojects merged commit 8f7588e into main May 27, 2026
1 check passed
@cobraprojects
cobraprojects deleted the codex/media-upload-validation-snapshot branch May 27, 2026 11:13
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.

1 participant