howone 0.2.3 → 0.2.5
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/01-architect/01-app-generation.md +8 -7
- package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
- package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +13 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
- package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
- package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
- package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
- package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
- package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
- package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
- package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
- package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
- package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
- package/templates/vite/.howone/skills/howone/SKILL.md +48 -8
- package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
- package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
- package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/src/App.tsx +9 -5
- package/templates/vite/src/lib/sdk.ts +7 -5
- package/templates/vite/bun.lock +0 -1478
|
@@ -1,299 +1,122 @@
|
|
|
1
1
|
# Raw HTTP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use `howone.raw` only for an app-owned endpoint that has no typed entity, auth, upload, or AI
|
|
4
|
+
binding. Do not use it to bypass manifest access rules or recreate `/data` and workflow calls.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Contract
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## The RawHttpClient Interface
|
|
8
|
+
`howone.raw` is a request client whose methods resolve to response **data**, not an AxiosResponse:
|
|
12
9
|
|
|
13
10
|
```ts
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
get(config: RequestConfig): Promise<AxiosResponse>
|
|
20
|
-
post(config: RequestConfig): Promise<AxiosResponse>
|
|
21
|
-
put(config: RequestConfig): Promise<AxiosResponse>
|
|
22
|
-
patch(config: RequestConfig): Promise<AxiosResponse>
|
|
23
|
-
delete(config: RequestConfig): Promise<AxiosResponse>
|
|
24
|
-
|
|
25
|
-
// Cancel an in-flight request by URL
|
|
26
|
-
cancelRequest(url: string): void
|
|
27
|
-
|
|
28
|
-
// Cancel all in-flight requests
|
|
29
|
-
cancelAllRequests(): void
|
|
30
|
-
}
|
|
11
|
+
const data = await howone.raw.get<Stats>({ url: '/custom/stats' })
|
|
12
|
+
const created = await howone.raw.post<Receipt>({
|
|
13
|
+
url: '/custom/receipts',
|
|
14
|
+
data: { orderId },
|
|
15
|
+
})
|
|
31
16
|
```
|
|
32
17
|
|
|
33
|
-
### RequestConfig
|
|
34
|
-
|
|
35
|
-
`RequestConfig` extends `AxiosRequestConfig` with optional interceptors:
|
|
36
|
-
|
|
37
18
|
```ts
|
|
38
|
-
type RequestConfig<T =
|
|
19
|
+
type RequestConfig<T = unknown> = AxiosRequestConfig & {
|
|
39
20
|
interceptors?: {
|
|
40
21
|
requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
|
|
41
|
-
requestInterceptorCatch?: (error:
|
|
42
|
-
responseInterceptor?: (
|
|
43
|
-
responseInterceptorCatch?: (error:
|
|
22
|
+
requestInterceptorCatch?: (error: unknown) => unknown
|
|
23
|
+
responseInterceptor?: (data: T) => T
|
|
24
|
+
responseInterceptorCatch?: (error: unknown) => unknown
|
|
44
25
|
}
|
|
45
|
-
showLoading?: boolean
|
|
46
26
|
}
|
|
47
27
|
```
|
|
48
28
|
|
|
49
|
-
|
|
29
|
+
The SDK attaches the current client token to authenticated non-public requests. It does not turn
|
|
30
|
+
an app-owned route into a platform contract; the backend must authenticate and authorize it.
|
|
50
31
|
|
|
51
|
-
##
|
|
32
|
+
## URL rules
|
|
52
33
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// GET
|
|
57
|
-
const response = await howone.raw.get({
|
|
58
|
-
url: '/api/custom/stats',
|
|
59
|
-
})
|
|
60
|
-
const data = response.data // untyped AxiosResponse.data
|
|
61
|
-
|
|
62
|
-
// POST with body
|
|
63
|
-
const response = await howone.raw.post({
|
|
64
|
-
url: '/api/custom/send-notification',
|
|
65
|
-
data: {
|
|
66
|
-
userId: '123',
|
|
67
|
-
message: 'Hello!',
|
|
68
|
-
},
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
// PUT
|
|
72
|
-
const response = await howone.raw.put({
|
|
73
|
-
url: `/api/custom/profile/${userId}`,
|
|
74
|
-
data: { displayName: 'Alice' },
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// PATCH
|
|
78
|
-
const response = await howone.raw.patch({
|
|
79
|
-
url: `/api/custom/settings`,
|
|
80
|
-
data: { theme: 'dark' },
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// DELETE
|
|
84
|
-
const response = await howone.raw.delete({
|
|
85
|
-
url: `/api/custom/sessions/${sessionId}`,
|
|
86
|
-
})
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Typed Responses
|
|
92
|
-
|
|
93
|
-
Wrap with generics for type safety:
|
|
34
|
+
The configured REST base already ends in `/api` for hosted environments. Use a path relative to that
|
|
35
|
+
base:
|
|
94
36
|
|
|
95
37
|
```ts
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
activeToday: number
|
|
99
|
-
storageUsed: number
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const response = await howone.raw.get<StatsResponse>({
|
|
103
|
-
url: '/api/custom/stats',
|
|
104
|
-
})
|
|
38
|
+
// Correct: https://api.howone.dev/api/custom/stats
|
|
39
|
+
const stats = await howone.raw.get<Stats>({ url: '/custom/stats' })
|
|
105
40
|
|
|
106
|
-
|
|
41
|
+
// Wrong: https://api.howone.dev/api/api/custom/stats
|
|
42
|
+
// howone.raw.get({ url: '/api/custom/stats' })
|
|
107
43
|
```
|
|
108
44
|
|
|
109
|
-
|
|
45
|
+
An absolute URL is an explicit app-owned escape hatch and is not how EAX routing is selected. Do not
|
|
46
|
+
put an EAX URL in a raw request; AI actions already use `env`.
|
|
110
47
|
|
|
111
|
-
## Query
|
|
48
|
+
## Query, headers, and cancellation
|
|
112
49
|
|
|
113
50
|
```ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
page: 1,
|
|
120
|
-
limit: 20,
|
|
121
|
-
sort: 'createdAt',
|
|
122
|
-
order: 'desc',
|
|
123
|
-
},
|
|
51
|
+
const result = await howone.raw.get<SearchResult>({
|
|
52
|
+
url: '/custom/search',
|
|
53
|
+
params: { q, page: 1, limit: 20 },
|
|
54
|
+
headers: { 'X-Request-Source': 'app' },
|
|
55
|
+
signal: controller.signal,
|
|
124
56
|
})
|
|
125
|
-
// Calls: GET /api/custom/search?q=dragons&page=1&limit=20&sort=createdAt&order=desc
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Custom Headers
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
const response = await howone.raw.post({
|
|
134
|
-
url: '/api/custom/webhook',
|
|
135
|
-
data: payload,
|
|
136
|
-
headers: {
|
|
137
|
-
'X-Webhook-Secret': import.meta.env.VITE_WEBHOOK_SECRET,
|
|
138
|
-
'X-Request-Source': 'app',
|
|
139
|
-
},
|
|
140
|
-
})
|
|
141
|
-
```
|
|
142
57
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
## Request Cancellation
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
// Cancel a specific request by URL
|
|
149
|
-
howone.raw.cancelRequest('/api/custom/long-running')
|
|
150
|
-
|
|
151
|
-
// Cancel all in-flight requests (e.g. on page unmount)
|
|
58
|
+
// Cancels every in-flight request with this exact method-independent URL.
|
|
59
|
+
howone.raw.cancelRequest('/custom/search')
|
|
152
60
|
howone.raw.cancelAllRequests()
|
|
61
|
+
```
|
|
153
62
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function DataComponent() {
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
howone.raw.get({ url: '/api/custom/data' })
|
|
161
|
-
.then(res => setData(res.data))
|
|
63
|
+
Prefer an `AbortController` per component operation so a new search does not cancel an unrelated
|
|
64
|
+
request. The SDK links the caller's signal to its internal controller and also supports concurrent
|
|
65
|
+
requests with the same method and URL.
|
|
162
66
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
67
|
+
```tsx
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
void howone.raw
|
|
71
|
+
.get<SearchResult>({ url: '/custom/search', params: { q }, signal: controller.signal })
|
|
72
|
+
.then(setResult)
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
if (!controller.signal.aborted) setError(error instanceof Error ? error.message : String(error))
|
|
75
|
+
})
|
|
76
|
+
return () => controller.abort()
|
|
77
|
+
}, [q])
|
|
168
78
|
```
|
|
169
79
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
## Per-Request Interceptors
|
|
80
|
+
## Per-request interceptors
|
|
173
81
|
|
|
174
|
-
|
|
82
|
+
The response interceptor sees the already-unwrapped data:
|
|
175
83
|
|
|
176
84
|
```ts
|
|
177
|
-
const
|
|
178
|
-
url: '/
|
|
179
|
-
data: payload,
|
|
85
|
+
const data = await howone.raw.get<RawStats>({
|
|
86
|
+
url: '/custom/stats',
|
|
180
87
|
interceptors: {
|
|
181
88
|
requestInterceptor: (config) => {
|
|
182
|
-
|
|
183
|
-
config.headers['X-Timestamp'] = Date.now().toString()
|
|
89
|
+
config.headers.set('X-Request-Id', crypto.randomUUID())
|
|
184
90
|
return config
|
|
185
91
|
},
|
|
186
|
-
responseInterceptor: (
|
|
187
|
-
|
|
188
|
-
console.log('Response status:', res.status)
|
|
189
|
-
return res
|
|
190
|
-
},
|
|
191
|
-
responseInterceptorCatch: (error) => {
|
|
192
|
-
// Handle specific error codes
|
|
193
|
-
if (error.response?.status === 503) {
|
|
194
|
-
console.error('Service temporarily unavailable')
|
|
195
|
-
}
|
|
196
|
-
return Promise.reject(error)
|
|
197
|
-
},
|
|
92
|
+
responseInterceptor: (value) => ({ ...value, loadedAt: Date.now() }),
|
|
93
|
+
responseInterceptorCatch: (error) => Promise.reject(error),
|
|
198
94
|
},
|
|
199
95
|
})
|
|
200
96
|
```
|
|
201
97
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
## Direct Axios Instance Access
|
|
205
|
-
|
|
206
|
-
For maximum control (multipart forms, streaming responses, etc.):
|
|
207
|
-
|
|
208
|
-
```ts
|
|
209
|
-
const instance = howone.raw.instance // Axios instance
|
|
210
|
-
|
|
211
|
-
// Multipart form data
|
|
212
|
-
const formData = new FormData()
|
|
213
|
-
formData.append('report', file)
|
|
214
|
-
formData.append('meta', JSON.stringify({ type: 'monthly' }))
|
|
215
|
-
|
|
216
|
-
const response = await instance.post('/api/custom/reports', formData, {
|
|
217
|
-
headers: { 'Content-Type': 'multipart/form-data' },
|
|
218
|
-
onUploadProgress: (e) => {
|
|
219
|
-
const percent = Math.round((e.loaded * 100) / (e.total ?? 1))
|
|
220
|
-
setProgress(percent)
|
|
221
|
-
},
|
|
222
|
-
})
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## React Pattern: Data Fetching with Raw HTTP
|
|
228
|
-
|
|
229
|
-
```tsx
|
|
230
|
-
import { useEffect, useState } from 'react'
|
|
231
|
-
import howone from '@/lib/sdk'
|
|
232
|
-
|
|
233
|
-
type AnalyticsData = {
|
|
234
|
-
views: number
|
|
235
|
-
clicks: number
|
|
236
|
-
conversions: number
|
|
237
|
-
period: string
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function Analytics({ projectId }: { projectId: string }) {
|
|
241
|
-
const [data, setData] = useState<AnalyticsData | null>(null)
|
|
242
|
-
const [loading, setLoading] = useState(true)
|
|
243
|
-
const [error, setError] = useState<string | null>(null)
|
|
244
|
-
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
let cancelled = false
|
|
247
|
-
|
|
248
|
-
howone.raw.get<AnalyticsData>({
|
|
249
|
-
url: '/api/custom/analytics',
|
|
250
|
-
params: { projectId, period: '30d' },
|
|
251
|
-
})
|
|
252
|
-
.then(res => { if (!cancelled) setData(res.data) })
|
|
253
|
-
.catch(err => { if (!cancelled) setError(err.message) })
|
|
254
|
-
.finally(() => { if (!cancelled) setLoading(false) })
|
|
255
|
-
|
|
256
|
-
return () => {
|
|
257
|
-
cancelled = true
|
|
258
|
-
howone.raw.cancelRequest('/api/custom/analytics')
|
|
259
|
-
}
|
|
260
|
-
}, [projectId])
|
|
261
|
-
|
|
262
|
-
if (loading) return <div>Loading analytics...</div>
|
|
263
|
-
if (error) return <div>Error: {error}</div>
|
|
264
|
-
if (!data) return null
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<div>
|
|
268
|
-
<p>Views: {data.views}</p>
|
|
269
|
-
<p>Clicks: {data.clicks}</p>
|
|
270
|
-
<p>Conversions: {data.conversions}</p>
|
|
271
|
-
</div>
|
|
272
|
-
)
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
---
|
|
98
|
+
Do not expect `value.status` or `value.data` in `responseInterceptor`; use the direct Axios instance
|
|
99
|
+
only when the installed SDK typings explicitly require a transport-level operation.
|
|
277
100
|
|
|
278
|
-
##
|
|
101
|
+
## Choosing the correct surface
|
|
279
102
|
|
|
280
|
-
|
|
|
103
|
+
| Need | Use |
|
|
281
104
|
|---|---|
|
|
282
|
-
|
|
|
283
|
-
|
|
|
284
|
-
|
|
|
285
|
-
|
|
|
286
|
-
|
|
|
287
|
-
| Fetching analytics or aggregated data not in entities | `howone.raw.get()` |
|
|
288
|
-
| Uploading files | `howone.upload.*` |
|
|
105
|
+
| Manifest entity CRUD/query | `howone.entities.Entity.*` |
|
|
106
|
+
| Manifest-approved public data | `howone.public.entities.Entity.*` |
|
|
107
|
+
| EAX workflow | `howone.ai.action.run/stream/events` |
|
|
108
|
+
| HowOne storage | `howone.upload.*` |
|
|
109
|
+
| App-owned backend route | `howone.raw.*` |
|
|
289
110
|
|
|
290
|
-
|
|
111
|
+
If the endpoint is missing from the platform contract, classify it as an explicit app-owned
|
|
112
|
+
integration. Do not call an internal platform route because it happens to work today.
|
|
291
113
|
|
|
292
|
-
## Common
|
|
114
|
+
## Common mistakes
|
|
293
115
|
|
|
294
|
-
| Mistake |
|
|
116
|
+
| Mistake | Fix |
|
|
295
117
|
|---|---|
|
|
296
|
-
| `
|
|
297
|
-
|
|
|
298
|
-
| Calling
|
|
299
|
-
|
|
|
118
|
+
| Reading `response.data` | Raw methods already return data. |
|
|
119
|
+
| Prefixing `/api` in a raw path | The REST base already includes `/api`. |
|
|
120
|
+
| Calling raw `/data/...` | Use generated entity bindings. |
|
|
121
|
+
| Passing `'*'` as a cross-window origin | Use explicit origins in the devtools Provider. |
|
|
122
|
+
| Cancelling by a broad substring | Use the exact request path or the caller's AbortSignal. |
|