howone 0.1.17 → 0.1.19
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/bin/index.mjs +36 -10
- package/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/SKILL.md +10 -2
- package/templates/vite/.howone/skills/howone-sdk/references/01-client-setup.md +28 -0
- package/templates/vite/.howone/skills/howone-sdk/references/02-entity-operations.md +99 -2
- package/templates/vite/.howone/skills/howone-sdk/references/04-auth.md +16 -1
- package/templates/vite/.howone/skills/howone-sdk/references/06-react-integration.md +10 -4
- package/templates/vite/.howone/skills/howone-sdk/references/08-manifest-codegen.md +37 -3
- package/templates/vite/AGENTS.md +49 -58
- package/templates/vite/package.json +1 -1
package/bin/index.mjs
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import {
|
|
4
|
-
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
5
4
|
import { readdir, rename, stat, writeFile } from "node:fs/promises";
|
|
6
|
-
import { copy, emptyDir, ensureDir, pathExists } from "fs-extra/esm";
|
|
7
5
|
import path from "node:path";
|
|
8
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { say, spinner } from "@astrojs/cli-kit";
|
|
9
|
+
import { cancel, confirm, isCancel, select, text } from "@clack/prompts";
|
|
10
|
+
import { copy, emptyDir, ensureDir, pathExists } from "fs-extra/esm";
|
|
9
11
|
import pc from "picocolors";
|
|
10
12
|
import validatePackageName from "validate-npm-package-name";
|
|
11
13
|
import yargsParser from "yargs-parser";
|
|
12
14
|
//#region src/index.ts
|
|
13
15
|
const packageJson = createRequire(import.meta.url)("../package.json");
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
14
17
|
const DEFAULT_PROJECT_NAME = "howone-app";
|
|
15
18
|
const TEMPLATES = ["vite", "nextjs"];
|
|
16
19
|
const TEMPLATE_ALIASES = {
|
|
@@ -111,10 +114,13 @@ function compact(values) {
|
|
|
111
114
|
async function runInitApp(pathArgs, options) {
|
|
112
115
|
const plan = await resolveCreatePlan(pathArgs, options, path.resolve(options.cwd ?? process.cwd()), process.stdout.isTTY && !options.yes && !options.json);
|
|
113
116
|
const howoneContext = resolveHowoneCreateContext();
|
|
114
|
-
if (options.json)
|
|
115
|
-
|
|
117
|
+
if (options.json) {
|
|
118
|
+
await createTemplate(plan, howoneContext, true);
|
|
119
|
+
await installTemplateDependencies(plan, true);
|
|
120
|
+
} else {
|
|
116
121
|
await say(pc.bold("HowOne"));
|
|
117
122
|
await createTemplate(plan, howoneContext, false);
|
|
123
|
+
await installTemplateDependencies(plan, false);
|
|
118
124
|
}
|
|
119
125
|
const result = {
|
|
120
126
|
ok: true,
|
|
@@ -128,11 +134,7 @@ async function runInitApp(pathArgs, options) {
|
|
|
128
134
|
env: howoneContext.env,
|
|
129
135
|
envFile: path.join(plan.targetDir, ".env")
|
|
130
136
|
} } : {},
|
|
131
|
-
nextSteps: [
|
|
132
|
-
`cd ${formatPathForShell(path.relative(plan.cwd, plan.targetDir) || ".")}`,
|
|
133
|
-
"bun install",
|
|
134
|
-
"bun run dev"
|
|
135
|
-
]
|
|
137
|
+
nextSteps: [`cd ${formatPathForShell(path.relative(plan.cwd, plan.targetDir) || ".")}`, "bun run dev"]
|
|
136
138
|
};
|
|
137
139
|
if (options.json) {
|
|
138
140
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -291,6 +293,30 @@ async function createTemplate(plan, howoneContext, json) {
|
|
|
291
293
|
while: () => performCreateTemplate(plan, howoneContext)
|
|
292
294
|
});
|
|
293
295
|
}
|
|
296
|
+
async function installTemplateDependencies(plan, json) {
|
|
297
|
+
const install = async () => {
|
|
298
|
+
try {
|
|
299
|
+
await execFileAsync("bun", ["install"], {
|
|
300
|
+
cwd: plan.targetDir,
|
|
301
|
+
maxBuffer: 10 * 1024 * 1024
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
throw new CliError("E_DEP_INSTALL_FAILED", "Failed to install dependencies with bun.", error && typeof error === "object" ? {
|
|
305
|
+
stdout: "stdout" in error ? String(error.stdout ?? "") : void 0,
|
|
306
|
+
stderr: "stderr" in error ? String(error.stderr ?? "") : void 0
|
|
307
|
+
} : void 0);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
if (json) {
|
|
311
|
+
await install();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await spinner({
|
|
315
|
+
start: "Installing dependencies with bun",
|
|
316
|
+
end: "Dependencies installed",
|
|
317
|
+
while: install
|
|
318
|
+
});
|
|
319
|
+
}
|
|
294
320
|
async function performCreateTemplate(plan, howoneContext) {
|
|
295
321
|
const templateDir = getTemplateDir(plan.template);
|
|
296
322
|
if (plan.force && await pathExists(plan.targetDir)) await emptyDir(plan.targetDir);
|
package/package.json
CHANGED
|
@@ -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,
|
|
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 —
|
|
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,17 +31,20 @@ 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
48
|
auth="required"
|
|
46
49
|
brand="visible"
|
|
47
50
|
theme="system"
|
|
@@ -62,7 +65,7 @@ type HowOneBrandMode = 'visible' | 'hidden'
|
|
|
62
65
|
interface HowOneProviderProps {
|
|
63
66
|
children: React.ReactNode
|
|
64
67
|
|
|
65
|
-
//
|
|
68
|
+
// Optional override. Prefer configuring projectId once in createClient.
|
|
66
69
|
projectId?: string
|
|
67
70
|
|
|
68
71
|
// Auth behaviour
|
|
@@ -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,7 +337,6 @@ 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
340
|
auth="required"
|
|
335
341
|
theme="system"
|
|
336
342
|
brand="visible"
|
|
@@ -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`
|
|
67
|
-
-
|
|
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
|
-
- [ ]
|
|
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)
|
package/templates/vite/AGENTS.md
CHANGED
|
@@ -1,53 +1,45 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This is a standard HowOne Vite app.
|
|
4
4
|
|
|
5
|
-
Read this file once when entering the project.
|
|
6
|
-
Follow it during implementation.
|
|
7
|
-
Do not repeatedly reread it unless it changes or the current task requires checking project rules again.
|
|
5
|
+
Read this file once when entering the project, follow it during implementation, and do not reread it unless it changes or the current task genuinely requires checking project rules again.
|
|
8
6
|
|
|
9
|
-
##
|
|
7
|
+
## Project Defaults
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
- Package manager: `bun`
|
|
10
|
+
- Common commands: `bun run dev`, `bun run typecheck`, `bun run build`
|
|
11
|
+
- The scaffold normally installs dependencies during `howone init app`. If `node_modules` is missing, run `bun install` before validation instead of treating missing binaries as code failures.
|
|
12
|
+
- Default stack: React, Vite, TypeScript, Tailwind CSS, shadcn/ui-style components, `@howone/sdk`
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Source of Truth
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
- trusted libraries and framework assumptions
|
|
18
|
-
- context-loading rules
|
|
19
|
-
- validation rules
|
|
20
|
-
- project-specific constraints
|
|
16
|
+
Use project-local sources in this order:
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
1. Synced HowOne manifests under `.howone/`
|
|
19
|
+
2. Generated SDK bindings such as `src/lib/sdk.ts`
|
|
20
|
+
3. The smallest directly relevant app files
|
|
21
|
+
4. Package/config metadata
|
|
22
|
+
5. Dependency source only when a concrete mismatch or missing detail remains
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Do not guess generated entity names, AI action names, schemas, workflow IDs, or auth behavior when a synced manifest exists.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
- current phase: explore, implement, validate, fix, or done
|
|
28
|
-
- files already read
|
|
29
|
-
- files changed
|
|
30
|
-
- last validation command and result
|
|
31
|
-
- known blockers or open questions
|
|
26
|
+
## Context Discipline
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Do not reread this file unless:
|
|
36
|
-
|
|
37
|
-
- this file changed
|
|
38
|
-
- the project policy summary is unavailable
|
|
39
|
-
- the task enters a nested project with its own `AGENTS.md`
|
|
40
|
-
- validation errors suggest a project rule was missed
|
|
28
|
+
Start with the smallest useful context:
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
1. Read the smallest file that directly controls the requested behavior.
|
|
31
|
+
2. Prefer manifests, config, and public library APIs before local library internals.
|
|
32
|
+
3. Expand only when implementation, validation, or a concrete ambiguity requires it.
|
|
43
33
|
|
|
44
|
-
For
|
|
34
|
+
For ordinary page or feature work:
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
- Start with the relevant route, page, feature component, or entry file.
|
|
37
|
+
- Read additional local files only when they directly control the behavior being changed or resolve a specific uncertainty.
|
|
38
|
+
- If a manifest, config file, or validation error answers the question, prefer that over widening the file search.
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
## Trusted Libraries
|
|
49
41
|
|
|
50
|
-
|
|
42
|
+
Treat common public libraries as known dependencies during ordinary feature work:
|
|
51
43
|
|
|
52
44
|
- React
|
|
53
45
|
- Vite
|
|
@@ -59,39 +51,38 @@ Examples of trusted public libraries:
|
|
|
59
51
|
- clsx
|
|
60
52
|
- class-variance-authority
|
|
61
53
|
|
|
62
|
-
|
|
54
|
+
Do not inspect `src/components/ui/*` or other library-style source files just to rediscover standard APIs.
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
2. Read `package.json`, `components.json`, `tsconfig.json`, or alias config only when needed.
|
|
66
|
-
3. Implement the feature.
|
|
67
|
-
4. Run build/typecheck.
|
|
68
|
-
5. Inspect the smallest relevant local file only if validation fails or the API is genuinely unclear.
|
|
56
|
+
Using or importing a standard UI primitive is not by itself a reason to read its local source. For ordinary feature work, use the public component name and existing import path first.
|
|
69
57
|
|
|
70
|
-
|
|
58
|
+
Read local component or library-style source only when:
|
|
71
59
|
|
|
72
|
-
-
|
|
73
|
-
- imports cannot be resolved from
|
|
74
|
-
- the
|
|
75
|
-
- the task
|
|
76
|
-
- the project has custom wrappers,
|
|
60
|
+
- validation fails
|
|
61
|
+
- imports cannot be resolved from package/config metadata
|
|
62
|
+
- the API is genuinely unclear
|
|
63
|
+
- the task edits that component
|
|
64
|
+
- the project has custom wrappers, variants, or unusual exports
|
|
77
65
|
- the user explicitly asks to follow local component conventions
|
|
78
66
|
|
|
79
67
|
Prefer failure-driven inspection over speculative context loading.
|
|
80
68
|
|
|
81
|
-
##
|
|
82
|
-
|
|
83
|
-
Start with the smallest useful context:
|
|
69
|
+
## HowOne Runtime and Auth
|
|
84
70
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
- Use synced HowOne manifests plus `src/lib/sdk.ts` as the source of truth for generated entity and AI bindings.
|
|
72
|
+
- For SDK work, use the `howone-sdk` skill before relying on package internals.
|
|
73
|
+
- Choose auth posture from the schema access contract, not from guesswork:
|
|
74
|
+
- authenticated/private app data uses `howone.entities.*`; the backend derives ownership from the JWT
|
|
75
|
+
- public pages use `howone.public.entities.*` only when the manifest explicitly allows public access
|
|
76
|
+
- For private authenticated features, use the app-level HowOne auth flow instead of inventing unauthenticated fallback data paths.
|
|
77
|
+
- Do not pass owner fields such as `created_by_id`, `created_by_user_id`, `ownerId`, or `puid` into authenticated entity queries or writes.
|
|
89
78
|
|
|
90
|
-
|
|
79
|
+
## Validation
|
|
91
80
|
|
|
92
|
-
|
|
81
|
+
After meaningful code changes:
|
|
93
82
|
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
1. If dependencies are unavailable, run `bun install` first.
|
|
84
|
+
2. Run the narrowest relevant validation, usually `bun run typecheck`.
|
|
85
|
+
3. Run `bun run build` when the change affects app wiring, SDK integration, routes, or production bundling.
|
|
86
|
+
4. If validation fails, inspect the smallest failing surface before widening context.
|
|
96
87
|
|
|
97
|
-
|
|
88
|
+
Work is complete when the requested change is implemented, relevant validation has been run, and any remaining blocker is reported clearly.
|
|
@@ -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.
|
|
17
|
+
"@howone/sdk": "2.0.0-beta.16",
|
|
18
18
|
"@tailwindcss/vite": "^4.2.1",
|
|
19
19
|
"class-variance-authority": "^0.7.1",
|
|
20
20
|
"clsx": "^2.1.1",
|