howone 0.1.23 → 0.1.25

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.
Files changed (24) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +180 -91
  3. package/templates/vite/.howone/skills/howone-sdk/01-architect/02-manifest-codegen.md +67 -4
  4. package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +463 -69
  5. package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +366 -64
  6. package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +204 -67
  7. package/templates/vite/.howone/skills/howone-sdk/02-database/04-query-dsl-and-responses.md +237 -0
  8. package/templates/vite/.howone/skills/howone-sdk/02-database/05-ai-persistence-patterns.md +372 -0
  9. package/templates/vite/.howone/skills/howone-sdk/03-sdk/01-client-setup.md +58 -36
  10. package/templates/vite/.howone/skills/howone-sdk/03-sdk/02-entity-operations.md +67 -0
  11. package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +267 -469
  12. package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +113 -320
  13. package/templates/vite/.howone/skills/howone-sdk/03-sdk/07-ai-action-calls.md +66 -16
  14. package/templates/vite/.howone/skills/howone-sdk/03-sdk/08-extension-boundaries.md +226 -0
  15. package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +159 -96
  16. package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +353 -96
  17. package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +181 -42
  18. package/templates/vite/.howone/skills/howone-sdk/04-ai/04-service-capability-catalog.md +281 -0
  19. package/templates/vite/.howone/skills/howone-sdk/04-ai/05-workflow-operations.md +256 -0
  20. package/templates/vite/.howone/skills/howone-sdk/04-ai/06-ai-feature-playbooks.md +296 -0
  21. package/templates/vite/.howone/skills/howone-sdk/SKILL.md +29 -12
  22. package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +2 -2
  23. package/templates/vite/package.json +1 -1
  24. package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
@@ -1,553 +1,372 @@
1
1
  # Auth
2
2
 
3
- ## Two Auth Layers
3
+ ## Environment (read this first — dev must not hit prod)
4
4
 
5
- HowOne has two distinct auth layers:
5
+ All auth APIs (`sendEmailVerificationCode`, `loginWithEmailCode`, `unifiedAuth.*`, `howone.auth.logout` revoke) use the **same `env` as `createClient`**, not the browser hostname and not a frozen default.
6
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 `03-sdk/04-react-integration.md` (`HowOneProvider`, `useHowoneContext`).
11
-
12
- ---
13
-
14
- ## client.auth — Token Management
7
+ | `VITE_HOWONE_ENV` / `createClient({ env })` | Auth REST API origin | Hosted login root (`auth: 'hosted'`) |
8
+ |---------------------------------------------|----------------------|--------------------------------------|
9
+ | `local` | `http://localhost:3002` | `https://howone.dev` |
10
+ | `dev` | `https://api.howone.dev` | `https://howone.dev` |
11
+ | `prod` | `https://api.howone.ai` | `https://howone.ai` |
15
12
 
16
13
  ```ts
17
- import howone from '@/lib/sdk'
14
+ // src/lib/sdk.ts — canonical (no extra auth fields required)
15
+ const client = createClient({
16
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
17
+ env: import.meta.env.VITE_HOWONE_ENV,
18
+ })
19
+ ```
18
20
 
19
- // Check if the current user is authenticated
20
- howone.auth.isAuthenticated() // boolean
21
+ Defaults when `auth` is omitted:
21
22
 
22
- // Get the current JWT token (null if not logged in)
23
- const token = howone.auth.getToken()
23
+ - Auth mode: **hosted** login/logout redirect to HowOne (`howone.dev` / `howone.ai` by `env`)
24
+ - Auth APIs still follow `env` (`dev` → `api.howone.dev`, `prod` → `api.howone.ai`)
24
25
 
25
- // Manually set a token (after a custom login flow)
26
- howone.auth.setToken(jwtToken)
26
+ Custom in-app login UI is **opt-in**: `auth: 'custom'` (see below).
27
27
 
28
- // Clear the token
29
- howone.auth.setToken(null)
28
+ ```ts
29
+ // main.tsx
30
+ import './lib/sdk' // registers env first
31
+ import { sendEmailVerificationCode } from '@howone/sdk'
32
+ ```
30
33
 
31
- // Redirect to the HowOne login page
32
- howone.auth.login()
33
- howone.auth.login('/dashboard') // optional return path after login
34
+ Rules for agents:
34
35
 
