@assembly-js/app-foundation 0.6.4 → 0.6.6

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,13 @@ 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.6.6
23
+ - [guidance] The build agent ends a chat turn by describing what changed instead of reporting its type-check, build, or the screens it didn't open, and knows the platform builds the app after every turn. It still claims "fixed" or "working" only for what it exercised, and talks about verification when asked or when fixing a reported bug.
24
+
25
+ ## 0.6.5
26
+ - [guidance] The `assembly-storage` skill says the `assembly` option of `detectPdfFields` is only an opt-in flag. The `assembly-ai` skill and the `assembly.fetch()` prose now call the AI proxy on the installer client (`getInstallerAssembly()`, gated by `await getAssembly()` first) instead of the viewer's client.
27
+ - [behavior] `detectPdfFields` now runs its AI step on the installer client, and degrades to a warning when there is no verified viewer. `options.assembly` is deprecated and ignored; passing it still opts in to the AI step.
28
+
22
29
  ## 0.6.4
23
30
  - [behavior] An app's sandbox now sends its sandbox grant when minting an app bearer, so the platform can tell its calls from the deployed app's.
24
31
 
package/CLAUDE.md CHANGED
@@ -684,17 +684,17 @@ export PATH="$HOME/.bun/bin:$PATH" && bun add <extra-package>
684
684
 
685
685
  ### Inner verification loop — type-check, don't full-build
686
686
 
687
- 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.
687
+ Use `bunx tsc --noEmit` to check changes while iterating (2–3s, full type errors). After every chat turn that changes app code, the platform runs the type-check and a full build and sends failures back for you to fix, so skip `bun run build` unless the task asks for a build or you changed `lib/schema.ts` and need the new tables now.
688
688
 
689
689
  ```bash
690
690
  # Inner loop — after edits, every few changes:
691
691
  export PATH="$HOME/.bun/bin:$PATH" && bunx tsc --noEmit
692
692
 
693
- # Final check — once, at the end of the task:
693
+ # Full build — only when the task asks for one:
694
694
  export PATH="$HOME/.bun/bin:$PATH" && bun run build
695
695
  ```
696
696
 
697
- If `tsc --noEmit` is clean and the task says "verify the build", run `bun run build` once and move on — don't loop on it.
697
+ When the task says "verify the build", run `bun run build` once.
698
698
 
699
699
  `bun run build` also validates `.assembly/app.json` (it runs the manifest validator first) and **fails** if the manifest is malformed or a key is misplaced — most commonly a surface like `notification` written at the top level instead of under `surfaces`. If the build stops with a `.assembly/app.json is invalid` error, fix the manifest as the message instructs — every surface belongs under the `surfaces` key. Run `bun run validate:manifest` to check it on its own.
700
700
 
