@assembly-js/app-foundation 0.0.31 → 0.0.33

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,7 +43,7 @@ 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
- - `assembly-ai` — AI features via Assembly's OpenAI proxy (no API keys needed)
46
+ - `assembly-ai` — AI features, including speech-to-text, via Assembly's OpenAI proxy (no API keys needed)
47
47
  - `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
48
  - `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
49
49
  - `assembly-avatar-sub` — AvatarSub company badge wherever a client is rendered, including tables of your own records that reference a client; row duplication only for lists OF clients
@@ -0,0 +1,73 @@
1
+ import '@assembly-js/app-bridge';
2
+ export type NavigateDestination = {
3
+ to: 'home';
4
+ } | {
5
+ to: 'client';
6
+ clientId: string;
7
+ tab?: string;
8
+ } | {
9
+ to: 'company';
10
+ companyId: string;
11
+ tab?: string;
12
+ } | {
13
+ to: 'channel';
14
+ channelId?: string;
15
+ } | {
16
+ to: 'files';
17
+ channelId?: string;
18
+ } | {
19
+ to: 'contract';
20
+ contractId: string;
21
+ } | {
22
+ to: 'form';
23
+ formId: string;
24
+ } | {
25
+ to: 'formResponse';
26
+ formId: string;
27
+ responseId: string;
28
+ } | {
29
+ to: 'invoice';
30
+ invoiceId: string;
31
+ } | {
32
+ to: 'subscription';
33
+ subscriptionId: string;
34
+ } | {
35
+ to: 'product';
36
+ } | {
37
+ to: 'helpdesk';
38
+ } | {
39
+ to: 'article';
40
+ articleId: string;
41
+ } | {
42
+ to: 'notifications';
43
+ } | {
44
+ to: 'automations';
45
+ } | {
46
+ to: 'settings';
47
+ page?: string;
48
+ } | {
49
+ to: 'app';
50
+ installId: string;
51
+ };
52
+ export type NavigateToOptions = {
53
+ replace?: boolean;
54
+ newTab?: boolean;
55
+ timeout?: number;
56
+ };
57
+ export type NavigateResult = {
58
+ ok: true;
59
+ } | {
60
+ ok: false;
61
+ reason: string;
62
+ };
63
+ declare module '@assembly-js/app-bridge' {
64
+ interface AssemblyBridgeApi {
65
+ postToParent(message: {
66
+ type: string;
67
+ [key: string]: unknown;
68
+ }, options?: {
69
+ droppable?: boolean;
70
+ }): void;
71
+ navigateTo(destination: NavigateDestination, options?: NavigateToOptions): Promise<NavigateResult>;
72
+ }
73
+ }
@@ -0,0 +1,13 @@
1
+ // The published @assembly-js/app-bridge types this package typechecks against
2
+ // (peer >=1.3.0) predate the public postToParent and navigateTo. Both land on
3
+ // the 2.0.0 set, where they're required interface members. Augment rather
4
+ // than depending on an unpublished tarball; call sites guard with `?.` /
5
+ // truthiness checks since older peers lack these at runtime.
6
+ //
7
+ // Exported as real types (not just declared ambiently) because an ambient
8
+ // .d.ts that nothing imports never reaches a published package's declaration
9
+ // output — tsc only emits/carries declarations that are part of the module
10
+ // graph. lib-client.ts imports these types from this module instead of from
11
+ // '@assembly-js/app-bridge' so the augmentation travels with it to consumers
12
+ // on older peers, which don't export these names at all.
13
+ import '@assembly-js/app-bridge';
@@ -1,3 +1,4 @@
1
+ import type { NavigateDestination, NavigateToOptions } from './bridge-vnext.js';
1
2
  export declare function reexchangeBearer(): Promise<boolean>;
2
3
  export declare function authFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
3
4
  export declare function downloadViaBlob(url: string, filename: string): Promise<void>;
@@ -9,3 +10,13 @@ export declare function downloadViaBlob(url: string, filename: string): Promise<
9
10
  * Exported for tests; runs once at module load in the browser.
10
11
  */
11
12
  export declare function bootstrapCookielessBearer(): Promise<boolean>;
