feat(mobile-sso): serve assetlinks.json + App Links redirect allowlist
All checks were successful
PR Checks / server-tests (pull_request) Successful in 9m23s
PR Checks / client-build (pull_request) Successful in 10m6s
PR Checks / bot-install (pull_request) Successful in 9m16s

Add the server side of Android App Links (M9 follow-up, docs/android/APP_LINKS.md):

- GET /.well-known/assetlinks.json at the web root, gated by the new admin
  setting `mobile_app_links_enabled` (default off -> 404; on-but-no-fingerprint
  -> 404). Emits the Digital Asset Links statement for the fixed published
  package (MOBILE_APP_PACKAGE) + MOBILE_APP_CERT_SHA256 fingerprint(s).
- mobileSso `/start` additionally accepts this shard's own self-origin
  https://<host>/mobile/callback when App Links are enabled — one additive
  exact-match entry, derived from APP_BASE_URL/request origin, never client
  input; the custom-scheme allowlist is never narrowed. The settings lookup is
  short-circuited for non-https redirects so custom-scheme rejections stay fast.
- settings.isMobileAppLinksEnabled() (fail-closed) + getPublic().mobileAppLinks;
  admin updateSettings validates the boolean; seed default off.

Tests: test/appLinks.test.js (route gating + allowlist). Full suite 284 pass.
Swagger unchanged (web-root verification file is #swagger.ignore'd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-20 18:37:17 -05:00
parent d37c3a46a9
commit bcc96e7cfb
7 changed files with 335 additions and 1 deletions

View File

@@ -54,6 +54,22 @@ async function isGameAccountSignupEnabled() {
return GAME_SIGNUP_OFFER.includes(await getGameSignupMode())
}
// Android App Links opt-in (M9 follow-up). When on, the shard auto-serves
// /.well-known/assetlinks.json and the mobile SSO bridge additionally accepts the
// self-origin https://<host>/mobile/callback redirect. Stored as the string
// 'true'/'false'; default off. See docs/android/APP_LINKS.md.
const MOBILE_APP_LINKS_KEY = 'mobile_app_links_enabled'
// Fail-closed: any read error (e.g. DB unavailable) reports "disabled" so a
// transient fault can never open the https redirect path or serve assetlinks.json.
async function isMobileAppLinksEnabled() {
try {
return String(await settingsDb.get(MOBILE_APP_LINKS_KEY)) === 'true'
} catch {
return false
}
}
async function get(key) {
return settingsDb.get(key)
}
@@ -113,6 +129,11 @@ async function getPublic() {
// that we use the first NTFY_ALLOWED_ORIGINS entry (a device endpoint must sit
// on an allowed origin anyway). Never NTFY_BASE_URL — it may be internal-only.
out.push = { ntfyUrl: publicNtfyUrl() }
// Whether this shard has opted into Android App Links (M9 follow-up). Lets a
// native client tell whether it may request the https App Link redirect_uri
// before doing so (the server would otherwise reject an unallowlisted one). The
// custom-scheme callback works regardless of this flag.
out.mobileAppLinks = String(all[MOBILE_APP_LINKS_KEY]) === 'true'
return out
}
@@ -142,4 +163,6 @@ module.exports = {
GAME_SIGNUP_MODES,
getGameSignupMode,
isGameAccountSignupEnabled,
MOBILE_APP_LINKS_KEY,
isMobileAppLinksEnabled,
}