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
@@ -1,616 +0,0 @@
1
- # Auth
2
-
3
- ## Two Auth Layers
4
-
5
- HowOne has two distinct auth layers:
6
-
7
- 1. **Client-level auth** (`client.auth.*`) — low-level token management, login/logout redirects.
8
- 2. **HowOne Auth service** (`unifiedAuth`, standalone functions) — headless OTP and OAuth flows for building custom login UIs.
9
-
10
- For React auth UI, see `03-sdk/04-react-integration.md` (`HowOneProvider`, `useHowoneContext`).
11
-
12
- ---
13
-
14
- ## client.auth — Token Management
15
-
16
- ```ts
17
- import howone from '@/lib/sdk'
18
-
19
- // Check if the current user is authenticated
20
- howone.auth.isAuthenticated() // boolean
21
-
22
- // Get the current JWT token (null if not logged in)
23
- const token = howone.auth.getToken()
24
-
25
- // Manually set a token (after a custom login flow)
26
- howone.auth.setToken(jwtToken)
27
-
28
- // Clear the token
29
- howone.auth.setToken(null)
30
-
31
- // Redirect to the HowOne login page
32
- howone.auth.login()
33
- howone.auth.login('/dashboard') // optional return path after login
34
-
35
- // Log out and clear session
36
- howone.auth.logout()
37
- ```
38
-
39
- ---
40
-
41
- ## User Profile
42
-
43
- ```ts
44
- import { HowOneAuthError } from '@howone/sdk'
45
- import howone from '@/lib/sdk'
46
-
47
- type UserProfile = {
48
- id: string // 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
- }
58
-
59
- // Returns null if not authenticated
60
- const user = await howone.me()
61
-
62
- // Returns the profile or throws HowOneAuthError if not authenticated
63
- const user = await howone.requireMe()
64
-
65
- // Alias for me()
66
- const user = await howone.session.user()
67
-
68
- // Force refresh from server (skip cache)
69
- const user = await howone.me({ refresh: true })
70
- ```
71
-
72
- ### Identity fields
73
-
74
- HowOne JWTs may contain both `userId` and `puid`. The SDK preserves both:
75
-
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.
79
-
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`.
83
-
84
- ### Pattern: guard a page
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 }
107
- }
108
- ```
109
-
110
- ---
111
-
112
- ## Email OTP Login
113
-
114
- Use `sendEmailVerificationCode` → `loginWithEmailCode` to build a custom email login form.
115
-
116
- The SDK normalizes HowOne auth envelopes. Both of these backend response shapes are valid:
117
-
118
- ```ts
119
- { success: true, token: 'jwt...' }
120
- { code: 0, data: { success: true, token: 'jwt...' } }
121
- ```
122
-
123
- After normalization, read `result.success`, `result.token`, and `result.message` from the top level.
124
-
125
- ### Standalone functions (imported directly)
126
-
127
- ```ts
128
- import {
129
- sendEmailVerificationCode,
130
- loginWithEmailCode,
131
- } from '@howone/sdk'
132
-
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
- }
159
- ```
160
-
161
- ### Using the unifiedAuth service
162
-
163
- ```ts
164
- import { unifiedAuth } from '@howone/sdk'
165
-
166
- // Same API as standalone functions
167
- const sendResult = await unifiedAuth.sendEmailVerificationCode(email, appName)
168
- const loginResult = await unifiedAuth.loginWithEmailCode(email, code)
169
-
170
- // Verify an existing token
171
- const { valid, user } = await unifiedAuth.verifyToken(token)
172
-
173
- // Logout (server-side invalidation)
174
- await unifiedAuth.logout(token)
175
- ```
176
-
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
- }
267
-
268
- return <p>Login successful!</p>
269
- }
270
- ```
271
-
272
- ---
273
-
274
- ## Phone OTP Login
275
-
276
- Same flow as email, using E.164 phone number format (e.g. `+14155552671`).
277
-
278
- ```ts
279
- import {
280
- sendPhoneVerificationCode,
281
- loginWithPhoneCode,
282
- } from '@howone/sdk'
283
-
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
- ```
305
-
306
- ### Phone number validation note
307
-
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`).
309
-
310
- ---
311
-
312
- ## OAuth — Google and GitHub
313
-
314
- ### Google Login
315
-
316
- ```ts
317
- import { unifiedAuth } from '@howone/sdk'
318
- import howone from '@/lib/sdk'
319
-
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
-
326
- howone.auth.setToken(result.token)
327
- }
328
- ```
329
-
330
- ### GitHub Login
331
-
332
- ```ts
333
- import { unifiedAuth } from '@howone/sdk'
334
-
335
- async function loginWithGitHub() {
336
- const result = await unifiedAuth.initiateGitHubLogin()
337
- howone.auth.setToken(result.token)
338
- }
339
- ```
340
-
341
- ### Handle OAuth redirect callback
342
-
343
- If you redirect instead of using a popup, call `checkOAuthCallback()` on the return page:
344
-
345
- ```ts
346
- import { unifiedOAuth } from '@howone/sdk'
347
- import howone from '@/lib/sdk'
348
-
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
- }
363
- }
364
- ```
365
-
366
- ### React — OAuth buttons
367
-
368
- ```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
- }
416
- ```
417
-
418
- ---
419
-
420
- ## Token Verification
421
-
422
- ```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
- }
433
- ```
434
-
435
- ## Custom Login UI Pattern
436
-
437
- Use this when the app should show its own login page instead of redirecting to `/howone/auth`.
438
-
439
- ### SDK setup
440
-
441
- ```ts
442
- // src/lib/sdk.ts
443
- import { createClient } from '@howone/sdk'
444
-
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
- })
450
-
451
- export default client
452
- ```
453
-
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
- ```tsx
461
- <HowOneProvider auth="none" brand="hidden">
462
- <App />
463
- </HowOneProvider>
464
- ```
465
-
466
- `auth="none"` disables the provider redirect so the app can render its own login form.
467
-
468
- ### App startup
469
-
470
- ```tsx
471
- useEffect(() => {
472
- let cancelled = false
473
-
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
- }, [])
489
- ```
490
-
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.
494
-
495
- ### Login success
496
-
497
- ```ts
498
- const result = await loginWithEmailCode(email, code)
499
- if (!result.success || !result.token) {
500
- setError(result.message ?? 'Invalid code')
501
- return
502
- }
503
-
504
- howone.auth.setToken(result.token)
505
- const user = await howone.me({ refresh: true })
506
- setUser(user)
507
- ```
508
-
509
- Use the same pattern for phone OTP and OAuth popup results.
510
-
511
- ### Logout
512
-
513
- ```ts
514
- const token = howone.auth.getToken()
515
- if (token) await unifiedAuth.logout(token)
516
- howone.auth.logout()
517
- setUser(null)
518
- ```
519
-
520
- `howone.auth.logout()` clears the SDK token store and active client token.
521
-
522
- ## Headless External Auth Pattern
523
-
524
- Use `auth.mode = "headless"` only when another auth system owns the token lifecycle.
525
-
526
- ```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
- })
536
- ```
537
-
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.
540
-
541
- ---
542
-
543
- ## Logout
544
-
545
- ```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)
551
- ```
552
-
553
- ---
554
-
555
- ## HowOneAuthError
556
-
557
- ```ts
558
- import { HowOneAuthError } from '@howone/sdk'
559
-
560
- try {
561
- const user = await howone.requireMe()
562
- } catch (err) {
563
- if (err instanceof HowOneAuthError) {
564
- // err.code === 'UNAUTHENTICATED'
565
- howone.auth.login('/current-page')
566
- }
567
- }
568
- ```
569
-
570
- ---
571
-
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
- })
583
-
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
- ```
604
-
605
- ---
606
-
607
- ## Common Mistakes
608
-
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()!)` |