@assembly-js/app-foundation 0.0.32 → 0.0.34
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
|
@@ -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
|
package/dist/next.js
CHANGED
|
@@ -1,25 +1,59 @@
|
|
|
1
|
+
// Dev-invariant wrapper for generated apps' Next config. Each app's
|
|
2
|
+
// next.config.ts is platform-owned boilerplate — `export default
|
|
3
|
+
// withAssemblyDev(appConfig)` — so the preview invariants live here in the
|
|
4
|
+
// versioned package instead of as config keys the build agent must know to
|
|
5
|
+
// preserve when it edits its own app.config.ts.
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
// Read non-root surface paths from the app manifest so location-prefix.ts can
|
|
9
|
+
// discover them at runtime instead of relying on the hardcoded /c and /n defaults.
|
|
10
|
+
function readSurfacePrefixes() {
|
|
11
|
+
try {
|
|
12
|
+
const manifestPath = path.join(process.cwd(), '.assembly', 'app.json');
|
|
13
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
14
|
+
const prefixes = Object.values(manifest.surfaces ?? {})
|
|
15
|
+
.map((s) => s.path)
|
|
16
|
+
.filter((p) => typeof p === 'string' && p !== '/');
|
|
17
|
+
return prefixes.length > 0 ? JSON.stringify(prefixes) : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
1
23
|
export function withAssemblyDev(config = {}) {
|
|
24
|
+
// Inject manifest-derived surface prefixes into the client bundle so
|
|
25
|
+
// location-prefix.ts honours whatever paths the app declares.
|
|
26
|
+
const surfacePrefixesJson = readSurfacePrefixes();
|
|
27
|
+
const baseConfig = surfacePrefixesJson
|
|
28
|
+
? {
|
|
29
|
+
...config,
|
|
30
|
+
env: {
|
|
31
|
+
...config.env,
|
|
32
|
+
NEXT_PUBLIC_ASSEMBLY_SURFACE_PREFIXES: surfacePrefixesJson,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
: config;
|
|
2
36
|
// Production builds (deploys) have no dev server, no HMR socket, and no
|
|
3
|
-
// sandbox host — the
|
|
37
|
+
// sandbox host — only the env injection above applies there.
|
|
4
38
|
if (process.env.NODE_ENV === 'production')
|
|
5
|
-
return
|
|
39
|
+
return baseConfig;
|
|
6
40
|
const sandboxHost = process.env.SANDBOX_HOST;
|
|
7
41
|
const allowedDevOrigins = sandboxHost
|
|
8
|
-
? [...new Set([sandboxHost, ...(
|
|
9
|
-
:
|
|
42
|
+
? [...new Set([sandboxHost, ...(baseConfig.allowedDevOrigins ?? [])])]
|
|
43
|
+
: baseConfig.allowedDevOrigins;
|
|
10
44
|
// A dev WebSocket that never connects would otherwise hang hydration forever
|
|
11
45
|
// on Next 16.3+. Debug data falls back to the flight stream, so
|
|
12
46
|
// nothing user-visible is lost. Cast because this package type-checks against
|
|
13
47
|
// its Next 15 devDependency, which predates the flag; runtimes that predate
|
|
14
48
|
// it ignore the unknown key.
|
|
15
49
|
const experimental = {
|
|
16
|
-
...
|
|
50
|
+
...baseConfig.experimental,
|
|
17
51
|
reactDebugChannel: false,
|
|
18
52
|
};
|
|
19
53
|
return {
|
|
20
|
-
...
|
|
54
|
+
...baseConfig,
|
|
21
55
|
...(allowedDevOrigins ? { allowedDevOrigins } : {}),
|
|
22
|
-
devIndicators:
|
|
56
|
+
devIndicators: baseConfig.devIndicators ?? false,
|
|
23
57
|
experimental,
|
|
24
58
|
};
|
|
25
59
|
}
|
package/package.json
CHANGED
|
@@ -25,7 +25,7 @@ If you are unsure, default to **internal-only**. Do not add a client surface unl
|
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
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
|
|
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
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.
|
|
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
|
|
|
@@ -58,7 +58,7 @@ The app's API contract (automation triggers/actions, webhook events) is NOT an `
|
|
|
58
58
|
|
|
59
59
|
## The `notification` surface
|
|
60
60
|
|
|
61
|
-
When
|
|
61
|
+
When a notification should open a specific record, declare a `notification` surface. That path is what the Notification Center embeds; it is not a button the app renders.
|
|
62
62
|
|
|
63
63
|
```json
|
|
64
64
|
{
|
|
@@ -71,10 +71,10 @@ When an app sends notifications with `ctaParams`, the Notification Center needs
|
|
|
71
71
|
|
|
72
72
|
When `notification` is declared:
|
|
73
73
|
- The build pipeline sets `notificationUrl` on the marketplace app to `<deployURL>/n`.
|
|
74
|
-
-
|
|
74
|
+
- Opening the notification embeds that URL in the Notification Center detail panel, with the token and `ctaParams` as search params. If `notificationUrl` is empty, the panel has no iframe: it shows the body and an "Open {app name}" button that opens the app's main view, and non-reserved `ctaParams` are forwarded as search params on that view.
|
|
75
75
|
- Your `/n` page shows its context-specific content from the `ctaParams` in the URL. Those always arrive; `notificationId` from the token payload is best-effort under the session-exchange flow, so never gate the record render on it.
|
|
76
76
|
|
|
77
|
-
**Declare the `notification` surface
|
|
77
|
+
**Declare the `notification` surface, and pass `ctaParams` that identify the record, whenever a notification should open that record.** A send with neither is only correct when the destination is the app's main view. See the `assembly-notifications` skill.
|
|
78
78
|
|
|
79
79
|
## Examples
|
|
80
80
|
|
|
@@ -29,7 +29,9 @@ The audience changes the experience and the code:
|
|
|
29
29
|
|
|
30
30
|
## Notification surfaces
|
|
31
31
|
|
|
32
|
-
A surface is the embedded view the Notification Center renders when an internal user opens a notification. It's a route (conventionally `/n`) that reads
|
|
32
|
+
A surface is the embedded view the Notification Center renders when an internal user opens a notification. It's a route (conventionally `/n`) that reads your `ctaParams` off the URL and renders the record they name.
|
|
33
|
+
|
|
34
|
+
Declare the surface, and pass `ctaParams` that identify the record, whenever opening the notification should land on that record (one retainer, one submission, one task). Without the surface the Center shows the title and body, plus a platform button labeled "Open {app name}" that opens the app's main view. Non-reserved `ctaParams` are forwarded as search params on that main view. Skip the surface only when that main view is the right destination.
|
|
33
35
|
|
|
34
36
|
Surfaces are internal-only. Clients don't render one, but `ctaParams` still travel with the notification and become URL search params on an email CTA click — that's how a client landing from an email gets enough context.
|
|
35
37
|
|
|
@@ -142,7 +144,7 @@ export async function GET() {
|
|
|
142
144
|
- `senderType` — read-side only. Automatically populated from `senderId` (client, internal user, or company). You do not set it on create.
|
|
143
145
|
- `deliveryTargets.inProduct` — include to make the notification appear in the Notification Center (for internal users) or trigger the badge (for clients). Requires `title`. `body` and `ctaParams` are optional. Omit entirely for email-only notifications.
|
|
144
146
|
- `deliveryTargets.email` — include to send an email. `subject`, `header`, and `title` (CTA button label) are required, plus one body: `htmlBody` for formatted content (see below) or `body` for plain text. Either `inProduct` or `email` must be present; both is also valid.
|
|
145
|
-
- `ctaParams` — a plain object of string key/value pairs that become URL search params when the user engages with the notification. Pass as an object — never `JSON.stringify()`. **Values must be strings** — stringify numbers and booleans yourself (e.g., `{ count: '3' }`, not `{ count: 3 }`). Nested objects aren't supported. Used by both the email CTA and the Notification Center surface.
|
|
147
|
+
- `ctaParams` — a plain object of string key/value pairs that become URL search params when the user engages with the notification. Pass as an object — never `JSON.stringify()`. **Values must be strings** — stringify numbers and booleans yourself (e.g., `{ count: '3' }`, not `{ count: 3 }`). Nested objects aren't supported. Used by both the email CTA and the Notification Center surface. A notification about one record always includes the id that selects it (`{ retainerId: retainer.id }`). Title and body text do not.
|
|
146
148
|
<!-- feature:notification-settings -->
|
|
147
149
|
- `notificationSettingId` — the id of one of the app's declared notification settings (see "Per-user notification settings" below). Pass it on **every** send of a declared category so internal users' Product/Email toggles apply. Omit it for sends with no declared setting.
|
|
148
150
|
<!-- /feature:notification-settings -->
|
|
@@ -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 -->
|