13
+ /**
14
+ * Returns an onClick handler that navigates the parent Assembly portal to the
15
+ * given typed destination. Use in button or anchor onClick props.
16
+ *
17
+ * @example
18
+ * <button onClick={linkTo({ to: 'invoice', invoiceId: invoice.id })}>
19
+ * View invoice
20
+ * </button>
21
+ */
22
+ export declare function linkTo(destination: NavigateDestination, options?: NavigateToOptions): () => void;
@@ -166,3 +166,22 @@ export function bootstrapCookielessBearer() {
166
166
  // Self-running, mirroring origin-bootstrap: module evaluation precedes
167
167
  // hydration, so the bearer is in hand before any component's first fetch.
168
168
  void bootstrapCookielessBearer();
169
+ /**
170
+ * Returns an onClick handler that navigates the parent Assembly portal to the
171
+ * given typed destination. Use in button or anchor onClick props.
172
+ *
173
+ * @example
174
+ * <button onClick={linkTo({ to: 'invoice', invoiceId: invoice.id })}>
175
+ * View invoice
176
+ * </button>
177
+ */
178
+ export function linkTo(destination, options) {
179
+ return () => {
180
+ // navigateTo rejects when the host never answers (e.g. the nav v2 flag is
181
+ // off) — catch it here, since the caller only gets a void onClick handler
182
+ // back and has no promise of its own to attach a handler to.
183
+ AssemblyBridge.navigateTo?.(destination, options)?.catch((err) => {
184
+ console.error('linkTo: navigateTo failed', err);
185
+ });
186
+ };
187
+ }
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 wrapper is a pass-through there.
37
+ // sandbox host — only the env injection above applies there.
4
38
  if (process.env.NODE_ENV === 'production')
5
- return config;
39
+ return baseConfig;
6
40
  const sandboxHost = process.env.SANDBOX_HOST;
7
41
  const allowedDevOrigins = sandboxHost
8
- ? [...new Set([sandboxHost, ...(config.allowedDevOrigins ?? [])])]
9
- : config.allowedDevOrigins;
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
- ...config.experimental,
50
+ ...baseConfig.experimental,
17
51
  reactDebugChannel: false,
18
52
  };
19
53
  return {
20
- ...config,
54
+ ...baseConfig,
21
55
  ...(allowedDevOrigins ? { allowedDevOrigins } : {}),
22
- devIndicators: config.devIndicators ?? false,
56
+ devIndicators: baseConfig.devIndicators ?? false,
23
57
  experimental,
24
58
  };
25
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
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-ai
3
- description: Use when the app needs AI features like text generation, summarization, analysis, or any LLM capability. Apps call Assembly's AI proxy — no API keys or external SDKs needed.
3
+ description: Use when the app needs AI features like text generation, summarization, analysis, vision, speech-to-text, or any LLM capability. Apps call Assembly's AI proxy — no API keys or external SDKs needed.
4
4
  ---
5
5
 
6
6
  ## How it works
@@ -96,6 +96,29 @@ const toDataUrl = (file: File): Promise<string> =>
96
96
 
97
97
  Keep images under ~4 MB to stay within the 6 MB Lambda payload limit (base64 adds ~33% overhead). For larger images, resize on the client before converting.
98
98
 
99
+ ## Audio (speech-to-text)
100
+
101
+ Record in the browser with `MediaRecorder` in the browser's native container: `audio/webm;codecs=opus`, or `audio/mp4` on Safari. Use a speech bitrate such as 24 kbps. Do not decode the recording to PCM or WAV in the browser.
102
+
103
+ Transcribe on the server. Post the file as `multipart/form-data` to `/v1/ai/openai/audio/transcriptions` through `assembly.fetch()`. Use `gpt-4o-mini-transcribe` (`whisper-1` is the alternative). Do not set a JSON `Content-Type` on this call. Pass the `FormData` as the body and let the runtime set the multipart header.
104
+
105
+ ```typescript
106
+ export async function transcribeAudio(file: File) {
107
+ const assembly = await getAssembly();
108
+ const body = new FormData();
109
+ body.append('file', file);
110
+ body.append('model', 'gpt-4o-mini-transcribe');
111
+ const response = await assembly.fetch('/v1/ai/openai/audio/transcriptions', {
112
+ method: 'POST',
113
+ body,
114
+ });
115
+ if (!response.ok) throw new Error(`AI request failed: ${response.status}`);
116
+ return response;
117
+ }
118
+ ```
119
+
120
+ Keep each uploaded file under 4 MB. The platform API rejects request bodies above roughly 4.5 MB with HTTP 413 before the proxy runs, and there is no richer error to catch. Keep each file under about five minutes so the upstream call also finishes inside the proxy's 27 second first-byte timeout. For longer recordings, split the recording into files that meet both bounds and persist each transcript piece as it completes.
121
+
99
122
  ## Model selection
100
123
 
101
124
  - Default to `gpt-4o-mini` for most tasks (fast, cheap, good enough)
@@ -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 only when the app uses `ctaParams` in notifications (see below).
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 an app sends notifications with `ctaParams`, the Notification Center needs to know where to send the user when they click the notification's CTA. Declare a `notification` surface to set that destination URL.
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
- - When a user clicks a notification CTA, the portal opens the app at `notificationUrl?token=<jwt>&<ctaParams>`.
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 whenever the app sends notifications with `ctaParams`.** If notifications have no `ctaParams`, the surface is optional. See the `assembly-notifications` skill for the full CTA params pattern.
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 the notification context — `notificationId` from the token payload plus your `ctaParams` — and renders the most useful thing: a filtered list of the items referenced, a single record with its key actions, or a status summary. When there's nothing more useful than "here's what happened, click to go deeper," skip it — the Notification Center then falls back to the title, body, and CTA from your `createNotification` call.
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 -->