release: Promote external-source installs + PAT + discovery to beta - #30
Merged
Conversation
Introduces a parallel install flow for non-App-Store sources (initial driver:
public GitHub releases). The signed App Store path is unchanged; the new path
trades the Nextcloud code-signing chain for: a trusted-source allowlist
(default ConductionNL/*), strict appId/version match against the extracted
appinfo/info.xml, and optional SHA-256 verification when the release publishes
a sibling .sha256 asset.
- New SourceInterface + SourceRegistry + AppStoreSource/GithubReleaseSource
drivers; per-app source binding persisted as JSON in app config
- TrustedSourceList enforces the allowlist with fnmatch-style globs
- ExternalReleaseInstallerService runs the same maintenance-mode/backup-on-
failure flow as the signed installer; both share post-extract migrations
via new InstallFinalizer (so signed and external paths cannot drift on
upgrade semantics)
- ApiController gains GET /api/sources, GET/POST /api/source/{appId}/binding
and accepts an optional source override on version-list and install; injects
IGroupManager/IUserSession/ServerVersion via constructor (was \OC::server)
- 39 PHPUnit unit tests covering source binding, allowlist matching, GitHub
source HTTP behaviour, source-id parsing; new bootstrap-unit-only.php so
unit tests run without the full Nextcloud server bootstrap
OpenSpec proposal: openspec/changes/add-external-source-installs/
Browser-verified end-to-end against running Nextcloud:
- /api/admin-check → ok
- /api/sources → returns 2 drivers + ConductionNL/* default allowlist
- POST /api/source/randomapp/bind {owner: randomuser} → 403 with allowlist message
- POST /api/source/openregister/bind {owner: ConductionNL} → 200, binding written
- GET /api/app/openregister/versions → returns real GitHub releases
- GET /api/app/larpingapp/versions (unbound) → falls through to App Store
- ?source=appstore override returns App Store versions without changing binding
Adds Personal Access Token (PAT) management so admins can install Nextcloud apps from private GitHub repositories. PATs are per-admin by default, can optionally be shared with other admins, are validated for least-privilege scope on upload, encrypted at rest via ICrypto, and never returned via the API in plaintext. - New table app_versions_pats (migration Version1000Date20260502120000) - Pat entity + PatMapper with findVisibleTo / findOwnedBy / deleteByOwner - PatManager.useToken() decrypts only inside a callback; plaintext is immediately discarded afterwards. Token never appears in API responses (only first 4 + last 4 chars as `tokenHint`) - PatValidator probes GET https://api.github.com/user: - Classic PAT (ghp_*): rejects any scope outside repo / public_repo by parsing X-OAuth-Scopes - Fine-grained PAT (github_pat_*): GitHub does not expose configured permissions; accepted with explicit `unverifiable_scope` warning - 401 → "Token is invalid or revoked"; 403 → rate limit message - Captures `github-authentication-token-expiration` header into expires_at - PatResolver picks the highest-priority non-expired PAT visible to the current uid that matches the binding's owner/repo (owner-owned > shared, then most-specific glob) - GithubReleaseSource and ExternalReleaseInstallerService now resolve a PAT for the bound owner/repo and attach `Authorization: Bearer <token>` to GitHub API + asset-download + sha256 fetches when a PAT is present. Public-only path (no PAT) unchanged. - PatDeeplinkBuilder generates prefilled GitHub URLs: - Classic: full prefill (scopes=repo, description with NC host) - Fine-grained: page link + structured instructions array - UserDeletedListener removes PATs owned by a deleted Nextcloud user - ApiController gains GET/POST/PATCH/DELETE /api/pats and GET /api/pats/deeplink - 25 new PHPUnit tests for PatValidator (scope detection, expiry parsing, rejection paths, case-insensitive headers), PatManager (encryption roundtrip, hint builder, useToken callback semantics), PatResolver (pattern matching, owner priority, expired PAT skip), and PatDeeplinkBuilder (classic prefill, fine-grained instructions) OpenSpec proposal: openspec/changes/add-github-pat-management/ Browser-verified end-to-end: - Migration runs and creates table (with shortened index names; default-false on bool column omitted to satisfy MigrationService validation) - GET /api/pats returns empty list initially - GET /api/pats/deeplink?kind=classic returns prefilled URL with scopes=repo and a description including the Nextcloud host - GET /api/pats/deeplink?kind=fine-grained returns the page link plus a read-only-permissions instructions array - POST /api/pats with empty fields → 400 "label, targetPattern and token are required" - POST /api/pats with invalid token (ghp_definitelyinvalid...) → 400 "Token is invalid or revoked" (verified the validator hits api.github.com, reads 401, and surfaces a useful message rather than the catch-all) - DELETE /api/pats/{unknown id} → 404 - Proposal 1 path unchanged: openregister GitHub-bound versions list still returns 100 releases, no regression
Adds a single GET /api/discover?q={query}&sources={csv}&installedOnly={bool}
endpoint that aggregates results from registered discovery providers and
returns a uniform result shape the UI can render as cards with one
"Install from {source}" button per source candidate per app.
- DiscoveryProviderInterface + DiscoveryHit + DiscoveryResult value objects
- AppStoreDiscovery: substring filter across name/summary/description/
categories on the cached App Store catalog (1-hour TTL via app config);
exact-match scoring ranks exact > prefix > substring
- GithubPrivateDiscovery: enabled when current admin has any PAT visible.
For each PAT, scopes a GitHub repo search to the PAT's target_pattern
(ConductionNL/* → org:ConductionNL user:ConductionNL; foo/bar → repo:foo/bar)
and surfaces matching repos. Repos outside the trusted-source allowlist
still surface but with installable=false + a clear actionable message.
Wildcard-only patterns (target_pattern=*) are skipped to avoid leaking
global searches.
- GithubSearchDiscovery: opt-in via app config flag
`app_versions.discovery.github_search_enabled`. Disabled by default.
Public unauthenticated search with topic:nextcloud-app filter.
Honors trusted-source allowlist same way GithubPrivateDiscovery does.
- DiscoveryAggregator: merges hits by appId, builds sourceCandidates list
per app, prefers App Store summary/icon when present, annotates with
installedVersion from IAppManager, sorts installed apps first.
Provider failures surface in `errors` envelope without breaking the
whole search.
- ApiController gains GET /api/discover with admin gate + query length
validation (2-100 chars).
- 27 new PHPUnit tests covering: aggregator dedup, multi-provider merge,
installedOnly filter, source filter, sort order, error propagation,
App Store summary preference; AppStore fetch+filter+score+caching;
GitHub Private scope clause builder, allowlist annotation, wildcard skip,
PAT iteration; GitHub Search toggle, allowlist annotation, rate-limit
graceful degradation, malformed JSON.
OpenSpec proposal: openspec/changes/add-app-discovery-search/
Browser-verified end-to-end against running Nextcloud:
- GET /api/discover?q=register → 3 providers reported (appstore enabled,
github-private disabled because no PATs, github-search disabled because
flag off); 2 hits including openregister (already installed at
0.2.13-unstable.80) and registertocontact
- GET /api/discover?q=a → 400 "Query must be at least 2 characters"
- GET /api/discover?q=open&installedOnly=true → 2 results (opencatalogi,
openregister), both with installedVersion populated
- After `occ config:app:set app_versions discovery.github_search_enabled
--value=true`, q=opencatalogi merges App Store + GitHub Search into one
row with sourceCandidates=[(appstore, installable=true),
(github-search, installable=false)] (the GitHub repo found wasn't in
ConductionNL/* allowlist, so installable=false with the right reason)
- 91 unit tests pass (39 from PR #25 + 25 from PR #26 + 27 new)
UI work (Vue search bar, source chips, result cards) deliberately out of
scope here — the existing frontend bundle is not built in dev and the UI
overhaul is its own design pass. The new endpoint is consumable by any
client (planned Vue rewrite, admin tooling, openapi.json).
Tracking issue #24 covers public Software Catalogus as a future provider.
- Promote `external-sources` to canonical spec at openspec/specs/external-sources/spec.md (status: implemented) - Merge ADDED/MODIFIED requirements into openspec/specs/version-management/spec.md (status: idea → implemented): adds Source binding, Explicit source override, and rewrites Fetch Available Versions to query the bound source - Move openspec/changes/add-external-source-installs/ → openspec/changes/archive/2026-05-02-add-external-source-installs/ per OpenSpec convention
…github-pat-management
- Promote `pat-management` to canonical spec at openspec/specs/pat-management/spec.md (status: implemented) - Move openspec/changes/add-github-pat-management/ → openspec/changes/archive/2026-05-02-add-github-pat-management/
…-discovery-search
- Promote `app-discovery` to canonical spec at openspec/specs/app-discovery/spec.md (status: implemented) - Move openspec/changes/add-app-discovery-search/ → openspec/changes/archive/2026-05-02-add-app-discovery-search/
…installs feat: Add external-source install path with trusted-source allowlist
…ement feat: Add encrypted PAT storage for GitHub private-repo installs
…arch feat: Add multi-source app-discovery search API
This was referenced May 2, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Promotes the consolidated
developmentwork tobeta. Combines three already-merged feature PRs:add-external-source-installs(trusted-source allowlist, GitHub releases, parallel external installer,InstallFinalizershared by signed + external paths)add-github-pat-management(encrypted PAT storage, classic + fine-grained validation, deeplinks, user-deleted listener)add-app-discovery-search(multi-source/api/discover, AppStore + GitHub-private + opt-in GitHub-search providers, aggregator with installed-version annotation)OpenSpec hygiene already done on each feature PR: 4 canonical specs at
openspec/specs/{version-management,external-sources,pat-management,app-discovery}/spec.md(allstatus: implemented); 0 active changes; 3 archived underopenspec/changes/archive/2026-05-02-*/.Test plan
vendor/bin/phpunit -c tests/phpunit-unit-only.xml --no-coverage— 91 tests, 213 assertions