Files
saphid--frame-control/ui/apk_sources/__init__.py
T
saphidandClaude Opus 5.5 84912ea4e1 Store: repositories behind a login, and a GitHub token in source settings
F-Droid repositories can now be added with HTTP basic auth, either as
https://user:pass@host/fdroid/repo (as F-Droid's client accepts) or with
separate username/password fields (store form, server API, and the CLI's
--username/--password-stdin). The login is stripped from the saved URL and
kept in a new shared module, apk_sources/_credentials.py, which writes
source-credentials.json in the data folder with mode 0600.

A credential is only ever added per request to its exact origin (scheme,
host, port): redirects elsewhere get nothing and have Authorization
removed. HTTPS only. 401 (or 403 with a login) on add gives a "check the
username and password" error and saves nothing; signature, pin, TOFU,
hash and rollback checks are unchanged. Removing a repository deletes its
login.

GitHub gets a user-settable token (FRAME_GITHUB_TOKEN still wins), sent as
Bearer only to api.github.com. If GitHub answers 401, the request is
retried once without it and the rejection is reported in source settings.
The source list shows only whether a token is set, never its value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:46:35 +11:00

58 lines
2.5 KiB
Python

"""APK sources: every place Frame Control can find and download APKs.
One module per source kind in this package. Each module exposes the same small
interface so ``search.py`` can query them all and show where every result came
from. Python stdlib only; must run on Python 3.9.
Module interface
----------------
KIND = 'sidequest' # stable id of the source kind
def sources() -> list[dict] # configured sources of this kind (a kind can have
# several, e.g. one per F-Droid-format repo)
def search(source, query, limit=50) -> list[dict] # Entry dicts, best first
def details(source, entry_id) -> dict # Entry with 'versions'
def download(source, entry_id, version_code=None) -> dict
# {'apk': local path, 'obb': [paths], 'sha256': hex or None, 'verified': bool}
# Raise SourceError with a user-readable message on failure.
Source dict
-----------
{'id': 'sidequest', 'kind': KIND, 'name': 'SideQuest', 'url': 'https://...',
'builtin': True, 'enabled': True, 'trust': 'official' | 'community' | 'user'}
Entry dict (missing facts are None, never guessed)
----------
{'source': source id, 'id': source-local id, 'package': 'org.example.app' or None,
'name': str, 'summary': str, 'icon': url or None, 'page': url or None,
'version': '1.2', 'version_code': 12, 'min_sdk': 24, 'abis': ['arm64-v8a'],
'vr': True/False/None, 'size': bytes, 'free': True, 'license': 'GPL-3.0' or None,
'updated': 'YYYY-MM-DD', 'downloadable': bool, # False = open page only
'versions': [ {version, version_code, min_sdk, size, updated}, ... ]} # details() only
Rules
-----
- Only sources that distribute APKs with the developer's consent: free listings,
never paid apps re-hosted, no licence or entitlement workarounds.
- Honour each site's terms and robots rules; if automated download isn't allowed,
return entries with 'downloadable': False and a 'page' link instead.
- Cache indexes under frame_host.cache_dir('apk-sources'); send a clear
User-Agent ('FrameControl/<version>'); keep requests modest.
- Tests use recorded fixtures, never the network.
"""
class SourceError(Exception):
"""User-readable failure from a source (network, format, verification)."""
class SourceAuthError(SourceError):
"""The server refused our login (HTTP 401, or 403 when a login was sent)."""
class SourceLimited(SourceError):
"""The source's host asked us to slow down; retry_after is in seconds."""
def __init__(self, message, retry_after=None):
super().__init__(message)
self.retry_after = retry_after