agnes-the-ai-analyst/src/marketplace_urls.py
minasarustamyan 63ae676b27
perf(marketplace): cache cover photos + restore Curated filter spacing (#294)
* 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>
2026-05-14 10:09:32 +02:00

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,
)