howone 0.1.16 → 0.1.18

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howone",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "private": false,
5
5
  "description": "HowOne command line tools for creating app templates.",
6
6
  "type": "module",
@@ -12,13 +12,13 @@ This skill is the authoritative source of truth for all `@howone/sdk` usage in g
12
12
  | Module | File | What It Covers |
13
13
  |---|---|---|
14
14
  | Client Setup | `references/01-client-setup.md` | `createClient`, env vars, `CreateClientOptions`, all client fields |
15
- | Entity Operations | `references/02-entity-operations.md` | CRUD, `query`, `list`, `aggregate`, `bulkCreate`, typed records |
15
+ | Entity Operations | `references/02-entity-operations.md` | authenticated CRUD/query, public entity reads/writes, include, pagination, typed records |
16
16
  | AI Actions | `references/03-ai-actions.md` | `defineAiAction`, `run`/`stream`/`events`, zod schemas, SSE callbacks |
17
17
  | Auth | `references/04-auth.md` | Email OTP, Phone OTP, OAuth (Google/GitHub), token management |
18
18
  | File Upload | `references/05-file-upload.md` | `upload.file`, `upload.image`, `upload.batch`, progress, abort |
19
19
  | React Integration | `references/06-react-integration.md` | `HowOneProvider`, `useHowoneContext`, `FloatingButton`, `Loading` |
20
20
  | Raw HTTP | `references/07-raw-http.md` | `client.raw.*`, custom API calls, interceptors |
21
- | Manifest Codegen | `references/08-manifest-codegen.md` | Reading `.howone/database` and `.howone/ai`, generating `src/lib/sdk.ts` |
21
+ | Manifest Codegen | `references/08-manifest-codegen.md` | Reading `.howone/database` and `.howone/ai`, generating `src/lib/sdk.ts`, public access-aware types |
22
22
 
23
23
  ## Workflow
24
24
 
@@ -52,6 +52,7 @@ export type TodoRecord = EntityRecord & {
52
52
  }
53
53
  export type TodoCreate = { title: string; completed: boolean }
54
54
  export type TodoUpdate = Partial<TodoCreate>
55
+ export type TodoPublicQuery = { completed?: boolean; limit?: number }
55
56
 
56
57
  // Schemas
57
58
  export const summarizeTodoInputSchema = z.object({ title: z.string().min(1) })
@@ -83,7 +84,14 @@ export default howone
83
84
  ## Hard Rules
84
85
 
85
86
  - **Vite env only**: use `import.meta.env.VITE_HOWONE_PROJECT_ID` and `import.meta.env.VITE_HOWONE_ENV`. Never hardcode project IDs or add `?? 'prod'` fallbacks.
87
+ - **Single SDK config source**: configure `projectId` and `env` only in `createClient`. Do not pass env/project config to `HowOneProvider`; the React provider reads the SDK config set by `createClient`.
88
+ - **Authenticated owner semantics**: authenticated entity APIs derive owner from the JWT. Do not pass `created_by_id`, `created_by_user_id`, `ownerId`, or `puid` to authenticated queries or writes.
89
+ - **Public namespace**: public reads/writes must use `howone.public.entities.*` or `client.public.entity(...)`. Do not call authenticated `howone.entities.*` from public landing pages.
90
+ - **Public scoped queries**: for `access.public.read = "scoped"`, use `queryScoped` / `query.scoped` and pass every `requiredScopes` field such as `ownerId` and `slug`.
91
+ - **Public writes**: only generate public `create` / `update` calls when manifest access explicitly allows them. Public create must include `created_by_user_id` or the schema-defined owner scope.
86
92
  - **Explicit types**: define `EntityRecord`, `Create`, and `Update` types explicitly. Never derive create types from `Omit<EntityRecord & ...>` — it widens the payload.
93
+ - **System fields**: never include `id`, `_id`, `created_date`, `updated_date`, `created_by_id`, `schema_version_id`, `schema_version_number`, or `is_sample` in create/update input types.
94
+ - **Schema versions**: treat `schemaVersionId` / `schemaVersionNumber` on records as write-time schema metadata, not business versioning.
87
95
  - **AI action naming**: do not name actions `run`, `stream`, or `events` — those are reserved method names.
