Skip to content

feat: delete the account from the app - #38

Merged
lemarier merged 4 commits into
mainfrom
lemarier/delete-the-account-from-the-app
Sep 25, 2026
Merged

lemarier merged 4 commits into
mainfrom
lemarier/delete-the-account-from-the-app

Conversation

@lemarier

@lemarier lemarier commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Change

Closes #36. Signed-in people can delete their account from the account sheet, as App Store guideline 5.1.1(v) requires. The app calls DELETE /v1/account on the Origin89 cloud. When the cloud answers reauthentication_required (the sign-in is older than 5 minutes), the app opens AuthKit again with max_age=0 and login_hint, which starts a new session. It keeps that session only if the same user signed in, then retries once. If another account signs in at that point, or the person switches accounts while the request is out, the app deletes nothing and says so.

Nothing on the phone changes until the cloud answers 204. The confirmation asks what happens to the pairings made in the account. Keep moves them to the signed-out scope, where a pairing replaces a signed-out one for the same controller and the account's last controller carries over. Remove deletes them. The app then signs out. A provider_unavailable 502 leaves everything as it was, and a retry finishes. KeychainEnrolmentStore.save now updates an existing entry in place, so a failed save never loses the older pairing. Keep lists the account's pairings with a throwing storedDeviceIDs(), so a Keychain that can't be read (a locked phone) stops the move instead of reading as no pairings. If the pairings cannot be moved or removed, the account is still signed out and its pairings stay hidden under the deleted account. Controllers are never touched.

  • CloudClient (SetupKit) sends v1 requests with the account's access token and maps the contract's {"error":{"code","message"}} to a typed CloudError. Link enrolled controllers to a site after sign-in #35 builds its sites and linking calls on it.
  • Account.reauthenticate(using:) handles the fresh sign-in that linking also needs.
  • Account.xcconfig sets CLOUD_BASE_URL. Release uses https://cloud.origin89.com, paired with the production WorkOS client. Debug has no cloud until a staging Worker is deployed, since each cloud accepts only its own environment's tokens; in Debug, Delete account is disabled with a note.

Validation

just check passes: swift-format, Rust fmt/clippy/tests, 196 SetupKit tests, the simulator app build and the bench build. xcodebuild -showBuildSettings gives Release the production client and cloud URL, and Debug the staging client and no cloud.

  • Cloud client: HTTPS-only configuration; bearer token, path and JSON body; every contract error code, an unknown code, a non-JSON 500 and 400, offline, an off-contract body; signed out sends nothing.
  • Deletion: keep and remove each hand over the pairings and sign out; a stale sign-in signs in again (max_age=0, login_hint) and retries with the new token; another account signing in to confirm, an account switch during the request (checked to fail without the guard), or a cancel, deletes nothing; a 502 changes nothing and the retry succeeds; pairings that cannot be handed over are reported; a Keychain that keeps the session still signs out; signed out cannot delete.

Not done: a deletion against the production cloud from a device, and a device check that AuthKit's max_age=0 starts a new session that the cloud's 5-minute check accepts. KeychainAccountPairings has no unit test; the tests use an in-memory store.

Copilot AI lite review requested due to automatic review settings September 25, 2026 15:36
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: 5217b4b7-66e9-428e-a28c-eb028d31c422

📥 Commits

Reviewing files that changed from the base of the PR and between 8fd7b35 and 7ab2e20.

📒 Files selected for processing (2)
  • apps/ios/SetupKit/Tests/SetupKitTests/AccountTests.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/CloudTests.swift

Limit details: You’ve used all 10 included reviews currently available.


📝 Walkthrough

Walkthrough

The iOS app adds an HTTPS cloud client, account reauthentication, and account deletion with keep-or-forget pairing choices. Deletion retries after required reauthentication, then handles local pairings and session state. The account screen presents the deletion flow and reports progress and errors. Release builds configure the cloud URL. Tests cover cloud requests, reauthentication, deletion, and pairing handoff.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 7ab2e

Account deletion is irreversible, but its local cleanup can fail or be interrupted after the cloud succeeds. The app can then leave pairings or a saved session in a state that does not match the apparent deletion outcome.

Retained concerns

  • Medium · security · inferred: After cloud deletion succeeds, a failed or interrupted pairing handoff has no shown handoff-only recovery path. Keeping pairings can leave partial signed-out copies, while the account is signed out and a repeat must first pass through cloud deletion again.
  • Medium · security · inferred: If removal of the deleted account’s saved session fails, in-memory sign-out succeeds but the persisted session can be loaded as signed in on the next launch. Whether its cloud tokens are rejected does not resolve the intervening local identity state.
  • Medium · security · inferred: The account-switch guard covers the reauthentication retry, not a successful first request. If the original account’s in-flight DELETE succeeds after another account signs in, the code still hands off the original account’s pairings; it cannot ensure that switching accounts cancels cloud deletion.
Security review details

Security Blast Radius

  • inferred — The independently affected local assets are the deleted owner’s saved session and pairings, plus signed-out pairing storage that the app also includes in signed-in flows. Cloud-side account and downstream deletion scope cannot be established from the client code.

Security Findings and Attack Paths

  • inferred — On a shared or interrupted device, cloud success followed by incomplete local cleanup can expose a different local identity state from the reported deletion: pairing copies may already be in signed-out storage, or a saved session may reappear on launch. Neither path establishes unauthorized cloud access without server-side evidence.

