@assembly-js/app-foundation 0.14.2 → 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 CHANGED
@@ -19,6 +19,9 @@ 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
+
22
25
  ## 0.14.2
23
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.
24
27
 
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-js/app-foundation",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -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.