35
- // Log out and clear session
36
- howone.auth.logout()
37
- ```
36
+ - **Do not** hardcode `api.howone.ai` or `howone.ai` in app code.
37
+ - **Do not** import auth helpers before `./lib/sdk` (env would stay default `prod`).
38
+ - **Do not** rely on `localhost` hostname to pick `local`; use `env: 'local'` or `env: 'dev'` explicitly.
39
+ - Entity, AI, upload, and auth endpoints all follow this single `env` pin.
38
40
 
39
41
  ---
40
42
 
41
- ## User Profile
43
+ ## Custom login (opt-in only)
42
44
 
43
- ```ts
44
- import { HowOneAuthError } from '@howone/sdk'
45
- import howone from '@/lib/sdk'
45
+ Add `auth: 'custom'` when the app renders its own `/login` page but still uses HowOne OTP/OAuth APIs:
46
46
 
47
- type UserProfile = {
48
- id: string // backend owner id; same as userId when JWT has userId
49
- userId?: string // authenticated backend user id when present in JWT
50
- puid?: string // public UUID; do not use as public ownerId scope
51
- email?: string
52
- name?: string
53
- avatarUrl?: string
54
- appId?: string
55
- roles?: string[]
56
- metadata?: Record<string, unknown>
57
- }
47
+ ```ts
48
+ const client = createClient({
49
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
50
+ env: import.meta.env.VITE_HOWONE_ENV,
51
+ auth: 'custom',
52
+ loginPath: '/login',
53
+ })
54
+ ```
58
55
 
59
- // Returns null if not authenticated
60
- const user = await howone.me()
56
+ That wires:
61
57
 
62
- // Returns the profile or throws HowOneAuthError if not authenticated
63
- const user = await howone.requireMe()
58
+ | Behavior | Result |
59
+ |----------|--------|
60
+ | `howone.auth.logout()` | Clears token + revokes server session → stays on your app → navigates to `loginPath` |
61
+ | `howone.auth.login()` | Navigates to `loginPath` (never howone.dev / howone.ai) |
62
+ | `HowOneProvider` `useHowoneContext().logout()` | Same as `howone.auth.logout()` when `createClient` ran first |
64
63
 
65
- // Alias for me()
66
- const user = await howone.session.user()
64
+ Pair with React:
67
65
 
68
- // Force refresh from server (skip cache)
69
- const user = await howone.me({ refresh: true })
66
+ ```tsx
67
+ <HowOneProvider auth="none" brand="visible">
68
+ <App />
69
+ </HowOneProvider>
70
70
  ```
71
71
 
72
- ### Identity fields
73
-
74
- HowOne JWTs may contain both `userId` and `puid`. The SDK preserves both:
72
+ `auth="none"` on the provider means **no automatic redirect**; route guards call `howone.me()` and `navigate('/login')` yourself. Keep `brand="visible"` unless the user explicitly asks to hide the bottom-right HowOne logo.
75
73
 
76
- - `user.id` / `user.userId` identifies the authenticated backend user.
77
- - `user.puid` is the public UUID and should not be used for entity owner filters.
78
- - `query.mine()` requires auth and lets the backend derive ownership from the JWT.
74
+ | `createClient({ auth })` | Login UI | `auth.login()` | `auth.logout()` default redirect |
75
+ |--------------------------|----------|----------------|----------------------------------|
76
+ | *(omit)* | **HowOne hosted** | howone `/auth` | howone `/auth` |
77
+ | `'custom'` | Your `/login` + HowOne APIs | → `loginPath` | → `loginPath` |
78
+ | `'hosted'` | HowOne hosted (same as default) | → howone `/auth` | → howone `/auth` |
79
+ | `'headless'` | External (Clerk, etc.) | no-op | no redirect |
80
+ | `'none'` | N/A (public app) | no-op | no redirect |
79
81
 
80
- For public share URLs that need scoped owner data, use `howone.public.entities.*`
81
- and pass the schema-required public scope, usually the shared owner id stored on a record.
82
- Do not pass `puid` as `ownerId`.
82
+ Legacy alias: `'managed'` `'hosted'`.
83
83
 
84
- ### Pattern: guard a page
84
+ ### Advanced object form
85
85
 
86
- ```tsx
87
- import { useEffect, useState } from 'react'
88
- import { HowOneAuthError } from '@howone/sdk'
89
- import howone, { type UserProfile } from '@/lib/sdk'
90
-
91
- function useCurrentUser() {
92
- const [user, setUser] = useState<UserProfile | null>(null)
93
- const [loading, setLoading] = useState(true)
94
-
95
- useEffect(() => {
96
- howone.me()
97
- .then(setUser)
98
- .catch(err => {
99
- if (err instanceof HowOneAuthError) {
100
- howone.auth.login()
101
- }
102
- })
103
- .finally(() => setLoading(false))
104
- }, [])
105
-
106
- return { user, loading }
86
+ ```ts
87
+ auth: {
88
+ mode: 'custom',
89
+ loginPath: '/sign-in',
90
+ logoutPath: '/sign-in', // optional; defaults to loginPath
91
+ guard: 'required', // optional; use with HowOneProvider auth="required" for auto-redirect to loginPath
92
+ getToken: async () => null, // only for headless
107
93
  }
