@assembly-js/app-foundation 0.14.1 → 0.14.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 +6 -0
- package/CLAUDE.md +1 -0
- package/package.json +1 -1
- package/skills/assembly-forms/SKILL.md +15 -1
- package/skills/assembly-oauth-secrets/SKILL.md +62 -0
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.14.3
|
|
23
|
+
- [guidance] The build agent now sets up services that sign in with OAuth (such as Dropbox) with a connection that renews itself, explains every setup step up front, and never asks for a token in chat.
|
|
24
|
+
|
|
25
|
+
## 0.14.2
|
|
26
|
+
- [guidance] The build agent now keeps each row's own id in forms with add-and-remove rows (answer options, line items), so a selected row is no longer rejected as missing.
|
|
27
|
+
|
|
22
28
|
## 0.14.1
|
|
23
29
|
- [guidance] The build agent is taught that a call to another app through the installer client or a raw `fetch` carries the client in a client's request, that the platform can refuse it with `viewer_cannot_open_app`, and which cases the check does not cover; a route that allows only app callers answers a client who arrives through another app with 403, so it must list `client` to admit them; a `check-app-only-routes: intended` comment only tells the check script that a route should stay app-only.
|
|
24
30
|
|
package/CLAUDE.md
CHANGED
|
@@ -62,6 +62,7 @@ Assembly-specific gotchas live in `.claude/skills/<name>/SKILL.md`. Invoke them
|
|
|
62
62
|
- `assembly-ai` — AI features, including speech-to-text, via Assembly's OpenAI proxy (no API keys needed)
|
|
63
63
|
- `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.
|
|
64
64
|
- `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
|
|
65
|
+
- `assembly-oauth-secrets` — REQUIRED before a `requiredSecrets` entry for a provider that signs in with OAuth (Dropbox, Google, Microsoft; any access token that expires): store the app key, app secret and refresh token, never the access token; give the user every setup step in one message; never ask for a token in chat.
|
|
65
66
|
- `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
|
|
66
67
|
- `assembly-user-company-selector` — UserCompanySelector for picking clients, internal users, companies
|
|
67
68
|
- `assembly-forms` — react-hook-form + zod + Field component validation pattern
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: assembly-forms
|
|
3
|
-
description: Use when the app has multi-field forms that need client-side validation with per-field error messages. Covers react-hook-form + zod + Assembly UI Field component. NOT the Assembly Forms/intake module — for that (surveys, intake) use the SDK's listForms/retrieveForm/requestFormResponse instead.
|
|
3
|
+
description: Use when the app has multi-field forms that need client-side validation with per-field error messages. Covers react-hook-form + zod + Assembly UI Field component, and useFieldArray rows that carry their own id (answer options, line items). NOT the Assembly Forms/intake module — for that (surveys, intake) use the SDK's listForms/retrieveForm/requestFormResponse instead.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -95,6 +95,20 @@ The installed zod is v4. Use the v4 idioms below; the v3 forms are deprecated (t
|
|
|
95
95
|
- Flatten/format issues with the top-level `z.flattenError(err)` / `z.treeifyError(err)` — **not** `err.flatten()` / `err.format()`. Reading `err.issues` directly is fine.
|
|
96
96
|
- Import from `'zod'` (top-level) — **not** the `'zod/v4'` subpath.
|
|
97
97
|
|
|
98
|
+
## Dynamic rows: `useFieldArray` replaces each row's `id`
|
|
99
|
+
|
|
100
|
+
`useFieldArray` writes its own generated key into every entry of `fields` under `id`, replacing an `id` the row already has (a database id, an option id another field points at). The form values keep the row's real id, so a value read from `field.id` (a radio `value`, a `correctOptionId`, a remove-by-id) matches no row and validation rejects a visibly selected answer. Pass `keyName: 'key'` whenever rows carry their own `id`:
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
// ❌ field.id is react-hook-form's generated key, not the option's id
|
|
104
|
+
const { fields } = useFieldArray({ control, name: 'options' });
|
|
105
|
+
<RadioGroupItem key={field.id} value={field.id} />
|
|
106
|
+
|
|
107
|
+
// ✅ field.key is the generated key; field.id stays the option's id
|
|
108
|
+
const { fields } = useFieldArray({ control, name: 'options', keyName: 'key' });
|
|
109
|
+
<RadioGroupItem key={field.key} value={field.id} />
|
|
110
|
+
```
|
|
111
|
+
|
|
98
112
|
## Key patterns
|
|
99
113
|
|
|
100
114
|
- `data-invalid={!!errors.fieldName}` on `Field` triggers destructive (red) styling on the label and input border
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: assembly-oauth-secrets
|
|
3
|
+
description: Use BEFORE declaring a requiredSecrets entry for a provider that signs in with OAuth 2 and is not connectable through assembly-integrations (Dropbox, Google, Microsoft, Box, Zoom, any API whose access tokens expire), and when a saved access token is rejected with 401 or expired_access_token. Covers declaring the app key, app secret and refresh token instead of an access token, renewing the access token in server code, and the setup steps to give the user in one message without asking for any token in chat.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
An OAuth access token expires within hours (Dropbox: about four), so one saved as a secret works briefly and is then rejected until someone replaces it by hand. If the `assembly-integrations` skill is present and lists the provider, use it and stop here. Otherwise the app stores three long-lived values and mints its own access tokens.
|
|
7
|
+
|
|
8
|
+
## Declare the app key, app secret and refresh token
|
|
9
|
+
|
|
10
|
+
Never declare the access token. Declare all three in one change, each with a `description` saying where the value comes from:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
"requiredSecrets": [
|
|
14
|
+
{ "key": "DROPBOX_APP_KEY", "label": "Dropbox app key", "description": "From your app's Settings tab in the Dropbox App Console.", "type": "text" },
|
|
15
|
+
{ "key": "DROPBOX_APP_SECRET", "label": "Dropbox app secret", "description": "Shown next to the app key in the Dropbox App Console.", "type": "secret" },
|
|
16
|
+
{ "key": "DROPBOX_REFRESH_TOKEN", "label": "Dropbox refresh token", "description": "The refresh_token value from the one-time setup step.", "type": "secret" }
|
|
17
|
+
]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Renew the access token in server code
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// lib/dropbox.ts (server only)
|
|
24
|
+
let cached: { token: string; expiresAt: number } | null = null;
|
|
25
|
+
|
|
26
|
+
export async function getDropboxAccessToken(): Promise<string> {
|
|
27
|
+
if (cached && cached.expiresAt > Date.now() + 60_000) return cached.token;
|
|
28
|
+
const res = await fetch('https://api.dropboxapi.com/oauth2/token', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
body: new URLSearchParams({
|
|
31
|
+
grant_type: 'refresh_token',
|
|
32
|
+
refresh_token: process.env.DROPBOX_REFRESH_TOKEN ?? '',
|
|
33
|
+
client_id: process.env.DROPBOX_APP_KEY ?? '',
|
|
34
|
+
client_secret: process.env.DROPBOX_APP_SECRET ?? '',
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error('Dropbox refused to renew access');
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
|
|
40
|
+
return cached.token;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Give the user the whole setup in one message
|
|
45
|
+
|
|
46
|
+
Send every step in the reply where you choose this path, before they try anything. A user who learns the steps one failure at a time spends hours on it. Use the form labels, not the key names. For Dropbox:
|
|
47
|
+
|
|
48
|
+
1. In the Dropbox App Console, create an app and turn on the permissions the app needs (changing them later means repeating steps 2 and 3). Copy the app key and app secret.
|
|
49
|
+
2. Open this link in a browser with their app key in place of APP_KEY, approve, and copy the code Dropbox shows: `https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&response_type=code&token_access_type=offline`. Without `token_access_type=offline` step 3 returns no refresh token (Google's equivalent is `access_type=offline&prompt=consent`).
|
|
50
|
+
3. Exchange the code once, on their own computer. This step needs a terminal; say so up front, and that the code works once and expires within minutes:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl https://api.dropboxapi.com/oauth2/token -d grant_type=authorization_code -d code=CODE -u APP_KEY:APP_SECRET
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The reply contains a `refresh_token`. If it has only an `access_token`, the link in step 2 lacked the offline parameter: repeat steps 2 and 3.
|
|
57
|
+
4. Enter the app key, app secret and refresh token in the secure form under your reply (later: composer **+** menu, **App secrets**). The refresh token keeps working until they revoke it.
|
|
58
|
+
|
|
59
|
+
## Never ask for a token in chat
|
|
60
|
+
|
|
61
|
+
- Never ask the user to paste an access token, refresh token, authorization code, app secret or the command's output. To debug step 3, ask for the error text, or whether the reply had a `refresh_token` field.
|
|
62
|
+
- Never run the exchange or a test call yourself. If a value lands in chat anyway, CLAUDE.md "When the user offers you a key" applies.
|