Skip to content

feat: Sign in with AssemblyAI (PE-2657) - #254

Merged
alexkroman merged 4 commits into
mainfrom
aawoyemi/feat/PE-2657/assemblyai-sign-in
Oct 21, 2026
Merged

alexkroman merged 4 commits into
mainfrom
aawoyemi/feat/PE-2657/assemblyai-sign-in

Conversation

@damafeez

@damafeez damafeez commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What & why

Adds "Sign in with AssemblyAI" as an alternative to pasting a key (PE-2657, part of the Agent OAuth epic PE-2644). The member signs in and approves Blurt on AssemblyAI's consent page. Blurt then trades the access token for the connection's own API key and stores it where a pasted key goes, so dictation, the press gate and Settings read it unchanged.

Three commits:

  1. Sign in with AssemblyAI
    • Authorization code with PKCE, as a public client, in ASWebAuthenticationSession.
    • Renewal at launch, then when the key rotates (see the third commit).
    • Sign Out ends the connection server-side (POST /api/v2/oauth/api-key/revoke), then clears it on the Mac. If the server can't confirm, everything stays in place and the user can retry.
  2. A signed-in key dictates against its sign-in's host. AssemblyAITranscriber now takes its host as a baseURL closure, read at every request and warm-up. The app's closure returns the signed-in key's host (DictationOrigin), and production otherwise. A non-production key only works against its own deployment's dictation host.
  3. Renew when the key rotates, not daily.
    • Renewal is due at the exchange's api_key_rotates_at (from DeepLearning#22337, which rotates inside a window before expiry). Without it, renewal falls back to two days before expiry.
    • runRenewal sleeps until then instead of checking hourly.
    • KeyRejectionReporter renews at once when dictation refuses the key.
    • A guard stops a server that returns the same key from causing a retry loop.

Off by default. A build turns it on with BLURT_OAUTH_* settings. They name AssemblyAI's non-production deployments, so they aren't in this repo:

  • They live in a gitignored App/Blurt/Config/SignIn.local.xcconfig, which SignIn.xcconfig includes for the dev configurations only.
  • Copy SignIn.local.xcconfig.example to set it up.
  • Release has none, so shipping builds offer only the paste-a-key path.

AGENTS.md (Settings, persistence) describes the design.

How it was tested

  • scripts/check.sh passes locally on each commit: engine coverage 89.1%, iOS check green, swift-format and swiftlint clean
  • I read AGENTS.md and this doesn't reintroduce anything deliberately removed. There are no new dependencies (CryptoKit is a system framework).
  • Docs updated: AGENTS.md, Sources/AssemblyAI/README.md, Sources/BlurtEngine/README.md

Unit tests:

  • PKCE against RFC 7636's worked example.
  • Callback checks.
  • Token, exchange and revoke status mapping.
  • Renewal rules.
  • Sign-out paths, including an unreadable Keychain item.
  • Two race tests: a sign-out or reset during a renewal must win.
  • The transcriber reading its host per request.

End to end against a non-production deployment (ad-hoc Debug build):

  • Sign-in completes.
  • Microphone and Accessibility are granted.
  • With the first commit alone, dictation got a 401. The second commit points a signed-in key at its own deployment's dictation host.

Reviewed by the repo's three agents: swift6-concurrency, macos-hig and cleanup. Their findings are applied.

🤖 Generated with Claude Code

Screenshots
image

image

damafeez and others added 2 commits October 20, 2026 13:30
The alternative to pasting a key: the member signs in and approves Blurt
on AssemblyAI's consent page (authorization code + PKCE, public client,
ASWebAuthenticationSession, redirect com.assemblyai.blurt://oauth/callback),
and Blurt trades the access token for the connection's own API key, which
it stores where a pasted key goes.

- AssemblyAI library: OAuthAuthorization (PKCE, consent URL, callback
  check) and OAuthClient (token endpoint, POST /api/v2/oauth/api-key and
  /revoke). Refused calls log the route, status and server reason, never
  a token.
- Engine: AssemblyAIAccount (sign-in, renewal, sign-out), SignInSession
  in its own Keychain item with a read that keeps "none" apart from
  "unavailable", CredentialRows (which rows show), runRenewal (launch,
  then hourly; renews daily or within two days of key expiry). A
  generation counter keeps a renewal from undoing a sign-out or reset.
- Shell: AccountSignIn (the web session) and AccountRowView ("Sign In
  with AssemblyAI…" / "Signed in as …" + Sign Out; recoverable failures
  inline, Keychain faults as alerts). The reset clears the session too.
- Off unless a build sets the BLURT_OAUTH_* settings. They name
  AssemblyAI's non-production deployments, so they live in a gitignored
  App/Blurt/Config/SignIn.local.xcconfig, included by SignIn.xcconfig on
  the dev configurations only (example file committed). Release has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A key minted by a non-production sign-in only works against that
deployment's dictation host, while dictation always went to production,
so such a key got a 401 on every dictation.

- AssemblyAITranscriber takes its host as a closure (`baseURL`), read at
  every request and warm-up, in place of a fixed URL. The default is
  `defaultBaseURL` (production); a data-zone host goes here too.
- DictationOrigin (a Mutex) holds the signed-in key's host. The account
  keeps it current as it signs in and out, and the app's `baseURL`
  closure reads it, so the transcriber knows nothing of sign-in.
- BLURT_OAUTH_DICTATION_ORIGIN (optional, in the gitignored local
  xcconfig) sets that host. Unset, or a pasted key, means production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@damafeez damafeez mentioned this pull request Oct 20, 2026
3 tasks done
damafeez and others added 2 commits October 20, 2026 15:37
- Renewal is due when the exchange says the key rotates
  (api_key_rotates_at, AMS's early-rotation window), else two days
  before expiry. No daily rule: one renewal per key is all the refresh
  token's three-month sliding lifetime needs.
- runRenewal sleeps until the renewal is due (at most a day per sleep,
  on the continuous clock so a renewal due during sleep runs on wake)
  instead of checking hourly.
- KeyRejectionReporter wraps the transcriber: a dictation refused for
  its key (401, or the API's invalid-key 404) calls keyWasRejected(),
  which renews at once, at most every 15 minutes. The refused utterance
  itself can't be retried.
- A renewal that brings back the same key with its renewal already due
  waits for the key's expiry (or a day once expired) instead of asking
  every minute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jscpd flagged the two renewal-race tests' identical nine-line setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@damafeez
damafeez marked this pull request as ready for review October 21, 2026 17:27
@damafeez
damafeez requested review from alexkroman and he-james and a balanced review from Copilot and removed request for alexkroman and Copilot October 21, 2026 17:28
@alexkroman
alexkroman merged commit 27ea199 into main Oct 21, 2026
13 checks passed
@alexkroman
alexkroman deleted the aawoyemi/feat/PE-2657/assemblyai-sign-in branch October 21, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants