howone 0.1.23 → 0.1.26

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 (36) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +215 -0
  3. package/templates/vite/.howone/skills/{howone-sdk → howone}/01-architect/02-manifest-codegen.md +67 -4
  4. package/templates/vite/.howone/skills/howone/02-database/01-schema-design.md +541 -0
  5. package/templates/vite/.howone/skills/howone/02-database/02-schema-operations.md +398 -0
  6. package/templates/vite/.howone/skills/howone/02-database/03-data-access-patterns.md +309 -0
  7. package/templates/vite/.howone/skills/howone/02-database/04-query-dsl-and-responses.md +237 -0
  8. package/templates/vite/.howone/skills/howone/02-database/05-ai-persistence-patterns.md +372 -0
  9. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/01-client-setup.md +58 -36
  10. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/02-entity-operations.md +67 -0
  11. package/templates/vite/.howone/skills/howone/03-sdk/03-auth.md +414 -0
  12. package/templates/vite/.howone/skills/howone/03-sdk/04-react-integration.md +191 -0
  13. package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/07-ai-action-calls.md +168 -64
  14. package/templates/vite/.howone/skills/howone/03-sdk/08-extension-boundaries.md +226 -0
  15. package/templates/vite/.howone/skills/howone/04-ai/01-ai-capability-architecture.md +205 -0
  16. package/templates/vite/.howone/skills/howone/04-ai/02-workflow-contract-rules.md +426 -0
  17. package/templates/vite/.howone/skills/howone/04-ai/03-ai-sdk-handoff.md +234 -0
  18. package/templates/vite/.howone/skills/howone/04-ai/04-service-capability-catalog.md +281 -0
  19. package/templates/vite/.howone/skills/howone/04-ai/05-workflow-operations.md +256 -0
  20. package/templates/vite/.howone/skills/howone/04-ai/06-ai-feature-playbooks.md +296 -0
  21. package/templates/vite/.howone/skills/{howone-sdk → howone}/SKILL.md +29 -12
  22. package/templates/vite/.howone/skills/howone/agents/openai.yaml +4 -0
  23. package/templates/vite/package.json +1 -1
  24. package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +0 -126
  25. package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +0 -147
  26. package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +0 -96
  27. package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +0 -172
  28. package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +0 -616
  29. package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +0 -398
  30. package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
  31. package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +0 -142
  32. package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +0 -169
  33. package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +0 -80
  34. package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +0 -4
  35. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/05-file-upload.md +0 -0
  36. /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/06-raw-http.md +0 -0
