@assembly-js/app-foundation 0.0.48 → 0.0.50

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/CHANGELOG.md CHANGED
@@ -14,6 +14,12 @@ A foundation update reads the sections between an app's version and the target:
14
14
  from every bullet except `[guidance]`, agent work from the check scripts those `[action]`
15
15
  bullets ship. Write `[fix]`, `[behavior]` and `[action]` bullets for the app's owner.
16
16
 
17
+ ## 0.0.50
18
+ - [guidance] The build agent is no longer told to reinstall packages that come pre-installed, offered a toast library the platform bans, or shown a secret type the manifest rejects, and its header guidance no longer contradicts itself when the app owns its header. It is also told why the dashboard and client surfaces take separate routes and to detect the viewer with `getSessionIdentity()`.
19
+
20
+ ## 0.0.49
21
+ - [fix] Scheduled-work wiring, proxy, and session-token problems are now caught when the platform updates the app, not only when the app's own change touches them.
22
+
17
23
  ## 0.0.48
18
24
  - [guidance] Builds now warn when an app lacks the retry screen for platform outages, calls the child-row batch detached from the database, or imports a PDF library on the server that breaks after publishing, with the exact steps to fix each; nothing in an existing app changes until its code is updated.
19
25
 
package/CLAUDE.md CHANGED
@@ -41,7 +41,9 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
41
41
  - `assembly-automations` — REQUIRED when the request asks for a platform-side effect in reaction to an in-app event (a message, a CRM field, a contract, a form, a file — not the app's own tables), even if the user never says "automation": declare the app's API contract in `.assembly/api-resources.json` (a separate file from `app.json`) — domain events that become automation triggers + workspace webhooks, and staff-invokable operations that become automation actions. Declared events must also be emitted at runtime — inside `after()`, reporting the acting user on the documented headers — declaring alone never fires anything. Read it before adding either.
42
42
  <!-- /feature:api-resources -->
43
43
  - `assembly-app-surfaces` — REQUIRED before writing `.assembly/app.json`: declare which surfaces (dashboard, client portal, notification) the app exposes; omitting the client surface keeps the app off client portal nav entirely
44
+ <!-- !feature:app-owned-header -->
44
45
  - `assembly-app-bridge-header` — breadcrumbs (don't include app name), CTAs
46
+ <!-- /feature:app-owned-header -->
45
47
  - `assembly-app-bridge-toast` — show toast notifications in the parent dashboard
46
48
  <!-- feature:deep-link-bridge-nav -->
47
49
  - `assembly-platform-links` — link platform records (client, company, invoice, contract, form, file) to their portal detail page via `linkTo()`; never hand-build a host URL. Also links between workspace apps by install id. Check the app's installed app-bridge first (see the skill) — `navigateTo` doesn't exist on older peers — and only link staff-only destinations (client, company, form, etc.) from the dashboard surface, never the client portal.
@@ -457,9 +459,9 @@ Because the app runs in an iframe inside the parent dashboard, several normal Ne
457
459
  - **Cookies must be `sameSite: 'none'` and `secure: true`.** Anything else is dropped silently in cross-origin iframes.
458
460
  - **No top-level navigation.** Don't use `window.location = '...'` to go to external URLs — it'll either break out of the iframe or be blocked. For external links use `<a target="_blank" rel="noreferrer">`.
459
461
  - **No `window.open(..., '_self')`.** Same issue.
462
+ <!-- !feature:app-owned-header -->
460
463
  - **The portal owns the chrome (header, sidebar).** Don't render your own breadcrumbs or page titles via DOM. Use `@assembly-js/app-bridge` to set them.
461
464
  - **Never put the app name in breadcrumbs.** The parent portal *automatically* prepends the app's name as breadcrumb 0. If you also pass `{ label: 'MyApp' }` as the first entry of `setBreadcrumbs`, it renders twice ("MyApp / MyApp / Reports"). For a flat one-page app pass `[]` (or skip `setBreadcrumbs` entirely). For a multi-level app pass only the sub-pages: `setBreadcrumbs([{ label: 'Reports' }, { label: 'Q4 2026' }])`. Read `.claude/skills/assembly-app-bridge-header/SKILL.md` for full examples.
462
- <!-- !feature:app-owned-header -->
463
465
  - **Action buttons go in the header — set via the `useAssemblyHeader` hook.** Submit, Save, Edit, Create — any primary page-level action MUST live in the header (`primaryCta`), not a `<Button>` in the page body. Secondary actions use `secondaryCta`; overflow/tertiary actions use `actionsMenu`. Drive all of it through the `useAssemblyHeader` hook (`app/components/use-assembly-header.ts`) — **never call `AssemblyBridge.header.set*` from your own `useEffect`** (that hand-rolling is what makes header buttons vanish after a re-render). See `.claude/skills/assembly-app-bridge-header/SKILL.md` for the full API.
464
466
  **Exception: the notification surface (`/n`).** The Notification Center embeds the `/n` route in a plain `<iframe>` without app bridge header support. `useAssemblyHeader` calls are silently ignored — header CTAs never appear. On the notification surface, render primary actions as inline `<Button>` elements in the page body. Detect the notification surface by the route — code under `app/n/` IS the surface; for a shared component pass an explicit prop from the `/n` page rather than inferring from `payload.notificationId`, which is best-effort under the session-exchange flow. This exception applies ONLY to the `/n` route; all other surfaces MUST use the header. **Do not reuse a component that calls `useAssemblyHeader` on the `/n` surface** — if it also has an inline action button, both the header CTA and the inline button will render on non-notification surfaces (duplicate CTAs). Either create a separate component for `/n` or accept a prop that switches between header and inline CTAs.
465
467
  <!-- /feature:app-owned-header -->
@@ -528,7 +530,7 @@ Two paths, decided by whether an `assembly-integrations` skill is present and, i
528
530
  "key": "SENDGRID_API_KEY",
529
531
  "label": "SendGrid API key",
530
532
  "description": "Used to send ticket-submitted emails. Create one under SendGrid Settings > API Keys.",
531
- "type": "password",
533
+ "type": "api_key",
532
534
  "docsUrl": "https://docs.sendgrid.com/ui/account-and-settings/api-keys"
533
535
  }
534
536
  ]
