@tdfc/sunbreak-react 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +640 -0
- package/dist/index.cjs +4 -4
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,3 +15,643 @@ Complete integration guides and API references are available at:
|
|
|
15
15
|
```bash
|
|
16
16
|
npm install @tdfc/sunbreak-react
|
|
17
17
|
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table of Contents
|
|
22
|
+
|
|
23
|
+
- [Architecture Overview](#architecture-overview)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [Core Concepts](#core-concepts)
|
|
26
|
+
- [Authentication Flows](#authentication-flows)
|
|
27
|
+
- [State Machine](#state-machine)
|
|
28
|
+
- [Autopilot System](#autopilot-system)
|
|
29
|
+
- [Provider Adapters](#provider-adapters)
|
|
30
|
+
- [Cryptographic Layer](#cryptographic-layer)
|
|
31
|
+
- [Storage Layer](#storage-layer)
|
|
32
|
+
- [HTTP Layer](#http-layer)
|
|
33
|
+
- [Public API](#public-api)
|
|
34
|
+
- [Expected Behaviors](#expected-behaviors)
|
|
35
|
+
- [Debugging](#debugging)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Architecture Overview
|
|
40
|
+
|
|
41
|
+
The SDK follows a layered architecture with clear separation of concerns:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
+------------------------------------------------------------------+
|
|
45
|
+
| SunbreakProvider |
|
|
46
|
+
| (React Context - Composes all providers and exposes public API) |
|
|
47
|
+
+------------------------------------------------------------------+
|
|
48
|
+
| Autopilot |
|
|
49
|
+
| (9 Effects - Orchestrates lifecycle: probe -> auth -> session) |
|
|
50
|
+
+------------------------------------------------------------------+
|
|
51
|
+
| SessionStateMachine | Auth Hooks | Session Manager |
|
|
52
|
+
| (State transitions) | (register, | (session fetch, |
|
|
53
|
+
| | refresh) | request wrapper) |
|
|
54
|
+
+------------------------------------------------------------------+
|
|
55
|
+
| Crypto Utils | Storage Layer | HTTP Layer |
|
|
56
|
+
| (DPoP, PODE, | (IndexedDB + | (Error classification, |
|
|
57
|
+
| JWK thumbs) | localStorage) | fresh host rotation) |
|
|
58
|
+
+------------------------------------------------------------------+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Key Design Principles
|
|
62
|
+
|
|
63
|
+
1. **Single Source of Truth**: `SessionStateMachine` controls all auth state transitions
|
|
64
|
+
2. **Proof Isolation**: Proof changes don't trigger re-registration when already authenticated
|
|
65
|
+
3. **HttpOnly Cookie Awareness**: Session history tracked separately from visible state
|
|
66
|
+
4. **Race Condition Prevention**: Locks prevent concurrent operations (`refreshLock`, `registerLock`, `probeLock`)
|
|
67
|
+
5. **Automatic Recovery**: Session failures trigger graceful fallbacks
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { SunbreakProvider, useSunbreak } from "@sunbreak/react";
|
|
75
|
+
|
|
76
|
+
function App() {
|
|
77
|
+
return (
|
|
78
|
+
<SunbreakProvider
|
|
79
|
+
clientId="your-client-id"
|
|
80
|
+
wallet={connectedWalletAddress}
|
|
81
|
+
providerAdapter={{
|
|
82
|
+
name: "privy",
|
|
83
|
+
appId: "your-privy-app-id",
|
|
84
|
+
getToken: () => privy.getAccessToken(),
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
<YourApp />
|
|
88
|
+
</SunbreakProvider>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function YourApp() {
|
|
93
|
+
const { authenticated, loading, session, get, post } = useSunbreak();
|
|
94
|
+
|
|
95
|
+
if (loading) return <div>Loading...</div>;
|
|
96
|
+
if (!authenticated) return <div>Please connect your wallet</div>;
|
|
97
|
+
|
|
98
|
+
return <div>Authenticated!</div>;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Core Concepts
|
|
105
|
+
|
|
106
|
+
### Proof Types
|
|
107
|
+
|
|
108
|
+
The SDK supports four proof methods for authentication:
|
|
109
|
+
|
|
110
|
+
| Method | Use Case | Fingerprint Format |
|
|
111
|
+
| -------------- | ------------------------------- | ---------------------------- |
|
|
112
|
+
| `provider_jwt` | Privy, Dynamic, Web3Auth tokens | `{issuer}:{sub_claim}` |
|
|
113
|
+
| `siwe` | Sign-In With Ethereum messages | `siwe:{signature_prefix}` |
|
|
114
|
+
| `eip191` | Personal sign messages | `eip191:{signature_prefix}` |
|
|
115
|
+
| `ed25519` | Solana/Ed25519 signatures | `ed25519:{signature_prefix}` |
|
|
116
|
+
|
|
117
|
+
### Session States
|
|
118
|
+
|
|
119
|
+
The SDK manages five distinct session states:
|
|
120
|
+
|
|
121
|
+
| State | Description | Next Actions |
|
|
122
|
+
| --------------- | ------------------------------------------------ | --------------------- |
|
|
123
|
+
| `UNKNOWN` | Initial state, haven't determined session status | Probe |
|
|
124
|
+
| `UNREGISTERED` | No session history, need proof to register | Register (with proof) |
|
|
125
|
+
| `REFRESHABLE` | Have session history (boundWallet/refreshId) | Refresh first |
|
|
126
|
+
| `AUTHENTICATED` | Have valid access token | Session calls allowed |
|
|
127
|
+
| `EXPIRED` | Refresh failed with "missing refresh identifier" | Register (with proof) |
|
|
128
|
+
|
|
129
|
+
### Key Terminology
|
|
130
|
+
|
|
131
|
+
- **boundWallet**: The wallet address associated with the current session (persisted)
|
|
132
|
+
- **refreshId**: Identifier for the refresh token (may be in HttpOnly cookie)
|
|
133
|
+
- **proofFingerprint**: Hash of credentials to detect changes
|
|
134
|
+
- **inActiveSession**: Flag preventing re-registration loops
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Authentication Flows
|
|
139
|
+
|
|
140
|
+
### First-Time Registration
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
1. Wallet connects
|
|
144
|
+
2. Probe runs -> warms DPoP nonce cache
|
|
145
|
+
3. State machine initializes -> UNREGISTERED (no session history)
|
|
146
|
+
4. Proof becomes available (via provider adapter or prop)
|
|
147
|
+
5. shouldAttemptRegister() returns true
|
|
148
|
+
6. Register request sent with:
|
|
149
|
+
- DPoP token (signed with ephemeral key)
|
|
150
|
+
- PODE (Proof of Delegation from device root key)
|
|
151
|
+
- Proof payload
|
|
152
|
+
7. Success -> AUTHENTICATED, boundWallet saved, fingerprint stored
|
|
153
|
+
8. Session call fetches allowed/expiry data
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Returning User (Page Refresh)
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
1. Page loads
|
|
160
|
+
2. MetaProvider hydrates from IndexedDB/localStorage
|
|
161
|
+
3. Probe runs -> warms nonce cache
|
|
162
|
+
4. State machine initializes -> REFRESHABLE (has boundWallet/refreshId)
|
|
163
|
+
5. shouldAttemptRefresh() returns true
|
|
164
|
+
6. Refresh request sent with:
|
|
165
|
+
- DPoP token
|
|
166
|
+
- boundWallet (from storage, even if wallet not connected yet)
|
|
167
|
+
- HttpOnly refresh cookie
|
|
168
|
+
7. Success -> AUTHENTICATED
|
|
169
|
+
8. Session call runs
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Credential Change Detection
|
|
173
|
+
|
|
174
|
+
When a user switches accounts (e.g., logs into a different Privy account):
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
1. New token arrives from provider adapter
|
|
178
|
+
2. Fingerprint computed: {issuer}:{sub_claim}
|
|
179
|
+
3. Compared against stored registeredProofId
|
|
180
|
+
4. If different:
|
|
181
|
+
- onNewCredentialsReceived() called
|
|
182
|
+
- inActiveSession set to false
|
|
183
|
+
- State transitions to UNREGISTERED
|
|
184
|
+
- Re-registration allowed
|
|
185
|
+
5. If same:
|
|
186
|
+
- Registration blocked (prevents loops)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Wallet Change Handling
|
|
190
|
+
|
|
191
|
+
**Same wallet reconnects** (matches boundWallet):
|
|
192
|
+
|
|
193
|
+
- State -> `REFRESHABLE`
|
|
194
|
+
- Refresh attempted using existing session
|
|
195
|
+
|
|
196
|
+
**Different wallet connects**:
|
|
197
|
+
|
|
198
|
+
- State -> `UNREGISTERED`
|
|
199
|
+
- Auth state cleared
|
|
200
|
+
- Key rotation triggered
|
|
201
|
+
- Re-registration required
|
|
202
|
+
|
|
203
|
+
**Wallet disconnects**:
|
|
204
|
+
|
|
205
|
+
- State -> `UNKNOWN`
|
|
206
|
+
- All auth state cleared
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## State Machine
|
|
211
|
+
|
|
212
|
+
The `SessionStateMachine` is the single source of truth for authentication state.
|
|
213
|
+
|
|
214
|
+
### State Transition Diagram
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
+--------------------------------------+
|
|
218
|
+
| |
|
|
219
|
+
v |
|
|
220
|
+
+---------+ |
|
|
221
|
+
| UNKNOWN | <--- Wallet disconnect |
|
|
222
|
+
+----+----+ |
|
|
223
|
+
| |
|
|
224
|
+
| Probe completes |
|
|
225
|
+
| |
|
|
226
|
+
+----------+----------+ |
|
|
227
|
+
| | |
|
|
228
|
+
v v |
|
|
229
|
+
+--------------+ +-------------+ |
|
|
230
|
+
| UNREGISTERED | | REFRESHABLE | <-- Token expired |
|
|
231
|
+
+------+-------+ +------+------+ |
|
|
232
|
+
| | |
|
|
233
|
+
| Register | Refresh |
|
|
234
|
+
| succeeds | succeeds |
|
|
235
|
+
| | |
|
|
236
|
+
+-------+-----------+ |
|
|
237
|
+
| |
|
|
238
|
+
v |
|
|
239
|
+
+---------------+ |
|
|
240
|
+
| AUTHENTICATED |-------------------------------->+
|
|
241
|
+
+---------------+
|
|
242
|
+
|
|
|
243
|
+
| Refresh fails
|
|
244
|
+
| (missing refresh identifier)
|
|
245
|
+
v
|
|
246
|
+
+---------+
|
|
247
|
+
| EXPIRED | --- Can register again with proof
|
|
248
|
+
+---------+
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Decision Methods
|
|
252
|
+
|
|
253
|
+
| Method | Returns `true` when |
|
|
254
|
+
| ---------------------------------- | ------------------------------------------------------------------------------- |
|
|
255
|
+
| `shouldAttemptProbe()` | State is `UNKNOWN` |
|
|
256
|
+
| `shouldAttemptRefresh(ctx)` | State is `REFRESHABLE` or `AUTHENTICATED`, wallet available |
|
|
257
|
+
| `shouldAttemptRegister(ctx)` | State is `UNREGISTERED` or `EXPIRED`, not in active session, has wallet + proof |
|
|
258
|
+
| `shouldWaitForInitialRefresh(...)` | Returning user, refresh not yet attempted |
|
|
259
|
+
|
|
260
|
+
### Key Flags
|
|
261
|
+
|
|
262
|
+
- **`inActiveSession`**: Set `true` after successful register/refresh; prevents re-registration
|
|
263
|
+
- **`hadSessionHistory`**: Tracks if user ever had a session; survives wallet changes
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Autopilot System
|
|
268
|
+
|
|
269
|
+
The autopilot orchestrates the SDK lifecycle through 9 React effects:
|
|
270
|
+
|
|
271
|
+
### Effect #0: Probe + State Machine Initialize
|
|
272
|
+
|
|
273
|
+
- **Trigger**: `metaReady` becomes true
|
|
274
|
+
- **Actions**: Initialize state machine, run probe request
|
|
275
|
+
- **Guards**: Three-layer protection against double-probing (probeLock, hasProbedRef, pageProbeGuard)
|
|
276
|
+
|
|
277
|
+
### Effect #1: Wallet Change Handling
|
|
278
|
+
|
|
279
|
+
- **Trigger**: `st.wallet` changes
|
|
280
|
+
- **Actions**:
|
|
281
|
+
- Disconnect: Clear all auth state, transition to UNKNOWN
|
|
282
|
+
- Rotation: Clear state, rotate keys, update state machine
|
|
283
|
+
- Reconnection: Check if matches boundWallet
|
|
284
|
+
|
|
285
|
+
### Effect #2: Provider Adapter Token -> Proof
|
|
286
|
+
|
|
287
|
+
- **Trigger**: Provider adapter available + wallet connected
|
|
288
|
+
- **Actions**: Fetch token, compute fingerprint, detect credential changes, attempt register
|
|
289
|
+
- **Guards**: Cooldown, metaReady, wallet presence
|
|
290
|
+
|
|
291
|
+
### Effect #3: Proof Prop -> Register
|
|
292
|
+
|
|
293
|
+
- **Trigger**: `proofProp` changes
|
|
294
|
+
- **Actions**: Similar to Effect #2 but for direct proof props
|
|
295
|
+
- **Guards**: Same as Effect #2
|
|
296
|
+
|
|
297
|
+
### Effect #4: Initial Refresh
|
|
298
|
+
|
|
299
|
+
- **Trigger**: `metaReady` + state machine decides refresh
|
|
300
|
+
- **Actions**: Wait for probe, attempt refresh, call session on success
|
|
301
|
+
|
|
302
|
+
### Effect #5: Init Resolved
|
|
303
|
+
|
|
304
|
+
- **Trigger**: Various initialization conditions
|
|
305
|
+
- **Actions**: Mark initialization complete, resolve init barrier
|
|
306
|
+
|
|
307
|
+
### Effect #6: Session After Auth
|
|
308
|
+
|
|
309
|
+
- **Trigger**: `authenticated` becomes true
|
|
310
|
+
- **Actions**: Call session to fetch allowed/expiry data
|
|
311
|
+
- **Guards**: didInitialSession prevents double calls
|
|
312
|
+
|
|
313
|
+
### Effect #7: Wallet Mismatch Reset
|
|
314
|
+
|
|
315
|
+
- **Trigger**: wallet != authWalletRef
|
|
316
|
+
- **Actions**: Clear auth if wallet changed after authentication
|
|
317
|
+
|
|
318
|
+
### Effect #8: Refresh on Focus
|
|
319
|
+
|
|
320
|
+
- **Trigger**: Window gains focus + session near expiry
|
|
321
|
+
- **Actions**: Refresh token, call session
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Provider Adapters
|
|
326
|
+
|
|
327
|
+
Provider adapters bridge authentication providers to the SDK:
|
|
328
|
+
|
|
329
|
+
### Privy Adapter
|
|
330
|
+
|
|
331
|
+
```typescript
|
|
332
|
+
const privyAdapter = {
|
|
333
|
+
name: "privy",
|
|
334
|
+
appId: "your-privy-app-id",
|
|
335
|
+
getToken: () => privy.getAccessToken(),
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Dynamic Adapter
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
const dynamicAdapter = {
|
|
343
|
+
name: "dynamic",
|
|
344
|
+
envId: "your-dynamic-env-id",
|
|
345
|
+
expectedAud: "optional-audience",
|
|
346
|
+
getToken: () => dynamic.getToken(),
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Custom Adapter
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
const customAdapter = {
|
|
354
|
+
name: "custom",
|
|
355
|
+
meta: {
|
|
356
|
+
/* your metadata */
|
|
357
|
+
},
|
|
358
|
+
getToken: () => yourProvider.getToken(),
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Token -> Proof Conversion
|
|
363
|
+
|
|
364
|
+
The SDK automatically converts provider tokens to proof objects:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Input: JWT token from adapter
|
|
368
|
+
const token = await adapter.getToken();
|
|
369
|
+
|
|
370
|
+
// Output: ProviderJwtProof
|
|
371
|
+
const proof = {
|
|
372
|
+
method: "provider_jwt",
|
|
373
|
+
issuer: "privy",
|
|
374
|
+
token: token,
|
|
375
|
+
meta: { app_id: adapter.appId },
|
|
376
|
+
};
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Cryptographic Layer
|
|
382
|
+
|
|
383
|
+
### DPoP (Demonstration of Proof-of-Possession)
|
|
384
|
+
|
|
385
|
+
DPoP tokens prove possession of a private key without revealing it:
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
const dpop = await createDpop({
|
|
389
|
+
method: "POST",
|
|
390
|
+
url: "https://api.sunbreak.com/auth/register",
|
|
391
|
+
nonce: cachedNonce, // From previous response
|
|
392
|
+
privateKey: ephemeralPrivateKey,
|
|
393
|
+
publicJwk: ephemeralPublicJwk,
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Soft Nonce Caching**: The SDK caches DPoP nonces per endpoint, reducing round trips.
|
|
398
|
+
|
|
399
|
+
### PODE (Proof of Delegation)
|
|
400
|
+
|
|
401
|
+
PODE proves that the ephemeral key was authorized by a device root key:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const pode = await createPode({
|
|
405
|
+
rootPrivateKey,
|
|
406
|
+
rootPublicJwk,
|
|
407
|
+
childJkt: thumbprint(ephemeralPublicJwk),
|
|
408
|
+
clientId: "your-client-id",
|
|
409
|
+
sid: sessionId,
|
|
410
|
+
ttlSec: 300,
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Root Key Persistence**: The device root key survives across sessions for continuity.
|
|
415
|
+
|
|
416
|
+
### JWK Thumbprints
|
|
417
|
+
|
|
418
|
+
Key thumbprints (JKT) uniquely identify public keys:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
const jkt = await ecP256ThumbprintJkt(publicJwk);
|
|
422
|
+
// Returns: Base64url-encoded SHA-256 hash of canonical JWK
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Storage Layer
|
|
428
|
+
|
|
429
|
+
### Dual-Layer Persistence
|
|
430
|
+
|
|
431
|
+
| Storage | Priority | Use Case |
|
|
432
|
+
| ------------ | -------- | -------------------------------------- |
|
|
433
|
+
| IndexedDB | Primary | Full metadata storage, survives longer |
|
|
434
|
+
| localStorage | Fallback | Sync operations, quick access |
|
|
435
|
+
|
|
436
|
+
### Stored Metadata (Meta)
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
type Meta = {
|
|
440
|
+
boundWallet: string | null; // Wallet bound to session
|
|
441
|
+
clientId: string | null; // Application client ID
|
|
442
|
+
jkt: string | null; // Ephemeral key thumbprint
|
|
443
|
+
refreshId: string | null; // Refresh token identifier
|
|
444
|
+
lastPolicyHash: string | null; // For policy caching
|
|
445
|
+
lastPolicyProof: string | null; // Policy signature
|
|
446
|
+
lastHost: string | null; // Last successful API host
|
|
447
|
+
rootJkt: string | null; // Device root key thumbprint
|
|
448
|
+
registeredProofId: string | null; // Proof fingerprint for change detection
|
|
449
|
+
};
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Storage Key Format
|
|
453
|
+
|
|
454
|
+
```
|
|
455
|
+
sunbreak:meta:{clientId} // Client-specific
|
|
456
|
+
sunbreak:meta // Legacy fallback
|
|
457
|
+
sunbreak:keypair // Ephemeral keypair
|
|
458
|
+
sunbreak:rootkeypair // Device root keypair
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## HTTP Layer
|
|
464
|
+
|
|
465
|
+
### Request Flow
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
1. Request initiated (get/post)
|
|
469
|
+
2. awaitKeyStable() - Wait for any key rotation
|
|
470
|
+
3. awaitProbe() - Ensure probe completed
|
|
471
|
+
4. ensureKeypair() - Generate key if needed
|
|
472
|
+
5. Create DPoP token
|
|
473
|
+
6. Build X-Sunbreak-Meta header
|
|
474
|
+
7. Attach authorization (if authenticated)
|
|
475
|
+
8. Send request
|
|
476
|
+
9. Handle 401 -> Retry with new nonce
|
|
477
|
+
10. Parse response, update nonce cache
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Error Classification
|
|
481
|
+
|
|
482
|
+
| Code | Classification | SDK Behavior |
|
|
483
|
+
| --------- | -------------- | ---------------------------------- |
|
|
484
|
+
| 401 | Auth expired | Retry with new nonce, then refresh |
|
|
485
|
+
| 403 | Forbidden | Return error (rate limit possible) |
|
|
486
|
+
| 429 | Rate limited | Set cooldown, return error |
|
|
487
|
+
| 503 | Unavailable | May indicate WAF block |
|
|
488
|
+
| Other 5xx | Server error | Return error |
|
|
489
|
+
|
|
490
|
+
### Fresh Host Rotation
|
|
491
|
+
|
|
492
|
+
When the primary host fails (WAF/ALB issues), the SDK rotates to a fresh subdomain:
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// Primary: api.sunbreak.com
|
|
496
|
+
// Fallback: api-{random}.sunbreak.com
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Public API
|
|
502
|
+
|
|
503
|
+
### SunbreakContextType
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
interface SunbreakContextType {
|
|
507
|
+
// HTTP Methods
|
|
508
|
+
get: <T>(path: string, opts?: RequestInit) => Promise<T | undefined>;
|
|
509
|
+
post: <T>(
|
|
510
|
+
path: string,
|
|
511
|
+
body?: unknown,
|
|
512
|
+
opts?: RequestInit
|
|
513
|
+
) => Promise<T | undefined>;
|
|
514
|
+
|
|
515
|
+
// Session
|
|
516
|
+
session: () => Promise<SessionResp | undefined>;
|
|
517
|
+
refresh: () => Promise<boolean>;
|
|
518
|
+
|
|
519
|
+
// State
|
|
520
|
+
authenticated: boolean;
|
|
521
|
+
loading: boolean;
|
|
522
|
+
error: string | null;
|
|
523
|
+
allowed: boolean | null;
|
|
524
|
+
sessionExpiry: number | null;
|
|
525
|
+
sessionData: SessionResp | null;
|
|
526
|
+
wallet?: string;
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### useSunbreak Hook
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
const {
|
|
534
|
+
authenticated, // true when session is active
|
|
535
|
+
loading, // true during any auth operation
|
|
536
|
+
error, // Error message if any
|
|
537
|
+
allowed, // From session response (app-specific)
|
|
538
|
+
sessionExpiry, // Unix timestamp of session expiry
|
|
539
|
+
sessionData, // Full session response
|
|
540
|
+
get, // Authenticated GET request
|
|
541
|
+
post, // Authenticated POST request
|
|
542
|
+
session, // Manually fetch session
|
|
543
|
+
refresh, // Manually trigger refresh
|
|
544
|
+
} = useSunbreak();
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Expected Behaviors
|
|
550
|
+
|
|
551
|
+
### First Load (New User)
|
|
552
|
+
|
|
553
|
+
1. Provider shows loading briefly
|
|
554
|
+
2. Probe request fires
|
|
555
|
+
3. User connects wallet
|
|
556
|
+
4. Authentication proof generated
|
|
557
|
+
5. Register request succeeds
|
|
558
|
+
6. Session fetched
|
|
559
|
+
7. `authenticated` becomes `true`
|
|
560
|
+
|
|
561
|
+
### Page Refresh (Returning User)
|
|
562
|
+
|
|
563
|
+
1. Provider shows loading
|
|
564
|
+
2. Meta loaded from storage
|
|
565
|
+
3. Probe fires
|
|
566
|
+
4. Refresh attempted with boundWallet
|
|
567
|
+
5. Session fetched
|
|
568
|
+
6. `authenticated` becomes `true`
|
|
569
|
+
7. Wallet may connect later (OK - session already active)
|
|
570
|
+
|
|
571
|
+
### Wallet Switch
|
|
572
|
+
|
|
573
|
+
1. State cleared immediately
|
|
574
|
+
2. Keys rotated
|
|
575
|
+
3. If same wallet as boundWallet: refresh attempted
|
|
576
|
+
4. If different wallet: requires new proof to register
|
|
577
|
+
|
|
578
|
+
### Session Expiry
|
|
579
|
+
|
|
580
|
+
1. On window focus near expiry: auto-refresh
|
|
581
|
+
2. If refresh succeeds: session updated
|
|
582
|
+
3. If refresh fails: may need re-registration
|
|
583
|
+
|
|
584
|
+
### Provider Account Switch
|
|
585
|
+
|
|
586
|
+
1. New token detected via fingerprint comparison
|
|
587
|
+
2. `onNewCredentialsReceived()` called
|
|
588
|
+
3. Re-registration permitted
|
|
589
|
+
4. New session established
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Debugging
|
|
594
|
+
|
|
595
|
+
### Enable Debug Logging
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
<SunbreakProvider
|
|
599
|
+
clientId="your-client-id"
|
|
600
|
+
wallet={wallet}
|
|
601
|
+
debug={true} // Enables verbose console logging
|
|
602
|
+
>
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Logger Methods
|
|
606
|
+
|
|
607
|
+
The SDK uses structured logging with these categories:
|
|
608
|
+
|
|
609
|
+
| Method | Use Case |
|
|
610
|
+
| ------------------------------------------- | ------------------------------------------- |
|
|
611
|
+
| `logger.flow(name, msg)` | Auth flow events (probe, register, refresh) |
|
|
612
|
+
| `logger.api(method, path, info)` | HTTP request/response |
|
|
613
|
+
| `logger.guard(name, passed, reason)` | Guard conditions |
|
|
614
|
+
| `logger.decision(question, answer, reason)` | State machine decisions |
|
|
615
|
+
| `logger.state(from, to, reason)` | State transitions |
|
|
616
|
+
|
|
617
|
+
### State Machine Report
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
const report = stateMachine.getStateReport(context);
|
|
621
|
+
console.log(report);
|
|
622
|
+
// +-------------------------------------------+
|
|
623
|
+
// | Session State Machine Report |
|
|
624
|
+
// +-------------------------------------------+
|
|
625
|
+
// | Current State: authenticated |
|
|
626
|
+
// | Previous State: refreshable |
|
|
627
|
+
// | Active Session: true |
|
|
628
|
+
// | Had History: true |
|
|
629
|
+
// +-------------------------------------------+
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Common Issues
|
|
633
|
+
|
|
634
|
+
**"Stuck in loading"**
|
|
635
|
+
|
|
636
|
+
- Check if wallet is connected
|
|
637
|
+
- Check if provider adapter is returning tokens
|
|
638
|
+
- Verify `metaReady` is becoming true
|
|
639
|
+
- Check browser console for probe/register errors
|
|
640
|
+
|
|
641
|
+
**"Re-registration loops"**
|
|
642
|
+
|
|
643
|
+
- Verify proof fingerprint is consistent
|
|
644
|
+
- Check `inActiveSession` flag
|
|
645
|
+
- Ensure provider isn't returning different tokens on each call
|
|
646
|
+
|
|
647
|
+
**"Session not fetching"**
|
|
648
|
+
|
|
649
|
+
- Check `didInitialSession` flag
|
|
650
|
+
- Verify `authenticated` is true
|
|
651
|
+
- Check wallet matches boundWallet
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## License
|
|
656
|
+
|
|
657
|
+
MIT
|