Add resolveExtensionMetadata() with cascading priority: org-level
repo-scoped custom fields → update_stream_config table → repo-derived
defaults. All six feed generators (Joomla, WordPress, Composer, Drupal,
PrestaShop, WHMCS) now use this unified resolver. Repos can be migrated
to custom fields gradually since the config table remains as fallback.
Ref #492
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add release_stream_map table for explicitly assigning releases to
update streams. When a mapping exists, it overrides automatic tag
detection. When absent, falls back to tag name/suffix matching.
New model: ReleaseStreamMap with SetReleaseStream, GetReleaseStream,
ResolveReleaseStream (manual first, auto fallback).
UI: stream selector dropdown on release create/edit page, shown when
licensing is enabled. Options: auto-detect (default) or any
configured stream (stable, release-candidate, beta, etc.).
All three feed generators (Joomla, Dolibarr, WordPress) now use
ResolveReleaseStream instead of MatchStreamFromTag.
Migration v340 updated with release_stream_map table creation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
isStreamName was checking the extracted version (empty for stream
tags) instead of the original tag name. Now checks rel.TagName
directly, and also falls through when extractVersion returns empty.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MatchStreamFromTag now checks if the tag name directly matches a
stream name (e.g. "stable", "release-candidate", "development")
before falling back to suffix matching. Supports both conventions:
1. Stream-name tags: tag IS the stream (MokoWaaS style)
2. Version tags: tag has version + suffix (v1.0.0-rc1 style)
When a stream-name tag is detected, the version number is extracted
from the release title instead of the tag. Falls back to tag name
if no version found in title.
Applied across all feed generators: Joomla XML, Dolibarr JSON,
WordPress JSON, and Changelog XML.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SECURITY: Add verifyPackageOwnership/verifyKeyOwnership checks to
all API handlers that accept ID parameters. Prevents cross-org
access where an admin of org A could modify org B's license data.
FIX: RepoScope validation now properly parses JSON arrays using
json.Unmarshal instead of strings.Contains. The old approach matched
substrings (repo ID "2" matched inside "12"). Now uses typed int64
comparison.
FIX: Add {{$.CsrfTokenHtml}} to both delete confirmation modal
forms (package and key) in repo and org templates. Without CSRF
tokens, the form-fetch-action POST requests would be rejected.
FIX: HTML-escape release notes in WordPress changelog to prevent
XSS via malicious release note content reaching WP admin dashboards.
FIX: Parse AllowedChannels JSON format before comma-split fallback
to avoid garbage values from splitting JSON arrays by comma.
FIX: Add missing third return value (false) on error path in
validateUpdateKey to prevent compile error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New endpoint: GET /{owner}/{repo}/updates/wordpress.json
Generates JSON compatible with the YahnisElsts plugin-update-checker
library — the standard for commercial WordPress plugin self-hosted
updates. Returns name, slug, version, download_url, homepage,
requires_php, author, sections (changelog HTML), icons, and banners.
License key validation: reads from ?license_key=, ?dlid=, or ?key=
query params (PUC sends these via addQueryArgFilter). When RequireKey
is enabled, returns minimal empty response without download_url.
Changelog section built from release notes (last 10 stable releases),
converting markdown list items to HTML <ul>/<li> elements.
Icon/banner URLs point to conventional paths in the repo:
assets/icon-128x128.png, assets/icon-256x256.png
assets/banner-772x250.png, assets/banner-1544x500.png
Route registered at /updates/wordpress.json alongside existing
/updates.xml (Joomla) and /updates/dolibarr.json.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>