@assembly-js/app-foundation 0.0.36 → 0.0.37
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/CLAUDE.md +17 -1
- package/dist/replace-rows.js +2 -1
- package/package.json +1 -1
- package/skills/assembly-ai/SKILL.md +8 -7
package/CLAUDE.md
CHANGED
|
@@ -714,9 +714,25 @@ For an imperative one-off (button handler, mutation), use `callAction` from `use
|
|
|
714
714
|
- **Actions take no token.** Never accept, read, or pass a token — `getAssembly()` resolves the bearer itself. Never touch `AssemblyBridge.sessionToken.*` in a component or copy anything into your own `useState`/`useRef`.
|
|
715
715
|
- **Data actions must let `TokenInvalidError` propagate** — never `try/catch` it into `null`/empty. The auto-retry only fires if the action surfaces the expiry. And never hand-roll refresh/retry; `useServerAction`/`callAction` already do it.
|
|
716
716
|
|
|
717
|
+
#### Server action or route handler?
|
|
718
|
+
|
|
719
|
+
A client-to-server call is a server action by default. Its argument is a TypeScript parameter, so a payload that does not match the signature fails `tsc` in your inner loop. A route handler's body is an untyped wire contract — `JSON.stringify` on one side, `request.json()` on the other — and nothing checks that the two agree until a user sends the first request, which you cannot do from the sandbox. A mismatch `tsc` would catch in an action ships silently through a route and 400s for every user.
|
|
720
|
+
|
|
721
|
+
Write a route handler only when the caller needs a raw `Response` — streamed bytes, a file download, a webhook or cron entry point — or when the caller is not this app's own client component. When you do, define the body schema in a shared `lib/` module with no server-only imports, and type the client's payload with `z.input<typeof schema>` so the compiler checks the wire shape:
|
|
722
|
+
|
|
723
|
+
```ts
|
|
724
|
+
// lib/reply-contract.ts — imported by both the route and the component (`bun add zod` if it is not installed yet)
|
|
725
|
+
import { z } from 'zod';
|
|
726
|
+
export const replyBody = z.object({ parentId: z.string().uuid().nullish(), text: z.string().min(1) });
|
|
727
|
+
export type ReplyBody = z.input<typeof replyBody>;
|
|
728
|
+
|
|
729
|
+
// app/api/replies/route.ts: const parsed = replyBody.safeParse(await request.json());
|
|
730
|
+
// component: const body: ReplyBody = { parentId, text }; // tsc rejects what the route would 400
|
|
731
|
+
```
|
|
732
|
+
|
|
717
733
|
#### Client → server via route handlers — `authFetch`
|
|
718
734
|
|
|
719
|
-
For
|
|
735
|
+
For the route-handler calls that remain, use `authFetch` from `app/components/lib-client.ts`. The bearer rides the cookie automatically; `authFetch` adds the required CSRF header and re-exchanges + retries once on a bearer expiry:
|
|
720
736
|
|
|
721
737
|
```tsx
|
|
722
738
|
'use client';
|
package/dist/replace-rows.js
CHANGED
|
@@ -33,7 +33,8 @@ async function runStatements(db, statements) {
|
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
const batch = db.batch;
|
|
36
|
-
|
|
36
|
+
// drizzle's batch reads this.session, so it must be invoked on db, never as a bare function.
|
|
37
|
+
await batch.call(db, [first, ...rest]);
|
|
37
38
|
}
|
|
38
39
|
export async function replaceChildRows(db, spec) {
|
|
39
40
|
// Ids exist before the delete is built, so a row added in this save is on the keep-list.
|
package/package.json
CHANGED
|
@@ -37,25 +37,26 @@ export async function chatCompletion(messages: Array<{ role: string; content: st
|
|
|
37
37
|
|
|
38
38
|
## Server-side only
|
|
39
39
|
|
|
40
|
-
ALL AI calls MUST happen server-side (
|
|
40
|
+
ALL AI calls MUST happen server-side (server actions, server components, route handlers). Never call the AI proxy from client components.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Expose an AI call to the client as a server action — its arguments are type-checked end to end, so a payload mismatch fails `tsc` instead of the user's first request. Reserve a route handler for the streaming case below, where the client needs the raw `Response`.
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
// app/
|
|
46
|
-
|
|
45
|
+
// app/actions/summarize.ts
|
|
46
|
+
'use server';
|
|
47
47
|
import { chatCompletion } from '@/lib/ai';
|
|
48
48
|
|
|
49
|
-
export async function
|
|
50
|
-
const { text } = await request.json();
|
|
49
|
+
export async function summarize(text: string): Promise<string> {
|
|
51
50
|
const response = await chatCompletion([
|
|
52
51
|
{ role: 'user', content: `Summarize the following:\n\n${text}` },
|
|
53
52
|
]);
|
|
54
53
|
const data = await response.json();
|
|
55
|
-
return
|
|
54
|
+
return data.choices[0].message.content;
|
|
56
55
|
}
|
|
57
56
|
```
|
|
58
57
|
|
|
58
|
+
From a client component: `const summary = await callAction(() => summarize(text), { idempotent: true });` — the call writes nothing, so it is safe to re-run after a bearer renewal.
|
|
59
|
+
|
|
59
60
|
## Vision (image inputs)
|
|
60
61
|
|
|
61
62
|
The proxy supports OpenAI's vision API. Use `gpt-4o` with the multi-part content format to analyze images. The `content` field becomes an array of typed parts instead of a plain string:
|