108
94
  ```
109
95
 
110
96
  ---
111
97
 
112
- ## Email OTP Login
98
+ ## Two auth layers
113
99
 
114
- Use `sendEmailVerificationCode` `loginWithEmailCode` to build a custom email login form.
100
+ 1. **`createClient({ auth })`** strategy for login/logout destinations (hosted vs custom).
101
+ 2. **`unifiedAuth` / standalone OTP & OAuth functions** — headless APIs to build your login UI.
115
102
 
116
- The SDK normalizes HowOne auth envelopes. Both of these backend response shapes are valid:
103
+ React: `HowOneProvider` + `useHowoneContext` route guard only (`auth="required" | "optional" | "none"`).
117
104
 
118
- ```ts
119
- { success: true, token: 'jwt...' }
120
- { code: 0, data: { success: true, token: 'jwt...' } }
121
- ```
105
+ The underlying SDK auth state is managed by `AuthManager`. App code normally uses `client.auth`,
106
+ `client.me()`, `client.requireMe()`, and `HowOneProvider`; SDK contributors use `AuthAdapter` when
107
+ custom token ownership is required.
122
108
 
123
- After normalization, read `result.success`, `result.token`, and `result.message` from the top level.
109
+ ---
124
110
 
125
- ### Standalone functions (imported directly)
111
+ ## `client.auth` API
126
112
 
127
113
  ```ts
128
- import {
129
- sendEmailVerificationCode,
130
- loginWithEmailCode,
131
- } from '@howone/sdk'
114
+ import howone from '@/lib/sdk'
132
115
 
133
- // Step 1 send OTP
134
- const sendResult = await sendEmailVerificationCode(
135
- 'user@example.com',
136
- 'MyAppName', // optional, shown in the email
137
- )
138
- // sendResult.success: boolean
139
- // sendResult.expiresIn: number (seconds until expiry)
140
- // sendResult.message: string (error message if !success)
141
- // sendResult.code: number (business error code if any)
142
-
143
- // Step 2 — verify OTP and get token
144
- const loginResult = await loginWithEmailCode(
145
- 'user@example.com',
146
- '123456', // 6-digit OTP from email
147
- )
148
- // loginResult.success: boolean
149
- // loginResult.token: string (JWT, present on success)
150
- // loginResult.user: { email, name? }
151
- // loginResult.redirect_url: string (if configured)
152
- // loginResult.message: string
153
- // loginResult.code: number
154
-
155
- if (loginResult.success && loginResult.token) {
156
- howone.auth.setToken(loginResult.token)
157
- // Persists the token and updates the active SDK client.
158
- }
116
+ howone.auth.mode // 'custom' | 'hosted' | 'headless' | 'none'
117
+ howone.auth.guard // 'required' | 'optional' | 'none'
118
+ howone.auth.loginPath // e.g. '/login'
119
+ howone.auth.logoutPath // e.g. '/login'
120
+
121
+ howone.auth.setToken(jwt)
122
+ howone.auth.getToken()
123
+ howone.auth.isAuthenticated()
124
+
125
+ howone.auth.login(returnUrl?) // respects auth mode
126
+ await howone.auth.logout() // respects auth mode
127
+ await howone.auth.clearSession() // clear only; redirect: false
128
+ await howone.auth.clearSession({ redirect: '/goodbye' })
129
+ await howone.auth.logout({ redirect: false })
130
+ howone.auth.subscribe((state) => {
131
+ // state: { token, user, isAuthenticated, isLoading }
132
+ })
159
133
  ```
160
134
 
161
- ### Using the unifiedAuth service
162
-
163
- ```ts
164
- import { unifiedAuth } from '@howone/sdk'
135
+ ---
165
136
 
166
- // Same API as standalone functions
167
- const sendResult = await unifiedAuth.sendEmailVerificationCode(email, appName)
168
- const loginResult = await unifiedAuth.loginWithEmailCode(email, code)
137
+ ## AuthManager / AuthAdapter extension point
169
138
 
170
- // Verify an existing token
171
- const { valid, user } = await unifiedAuth.verifyToken(token)
139
+ Use an adapter when the token is owned outside the default HowOne local session, for example an
140
+ external auth provider, embedded shell, native app bridge, or custom host application.
172
141
 
