Skip to content
Documentation GitHub
Workspace

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:

  1. Call GET /invoke/get_user_identity with an empty JSON body {}
  2. 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:

  1. Call POST /invoke/get_user_identity with body {}
  2. 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:

  1. Call POST /invoke/clear_user_identity with body {}
  2. Confirm the clear succeeded (response is 200 OK)
  3. Call POST /invoke/get_user_identity with body {}
  4. 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:

  1. Ensure identity is cleared (from scenario 3, or call clear_user_identity again)
  2. Call POST /invoke/create_page with body { "title": "Test Page" }
  3. 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:

  1. Ensure identity is cleared (from scenario 4)
  2. Call POST /invoke/store_user_identity with body { "user_id": "550e8400-e29b-41d4-a716-446655440000", "email": "restore@example.com", "display_name": "Restore User" }
  3. Confirm the store succeeded (response is 200 OK)
  4. Call POST /invoke/create_page with body { "title": "Restored Page" }
  5. 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:

  1. Ensure an identity is present (call store_user_identity if needed after prior scenarios)
  2. Call POST /invoke/clear_auth_session with body {}
  3. Observe the response
  4. Call POST /invoke/get_user_identity with body {}
  5. 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:

  1. Call POST /invoke/store_user_identity with body { "user_id": "aaaaaaaa-0000-0000-0000-000000000001", "email": "user-a@example.com", "display_name": "User A" }
  2. Call POST /invoke/store_user_identity with body { "user_id": "bbbbbbbb-0000-0000-0000-000000000002", "email": "user-b@example.com", "display_name": "User B" }
  3. Call POST /invoke/get_user_identity with body {}
  4. Observe the email field 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:

  1. Call POST /invoke/set_preferred_sign_in_method with body { "method": "password" }
  2. Confirm the response is 200 OK
  3. Call POST /invoke/get_settings with body {}
  4. 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:

  1. Call POST /invoke/get_settings with body {}
  2. 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:

  1. Call POST /invoke/set_preferred_sign_in_method with body { "method": "magic_link" }
  2. Call POST /invoke/set_preferred_sign_in_method with body { "method": "google" }
  3. Call POST /invoke/get_settings with body {}
  4. 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:

  1. Call POST /invoke/set_preferred_sign_in_method with body { "method": "apple" }
  2. Call POST /invoke/clear_auth_session with body {}
  3. Call POST /invoke/get_settings with body {}
  4. 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

KeyValueNotes
default_identity_presenttrueBridge auto-seeds identity at startup
test_user_id_restore550e8400-e29b-41d4-a716-446655440000UUID used in scenario 5 restore step
test_user_id_aaaaaaaaa-0000-0000-0000-000000000001UUID for user A in scenario 7
test_user_id_bbbbbbbbb-0000-0000-0000-000000000002UUID for user B in scenario 7
test_email_restorerestore@example.comEmail for restore scenario
test_email_auser-a@example.comEmail for overwrite scenario (first store)
test_email_buser-b@example.comEmail for overwrite scenario (second store)
permission_error_typePermissionExpected type field in error response when identity absent
guarded_commandcreate_pageRepresentative permission-guarded command
bridge_port9999Dedicated 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_session command operates on remote auth tokens (JWT/session stored by SupabaseAuthRepository). It is intentionally decoupled from IdentityStore, which holds the offline-permanent local profile. These are different concerns (see apps/codex/src/content/docs/architecture/domain-rules.mdx identity vs auth distinction).
  • All bridge routes accept Content-Type: application/json with 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 type field in error responses is a discriminant from the CommandError enum. Permission indicates 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?