howone 0.1.11 → 0.1.12

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.
@@ -0,0 +1,484 @@
1
+ # Auth
2
+
3
+ ## Two Auth Layers
4
+
5
+ HowOne has two distinct auth layers:
6
+
7
+ 1. **Client-level auth** (`client.auth.*`) — low-level token management, login/logout redirects.
8
+ 2. **HowOne Auth service** (`unifiedAuth`, standalone functions) — headless OTP and OAuth flows for building custom login UIs.
9
+
10
+ For React auth UI, see `references/06-react-integration.md` (`HowOneProvider`, `useHowoneContext`).
11
+
12
+ ---
13
+
14
+ ## client.auth — Token Management
15
+
16
+ ```ts
17
+ import howone from '@/lib/sdk'
18
+
19
+ // Check if the current user is authenticated
20
+ howone.auth.isAuthenticated() // boolean
21
+
22
+ // Get the current JWT token (null if not logged in)
23
+ const token = howone.auth.getToken()
24
+
25
+ // Manually set a token (after a custom login flow)
26
+ howone.auth.setToken(jwtToken)
27
+
28
+ // Clear the token
29
+ howone.auth.setToken(null)
30
+
31
+ // Redirect to the HowOne login page
32
+ howone.auth.login()
33
+ howone.auth.login('/dashboard') // optional return path after login
34
+
35
+ // Log out and clear session
36
+ howone.auth.logout()
37
+ ```
38
+
39
+ ---
40
+
41
+ ## User Profile
42
+
43
+ ```ts
44
+ import { HowOneAuthError } from '@howone/sdk'
45
+ import howone from '@/lib/sdk'
46
+
47
+ type UserProfile = {
48
+ id: string
49
+ email?: string
50
+ name?: string
51
+ avatarUrl?: string
52
+ roles?: string[]
53
+ metadata?: Record<string, unknown>
54
+ }
55
+
56
+ // Returns null if not authenticated
57
+ const user = await howone.me()
58
+
59
+ // Returns the profile or throws HowOneAuthError if not authenticated
60
+ const user = await howone.requireMe()
61
+
62
+ // Alias for me()
63
+ const user = await howone.session.user()
64
+
65
+ // Force refresh from server (skip cache)
66
+ const user = await howone.me({ refresh: true })
67
+ ```
68
+
69
+ ### Pattern: guard a page
70
+
71
+ ```tsx
72
+ import { useEffect, useState } from 'react'
73
+ import { HowOneAuthError } from '@howone/sdk'
74
+ import howone, { type UserProfile } from '@/lib/sdk'
75
+
76
+ function useCurrentUser() {
77
+ const [user, setUser] = useState<UserProfile | null>(null)
78
+ const [loading, setLoading] = useState(true)
79
+
80
+ useEffect(() => {
81
+ howone.me()
82
+ .then(setUser)
83
+ .catch(err => {
84
+ if (err instanceof HowOneAuthError) {
85
+ howone.auth.login()
86
+ }
87
+ })
88
+ .finally(() => setLoading(false))
89
+ }, [])
90
+
91
+ return { user, loading }
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Email OTP Login
98
+
99
+ Use `sendEmailVerificationCode` → `loginWithEmailCode` to build a custom email login form.
100
+
101
+ ### Standalone functions (imported directly)
102
+
103
+ ```ts
104
+ import {
105
+ sendEmailVerificationCode,
106
+ loginWithEmailCode,
107
+ } from '@howone/sdk'
108
+
109
+ // Step 1 — send OTP
110
+ const sendResult = await sendEmailVerificationCode(
111
+ 'user@example.com',
112
+ 'MyAppName', // optional, shown in the email
113
+ )
114
+ // sendResult.success: boolean
115
+ // sendResult.expiresIn: number (seconds until expiry)
116
+ // sendResult.message: string (error message if !success)
117
+ // sendResult.code: number (business error code if any)
118
+
119
+ // Step 2 — verify OTP and get token
120
+ const loginResult = await loginWithEmailCode(
121
+ 'user@example.com',
122
+ '123456', // 6-digit OTP from email
123
+ )
124
+ // loginResult.success: boolean
125
+ // loginResult.token: string (JWT, present on success)
126
+ // loginResult.user: { email, name? }
127
+ // loginResult.redirect_url: string (if configured)
128
+ // loginResult.message: string
129
+ // loginResult.code: number
130
+
131
+ if (loginResult.success && loginResult.token) {
132
+ howone.auth.setToken(loginResult.token)
133
+ // Now client.auth.isAuthenticated() === true
134
+ }
135
+ ```
136
+
137
+ ### Using the unifiedAuth service
138
+
139
+ ```ts
140
+ import { unifiedAuth } from '@howone/sdk'
141
+
142
+ // Same API as standalone functions
143
+ const sendResult = await unifiedAuth.sendEmailVerificationCode(email, appName)
144
+ const loginResult = await unifiedAuth.loginWithEmailCode(email, code)
145
+
146
+ // Verify an existing token
147
+ const { valid, user } = await unifiedAuth.verifyToken(token)
148
+
149
+ // Logout (server-side invalidation)
150
+ await unifiedAuth.logout(token)
151
+ ```
152
+
153
+ ### React component — email OTP form
154
+
155
+ ```tsx
156
+ import { useState } from 'react'
157
+ import { sendEmailVerificationCode, loginWithEmailCode } from '@howone/sdk'
158
+ import howone from '@/lib/sdk'
159
+
160
+ type Step = 'email' | 'code' | 'done'
161
+
162
+ export function EmailLoginForm({ onSuccess }: { onSuccess: () => void }) {
163
+ const [step, setStep] = useState<Step>('email')
164
+ const [email, setEmail] = useState('')
165
+ const [code, setCode] = useState('')
166
+ const [loading, setLoading] = useState(false)
167
+ const [error, setError] = useState<string | null>(null)
168
+
169
+ async function handleSendCode(e: React.FormEvent) {
170
+ e.preventDefault()
171
+ setLoading(true)
172
+ setError(null)
173
+ try {
174
+ const result = await sendEmailVerificationCode(email)
175
+ if (!result.success) {
176
+ setError(result.message ?? 'Failed to send code')
177
+ return
178
+ }
179
+ setStep('code')
180
+ } finally {
181
+ setLoading(false)
182
+ }
183
+ }
184
+
185
+ async function handleVerifyCode(e: React.FormEvent) {
186
+ e.preventDefault()
187
+ setLoading(true)
188
+ setError(null)
189
+ try {
190
+ const result = await loginWithEmailCode(email, code)
191
+ if (!result.success || !result.token) {
192
+ setError(result.message ?? 'Invalid code')
193
+ return
194
+ }
195
+ howone.auth.setToken(result.token)
196
+ setStep('done')
197
+ onSuccess()
198
+ } finally {
199
+ setLoading(false)
200
+ }
201
+ }
202
+
203
+ if (step === 'email') {
204
+ return (
205
+ <form onSubmit={handleSendCode}>
206
+ <input
207
+ type="email"
208
+ value={email}
209
+ onChange={e => setEmail(e.target.value)}
210
+ placeholder="Enter your email"
211
+ required
212
+ />
213
+ {error && <p className="error">{error}</p>}
214
+ <button type="submit" disabled={loading}>
215
+ {loading ? 'Sending...' : 'Send Code'}
216
+ </button>
217
+ </form>
218
+ )
219
+ }
220
+
221
+ if (step === 'code') {
222
+ return (
223
+ <form onSubmit={handleVerifyCode}>
224
+ <p>Enter the code sent to {email}</p>
225
+ <input
226
+ type="text"
227
+ value={code}
228
+ onChange={e => setCode(e.target.value)}
229
+ placeholder="6-digit code"
230
+ maxLength={6}
231
+ required
232
+ />
233
+ {error && <p className="error">{error}</p>}
234
+ <button type="submit" disabled={loading}>
235
+ {loading ? 'Verifying...' : 'Login'}
236
+ </button>
237
+ <button type="button" onClick={() => setStep('email')}>
238
+ Back
239
+ </button>
240
+ </form>
241
+ )
242
+ }
243
+
244
+ return <p>Login successful!</p>
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Phone OTP Login
251
+
252
+ Same flow as email, using E.164 phone number format (e.g. `+14155552671`).
253
+
254
+ ```ts
255
+ import {
256
+ sendPhoneVerificationCode,
257
+ loginWithPhoneCode,
258
+ } from '@howone/sdk'
259
+
260
+ // Step 1 — send SMS OTP
261
+ const sendResult = await sendPhoneVerificationCode(
262
+ '+14155552671', // must be E.164 format
263
+ 'MyAppName', // optional
264
+ )
265
+ // sendResult.success: boolean
266
+ // sendResult.expires_in: number (seconds)
267
+
268
+ // Step 2 — verify OTP
269
+ const loginResult = await loginWithPhoneCode(
270
+ '+14155552671',
271
+ '123456',
272
+ )
273
+ // loginResult.success: boolean
274
+ // loginResult.token: string (JWT on success)
275
+ // loginResult.user: { phone_e164?, email?, name? }
276
+
277
+ if (loginResult.success && loginResult.token) {
278
+ howone.auth.setToken(loginResult.token)
279
+ }
280
+ ```
281
+
282
+ ### Phone number validation note
283
+
284
+ The SDK validates that phone numbers are in E.164 format internally. Always pass the number with country code (e.g. `+1`, `+44`, `+86`).
285
+
286
+ ---
287
+
288
+ ## OAuth — Google and GitHub
289
+
290
+ ### Google Login
291
+
292
+ ```ts
293
+ import { unifiedAuth } from '@howone/sdk'
294
+ import howone from '@/lib/sdk'
295
+
296
+ async function loginWithGoogle() {
297
+ // Opens a popup window for Google OAuth
298
+ const result = await unifiedAuth.initiateGoogleLogin()
299
+ // result.token: string (JWT)
300
+ // result.user: any
301
+
302
+ howone.auth.setToken(result.token)
303
+ }
304
+ ```
305
+
306
+ ### GitHub Login
307
+
308
+ ```ts
309
+ import { unifiedAuth } from '@howone/sdk'
310
+
311
+ async function loginWithGitHub() {
312
+ const result = await unifiedAuth.initiateGitHubLogin()
313
+ howone.auth.setToken(result.token)
314
+ }
315
+ ```
316
+
317
+ ### Handle OAuth redirect callback
318
+
319
+ If you redirect instead of using a popup, call `checkOAuthCallback()` on the return page:
320
+
321
+ ```ts
322
+ import { unifiedOAuth } from '@howone/sdk'
323
+ import howone from '@/lib/sdk'
324
+
325
+ // Call on the OAuth callback page (e.g. /auth/callback)
326
+ function handleOAuthReturn() {
327
+ const result = unifiedOAuth.checkOAuthCallback()
328
+ // result.success: boolean
329
+ // result.token: string (if success)
330
+ // result.error: string (if failure)
331
+ // result.user: any
332
+
333
+ if (result.success && result.token) {
334
+ howone.auth.setToken(result.token)
335
+ window.location.href = '/dashboard'
336
+ } else {
337
+ console.error('OAuth failed:', result.error)
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### React — OAuth buttons
343
+
344
+ ```tsx
345
+ import { unifiedAuth } from '@howone/sdk'
346
+ import howone from '@/lib/sdk'
347
+
348
+ export function OAuthButtons({ onSuccess }: { onSuccess: () => void }) {
349
+ const [loading, setLoading] = useState<'google' | 'github' | null>(null)
350
+ const [error, setError] = useState<string | null>(null)
351
+
352
+ async function handleGoogle() {
353
+ setLoading('google')
354
+ setError(null)
355
+ try {
356
+ const { token } = await unifiedAuth.initiateGoogleLogin()
357
+ howone.auth.setToken(token)
358
+ onSuccess()
359
+ } catch (err) {
360
+ setError(err instanceof Error ? err.message : 'Google login failed')
361
+ } finally {
362
+ setLoading(null)
363
+ }
364
+ }
365
+
366
+ async function handleGitHub() {
367
+ setLoading('github')
368
+ setError(null)
369
+ try {
370
+ const { token } = await unifiedAuth.initiateGitHubLogin()
371
+ howone.auth.setToken(token)
372
+ onSuccess()
373
+ } catch (err) {
374
+ setError(err instanceof Error ? err.message : 'GitHub login failed')
375
+ } finally {
376
+ setLoading(null)
377
+ }
378
+ }
379
+
380
+ return (
381
+ <div>
382
+ <button onClick={handleGoogle} disabled={loading !== null}>
383
+ {loading === 'google' ? 'Connecting...' : 'Continue with Google'}
384
+ </button>
385
+ <button onClick={handleGitHub} disabled={loading !== null}>
386
+ {loading === 'github' ? 'Connecting...' : 'Continue with GitHub'}
387
+ </button>
388
+ {error && <p className="error">{error}</p>}
389
+ </div>
390
+ )
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Token Verification
397
+
398
+ ```ts
399
+ import { unifiedAuth } from '@howone/sdk'
400
+
401
+ async function checkToken(token: string) {
402
+ const { valid, user } = await unifiedAuth.verifyToken(token)
403
+ if (valid) {
404
+ console.log('User:', user)
405
+ } else {
406
+ console.log('Token is invalid or expired')
407
+ }
408
+ }
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Logout
414
+
415
+ ```ts
416
+ // Client-side logout (clears local token storage)
417
+ howone.auth.logout()
418
+
419
+ // Server-side invalidation via unifiedAuth
420
+ await unifiedAuth.logout(token)
421
+ ```
422
+
423
+ ---
424
+
425
+ ## HowOneAuthError
426
+
427
+ ```ts
428
+ import { HowOneAuthError } from '@howone/sdk'
429
+
430
+ try {
431
+ const user = await howone.requireMe()
432
+ } catch (err) {
433
+ if (err instanceof HowOneAuthError) {
434
+ // err.code === 'UNAUTHENTICATED'
435
+ howone.auth.login('/current-page')
436
+ }
437
+ }
438
+ ```
439
+
440
+ ---
441
+
442
+ ## Auth Mode in createClient
443
+
444
+ Choose the auth mode that matches your login strategy:
445
+
446
+ ```ts
447
+ // Managed (default) — SDK owns the token lifecycle
448
+ const client = createClient({
449
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
450
+ env: import.meta.env.VITE_HOWONE_ENV,
451
+ auth: { mode: 'managed' },
452
+ })
453
+
454
+ // Headless — you own the token (for Clerk, Supabase, custom JWTs, etc.)
455
+ const client = createClient({
456
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
457
+ env: import.meta.env.VITE_HOWONE_ENV,
458
+ auth: {
459
+ mode: 'headless',
460
+ getToken: async () => {
461
+ return localStorage.getItem('auth_token')
462
+ },
463
+ tokenCacheMs: 30_000,
464
+ },
465
+ })
466
+
467
+ // None — unauthenticated, public API access only
468
+ const client = createClient({
469
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
470
+ env: import.meta.env.VITE_HOWONE_ENV,
471
+ auth: { mode: 'none' },
472
+ })
473
+ ```
474
+
475
+ ---
476
+
477
+ ## Common Mistakes
478
+
479
+ | Mistake | Correct Pattern |
480
+ |---|---|
481
+ | Phone number without country code: `'13800138000'` | Use E.164: `'+8613800138000'` |
482
+ | Using `loginWithEmailCode` without first calling `sendEmailVerificationCode` | Always send the code first |
483
+ | Setting token manually when using `auth.mode: 'managed'` | Use `managed` mode which handles storage, or switch to `headless` |
484
+ | Calling `unifiedAuth.logout()` without the token argument | Pass `token`: `await unifiedAuth.logout(howone.auth.getToken()!)` |