173
- // Logout (server-side invalidation)
174
- await unifiedAuth.logout(token)
142
+ ```ts
143
+ const client = createClient({
144
+ projectId,
145
+ env,
146
+ auth: {
147
+ mode: 'headless',
148
+ adapter: {
149
+ name: 'external-auth',
150
+ getToken: () => externalAuth.getToken(),
151
+ setToken: (token) => externalAuth.setToken(token),
152
+ getUser: async (token) => externalAuth.getUser(token),
153
+ login: ({ returnUrl } = {}) => {
154
+ router.push(`/login?redirect=${encodeURIComponent(returnUrl ?? '/')}`)
155
+ },
156
+ logout: () => {
157
+ externalAuth.clear()
158
+ router.push('/')
159
+ },
160
+ subscribe: (listener) => externalAuth.onChange(() => {
161
+ listener({
162
+ token: externalAuth.getToken(),
163
+ user: externalAuth.getUserSync(),
164
+ isAuthenticated: Boolean(externalAuth.getToken()),
165
+ isLoading: false,
166
+ })
167
+ }),
168
+ },
169
+ tokenCacheMs: 30_000,
170
+ },
171
+ })
175
172
  ```
176
173
 
177
- ### React component — email OTP form
178
-
179
- ```tsx
180
- import { useState } from 'react'
181
- import { sendEmailVerificationCode, loginWithEmailCode } from '@howone/sdk'
182
- import howone from '@/lib/sdk'
183
-
184
- type Step = 'email' | 'code' | 'done'
185
-
186
- export function EmailLoginForm({ onSuccess }: { onSuccess: () => void }) {
187
- const [step, setStep] = useState<Step>('email')
188
- const [email, setEmail] = useState('')
189
- const [code, setCode] = useState('')
190
- const [loading, setLoading] = useState(false)
191
- const [error, setError] = useState<string | null>(null)
192
-
193
- async function handleSendCode(e: React.FormEvent) {
194
- e.preventDefault()
195
- setLoading(true)
196
- setError(null)
197
- try {
198
- const result = await sendEmailVerificationCode(email)
199
- if (!result.success) {
200
- setError(result.message ?? 'Failed to send code')
201
- return
202
- }
203
- setStep('code')
204
- } finally {
205
- setLoading(false)
206
- }
207
- }
208
-
209
- async function handleVerifyCode(e: React.FormEvent) {
210
- e.preventDefault()
211
- setLoading(true)
212
- setError(null)
213
- try {
214
- const result = await loginWithEmailCode(email, code)
215
- if (!result.success || !result.token) {
216
- setError(result.message ?? 'Invalid code')
217
- return
218
- }
219
- howone.auth.setToken(result.token)
220
- setStep('done')
221
- onSuccess()
222
- } finally {
223
- setLoading(false)
224
- }
225
- }
226
-
227
- if (step === 'email') {
228
- return (
229
- <form onSubmit={handleSendCode}>
230
- <input
231
- type="email"
232
- value={email}
233
- onChange={e => setEmail(e.target.value)}
234
- placeholder="Enter your email"
235
- required
236
- />
237
- {error && <p className="error">{error}</p>}
238
- <button type="submit" disabled={loading}>
239
- {loading ? 'Sending...' : 'Send Code'}
240
- </button>
241
- </form>
242
- )
243
- }
244
-
245
- if (step === 'code') {
246
- return (
247
- <form onSubmit={handleVerifyCode}>
248
- <p>Enter the code sent to {email}</p>
249
- <input
250
- type="text"
251
- value={code}
252
- onChange={e => setCode(e.target.value)}
253
- placeholder="6-digit code"
254
- maxLength={6}
255
- required
256
- />
257
- {error && <p className="error">{error}</p>}
258
- <button type="submit" disabled={loading}>
259
- {loading ? 'Verifying...' : 'Login'}
260
- </button>
261
- <button type="button" onClick={() => setStep('email')}>
262
- Back
263
- </button>
264
- </form>
265
- )
266
- }
174
+ Adapter rules:
267
175
 
268
- return <p>Login successful!</p>
269
- }
270
- ```
176
+ - Use `mode: 'headless'` for external token providers.
177
+ - Use `mode: 'custom'` for in-app login pages that still call HowOne OTP/OAuth APIs.
178
+ - Do not create extra token storage keys in app code. Route all token writes through `client.auth.setToken()`.
179
+ - Do not implement app UI in the adapter. It may navigate or notify through callbacks, but visible UI belongs to the app.
180
+ - `getToken` may be sync or async; the SDK caches external tokens according to `tokenCacheMs`.
181
+ - `subscribe` is optional but recommended when the external auth provider can change outside SDK calls.
271
182
 
272
183
  ---
273
184
 
