From 03b0be18eaa6fc532b54986795d92d801c5b9fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 18:57:53 +0200 Subject: [PATCH] feat(ios): serve apple-app-site-association on the app domains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Next.js Route Handler at `/.well-known/apple-app-site-association` that answers 200 with `application/json` on every host that fronts the main app — both `healthlog.bombeck.io` (maintainer prod) and `demo.healthlog.dev` (demo) share this handler since they ride the same standalone runtime. The body advertises the iOS bundle's App ID prefix (`S8WDX4W5KX.dev.healthlog.app`) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app — the same ceremony that today exists at `/api/auth/passkey/*` on both domains. The `applinks.details` array stays empty for now; Universal Links are out of scope for the first cut and pick up later once the iOS side opts in to deep-linking specific routes. Pairs with the proxy `/.well-known/` allowlist entry — without that bypass the auth gate would 307 Apple's CDN fetch to `/auth/login` before the handler ran. --- .../apple-app-site-association/route.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/app/.well-known/apple-app-site-association/route.ts diff --git a/src/app/.well-known/apple-app-site-association/route.ts b/src/app/.well-known/apple-app-site-association/route.ts new file mode 100644 index 000000000..668f62d26 --- /dev/null +++ b/src/app/.well-known/apple-app-site-association/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from "next/server"; + +/** + * Apple App Site Association (AASA) handler. + * + * Apple fetches `/.well-known/apple-app-site-association` over HTTPS + * without credentials, with no extension and with a strict + * `application/json` Content-Type expectation. iOS uses the body to: + * - Wire Web Credentials so the website origin and the iOS bundle + * share passkey ceremonies (HealthLog passkeys live at + * `/api/auth/passkey/*`, so the origins must match the same App ID). + * - Wire Universal Links — out of scope for the first cut, hence the + * empty `applinks.details` array. Future entries here pick up + * deep-linking once the iOS side opts in. + * + * The `webcredentials.apps` entry is the App ID prefix + * (`.`) for the HealthLog iOS app. Apple bundles the + * file on its CDN proxy + * (`https://app-site-association.cdn-apple.com/a/v1/`) and serves + * it to devices, so the origin only needs to respond reliably; Apple's + * one-hour CDN TTL pairs with the `Cache-Control` directive below. + * + * Both `healthlog.bombeck.io` (maintainer prod) and `demo.healthlog.dev` + * (demo) share this handler — the response is host-independent. + */ +const AASA = { + applinks: { + apps: [], + details: [], + }, + webcredentials: { + apps: ["S8WDX4W5KX.dev.healthlog.app"], + }, +}; + +export function GET() { + return NextResponse.json(AASA, { + headers: { + "Content-Type": "application/json", + "Cache-Control": "public, max-age=3600", + }, + }); +}