Documentation Index
Fetch the complete documentation index at: https://docs.myme.so/llms.txt
Use this file to discover all available pages before exploring further.
Authentication patterns
API keys are the simplest path — embed or seed via fromKeychain(...) and move on. Third-party apps that want per-user consent use OAuth 2.1 with PKCE instead. The SDK doesn’t ship a built-in OAuth client — it’s an integration pattern — but the Keychain helpers handle token storage and rotation once you’ve exchanged for tokens.
OAuth 2.1 with ASWebAuthenticationSession
Four moving parts: ASWebAuthenticationSession for the authorize redirect, URLSession for the token exchange, the SDK’s Keychain helpers for persistence, and a catch on UnauthorizedError to trigger refresh.
1. Launch the authorize flow. Generate a PKCE verifier + challenge, build /auth/authorize, and let ASWebAuthenticationSession handle the redirect back.
import AuthenticationServices
import CryptoKit
let verifier = randomURLSafeBase64(bytes: 32)
let challenge = Data(SHA256.hash(data: Data(verifier.utf8)))
.base64EncodedString()
.replacingOccurrences(of: "=", with: "")
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
var authorize = URLComponents(string: "\(serverURL)/auth/authorize")!
authorize.queryItems = [
.init(name: "client_id", value: clientId),
.init(name: "redirect_uri", value: "\(scheme)://callback"),
.init(name: "response_type", value: "code"),
.init(name: "scope", value: "core.note:read core.note:write"),
.init(name: "state", value: randomURLSafeBase64(bytes: 16)),
.init(name: "code_challenge", value: challenge),
.init(name: "code_challenge_method", value: "S256"),
]
let session = ASWebAuthenticationSession(
url: authorize.url!,
callbackURLScheme: scheme
) { callbackURL, error in
// Parse `code` and verify `state` round-trips, then POST /auth/token below.
}
session.presentationContextProvider = contextProvider
session.start()
2. Exchange the code for tokens via POST /auth/token with grant_type=authorization_code, the code, and the original code_verifier. The response carries access_token (marfa_at_...), refresh_token (marfa_rt_...), and expires_in.
3. Persist the access token via the SDK’s Keychain helper — it becomes the API key for subsequent calls. Store the refresh token separately (same service, different account).
let client = MarfaClient(url: serverURL, apiKey: tokens.accessToken)
try await client.saveToKeychain(service: "marfa.oauth", account: "access_token")
// refresh_token stored with account: "refresh_token" via SecureStorage of your choosing
On subsequent launches, rehydrate without the auth flow:
let client = try await MarfaClient.fromKeychain(
service: "marfa.oauth",
account: "access_token",
url: serverURL
)
4. Refresh on expiry. Any SDK call can throw UnauthorizedError when the access token expires. Catch it, exchange the refresh token for a new pair, overwrite the Keychain entries, and retry:
do {
return try await client.items.list(filters: .init(type: "core.note"))
} catch is UnauthorizedError {
try await refreshTokens() // POST /auth/token, grant_type=refresh_token
return try await client.items.list(filters: .init(type: "core.note"))
}
Refresh tokens rotate on every exchange — the prior refresh token is invalidated. Reusing a rotated token returns 400 token_reuse_detected; the app must restart the authorize flow. Store the latest pair atomically.
ASWebAuthenticationSession requires a UI host — a UIWindow on iOS, NSWindow on macOS. Server-side Swift, background extensions, and command-line tools cannot run this flow; they must use an API key.
See Authentication → OAuth 2.1 for the full endpoint reference, scope grammar, consent-screen behavior, and refresh-token rotation rules.