274
- ## Phone OTP Login
185
+ ## Custom login page (full pattern for AI codegen)
275
186
 
276
- Same flow as email, using E.164 phone number format (e.g. `+14155552671`).
187
+ ### 1. SDK (`src/lib/sdk.ts`)
277
188
 
278
189
  ```ts
279
- import {
280
- sendPhoneVerificationCode,
281
- loginWithPhoneCode,
282
- } from '@howone/sdk'
190
+ import { createClient, defineEntities, withEntities } from '@howone/sdk'
283
191
 
284
- // Step 1 — send SMS OTP
285
- const sendResult = await sendPhoneVerificationCode(
286
- '+14155552671', // must be E.164 format
287
- 'MyAppName', // optional
288
- )
289
- // sendResult.success: boolean
290
- // sendResult.expires_in: number (seconds)
291
-
292
- // Step 2 — verify OTP
293
- const loginResult = await loginWithPhoneCode(
294
- '+14155552671',
295
- '123456',
296
- )
297
- // loginResult.success: boolean
298
- // loginResult.token: string (JWT on success)
299
- // loginResult.user: { phone_e164?, email?, name? }
300
-
301
- if (loginResult.success && loginResult.token) {
302
- howone.auth.setToken(loginResult.token)
303
- }
304
- ```
192
+ const client = createClient({
193
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
194
+ env: import.meta.env.VITE_HOWONE_ENV,
195
+ })
305
196
 
306
- ### Phone number validation note
197
+ export default withEntities(client, defineEntities({ /* ... */ }))
198
+ ```
307
199
 
308
- The SDK validates that phone numbers are in E.164 format internally. Always pass the number with country code (e.g. `+1`, `+44`, `+86`).
200
+ ### 2. Provider (`main.tsx`)
309
201
 
310
- ---
202
+ ```tsx
203
+ import { HowOneProvider } from '@howone/sdk/react'
204
+ import './lib/sdk' // registers auth config
311
205
 
312
- ## OAuth — Google and GitHub
206
+ <HowOneProvider auth="none" brand="visible">
207
+ <App />
208
+ </HowOneProvider>
209
+ ```
313
210
 
314
- ### Google Login
211
+ ### 3. Login route (`/login`) — your styles, HowOne APIs
315
212
 
316
- ```ts
317
- import { unifiedAuth } from '@howone/sdk'
213
+ ```tsx
214
+ import {
215
+ sendEmailVerificationCode,
216
+ loginWithEmailCode,
217
+ unifiedAuth,
218
+ } from '@howone/sdk'
318
219
  import howone from '@/lib/sdk'
319
220
 
320
- async function loginWithGoogle() {
321
- // Opens a popup window for Google OAuth
322
- const result = await unifiedAuth.initiateGoogleLogin()
323
- // result.token: string (JWT)
324
- // result.user: any
325
-
221
+ // Email OTP
222
+ const send = await sendEmailVerificationCode(email)
223
+ const result = await loginWithEmailCode(email, code)
224
+ if (result.success && result.token) {
326
225
  howone.auth.setToken(result.token)
226
+ const user = await howone.me({ refresh: true })
227
+ navigate('/')
327
228
  }
328
- ```
329
-
330
- ### GitHub Login
331
229
 
332
- ```ts
333
- import { unifiedAuth } from '@howone/sdk'
230
+ // Google (popup — user stays on your page)
231
+ const { token } = await unifiedAuth.initiateGoogleLogin()
232
+ howone.auth.setToken(token)
334
233
 
335
- async function loginWithGitHub() {
336
- const result = await unifiedAuth.initiateGitHubLogin()
337
- howone.auth.setToken(result.token)
338
- }
234
+ // GitHub
235
+ const { token } = await unifiedAuth.initiateGitHubLogin()
236
+ howone.auth.setToken(token)
339
237
  ```
340
238
 
341
- ### Handle OAuth redirect callback
239
+ Phone OTP: `sendPhoneVerificationCode` + `loginWithPhoneCode` (E.164, e.g. `+8613800138000`).
342
240
 
343
- If you redirect instead of using a popup, call `checkOAuthCallback()` on the return page:
241
+ OAuth full-page callback (optional route `/auth/callback`):
344
242
 
