@assembly-js/app-foundation 0.0.10 → 0.0.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-app-bridge-header
3
- description: Use any time the app sets breadcrumbs, primary CTA, secondary CTA, or actions menu in the Assembly header chrome. Always drive the header through the useAssemblyHeader hook — never call AssemblyBridge.header.set* from your own useEffect. Critical — the parent automatically prepends the app's name as breadcrumb 0; never include it yourself, or it renders twice.
3
+ description: Use any time the app sets breadcrumbs, primary/secondary CTA, or actions menu in the header chrome. Always drive it via useAssemblyHeader — never call AssemblyBridge.header.set* from your own useEffect. The parent auto-prepends the app name as breadcrumb 0 — never include it yourself.
4
4
  ---
5
5
 
6
6
  The Assembly header (breadcrumbs, primary/secondary CTA buttons, overflow actions menu) is controlled from your app. **Drive it with the `useAssemblyHeader` hook** — it ships in the foundation at `app/components/use-assembly-header.ts`.
@@ -70,6 +70,20 @@ useAssemblyHeader({ onRootClick: goHome, breadcrumbs: [{ label: 'Reports' }] });
70
70
 
71
71
  Use breadcrumbs only for hierarchical navigation, not branding. A flat one-page app omits `breadcrumbs` (or passes `[]`). A multi-level app uses one entry per level: `[{ label: 'Reports' }, { label: 'Q4 2026' }]`. A breadcrumb's `onClick` makes it clickable; omit it for the current/leaf level.
72
72
 
73
+ ## Loading state on a `primaryCta` — no `disabled`/`loading` prop
74
+
75
+ Header CTAs have no `disabled` or `loading` prop. Indicate progress by changing the `primaryCta` value itself — swap the label and replace `onClick` with a no-op while the action runs, then restore both when it completes:
76
+
77
+ ```tsx
78
+ useAssemblyHeader({
79
+ primaryCta: isSaving
80
+ ? { label: 'Saving…', icon: 'Check', onClick: () => {} }
81
+ : { label: 'Save', icon: 'Check', onClick: save },
82
+ });
83
+ ```
84
+
85
+ Because the hook re-applies whatever value you pass on every render, this is just a state change — no imperative set/clear needed. Don't let the user click a button and see nothing happen.
86
+
73
87
  ## Icon and color constraints
74
88
 