@@ -22,19 +22,18 @@ type CreateClientOptions = {
22
22
  caseStyle?: 'camel' | 'snake' // Default: 'camel'
23
23
  mode?: 'auto' | 'standalone' | 'embedded'
24
24
 
25
- // ── Auth ──────────────────────────────────────────────────
26
- auth?: {
27
- mode?: 'none' | 'managed' | 'headless'
28
- getToken?: () => Promise<string | null> // Custom token provider (headless)
29
- tokenCacheMs?: number // How long to cache the token
30
- tokenInjection?: {
31
- allowedOrigins?: string[]
32
- waitMs?: number
33
- clearUrlParamsAfterInjectionMs?: number
34
- clearAllUrlParams?: boolean
35
- sensitiveParams?: string[]
36
- }
25
+ // ── Auth (one parameter for custom login) ─────────────────
26
+ auth?: 'custom' | 'hosted' | 'headless' | 'none' | {
27
+ mode?: 'custom' | 'hosted' | 'headless' | 'none' | 'managed'
28
+ loginPath?: string // default '/login' when mode is 'custom'
29
+ logoutPath?: string
30
+ guard?: 'required' | 'optional' | 'none'
31
+ getToken?: () => Promise<string | null>
32
+ adapter?: AuthAdapter
33
+ tokenCacheMs?: number
37
34
  }
35
+ loginPath?: string // shorthand when auth is 'custom'
36
+ logoutPath?: string
38
37
 
39
38
  // ── Limit-exceeded callbacks ───────────────────────────────
40
39
  limitExceeded?: {
@@ -99,12 +98,16 @@ client.me(options?) // Promise<UserProfile | null>
99
98
  client.requireMe(options?) // Promise<UserProfile> throws if unauthenticated
100
99
  client.session.user() // alias for client.me()
101
100
 
102
- // Auth helpers
101
+ // Auth helpers (behavior driven by createClient auth mode)
102
+ client.auth.mode // 'custom' | 'hosted' | 'headless' | 'none'
103
+ client.auth.loginPath // e.g. '/login'
103
104
  client.auth.setToken(token: string | null)
104
105
  client.auth.getToken(): string | null
105
106
  client.auth.isAuthenticated(): boolean
106
- client.auth.login(redirect?: string) // redirects to HowOne login page
107
- client.auth.logout()
107
+ client.auth.login(returnUrl?: string)
108
+ await client.auth.logout()
109
+ await client.auth.clearSession({ redirect?: false | string })
110
+ client.auth.subscribe((state) => { ... }) // auth state callback
108
111
 
109
112
  // URL utilities
110
113
  client.sanitizeUrl(opts?: { clearAll?: boolean; sensitiveParams?: string[] })
@@ -121,6 +124,8 @@ import {
121
124
  defineAiAction,
122
125
  defineAiActions,
123
126
  defineEntities,
127
+ pickEntityPayload,
128
+ runAiActionAndPersist,
124
129
  type EntityRecord,
125
130
  withAiActions,
126
131
  withEntities,
@@ -159,6 +164,17 @@ const howone = withAiActions(withEntities(client, entities), ai)
159
164
  export default howone
160
165
  ```
161
166
 
167
+ SDK utility exports that generated apps may use:
168
+
169
+ | Utility | Use |
170
+ |---|---|
171
+ | `pickEntityPayload(definition, payload)` | Keep only schema-declared business fields before create/update. |
172
+ | `validateEntityPayload(definition, payload)` | Return structured issues for unknown/system/ownership/missing required fields. |
173
+ | `assertEntityPayload(definition, payload)` | Throw structured `EntityPayloadValidationError` before unsafe writes. |
174
+ | `validatePublicEntityQuery(definition, options)` | Check public filters, sorts, scopes, and limits against `access.public`. |
175
+ | `assertPublicEntityQuery(definition, options)` | Throw before generating an invalid public query. |
176
+ | `runAiActionAndPersist(options)` | Standard pending-first AI execution + entity persistence helper. |
177
+
162
178
  ---
163
179
 
164
180
  ## Environment Variables
@@ -173,40 +189,46 @@ VITE_HOWONE_ENV=prod
173
189
  Rules:
174
190
  - **Do not** add `?? 'prod'` or `?? ''` fallbacks. Missing env vars should surface as misconfiguration errors.
175
191
  - **Do not** hardcode project IDs in source. Use the env var.
176
- - `env` accepts `'local'`, `'dev'`, or `'prod'`. SDK routes API calls to the correct endpoint automatically.
192
+ - `env` accepts `'local'`, `'dev'`, or `'prod'`. **Auth OTP/OAuth, entities, AI, and uploads all use this same env.**
193
+ - Import `src/lib/sdk.ts` before calling `loginWithEmailCode` / `unifiedAuth` so env is pinned (otherwise auth defaults to prod APIs).
194
+
195
+ | `env` | API base | Auth API example |
196
+ |-------|----------|------------------|
197
+ | `local` | `http://localhost:3002/api` | `http://localhost:3002/api/auth/email/send-code` |
198
+ | `dev` | `https://api.howone.dev/api` | `https://api.howone.dev/api/auth/email/send-code` |
199
+ | `prod` | `https://api.howone.ai/api` | `https://api.howone.ai/api/auth/email/send-code` |
177
200
 
178
201
  ---
179
202
 
180
203
  ## Auth Modes
181
204
 
205
+ See `03-sdk/03-auth.md` for the full custom-login playbook.
206
+
182
207
  ```ts
183
- // Managed (default) SDK handles login redirect and token storage
184
- const client = createClient({
185
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
186
- env: import.meta.env.VITE_HOWONE_ENV,
187
- auth: { mode: 'managed' },
188
- })
208
+ // DefaultHowOne hosted login (howone.dev / howone.ai)
209
+ createClient({ projectId, env })
189
210
 
190
- // Headless provide your own token (e.g. Supabase, Clerk, etc.)
191
- const client = createClient({
192
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
193
- env: import.meta.env.VITE_HOWONE_ENV,
211
+ // Custom in-app login page; auth APIs still HowOne
212
+ createClient({ projectId, env, auth: 'custom', loginPath: '/login' })
213
+
214
+ // Headless — external JWT provider
215
+ createClient({
216
+ projectId,
217
+ env,
194
218
  auth: {
195
219
  mode: 'headless',
196
- getToken: async () => {
197
- // return your JWT or null
198
- return localStorage.getItem('my_token')
220
+ adapter: {
221
+ getToken: async () => externalAuth.getToken(),
222
+ setToken: (token) => externalAuth.setToken(token),
223
+ login: ({ returnUrl } = {}) => router.push(`/login?redirect=${encodeURIComponent(returnUrl ?? '/')}`),
224
+ logout: () => router.push('/'),
199
225
  },
200
- tokenCacheMs: 60_000, // cache for 1 minute
226
+ tokenCacheMs: 60_000,
201
227
  },
202
228
  })
203
229
 
204
- // None — no auth, all requests are unauthenticated
205
- const client = createClient({
206
- projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
207
- env: import.meta.env.VITE_HOWONE_ENV,
208
- auth: { mode: 'none' },
209
- })
230
+ // None — public app, no auth
231
+ createClient({ projectId, env, auth: 'none' })
210
232
  ```
211
233
 
212
234
  ---
@@ -260,7 +260,9 @@ const result = await howone.public.entities.Story.query({
260
260
  ```ts
261
261
  type FieldOperator<T> = {
262
262
  eq?: T // exact match (same as plain value)
263
+ equals?: T // alias for eq
263
264
  ne?: T // not equal
265
+ not?: T // not equal alias
264
266
  gt?: T // greater than
265
267
  gte?: T // greater than or equal
266
268
  lt?: T // less than
@@ -268,9 +270,14 @@ type FieldOperator<T> = {
268
270
  contains?: string // substring (string fields)
269
271
  like?: string // SQL LIKE pattern
270
272
  startsWith?: string
273
+ starts?: string
271
274
  endsWith?: string
275
+ ends?: string
272
276
  in?: T[] // value in array
273
277
  notIn?: T[] // value not in array
278
+ null?: boolean // null / not null
279
+ empty?: boolean // empty / not empty
280
+ exists?: boolean // field exists / missing
274
281
  }
275
282
 
276
283
  // Examples
@@ -346,6 +353,26 @@ Public query fields must be present in `access.public.allowedFilters`, and publi
346
353
  fields must be present in `access.public.allowedSorts`. If a public query is rejected,
347
354
  fix the schema access contract instead of falling back to authenticated APIs.
348
355
 
356
+ When generated code has the synced entity definition available, validate public queries before
357
+ calling the API:
358
+
359
+ ```ts
360
+ import { assertPublicEntityQuery } from '@howone/sdk'
361
+ import { articleEntityDefinition } from '@/lib/sdk'
362
+
363
+ const query = {
364
+ where: { status: 'published' },
365
+ orderBy: { publishedAt: 'desc' },
366
+ page: { number: 1, size: 20 },
367
+ }
368
+
369
+ assertPublicEntityQuery(articleEntityDefinition, query)
370
+ const result = await howone.public.entities.Article.query(query)
371
+ ```
372
+
373
+ Use `validatePublicEntityQuery()` when the app wants to show its own validation UI instead of
374
+ throwing.
375
+
349
376
  ---
350
377
 
351
378
  ## Public Writes
@@ -365,6 +392,44 @@ await howone.public.entities.ContactMessage.create({
365
392
 
366
393
  ---
367
394
 
395
+ ## Payload Contract Utilities
396
+
397
+ Use these helpers when code maps form state, AI output, or mixed UI state into entity writes.
398
+ They prevent the common mistake of sending UI-only, workflow-envelope, ownership, or system fields.
399
+
400
+ ```ts
401
+ import { pickEntityPayload, assertEntityPayload } from '@howone/sdk'
402
+ import { generationEntityDefinition } from '@/lib/sdk'
403
+
404
+ const draft = {
405
+ prompt,
406
+ status: 'pending',
407
+ created_by_id: user.id, // stripped/rejected
408
+ gradientDirection: 'to right', // stripped/rejected unless schema declares it
409
+ }
410
+
411
+ const payload = pickEntityPayload(generationEntityDefinition, draft)
412
+ assertEntityPayload(generationEntityDefinition, payload)
413
+
414
+ await howone.entities.Generation.create(payload)
415
+ ```
416
+
417
+ Rules:
418
+
419
+ - Use `pickEntityPayload()` when transforming broad UI objects into narrow create/update payloads.
420
+ - Use `validateEntityPayload()` to collect issues for app-owned validation UI.
421
+ - Use `assertEntityPayload()` before writes in generated helper functions.
422
+ - For updates, pass `{ partial: true }` to avoid requiring create-time fields.
423
+ - These helpers do not replace backend validation; they make generated frontend code fail earlier
424
+ with clearer errors.
425
+
426
+ ```ts
427
+ assertEntityPayload(generationEntityDefinition, update, { partial: true })
428
+ await howone.entities.Generation.update(id, update)
429
+ ```
430
+
431
+ ---
432
+
368
433
  ## Bulk Create
369
434
 
370
435
  ```ts
@@ -474,3 +539,5 @@ function useDeleteStory() {
474
539
  | `client.entity('Story')` without generics | `client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story')` |
475
540
  | Using `list()` when you need pagination | Use `query()` for paginated UIs |
476
541
  | Calling `query()` inside render without guarding re-runs | Wrap in `useEffect` with cancellation or use TanStack Query |
542
+ | Sending form/workflow object directly to `create()` | Use `pickEntityPayload()` and `assertEntityPayload()` |
543
+ | Public query with illegal field/sort | Use `assertPublicEntityQuery()` and fix schema guardrails |
@@ -0,0 +1,414 @@
1
+ # Auth
2
+
3
+ ## Environment (read this first — dev must not hit prod)
4
+
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
+
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` |
12
+
13
+ ```ts
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
+ ```
20
+
21
+ Defaults when `auth` is omitted:
22
+
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`)
25
+
26
+ Custom in-app login UI is **opt-in**: `auth: 'custom'` (see below).
27
+
28
+ ```ts
29
+ // main.tsx
30
+ import './lib/sdk' // registers env first
31
+ import { sendEmailVerificationCode } from '@howone/sdk'
32
+ ```
33
+
34
+ Rules for agents:
35
+
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.
40
+
41
+ ---
42
+
43
+ ## Custom login (opt-in only)
44
+
45
+ Add `auth: 'custom'` when the app renders its own `/login` page but still uses HowOne OTP/OAuth APIs:
46
+
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
+ ```
55
+
56
+ That wires:
57
+
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 |
63
+
64
+ Pair with React:
65
+
66
+ ```tsx
67
+ <HowOneProvider auth="none" brand="visible">
68
+ <App />
69
+ </HowOneProvider>
70
+ ```
71
+
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.
73
+
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 |
81
+
82
+ Legacy alias: `'managed'` → `'hosted'`.
83
+
84
+ ### Advanced object form
85
+
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
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Two auth layers
99
+
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.
102
+
103
+ React: `HowOneProvider` + `useHowoneContext` — route guard only (`auth="required" | "optional" | "none"`).
104
+
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.
108
+
109
+ ---
110
+
111
+ ## `client.auth` API
112
+
113
+ ```ts
114
+ import howone from '@/lib/sdk'
115
+
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
+ })
133
+ ```
134
+
135
+ ---
136
+
137
+ ## AuthManager / AuthAdapter extension point
138
+
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.
141
+
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
+ })
172
+ ```
173
+
174
+ Adapter rules:
175
+
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.
182
+
183
+ ---
184
+
185
+ ## Custom login page (full pattern for AI codegen)
186
+
187
+ ### 1. SDK (`src/lib/sdk.ts`)
188
+
189
+ ```ts
190
+ import { createClient, defineEntities, withEntities } from '@howone/sdk'
191
+
192
+ const client = createClient({
193
+ projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
194
+ env: import.meta.env.VITE_HOWONE_ENV,
195
+ })
196
+
197
+ export default withEntities(client, defineEntities({ /* ... */ }))
198
+ ```
199
+
200
+ ### 2. Provider (`main.tsx`)
201
+
202
+ ```tsx
203
+ import { HowOneProvider } from '@howone/sdk/react'
204
+ import './lib/sdk' // registers auth config
205
+
206
+ <HowOneProvider auth="none" brand="visible">
207
+ <App />
208
+ </HowOneProvider>
209
+ ```
210
+
211
+ ### 3. Login route (`/login`) — your styles, HowOne APIs
212
+
213
+ ```tsx
214
+ import {
215
+ sendEmailVerificationCode,
216
+ loginWithEmailCode,
217
+ unifiedAuth,
218
+ } from '@howone/sdk'
219
+ import howone from '@/lib/sdk'
220
+
221
+ // Email OTP
222
+ const send = await sendEmailVerificationCode(email)
223
+ const result = await loginWithEmailCode(email, code)
224
+ if (result.success && result.token) {
225
+ howone.auth.setToken(result.token)
226
+ const user = await howone.me({ refresh: true })
227
+ navigate('/')
228
+ }
229
+
230
+ // Google (popup — user stays on your page)
231
+ const { token } = await unifiedAuth.initiateGoogleLogin()
232
+ howone.auth.setToken(token)
233
+
234
+ // GitHub
235
+ const { token } = await unifiedAuth.initiateGitHubLogin()
236
+ howone.auth.setToken(token)
237
+ ```
238
+
239
+ Phone OTP: `sendPhoneVerificationCode` + `loginWithPhoneCode` (E.164, e.g. `+8613800138000`).
240
+
241
+ OAuth full-page callback (optional route `/auth/callback`):
242
+
243
+ ```ts
244
+ import { unifiedOAuth } from '@howone/sdk'
245
+
246
+ const result = unifiedOAuth.checkOAuthCallback()
247
+ if (result.success && result.token) {
248
+ howone.auth.setToken(result.token)
249
+ navigate('/')
250
+ }
251
+ ```
252
+
253
+ ### 4. Protected routes
254
+
255
+ ```tsx
256
+ useEffect(() => {
257
+ howone.me()
258
+ .then(setUser)
259
+ .catch(() => navigate('/login', { replace: true }))
260
+ }, [])
261
+ ```
262
+
263
+ Use `howone.me()`, not `howone.auth.isAuthenticated()`, for first load.
264
+
265
+ ### 5. Logout button
266
+
267
+ ```ts
268
+ await howone.auth.logout()
269
+ // custom mode: already navigates to loginPath; no howone.dev redirect
270
+ ```
271
+
272
+ Do **not** call hosted-only patterns when `auth: 'custom'` is set:
273
+
274
+ - ~~`howone.auth.login()` expecting howone.ai~~ (goes to `/login` instead — OK)
275
+ - ~~Manual `window.location` to howone.dev~~
276
+
277
+ ---
278
+
279
+ ## Hosted login (default)
280
+
281
+ Omit `auth` — this is the default for `createClient({ projectId, env })`:
282
+
283
+ ```ts
284
+ createClient({ projectId, env })
285
+ ```
286
+
287
+ ```tsx
288
+ <HowOneProvider auth="required">
289
+ <App />
290
+ </HowOneProvider>
291
+ ```
292
+
293
+ Unauthenticated users redirect to HowOne hosted `/auth`.
294
+
295
+ ---
296
+
297
+ ## Headless external auth
298
+
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
+ })
313
+ ```
314
+
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.)
325
+
326
+ ### Email OTP
327
+
328
+ ```ts
329
+ import { sendEmailVerificationCode, loginWithEmailCode } from '@howone/sdk'
330
+
331
+ await sendEmailVerificationCode(email, appName?)
332
+ const result = await loginWithEmailCode(email, code)
333
+ // result.token on success → howone.auth.setToken(result.token)
334
+ ```
335
+
336
+ ### Phone OTP
337
+
338
+ ```ts
339
+ import { sendPhoneVerificationCode, loginWithPhoneCode } from '@howone/sdk'
340
+ ```
341
+
342
+ ### OAuth popup
343
+
344
+ ```ts
345
+ import { unifiedAuth } from '@howone/sdk'
346
+
347
+ await unifiedAuth.initiateGoogleLogin()
348
+ await unifiedAuth.initiateGitHubLogin()
349
+ ```
350
+
351
+ ### Server logout
352
+
353
+ ```ts
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()
359
+ ```
360
+
361
+ With `auth: 'custom'`, `howone.auth.logout()` already revokes and navigates locally.
362
+
363
+ ---
364
+
365
+ ## User profile
366
+
367
+ ```ts
368
+ const user = await howone.me()
369
+ const user = await howone.requireMe() // throws HowOneAuthError
370
+ ```
371
+
372
+ ---
373
+
374
+ ## HowOneAuthError
375
+
376
+ ```ts
377
+ import { HowOneAuthError } from '@howone/sdk'
378
+
379
+ try {
380
+ await howone.requireMe()
381
+ } catch (err) {
382
+ if (err instanceof HowOneAuthError) {
383
+ howone.auth.login() // custom → /login; hosted → howone.ai
384
+ }
385
+ }
386
+ ```
387
+
388
+ ---
389
+
390
+ ## Common mistakes (AI agents)
391
+
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 |
403
+
404
+ ---
405
+
406
+ ## Non-negotiable for agents
407
+
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.