Skip to content

feat(media): complete v3 plugin sweep - #147

Merged
olliethedev merged 2 commits into
v3from
feat/media-phase2-sweep
Aug 20, 2026
Merged

feat(media): complete v3 plugin sweep#147
olliethedev merged 2 commits into
v3from
feat/media-phase2-sweep

Conversation

@olliethedev

@olliethedev olliethedev commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • migrate media asset/folder JSON operations to the v3 resource factory and add public form hooks
  • correct media query-key/folder semantics, normalize and bound search, and keep upload caches fresh across picker tabs
  • add URL-backed library state, nested folder behavior, permissions, i18n, and notifier integration
  • update media docs, registry output, unit coverage, and cross-framework E2E coverage

Compatibility notes

  • media list keys now use the corrected mediaAssets / mediaFolders resource prefixes
  • useFolders(undefined) lists all folders; useFolders(null) lists root folders via the __root__ wire sentinel

Test plan

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm test
  • pnpm knip
  • docs production build
  • registry generation and consumer build
  • focused media tests (58)
  • media E2E on Next.js (7/7)
  • media E2E on TanStack Start (7/7)
  • media E2E on React Router (7/7)

Note

Medium Risk
Broad Media UI and cache-key changes affect listing, search, and picker freshness; permissions are presentation-only and backend hooks remain the security boundary. Folder list wire semantics (__root__) and query prefix changes could break consumers relying on old keys or ambiguous useFolders behavior.

Overview
This PR finishes the Media plugin’s v3 client sweep: JSON asset/folder operations move onto the shared resource factory (mediaResources / createResource), with new useRegisterAssetForm and useCreateFolderForm for custom forms (field errors, notify, cache invalidation). Uploads stay on the custom transport; create/delete/register invalidations use refetchType: "all" so inactive Browse lists refresh after Upload/URL tabs.

Query keys and API semantics are corrected: prefixes are mediaAssets / mediaFolders; useFolders(undefined) vs useFolders(null) are distinct (all folders vs root via __root__ sentinel). Asset search is trimmed, capped at 200 chars, and server search scans at most the newest 1,000 rows before pagination.

Standalone /media syncs folder and debounced q to the URL via useListState; the embedded MediaPicker keeps folder/search local. UI wires useCan / CanAccess, useTranslate, and useNotify (replacing direct toasts), gates create/upload/URL/delete controls, and protects the library route with media:asset read. Docs, registry JSON, unit tests, client sweep tests, and E2E for URL folder/search behavior are updated.

The client resource layer also gains optional refetchType on mutations for the same inactive-cache behavior.

Reviewed by Cursor Bugbot for commit b05a8c6. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
better-stack-docs Ready Ready Preview Aug 20, 2026 2:47am
better-stack-playground Ready Ready Preview Aug 20, 2026 2:47am

Request Review

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2796795. Configure here.

Comment thread packages/stack/src/plugins/media/query-keys.ts

@cursor cursor 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.

Security Review — feat(media): complete v3 plugin sweep

Reviewed the full diff across 25 files. No high-confidence vulnerabilities found. Several security-relevant changes are noted below.


Positive security changes

Search query hardening (schemas.ts, getters.ts)

  • AssetListQuerySchema now enforces .max(200) on the query field — closes the unbounded input vector.
  • The in-memory fallback scan is capped at 1,000 rows (limit: needsInMemoryFilter ? 1000 : query.limit), preventing a full-table load for large libraries. This is a conscious, bounded trade-off; the cap is documented inline.
  • Query is .trim()-ed and coerced to undefined when blank both in the schema and in getters.ts, eliminating whitespace-only strings from DB comparisons.

URL registration validation unchanged (plugin.ts)

  • POST /media/assets still enforces the existing adapter-specific allowlist (allowedUrlPrefixes, S3 prefix match, Vercel Blob hostname suffix). localAdapter continues to reject all client-supplied URLs. The new useRegisterAssetForm hook on the client side does not bypass or weaken these server-side checks.

FolderListQuerySchema sentinel (schemas.ts)

  • The __root__ sentinel is sanitised by Zod's .transform() before reaching the listFolders getter and the onBeforeListFolders hook. The hook's updated signature { parentId?: string | null } correctly reflects the post-transform value, so no information is hidden from authorization hooks.

Client-side useCan() checks (multiple components)

  • Upload, URL registration, delete, and folder creation controls are hidden/disabled when the permission check returns false. These are presentation-layer guards, correctly documented as such: the authoritative boundary remains the backend onBefore* hooks. No authz bypass is introduced.

Items warranting attention (not blocking, but worth tracking)

filenameFromUrl decodes URL path segments before sending to the server (use-media.tsx)

function filenameFromUrl(url: string): string {
  try {
    const filename = new URL(url).pathname.split("/").filter(Boolean).pop();
    return filename ? decodeURIComponent(filename) : "asset";
  } catch {
    return "asset";
  }
}

If the last path segment contains a percent-encoded slash (%2F), decodeURIComponent will produce a literal / in the filename stored in the DB (e.g. GET https://cdn.example.com/images%2Fphoto.jpgfilename: "images/photo.jpg"). This is metadata pollution rather than a filesystem risk (the server never uses filename for I/O in the asset-registration path, and localAdapter rejects client URLs entirely). However, if the stored filename is later served in a Content-Disposition header without sanitization, a crafted value like foo%2Fetc%2Fpasswd could influence path interpretation in some HTTP clients. Suggest adding a strip of / and \ from the decoded result:

return filename ? decodeURIComponent(filename).replace(/[\/\\]/g, "-") : "asset";

In-memory search DoS surface for authenticated users
The 1,000-row fallback cap is a significant improvement, but a user with write-heavy access could still trigger repeated 1,000-row scans. If the adapter ever gains native substring search, the in-memory path should be removed rather than just capped.

Error messages propagated to the UI
The errorMessage callbacks in useRegisterAssetForm and useCreateFolderForm surface error.message directly:

errorMessage: (error) => error.message || t("media.toasts.registerError", "...")

This is consistent with the prior implementation but means detailed backend error strings (including any internal context) reach the browser. Ensure the backend throws only user-safe messages on validation/permission failures.


Checklist summary

Category Finding
SQL / command injection None — adapter where API used exclusively; in-memory filter uses .includes()
Path traversal Low risk in metadata only; filenameFromUrl decoding noted above
Authz bypass None — client guards are presentation-only; backend hooks unchanged
Secrets / token leakage None — no new logging of sensitive fields
SSRF None — URL allowlist enforcement predates and is unmodified by this PR
XSS None — asset URLs rendered via React JSX (text/attribute encoding); confirm() uses plain text
Unsafe deserialization None
Dependency risk None — no new external dependencies introduced
Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

@github-actions

Copy link
Copy Markdown
Contributor

Shadcn registry validated — no registry changes detected.

@olliethedev
olliethedev merged commit 4002622 into v3 Aug 20, 2026
9 checks passed
@olliethedev
olliethedev deleted the feat/media-phase2-sweep branch August 20, 2026 12:04
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