eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1import AuthenticationServices
2import Foundation
3
4// πŸ€– The identity anchors β€” Sign in with Apple and email codes (issues
5// #1091/#1112) β€” as native doors. An anchor verify answers exactly like a
6// passkey ceremony finish ({token, proof, created}), but no PRF is in play,
7// so an anchor session AUTHENTICATES and can never DECRYPT: the backup blob
8// stays sealed until a passkey sign-in supplies the PRF. These are the small
9// client calls over home's endpoints (siwa.go / email.go), riding
10// HomeClient's base + plumbing; sheet presentation belongs to each
11// platform's UI (SwiftUI's SignInWithAppleButton on iOS).
12
13extension HomeClient {
14 /// What every anchor verify answers β€” the same shape as a ceremony
15 /// finish, plus whether the account was created on the spot (the
16 /// seamless front door: an unknown Apple ID / address becomes a fresh
17 /// account, no separate registration).
18 public struct AnchorFinish: Decodable {
19 public let token: String
20 public let proof: String?
21 public let created: Bool?
22 }
23
24 /// POST /api/siwa/verify β€” Apple's signed identity token in, a session
25 /// out. The nonce must be the very string the sheet's request carried;
26 /// home compares the token's nonce claim against it verbatim.
27 public func siwaVerify(identityToken: String, nonce: String) async throws -> AnchorFinish {
28 try await postJSON("/api/siwa/verify",
29 ["identity_token": identityToken, "nonce": nonce])
30 }
31
32 /// POST /api/email/request β€” send the 6-digit code (15-minute,
33 /// single-use). The server answers the same 200 known-or-not (no
34 /// account enumeration), so success only ever means "sent".
35 public func emailRequest(email: String) async throws {
36 struct Sent: Decodable { let sent: Bool? }
37 let _: Sent = try await postJSON("/api/email/request", ["email": email])
38 }
39
40 /// POST /api/email/verify β€” the typed code in, a session out.
41 public func emailVerify(email: String, code: String) async throws -> AnchorFinish {
42 try await postJSON("/api/email/verify", ["email": email, "code": code])
43 }
44
45 /// POST /api/oidc/{provider}/verify β€” a Google/Microsoft identity token
46 /// in, a session out (oidc.go answers the same contract as SiwA). The
47 /// nonce is the very string the authorize URL carried; home checks the
48 /// token's nonce claim against it verbatim.
49 public func oidcVerify(provider: String, identityToken: String, nonce: String) async throws -> AnchorFinish {
50 try await postJSON("/api/oidc/\(provider)/verify",
51 ["identity_token": identityToken, "nonce": nonce])
52 }
53
54 /// POST /api/oidc/github/verify β€” GitHub's single-use code in, a session
55 /// out; the server holds the client secret and does the exchange
56 /// (github.go), so no token or nonce ever reaches the client.
57 public func githubVerify(code: String) async throws -> AnchorFinish {
58 try await postJSON("/api/oidc/github/verify", ["code": code])
59 }
60
61 /// GET /api/features' doors block β€” which web-flow doors this home
62 /// server has lit, plus GitHub's per-instance client id. Nil on any
63 /// error or an older server: callers simply don't show those doors.
64 public struct SigninDoors: Decodable, Equatable, Sendable {
65 public let google: Bool?
66 public let microsoft: Bool?
67 public let githubClientID: String?
68 enum CodingKeys: String, CodingKey {
69 case google, microsoft
70 case githubClientID = "github_client_id"
71 }
72 }
73
74 public func signinDoors() async -> SigninDoors? {
75 struct R: Decodable { let doors: SigninDoors? }
76 guard let (data, resp) = try? await URLSession.shared.data(from: base.appending(path: "/api/features")),
77 (resp as? HTTPURLResponse)?.statusCode == 200
78 else { return nil }
79 return (try? JSONDecoder().decode(R.self, from: data))?.doors
80 }
81
82 private func postJSON<T: Decodable>(_ path: String, _ body: [String: String]) async throws -> T {
83 var req = URLRequest(url: base.appending(path: path))
84 req.httpMethod = "POST"
85 req.setValue("application/json", forHTTPHeaderField: "Content-Type")
86 req.httpBody = try JSONEncoder().encode(body)
87 let (data, resp) = try await URLSession.shared.data(for: req)
88 try Self.ensureOK(resp, data)
89 return try JSONDecoder().decode(T.self, from: data)
90 }
91}
92
93/// The Apple door's non-UI halves: the nonce and the token extraction.
94public enum HomeAnchorAuth {
95 /// 32 random bytes as hex β€” set as the Apple request's nonce AND sent in
96 /// the verify body, the SAME string both times (home compares the
97 /// identity token's nonce claim verbatim; no hashing convention rides in
98 /// between, unlike some SiwA integrations).
99 public static func mintNonce() -> String {
100 var bytes = [UInt8](repeating: 0, count: 32)
101 _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
102 return bytes.map { String(format: "%02x", $0) }.joined()
103 }
104
105 /// The signed JWT out of Apple's credential (UTF-8 bytes on the wire).
106 public static func identityToken(from credential: ASAuthorizationAppleIDCredential) -> String? {
107 credential.identityToken.flatMap { String(data: $0, encoding: .utf8) }
108 }
109}
110