345
243
  ```ts
346
244
  import { unifiedOAuth } from '@howone/sdk'
347
- import howone from '@/lib/sdk'
348
245
 
349
- // Call on the OAuth callback page (e.g. /auth/callback)
350
- function handleOAuthReturn() {
351
- const result = unifiedOAuth.checkOAuthCallback()
352
- // result.success: boolean
353
- // result.token: string (if success)
354
- // result.error: string (if failure)
355
- // result.user: any
356
-
357
- if (result.success && result.token) {
358
- howone.auth.setToken(result.token)
359
- window.location.href = '/dashboard'
360
- } else {
361
- console.error('OAuth failed:', result.error)
362
- }
246
+ const result = unifiedOAuth.checkOAuthCallback()
247
+ if (result.success && result.token) {
248
+ howone.auth.setToken(result.token)
249
+ navigate('/')
363
250
  }
364
251
  ```
365
252
 
366
- ### React OAuth buttons
253
+ ### 4. Protected routes
367
254
 
368
255
  ```tsx
369
- import { unifiedAuth } from '@howone/sdk'
370
- import howone from '@/lib/sdk'
371
-
372
- export function OAuthButtons({ onSuccess }: { onSuccess: () => void }) {
373
- const [loading, setLoading] = useState<'google' | 'github' | null>(null)
374
- const [error, setError] = useState<string | null>(null)
375
-
376
- async function handleGoogle() {
377
- setLoading('google')
378
- setError(null)
379
- try {
380
- const { token } = await unifiedAuth.initiateGoogleLogin()
381
- howone.auth.setToken(token)
382
- onSuccess()
383
- } catch (err) {
384
- setError(err instanceof Error ? err.message : 'Google login failed')
385
- } finally {
386
- setLoading(null)
387
- }
388
- }
389
-
390
- async function handleGitHub() {
391
- setLoading('github')
392
- setError(null)
393
- try {
394
- const { token } = await unifiedAuth.initiateGitHubLogin()
395
- howone.auth.setToken(token)
396
- onSuccess()
397
- } catch (err) {
398
- setError(err instanceof Error ? err.message : 'GitHub login failed')
399
- } finally {
400
- setLoading(null)
401
- }
402
- }
403
-
404
- return (
405
- <div>
406
- <button onClick={handleGoogle} disabled={loading !== null}>
407
- {loading === 'google' ? 'Connecting...' : 'Continue with Google'}
408
- </button>
409
- <button onClick={handleGitHub} disabled={loading !== null}>
410
- {loading === 'github' ? 'Connecting...' : 'Continue with GitHub'}
411
- </button>
412
- {error && <p className="error">{error}</p>}
413
- </div>
414
- )
415
- }
256
+ useEffect(() => {
257
+ howone.me()
258
+ .then(setUser)
259
+ .catch(() => navigate('/login', { replace: true }))
260
+ }, [])
416
261
  ```
417
262
 
418
- ---
263
+ Use `howone.me()`, not `howone.auth.isAuthenticated()`, for first load.
419
264
 
420
- ## Token Verification
265
+ ### 5. Logout button
421
266
 
422
267
  ```ts
423
- import { unifiedAuth } from '@howone/sdk'
424
-
425
- async function checkToken(token: string) {
426
- const { valid, user } = await unifiedAuth.verifyToken(token)
427
- if (valid) {
428
- console.log('User:', user)
429
- } else {
430
- console.log('Token is invalid or expired')
431
- }
432
- }
268
+ await howone.auth.logout()
269
+ // custom mode: already navigates to loginPath; no howone.dev redirect
433
270
  ```
434
271
 
435
- ## Custom Login UI Pattern
272
+ Do **not** call hosted-only patterns when `auth: 'custom'` is set:
436
273
 
437
- Use this when the app should show its own login page instead of redirecting to `/howone/auth`.
274
+ - ~~`howone.auth.login()` expecting howone.ai~~ (goes to `/login` instead OK)
275
+ - ~~Manual `window.location` to howone.dev~~
438
276
 
439
- ### SDK setup
277
+ ---
440
278
 
441
- ```ts
442
- // src/lib/sdk.ts
443
- import { createClient } from '@howone/sdk'
279
+ ## Hosted login (default)
444
280
 
445
- const client = createClient({
446
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
447
- env: import.meta.env.VITE_HOWONE_ENV,
448
- auth: { mode: 'managed' },
449
- })
281
+ Omit `auth` this is the default for `createClient({ projectId, env })`:
450
282
 
451
- export default client
283
+ ```ts
284
+ createClient({ projectId, env })
452
285
  ```
453
286
 
454
- `managed` is still correct for most custom login pages because `howone.auth.setToken(token)`
455
- persists the token through the SDK auth store. Use `headless` only when an external auth
456
- provider owns token storage.
457
-
458
- ### Provider setup
459
-
460
287
  ```tsx
461
- <HowOneProvider auth="none" brand="hidden">
288
+ <HowOneProvider auth="required">
462
289
  <App />
463
290
  </HowOneProvider>
