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
64 lines
2.5 KiB
JavaScript
64 lines
2.5 KiB
JavaScript
// ── /.well-known/* — web-root, non-API endpoints ──────────────────────────
|
|
//
|
|
// Android App Links verification file (M9 follow-up, docs/android/APP_LINKS.md).
|
|
// Served at the web root (outside /api/v1) because Android's Play/verifier fetch
|
|
// it from a fixed path. It is gated by the `mobile_app_links_enabled` admin
|
|
// setting: off ⇒ 404 (the app stays on the custom-scheme callback for this shard).
|
|
//
|
|
// The asserted package + fingerprint are constants of the ONE published app, not
|
|
// per-shard: the same binary is verifiable against every shard that opts in.
|
|
|
|
const settings = require('../model/settings/settings.model')
|
|
const log = require('../utils/logger')('well-known')
|
|
|
|
// Fixed identity of the published app. Overridable via env for a white-label build
|
|
// that ships under a different package / release cert.
|
|
const PACKAGE = (process.env.MOBILE_APP_PACKAGE || 'com.runicgateway.app').trim()
|
|
|
|
// Release signing-cert SHA-256 fingerprint(s), comma-separated. Multiple entries
|
|
// support cert rotation (old + new) and a debug + release cert during testing.
|
|
// Colons and case are normalized to the upper-cased, colon-separated form the
|
|
// Digital Asset Links spec expects.
|
|
function fingerprints() {
|
|
return (process.env.MOBILE_APP_CERT_SHA256 || '')
|
|
.split(',')
|
|
.map((s) => s.trim().toUpperCase())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
let warnedNoFingerprint = false
|
|
|
|
// GET /.well-known/assetlinks.json
|
|
async function assetlinks(req, res) {
|
|
// #swagger.ignore = true (web-root verification file, not part of the API surface)
|
|
const enabled = await settings.isMobileAppLinksEnabled() // fail-closed on any error
|
|
const fps = fingerprints()
|
|
|
|
// Off, or on-but-unconfigured, both 404 — serving a statement with no fingerprint
|
|
// asserts nothing and would only mislead the verifier.
|
|
if (!enabled || fps.length === 0) {
|
|
if (enabled && fps.length === 0 && !warnedNoFingerprint) {
|
|
warnedNoFingerprint = true
|
|
log.warn(
|
|
'mobile_app_links_enabled is ON but MOBILE_APP_CERT_SHA256 is unset — assetlinks.json 404s',
|
|
)
|
|
}
|
|
return res.status(404).json({ message: 'Not found' })
|
|
}
|
|
|
|
// The OS/verifier re-fetch this; it changes only on a cert rotation.
|
|
res.set('Cache-Control', 'public, max-age=3600')
|
|
return res.json([
|
|
{
|
|
relation: ['delegate_permission/common.handle_all_urls'],
|
|
target: {
|
|
namespace: 'android_app',
|
|
package_name: PACKAGE,
|
|
sha256_cert_fingerprints: fps,
|
|
},
|
|
},
|
|
])
|
|
}
|
|
|
|
module.exports = { assetlinks }
|