Auth & Identity
Auth & Identity
Covers the full lifecycle of the local identity store: reading a seeded identity, clearing it, verifying that permission-guarded commands fail without an identity, restoring access by storing a new identity, confirming that auth session operations do not disturb the identity store, and verifying that multiple successive stores overwrite cleanly. This spec is P0 because the identity store is a security boundary: it gates all workspace-mutating commands behind a check that a local user identity exists, and any regression that bypasses or corrupts this gate could allow unauthorized writes or permanently lock out legitimate users.
The HTTP bridge auto-seeds a stub identity at startup via IdentityStore::store_identity, so bridge instances always
have an identity by default. API-level tests call the bridge routes directly (/invoke/{command}) with a JSON body
using snake_case parameter names matching the Rust command signatures.
Preconditions
- HTTP bridge running on port 9999 with workspace
.data/workspaces/e2e-9 - The bridge has auto-seeded an identity on startup (default behavior)
- All scenarios run serially because scenarios 3–7 depend on identity state set or cleared by prior steps
Scenarios
Seed: seed.spec.ts
1. Identity is present by default on seeded workspace
When the bridge starts, it auto-seeds a stub identity. get_user_identity should immediately return a non-null identity
object.
Steps:
- Call
GET /invoke/get_user_identitywith an empty JSON body{} - Observe the response status and body
Expected: The response status is 200 OK. The response body is a non-null JSON object. The object has an id field
that is a non-empty string. The object has an email field that is a non-empty string.
2. get_user_identity returns stored identity fields
The returned identity object exposes both id and email as top-level fields with non-null values.
Steps:
- Call
POST /invoke/get_user_identitywith body{} - Parse the response body as JSON
Expected: The response body contains id (non-empty string, typically a UUID), email (non-empty string containing
@), and optionally display_name. No fields are null or undefined. The response status is 200 OK.
3. clear_user_identity removes identity
After calling clear_user_identity, subsequent calls to get_user_identity return null.
Steps:
- Call
POST /invoke/clear_user_identitywith body{} - Confirm the clear succeeded (response is
200 OK) - Call
POST /invoke/get_user_identitywith body{} - Parse the response body
Expected: Step 1 returns 200 OK. Step 3 returns 200 OK with a response body of null (not an empty object — the
value is JSON null). No identity fields are present.
4. Permission-guarded command fails without identity
After clearing the identity, calling a guarded command (create_page) returns a permission error response.
Steps:
- Ensure identity is cleared (from scenario 3, or call
clear_user_identityagain) - Call
POST /invoke/create_pagewith body{ "title": "Test Page" } - Observe the response status and body
Expected: The response status is not 200 OK (non-OK, e.g. 403 or 400). The response body is a JSON object with
a type field equal to "Permission". No page is created.
5. store_user_identity restores permissions
Storing a new identity after a clear re-enables permission-guarded commands.
Steps:
- Ensure identity is cleared (from scenario 4)
- Call
POST /invoke/store_user_identitywith body{ "user_id": "550e8400-e29b-41d4-a716-446655440000", "email": "restore@example.com", "display_name": "Restore User" } - Confirm the store succeeded (response is
200 OK) - Call
POST /invoke/create_pagewith body{ "title": "Restored Page" } - Observe the response status
Expected: Step 2 returns 200 OK. Step 4 returns 200 OK. No permission error is returned. The page is created
successfully (response body contains an id or slug field).
6. Identity survives auth session clear
Calling clear_auth_session removes remote auth tokens but does not disturb the local identity store.
Steps:
- Ensure an identity is present (call
store_user_identityif needed after prior scenarios) - Call
POST /invoke/clear_auth_sessionwith body{} - Observe the response
- Call
POST /invoke/get_user_identitywith body{} - Parse the response body
Expected: Step 2 returns 200 OK. Step 4 returns 200 OK with a non-null identity object. The id and email
fields match the identity stored in step 1. The auth session clear did not touch the identity store.
7. Multiple identity stores overwrite cleanly
Storing identity A, then immediately storing identity B, results in get_user_identity returning B’s email.
Steps:
- Call
POST /invoke/store_user_identitywith body{ "user_id": "aaaaaaaa-0000-0000-0000-000000000001", "email": "user-a@example.com", "display_name": "User A" } - Call
POST /invoke/store_user_identitywith body{ "user_id": "bbbbbbbb-0000-0000-0000-000000000002", "email": "user-b@example.com", "display_name": "User B" } - Call
POST /invoke/get_user_identitywith body{} - Observe the
emailfield in the response
Expected: Step 3 returns a non-null object. The email field is "user-b@example.com" (user B, not user A). The
id field matches "bbbbbbbb-0000-0000-0000-000000000002". The store operation is idempotent-overwrite: the second
store fully replaces the first.
8. set_preferred_sign_in_method persists preference
Setting a preferred sign-in method via set_preferred_sign_in_method is reflected in subsequent get_settings calls.
Steps:
- Call
POST /invoke/set_preferred_sign_in_methodwith body{ "method": "password" } - Confirm the response is
200 OK - Call
POST /invoke/get_settingswith body{} - Parse the response body and inspect
preferred_sign_in_method
Expected: Step 1 returns 200 OK. Step 3 returns 200 OK with a response body where preferred_sign_in_method
equals "password".
9. Preferred sign-in method defaults to null
On fresh settings (before any set_preferred_sign_in_method call), get_settings returns preferred_sign_in_method as
null.
Steps:
- Call
POST /invoke/get_settingswith body{} - Parse the response body
Expected: The preferred_sign_in_method field is null (or absent, which serde deserializes as null).
10. Preferred sign-in method overwrites cleanly
Setting to one value, then immediately setting to another, results in the second value being returned.
Steps:
- Call
POST /invoke/set_preferred_sign_in_methodwith body{ "method": "magic_link" } - Call
POST /invoke/set_preferred_sign_in_methodwith body{ "method": "google" } - Call
POST /invoke/get_settingswith body{} - Inspect
preferred_sign_in_method
Expected: The preferred_sign_in_method field equals "google" (not "magic_link"). The most recent write wins.
11. Preferred sign-in method survives auth session clear
Settings are stored in a separate JSON file from auth session state. Clearing the auth session must not disturb the preferred sign-in method.
Steps:
- Call
POST /invoke/set_preferred_sign_in_methodwith body{ "method": "apple" } - Call
POST /invoke/clear_auth_sessionwith body{} - Call
POST /invoke/get_settingswith body{} - Inspect
preferred_sign_in_method
Expected: The preferred_sign_in_method field still equals "apple". The auth session clear does not touch the
settings file.
Test Data
| Key | Value | Notes |
|---|---|---|
| default_identity_present | true | Bridge auto-seeds identity at startup |
| test_user_id_restore | 550e8400-e29b-41d4-a716-446655440000 | UUID used in scenario 5 restore step |
| test_user_id_a | aaaaaaaa-0000-0000-0000-000000000001 | UUID for user A in scenario 7 |
| test_user_id_b | bbbbbbbb-0000-0000-0000-000000000002 | UUID for user B in scenario 7 |
| test_email_restore | restore@example.com | Email for restore scenario |
| test_email_a | user-a@example.com | Email for overwrite scenario (first store) |
| test_email_b | user-b@example.com | Email for overwrite scenario (second store) |
| permission_error_type | Permission | Expected type field in error response when identity absent |
| guarded_command | create_page | Representative permission-guarded command |
| bridge_port | 9999 | Dedicated bridge port for auth-identity tests |
Notes
- Scenarios 3–7 must run serially because each test leaves the identity store in a specific state that the next test
depends upon. Use
test.describe.serial(). - The HTTP bridge seeds identity automatically on startup, so scenario 1 and 2 do not need setup calls.
- The
clear_auth_sessioncommand operates on remote auth tokens (JWT/session stored bySupabaseAuthRepository). It is intentionally decoupled fromIdentityStore, which holds the offline-permanent local profile. These are different concerns (seeapps/codex/src/content/docs/architecture/domain-rules.mdxidentity vs auth distinction). - All bridge routes accept
Content-Type: application/jsonwith a JSON body. Empty-param commands use{}as the body (not an empty string or omitted body). - Parameter names are snake_case (
user_id,display_name) matching Rust Tauri command signatures. Do not use camelCase. - The
typefield in error responses is a discriminant from theCommandErrorenum.Permissionindicates the caller lacks a required identity or capability. - In the bridge environment, the auth step in the onboarding tour auto-completes because the bridge seeds a backend identity. Tests in this spec are API-only and do not require UI interaction.
Was this page helpful?
Thanks for your feedback!