howone 0.1.10 → 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.
- package/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +75 -27
- package/templates/vite/.howone/skills/howone-sdk/references/01-client-setup.md +278 -0
- package/templates/vite/.howone/skills/howone-sdk/references/02-entity-operations.md +379 -0
- package/templates/vite/.howone/skills/howone-sdk/references/03-ai-actions.md +489 -0
- package/templates/vite/.howone/skills/howone-sdk/references/04-auth.md +484 -0
- package/templates/vite/.howone/skills/howone-sdk/references/05-file-upload.md +319 -0
- package/templates/vite/.howone/skills/howone-sdk/references/06-react-integration.md +394 -0
- package/templates/vite/.howone/skills/howone-sdk/references/07-raw-http.md +299 -0
- package/templates/vite/.howone/skills/howone-sdk/references/08-manifest-codegen.md +400 -0
- package/templates/vite/.howone/skills/howone-sdk/references/usage-patterns.md +0 -443
|
@@ -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()!)` |
|