How LlmConnection Stores OAuth Identity Information After Token Exchange

After a successful OAuth token exchange, the system stores identity metadata—including account UUID, email, organization details, and verification timestamp—directly on the LlmConnection record through guarded field assignments in the RPC handler and explicit preservation logic in the storage layer.

The craft-ai-agents/craft-agents-oss repository implements a fault-tolerant persistence mechanism for OAuth identity data within LLM connection configurations. When users authorize providers like Anthropic, the server captures the authenticated account and organization metadata from the token response and commits it to the connection's persistent storage. This allows the UI to identify when multiple connections resolve to the same underlying Claude account, preventing duplicate configurations.

Parsing the OAuth Identity Response

The RPC handler responsible for LLM connection setup receives the OAuth flow result in src/packages/server-core/src/handlers/rpc/llm-connections.ts. It extracts the oauthIdentity object from the request payload and performs guarded assignments to copy only the fields that are present. This "fail-soft" behavior prevents overwriting existing data with undefined values when the identity response contains partial information.

// src/packages/server-core/src/handlers/rpc/llm-connections.ts
const oauthIdentity = setup.oauthIdentity
if (oauthIdentity?.account || oauthIdentity?.organization) {
  if (oauthIdentity.account?.uuid)   updates.oauthAccountUuid   = oauthIdentity.account.uuid
  if (oauthIdentity.account?.emailAddress) updates.oauthAccountEmail = oauthIdentity.account.emailAddress
  if (oauthIdentity.organization?.uuid)   updates.oauthOrganizationUuid = oauthIdentity.organization.uuid
  if (oauthIdentity.organization?.name)   updates.oauthOrganizationName = oauthIdentity.organization.name
  updates.oauthProfileVerifiedAt = Date.now()
}

Lines 63–74 demonstrate the conditional extraction that builds the updates object. The code specifically checks for the existence of each nested property before assignment, ensuring the update payload contains only valid identity data.

Merging Updates with Existing Data

After constructing the update payload, the handler calls updateLlmConnection(slug, updates). The generic storage logic in src/packages/shared/src/config/storage.ts merges these values with the existing connection record. Lines 2704–2709 implement explicit preservation logic that retains existing OAuth fields when they are not included in the current update.

// src/packages/shared/src/config/storage.ts
oauthAccountUuid: updates.oauthAccountUuid !== undefined ? updates.oauthAccountUuid : existing.oauthAccountUuid,
oauthAccountEmail: updates.oauthAccountEmail !== undefined ? updates.oauthAccountEmail : existing.oauthAccountEmail,
oauthOrganizationUuid: updates.oauthOrganizationUuid !== undefined ? updates.oauthOrganizationUuid : existing.oauthOrganizationUuid,
oauthOrganizationName: updates.oauthOrganizationName !== undefined ? updates.oauthOrganizationName : existing.oauthOrganizationName,
oauthProfileVerifiedAt: updates.oauthProfileVerifiedAt !== undefined ? updates.oauthProfileVerifiedAt : existing.oauthProfileVerifiedAt,

This strict undefined check pattern ensures that OAuth identity information survives subsequent updates to unrelated connection properties, such as API endpoint URLs or model parameters.

Schema Definition for OAuth Fields

The LlmConnection interface defines the storage contract for these identity fields in src/packages/shared/src/config/llm-connections.ts. Lines 92–100 reserve optional properties that accommodate the OAuth data structure while maintaining backward compatibility with connections that do not use OAuth authentication.

// src/packages/shared/src/config/llm-connections.ts
/** --- Resolved Anthropic OAuth identity (issue #838) --- */
oauthAccountUuid?: string;
oauthAccountEmail?: string;
oauthOrganizationUuid?: string;
oauthOrganizationName?: string;
oauthProfileVerifiedAt?: number; // epoch ms

The oauthProfileVerifiedAt field stores the Unix epoch timestamp in milliseconds, marking when the identity was last verified. This metadata enables the UI to display the recency of the OAuth authorization and detect stale connections.

Complete Implementation Flow

When implementing OAuth authorization for an Anthropic connection, the client initiates the flow and the server persists the resulting identity as follows:

// Client side: Trigger the OAuth flow
await startAnthropicOAuth({ slug: connection.slug });

// Server side: Handler processes the callback
if (setup.oauthIdentity?.account || setup.oauthIdentity?.organization) {
  // Extract present fields only
  if (setup.oauthIdentity.account?.uuid)   updates.oauthAccountUuid   = setup.oauthIdentity.account.uuid;
  if (setup.oauthIdentity.account?.emailAddress) updates.oauthAccountEmail = setup.oauthIdentity.account.emailAddress;
  if (setup.oauthIdentity.organization?.uuid)   updates.oauthOrganizationUuid = setup.oauthIdentity.organization.uuid;
  if (setup.oauthIdentity.organization?.name)   updates.oauthOrganizationName = setup.oauthIdentity.organization.name;
  updates.oauthProfileVerifiedAt = Date.now();
}

// Persist to storage with merge protection
await updateLlmConnection(setup.slug, updates);

Summary

  • Guarded Extraction: The RPC handler in llm-connections.ts copies only present OAuth fields to prevent undefined overwrites.
  • Explicit Preservation: The storage layer uses strict !== undefined checks to retain existing identity data during partial updates.
  • Schema Support: The LlmConnection interface defines optional fields for account UUID, email, organization UUID/name, and verification timestamp.
  • UI Integration: Stored identity data enables the interface to flag duplicate connections pointing to the same Claude account.

Frequently Asked Questions

What specific OAuth fields does LlmConnection store?

The connection record stores five fields: oauthAccountUuid and oauthAccountEmail for the user account, oauthOrganizationUuid and oauthOrganizationName for the organization context, and oauthProfileVerifiedAt as a timestamp marking when the identity was verified. These fields are defined as optional strings and numbers in the TypeScript interface.

How does the storage layer prevent overwriting existing OAuth data?

The update logic in storage.ts explicitly checks each OAuth field using !== undefined ternary operators. If a field is missing from the update payload, the storage layer retains the existing value rather than replacing it with undefined. This ensures that OAuth identity information persists across updates that modify unrelated connection properties.

Why is the verification timestamp stored as an epoch number?

The oauthProfileVerifiedAt field uses a Unix epoch timestamp in milliseconds to provide a language-agnostic, sortable value that indicates when the OAuth identity was last validated. This allows the UI to display the relative age of the authorization and helps identify potentially stale connections that may require re-authentication.

Where does the initial OAuth identity object originate?

The identity object originates from the OAuth provider's token exchange response, specifically after authorizing with Anthropic. The oauthIdentity payload is sent to the llm-connections.ts RPC handler as part of the connection setup request, containing the authenticated account and organization details resolved during the OAuth flow.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →