CLAUDE.md#
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview#
This is a Deno-native AT Protocol OAuth client library that provides handle-focused authentication using Web Crypto API. It's an alternative to @atproto/oauth-client-node built specifically to solve Node.js crypto compatibility issues in Deno environments.
Key Design Philosophy:
- Handle-focused (accepts
alice.bsky.socialonly, not DIDs or URLs) - Uses Slingshot resolver by default with multiple fallbacks
- Web Crypto API for cross-platform compatibility (no Node.js crypto)
- DPoP (Demonstrating Proof of Possession) authentication with ES256 keys
Development Commands#
# Run all tests with required permissions
deno test --allow-net --allow-read
# Run a specific test file
deno test --allow-net --allow-read tests/session_test.ts
# Type checking
deno check mod.ts
deno check **/*.ts
# Format code
deno fmt # check formatting
deno fmt --check # verify formatting
deno fmt <file> # format specific file
# Lint code
deno lint
# Run full CI suite (format, lint, check, test)
deno task ci
Architecture#
Core Components#
-
OAuthClient (
src/client.ts) - Main OAuth flow orchestration (~680 lines)- Authorization URL generation with PKCE
- Token exchange and refresh with DPoP
- Pushed Authorization Request (PAR) support
- Dual concurrency locks:
restoreLocksfor sessions,refreshLocksfor refresh operations - Configurable logging via Logger interface
-
Session (
src/session.ts) - Authenticated session management- Token lifecycle management (access + refresh tokens)
- DPoP-authenticated request handling with automatic nonce retry
- 5-minute expiration buffer for token refresh
- Serializable session state
-
Handle Resolvers (
src/resolvers.ts) - AT Protocol handle resolutionSlingshotResolver(default): Slingshot → Bluesky API → direct lookup fallback chainDirectoryResolver: Bluesky API onlyCustomResolver: User-provided resolution function- OAuth endpoint discovery from PDS metadata
-
PKCE (
src/pkce.ts) - PKCE utilities- Code verifier generation (32 random bytes)
- Code challenge generation (SHA-256 of verifier)
- Base64URL encoding utility
-
Token Exchange (
src/token-exchange.ts) - Token operations- Authorization code exchange for tokens
- Refresh token exchange
- Shared DPoP retry logic (deduplicates nonce handling)
-
DPoP (
src/dpop.ts) - Proof of Possession implementation- ES256 (ECDSA P-256) key generation using Web Crypto API
- JWT proof generation with
jsr:@panva/jose(not npm version) - Key import with validation (no type assertions)
- Automatic nonce handling on 401 challenges
-
Storage (
src/storage.ts) - Session persistence abstractionsMemoryStorage: In-memory with TTL supportSQLiteStorage: Deno SQLite backend example with type validationLocalStorage: Browser/localStorage compatible- All storage is async with TTL support
-
Logger (
src/logger.ts) - Logging abstractionLoggerinterface with debug/info/warn/error methodsNoOpLogger: Default silent loggerConsoleLogger: Development/debugging logger- Inject custom logger via
OAuthClientConfig.logger
-
Error Handling (
src/errors.ts) - Typed error hierarchy- All errors extend
OAuthErrorbase class - Specific error types for each failure mode
- Error chaining with
causesupport
- All errors extend
Critical Implementation Details#
Web Crypto API Usage:
- MUST use
crypto.subtle.generateKey()with{ name: "ECDSA", namedCurve: "P-256" } - NEVER use Node.js crypto APIs - they don't work in Deno
- Import jose from
jsr:@panva/joseNOTnpm:jose
DPoP Authentication:
- Every token request requires DPoP proof with ES256 signature
- Access token hash (
athclaim) required for authenticated requests - Servers may respond with 400/401 +
DPoP-Nonceheader requiring retry
Concurrency Safety:
OAuthClient.restore()usesrestoreLocksMap (keyed by sessionId)OAuthClient.refresh()usesrefreshLocksMap (keyed by DID)- Multiple concurrent calls wait on single operation and share the result
- Locks are always cleaned up in finally blocks
Error Handling Strategy:
restore()ALWAYS throws errors (never returns null)- Use try/catch to handle
SessionNotFoundError,RefreshTokenExpiredError,NetworkError - All errors include
causechain for debugging
Logging:
- Client uses injected
Loggerinstance (defaults toNoOpLogger) - Use
ConsoleLoggerfor development/debugging - Implement custom
Loggerinterface for production logging - All sensitive operations logged at appropriate levels
Token Refresh:
- Sessions considered expired if token expires within 5 minutes
- Refresh may or may not rotate refresh token (server-dependent)
- Refresh failures throw typed errors:
RefreshTokenExpiredError,NetworkError, etc.
Testing#
- All tests use
Deno.test()with nestedt.step()for organization - Use
jsr:@std/assert@1for assertions - Tests require
--allow-netfor resolver/OAuth endpoint tests - Tests are isolated - use helper functions like
createTestSessionData() - NO mocking of external services - tests use real AT Protocol endpoints
Security Considerations#
- OAuth state validation prevents CSRF attacks (10-minute TTL on PKCE data)
- PKCE flow protects against authorization code interception
- DPoP binds tokens to specific key pairs
- No credentials should ever be committed to the repository
Publishing#
- Package published to JSR (not npm)
- Version in
deno.jsonmust be updated before publishing - Run
deno publishfrom repository root - See
CHANGELOG.mdfor version history
Common Tasks#
Adding a new error type:
- Create class extending appropriate base in
src/errors.ts - Add JSDoc with
@exampleshowing usage - Export from
mod.ts - Add test case in
tests/errors_test.ts
Adding a new storage backend:
- Implement
OAuthStorageinterface insrc/storage.ts - Handle TTL and expiration logic
- Export from
mod.ts - Add test coverage in
tests/storage_test.ts
Modifying OAuth flow:
- Changes to
client.tslikely require testing against real AT Protocol servers - Ensure PKCE data cleanup happens even on errors
- Maintain per-session refresh lock semantics