docs(android): spec Android App Links (assetlinks.json + build-time host)
Promote APP_LINKS.md from a deferred design note into an implementation spec matching the website `feat/mobile-app-links` and android `feat/app-links` branches: the server-side `/.well-known/assetlinks.json` route + `mobile_app_links_enabled` toggle + additive redirect-allowlist entry, and the app-side `autoVerify` intent-filter driven by a build-time `appLinkHost` (a single multi-tenant APK cannot autoVerify open-ended shard domains, so App Links are a white-label / first-party build opt-in; the custom scheme stays the permanent fallback). Update PLAN.md §9 (M9 follow-up) and the §14 open item, and the redirect-URI allowlist section of website/BACKEND_DESIGN.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
@@ -1,17 +1,18 @@
|
|||||||
# Architecture note — App Links & the multi-tenant callback problem
|
# Android App Links — implementation spec
|
||||||
|
|
||||||
Status: **design note; not yet implemented.** Written before the App Links work begins so the
|
Status: **implementation spec (M9 follow-up).** Stacks on the native SSO bridge (M9 Part 2):
|
||||||
multi-tenancy question is decided on paper first (per the mobile-SSO spec). The native SSO bridge
|
the app already handles the **custom-scheme** callback `runicgateway://auth/callback`, and that stays
|
||||||
ships with the **custom-scheme** callback only (`runicgateway://auth/callback`); everything below is
|
the permanent default and universal fallback. App Links are an **opt-in hardening** layered on top —
|
||||||
the *later* hardening path and its open design questions.
|
a verified `https://` callback that only the domain's real owner can claim.
|
||||||
|
|
||||||
Read alongside: the "Mobile SSO Authorization Bridge" section of
|
Read alongside: the "Mobile SSO Authorization Bridge" section of
|
||||||
[`../website/BACKEND_DESIGN.md`](../website/BACKEND_DESIGN.md) (the endpoints/tables), and
|
[`../website/BACKEND_DESIGN.md`](../website/BACKEND_DESIGN.md) (endpoints/tables/allowlist), and
|
||||||
[`PLAN.md`](./PLAN.md) §4.2 / §9 (the app milestone).
|
[`PLAN.md`](./PLAN.md) §4.2 / §9 (the app milestone). This spec matches what ships on the
|
||||||
|
`feat/mobile-app-links` (website) and `feat/app-links` (android) branches.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. The problem
|
## 1. The problem it solves
|
||||||
|
|
||||||
The mobile SSO bridge redirects the browser back to the app with a one-time code:
|
The mobile SSO bridge redirects the browser back to the app with a one-time code:
|
||||||
|
|
||||||
@@ -19,12 +20,11 @@ The mobile SSO bridge redirects the browser back to the app with a one-time code
|
|||||||
runicgateway://auth/callback?code=…&state=…
|
runicgateway://auth/callback?code=…&state=…
|
||||||
```
|
```
|
||||||
|
|
||||||
A **custom URI scheme** is fine for a self-hosted, single-tenant, internal client, but it is *not*
|
A **custom URI scheme** is fine for a self-hosted internal client, but it is not *owned* by anyone:
|
||||||
owned by anyone: any other Android app can also register an intent-filter for
|
any other Android app can register an intent-filter for `runicgateway://auth/callback` and, if chosen
|
||||||
`runicgateway://auth/callback` and, if chosen by the user (or if it registers more specifically),
|
by the user, intercept the callback. The code is single-use, PKCE-bound (Layer B), and short-lived —
|
||||||
intercept the callback. The code is single-use, PKCE-bound, and short-lived — so an interceptor
|
so an interceptor still cannot complete `/exchange` without the app's `code_verifier` — but a hijacked
|
||||||
still cannot complete `/exchange` without the app's `code_verifier` — but a hijacked callback is
|
callback is still a denial-of-service and a phishing surface we would rather close.
|
||||||
still a denial-of-service and a phishing surface we would rather close.
|
|
||||||
|
|
||||||
**Android App Links** (verified `https://` deep links) close it: the OS only routes an `https://`
|
**Android App Links** (verified `https://` deep links) close it: the OS only routes an `https://`
|
||||||
link to an app that has proven, via a file served from *that domain*, that it owns the app. An
|
link to an app that has proven, via a file served from *that domain*, that it owns the app. An
|
||||||
@@ -40,64 +40,157 @@ Links verification is **per-domain**: the domain must serve
|
|||||||
https://<shard-domain>/.well-known/assetlinks.json
|
https://<shard-domain>/.well-known/assetlinks.json
|
||||||
```
|
```
|
||||||
|
|
||||||
asserting the Android app's **package name** + **signing-certificate SHA-256 fingerprint**. The one
|
asserting the Android app's **package name** + **signing-certificate SHA-256 fingerprint**.
|
||||||
published app binary (one package name, one signing cert) must therefore be verifiable against
|
|
||||||
**every** shard domain that wants App Links — a domain set that is open-ended and not known at build
|
|
||||||
time.
|
|
||||||
|
|
||||||
Two consequences:
|
That is only half the problem. The other half is an Android platform constraint that decides the whole
|
||||||
|
shape of the app side:
|
||||||
|
|
||||||
1. **The shard must serve `assetlinks.json`.** Shard owners will not hand-edit a JSON file with a
|
> **`android:autoVerify` needs a *literal* host at build time.** An intent-filter's `<data android:host>`
|
||||||
cert fingerprint. The website has to **auto-serve** it from an admin setting.
|
> is a static string in the merged manifest; there is no "any host" or runtime host. A **single
|
||||||
2. **The app must know which shard domain it is paired to** before it can trust an App Link for that
|
> published multi-tenant APK therefore cannot autoVerify an open-ended set of shard domains** — the set
|
||||||
domain. This is a *pairing/bootstrapping* problem, not just a callback-security detail — it is the
|
> is not known when the APK is built.
|
||||||
part that makes App Links more than a drop-in swap for the custom scheme.
|
|
||||||
|
|
||||||
## 3. Proposed shape (when we build it)
|
So App Links here are **not** a drop-in replacement for the custom scheme. They split into two pieces
|
||||||
|
that ship independently:
|
||||||
|
|
||||||
### 3.1 Server: auto-served `assetlinks.json`
|
1. **Server (`assetlinks.json`) — shippable now, benefits any App-Links-capable build.** Every shard
|
||||||
|
can auto-serve its Digital Asset Links statement behind an admin toggle. This is a pure add and is
|
||||||
|
implemented on `feat/mobile-app-links`.
|
||||||
|
2. **App (`autoVerify` intent-filter) — a *build-time* opt-in.** Because the host must be baked in,
|
||||||
|
App Links are available to:
|
||||||
|
- a **white-label / first-party build** that bakes one shard's host (`-PappLinkHost=play.myshard.com`);
|
||||||
|
- a future **canonical relay domain** (`runicgateway.app`, PLAN §14 — *not yet secured*) that all
|
||||||
|
shards could bounce their final callback through, autoVerified by the generic build.
|
||||||
|
|
||||||
- One published app ⇒ one package name (`com.runicgateway.app`) and one release signing cert. Its
|
The **generic multi-tenant build bakes no host and stays custom-scheme-only** — correct and safe.
|
||||||
SHA-256 fingerprint is a **constant of the published app**, not shard-specific.
|
|
||||||
- Add a website route `GET /.well-known/assetlinks.json` (served at the **web root**, outside
|
|
||||||
`/api/v1`) that emits the Digital Asset Links statement for that fixed package + fingerprint.
|
|
||||||
- Gate it behind an admin setting `mobile_app_links_enabled` (default **off**). Off ⇒ the route 404s
|
|
||||||
and the app stays on the custom scheme for that shard. On ⇒ the shard opts into App Links.
|
|
||||||
- The fingerprint is the same for every shard, so it can be a shipped constant / env default
|
|
||||||
(`MOBILE_APP_CERT_SHA256`) rather than something each owner types. The **only** per-shard action is
|
|
||||||
flipping the setting on.
|
|
||||||
- When enabled, the shard also registers its `https://<domain>/mobile/callback` URL into the mobile
|
|
||||||
redirect-URI allowlist (see the bridge's exact-match allowlist) **in addition to** the custom
|
|
||||||
scheme — the custom scheme is never removed, it is the universal fallback.
|
|
||||||
|
|
||||||
### 3.2 App: which domain do I trust?
|
The custom scheme is never removed. It is the fallback on every build, for every shard, always.
|
||||||
|
|
||||||
- The app already stores the shard **base URL** it is paired to (first-run connect flow, PLAN §3).
|
## 3. Server design — `feat/mobile-app-links`
|
||||||
That base URL's host is the *only* domain the app should accept an App Link callback from.
|
|
||||||
- The intent-filter for `https://…/mobile/callback` cannot be scoped to a runtime host in the
|
|
||||||
manifest (intent-filters are static). Options, in order of preference:
|
|
||||||
1. **Custom scheme stays the default**; App Links are an *opt-in* the app only relies on after it
|
|
||||||
has (a) a paired base URL and (b) confirmed that host serves a valid `assetlinks.json`. Until
|
|
||||||
both hold, the app requests the custom-scheme `redirect_uri` at `/start`. This keeps a single
|
|
||||||
code path and avoids trusting an unverified `https` callback.
|
|
||||||
2. Register a broad `https` autoVerify intent-filter and **reject at runtime** any callback whose
|
|
||||||
host ≠ the paired base-URL host. AutoVerify only succeeds for domains that actually serve the
|
|
||||||
file, so in practice only real, opted-in shard domains route to the app; the runtime host check
|
|
||||||
is defense-in-depth.
|
|
||||||
- **Decision to make at build time:** whether to ship the `https` autoVerify intent-filter at all in
|
|
||||||
v1 of the native SSO client, or defer it entirely and ship custom-scheme-only. Given the spec's
|
|
||||||
guidance ("custom scheme is the practical default; App Links can be layered on per-instance"),
|
|
||||||
**custom-scheme-only for the first native-SSO release** is the recommended path.
|
|
||||||
|
|
||||||
## 4. Recommendation
|
### 3.1 Auto-served `assetlinks.json`
|
||||||
|
|
||||||
- **This round:** custom scheme only. No `assetlinks.json` route, no autoVerify intent-filter, no
|
- **Route:** `GET /.well-known/assetlinks.json`, served at the **web root** (outside `/api/v1`, before
|
||||||
pairing changes. The bridge's redirect-URI allowlist contains exactly the one fixed
|
the SPA catch-all) in `server/src/app.js`.
|
||||||
application-owned callback (`runicgateway://auth/callback`).
|
- **Gate:** the admin setting `mobile_app_links_enabled` (default **off**). Off ⇒ the route **404s** and
|
||||||
- **Follow-up (opt-in hardening), only if/when the app is published publicly:** implement §3.1
|
the app stays on the custom scheme for that shard. On ⇒ the shard opts into App Links.
|
||||||
(auto-served `assetlinks.json` behind an admin toggle) and §3.2 option 1 (App Links relied on only
|
- **Body:** the Digital Asset Links statement for the fixed package `com.runicgateway.app` and the
|
||||||
after the paired host is verified). Keep the custom scheme as the permanent fallback.
|
release signing cert SHA-256 fingerprint(s):
|
||||||
|
|
||||||
Nothing in the bridge's server design has to change to add App Links later: it is purely *more
|
```json
|
||||||
entries in the redirect-URI allowlist* plus a static file route. That is the point of keeping the
|
[
|
||||||
allowlist exact-match and configurable from day one.
|
{
|
||||||
|
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||||
|
"target": {
|
||||||
|
"namespace": "android_app",
|
||||||
|
"package_name": "com.runicgateway.app",
|
||||||
|
"sha256_cert_fingerprints": ["AB:CD:…"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Fingerprint source:** env `MOBILE_APP_CERT_SHA256` — comma-separated (supports **cert rotation** and
|
||||||
|
a debug + release cert during testing). It is a **constant of the published app**, identical for every
|
||||||
|
shard, so it is a shipped/env default, not something each owner types. The package name is likewise
|
||||||
|
fixed (`MOBILE_APP_PACKAGE`, default `com.runicgateway.app`).
|
||||||
|
- **Enabled but no fingerprint configured ⇒ 404** (+ a one-time warn): serving a statement with no
|
||||||
|
fingerprint asserts nothing and would only mislead the verifier.
|
||||||
|
- Response is `application/json`, `Cache-Control: public, max-age=3600` (the Play verifier and the OS
|
||||||
|
re-fetch it; it changes only on a cert rotation).
|
||||||
|
|
||||||
|
### 3.2 Redirect-URI allowlist extension
|
||||||
|
|
||||||
|
`mobileSso.controller` validates the app's `redirect_uri` by **exact match** against
|
||||||
|
`MOBILE_AUTH_REDIRECT_URIS` (default `runicgateway://auth/callback`). App Links add exactly one more
|
||||||
|
acceptable value, and **only when the toggle is on**:
|
||||||
|
|
||||||
|
- When `mobile_app_links_enabled`, `/start` additionally accepts the **self-origin** HTTPS callback
|
||||||
|
`https://<request-host>/mobile/callback` (derived from the request/`APP_BASE_URL`, never from
|
||||||
|
attacker-controlled input). Still **exact match** — never a prefix match.
|
||||||
|
- The static custom-scheme allowlist is never narrowed; the HTTPS entry is *additive*.
|
||||||
|
- No new table or schema: the check reads the one boolean setting.
|
||||||
|
|
||||||
|
### 3.3 Public settings advertise the capability
|
||||||
|
|
||||||
|
`settings.getPublic()` gains `mobileAppLinks: <bool>` (mirrors the toggle) so a client can tell whether
|
||||||
|
a shard opted in before requesting an HTTPS `redirect_uri` (a white-label build uses it to avoid asking
|
||||||
|
for a callback the server would reject).
|
||||||
|
|
||||||
|
## 4. App design — `feat/app-links`
|
||||||
|
|
||||||
|
### 4.1 Build-time host (`appLinkHost`)
|
||||||
|
|
||||||
|
- Gradle property `appLinkHost` (default empty). Wired in `app/build.gradle.kts` into **both**:
|
||||||
|
- `BuildConfig.APP_LINK_HOST` — read by `SsoAuthManager` to decide the redirect;
|
||||||
|
- `manifestPlaceholders["appLinkHost"]` — substituted into the App Link intent-filter's host.
|
||||||
|
- **Default (generic build):** empty ⇒ `BuildConfig.APP_LINK_HOST = ""` and the placeholder falls back
|
||||||
|
to the reserved sentinel `runic-gateway.invalid` (RFC 6761 — never resolves), so the `autoVerify`
|
||||||
|
filter is **inert**: it matches no real link and verification simply never succeeds. No custom-scheme
|
||||||
|
behaviour changes.
|
||||||
|
- **White-label build:** `./gradlew assembleRelease -PappLinkHost=play.myshard.com` bakes that one host
|
||||||
|
into the filter and enables the HTTPS redirect for that host.
|
||||||
|
|
||||||
|
### 4.2 Manifest
|
||||||
|
|
||||||
|
A second intent-filter on `MainActivity`, alongside the unchanged custom-scheme one:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<intent-filter android:autoVerify="true">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="https"
|
||||||
|
android:host="${appLinkHost}"
|
||||||
|
android:path="/mobile/callback" />
|
||||||
|
</intent-filter>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 `SsoAuthManager` (pure Kotlin, unit-tested on the JVM)
|
||||||
|
|
||||||
|
- **Redirect selection in `buildStartUrl`:** request the HTTPS `redirect_uri`
|
||||||
|
`https://<pairedHost>/mobile/callback` **iff** `BuildConfig.APP_LINK_HOST` is non-blank *and* equals
|
||||||
|
the paired base-URL host (case-insensitive); otherwise the fixed custom-scheme `REDIRECT_URI`. A
|
||||||
|
white-label build that bakes the host is responsible for enabling the server toggle too (§3.2).
|
||||||
|
- **Verified-callback matcher + host-trust check:** a new `matchesAppLinkCallback(scheme, host, path)`
|
||||||
|
accepts only `scheme == https`, `path == /mobile/callback`, and **`host == the paired base-URL host`**.
|
||||||
|
The paired-host equality is defense-in-depth: even though `autoVerify` already means only a real,
|
||||||
|
opted-in shard domain can route here, the app still refuses any HTTPS callback whose host isn't the
|
||||||
|
shard it is currently paired to.
|
||||||
|
- The rest is unchanged: both matchers feed the *same* `complete(state, code, error)` → `/exchange` →
|
||||||
|
`SessionManager.onSignedIn`. There is no second auth path.
|
||||||
|
|
||||||
|
### 4.4 `MainActivity`
|
||||||
|
|
||||||
|
`handleSsoCallback` routes a VIEW intent through **`matchesCallback(...) || matchesAppLinkCallback(...)`**;
|
||||||
|
everything downstream (state check, exchange, sign-in) is shared. Custom-scheme and App Link callbacks
|
||||||
|
are indistinguishable past the edge.
|
||||||
|
|
||||||
|
## 5. Turning it on for a shard
|
||||||
|
|
||||||
|
1. Publish/point the app build at the shard host (`-PappLinkHost=<host>`) — or use the generic build and
|
||||||
|
leave App Links off.
|
||||||
|
2. Set `MOBILE_APP_CERT_SHA256` (release cert fingerprint) in the website env.
|
||||||
|
3. Admin → Shard/Settings: enable **App Links** (`mobile_app_links_enabled`).
|
||||||
|
4. Verify `https://<host>/.well-known/assetlinks.json` returns the statement; confirm Android verifies
|
||||||
|
(`adb shell pm get-app-links com.runicgateway.app`).
|
||||||
|
|
||||||
|
If any step is skipped the app transparently keeps using the custom scheme — nothing breaks.
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
- **Server (`node --test`):** route 404s when the toggle is off; 404s when on but no fingerprint;
|
||||||
|
returns the correct statement + content-type when on and configured; the redirect allowlist accepts
|
||||||
|
`https://<host>/mobile/callback` only when enabled and rejects it otherwise (custom scheme always
|
||||||
|
accepted).
|
||||||
|
- **App (JVM unit tests):** `matchesAppLinkCallback` accepts only https + `/mobile/callback` + the paired
|
||||||
|
host and rejects a foreign host / http / wrong path; `buildStartUrl` requests the HTTPS redirect only
|
||||||
|
when the baked host matches the paired host, else the custom scheme.
|
||||||
|
|
||||||
|
## 7. What does *not* change
|
||||||
|
|
||||||
|
- The bridge's server design (PKCE Layer A/B, single-use codes, `/start` + `/exchange`) is untouched;
|
||||||
|
App Links are *one more allowlist entry* + *one static file route*. That is the whole point of keeping
|
||||||
|
the allowlist exact-match and configurable from day one.
|
||||||
|
- The custom scheme remains on every build and is the permanent fallback.
|
||||||
|
- No change to `servuo-plugins/` — App Links are entirely a website ↔ app concern.
|
||||||
|
|||||||
@@ -794,8 +794,14 @@ push, and Play (M6–M8) follow the designed app.
|
|||||||
`code_verifier`/`code_challenge` + `state`; open the Custom Tab at `/auth/mobile/sso/start`;
|
`code_verifier`/`code_challenge` + `state`; open the Custom Tab at `/auth/mobile/sso/start`;
|
||||||
verify `state` on the callback; `POST …/exchange`; store the returned pair in the existing
|
verify `state` on the callback; `POST …/exchange`; store the returned pair in the existing
|
||||||
`TokenStore` (M3). No new token-storage or refresh code — it feeds the M3 session machinery.
|
`TokenStore` (M3). No new token-storage or refresh code — it feeds the M3 session machinery.
|
||||||
- **Deferred:** App Links / per-shard `assetlinks.json` / pairing — custom scheme only for now
|
- **Follow-up — App Links (opt-in hardening on top of Part 2):** a website `GET
|
||||||
([`APP_LINKS.md`](./APP_LINKS.md)).
|
/.well-known/assetlinks.json` route behind the `mobile_app_links_enabled` admin toggle, a
|
||||||
|
self-origin HTTPS entry added to the redirect-URI allowlist when enabled, and an app-side
|
||||||
|
`autoVerify` intent-filter for `https://<host>/mobile/callback` driven by a **build-time**
|
||||||
|
`appLinkHost` (a single multi-tenant APK cannot autoVerify open-ended shard domains, so the
|
||||||
|
generic build stays custom-scheme; white-label/first-party builds bake one host). The custom
|
||||||
|
scheme remains the permanent fallback on every build. Full spec + rollout in
|
||||||
|
[`APP_LINKS.md`](./APP_LINKS.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -915,7 +921,12 @@ it is a UX convenience, not a v1 requirement — deferred at M3, descoped at M6;
|
|||||||
`runicgateway.app` domain (needed for a verified app-link host and a matching package namespace). Also
|
`runicgateway.app` domain (needed for a verified app-link host and a matching package namespace). Also
|
||||||
the fixed launcher name (baked at build even though in-app branding is per-shard — one APK, any shard).
|
the fixed launcher name (baked at build even though in-app branding is per-shard — one APK, any shard).
|
||||||
Since SSO/invite/reset are website-handled, the app mostly *opens* website URLs rather than needing its
|
Since SSO/invite/reset are website-handled, the app mostly *opens* website URLs rather than needing its
|
||||||
own verified app links — confirm whether any deep-link-back is wanted at all for v1.
|
own verified app links. **App Links resolved (M9 follow-up):** the SSO callback is the one place a
|
||||||
|
verified deep-link-back helps; the server side (`assetlinks.json` + toggle) ships for any shard, but
|
||||||
|
the app-side `autoVerify` needs a **literal build-time host**, so it is a white-label/first-party build
|
||||||
|
opt-in (`-PappLinkHost=<host>`) — the generic multi-tenant build stays custom-scheme. A canonical
|
||||||
|
`runicgateway.app` relay host, if secured, would let the generic build autoVerify one central domain.
|
||||||
|
See [`APP_LINKS.md`](./APP_LINKS.md).
|
||||||
- ntfy: exact upstream image + pinned tag (Part-1 landed the compose service — confirm the tag), and
|
- ntfy: exact upstream image + pinned tag (Part-1 landed the compose service — confirm the tag), and
|
||||||
its reverse-proxy hostname/path. The hostname must land in `NTFY_ALLOWED_ORIGINS` before M7 Part 2 is
|
its reverse-proxy hostname/path. The hostname must land in `NTFY_ALLOWED_ORIGINS` before M7 Part 2 is
|
||||||
end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). No
|
end-to-end testable (the app registers an endpoint on that origin; the SSRF guard rejects others). No
|
||||||
|
|||||||
@@ -317,8 +317,17 @@ layers (a different app instance triggering `/start` cannot complete someone els
|
|||||||
against a configured allowlist (`MOBILE_AUTH_REDIRECT_URIS`, default the one fixed application-owned
|
against a configured allowlist (`MOBILE_AUTH_REDIRECT_URIS`, default the one fixed application-owned
|
||||||
callback `runicgateway://auth/callback`) — **never prefix match** (prefix matching on custom schemes
|
callback `runicgateway://auth/callback`) — **never prefix match** (prefix matching on custom schemes
|
||||||
is a known open-redirect vector). Tokens are **never** placed in the callback URL — only the
|
is a known open-redirect vector). Tokens are **never** placed in the callback URL — only the
|
||||||
short-lived authorization code. HTTPS App Link URIs can be appended to the allowlist later per shard;
|
short-lived authorization code.
|
||||||
that is the *only* server change App Links require (see docs/android/APP_LINKS.md).
|
|
||||||
|
*App Links (implemented).* When the admin toggle `mobile_app_links_enabled` is **on**, `/start` also
|
||||||
|
accepts the self-origin HTTPS callback `https://<request-host>/mobile/callback` — one *additive*
|
||||||
|
exact-match entry, derived from the request/`APP_BASE_URL` and never from client input; the
|
||||||
|
custom-scheme allowlist is never narrowed. The shard then auto-serves `GET
|
||||||
|
/.well-known/assetlinks.json` (fixed package `com.runicgateway.app` + `MOBILE_APP_CERT_SHA256`
|
||||||
|
fingerprints; 404 when the toggle is off or no fingerprint is configured), and
|
||||||
|
`settings.getPublic()` advertises `mobileAppLinks: <bool>`. These two things — one static file route
|
||||||
|
and one more allowlist entry — are the *entire* server surface App Links require. See
|
||||||
|
docs/android/APP_LINKS.md.
|
||||||
|
|
||||||
**TOTP through the bridge.** A 2FA account keeps full parity: the callback stages the existing
|
**TOTP through the bridge.** A 2FA account keeps full parity: the callback stages the existing
|
||||||
pending-TOTP cookie (now also carrying the bridge `session_id`) and bounces the Custom Tab through the
|
pending-TOTP cookie (now also carrying the bridge `session_id`) and bounces the Custom Tab through the
|
||||||
|
|||||||
Reference in New Issue
Block a user