@@ -642,9 +644,11 @@ If you forget this, `bun`/`bunx` commands will fail with "command not found". Th
642
644
  ### Install (use bun)
643
645
 
644
646
  ```bash
645
- export PATH="$HOME/.bun/bin:$PATH" && bun add @assembly-js/node-sdk @assembly-js/app-bridge
647
+ export PATH="$HOME/.bun/bin:$PATH" && bun add <extra-package>
646
648
  ```
647
649
 
650
+ `@assembly-js/node-sdk` and `@assembly-js/app-bridge` are already installed, so don't `bun add` them. Use `bun add` only for an extra package the task needs.
651
+
648
652
  ### Inner verification loop — type-check, don't full-build
649
653
 
650
654
  Use `bunx tsc --noEmit` to verify your changes while iterating. It runs in 2–3s and gives you the full, untruncated type errors. Reserve `bun run build` (~15–20s for a full Next build) for the **final** check at the end of the task, not after every edit. Running `bun run build` between trivial edits adds ~15s of dead time per cycle.
@@ -807,7 +811,7 @@ This app uses the **Assembly UI** component registry (`@assembly-ui` namespace i
807
811
 
808
812
  **shadcn drop-in replacements** — same composition patterns, same sub-component names, same imports. Built on `@base-ui/react` — root props differ from Radix/shadcn (see documented differences above; check `components/ui/<name>.tsx` if you hit a type error):
809
813
 
810
- accordion, alert, alert-dialog, avatar, badge, breadcrumb, button, button-group, calendar, card, checkbox, collapsible, combobox, dialog, dropdown-menu, field, input, input-group, item, label, popover, progress, radio-group, select, separator, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip
814
+ accordion, alert, alert-dialog, avatar, badge, breadcrumb, button, button-group, calendar, card, checkbox, collapsible, combobox, dialog, dropdown-menu, field, input, input-group, item, label, popover, progress, radio-group, select, separator, skeleton, slider, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip
811
815
 
812
816
  **Custom Assembly components** — these are NOT shadcn drop-ins. They have their own unique APIs and do not follow standard shadcn composition patterns. Read the usage instructions below before using them:
813
817
 
@@ -10,7 +10,8 @@
10
10
  // Node are guaranteed.
11
11
  //
12
12
  // The build fails only for cron wiring the current turn is changing (see
13
- // cronWiringChangedThisTurn). Everything else — a template clone, a Vercel
13
+ // cronWiringChangedThisTurn), or for any finding with --strict (a platform
14
+ // update turn). Everything else — a template clone, a Vercel
14
15
  // build, an update turn that never touched the cron — gets the same findings
15
16
  // as warnings: those paths have no agent who can act on a failure, so failing
16
17
  // them would only strand an app that already worked. A below-floor schedule
@@ -38,6 +39,10 @@ const ROUTE_FILE = /^route\.(tsx|ts|jsx|js)$/;
38
39
  // The turn program exports the SHA the sandbox started this turn at; the files
39
40
  // below are the ones whose change since then makes the findings the agent's own.
40
41
  const TURN_BASELINE_ENV = 'ASSEMBLY_TURN_BASELINE_SHA';
42
+ // Set by the turn program on a platform update turn, where the update itself is
43
+ // the mandate to fix whatever is wrong, so findings fail the build whether or
44
+ // not the turn touched the cron wiring.
45
+ const STRICT = process.argv.includes('--strict') || process.env.ASSEMBLY_UPDATE_TURN === '1';
41
46
  const CRON_WIRING_PATHS = ['vercel.json', 'app/api/cron/'];
42
47
 
43
48
  function report(lines, { fatal }) {
@@ -312,4 +317,4 @@ if (entries.length > 0) {
312
317
  }
313
318
 
314
319
  if (preexistingIntervals.length > 0) report(preexistingIntervals, { fatal: false });
315
- if (problems.length > 0) report(problems, { fatal: cronWiringChangedThisTurn() });
320
+ if (problems.length > 0) report(problems, { fatal: STRICT || cronWiringChangedThisTurn() });
@@ -13,10 +13,11 @@
13
13
  // carries a session token, so nothing here needs the network.
14
14
  //
15
15
  // Like check-crons, a broken proxy fails the build only when the current turn
16
- // changed it (or with --strict, which the update acceptance passes); otherwise
17
- // the findings are warnings, since there is no agent to act on them. An app
18
- // still on the pre-bearer foundation only ever warns outside --strict: the
19
- // platform security update is what fixes it, not an ordinary turn.
16
+ // changed it (or with --strict, which the update acceptance passes, or on a
17
+ // platform update turn); otherwise the findings are warnings, since there is
18
+ // no agent to act on them. An app still on the pre-bearer foundation only ever
19
+ // warns outside those: the platform security update is what fixes it, not an
20
+ // ordinary turn.
20
21
  import { existsSync, readFileSync } from 'node:fs';
21
22
  import { spawnSync } from 'node:child_process';
22
23
  import { createRequire } from 'node:module';
@@ -25,7 +26,10 @@ import { join } from 'node:path';
25
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
26
27
 
27
28
  const ROOT = process.cwd();
28
- const STRICT = process.argv.includes('--strict');
29
+ // Set by the turn program on a platform update turn, where the update itself is
30
+ // the mandate to fix whatever is wrong. Routine updates only reach apps that
31
+ // took the security update, so no pre-bearer app sees this outside it.
32
+ const STRICT = process.argv.includes('--strict') || process.env.ASSEMBLY_UPDATE_TURN === '1';
29
33
  const PROXY_FILES = ['proxy.ts', 'middleware.ts'];
30
34
  const REEXCHANGE_ROUTE = 'app/api/assembly-reexchange/route.ts';
31
35
  const REEXCHANGE_PATH = '/api/assembly-reexchange';
@@ -19,11 +19,16 @@
19
19
  // logic_build.go (mintBearerProbePath / packageWorldProbePath): mint-bearer.ts
20
20
  // present -> bearer-capable, fatal; absent + assembly.ts present -> legacy, warn
21
21
  // and exit 0; neither present -> package-world app (bearer-capable via
22
- // @assembly-js/app-foundation in node_modules), fatal as today.
22
+ // @assembly-js/app-foundation in node_modules), fatal as today. With --strict
23
+ // or on a platform update turn it is fatal even in a legacy app.
23
24
  import { readFileSync, readdirSync, existsSync } from 'node:fs';
24
25
  import { join, relative, sep } from 'node:path';
25
26
 
26
27
  const ROOT = process.cwd();
28
+ // Set by the turn program on a platform update turn, where the update itself is
29
+ // the mandate to fix whatever is wrong. Routine updates only reach apps that
30
+ // took the security update, so no pre-bearer app sees this outside it.
31
+ const STRICT = process.argv.includes('--strict') || process.env.ASSEMBLY_UPDATE_TURN === '1';
27
32
  const APP_DIR = join(ROOT, 'app');
28
33
 
29
34
  // Mirrors mintBearerProbePath / packageWorldProbePath in logic_build.go.
@@ -129,7 +134,7 @@ for (const file of pageFiles(APP_DIR)) {
129
134
 
130
135
  if (violations.length > 0) {
131
136
  const isLegacy =
132
- !existsSync(join(ROOT, MINT_BEARER_PROBE_PATH)) && existsSync(join(ROOT, PACKAGE_WORLD_PROBE_PATH));
137
+ !STRICT && !existsSync(join(ROOT, MINT_BEARER_PROBE_PATH)) && existsSync(join(ROOT, PACKAGE_WORLD_PROBE_PATH));
133
138
 
134
139
  console.error('');
135
140
  console.error('A page reads the session token from searchParams:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -75,5 +75,5 @@ interface ToastShowPayload {
75
75
  ## Rules
76
76
 
77
77
  - **Client components only.** `AssemblyBridge` posts messages to the parent window — call it in `'use client'` components only.
78
- - **Don't build toast UI inside the iframe.** The parent dashboard renders the toast in its own chrome; your iframe should have no toast component.
78
+ - **Don't build toast UI inside the iframe.** The parent dashboard renders the toast in its own chrome; your iframe should have no toast component, so don't install or import `sonner` or `react-hot-toast`.
79
79
  - **Don't stack multiple toasts for one action.** Show one toast per user action.
@@ -26,7 +26,7 @@ If you are unsure, default to **internal-only**. Do not add a client surface unl
26
26
  ```
27
27
 
28
28
  - `internal` is **required** — every Vibe app must have a dashboard surface. `client` is optional; omit it for internal-only apps. `notification` is optional; declare it when opening a notification should land on a specific record (see below).
29
- - `path` is the route within your app for that surface. Must start with `/`. Use `/` for the surface's root; use distinct sub-paths if both surfaces share one Next.js app.
29
+ - `path` is the route within your app for that surface. Must start with `/`. Use `/` for the surface's root; use a distinct sub-path (`/c` for the client surface) for each other surface. Two routes, because the same path can't tell the dashboard viewer from the client viewer without branching on the token, and each surface needs its own layout and its own access rules.
30
30
  - Generate the corresponding routes in your app code. E.g. if you declare `client.path = "/c"`, you must have a `/c` page in the app.
31
31
 
32
32
  <!-- !feature:notification-settings -->
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-client-users
3
- description: Use when the app needs to read, list, create, or update client users — the customers of the business that owns the Assembly workspace. Clients sign into the branded Client Portal and see only their own data. Detect via tokenPayload.clientId.
3
+ description: Use when the app needs to read, list, create, or update client users — the customers of the business that owns the Assembly workspace. Clients sign into the branded Client Portal and see only their own data. Detect via getSessionIdentity().
4
4
  ---
5
5
 
6
6
  Client users are **customers of the business** that owns the Assembly workspace. They sign into the branded Client Portal at the workspace's domain. They see only their own data — their own messages, their own files, their own invoices.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: assembly-internal-users
3
- description: Use when the app reads/lists internal users, gates a feature to workspace admins (role is "admin" or "staff"), 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.
3
+ description: Use when the app reads/lists internal users, gates a feature to workspace admins (role is "admin" or "staff"), 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 getSessionIdentity().
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).