464
291
  ```
465
292
 
466
- `auth="none"` disables the provider redirect so the app can render its own login form.
293
+ Unauthenticated users redirect to HowOne hosted `/auth`.
467
294
 
468
- ### App startup
295
+ ---
469
296
 
470
- ```tsx
471
- useEffect(() => {
472
- let cancelled = false
297
+ ## Headless external auth
473
298
 
474
- howone.me()
475
- .then((user) => {
476
- if (!cancelled) setUser(user)
477
- })
478
- .catch(() => {
479
- if (!cancelled) setUser(null)
480
- })
481
- .finally(() => {
482
- if (!cancelled) setLoading(false)
483
- })
484
-
485
- return () => {
486
- cancelled = true
487
- }
488
- }, [])
299
+ ```ts
300
+ createClient({
301
+ projectId,
302
+ env,
303
+ auth: {
304
+ mode: 'headless',
305
+ adapter: {
306
+ getToken: async () => externalAuth.getJwt(),
307
+ login: ({ returnUrl } = {}) => externalAuth.login({ returnUrl }),
308
+ logout: () => externalAuth.logout(),
309
+ },
310
+ tokenCacheMs: 30_000,
311
+ },
312
+ })
489
313
  ```
490
314
 
491
- Do not use `howone.auth.isAuthenticated()` as the first-load gate for protected UI. It is a
492
- synchronous token presence check. `howone.me()` verifies/restores the current session and is the
493
- right source of truth for initial render.
315
+ Do not use `howone.auth.setToken` for Clerk/Supabase unless bridging into HowOne JWT.
316
+
317
+ Headless mode should not redirect to HowOne hosted auth by default. The external adapter decides
318
+ what `login` and `logout` mean.
319
+
320
+ ---
321
+
322
+ ## Email / phone / OAuth API reference
323
+
324
+ (Same as before — see sections below for request/response shapes.)
494
325
 
495
- ### Login success
326
+ ### Email OTP
496
327
 
497
328
  ```ts
498
- const result = await loginWithEmailCode(email, code)
499
- if (!result.success || !result.token) {
500
- setError(result.message ?? 'Invalid code')
501
- return
502
- }
329
+ import { sendEmailVerificationCode, loginWithEmailCode } from '@howone/sdk'
503
330
 
504
- howone.auth.setToken(result.token)
505
- const user = await howone.me({ refresh: true })
506
- setUser(user)
331
+ await sendEmailVerificationCode(email, appName?)
332
+ const result = await loginWithEmailCode(email, code)
333
+ // result.token on success → howone.auth.setToken(result.token)
507
334
  ```
508
335
 
509
- Use the same pattern for phone OTP and OAuth popup results.
510
-
511
- ### Logout
336
+ ### Phone OTP
512
337
 
513
338
  ```ts
514
- const token = howone.auth.getToken()
515
- if (token) await unifiedAuth.logout(token)
516
- howone.auth.logout()
517
- setUser(null)
339
+ import { sendPhoneVerificationCode, loginWithPhoneCode } from '@howone/sdk'
518
340
  ```
519
341
 
520
- `howone.auth.logout()` clears the SDK token store and active client token.
342
+ ### OAuth popup
343
+
344
+ ```ts
345
+ import { unifiedAuth } from '@howone/sdk'
521
346
 
522
- ## Headless External Auth Pattern
347
+ await unifiedAuth.initiateGoogleLogin()
348
+ await unifiedAuth.initiateGitHubLogin()
349
+ ```
523
350
 
524
- Use `auth.mode = "headless"` only when another auth system owns the token lifecycle.
351
+ ### Server logout
525
352
 
526
353
  ```ts
527
- const client = createClient({
528
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
529
- env: import.meta.env.VITE_HOWONE_ENV,
530
- auth: {
531
- mode: 'headless',
532
- getToken: async () => externalAuth.getJwt(),
533
- tokenCacheMs: 30_000,
534
- },
535
- })
354
+ import { unifiedAuth } from '@howone/sdk'
355
+
356
+ const token = howone.auth.getToken()
357
+ if (token) await unifiedAuth.logout(token)
358
+ await howone.auth.logout()
536
359
  ```
537
360
 
538
- Do not invent a second localStorage key for HowOne OTP/OAuth login. Use `howone.auth.setToken`
539
- unless the user explicitly asks to integrate an external auth provider.
361
+ With `auth: 'custom'`, `howone.auth.logout()` already revokes and navigates locally.
540
362
 
541
363
  ---
542
364
 
543
- ## Logout
365
+ ## User profile
544
366
 
