@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 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 mutations and route-handler calls from client components, 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:
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';
@@ -33,7 +33,8 @@ async function runStatements(db, statements) {
33
33
  return;
34
34
  }
35
35
  const batch = db.batch;
36
- await batch([first, ...rest]);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -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 (API routes, server components, server actions). Never call the AI proxy from client components.
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
- Example Next.js API route:
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/api/ai/summarize/route.ts
46
- import { NextRequest, NextResponse } from 'next/server';
45
+ // app/actions/summarize.ts
46
+ 'use server';
47
47
  import { chatCompletion } from '@/lib/ai';
48
48
 
49
- export async function POST(request: NextRequest) {
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 NextResponse.json({ summary: data.choices[0].message.content });
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: