Skip to main content

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.