@assembly-js/app-foundation 0.7.1 → 0.7.3
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
|
@@ -19,6 +19,12 @@ A patch release never changes what an app's build requires; a minor release may
|
|
|
19
19
|
not by hand; see `changes/README.md`. CI fails a PR that changes a check script without a
|
|
20
20
|
`bump: minor` change file.
|
|
21
21
|
|
|
22
|
+
## 0.7.3
|
|
23
|
+
- [guidance] The build agent's auth guide is now its own section in `CLAUDE.md` instead of sitting under "Design system", and the rule for choosing between `getAssembly()` and `getInstallerAssembly()` is stated once in `assembly-route-auth` rather than repeated in the notifications, automations and internal-users skills.
|
|
24
|
+
|
|
25
|
+
## 0.7.2
|
|
26
|
+
- [guidance] The build agent now sets `allow="autoplay; fullscreen; picture-in-picture; encrypted-media"` and `allowfullscreen` on embedded video player iframes, so Vimeo and YouTube players can go fullscreen, autoplay and use picture-in-picture inside the portal.
|
|
27
|
+
|
|
22
28
|
## 0.7.1
|
|
23
29
|
- [fix] The location reporter no longer sends the host's embed-session params (`token`, `clientId`, `companyId`) with the app's location, so they never reach the host URL or a copied deep link.
|
|
24
30
|
|
package/CLAUDE.md
CHANGED
|
@@ -474,6 +474,7 @@ Because the app runs in an iframe inside the parent dashboard, several normal Ne
|
|
|
474
474
|
- **Cookies must be `sameSite: 'none'` and `secure: true`.** Anything else is dropped silently in cross-origin iframes.
|
|
475
475
|
- **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">`.
|
|
476
476
|
- **No `window.open(..., '_self')`.** Same issue.
|
|
477
|
+
- **Nested video players re-delegate their permissions.** When you embed a Vimeo, YouTube or similar player in an `<iframe>`, set `allow="autoplay; fullscreen; picture-in-picture; encrypted-media"` and `allowfullscreen` on that iframe. Permissions pass down one frame at a time: the portal grants them to your app, but the player only gets them if your iframe grants them again, so without the attribute fullscreen, picture-in-picture and DRM playback fail silently.
|
|
477
478
|
<!-- !feature:app-owned-header -->
|
|
478
479
|
- **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.
|
|
479
480
|
- **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.
|
|
@@ -909,7 +910,7 @@ export async function GET(request: NextRequest) {
|
|
|
909
910
|
}
|
|
910
911
|
```
|
|
911
912
|
|
|
912
|
-
|
|
913
|
+
## How auth works — read this first (it's automatic, and not what you'd guess)
|
|
913
914
|
|
|
914
915
|
Your app authenticates every API call with a short-lived, user-scoped **bearer token**. You never handle the raw session token and you never pass a token to anything — the foundation manages the whole lifecycle:
|
|
915
916
|
|
|
@@ -928,7 +929,7 @@ Every authenticated call your app makes is one of exactly three kinds:
|
|
|
928
929
|
2. **Installer client** — `getInstallerAssembly()`, allowed inside a user request. Use it for staff-wide reads and writes the app is entitled to on its own, including notification helpers, app storage and connectors. It acts as the installing admin, so filter anything from it before showing it to a client. A viewer cookie does not make it refuse; `getOfflineAssembly()` does.
|
|
929
930
|
3. **Offline bearer** — for background work with no user in the loop (cron, webhooks). Call `getOfflineAssembly()` (see "Offline bearer" below); it mints an app-scoped bearer from your API key server-side. Only needed outside a request; it refuses when a viewer is present.
|
|
930
931
|
|
|
931
|
-
|
|
932
|
+
### `getAssembly()` — the only auth helper you need
|
|
932
933
|
|
|
933
934
|
`getAssembly` comes from `@assembly-js/app-foundation/server`, an installed dependency — import from it, never edit or reimplement it. (Its `/client` subpath is the subset a `'use client'` module may import; `/server` covers everything server-side.) Call `getAssembly()` with **no arguments** anywhere server-side (server component, server action, route handler). It reads the bearer for the current request and returns an authenticated, per-request client:
|
|
934
935
|
|
|
@@ -941,7 +942,7 @@ const items = await assembly.listItems({});
|
|
|
941
942
|
|
|
942
943
|
`getAssembly()` is async — always `await` it. If the bearer is missing or expired it throws `TokenInvalidError`; let it propagate, and the client helpers (`useServerAction` / `authFetch`) re-exchange and retry for you. If the platform cannot be reached to verify the bearer it throws `AuthUnavailableError` (and `getSessionIdentity()` does too) — a different error on purpose: it is not an expiry, so never catch it into `null`/signed-out and never re-exchange on it; let a route return 503, and render a retry state ("Couldn't load, try again" with a retry button) wherever a server component reads identity.
|
|
943
944
|
|
|
944
|
-
|
|
945
|
+
### Identity — who is the viewer?
|
|
945
946
|
|
|
946
947
|
Server-side, `getSessionIdentity()` returns the viewer the platform verified for the bearer (no token to pass; the check is cached briefly per server instance):
|
|
947
948
|
|
|
@@ -971,7 +972,7 @@ export default function MyWidget() {
|
|
|
971
972
|
|
|
972
973
|
Branch your UI on identity **before** making data calls — it's faster and more reliable than catching a 403.
|
|
973
974
|
|
|
974
|
-
|
|
975
|
+
### Fetching data — server actions via `useServerAction`
|
|
975
976
|
|
|
976
977
|
Put SDK calls in `'use server'` actions that take **no token** and call `getAssembly()`. From the client, run them through `useServerAction` (`app/components/use-server-action.ts`) — it manages loading/error state and **silently re-exchanges + retries once** on a 60-minute bearer expiry, so you never write refresh logic:
|
|
977
978
|
|
|
@@ -1006,7 +1007,7 @@ For an imperative one-off (button handler, mutation), use `callAction` from `use
|
|
|
1006
1007
|
- **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`.
|
|
1007
1008
|
- **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.
|
|
1008
1009
|
|
|
1009
|
-
|
|
1010
|
+
### Server action or route handler?
|
|
1010
1011
|
|
|
1011
1012
|
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.
|
|
1012
1013
|
|
|
@@ -1022,7 +1023,7 @@ export type ReplyBody = z.input<typeof replyBody>;
|
|
|
1022
1023
|
// component: const body: ReplyBody = { parentId, text }; // tsc rejects what the route would 400
|
|
1023
1024
|
```
|
|
1024
1025
|
|
|
1025
|
-
|
|
1026
|
+
### Client → server via route handlers — `authFetch`
|
|
1026
1027
|
|
|
1027
1028
|
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:
|
|
1028
1029
|
|
|
@@ -1045,7 +1046,7 @@ Every state-changing request (POST/PUT/PATCH/DELETE) **must** go through `authFe
|
|
|
1045
1046
|
|
|
1046
1047
|
In current apps `proxy.ts` lets a request that carries no cookies skip the CSRF check, so an inbound third-party call (a webhook) needs no proxy change; an older `proxy.ts` without that rule still answers 403 to a header-less POST. Authenticate the caller inside the route (signature or shared secret), fail closed when its secret is unset, and never add an unauthenticated test endpoint.
|
|
1047
1048
|
|
|
1048
|
-
|
|
1049
|
+
### Route handlers — `getAssemblyForRequest`
|
|
1049
1050
|
|
|
1050
1051
|
```ts
|
|
1051
1052
|
// app/api/clients/[clientId]/flavor/route.ts
|
|
@@ -1075,7 +1076,7 @@ export async function PATCH(
|
|
|
1075
1076
|
|
|
1076
1077
|
**Auth is entirely cookie-borne: no `?token=`, no `x-assembly-token` header, no token proxy, no `TokenLink`/`useWithToken`, no client-side token at all.** The middleware put the bearer in the cookie; `getAssembly()` / `getAssemblyForRequest()` read it. That's the whole story.
|
|
1077
1078
|
|
|
1078
|
-
|
|
1079
|
+
### Offline bearer — background work with no user (cron, webhooks)
|
|
1079
1080
|
|
|
1080
1081
|
**Anything on a schedule — a reminder, a digest, a recurring send — must first read the `assembly-scheduled-work` skill.** The route below only ever runs if `vercel.json` declares a cron for it and the route verifies `CRON_SECRET`; the offline bearer is the auth half, the skill is the scheduling half.
|
|
1081
1082
|
|
|
@@ -1102,7 +1103,7 @@ Rules for the offline path:
|
|
|
1102
1103
|
- **`getSessionIdentity()` returns `null`** here — the bearer is the app, not a person. Anything that needs a user id (e.g. a notification `senderId`) must be chosen explicitly (a system/admin internal user), not read from identity.
|
|
1103
1104
|
- Still bearer-only: no `?token=`, no `x-assembly-token`, no X-API-Key on data calls. The app key is used solely to mint the bearer.
|
|
1104
1105
|
|
|
1105
|
-
|
|
1106
|
+
### Auth guards for `[id]`-nested routes — `requireAuth` and `requireCompanyScope`
|
|
1106
1107
|
|
|
1107
1108
|
Two pre-built helpers guard API routes that operate on company-scoped resources:
|
|
1108
1109
|
|
package/package.json
CHANGED
|
@@ -271,8 +271,6 @@ Every event also lands in the workspace's audit log, where a workspace admin rea
|
|
|
271
271
|
|
|
272
272
|
The platform runs the action by calling the record's `baseUrl`, and `requireAuth` cannot authenticate that call. `lib/assembly-actions.ts` re-exports the wrapper from the foundation package — write that re-export, don't paste a second copy of the handler.
|
|
273
273
|
|
|
274
|
-
Use `getAssembly()` for anything shown to or done for the person using the app, and `getInstallerAssembly()` for staff-wide reads and writes the app is entitled to on its own, including notification helpers.
|
|
275
|
-
|
|
276
274
|
```ts
|
|
277
275
|
// lib/assembly-actions.ts
|
|
278
276
|
export {
|
|
@@ -18,8 +18,6 @@ Never call `assembly.getTokenPayload()` for this — see `assembly-session-token
|
|
|
18
18
|
|
|
19
19
|
If `internalUserId` is set, the current viewer is staff. (`clientId` is set for client viewers — see `assembly-client-users` skill.)
|
|
20
20
|
|
|
21
|
-
Use `getAssembly()` for anything shown to or done for the person using the app, and `getInstallerAssembly()` for staff-wide reads and writes the app is entitled to on its own, including notification helpers.
|
|
22
|
-
|
|
23
21
|
## SDK methods
|
|
24
22
|
|
|
25
23
|
- `listInternalUsers({ nextToken, limit })` — paged list of all staff in the workspace
|
|
@@ -16,8 +16,6 @@ For a required condition alert (a retainer reaching 90%, going over its allowanc
|
|
|
16
16
|
|
|
17
17
|
Every notification is three decisions: **audience** (internal users and/or clients), **channel** (in-product, email, or both), and **context on arrival** (what the recipient sees and can do). Get those right; the rest is mechanics.
|
|
18
18
|
|
|
19
|
-
Use `getAssembly()` for anything shown to or done for the person using the app, and `getInstallerAssembly()` for staff-wide reads and writes the app is entitled to on its own, including notification helpers.
|
|
20
|
-
|
|
21
19
|
### Internal vs client experience
|
|
22
20
|
|
|
23
21
|
The audience changes the experience and the code:
|