88
96
  - **Call shape**: `howone.ai.<actionName>.run(input)` / `.stream(input)` / `.events(input)`. Never `howone.ai.run.<actionName>(input)`.
89
97
  - **No hooks**: `@howone/sdk/react` provides auth/theme/loading UI only — no entity, query, or AI data hooks.
@@ -67,6 +67,22 @@ client.entity<TRecord, TCreate, TUpdate>(entityName: string): EntityClient
67
67
  // Typed entity map (populated via withEntities)
68
68
  client.entities: Record<string, EntityClient>
69
69
 
70
+ // Public entity namespace (never sends Authorization)
71
+ client.public.entity<TRecord, TPublicCreate, TPublicUpdate>(entityName: string): PublicEntityClient
72
+ client.public.entities: Record<string, PublicEntityClient>
73
+ client.public.raw: RawHttpClient
74
+
75
+ // Schema contract/version client
76
+ client.schema.listDefinitions()
77
+ client.schema.getDefinition(entityName)
78
+ client.schema.operate(operation)
79
+ client.schema.previewPatch(patch, { expectedVersionId, reason })
80
+ client.schema.applyPatch(patch, { expectedVersionId, reason })
81
+ client.schema.getState()
82
+ client.schema.listVersions()
83
+ client.schema.getVersion(versionId)
84
+ client.schema.restore(versionId, reason?)
85
+
70
86
  // AI action runner (low-level)
71
87
  client.ai: AiClient
72
88
 
