eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Native onboarding: a link β†’ the app β†’ signed in
2
3**Status: design (2026-07-09), sequenced for building.** The goal, stated as
4the experience: someone taps an invite link and ends up **in the native app**,
5**in the space**, and **signed in with a passkey** β€” and when they later set up
6their other device (Mac first or iPhone first, either order), the passkey
7ceremony on the second device *is* the sync: everything appears.
8
9This replaces the retired paste-a-link joins (deleted 2026-07-08 with the
10sign-in-only door) with something better, not with their return. The design
11leans on two invariants from `docs/onboarding-and-home-server.md` that are
12already load-bearing:
13
14- **Deferred claim.** Opening a link never consumes the invite β€” claiming is
15 an explicit tap. This is what makes "the app AND the browser both saw the
16 link" safe: whoever *joins* claims; everyone else just looked.
17- **Fragments never reach a server.** The invite secret `T` lives in the
18 `#fragment`; native parsing (`InviteLink.swift`) and the claim derivation
19 (`InviteCrypto.derive`) keep it that way.
20
21## What already exists (more than it looks)
22
23The 2026-07-09 create-a-space work built the missing spine without meaning to
24advertise it:
25
26- **`SpaceClaim` (LChatCore)** claims a key-invite **natively**: derive
27 `{id, wrapKey, claimSecret}` from `T`, `GET /api/key-invite/{id}`, claim for
28 a device token, generate the identity keypair, register the pubkey, resolve
29 `myUserId`, even bootstrap the instance key when first-in. Today it handles
30 the setup-invite shape; the member/DM and add-device shapes are small
31 extensions (below).
32- **Every instance already serves the AASA.** `GET
33 /.well-known/apple-app-site-association` (main.go `serveAASA`) matches
34 `/join/*` for the iOS app id on **every** instance, tested in
35 `handlers_test.go`. The server side of universal links shipped long ago.
36- **`inviteLink(fromOpenedURL:)` + `parseJoinLink`** (LChatCore) still parse
37 both the custom scheme (`elevenmessenger://add?url=…` β€” the join page's
38 "Open in Eleven" button) and raw https join links.
39- **Passkey sign-in and the vault sync** are shipped on both platforms; the
40 cross-device story ("the second device's passkey dance does the sync") is
41 already true for everything in the vault.
42
43What's missing is the *middle*: neither app currently handles a join URL at
44all (iOS has no `onOpenURL`; the Mac accepts only the sign-in callback), the
45iOS `applinks` entitlement covers `*.elevenmessenger.com` but not `*.11ch.at`
46where the fleet actually lives, and there's no native welcome screen.
47
48## The flow, end to end
49
50### A. The link opens the app (iOS first)
51
521. **Entitlements**: add `applinks:*.11ch.at` beside the existing
53 `applinks:*.elevenmessenger.com`. (Apple's rule: one wildcard prefix per
54 entry, every domain named in the shipped binary β€” so arbitrary custom
55 domains like `chat.paulca.com` can't be covered generically. They keep the
56 web join page, which still works everywhere, plus its "Open in Eleven"
57 custom-scheme button that reaches the same handler.)
582. **Handlers**: `onOpenURL` + `onContinueUserActivity(NSUserActivityTypeBrowsingWeb)`
59 on the iOS root β†’ `inviteLink(fromOpenedURL:)` β†’ `parseJoinLink` β†’ present
60 the **welcome card** (never claim here β€” deferred claim).
613. App not installed: the link lands on the web join page exactly as today.
62 Distribution polish (smart App Store banner on join.html) is phase 4 of
63 the onboarding doc and stays there.
64
65### B. The native welcome card
66
67A sheet with the space's **public branding** (`/api/group` name +
68`/api/server-avatar` β€” public by design so this exact card can exist), the
69suggested name from the fragment's `~name` tail prefilled into a name field,
70and one button: **Join**. Tapping Join is the explicit claim.
71
72`SpaceClaim` grows the two remaining invite shapes (both already documented in
73join.js):
74
75- **Member/DM invite** (`{threadKey}` payload): decrypt the thread key with
76 `wrapKey`, generate the keypair, wrap the thread key to our own new pubkey,
77 send that as `wrapped` in the claim. Lands in the pending DM.
78- **Add-device link** (`{priv, pub}` payload): adopt the existing identity β€”
79 no new keypair, no label publish.
80
81Either way the result is a `SavedInstance` through the same `app.add` path,
82which persists it, selects it, and pushes it to the vault when signed in.
83
84### C. The passkey moment
85
86Right after landing in the space β€” not before, the chat is the payoff β€” the
87app makes the save-your-spaces ask:
88
89- **No home session yet**: one inline card at the top of the thread list
90 (never a toast): "Keep this space safe β€” save it to Eleven" β†’ the existing
91 explicit mint (`HomeSignInModel.backUp`). This is the native twin of the
92 welcome conversation's `access` step, which deliberately runs *first* on
93 the web too. Declining defers, matching the web's reminder cadence.
94- **Session exists** (e.g. an invited member who already had Eleven): nothing
95 to ask β€” `app.add` already pushed the new space into the vault.
96
97### D. The second device
98
99Nothing new to build: install the app β†’ **Sign in** β†’ the passkey ceremony
100restores the vault β†’ every space appears with credentials, order, stars. This
101is shipped behavior; the design just makes it the *stated* contract: **the
102sync IS the sign-in.**
103
104### E. The Mac
105
106Same shape, one platform beat behind (iOS-first rule):
107
1081. Server: `serveAASA` adds the Mac app id
109 (`J5NDF5PCD8.com.elevenmessenger.messenger.mac`) beside the iOS one β€”
110 **done** (PR #1383).
1112. Entitlements: the Mac gains its first `applinks` entries (same domains) β€”
112 **done** (`LChatMac.entitlements`, with #1383's server half; the Mac App
113 ID needs the Associated Domains capability before a release build's
114 profile authorizes it).
1153. `LChatMacApp.onOpenURL` grows a second accepted URL family: join links β†’
116 the same welcome card in a sheet.
117
118macOS routes universal links less aggressively than iOS (browser-first unless
119the user opts in), so the join page's "Open in Eleven" button matters more
120here β€” it fires the custom scheme, which macOS routes unconditionally.
121
122## What deliberately stays out
123
124- ~~**No welcome *conversation* natively (yet).**~~ **Superseded β€” the
125 interpreter shipped.** The Swift flow interpreter is real now
126 (`FlowEngine.swift` running the bundled `Flows/welcome.json`, a byte-derived
127 copy of `welcome.flow.js` drift-gated by `test/flow.test.mjs`, hosted by
128 `ElevenConversationModel`), and a native member/DM-invite claim creates the
129 welcome app-thread and lands the joiner in the full conversation β€” which
130 asks their name β€” on both platforms (`SpaceClaim.claim` β†’
131 `WelcomeLanding.markPending`/`threadToLand`, #1376). The passkey ceremony
132 step stays web-only by design (`env.native` skips it β€” native access *is*
133 the sign-in).
134- **No QR scanning revival, no paste field.** Links open the app; that's the
135 door. (The `NSCameraUsageDescription` string left from the retired QR flow
136 should be dropped when the welcome card lands.)
137- **No change to web joins.** join.html remains the universal fallback and
138 the only path on uncovered custom domains.
139
140## Build order
141
1421. **iOS**: entitlement + URL handlers + welcome card + `SpaceClaim` member/DM
143 + add-device shapes (with tests against join.js's shapes) + the
144 post-join passkey nudge. Ships alone; Mac unaffected.
1452. **Server**: AASA gains the Mac app id (one-line change + test update).
1463. **Mac**: entitlements + handler + the same card.
1474. **Polish**: smart banner on join.html; drop the stale camera-permission
148 string; revisit the welcome conversation once scripts run natively.
149
150## Risks / open questions
151
152- **TestFlight + associated domains**: applinks work on TestFlight builds via
153 the AASA's app id β€” no `?mode=developer` needed for the new wildcard (that
154 mode never worked on macOS anyway; see LChatMac-Dev.entitlements).
155- **Wildcard AASA fetch**: Apple's CDN fetches the AASA per-host on install;
156 `*.11ch.at` hosts each serve it (single binary β€” already true).
157- **A claimed-but-never-saved device**: someone joins natively, never saves a
158 passkey, loses the phone β†’ the space is orphaned like any keyless loss.
159 Unchanged from web behavior; the reminder cadence is the mitigation.
160