75
89
  - **`icon` must be a `CtaIcon`** — the only valid values are `'Archive' | 'Plus' | 'Templates' | 'Trash' | 'Check'`. Any other string (e.g. `'Download'`, `'Settings'`, `'Edit'`) is a type error — omit the icon (it's optional) when none fit.
@@ -1,7 +1,7 @@
1
1
  <!-- feature:api-resources -->
2
2
  ---
3
3
  name: assembly-automations
4
- description: Use when the request describes a workflow that integrates with the Assembly platform — messaging a client, updating a CRM custom field, sending a contract or form, sharing a file, anything landing on Assembly's own data rather than only the app's tables — and it should happen in reaction to something happening in the app rather than because the user pressed a button. "When a time entry is logged, message the contact" and "completing a project should update the client's status field" both qualify. Both halves are required, and the word "automation" is not — a reaction that only writes the app's own tables is ordinary app code, and so is a platform write the user triggers directly in the app UI. Also use when the request asks the app to fire webhooks, drive or react to another tool, or expose operations others can invoke. Declare those events and operations in .assembly/api-resources.json so they appear as automation triggers and actions, emit each declared event at runtime with assembly.fetch, and serve each exposed operation with automationActionRoute. Covers the manifest schema, the two field shapes, event naming, the recipient-id contract, trigger configuration, dropdown option sources for action inputs, runtime event emission, and the action route contract.
4
+ description: REQUIRED when a request should react to an in-app event AND write to the Assembly platform (message a client, set a CRM field, send a contract/form, share a file) — not just the app's tables. Covers api-resources.json schema, event naming, triggers/actions, webhooks, automationActionRoute.
5
5
  ---
6
6
 
7
7
  An app's API contract — the events it emits and the operations workspace automations can invoke — is declared in **`.assembly/api-resources.json`** at the repo root. The platform reads it at publish and registers the app's triggers and actions. Which file owns what: `.assembly/app.json` = how the platform runs the app (surfaces, secrets, integrations); `.assembly/api-resources.json` = the API contract the app exposes. Never mix their keys.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-avatar-sub
3
- description: Use whenever a client's name or avatar is rendered in a list, table, card, or detail header — including a table of the app's own records (reports, requests, tasks, invoices) that merely reference a client, not just a client directory. Clients belong to many companies, so a client shown without its company is ambiguous. Covers the AvatarSub company badge and the row-duplication pattern via expandClientsByCompany.
3
+ description: Use whenever a client's name or avatar renders in a list, table, card, or detail header — including tables of app records that reference a client. Clients belong to many companies, so an avatar without its company is ambiguous. Covers AvatarSub + expandClientsByCompany row-duplication.
4
4
  ---
5
5
 
6
6
  `AvatarSub` renders a single nested sub-avatar overlaid at the bottom-right of the parent avatar. It always contains exactly one `Avatar`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-clients-companies
3
- description: Use when an app stores or scopes data by client or company — record keys, scope filters, company selectors, CRM-tab scoping. Clients belong to MANY companies (client.companyIds[]); a client viewer always carries BOTH a clientId and an active companyId, so scoped tables need both columns and must filter on the intersection; client.companyId (on the client object) is deprecated.
3
+ description: Use when an app stores or scopes data by client or company — record keys, scope filters, company selectors, CRM-tab scoping. Clients belong to MANY companies (companyIds[]); a client viewer carries both clientId and active companyId. client.companyId is deprecated.
4
4
  ---
5
5
 
6
6
  Clients and companies are **many-to-many**: a client belongs to many companies via `client.companyIds: string[]`. In the portal, a client can switch between their companies — the viewer's `companyId` always reflects whichever company they currently have active. Apps that ignore this model show incomplete company pickers, or key and filter records on one dimension and then leak a company-A record into the same client's company-B view. This skill covers how to scope, key, and filter records correctly.
@@ -69,6 +69,10 @@ await assembly.updateClient({
69
69
 
70
70
  Send the structured object — a bare string is accepted for backwards compatibility with older data, but it collapses to a single `fullAddress` string and loses city/region/postal code/country as separate values. Every property is optional except that the field is only considered set if `country` or `fullAddress` is non-empty; `fullAddress` is derived automatically from the other components if you omit it.
71
71
 
72
+ ## Every other field type
73
+
74
+ Non-option, non-address fields take a direct string value — stringify numbers and booleans yourself: `{ mrr: '5000' }`, not `{ mrr: 5000 }`.
75
+
72
76
  ## Symptoms when it goes wrong
73
77
 
74
78
  | Mistake | What you'll see |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-integrations
3
- description: Use BEFORE declaring requiredIntegrations in .assembly/app.json and before writing any call through getIntegration() from @assembly-js/app-foundation/server (Slack and other OAuth providers — supported list inside). Covers manifest declaration, the broker client, per-provider gotchas (pagination, IDs vs names, membership/join), and ConnectorError handling.
3
+ description: Use BEFORE declaring requiredIntegrations in .assembly/app.json or calling getIntegration() from @assembly-js/app-foundation/server (Slack + other providers). Covers manifest declaration, the broker client, per-provider gotchas (pagination, IDs vs names, membership), ConnectorError handling.
4
4
  ---
5
5
 
6
6
  ## How it works
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-internal-users
3
- description: Use when the app needs to read or list internal users, or when an internal viewer browses clients/companies in the Dashboard — staff (admins, agents, accountants, lawyers) running the Assembly workspace. Covers Client Access permissions (isClientAccessLimited, companyAccessList) and scoping internal views to the viewer's allowed companies. Internal users sign in to the Dashboard, never to the Client Portal. Detect via tokenPayload.internalUserId.
3
+ description: Use when the app reads/lists internal users, or an internal viewer browses clients/companies in the Dashboard. Covers Client Access (isClientAccessLimited, companyAccessList) scoping. Internal users sign into the Dashboard only. Detect via tokenPayload.internalUserId.
4
4
  ---
5
5
 
6
6
  Internal users are the **business team** running the Assembly workspace. They sign into the Dashboard at the workspace's domain, never the branded Client Portal. They have full read/write power over clients, companies, files, etc. — **within their Client Access scope** (see below).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-native-resources
3
- description: Use any time the app references a native Assembly message channel, form, or portal file/file channel. One non-negotiable rule — never hardcode a native resource ID; discover it at runtime. These IDs are workspace-scoped, so a hardcoded one is only valid in the workspace it came from and breaks the moment the app runs anywhere else. (Not react-hook-form validation — that's assembly-forms. Not app-private uploads — that's assembly-storage. This IS the skill for the native Messages, Forms and Files modules.)
3
+ description: Use any time the app references a native Assembly message channel, form, or portal file/file-channel. Never hardcode the ID — discover it at runtime; IDs are workspace-scoped. Not assembly-forms (validation) or assembly-storage (app-private uploads).
4
4
  ---
5
5
 
6
6
  ## The non-negotiable
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: assembly-notifications
3
3
  # <!-- !feature:notification-settings -->
4
- description: Use when the app needs to notify internal users or clients about something that happened. Covers in-product notifications, email, the Notification Center surface, and how to create notifications. Apps call `assembly.createNotification()` — no external services, no custom polling. To clear the client portal badge after sending a client notification, see the `clearing-client-notifications` skill.
4
+ description: Use when the app must notify internal users or clients about something that happened — in-product, email, or both. Covers createNotification, the Notification Center /n surface, and clearing the client badge (see clearing-client-notifications).
5
5
  # <!-- /feature:notification-settings -->
6
6
  # <!-- feature:notification-settings -->
7
- description: Use when the app needs to notify internal users or clients about something that happened. Covers in-product notifications, email, the Notification Center surface, and how to create notifications. Apps call `assembly.createNotification()` — no external services, no custom polling. Also covers per-user notification preferences for internal users — declaring toggleable notification categories under `notificationSettings` in `.assembly/app.json` and passing `notificationSettingId` on createNotification so each internal user's Product/Email toggles are honored. To clear the client portal badge after sending a client notification, see the `clearing-client-notifications` skill.
7
+ description: Use when the app must notify internal users or clients, or needs per-user notification preferences (Product/Email toggles). Covers createNotification, the /n surface, notificationSettings + notificationSettingId, and clearing the client badge (see clearing-client-notifications).
8
8
  # <!-- /feature:notification-settings -->
9
9
  ---
10
10
 
@@ -1,7 +1,7 @@
1
1
  <!-- feature:platform-mcp -->
2
2
  ---
3
3
  name: assembly-platform-mcp
4
- description: Use when the app you are building depends on workspace configuration that does not exist yet — most commonly a custom field on clients or companies (the user asked for "call clients at their phone number" and no Phone-type client field exists). You have a scoped platform MCP server (tool prefix mcp__platform__) for provisioning that configuration at build time instead of shipping an app that gates on missing workspace setup. Workspace configuration only — never client-facing data (no creating clients, sending messages, notifications, or invoices).
4
+ description: Use when the app depends on missing workspace config — e.g. no Phone-type client custom field exists yet. Provision it at build time via the scoped platform MCP server (mcp__platform__). Workspace config only — never client-facing data (no clients, messages, notifications, invoices).
5
5
  ---
6
6
 
7
7
  ## Provisioning custom fields at build time
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-route-errors
3
- description: Use in every app/api/**/route.ts handler. Return user-friendly { error } JSON with the right status; never return err.message from catch blocks — Postgres and driver strings leak to the UI. Import handleRouteError, badRequest, notFound, and internalError from @assembly-js/app-foundation/server. Validate values before they hit typed DB columns (e.g. parseUuid before a uuid-column lookup).
3
+ description: REQUIRED in every app/api/**/route.ts handler. Return { error } JSON with the right status; never return err.message — Postgres/driver strings leak. Use handleRouteError/badRequest/notFound/internalError from @assembly-js/app-foundation/server. Validate before hitting DB columns (e.g. parseUuid).
4
4
  ---
5
5
 
6
6
  Route handlers must return safe, user-facing error messages. A `catch` that does `NextResponse.json({ error: err.message }, { status: 500 })` leaks raw Postgres errors (`invalid input syntax for type uuid: "…"`) and other driver internals to the client.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-session-token
3
- description: Use any time an app reads identity, fetches data, or wonders about auth/token handling. Covers the bearer lifecycle (session token exchanged once by the middleware → bearer cookie → getAssembly() → silent re-exchange on expiry), getSessionIdentity/useAssembly for identity, useServerAction/callAction for SDK-method data fetching, and authFetch for route handlers. Cross-references assembly-route-auth for requireAuth/requireCompanyScope.
3
+ description: Use any time the app reads identity, fetches data, or touches auth/token handling. Covers the bearer lifecycle (session token, bearer cookie, getAssembly(), silent re-exchange), getSessionIdentity/useAssembly, useServerAction, authFetch. See assembly-route-auth for requireAuth/requireCompanyScope.
4
4
  ---
5
5
 
6
6
  Auth is **cookie-borne and automatic** — you never handle the session token and you never pass a token to anything. On the first load only, the parent opens your app at `…/?token=<session-token>`. The foundation middleware (`proxy.ts`) exchanges that session token exactly once (server-side, via `POST /v1/session`) for a short-lived **user-scoped bearer** and stores it in an HttpOnly, `SameSite=None` cookie. That mint is the only time the session token is ever used. Every server call your app makes reads the bearer from that cookie — `getAssembly()` returns an authenticated client with no token argument. The bearer lasts 60 minutes; when it expires, `authFetch` and `useServerAction` silently re-exchange (mint a fresh bearer from the parent's current session token via `/api/assembly-reexchange`) and retry — no reload, no code from you.
@@ -130,17 +130,18 @@ Everything above is **request-scoped**: a user is in the loop and `getAssembly()
130
130
 
131
131
  ```ts
132
132
  // app/api/cron/sync/route.ts — no viewer, no cookie
133
- import { getOfflineAssembly } from '@assembly-js/app-foundation/server';
133
+ import { getOfflineAssembly, listAll } from '@assembly-js/app-foundation/server';
134
134
 
135
135
  export async function GET() {
136
136
  const assembly = await getOfflineAssembly();
137
- const { data: clients = [] } = await assembly.listClients();
137
+ // Background work must cover everyone — walk every page, not just the first.
138
+ const clients = await listAll((nextToken) => assembly.listClients({ nextToken, limit: 100 }));
138
139
  // …background work…
139
140
  return Response.json({ ok: true });
140
141
  }
141
142
  ```
142
143
 
143
- It **throws** (not returns null) if `ASSEMBLY_API_KEY` is missing or the mint fails — there's no client to re-exchange in the background, so let it propagate. `getSessionIdentity()` returns `null` on this path (the bearer is the app, not a person). Only reach for this outside a request; ignore it for normal UI work.
144
+ It **throws** (not returns null) if `ASSEMBLY_API_KEY` is missing or the mint fails — there's no client to re-exchange in the background, so let it propagate. `getSessionIdentity()` returns `null` on this path (the bearer is the app, not a person). Only reach for this outside a request — using it inside a user request bypasses that viewer's own scope; ignore it for normal UI work.
144
145
 
145
146
  ## What NOT to do
146
147
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-storage
3
- description: Stores and serves app-private files (attachments, exports, uploads only the app reads) via pre-signed S3 URLs. The client PUTs bytes directly to S3 — never proxy them through an API route. Use when an app uploads or downloads its own files; NOT for files the user wants in the portal Files tab (use the Files module / createFile for those).
3
+ description: Use when an app uploads/downloads its own private files (attachments, exports) via pre-signed S3 URLs — the client PUTs bytes directly to S3, never proxied through an API route. NOT for files the user wants in the portal Files tab (use the Files module / createFile).
4
4
  ---
5
5
 
6
6
  ## How it works
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clearing-client-notifications
3
- description: REQUIRED whenever the app calls createNotification with recipientClientId. Use when building any client-facing view that shows content a client was notified about, or any client action that completes something a notification prompted — so the portal sidebar badge clears. Covers the two clearing patterns (clear-on-view vs clear-on-action) with the foundation's clearClientNotifications helper.
3
+ description: REQUIRED whenever createNotification is called with recipientClientId. Use when building a client-facing view showing notified content, or a client action completing what a notification prompted — so the sidebar badge clears. Covers clear-on-view vs clear-on-action via clearClientNotifications.
4
4
  ---
5
5
 
6
6
  ## Why this exists