* perf(marketplace): browser-cache cover photos + restore Curated tab filter spacing Cover photos on /marketplace grid now serve with `Cache-Control: public, max-age=2592000, immutable` plus URL fingerprinting (`?v=<commit-sha8>` for curated, `?v=<version_no>` for flea) so browser refresh stops hitting the server entirely for unchanged assets. Per-plugin RBAC dropped from the three image endpoints (curated_asset, curated_mirrored, get_entity_photo) in favor of login-only auth — eliminates _system_db_lock contention on parallel image requests. Per-request magic-bytes revalidation also dropped from curated_asset (it was re-reading the file just to discard the bytes, then FileResponse read it again). Spacing bug: sort-dropdown commit (6be1cee) wrapped .mp-filter-row in a new flex container with inline margin-bottom:4px, masking the original 12px CSS rule. Curated tab (where .mp-type-row is hidden) ended up with 4px between filters and the card grid. Wrapper margin restored to 12px. See CHANGELOG entry under [Unreleased] — the RBAC relaxation is called out under ### Security with explicit threat-model rationale for AI/human reviewers. * test(marketplace): update renamed-html-as-png test for dropped magic-bytes check Magic-bytes body validation was dropped from `curated_asset` in the previous commit — the request path now relies on extension allowlist + pinned Content-Type + nosniff + strict CSP to neuter mismatched payloads at the browser layer. Update the test to assert the new defense-in-depth posture (200 served, but Content-Type=image/png + nosniff + CSP=default-src 'none') rather than the gone 415. --------- Co-authored-by: Minas Arustamyan <arustamyan.minas@gmail.com>
67 lines
2.7 KiB
Python
67 lines
2.7 KiB
Python
"""URL builders for served curated-marketplace assets.
|
|
|
|
Single source of truth for the three served paths the FastAPI router
|
|
under ``app/api/marketplace.py`` exposes::
|
|
|
|
/api/marketplace/curated/<slug>/<plugin>/asset/<path> ← internal_asset_url
|
|
/api/marketplace/curated/<slug>/<plugin>/doc/<path> ← internal_doc_url
|
|
/api/marketplace/curated/<slug>/<plugin>/mirrored/<key> ← mirrored_url
|
|
|
|
The same shapes were previously inlined in two places — ``src/marketplace.py``
|
|
(sync-time enrichment, where the served URL gets stored in
|
|
``marketplace_plugins.cover_photo_url`` / ``doc_links``) and
|
|
``app/api/marketplace.py`` (request-time inner-detail enrichment for
|
|
skills / agents). Both call sites now import from here so a future
|
|
URL-format tweak (added prefix, signed-URL token, …) only needs to change
|
|
one file. The router endpoints themselves still own the path string
|
|
literals — keeping the builders' definition identical to the route
|
|
declaration is a checklist item, not a runtime guarantee.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
_ROUTE_PREFIX = "/api/marketplace/curated"
|
|
|
|
|
|
def _with_version(base: str, version: str | None) -> str:
|
|
"""Append ``?v=<version>`` for cache-busting when a version is supplied.
|
|
|
|
The endpoint itself ignores the query string — the version exists only
|
|
to push the URL into a new browser-cache bucket whenever the underlying
|
|
asset is known to have changed (i.e. the cloned marketplace repo's git
|
|
HEAD moved, per ``marketplace_registry.last_commit_sha``). Same source
|
|
state → same version → browser keeps the cached bytes.
|
|
"""
|
|
return f"{base}?v={version}" if version else base
|
|
|
|
|
|
def internal_asset_url(
|
|
slug: str, plugin_name: str, path: str, version: str | None = None,
|
|
) -> str:
|
|
"""Served URL for an internal asset (cover photo, icon, …) inside the
|
|
cloned marketplace working tree.
|
|
"""
|
|
return _with_version(
|
|
f"{_ROUTE_PREFIX}/{slug}/{plugin_name}/asset/{path}", version,
|
|
)
|
|
|
|
|
|
def internal_doc_url(slug: str, plugin_name: str, path: str) -> str:
|
|
"""Served URL for an internal doc reference inside the cloned working tree."""
|
|
return f"{_ROUTE_PREFIX}/{slug}/{plugin_name}/doc/{path}"
|
|
|
|
|
|
def mirrored_url(
|
|
slug: str, plugin_name: str, key: str, version: str | None = None,
|
|
) -> str:
|
|
"""Served URL for an external asset that has been mirrored to the cache.
|
|
|
|
``key`` is the cache-relative path minus the leading ``<plugin>/``
|
|
segment (the endpoint takes the plugin from the URL path, not the
|
|
key). See ``app/api/marketplace.py:curated_mirrored`` for the
|
|
consumer side.
|
|
"""
|
|
return _with_version(
|
|
f"{_ROUTE_PREFIX}/{slug}/{plugin_name}/mirrored/{key}", version,
|
|
)
|