@@ -713,9 +713,9 @@ If `tsc --noEmit` is clean and the task says "verify the build", run `bun run bu
713
713
 
714
714
  `tsc --noEmit` and `bun run build` prove the code compiles and the static checks pass. Neither renders a page, so both stay green while a page 500s for every visitor — a function passed into a client component, a browser-only import in server code, a field the sample record happened to have. A query against the sandbox database or an SDK call shows the data is there, not that the page reading it renders.
715
715
 
716
- You cannot open the app as a signed-in client or team member, and you cannot see the published app or its logs. The builder preview is the user's own signed-in session, so the one runtime signal you have is the dev-server log (`assembly-dev-server-logs`): after changing a page or route, read it for new errors before calling the change done, and when nothing has requested the changed screen yet, ask the user to open it in the preview.
716
+ You cannot open the app as a signed-in client or team member, and you cannot see the published app or its logs. The builder preview is the user's own signed-in session, so the one runtime signal you have is the dev-server log (`assembly-dev-server-logs`): after changing a page or route, read it and fix any new errors before calling the change done. Mention it only if something is wrong.
717
717
 
718
- Close every turn, and answer every "does it work" or "is it safe to publish", by naming the checks that ran in those terms and the screens you could not open — which page, as which kind of user — so the user opens them. Say this unprompted, every time, not only when the prompt asks for a manual check. Say "fixed", "working", or "good to publish" only for behaviour you exercised; otherwise say what you checked and what is still unseen.
718
+ Close a turn by describing what changed, in product terms — no tool or command names, no list of what you did not open or run. A limitation the user will hit is worth one line ("test cases still reset on reload; I can move them into the database"). Speak about verification only when the user asks whether something works or is safe to publish, or the turn fixes a bug they reported; then say in product terms what you exercised, or whether you reproduced the bug and saw it gone. Say "fixed", "working", or "good to publish" only for behaviour you exercised.
719
719
 
720
720
  When the user reports that the published app errors, do not blame a cold start, caching, or a stale tab without evidence. Ask for the page, who was signed in, and the exact error text or code; reproduce it in the sandbox — the same screen in the preview or the same request against the dev server — and read the dev-server log. If you cannot reproduce it, say so and ask for those details rather than asking the user to reload.
721
721
 
@@ -1260,9 +1260,11 @@ Convention-breakers to know up front:
1260
1260
 
1261
1261
  ### `assembly.fetch()` — for non-SDK endpoints
1262
1262
 
1263
- The SDK exposes a generic `fetch(path, init?)` method for calling Assembly API endpoints that aren't in the generated SDK methods (e.g. the AI proxy). It prepends the base URL and injects auth headers (`Authorization: Bearer` from the `getAssembly()` client, plus `X-Assembly-SDK-Version`) automatically — never `X-API-Key` on data calls — returning a standard `Response`:
1263
+ The SDK exposes a generic `fetch(path, init?)` method for calling Assembly API endpoints that aren't in the generated SDK methods (e.g. the AI proxy). It prepends the base URL and injects auth headers (`Authorization: Bearer` from the client you call it on, plus `X-Assembly-SDK-Version`) automatically — never `X-API-Key` on data calls — returning a standard `Response`. The AI proxy is app-owned, so call it on the installer client (`getInstallerAssembly()`, server-side only), gated by `await getAssembly()` first because that throws without a verified viewer:
1264
1264
 
1265
1265
  ```ts
