diff --git a/apps/apple/CascadeMac/Cascade.entitlements b/apps/apple/CascadeMac/Cascade.entitlements index ee95ab7..f6d0d59 100644 --- a/apps/apple/CascadeMac/Cascade.entitlements +++ b/apps/apple/CascadeMac/Cascade.entitlements @@ -2,6 +2,10 @@ + com.apple.developer.associated-domains + + applinks:cascade.stephens.page + com.apple.security.app-sandbox com.apple.security.network.client diff --git a/apps/apple/CascadeiOS/Cascade.entitlements b/apps/apple/CascadeiOS/Cascade.entitlements index 3d1ff87..8773a5c 100644 --- a/apps/apple/CascadeiOS/Cascade.entitlements +++ b/apps/apple/CascadeiOS/Cascade.entitlements @@ -2,7 +2,9 @@ - + com.apple.developer.associated-domains + + applinks:cascade.stephens.page + diff --git a/apps/apple/project.yml b/apps/apple/project.yml index ecca7cc..ba6e2f5 100644 --- a/apps/apple/project.yml +++ b/apps/apple/project.yml @@ -59,6 +59,14 @@ targets: # Outbound network for the optional account/listening-time sync # (magic-link sign-in + G-Counter sync to cascade-sync-server). com.apple.security.network.client: true + # Universal Links: lets the magic-link email (https://cascade.stephens.page + # /auth?token=…) open the app straight into completeSignIn() instead of a + # browser. Requires the matching AASA file hosted at + # https://cascade.stephens.page/.well-known/apple-app-site-association and + # a provisioning profile with the Associated Domains capability (paid + # Apple Developer Program — not available under free personal-team signing). + com.apple.developer.associated-domains: + - applinks:cascade.stephens.page settings: base: PRODUCT_BUNDLE_IDENTIFIER: page.stephens.cascade @@ -107,6 +115,13 @@ targets: NSHumanReadableCopyright: "© 2026 Jacob Stephens" entitlements: path: CascadeiOS/Cascade.entitlements + properties: + # Universal Links — see the macOS target above. iOS reaches the network + # without a sandbox entitlement, so associated-domains is the only key. + # (Must live here, not in the .entitlements file: XcodeGen regenerates + # that file on every run and would clobber a hand-written key — see #1.) + com.apple.developer.associated-domains: + - applinks:cascade.stephens.page settings: base: PRODUCT_BUNDLE_IDENTIFIER: page.stephens.cascade diff --git a/apps/web/public/.well-known/apple-app-site-association b/apps/web/public/.well-known/apple-app-site-association new file mode 100644 index 0000000..34eb42f --- /dev/null +++ b/apps/web/public/.well-known/apple-app-site-association @@ -0,0 +1,16 @@ +{ + "applinks": { + "details": [ + { + "appIDs": ["G38J85UN6P.page.stephens.cascade"], + "components": [ + { + "/": "/auth", + "?": { "token": "?*" }, + "comment": "Magic-link sign-in: open Cascade instead of the website." + } + ] + } + ] + } +} diff --git a/apps/web/public/auth/index.html b/apps/web/public/auth/index.html new file mode 100644 index 0000000..367c405 --- /dev/null +++ b/apps/web/public/auth/index.html @@ -0,0 +1,115 @@ + + + + + + + + Cascade — Sign in + + + + + +
+

Sign in to Cascade

+
+

+ If the Cascade app is installed it should have opened automatically. + Otherwise, open Cascade, then paste this sign-in code into the + “paste the sign-in link” box: +

+
+ + +
+

This code is single-use and expires 15 minutes after it was emailed.

+
+
+ + + diff --git a/docs/universal-link-signin.md b/docs/universal-link-signin.md new file mode 100644 index 0000000..8476474 --- /dev/null +++ b/docs/universal-link-signin.md @@ -0,0 +1,77 @@ +# Universal Link sign-in (open the magic link in the app) + +The account magic-link email contains +`https://cascade.stephens.page/auth?token=…`. By default that URL opens the +**website**. With Universal Links configured it instead opens the **Cascade +app** directly into `AppStore.completeSignIn(…)`, so the user never copies a +token by hand. + +## Pieces (all in this repo) + +| Piece | Where | Status | +|-------|-------|--------| +| `onOpenURL` → `handleOpenURL` → `completeSignIn` | `CascadeShared/App/AppStore.swift`, `Cascade*App.swift` | already wired | +| `associated-domains` entitlement (`applinks:cascade.stephens.page`) | `apps/apple/project.yml` (both targets, via `entitlements.properties` so XcodeGen doesn't clobber it) | added | +| AASA file | `apps/web/public/.well-known/apple-app-site-association` | added | +| Browser fallback for un-installed devices | `apps/web/public/auth/index.html` (shows the token to paste) | added | + +The app already extracts the token from either a full link or a bare token, so +no Swift changes are needed — only the OS-level link registration above. + +## Requirement: paid Apple Developer Program + +The `associated-domains` capability is **not available under free +personal-team signing**. A *signed* build with this entitlement fails with: + +``` +"CascadeMac" has entitlements that require signing with a development certificate. +``` + +So to ship this you must: + +1. Enrol in the paid Apple Developer Program. +2. Set `DEVELOPMENT_TEAM` in `project.yml` to your Team ID (`G38J85UN6P`) and + build with development/distribution signing (not ad-hoc `-`). +3. Enable the **Associated Domains** capability for the App ID in the developer + portal. + +> CI is unaffected: `apple.yml` compiles with `CODE_SIGNING_ALLOWED=NO`, which +> skips entitlement validation. Only signed artifacts (the ad-hoc DMG and the +> free `install-device.sh` flow) require the paid cert — those will need the +> paid team once this is merged. + +## Hosting the AASA file + +Apple fetches `https://cascade.stephens.page/.well-known/apple-app-site-association` +(no redirects, `Content-Type: application/json`, **no** `.json` extension). The +file is plain static content under `apps/web/public/`, but verify after deploy: + +```bash +curl -sI https://cascade.stephens.page/.well-known/apple-app-site-association +# 200, content-type application/json, served directly (no 30x) +``` + +Two gotchas: + +- The web build must copy the dotfolder. Confirm `.well-known/` lands in the + built `dist/` and the web server (Apache vhost) serves dotfolders and + extensionless files as JSON. +- Apple caches the AASA via its CDN + (`https://app-site-association.cdn-apple.com/a/v1/cascade.stephens.page`); + changes can take time to propagate. On a dev device, reinstalling the app + re-fetches it. + +## Verify end-to-end (signed build, real device) + +1. Install a properly-signed build on a device/Mac with the app. +2. Request a sign-in link from the app, open Mail, tap the link. +3. The app should foreground and land signed-in (no paste). If it opens the + website instead, the AASA isn't being served correctly or the entitlement + isn't in the signed build. + +## Security note + +The token rides in the URL, so keep it **single-use + short-lived** (the server +already expires it after 15 minutes / one use). Universal Links are safer than a +custom `cascade://` scheme because Apple verifies domain ownership via the AASA +file, so no other app can claim `cascade.stephens.page/auth`.