@tdfc/sunbreak-react
v0.1.14
Published
SDK for connecting to the Sunbreak API
Downloads
1,733
Readme
Sunbreak React SDK
The official React client for the Sunbreak API.
This SDK seamlessly connects your application to Sunbreak, executing necessary checks in the background to provide real-time access verdicts based on your specific compliance policies.
Documentation
Complete integration guides and API references are available at:
Installation
npm install @tdfc/sunbreak-reactTable of Contents
- Architecture Overview
- Quick Start
- Core Concepts
- Authentication Flows
- State Machine
- Autopilot System
- Provider Adapters
- Cryptographic Layer
- Storage Layer
- HTTP Layer
- Public API
- Expected Behaviors
- Debugging
Architecture Overview
The SDK follows a layered architecture with clear separation of concerns:
+------------------------------------------------------------------+
| SunbreakProvider |
| (React Context - Composes all providers and exposes public API) |
+------------------------------------------------------------------+
| Autopilot |
| (9 Effects - Orchestrates lifecycle: probe -> auth -> session) |
+------------------------------------------------------------------+
| SessionStateMachine | Auth Hooks | Session Manager |
| (State transitions) | (register, | (session fetch, |
| | refresh) | request wrapper) |
+------------------------------------------------------------------+
| Crypto Utils | Storage Layer | HTTP Layer |
| (DPoP, PODE, | (IndexedDB + | (Error classification, |
| JWK thumbs) | localStorage) | fresh host rotation) |
+------------------------------------------------------------------+Key Design Principles
- Single Source of Truth:
SessionStateMachinecontrols all auth state transitions - Proof Isolation: Proof changes don't trigger re-registration when already authenticated
- HttpOnly Cookie Awareness: Session history tracked separately from visible state
- Race Condition Prevention: Locks prevent concurrent operations (
refreshLock,registerLock,probeLock) - Automatic Recovery: Session failures trigger graceful fallbacks
Quick Start
import { SunbreakProvider, useSunbreak } from "@sunbreak/react";
function App() {
return (
<SunbreakProvider
clientId="your-client-id"
wallet={connectedWalletAddress}
providerAdapter={{
name: "privy",
appId: "your-privy-app-id",
getToken: () => privy.getAccessToken(),
}}
>
<YourApp />
</SunbreakProvider>
);
}
function YourApp() {
const { authenticated, loading, session, get, post } = useSunbreak();
if (loading) return <div>Loading...</div>;
if (!authenticated) return <div>Please connect your wallet</div>;
return <div>Authenticated!</div>;
}Core Concepts
Proof Types
The SDK supports four proof methods for authentication:
| Method | Use Case | Fingerprint Format |
| -------------- | ------------------------------- | ---------------------------- |
| provider_jwt | Privy, Dynamic, Web3Auth tokens | {issuer}:{sub_claim} |
| siwe | Sign-In With Ethereum messages | siwe:{signature_prefix} |
| eip191 | Personal sign messages | eip191:{signature_prefix} |
| ed25519 | Solana/Ed25519 signatures | ed25519:{signature_prefix} |
Session States
The SDK manages five distinct session states:
| State | Description | Next Actions |
| --------------- | ------------------------------------------------ | --------------------- |
| UNKNOWN | Initial state, haven't determined session status | Probe |
| UNREGISTERED | No session history, need proof to register | Register (with proof) |
| REFRESHABLE | Have session history (boundWallet/refreshId) | Refresh first |
| AUTHENTICATED | Have valid access token | Session calls allowed |
| EXPIRED | Refresh failed with "missing refresh identifier" | Register (with proof) |
Key Terminology
- boundWallet: The wallet address associated with the current session (persisted)
- refreshId: Identifier for the refresh token (may be in HttpOnly cookie)
- proofFingerprint: Hash of credentials to detect changes
- inActiveSession: Flag preventing re-registration loops
Authentication Flows
First-Time Registration
1. Wallet connects
2. Probe runs -> warms DPoP nonce cache
3. State machine initializes -> UNREGISTERED (no session history)
4. Proof becomes available (via provider adapter or prop)
5. shouldAttemptRegister() returns true
6. Register request sent with:
- DPoP token (signed with ephemeral key)
- PODE (Proof of Delegation from device root key)
- Proof payload
7. Success -> AUTHENTICATED, boundWallet saved, fingerprint stored
8. Session call fetches allowed/expiry dataReturning User (Page Refresh)
1. Page loads
2. MetaProvider hydrates from IndexedDB/localStorage
3. Probe runs -> warms nonce cache
4. State machine initializes -> REFRESHABLE (has boundWallet/refreshId)
5. shouldAttemptRefresh() returns true
6. Refresh request sent with:
- DPoP token
- boundWallet (from storage, even if wallet not connected yet)
- HttpOnly refresh cookie
7. Success -> AUTHENTICATED
8. Session call runsCredential Change Detection
When a user switches accounts (e.g., logs into a different Privy account):
1. New token arrives from provider adapter
2. Fingerprint computed: {issuer}:{sub_claim}
3. Compared against stored registeredProofId
4. If different:
- onNewCredentialsReceived() called
- inActiveSession set to false
- State transitions to UNREGISTERED
- Re-registration allowed
5. If same:
- Registration blocked (prevents loops)Wallet Change Handling
Same wallet reconnects (matches boundWallet):
- State ->
REFRESHABLE - Refresh attempted using existing session
Different wallet connects:
- State ->
UNREGISTERED - Auth state cleared
- Key rotation triggered
- Re-registration required
Wallet disconnects:
- State ->
UNKNOWN - All auth state cleared
State Machine
The SessionStateMachine is the single source of truth for authentication state.
State Transition Diagram
+--------------------------------------+
| |
v |
+---------+ |
| UNKNOWN | <--- Wallet disconnect |
+----+----+ |
| |
| Probe completes |
| |
+----------+----------+ |
| | |
v v |
+--------------+ +-------------+ |
| UNREGISTERED | | REFRESHABLE | <-- Token expired |
+------+-------+ +------+------+ |
| | |
| Register | Refresh |
| succeeds | succeeds |
| | |
+-------+-----------+ |
| |
v |
+---------------+ |
| AUTHENTICATED |-------------------------------->+
+---------------+
|
| Refresh fails
| (missing refresh identifier)
v
+---------+
| EXPIRED | --- Can register again with proof
+---------+Decision Methods
| Method | Returns true when |
| ---------------------------------- | ------------------------------------------------------------------------------- |
| shouldAttemptProbe() | State is UNKNOWN |
| shouldAttemptRefresh(ctx) | State is REFRESHABLE or AUTHENTICATED, wallet available |
| shouldAttemptRegister(ctx) | State is UNREGISTERED or EXPIRED, not in active session, has wallet + proof |
| shouldWaitForInitialRefresh(...) | Returning user, refresh not yet attempted |
Key Flags
inActiveSession: Settrueafter successful register/refresh; prevents re-registrationhadSessionHistory: Tracks if user ever had a session; survives wallet changes
Autopilot System
The autopilot orchestrates the SDK lifecycle through 9 React effects:
Effect #0: Probe + State Machine Initialize
- Trigger:
metaReadybecomes true - Actions: Initialize state machine, run probe request
- Guards: Three-layer protection against double-probing (probeLock, hasProbedRef, pageProbeGuard)
Effect #1: Wallet Change Handling
- Trigger:
st.walletchanges - Actions:
- Disconnect: Clear all auth state, transition to UNKNOWN
- Rotation: Clear state, rotate keys, update state machine
- Reconnection: Check if matches boundWallet
Effect #2: Provider Adapter Token -> Proof
- Trigger: Provider adapter available + wallet connected
- Actions: Fetch token, compute fingerprint, detect credential changes, attempt register
- Guards: Cooldown, metaReady, wallet presence
Effect #3: Proof Prop -> Register
- Trigger:
proofPropchanges - Actions: Similar to Effect #2 but for direct proof props
- Guards: Same as Effect #2
Effect #4: Initial Refresh
- Trigger:
metaReady+ state machine decides refresh - Actions: Wait for probe, attempt refresh, call session on success
Effect #5: Init Resolved
- Trigger: Various initialization conditions
- Actions: Mark initialization complete, resolve init barrier
Effect #6: Session After Auth
- Trigger:
authenticatedbecomes true - Actions: Call session to fetch allowed/expiry data
- Guards: didInitialSession prevents double calls
Effect #7: Wallet Mismatch Reset
- Trigger: wallet != authWalletRef
- Actions: Clear auth if wallet changed after authentication
Effect #8: Refresh on Focus
- Trigger: Window gains focus + session near expiry
- Actions: Refresh token, call session
Provider Adapters
Provider adapters bridge authentication providers to the SDK:
Privy Adapter
const privyAdapter = {
name: "privy",
appId: "your-privy-app-id",
getToken: () => privy.getAccessToken(),
};Dynamic Adapter
const dynamicAdapter = {
name: "dynamic",
envId: "your-dynamic-env-id",
expectedAud: "optional-audience",
getToken: () => dynamic.getToken(),
};Custom Adapter
const customAdapter = {
name: "custom",
meta: {
/* your metadata */
},
getToken: () => yourProvider.getToken(),
};Token -> Proof Conversion
The SDK automatically converts provider tokens to proof objects:
// Input: JWT token from adapter
const token = await adapter.getToken();
// Output: ProviderJwtProof
const proof = {
method: "provider_jwt",
issuer: "privy",
token: token,
meta: { app_id: adapter.appId },
};Cryptographic Layer
DPoP (Demonstration of Proof-of-Possession)
DPoP tokens prove possession of a private key without revealing it:
const dpop = await createDpop({
method: "POST",
url: "https://api.sunbreak.com/auth/register",
nonce: cachedNonce, // From previous response
privateKey: ephemeralPrivateKey,
publicJwk: ephemeralPublicJwk,
});Soft Nonce Caching: The SDK caches DPoP nonces per endpoint, reducing round trips.
PODE (Proof of Delegation)
PODE proves that the ephemeral key was authorized by a device root key:
const pode = await createPode({
rootPrivateKey,
rootPublicJwk,
childJkt: thumbprint(ephemeralPublicJwk),
clientId: "your-client-id",
sid: sessionId,
ttlSec: 300,
});Root Key Persistence: The device root key survives across sessions for continuity.
JWK Thumbprints
Key thumbprints (JKT) uniquely identify public keys:
const jkt = await ecP256ThumbprintJkt(publicJwk);
// Returns: Base64url-encoded SHA-256 hash of canonical JWKStorage Layer
Dual-Layer Persistence
| Storage | Priority | Use Case | | ------------ | -------- | -------------------------------------- | | IndexedDB | Primary | Full metadata storage, survives longer | | localStorage | Fallback | Sync operations, quick access |
Stored Metadata (Meta)
type Meta = {
boundWallet: string | null; // Wallet bound to session
clientId: string | null; // Application client ID
jkt: string | null; // Ephemeral key thumbprint
refreshId: string | null; // Refresh token identifier
lastPolicyHash: string | null; // For policy caching
lastPolicyProof: string | null; // Policy signature
lastHost: string | null; // Last successful API host
rootJkt: string | null; // Device root key thumbprint
registeredProofId: string | null; // Proof fingerprint for change detection
};Storage Key Format
sunbreak:meta:{clientId} // Client-specific
sunbreak:meta // Legacy fallback
sunbreak:keypair // Ephemeral keypair
sunbreak:rootkeypair // Device root keypairHTTP Layer
Request Flow
1. Request initiated (get/post)
2. awaitKeyStable() - Wait for any key rotation
3. awaitProbe() - Ensure probe completed
4. ensureKeypair() - Generate key if needed
5. Create DPoP token
6. Build X-Sunbreak-Meta header
7. Attach authorization (if authenticated)
8. Send request
9. Handle 401 -> Retry with new nonce
10. Parse response, update nonce cacheError Classification
| Code | Classification | SDK Behavior | | --------- | -------------- | ---------------------------------- | | 401 | Auth expired | Retry with new nonce, then refresh | | 403 | Forbidden | Return error (rate limit possible) | | 429 | Rate limited | Set cooldown, return error | | 503 | Unavailable | May indicate WAF block | | Other 5xx | Server error | Return error |
Fresh Host Rotation
When the primary host fails (WAF/ALB issues), the SDK rotates to a fresh subdomain:
// Primary: api.sunbreak.com
// Fallback: api-{random}.sunbreak.comPublic API
SunbreakContextType
interface SunbreakContextType {
// HTTP Methods
get: <T>(path: string, opts?: RequestInit) => Promise<T | undefined>;
post: <T>(
path: string,
body?: unknown,
opts?: RequestInit
) => Promise<T | undefined>;
// Session
session: () => Promise<SessionResp | undefined>;
refresh: () => Promise<boolean>;
// State
authenticated: boolean;
loading: boolean;
error: string | null;
allowed: boolean | null;
sessionExpiry: number | null;
sessionData: SessionResp | null;
wallet?: string;
}useSunbreak Hook
const {
authenticated, // true when session is active
loading, // true during any auth operation
error, // Error message if any
allowed, // From session response (app-specific)
sessionExpiry, // Unix timestamp of session expiry
sessionData, // Full session response
get, // Authenticated GET request
post, // Authenticated POST request
session, // Manually fetch session
refresh, // Manually trigger refresh
} = useSunbreak();Expected Behaviors
First Load (New User)
- Provider shows loading briefly
- Probe request fires
- User connects wallet
- Authentication proof generated
- Register request succeeds
- Session fetched
authenticatedbecomestrue
Page Refresh (Returning User)
- Provider shows loading
- Meta loaded from storage
- Probe fires
- Refresh attempted with boundWallet
- Session fetched
authenticatedbecomestrue- Wallet may connect later (OK - session already active)
Wallet Switch
- State cleared immediately
- Keys rotated
- If same wallet as boundWallet: refresh attempted
- If different wallet: requires new proof to register
Session Expiry
- On window focus near expiry: auto-refresh
- If refresh succeeds: session updated
- If refresh fails: may need re-registration
Provider Account Switch
- New token detected via fingerprint comparison
onNewCredentialsReceived()called- Re-registration permitted
- New session established
Debugging
Enable Debug Logging
<SunbreakProvider
clientId="your-client-id"
wallet={wallet}
debug={true} // Enables verbose console logging
>Logger Methods
The SDK uses structured logging with these categories:
| Method | Use Case |
| ------------------------------------------- | ------------------------------------------- |
| logger.flow(name, msg) | Auth flow events (probe, register, refresh) |
| logger.api(method, path, info) | HTTP request/response |
| logger.guard(name, passed, reason) | Guard conditions |
| logger.decision(question, answer, reason) | State machine decisions |
| logger.state(from, to, reason) | State transitions |
State Machine Report
const report = stateMachine.getStateReport(context);
console.log(report);
// +-------------------------------------------+
// | Session State Machine Report |
// +-------------------------------------------+
// | Current State: authenticated |
// | Previous State: refreshable |
// | Active Session: true |
// | Had History: true |
// +-------------------------------------------+Common Issues
"Stuck in loading"
- Check if wallet is connected
- Check if provider adapter is returning tokens
- Verify
metaReadyis becoming true - Check browser console for probe/register errors
"Re-registration loops"
- Verify proof fingerprint is consistent
- Check
inActiveSessionflag - Ensure provider isn't returning different tokens on each call
"Session not fetching"
- Check
didInitialSessionflag - Verify
authenticatedis true - Check wallet matches boundWallet
License
MIT
