@assembly-js/app-foundation 0.0.25-rc.4 → 0.0.25-rc.6

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
@@ -56,6 +56,7 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
56
56
  - `assembly-route-auth` — REQUIRED on every `app/api/.../[id]/route.ts` handler: `requireAuth` + `requireCompanyScope` before returning or mutating data.
57
57
  - `assembly-scheduled-work` — REQUIRED when the prompt implies anything on a schedule or while nobody has the app open (remind, daily digest, every week, follow up in N days, at a specific time): one `*/30 * * * *` Vercel cron in `vercel.json` hitting one dispatcher route, every scheduled action a row in a schedule table, `CRON_SECRET` declared in `requiredSecrets` and verified on every tick, `getOfflineAssembly()` for platform calls. Checking for due work on page load ships a schedule that never fires.
58
58
  - `assembly-route-errors` — REQUIRED in every route handler: use `handleRouteError` from `@assembly-js/app-foundation/server` in catch blocks — never return `err.message`. Validate values before they hit typed DB columns (e.g. `parseUuid` before a uuid-column lookup).
59
+ - `assembly-action-results` — REQUIRED when a server action refuses work the UI should explain (validation, permission, not ready to send): return `{ ok: false, message }` instead of throwing. A thrown message is redacted in production.
59
60
  - `assembly-dev-server-logs` — the dev server's stdout/stderr (server-side errors, stack traces, console output) is readable at `/tmp/dev-server.log`; check it before guessing at a 500 or broken preview.
60
61
 
61
62
  ---
@@ -1147,6 +1148,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
1147
1148
  - ❌ Don't detect viewer type with `try { listClients() } catch (403)`. Use `getSessionIdentity()` (server) or `useAssembly()` (client) and branch on `internalUserId` vs `clientId`.
1148
1149
  - ❌ Don't include `companyId` (or any other unchanged field) in `updateClient.requestBody` "for completeness". Each field in `requestBody` is treated as a write — passing `companyId` triggers a company-reassignment path that clients can't perform, and the whole update fails. Pass ONLY the fields the user is actually changing.
1149
1150
  - ❌ Don't hardcode mutation error messages like "Failed to save preference". Read `error` from the response body and surface it to the user.
1151
+ - ❌ Don't `throw new Error` with a sentence the user should read from a server action. Production redacts that throw, so the toast shows a generic React error and the response is HTTP 500. Return `{ ok: false, message }` and toast `message`. Let `TokenInvalidError` and unexpected failures throw. See `assembly-action-results`.
1150
1152
  - ❌ Don't `Promise.all` a list-sized write fan-out (one `createNotification` / `markNotificationRead` / update per recipient or record). Writes cap at 5 concurrent per token; firing a whole list 429s and silently drops the overflow. Use `sendThrottled` from `@assembly-js/app-foundation/server`. A fixed handful of calls is the only exception. See "Write rate limit" above.
1151
1153
  - ❌ Don't use title case — see "Labels, copy, and dropdowns" above.
1152
1154
  - ❌ Don't render raw IDs or pass a raw ID string as `value` on `ComboboxItem` — see "Select and Combobox — base-ui differences" above.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.25-rc.4",
3
+ "version": "0.0.25-rc.6",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: assembly-action-results
3
+ description: REQUIRED when a server action refuses work for a reason the UI should show — validation, permission, or not ready (a draft that cannot be sent yet). Return { ok: false, message } instead of throwing. A thrown Error is redacted in production to a generic React error, so the toast shows that instead of your sentence. Let TokenInvalidError and unexpected failures still throw.
4
+ ---
5
+
6
+ Production Next.js redacts a thrown server-action error before it reaches the client. The name, code, and message become "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details." (React error #441), and the response is HTTP 500. The sentence you threw stays in the server log. A toast that reads `err.message` shows the redacted string.
7
+
8
+ Return the refusal as data. Throw only for failures the user should not read, and for `TokenInvalidError` — `callAction` renews the bearer only when the action throws.
9
+
10
+ ## The rule
11
+
12
+ ```ts
13
+ 'use server';
14
+
15
+ export type SendProposalResult =
16
+ | { ok: true; detail: ProposalDetail }
17
+ | { ok: false; message: string };
18
+
19
+ export async function sendProposal(id: string): Promise<SendProposalResult> {
20
+ const viewer = await getViewerContext(); // TokenInvalidError must throw
21
+ if (!viewer || viewer.kind !== 'internal') {
22
+ return { ok: false, message: 'Only staff can send proposals' };
23
+ }
24
+ if (!hasContentBlock) {
25
+ return { ok: false, message: 'Add at least one content block before sending' };
26
+ }
27
+ return { ok: true, detail };
28
+ }
29
+ ```
30
+
31
+ ```tsx
32
+ const result = await callAction(() => sendProposal(id));
33
+ if (!result.ok) {
34
+ AssemblyBridge.toast.show({
35
+ title: 'Could not send proposal',
36
+ description: result.message,
37
+ variant: 'error',
38
+ });
39
+ return;
40
+ }
41
+ ```
42
+
43
+ Leave off `{ idempotent: true }` for a write. `callAction` re-runs an idempotent call after any throw, including one that happens after the write already landed. Pass it only when a second call is a no-op, such as a conditional `draft` to `sent` update.
44
+
45
+ Name the control the user can act on. "Add at least one section" does not help when the editor's heading is "Content blocks".
46
+
47
+ ## What still throws
48
+
49
+ - Bearer expiry (`TokenInvalidError` from `getAssembly()`). Returning it as `{ ok: false }` stops the silent re-exchange.
50
+ - Unexpected failures (the database driver, an SDK 500). Leave those thrown. Do not copy `err.message` into the returned `message` — that string can be a driver error.
51
+
52
+ Route handlers return JSON and a status. Use `assembly-route-errors` there, not this result shape.
@@ -117,6 +117,9 @@ export const GET = cronRoute(async () => {
117
117
  ran++;
118
118
  } catch (err) {
119
119
  // One failing row must not poison the batch; leave it for the next tick.
120
+ // The error log is how the platform learns an action failed inside a
121
+ // tick that still returns 200 — keep it.
122
+ console.error('scheduled action failed', claimed.action, err);
120
123
  await db
121
124
  .update(scheduledActions)
122
125
  .set({ status: 'pending', claimedAt: null, lastError: err instanceof Error ? err.message : String(err) })