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.
- package/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +180 -91
- package/templates/vite/.howone/skills/howone-sdk/01-architect/02-manifest-codegen.md +67 -4
- package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +463 -69
- package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +366 -64
- package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +204 -67
- package/templates/vite/.howone/skills/howone-sdk/02-database/04-query-dsl-and-responses.md +237 -0
- package/templates/vite/.howone/skills/howone-sdk/02-database/05-ai-persistence-patterns.md +372 -0
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/01-client-setup.md +58 -36
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/02-entity-operations.md +67 -0
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +267 -469
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +113 -320
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/07-ai-action-calls.md +66 -16
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/08-extension-boundaries.md +226 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +159 -96
- package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +353 -96
- package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +181 -42
- package/templates/vite/.howone/skills/howone-sdk/04-ai/04-service-capability-catalog.md +281 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/05-workflow-operations.md +256 -0
- package/templates/vite/.howone/skills/howone-sdk/04-ai/06-ai-feature-playbooks.md +296 -0
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +29 -12
- package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +2 -2
- package/templates/vite/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
|
@@ -1,553 +1,372 @@
|
|
|
1
1
|
# Auth
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Environment (read this first — dev must not hit prod)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
howone.auth.isAuthenticated() // boolean
|
|
21
|
+
Defaults when `auth` is omitted:
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
howone.auth.setToken(jwtToken)
|
|
26
|
+
Custom in-app login UI is **opt-in**: `auth: 'custom'` (see below).
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
```ts
|
|
29
|
+
// main.tsx
|
|
30
|
+
import './lib/sdk' // registers env first
|
|
31
|
+
import { sendEmailVerificationCode } from '@howone/sdk'
|
|
32
|
+
```
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
howone.auth.login()
|
|
33
|
-
howone.auth.login('/dashboard') // optional return path after login
|
|
34
|
+
Rules for agents:
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
##
|
|
43
|
+
## Custom login (opt-in only)
|
|
42
44
|
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
60
|
-
const user = await howone.me()
|
|
56
|
+
That wires:
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
const user = await howone.session.user()
|
|
64
|
+
Pair with React:
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
<HowOneProvider auth="none" brand="visible">
|
|
68
|
+
<App />
|
|
69
|
+
</HowOneProvider>
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
84
|
+
### Advanced object form
|
|
85
85
|
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
##
|
|
98
|
+
## Two auth layers
|
|
113
99
|
|
|
114
|
-
|
|
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
|
-
|
|
103
|
+
React: `HowOneProvider` + `useHowoneContext` — route guard only (`auth="required" | "optional" | "none"`).
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
109
|
+
---
|
|
124
110
|
|
|
125
|
-
|
|
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
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
//
|
|
149
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
import { unifiedAuth } from '@howone/sdk'
|
|
135
|
+
---
|
|
165
136
|
|
|
166
|
-
|
|
167
|
-
const sendResult = await unifiedAuth.sendEmailVerificationCode(email, appName)
|
|
168
|
-
const loginResult = await unifiedAuth.loginWithEmailCode(email, code)
|
|
137
|
+
## AuthManager / AuthAdapter extension point
|
|
169
138
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
185
|
+
## Custom login page (full pattern for AI codegen)
|
|
275
186
|
|
|
276
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
197
|
+
export default withEntities(client, defineEntities({ /* ... */ }))
|
|
198
|
+
```
|
|
307
199
|
|
|
308
|
-
|
|
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
|
-
|
|
206
|
+
<HowOneProvider auth="none" brand="visible">
|
|
207
|
+
<App />
|
|
208
|
+
</HowOneProvider>
|
|
209
|
+
```
|
|
313
210
|
|
|
314
|
-
###
|
|
211
|
+
### 3. Login route (`/login`) — your styles, HowOne APIs
|
|
315
212
|
|
|
316
|
-
```
|
|
317
|
-
import {
|
|
213
|
+
```tsx
|
|
214
|
+
import {
|
|
215
|
+
sendEmailVerificationCode,
|
|
216
|
+
loginWithEmailCode,
|
|
217
|
+
unifiedAuth,
|
|
218
|
+
} from '@howone/sdk'
|
|
318
219
|
import howone from '@/lib/sdk'
|
|
319
220
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
333
|
-
|
|
230
|
+
// Google (popup — user stays on your page)
|
|
231
|
+
const { token } = await unifiedAuth.initiateGoogleLogin()
|
|
232
|
+
howone.auth.setToken(token)
|
|
334
233
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
234
|
+
// GitHub
|
|
235
|
+
const { token } = await unifiedAuth.initiateGitHubLogin()
|
|
236
|
+
howone.auth.setToken(token)
|
|
339
237
|
```
|
|
340
238
|
|
|
341
|
-
|
|
239
|
+
Phone OTP: `sendPhoneVerificationCode` + `loginWithPhoneCode` (E.164, e.g. `+8613800138000`).
|
|
342
240
|
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
###
|
|
253
|
+
### 4. Protected routes
|
|
367
254
|
|
|
368
255
|
```tsx
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
265
|
+
### 5. Logout button
|
|
421
266
|
|
|
422
267
|
```ts
|
|
423
|
-
|
|
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
|
-
|
|
272
|
+
Do **not** call hosted-only patterns when `auth: 'custom'` is set:
|
|
436
273
|
|
|
437
|
-
|
|
274
|
+
- ~~`howone.auth.login()` expecting howone.ai~~ (goes to `/login` instead — OK)
|
|
275
|
+
- ~~Manual `window.location` to howone.dev~~
|
|
438
276
|
|
|
439
|
-
|
|
277
|
+
---
|
|
440
278
|
|
|
441
|
-
|
|
442
|
-
// src/lib/sdk.ts
|
|
443
|
-
import { createClient } from '@howone/sdk'
|
|
279
|
+
## Hosted login (default)
|
|
444
280
|
|
|
445
|
-
|
|
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
|
-
|
|
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="
|
|
288
|
+
<HowOneProvider auth="required">
|
|
462
289
|
<App />
|
|
463
290
|
</HowOneProvider>
|
|
464
291
|
```
|
|
465
292
|
|
|
466
|
-
|
|
293
|
+
Unauthenticated users redirect to HowOne hosted `/auth`.
|
|
467
294
|
|
|
468
|
-
|
|
295
|
+
---
|
|
469
296
|
|
|
470
|
-
|
|
471
|
-
useEffect(() => {
|
|
472
|
-
let cancelled = false
|
|
297
|
+
## Headless external auth
|
|
473
298
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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.
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
###
|
|
326
|
+
### Email OTP
|
|
496
327
|
|
|
497
328
|
```ts
|
|
498
|
-
|
|
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
|
-
|
|
505
|
-
const
|
|
506
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
### Logout
|
|
336
|
+
### Phone OTP
|
|
512
337
|
|
|
513
338
|
```ts
|
|
514
|
-
|
|
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
|
-
|
|
342
|
+
### OAuth popup
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { unifiedAuth } from '@howone/sdk'
|
|
521
346
|
|
|
522
|
-
|
|
347
|
+
await unifiedAuth.initiateGoogleLogin()
|
|
348
|
+
await unifiedAuth.initiateGitHubLogin()
|
|
349
|
+
```
|
|
523
350
|
|
|
524
|
-
|
|
351
|
+
### Server logout
|
|
525
352
|
|
|
526
353
|
```ts
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
365
|
+
## User profile
|
|
544
366
|
|
|
545
367
|
```ts
|
|
546
|
-
|
|
547
|
-
howone.
|
|
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
|
-
|
|
380
|
+
await howone.requireMe()
|
|
562
381
|
} catch (err) {
|
|
563
382
|
if (err instanceof HowOneAuthError) {
|
|
564
|
-
//
|
|
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
|
-
##
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
##
|
|
406
|
+
## Non-negotiable for agents
|
|
608
407
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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.
|