1266
+ await getAssembly(); // the gate: throws without a verified viewer
1267
+ const assembly = await getInstallerAssembly();
1266
1268
  const response = await assembly.fetch('/v1/ai/openai/chat/completions', {
1267
1269
  method: 'POST',
1268
1270
  body: JSON.stringify({ model: 'gpt-4o-mini', messages }),
@@ -1508,7 +1510,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
1508
1510
  - ❌ Don't import a native or browser-only library into server code without the serverless setup — `bun run build` passes but the deployed route crashes at request time. Decide the layer first per "Server runs in serverless Node" above.
1509
1511
  - ❌ Don't preview a PDF (or any file) in an `<iframe>`, `<embed>` or `<object>` — the portal frame is sandboxed and Chrome won't run its PDF viewer inside it, so the preview renders blank while the build passes. Open the pre-signed URL in a new tab (`assembly-storage` Pattern A) or render pages to a canvas client-side with `next/dynamic` + `ssr: false`.
1510
1512
  - ❌ Don't build a PDF-to-form feature on AcroForm fields alone. Call `detectPdfFields` and branch on `method`: `none` and `xfa` need a visible explanation plus a manual add-field path, and `text` fields are suggestions the user must be able to edit before saving. Plenty of real forms are Word exports, scans or XFA, so an AcroForm-only app tells those users their form is empty.
1511
- - ❌ Don't call a change verified, working, or safe to publish because `tsc --noEmit` and `bun run build` passed — name the checks that ran and the signed-in screens left unopened. See "What a passing build proves" above.
1513
+ - ❌ Don't call a change verified, working, or safe to publish because `tsc --noEmit` and `bun run build` passed, and don't close a turn with a report of your checks. See "What a passing build proves" above.
1512
1514
  - ❌ Don't pass a function — or an object that holds one, like a question/field config with `visibleWhen`, `validate`, `render`, or `format` callbacks — as a prop from a server component or page into a `'use client'` component. Props cross that boundary as serialized data, so the render throws `Functions cannot be passed directly to Client Components` and the page 500s for every visitor no matter what the record holds. `tsc --noEmit` and `bun run build` both pass. Keep such a config in a shared module the client component imports itself, or express the logic as data (`{ showWhen: { field: 'entityType', equals: 'llc' } }`) and interpret it client-side.
1513
1515
  - ❌ Don't call a function imported from a `'use client'` file from a server component, layout, or server action — e.g. an answer formatter defined in the client component file that the `/n` notification page also renders. The server gets a client reference, not the function, so the render throws and the route 500s for every visitor; `tsc --noEmit` passes. A `'use client'` file should export only components and hooks; put a shared helper in a plain module (`lib/…`) both sides import. `bun run build` fails on this (`check:client-boundary`).
1514
1516
  - ❌ Don't use `Select` or `Combobox` to pick any Assembly person or organization record (clients, internal users, team members, staff, companies). Always use `UserCompanySelector` — in forms, filters, dropdowns, "assign to" controls, everywhere.
package/dist/pdf.d.ts CHANGED
@@ -44,8 +44,12 @@ export declare class PdfUnreadableError extends Error {
44
44
  * Finds the fields a person has to fill in on a PDF form: its AcroForm fields when
45
45
  * it has them, otherwise fields read from its text by Assembly AI when `assembly`
46
46
  * is given. Throws PdfUnreadableError when the bytes are not a readable PDF.
47
- * Server side only: the AI step runs through the app's Assembly client.
47
+ * Server side only: the AI step runs through the installer client.
48
48
  */
49
49
  export declare function detectPdfFields(data: ArrayBuffer | Uint8Array, options?: {
50
+ /**
51
+ * @deprecated The client is ignored: the AI step uses the installer client, since the AI proxy
52
+ * is app-owned. Kept so existing apps that pass it keep working; its presence still opts in.
53
+ */
50
54
  assembly?: AssemblyAPI;
51
55
  }): Promise<PdfFieldDetection>;
package/dist/pdf.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getInstallerAssemblyForCaller } from './installer-caller.js';
1
2
  // pdf.js rejects a Node Buffer outright, so hand it a plain Uint8Array view.
2
3
  function toBytes(data) {
3
4
  return data instanceof ArrayBuffer
@@ -98,7 +99,7 @@ const AI_RESPONSE_FORMAT = {
98
99
  * Finds the fields a person has to fill in on a PDF form: its AcroForm fields when
99
100
  * it has them, otherwise fields read from its text by Assembly AI when `assembly`
100
101
  * is given. Throws PdfUnreadableError when the bytes are not a readable PDF.
101
- * Server side only: the AI step runs through the app's Assembly client.
102
+ * Server side only: the AI step runs through the installer client.
102
103
  */
103
104
  export async function detectPdfFields(data, options) {
104
105
  const bytes = toBytes(data);
@@ -137,7 +138,7 @@ export async function detectPdfFields(data, options) {
137
138
  return { method: 'none', fields: [], pages, text, warnings };
138
139
  if (!options?.assembly)
139
140
  return { method: 'text', fields: [], pages, text, warnings };
140
- const suggested = await suggestFieldsFromText(options.assembly, pageTexts);
141
+ const suggested = await suggestFieldsFromText(pageTexts);
141
142
  return {
142
143
  method: 'text',
143
144
  fields: suggested.fields,
@@ -247,7 +248,7 @@ async function extractPageTexts(bytes) {
247
248
  await pdf.loadingTask.destroy();
248
249
  }
249
250
  }
250
- async function suggestFieldsFromText(assembly, pageTexts) {
251
+ async function suggestFieldsFromText(pageTexts) {
251
252
  const warnings = [];
252
253
  const prompt = pageTexts
253
254
  .map((pageText, index) => {
@@ -261,6 +262,9 @@ async function suggestFieldsFromText(assembly, pageTexts) {
261
262
  .join('\n\n');
262
263
  let suggested;
263
264
  try {
265
+ // Inside the try so a missing verified viewer degrades to a warning like any other AI failure.
266
+ // Same viewer gate as storage and connectors: an app with token exchange off has no verified viewer here, and that is accepted.
267
+ const assembly = await getInstallerAssemblyForCaller();
264
268
  const response = await assembly.fetch(AI_CHAT_COMPLETIONS_PATH, {
265
269
  method: 'POST',
266
270
  headers: { 'Content-Type': 'application/json' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -13,13 +13,15 @@ The proxy mirrors OpenAI's API shape. Call any OpenAI endpoint path via:
13
13
 
14
14
  ## Setup
15
15
 
16
- Create a helper in `lib/ai.ts` that uses the existing Assembly SDK instance:
16
+ Create a helper in `lib/ai.ts` that calls the proxy on the installer client:
17
17
 
18
18
  ```typescript
19
- import { getAssembly } from '@assembly-js/app-foundation/server';
19
+ import { getAssembly, getInstallerAssembly } from '@assembly-js/app-foundation/server';
20
20
 
21
21
  export async function chatCompletion(messages: Array<{ role: string; content: string }>, options?: { model?: string; stream?: boolean }) {
22
- const assembly = await getAssembly();
22
+ // The AI proxy is app-owned, so it uses the installer client; getAssembly() is the gate, it throws without a verified viewer.
23
+ await getAssembly();
24
+ const assembly = await getInstallerAssembly();
23
25
  const response = await assembly.fetch('/v1/ai/openai/chat/completions', {
24
26
  method: 'POST',
25
27
  body: JSON.stringify({
@@ -41,7 +43,9 @@ export async function aiErrorMessage(response: Response): Promise<string> {
41
43
  }
42
44
  ```
43
45
 
44
- `assembly.fetch()` automatically prepends the base URL and injects auth headers (`Authorization: Bearer` from `getAssembly()`, plus `X-Assembly-SDK-Version`). It never sends `X-API-Key` on data calls. You only provide the path and request body.
46
+ `assembly.fetch()` automatically prepends the base URL and injects auth headers (`Authorization: Bearer` from the installer client, plus `X-Assembly-SDK-Version`). It never sends `X-API-Key` on data calls. You only provide the path and request body.
47
+
48
+ The AI proxy belongs to the app, not to the person using it, so these calls use the installer client from `getInstallerAssembly()` rather than the viewer's `getAssembly()` client. It is server-side only. Call `await getAssembly()` first: it throws when there is no verified viewer, which keeps the installer's authority from being used by an anonymous caller. Let that throw propagate out of the server action so the bearer renewal can retry it; return `{ ok: false, message }` only for the proxy's own refusals.
45
49
 
46
50
  ## Server-side only
47
51
 
@@ -87,7 +91,9 @@ The proxy supports OpenAI's vision API. Use `gpt-4o` with the multi-part content
87
91
 
88
92
  ```typescript
89
93
  export async function analyzeImage(base64DataUrl: string, prompt: string) {
90
- const assembly = await getAssembly();
94
+ // The AI proxy is app-owned, so it uses the installer client; getAssembly() is the gate, it throws without a verified viewer.
95
+ await getAssembly();
96
+ const assembly = await getInstallerAssembly();
91
97
  const response = await assembly.fetch('/v1/ai/openai/chat/completions', {
92
98
  method: 'POST',
93
99
  body: JSON.stringify({
@@ -128,7 +134,9 @@ Transcribe on the server. Post the file as `multipart/form-data` to `/v1/ai/open
128
134
 
129
135
  ```typescript
130
136
  export async function transcribeAudio(file: File) {
131
- const assembly = await getAssembly();
137
+ // The AI proxy is app-owned, so it uses the installer client; getAssembly() is the gate, it throws without a verified viewer.
138
+ await getAssembly();
139
+ const assembly = await getInstallerAssembly();
132
140
  const body = new FormData();
133
141
  body.append('file', file);
134
142
  body.append('model', 'gpt-4o-mini-transcribe');
@@ -168,8 +168,9 @@ free — no second upload.
168
168
  ### Detecting form fields in an uploaded PDF
169
169
 
170
170
  A PDF-to-form feature calls the helper in the processing route and lets `method` decide the UI. Passing
171
- `assembly` (from `getAssemblyForRequest(request)`) lets a text-only PDF get AI-suggested fields; without
172
- it, `text` comes back with no fields. A file that is not a PDF throws `PdfUnreadableError`, whose message
171
+ `assembly` (from `getAssemblyForRequest(request)`) opts a text-only PDF in to AI-suggested fields; without
172
+ it, `text` comes back with no fields. The AI step runs on the installer client, so the client you pass is
173
+ only the opt-in flag and is otherwise ignored (the option is deprecated). A file that is not a PDF throws `PdfUnreadableError`, whose message
173
174
  is safe to show.
174
175
 
175
176
  ```typescript