@@ -215,9 +231,12 @@ const client = createClient({
215
231
  ```ts
216
232
  type UserProfile = {
217
233
  id: string
234
+ userId?: string
235
+ puid?: string
218
236
  email?: string
219
237
  name?: string
220
238
  avatarUrl?: string
239
+ appId?: string
221
240
  roles?: string[]
222
241
  metadata?: Record<string, unknown>
223
242
  }
@@ -242,6 +261,15 @@ client.auth.logout()
242
261
 
243
262
  ---
244
263
 
264
+ ## Client Namespace Rules
265
+
266
+ - Use `client.entities.*` / `howone.entities.*` for authenticated app data. The backend derives owner from the JWT; do not pass owner filters or owner fields.
267
+ - Use `client.public.entities.*` / `howone.public.entities.*` for public landing pages, public article lists, scoped QR/profile pages, and public forms.
268
+ - Use `client.schema.*` only for backend contract management: definitions, schema operations, schema patch preview/apply, versions, and restore.
269
+ - Use `client.raw.*` only for custom endpoints not covered by typed SDK methods.
270
+
271
+ ---
272
+
245
273
  ## HowOneAuthError
246
274
 
247
275
  ```ts
@@ -18,7 +18,9 @@ type EntityRecord = {
18
18
  id: string
19
19
  createdDate?: string
20
20
  updatedDate?: string
21
- createdById?: string
21
+ createdById?: string // backend owner id from created_by_id
22
+ schemaVersionId?: string
23
+ schemaVersionNumber?: number
22
24
  isSample?: boolean
23
25
  [key: string]: unknown // index signature — important for typing
24
26
  }
@@ -105,6 +107,25 @@ type DeleteResult = {
105
107
  }
106
108
  ```
107
109
 
110
+ ## PublicEntityClient API
111
+
112
+ Public entity calls use `/api/entities/public/apps/:appId/...` and do not send auth headers.
113
+ Only use them when the entity manifest explicitly allows `access.public.read`,
114
+ `access.public.create`, or `access.public.update`.
115
+
116
+ ```ts
117
+ type PublicEntityClient<TRecord, TPublicCreate, TPublicUpdate> = {
118
+ name: string
119
+ query(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
120
+ query.scoped(options: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
121
+ queryScoped(options: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
122
+ get(id: string, options?: QueryOptions<TRecord>): Promise<TRecord | null>
123
+ getOrThrow(id: string, options?: QueryOptions<TRecord>): Promise<TRecord>
124
+ create(data: TPublicCreate): Promise<TRecord>
125
+ update(id: string, data: TPublicUpdate): Promise<TRecord>
126
+ }
127
+ ```
128
+
108
129
  ---
109
130
 
110
131
  ## CRUD Examples
@@ -208,7 +229,12 @@ const result = await howone.entities.Story.query({
208
229
  })
209
230
  ```
210
231
 
211
- ### query.mine — only current user's records
232
+ ### query.mine — current authenticated user's records
233
+
234
+ `query.mine()` is the preferred way to fetch records owned by the authenticated user.
235
+ It requires a valid auth token, then lets the backend derive owner from the JWT. Do not
236
+ manually pass `created_by_id`, `created_by_user_id`, `ownerId`, or `puid` in authenticated
237
+ queries or writes.
212
238
 
213
239
  ```ts
214
240
  const myStories = await howone.entities.Story.query.mine({
@@ -217,6 +243,18 @@ const myStories = await howone.entities.Story.query.mine({
217
243
  })
218
244
  ```
219
245
 
246
+ For public pages that cannot use the current auth session, switch to `howone.public`.
247
+ The public endpoint is controlled by `access.public.allowedFilters` and
248
+ `access.public.requiredScopes`:
249
+
250
+ ```ts
251
+ const result = await howone.public.entities.Story.query({
252
+ published: true,
253
+ page: { number: 1, size: 20 },
254
+ orderBy: { publishedAt: 'desc' },
255
+ })
256
+ ```
257
+
220
258
  ### WhereInput — field operators
221
259
 
222
260
  ```ts
@@ -246,6 +284,18 @@ const result = await howone.entities.Story.query({
246
284
  })
247
285
  ```
248
286
 
287
+ ### Include relations
288
+
289
+ `include` accepts a string or string array. All entities support `include: 'user'`.
290
+ Entity-specific relation names must come from the manifest `relations` contract.
291
+
292
+ ```ts
293
+ const result = await howone.entities.Story.query({
294
+ include: ['user', 'author'],
295
+ page: { number: 1, size: 20 },
296
+ })
297
+ ```
298
+
249
299
  ---
250
300
 
251
301
  ## list() — Simple Array Read
@@ -268,6 +318,53 @@ const stories = await howone.entities.Story.list({ limit: 50, sort: 'title' })
268
318
 
269
319
  ---
270
320
 
321
+ ## Public Reads
322
+
323
+ Use public reads for public lists and scoped public pages.
324
+
325
+ ```ts
326
+ const articles = await howone.public.entities.Article.query({
327
+ published: true,
328
+ category: 'ai',
329
+ page: { number: 1, size: 20 },
330
+ orderBy: { publishedAt: 'desc' },
331
+ })
332
+ ```
333
+
334
+ For `access.public.read = "scoped"`, pass every required scope:
335
+
336
+ ```ts
337
+ const profile = await howone.public.entities.QrProfile.queryScoped({
338
+ ownerId: ownerSharedUserId,
339
+ slug: 'wechat',
340
+ active: true,
341
+ limit: 1,
342
+ })
343
+ ```
344
+
345
+ Public query fields must be present in `access.public.allowedFilters`, and public sort
346
+ fields must be present in `access.public.allowedSorts`. If a public query is rejected,
347
+ fix the schema access contract instead of falling back to authenticated APIs.
348
+
349
+ ---
350
+
351
+ ## Public Writes
352
+
353
+ Only generate public writes when `access.public.create` / `access.public.update` allows
354
+ them. Public create must include `created_by_user_id` unless the schema defines a
355
+ different required owner scope.
356
+
357
+ ```ts
358
+ await howone.public.entities.ContactMessage.create({
359
+ created_by_user_id: projectUserId,
360
+ name: 'Ada',
361
+ email: 'ada@example.com',
362
+ message: 'Please contact me',
363
+ })
364
+ ```
365
+
366
+ ---
367
+
271
368
  ## Bulk Create
272
369
 
273
370
  ```ts
@@ -45,10 +45,13 @@ import { HowOneAuthError } from '@howone/sdk'
45
45
  import howone from '@/lib/sdk'
46
46
 
47
47
  type UserProfile = {
48
- id: string
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
49
51
  email?: string
50
52
  name?: string
51
53
  avatarUrl?: string
54
+ appId?: string
52
55
  roles?: string[]
53
56
  metadata?: Record<string, unknown>
54
57
  }
@@ -66,6 +69,18 @@ const user = await howone.session.user()
66
69
  const user = await howone.me({ refresh: true })
67
70
  ```
68
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
+
69
84
  ### Pattern: guard a page
70
85
 
71
86
  ```tsx
@@ -31,18 +31,21 @@ import {
31
31
 
32
32
  Wrap your entire app. Provides auth, theme, and toast context to all children.
33
33
 
34
+ `HowOneProvider` reads SDK environment and project defaults from `createClient`. Configure
35
+ `projectId` and `env` once in `src/lib/sdk.ts`; do not pass a separate env to the provider.
36
+
34
37
  ```tsx
35
38
  // main.tsx or app/layout.tsx
36
39
  import React from 'react'
37
40
  import ReactDOM from 'react-dom/client'
38
41
  import { HowOneProvider } from '@howone/sdk/react'
42
+ import './lib/sdk'
39
43
  import App from './App'
40
44
 
41
45
  ReactDOM.createRoot(document.getElementById('root')!).render(
42
46
  <React.StrictMode>
43
47
  <HowOneProvider
44
- projectId={import.meta.env.VITE_HOWONE_PROJECT_ID}
45
- auth="optional"
48
+ auth="required"
46
49
  brand="visible"
47
50
  theme="system"
48
51
  >
@@ -62,7 +65,7 @@ type HowOneBrandMode = 'visible' | 'hidden'
62
65
  interface HowOneProviderProps {
63
66
  children: React.ReactNode
64
67
 
65
- // Project configuration (optional if createClient already set it)
68
+ // Optional override. Prefer configuring projectId once in createClient.
66
69
  projectId?: string
67
70
 
68
71
  // Auth behaviour
@@ -101,7 +104,7 @@ interface HowOneProviderProps {
101
104
  </HowOneProvider>
102
105
 
103
106
  // Dark mode forced
104
- <HowOneProvider auth="optional" theme="dark" forceTheme>
107
+ <HowOneProvider auth="required" theme="dark" forceTheme>
105
108
  <App />
106
109
  </HowOneProvider>
107
110
  ```
@@ -115,10 +118,13 @@ Access auth state inside any component wrapped by `HowOneProvider`.
115
118
  ```ts
116
119
  type HowoneContextValue = {
117
120
  user: {
118
- id: string
121
+ id: string // backend owner id; same as userId when JWT has userId
122
+ userId?: string // authenticated backend user id when present in JWT
123
+ puid?: string // public UUID; do not use as public ownerId scope
119
124
  email: string
120
125
  name: string
121
126
  avatar: string
127
+ appId?: string
122
128
  } | null
123
129
  token: string | null
124
130
  isAuthenticated: boolean
@@ -323,6 +329,7 @@ import React from 'react'
323
329
  import ReactDOM from 'react-dom/client'
324
330
  import { HowOneProvider } from '@howone/sdk/react'
325
331
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
332
+ import './lib/sdk'
326
333
  import App from './App'
327
334
 
328
335
  const queryClient = new QueryClient()
@@ -330,8 +337,7 @@ const queryClient = new QueryClient()
330
337
  ReactDOM.createRoot(document.getElementById('root')!).render(
331
338
  <React.StrictMode>
332
339
  <HowOneProvider
333
- projectId={import.meta.env.VITE_HOWONE_PROJECT_ID}
334
- auth="optional"
340
+ auth="required"
335
341
  theme="system"
336
342
  brand="visible"
337
343
  >
@@ -62,9 +62,41 @@ Sync tools (`sync_schema_artifacts`, `sync_ai_artifacts`) write the manifests. T
62
62
  | `array` (items: object) | `Record<string, unknown>[]` or inline type |
63
63
  | `object` | `Record<string, unknown>` |
64
64
  | `enum` | `'value1' \| 'value2' \| ...` |
65
+ | `["string", "null"]` | `string \| null` |
65
66
 
66
- - Fields in `required: true` are non-optional in `Record` and `Create` types.
67
- - Fields in `required: false` (or absent from required list) get `?` in `Record` and are optional in `Create`.
67
+ - Fields in `required: true` are non-optional in `Record` types.
68
+ - In `Create` types, required fields are required only when they do not have `default` or `autoGenerate`.
69
+ - Fields with `default`, `defaultValue`, or `autoGenerate` are optional in `Create`.
70
+ - Nullable fields include `null`.
71
+ - System response fields are never generated into `Create` or `Update`.
72
+
73
+ ### Access-aware generated helpers
74
+
75
+ Use the manifest `access` block to decide which namespace UI code should call:
76
+
77
+ ```ts
78
+ export type ArticlePublicQuery = {
79
+ published?: boolean
80
+ category?: string
81
+ slug?: string
82
+ page?: { number?: number; size?: number }
83
+ orderBy?: { publishedAt?: 'asc' | 'desc'; updatedDate?: 'asc' | 'desc' }
84
+ }
85
+
86
+ const publicArticles = await howone.public.entities.Article.query({
87
+ published: true,
88
+ orderBy: { publishedAt: 'desc' },
89
+ })
90
+ ```
91
+
92
+ Rules:
93
+
94
+ - `access.authenticated.*` drives `howone.entities.*`.
95
+ - `access.public.read = "list"` allows `howone.public.entities.Entity.query`.
96
+ - `access.public.read = "scoped"` requires `queryScoped` / `query.scoped` and all `requiredScopes`.
97
+ - Public query types must include only `allowedFilters`, `allowedSorts`, `page`, `limit`, `search`, `include`, and `exactCount`.
98
+ - Public create/update types should only be emitted when `access.public.create/update` is not `"none"`.
99
+ - Public create must include `created_by_user_id` when the schema requires public owner assignment.
68
100
 
69
101
  ### Generated TypeScript from the example manifest
70
102
 
@@ -335,7 +367,9 @@ Before finalising generated code, verify:
335
367
 
336
368
  - [ ] Every entity from `.howone/database/manifest.json` has a `Record`, `Create`, and `Update` type
337
369
  - [ ] `Create` types are defined **explicitly** (not via `Omit`)
338
- - [ ] Optional fields match `required: false` in the manifest
370
+ - [ ] Create optionality accounts for `required`, `default`, `defaultValue`, `autoGenerate`, and nullable types
371
+ - [ ] System fields are not present in create/update input types
372
+ - [ ] Public query/write types are generated only from `access.public.allowedFilters`, `allowedSorts`, `requiredScopes`, and write permissions
339
373
  - [ ] Every AI action from `.howone/ai/manifest.json` has an `inputSchema` zod object
340
374
  - [ ] Required input fields are not `.optional()` in zod
341
375
  - [ ] AI action names match the manifest `id` exactly (case-sensitive)
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "@base-ui/react": "^1.4.1",
16
16
  "@fontsource-variable/inter": "^5.2.8",
17
- "@howone/sdk": "2.0.0-beta.9",
17
+ "@howone/sdk": "2.0.0-beta.15",
18
18
  "@tailwindcss/vite": "^4.2.1",
19
19
  "class-variance-authority": "^0.7.1",
20
20
  "clsx": "^2.1.1",
@@ -2,7 +2,7 @@ import { HowOneProvider } from '@howone/sdk/react'
2
2
 
3
3
  function App() {
4
4
  return (
5
- <HowOneProvider auth="optional" brand="visible">
5
+ <HowOneProvider brand="visible">
6
6
  <div className="App">
7
7
  <h1>Hello HowOne</h1>
8
8
  </div>