Store: add repositories in a background job and show the TOFU fingerprint

Adding a repository downloads and verifies its whole index, so it now runs as a
job (runJob in the UI) and reports 'Trusted on first use: <fingerprint>' when
no pin was given. fdroidrepos:// links pass the server check, as documented.
Jobs report SourceError messages without a 'SourceError:' prefix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-28 22:13:04 +10:00
1 parent ddcf3b2ad2
commit a9d78679ec
5 files changed
+62 -13

No files matched your search

+9 -3
View File
@@ -98,10 +98,16 @@ def set_enabled(source_id, enabled):
return {'message': source['name'] + (' enabled' if enabled else ' disabled')}
def repo_module():
module = next((m for m in modules()[0] if m.KIND == 'fdroid'), None)
if module is None or not hasattr(module, 'add_repo'):
raise SourceError('User repositories are not available in this build')
return module
def manage_repo(action, **kwargs):
mods, _ = modules()
module = next((m for m in mods if m.KIND == 'fdroid'), None)
if module is None or not hasattr(module, action):
module = repo_module()
if not hasattr(module, action):
raise SourceError('User repositories are not available in this build')
return getattr(module, action)(**kwargs)
+8 -1
View File
@@ -2080,7 +2080,14 @@ $("detailBody").onchange=e=>{if(e.target.name==='storeOffer'){sourceState.detail
$("sourcesOpen").onclick=()=>{loadSources();$('sourcesDialog').showModal();};
$("sourcesList").onchange=e=>{if(e.target.matches('[data-enable]'))changeSource({action:'enable',source:e.target.dataset.enable,enabled:e.target.checked});};
$("sourcesList").onclick=e=>{const b=e.target.closest('[data-remove]');if(b)changeSource({action:'remove',source:b.dataset.remove});};
$("addSource").onsubmit=e=>{e.preventDefault();const f=new FormData(e.target);changeSource({action:'add',url:f.get('url'),fingerprint:f.get('fingerprint')});};
async function addSource(body) {
$("sourceError").textContent="Checking the repository. Large ones can take a minute…";
let error="";
const result=await runJob('Add repository',`repo:${body.url}`,()=>api('/api/sources',body).catch(e=>{error=e.message;throw e;}),j=>{error=j.error||"";});
$("sourceError").textContent=result ? result.message : error || "We couldn't add that repository.";
if(result){$("addSource").reset();sourceState.source='';await loadSources();await searchSources();}
}
$("addSource").onsubmit=e=>{e.preventDefault();const f=new FormData(e.target);addSource({action:'add',url:f.get('url'),fingerprint:f.get('fingerprint')});};
document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>$(b.dataset.close).close());
$('appDetail').addEventListener('close',()=>{sourceState.detailRequest++;});
document.querySelectorAll('[data-store-tab]').forEach(b=>b.onclick=()=>{const browse=b.dataset.storeTab==='browse';$('appStore').hidden=!browse;$('androidLibrary').hidden=browse;$('repNew').hidden=browse;$('sourcesOpen').hidden=!browse;document.querySelectorAll('[data-store-tab]').forEach(t=>{t.classList.toggle('on',t===b);t.setAttribute('aria-pressed',t===b);});if(!browse)loadAndroid();});
+16 -5
View File
@@ -167,6 +167,8 @@ def start_job(label, work, progress=False):
fields = {"message": result.get("message") or f"{label}: done", "result": result}
except (Failure, frame_android.FrameError) as e:
fields = {"error": unreachable(str(e)) or str(e)}
except SourceError as e: # already user-readable, and about a store, not the Frame
fields = {"error": str(e)}
except Exception as e:
fields = {"error": f"{type(e).__name__}: {e}"}
finally:
@@ -1278,11 +1280,20 @@ def source_manage(body):
return apk_search.set_enabled(source_text(body, 'source'), body['enabled'])
if action == 'add':
url = source_text(body, 'url')
if urlparse(url).scheme != 'https' or not urlparse(url).hostname or urlparse(url).username:
raise Failure('Use an HTTPS repository URL without credentials', 400)
apk_search.manage_repo('add_repo', url=url, fingerprint=source_text(body, 'fingerprint', True),
name=source_text(body, 'name', True))
return {'message': 'Repository added'}
fingerprint, name = source_text(body, 'fingerprint', True), source_text(body, 'name', True)
parts = urlparse(url)
if parts.scheme not in ('https', 'fdroidrepos') or not parts.hostname or parts.username:
raise Failure('Use an HTTPS or fdroidrepos:// repository URL without credentials', 400)
apk_search.repo_module() # fail now if this build can't manage repositories
def add(): # downloads and verifies the whole index: a job, not a request
source = apk_search.manage_repo('add_repo', url=url, fingerprint=fingerprint, name=name)
message = 'Added ' + source['name']
if source.get('trust_on_first_use'):
message += '. Trusted on first use: ' + source['fingerprint'].upper()
return {'message': message, 'source': {k: source.get(k) for k in
('id', 'name', 'fingerprint', 'trust_on_first_use')}}
return start_job('Add repository', add)
if action == 'remove':
apk_search.manage_repo('remove_repo', source_id=source_text(body, 'source'))
return {'message': 'Repository removed'}