545
367
  ```ts
546
- // Client-side logout (clears local token storage)
547
- howone.auth.logout()
548
-
549
- // Server-side invalidation via unifiedAuth
550
- await unifiedAuth.logout(token)
368
+ const user = await howone.me()
369
+ const user = await howone.requireMe() // throws HowOneAuthError
551
370
  ```
552
371
 
553
372
  ---
@@ -558,59 +377,38 @@ await unifiedAuth.logout(token)
558
377
  import { HowOneAuthError } from '@howone/sdk'
559
378
 
560
379
  try {
561
- const user = await howone.requireMe()
380
+ await howone.requireMe()
562
381
  } catch (err) {
563
382
  if (err instanceof HowOneAuthError) {
564
- // err.code === 'UNAUTHENTICATED'
565
- howone.auth.login('/current-page')
383
+ howone.auth.login() // custom /login; hosted → howone.ai
566
384
  }
567
385
  }
568
386
  ```
569
387
 
570
388
  ---
571
389
 
572
- ## Auth Mode in createClient
573
-
574
- Choose the auth mode that matches your login strategy:
575
-
576
- ```ts
577
- // Managed (default) — SDK owns the token lifecycle
578
- const client = createClient({
579
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
580
- env: import.meta.env.VITE_HOWONE_ENV,
581
- auth: { mode: 'managed' },
582
- })
390
+ ## Common mistakes (AI agents)
583
391
 
584
- // Headless you own the token (for Clerk, Supabase, custom JWTs, etc.)
585
- const client = createClient({
586
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
587
- env: import.meta.env.VITE_HOWONE_ENV,
588
- auth: {
589
- mode: 'headless',
590
- getToken: async () => {
591
- return localStorage.getItem('auth_token')
592
- },
593
- tokenCacheMs: 30_000,
594
- },
595
- })
596
-
597
- // None — unauthenticated, public API access only
598
- const client = createClient({
599
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
600
- env: import.meta.env.VITE_HOWONE_ENV,
601
- auth: { mode: 'none' },
602
- })
603
- ```
392
+ | Mistake | Fix |
393
+ |---------|-----|
394
+ | Custom `/login` page with HowOne OTP/OAuth | Add `auth: 'custom'` in `createClient` |
395
+ | `HowOneProvider auth="required"` + custom login | Use `auth="none"`; guard with `howone.me()` |
396
+ | `howone.auth.logout()` expecting no redirect before this change | Now respects `auth: 'custom'` |
397
+ | `auth.isAuthenticated()` on first paint | Use `await howone.me()` |
398
+ | Phone without country code | E.164 `+86...` |
399
+ | JSON Schema in `defineAiAction` | Convert manifest JSON Schema to Zod |
400
+ | Second localStorage key for token | Only `howone.auth.setToken` |
401
+ | Custom external provider wired with `getToken` only but no logout | Add an `adapter.logout` if the host owns session clearing |
402
+ | App UI inside SDK auth adapter | Move UI to frontend components and use callbacks/navigation only |
604
403
 
605
404
  ---
606
405
 
607
- ## Common Mistakes
406
+ ## Non-negotiable for agents
608
407
 
609
- | Mistake | Correct Pattern |
610
- |---|---|
611
- | Phone number without country code: `'13800138000'` | Use E.164: `'+8613800138000'` |
612
- | Using `loginWithEmailCode` without first calling `sendEmailVerificationCode` | Always send the code first |
613
- | Hand-writing a separate localStorage token key for HowOne OTP/OAuth | Use `howone.auth.setToken(token)` |
614
- | Using `auth.isAuthenticated()` to decide first render after refresh | Use `await howone.me()` |
615
- | Setting `HowOneProvider auth="required"` with a custom in-app login form | Use `auth="none"` so the provider does not redirect |
616
- | Calling `unifiedAuth.logout()` without the token argument | Pass `token`: `await unifiedAuth.logout(howone.auth.getToken()!)` |
408
+ - Default **`createClient({ projectId, env })`** = HowOne hosted login. Add **`auth: 'custom'`** only for in-app login pages.
409
+ - Never hardcode howone.dev / howone.ai URLs in app login/logout when `auth: 'custom'`.
410
+ - Implement login UI with `sendEmailVerificationCode` / `loginWithEmailCode` / `unifiedAuth` — not iframe to hosted auth.
411
+ - After any successful login: `howone.auth.setToken(token)` then `await howone.me({ refresh: true })`.
412
+ - Logout: `await howone.auth.logout()` only (no manual redirect needed when `auth: 'custom'`).
413
+ - For external auth, use `auth.adapter`; do not patch request headers manually.
414
+ - SDK auth exposes state/callbacks only. Visible feedback, loading spinners, account menus, and errors are frontend app code.