@assembly-js/app-foundation 0.0.33 → 0.0.35
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 +10 -6
- package/package.json +1 -1
- package/skills/assembly-platform-links/SKILL.md +169 -0
package/CLAUDE.md
CHANGED
|
@@ -43,6 +43,9 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
|
|
|
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
44
|
- `assembly-app-bridge-header` — breadcrumbs (don't include app name), CTAs
|
|
45
45
|
- `assembly-app-bridge-toast` — show toast notifications in the parent dashboard
|
|
46
|
+
<!-- feature:deep-link-bridge-nav -->
|
|
47
|
+
- `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.
|
|
48
|
+
<!-- /feature:deep-link-bridge-nav -->
|
|
46
49
|
- `assembly-ai` — AI features, including speech-to-text, via Assembly's OpenAI proxy (no API keys needed)
|
|
47
50
|
- `assembly-storage` — app-private file uploads/downloads (NOT the portal Files module). The storage helpers come from `@assembly-js/app-foundation/server` — import them, don't rewrite them. POST only `{ fileName }` (a bare name) and store the key the platform returns; never build the S3 key yourself.
|
|
48
51
|
- `assembly-integrations` (present only when third-party integrations are enabled for this workspace) — third-party providers (Slack, etc.) via `requiredIntegrations` + `getIntegration()`. If this skill is present, read it and `.claude/skills/assembly-integrations/providers/<slug>.md` before declaring an integration or writing any provider call — the skill lists what's connectable, the provider doc has that provider's gotchas — and read them first when provider data displays wrongly (wrong format, raw IDs). If it is absent, managed OAuth integrations are not available here; use `requiredSecrets` for any external service
|
|
@@ -157,7 +160,9 @@ Before adding a feature, check whether Assembly already covers it. If it does, *
|
|
|
157
160
|
**Then ask who should perform it — you or the platform.** Reaching a module through the SDK is right when the user is doing the thing *now*: they pressed a button and are watching it happen. When the effect should instead happen *in reaction to* something that happens in the app ("when a project is completed, message the client"), that is a workflow that integrates with the Assembly platform, and **the platform performs it, not your code.** The test is where the effect lands: on Assembly's own data rather than only in your app's tables. Decide this **before** you write or plan the call — read the `assembly-automations` skill to decide it, and don't reach for the module's SDK method until you have.
|
|
158
161
|
<!-- /feature:api-resources -->
|
|
159
162
|
|
|
160
|
-
**
|
|
163
|
+
**Always available (every workspace):** Clients, companies, and custom fields — see "The data model" below. Use them; do not rebuild them. Notification infrastructure (`listNotifications`, `createNotification`, `deleteNotification`, `markNotificationRead`, `markNotificationUnread`) is also always available.
|
|
164
|
+
|
|
165
|
+
**Optional modules (may or may not be installed in this workspace):** Messages, Files, Contracts, Forms, Billing, Knowledge Base, Tasks, and Notes. Before suggesting one or calling its SDK, confirm it is installed — call `listAppInstalls`. If it is installed, use it through the SDK; do not rebuild it. If it is not installed, do not suggest it, do not plan the app around it, and do not ask the user to install it.
|
|
161
166
|
|
|
162
167
|
| Module | What it does | Reach via SDK |
|
|
163
168
|
|---|---|---|
|
|
@@ -169,10 +174,9 @@ Before adding a feature, check whether Assembly already covers it. If it does, *
|
|
|
169
174
|
| Billing | Invoices, subscriptions, payments | `createInvoice`, `listInvoices`, `retrieveInvoice`, `listInvoiceTemplates`, `createSubscription`, `listSubscriptions`, `retrieveSubscription`, `cancelSubscription`, `listSubscriptionTemplates`, `listProducts`, `retrieveProduct`, `listPrices`, `retrievePrice`, `listPayments` |
|
|
170
175
|
| Knowledge Base | Help articles / docs | (no SDK methods today; render-only) |
|
|
171
176
|
| Tasks | Assign tasks to clients/team | `retrieveTasks`, `createTask`, `retrieveTask`, `updateTask`, `deleteTask`, `listTaskTemplates`, `retrieveTaskTemplate` |
|
|
172
|
-
| Notifications | In-app notification feed | `listNotifications`, `createNotification`, `deleteNotification`, `markNotificationRead`, `markNotificationUnread` |
|
|
173
177
|
| Notes | Per-client/company internal notes | `createNote`, `listNotes`, `retrieveNote`, `updateNote`, `deleteNote` |
|
|
174
178
|
|
|
175
|
-
If the user's prompt says "build a CRM," the answer is mostly **wire up the existing Clients + Companies
|
|
179
|
+
If the user's prompt says "build a CRM," the answer is mostly **wire up the existing Clients + Companies** (and any optional modules that are actually installed) **in a custom view** — not invent your own.
|
|
176
180
|
|
|
177
181
|
---
|
|
178
182
|
|
|
@@ -329,7 +333,7 @@ Pair it with a server-component or page-level guard so the user sees a friendly
|
|
|
329
333
|
|
|
330
334
|
A Postgres database is provisioned for this app. `DATABASE_URL` is already in your shell environment.
|
|
331
335
|
|
|
332
|
-
Use it for any persistent data that Assembly's
|
|
336
|
+
Use it for any persistent data that Assembly's modules don't cover (custom records, time entries, app-specific state, etc.). **Check the modules section above first** — prefer Assembly SDK data over custom tables when an installed module already covers the need.
|
|
333
337
|
|
|
334
338
|
**`drizzle-orm`, `@neondatabase/serverless`, and `drizzle-kit` are pre-installed. `lib/db.ts`, `lib/schema.ts`, and `drizzle.config.ts` are already in the repo.** Do NOT run `bun add` for these packages or recreate those files.
|
|
335
339
|
|
|
@@ -1152,7 +1156,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
|
|
|
1152
1156
|
- ❌ Don't decode the session token client-side. It's encrypted; only the SDK on the server can read it.
|
|
1153
1157
|
- ❌ Don't use `URLSearchParams` on `window.location` to read the token in App Router server components — use the typed `searchParams` prop.
|
|
1154
1158
|
- ❌ Don't make Assembly API calls from client components without a server-side proxy. The apiKey can't leave the server.
|
|
1155
|
-
- ❌ Don't reinvent
|
|
1159
|
+
- ❌ Don't reinvent Clients, Companies, or custom fields — they are always available via the SDK. For Messages, Files, Forms, Tasks, Billing, Contracts, Notes, or Knowledge Base: confirm the module is installed (`listAppInstalls`) first; if it is, call the SDK methods listed above rather than rebuilding it. If it is not installed, do not suggest it or plan around it.
|
|
1156
1160
|
- ❌ Don't use `assembly.clients.list()` — the SDK is flat. Use `assembly.listClients(...)`.
|
|
1157
1161
|
- ❌ Don't cache decoded payloads for >30s — token rotates every 5 min and the user could be different on the next call (rare, but possible if a session is shared across tabs).
|
|
1158
1162
|
<!-- !feature:app-owned-header -->
|
|
@@ -1212,5 +1216,5 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
|
|
|
1212
1216
|
<!-- !feature:app-owned-header -->
|
|
1213
1217
|
6. **Use `AssemblyBridge.header.*`** for breadcrumbs/CTAs in client components. Don't render your own. (Exception: the notification surface `/n` has no header — render CTAs as inline buttons there.)
|
|
1214
1218
|
<!-- /feature:app-owned-header -->
|
|
1215
|
-
7. **Don't reinvent Messages/Files/Forms/Tasks/Billing
|
|
1219
|
+
7. **Don't reinvent Clients/Companies/custom fields.** For Messages/Files/Forms/Tasks/Billing and similar optional modules, confirm they are installed (`listAppInstalls`) before wiring them via SDK — and skip them if they aren't.
|
|
1216
1220
|
8. **Keep infrastructure plumbing out of chat.** Don't volunteer ASSEMBLY_ENV, COPILOT_ENV, token encryption, API key internals, or internal file names unprompted. Mid-build SDK auth warnings are expected — never surface them to the user. (Answering a user's direct technical question is fine.)
|
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<!-- feature:deep-link-bridge-nav -->
|
|
2
|
+
---
|
|
3
|
+
name: assembly-platform-links
|
|
4
|
+
description: Use whenever the app shows a platform record (client, company, invoice, contract, form, file) that the user should be able to navigate to, or when linking between workspace apps. Covers linkTo() for click handlers and AssemblyBridge.navigateTo() when you need the result. Never hand-build a host URL.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Before you start: confirm this app's bridge supports `navigateTo`
|
|
8
|
+
|
|
9
|
+
`navigateTo` only exists on app-bridge 2.x. An app's bridge version is frozen at birth — some apps are still running an older peer with no runtime implementation of it at all. On those, a `linkTo()` button still renders but clicking it does nothing: the optional-chain in its onClick silently no-ops.
|
|
10
|
+
|
|
11
|
+
Before writing any `linkTo()` or `AssemblyBridge.navigateTo()` call, check what this app actually has installed:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
grep -q navigateTo node_modules/@assembly-js/app-bridge/dist/index.d.ts && echo supported || echo unsupported
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If it reports `unsupported`, do not use this skill — there is no supported way for this app to trigger platform navigation yet. Skip the link rather than ship a button that silently does nothing.
|
|
18
|
+
|
|
19
|
+
## When to reach for this skill
|
|
20
|
+
|
|
21
|
+
Any time the app renders a **name or reference to a platform entity** — a client name, a company badge, an invoice number, a contract, a form, a file — offer a link that opens the platform's canonical detail page, on surfaces where that destination is reachable (see the "Client portal" column in the destination catalog below). Internal/staff users can navigate to any destination. Clients in the portal can only reach the destinations marked available there — their own invoices, contracts, help articles, files, channels, and other installed apps — not `client`, `company`, `form`, or the other staff-only destinations, which the host rejects for a client viewer.
|
|
22
|
+
|
|
23
|
+
Do not hand-build a URL. The host URL is opaque to the app and the path varies across deployments and surfaces. Use `linkTo()` or `AssemblyBridge.navigateTo()` instead.
|
|
24
|
+
|
|
25
|
+
## The helper: `linkTo(destination)`
|
|
26
|
+
|
|
27
|
+
`linkTo` (from `@assembly-js/app-foundation/lib-client`) returns a `() => void` onClick handler. Wire it to a `<button>` or anchor `onClick` prop. It calls `AssemblyBridge.navigateTo`, swallows the promise, and logs any failure to the console — you get a clean side-effect-free click handler without managing the promise yourself.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
'use client';
|
|
31
|
+
import { linkTo } from '@assembly-js/app-foundation/lib-client';
|
|
32
|
+
|
|
33
|
+
// ✅ RIGHT — navigates the host to the client's detail page
|
|
34
|
+
<button onClick={linkTo({ to: 'client', clientId: client.id })}>
|
|
35
|
+
{client.name}
|
|
36
|
+
</button>
|
|
37
|
+
|
|
38
|
+
// ✅ RIGHT — open in a new tab (internal user surface)
|
|
39
|
+
<button onClick={linkTo({ to: 'invoice', invoiceId: invoice.id }, { newTab: true })}>
|
|
40
|
+
View invoice
|
|
41
|
+
</button>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The direct call: `AssemblyBridge.navigateTo()`
|
|
45
|
+
|
|
46
|
+
When you need to know whether navigation succeeded — for example, to show a toast if the host can't open the record, or to disable a button after a failed attempt — call `navigateTo` directly and inspect the result.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
'use client';
|
|
50
|
+
import { AssemblyBridge } from '@assembly-js/app-bridge';
|
|
51
|
+
|
|
52
|
+
const result = await AssemblyBridge.navigateTo?.({ to: 'client', clientId });
|
|
53
|
+
if (result?.ok === false) {
|
|
54
|
+
// Host declined (viewer lacks permission or destination is gated)
|
|
55
|
+
setLinkDisabled(true);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Always call it as `AssemblyBridge.navigateTo?.()` with the optional-chain. The method is absent on app-bridge peers below 2.0.0; the `?.` silently no-ops rather than throwing.
|
|
60
|
+
|
|
61
|
+
## Destination catalog
|
|
62
|
+
|
|
63
|
+
The host rejects staff-only destinations when the viewer is a client in the portal — navigation resolves to `{ ok: false, reason: 'not_permitted' }` (and a `linkTo()` button just does nothing, since it swallows the result). Only render a link to a destination from the client portal surface when "Client portal" below is Yes.
|
|
64
|
+
|
|
65
|
+
| Destination | Required fields | Optional | Client portal |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `client` | `clientId` | `tab` | No — staff only |
|
|
68
|
+
| `company` | `companyId` | `tab` | No — staff only |
|
|
69
|
+
| `invoice` | `invoiceId` | — | Yes |
|
|
70
|
+
| `subscription` | `subscriptionId` | — | No — staff only |
|
|
71
|
+
| `contract` | `contractId` | — | Yes |
|
|
72
|
+
| `form` | `formId` | — | No — staff only |
|
|
73
|
+
| `formResponse` | `formId`, `responseId` | — | No — staff only |
|
|
74
|
+
| `article` | `articleId` | — | Yes |
|
|
75
|
+
| `files` | — | `channelId` | Yes |
|
|
76
|
+
| `channel` | — | `channelId` | Yes |
|
|
77
|
+
| `app` | `installId` | — | Yes, if the target app is installed for that viewer |
|
|
78
|
+
| `home` | — | — | No — staff only |
|
|
79
|
+
| `product` | — | — | No — staff only |
|
|
80
|
+
| `helpdesk` | — | — | Yes |
|
|
81
|
+
| `notifications` | — | — | No — staff only |
|
|
82
|
+
| `automations` | — | — | No — staff only |
|
|
83
|
+
| `settings` | — | `page` | No — staff only |
|
|
84
|
+
|
|
85
|
+
## Linking to another workspace app
|
|
86
|
+
|
|
87
|
+
When the workspace has multiple apps and the user should navigate between them, use the target app's **install id** — neither the token payload nor `useAssembly()` expose it (those only carry `instanceId`, for the app currently open). Look the companion app up server-side with `listAppInstalls()` and match it by `displayName`, the same name-matching pattern used for custom fields — never hardcode an install id, installs are minted per workspace:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// Server-side (route handler or server action)
|
|
91
|
+
import { getAssembly } from '@assembly-js/app-foundation/server';
|
|
92
|
+
|
|
93
|
+
const assembly = await getAssembly();
|
|
94
|
+
const installs = await assembly.listAppInstalls({});
|
|
95
|
+
const invoicing = installs.find((i) => i.displayName?.toLowerCase().includes('invoicing'));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
// ✅ RIGHT — links to a companion app by install id looked up server-side
|
|
100
|
+
<button onClick={linkTo({ to: 'app', installId: invoicing.id })}>
|
|
101
|
+
Open Invoicing
|
|
102
|
+
</button>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Pass the install id, not a deployment URL or a hand-built path.
|
|
106
|
+
|
|
107
|
+
## Internal surface example
|
|
108
|
+
|
|
109
|
+
An internal user sees a table of clients. Each row has a link that opens the client's Assembly CRM page:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
'use client';
|
|
113
|
+
import { linkTo } from '@assembly-js/app-foundation/lib-client';
|
|
114
|
+
import { ExternalLink } from 'lucide-react';
|
|
115
|
+
|
|
116
|
+
export function ClientRow({ client }: { client: Client }) {
|
|
117
|
+
return (
|
|
118
|
+
<tr>
|
|
119
|
+
<td>
|
|
120
|
+
<button
|
|
121
|
+
onClick={linkTo({ to: 'client', clientId: client.id })}
|
|
122
|
+
className="flex items-center gap-1 text-sm text-blue-600 hover:underline"
|
|
123
|
+
>
|
|
124
|
+
{client.name}
|
|
125
|
+
<ExternalLink className="h-3 w-3" />
|
|
126
|
+
</button>
|
|
127
|
+
</td>
|
|
128
|
+
</tr>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Client portal surface example
|
|
134
|
+
|
|
135
|
+
A client sees their own invoices. Each row links to the invoice record in the portal:
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
'use client';
|
|
139
|
+
import { linkTo } from '@assembly-js/app-foundation/lib-client';
|
|
140
|
+
|
|
141
|
+
export function InvoiceRow({ invoice }: { invoice: Invoice }) {
|
|
142
|
+
return (
|
|
143
|
+
<tr>
|
|
144
|
+
<td>
|
|
145
|
+
<button
|
|
146
|
+
onClick={linkTo({ to: 'invoice', invoiceId: invoice.id })}
|
|
147
|
+
className="text-sm text-blue-600 hover:underline"
|
|
148
|
+
>
|
|
149
|
+
{invoice.number}
|
|
150
|
+
</button>
|
|
151
|
+
</td>
|
|
152
|
+
</tr>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## What NOT to do
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
// ❌ WRONG — hand-building a host URL (opaque, breaks across deployments)
|
|
161
|
+
window.open(`https://app.assembly.com/clients/${clientId}`, '_blank');
|
|
162
|
+
|
|
163
|
+
// ❌ WRONG — plain <a> with a guessed platform path
|
|
164
|
+
<a href={`/clients/${clientId}`}>View client</a>
|
|
165
|
+
|
|
166
|
+
// ❌ WRONG — calling navigateTo without optional-chain (throws on older peers)
|
|
167
|
+
await AssemblyBridge.navigateTo({ to: 'client', clientId });
|
|
168
|
+
```
|
|
169
|
+
<!-- /feature:deep-link-bridge-nav -->
|