Trust Boundaries and Controls

  • observed — The client uses HTTPS configuration, bearer tokens, original-owner checks around the fresh-sign-in retry, and a cloud-success gate before local changes. The inspected client and simulated-response tests do not establish the cloud’s token-audience, account-ownership, repeated-DELETE, or revocation policies.

Resilience and Maintainability Implications

  • observed — A provider-unavailable response leaves local deletion work untouched for a later attempt, whereas a pairing-store failure after cloud success returns pairingsLeft and signs out. The account-screen caller discards that outcome.

Hardening Proposals

  • proposed — Define a durable, owner-bound post-deletion handoff and recovery contract, including what a repeated cloud DELETE returns, and surface incomplete handoff to the person deleting the account.
  • proposed — Specify and verify cloud-side account ownership, token revocation, and in-flight deletion semantics; distinguish a successful first DELETE from the account-switch case already covered by the reauthentication test.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 49 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR meets the coding requirements in issue #36. AccountView adds confirmation and an explicit keep-or-forget pairing choice. CloudClient.deleteAccount calls DELETE /v1/account, reauthenticate…
Out of Scope Changes check ✅ Passed The changed files support issue #36. Cloud configuration and request handling support account deletion. AuthKit changes support reauthentication. Pairing-store, account, setup-view, and last-controlle…
Title check ✅ Passed The title clearly and concisely identifies the main change: adding account deletion from the app.
Description check ✅ Passed The description explains the change, resulting behavior, compatibility constraints, validation coverage, and unperformed checks. It matches the required Change and Validation sections and provides suf…
  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@lemarier
lemarier added this pull request to stack #41 September 25, 2026 15:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: 67aee5df-d91c-4b22-8c21-0a92f3765e21

📥 Commits

Reviewing files that changed from the base of the PR and between 91a9c09 and 2836d8a.

📒 Files selected for processing (11)
  • apps/ios/Account.xcconfig
  • apps/ios/Origin89/AccountView.swift
  • apps/ios/Origin89/Info.plist
  • apps/ios/Origin89/Origin89App.swift
  • apps/ios/Origin89/SetupView.swift
  • apps/ios/SetupKit/Sources/SetupKit/Account.swift
  • apps/ios/SetupKit/Sources/SetupKit/AccountDeletion.swift
  • apps/ios/SetupKit/Sources/SetupKit/AuthKitClient.swift
  • apps/ios/SetupKit/Sources/SetupKit/CloudClient.swift
  • apps/ios/SetupKit/Sources/SetupKit/KeychainEnrolmentStore.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/CloudTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review.

Comment thread apps/ios/SetupKit/Sources/SetupKit/AccountDeletion.swift
Comment thread apps/ios/SetupKit/Sources/SetupKit/AccountDeletion.swift Outdated
query[kSecMatchLimit as String] = kSecMatchLimitAll
query[kSecReturnAttributes as String] = true
var result: CFTypeRef?
guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess else { return [] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not report a failed Keychain query as "no enrolments".

deviceIDs() returns [] for every status other than errSecSuccess. That includes errors such as errSecInteractionNotAllowed, not only errSecItemNotFound. KeychainAccountPairings.keep in AccountDeletion.swift (Lines 81-88) goes through this list and then calls own.removeAll(). If the attribute query fails and the delete succeeds, "keep pairings" copies nothing and deletes every pairing of the account. The code then reports .deleted. The code at Lines 82-84 tries to prevent this exact loss for unreadable items, but a failure at this point skips that check.

Make deviceIDs() throw on any status other than success or not-found. removeEveryAccount() already does this.

🐛 Proposed fix
-  public func deviceIDs() -> [String] {
+  public func deviceIDs() throws -> [String] {
     var query = items()
     query[kSecMatchLimit as String] = kSecMatchLimitAll
     query[kSecReturnAttributes as String] = true
     var result: CFTypeRef?
-    guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess else { return [] }
+    let status = SecItemCopyMatching(query as CFDictionary, &result)
+    if status == errSecItemNotFound { return [] }
+    guard status == errSecSuccess else { throw Failure(status: status) }

In AccountDeletion.swift, change the loop to for deviceID in try own.deviceIDs().

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
apps/ios/SetupKit/Tests/SetupKitTests/CloudTests.swift-225-225 (1)

225-225: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Hold the first cloud response until the account switch completes.

The request count shows that StubHTTP.send recorded the request; it does not show that the deletion task is still waiting. The stub yields once and can then return its 401 response. If deletion starts reauthentication first, it can consume the only authentication response before signIn uses it. Make the stub wait for an explicit release after the account switch, so this test exercises the intended race reliably. (raw.githubusercontent.com)


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Advanced

Run ID: c6af7091-42ca-4d9c-b5f7-7dcd83bfec41

📥 Commits

Reviewing files that changed from the base of the PR and between 2836d8a and 8fd7b35.

📒 Files selected for processing (3)
  • apps/ios/SetupKit/Sources/SetupKit/AccountDeletion.swift
  • apps/ios/SetupKit/Sources/SetupKit/KeychainEnrolmentStore.swift
  • apps/ios/SetupKit/Tests/SetupKitTests/CloudTests.swift

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.

@lemarier
lemarier merged commit 8427681 into main Sep 25, 2026
2 checks passed
@lemarier
lemarier deleted the lemarier/delete-the-account-from-the-app branch September 25, 2026 17:02
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.

Delete the account from the app

2 participants