@assembly-js/app-foundation 0.0.2-rc.1 → 0.0.2-rc.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assembly-js/app-foundation",
3
- "version": "0.0.2-rc.1",
3
+ "version": "0.0.2-rc.3",
4
4
  "description": "Versioned foundation helpers and build context for Assembly generated apps",
5
5
  "type": "module",
6
6
  "files": [
@@ -355,7 +355,7 @@ Each action route is then just the handler, served at the record's `baseUrl`:
355
355
 
356
356
  ```ts
357
357
  // app/api/assembly/time-entries/route.ts — the record's baseUrl
358
- import { automationActionRoute, ActionError } from '@/lib/assembly-actions';
358
+ import { automationActionRoute, ActionError } from '@assembly-js/app-foundation/server-actions';
359
359
  import { db } from '@/lib/db';
360
360
  import { timeEntries } from '@/lib/schema';
361
361
 
@@ -423,7 +423,7 @@ Serve the route with the same wrapper as actions — its token check is what aut
423
423
 
424
424
  ```ts
425
425
  // app/api/assembly/projects/route.ts — the options record's baseUrl
426
- import { automationActionRoute } from '@/lib/assembly-actions';
426
+ import { automationActionRoute } from '@assembly-js/app-foundation/server-actions';
427
427
  import { db } from '@/lib/db';
428
428
  import { projects } from '@/lib/schema';
429
429
  import { eq } from 'drizzle-orm';
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: assembly-integrations
3
- description: Use BEFORE declaring requiredIntegrations in .assembly/app.json and before writing any call through getIntegration() from @/lib/nango (Slack and other OAuth providers — supported list inside). Covers manifest declaration, the broker client, per-provider gotchas (pagination, IDs vs names, membership/join), and ConnectorError handling.
3
+ description: Use BEFORE declaring requiredIntegrations in .assembly/app.json and before writing any call through getIntegration() from @assembly-js/app-foundation/server (Slack and other OAuth providers — supported list inside). Covers manifest declaration, the broker client, per-provider gotchas (pagination, IDs vs names, membership/join), and ConnectorError handling.
4
4
  ---
5
5
 
6
6
  ## How it works
7
7
 
8
8
  When your app needs to talk to a third-party provider's API, declare the provider in `requiredIntegrations` in `.assembly/app.json`. The workspace admin connects the provider once via an OAuth popup; Assembly stores the connection through Nango.
9
9
 
10
- At runtime you call the provider through `getIntegration(provider)` from `@/lib/nango`. Calls go to the Assembly **connector broker** over your app's own authenticated session — you never see, handle, or store OAuth tokens or any Nango credentials, and there are **no per-provider environment variables**. The broker resolves the workspace's connection server-side (from your app's identity) and relays the provider's response back to you.
10
+ At runtime you call the provider through `getIntegration(provider)` from `@assembly-js/app-foundation/server`. Calls go to the Assembly **connector broker** over your app's own authenticated session — you never see, handle, or store OAuth tokens or any Nango credentials, and there are **no per-provider environment variables**. The broker resolves the workspace's connection server-side (from your app's identity) and relays the provider's response back to you.
11
11
 
12
12
  The flow:
13
13
 
@@ -47,7 +47,7 @@ Add a top-level `requiredIntegrations` array to `.assembly/app.json`. Each entry
47
47
  `getIntegration` is **async** and must run **server-side, inside an authenticated request scope** — an API route handler, a server component, or a server action where `getAssembly()` can read the viewer bearer cookie. It authenticates to the broker as the current viewer the same way every other data call does — no `x-assembly-token` header, no token argument. Do **not** call it from a client component, from a background/cron job, or outside a user request — it will fail to authenticate. In practice: call it from a route handler invoked via `authFetch`, or from a server component/action that uses `getAssembly()`.
48
48
 
49
49
  ```typescript
50
- import { getIntegration } from '@/lib/nango';
50
+ import { getIntegration } from '@assembly-js/app-foundation/server';
51
51
  ```
52
52
 
53
53
  `getIntegration(provider)` resolves to a thin client:
@@ -69,7 +69,7 @@ Some action inputs are flagged above as **resource inputs**: the value is the ID
69
69
  ```typescript
70
70
  // app/api/slack/post/route.ts
71
71
  import { NextRequest, NextResponse } from 'next/server';
72
- import { getIntegration, slackPostMessage } from '@/lib/nango';
72
+ import { getIntegration, slackPostMessage } from '@assembly-js/app-foundation/server';
73
73
 
74
74
  export async function POST(request: NextRequest) {
75
75
  const { channel, text } = await request.json();
@@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
89
89
  ```typescript
90
90
  // app/api/slack/history/route.ts
91
91
  import { NextRequest, NextResponse } from 'next/server';
92
- import { getIntegration } from '@/lib/nango';
92
+ import { getIntegration } from '@assembly-js/app-foundation/server';
93
93
 
94
94
  export async function GET(request: NextRequest) {
95
95
  const channelId = request.nextUrl.searchParams.get('channelId') ?? '';
@@ -110,13 +110,13 @@ provider `Response`.
110
110
 
111
111
  When the UI needs the user to choose a provider-side resource (a Slack channel, a Drive folder, a spreadsheet, an Airtable base), do NOT hand-roll the combobox plumbing — the foundation ships both halves:
112
112
 
113
- - **`resolveResource(provider, resolverAction, { query?, cursor?, fields, queryInput?, cursorInput?, input? })`** from `@/lib/nango` — server-side. Runs the resolver action and normalizes the provider's shape to `{ id, label }` options plus `nextCursor`. It returns a serializable `ResourcePage` — broker failures come back as `{ ok: false, error: { code, provider } }` instead of throwing, so the result survives a Server Action boundary (a thrown error is redacted in production). `fields` maps the action's result shape (dot paths allowed) — read it off the action's catalog signature above. Pass `queryInput` only when the action has a server-side search input; otherwise each page is filtered by label locally.
113
+ - **`resolveResource(provider, resolverAction, { query?, cursor?, fields, queryInput?, cursorInput?, input? })`** from `@assembly-js/app-foundation/server` — server-side. Runs the resolver action and normalizes the provider's shape to `{ id, label }` options plus `nextCursor`. It returns a serializable `ResourcePage` — broker failures come back as `{ ok: false, error: { code, provider } }` instead of throwing, so the result survives a Server Action boundary (a thrown error is redacted in production). `fields` maps the action's result shape (dot paths allowed) — read it off the action's catalog signature above. Pass `queryInput` only when the action has a server-side search input; otherwise each page is filtered by label locally.
114
114
  - **`<ResourcePicker>`** from `@/app/components/resource-picker` — client-side searchable combobox with debounced search, cursor pagination ("Load more"), loading/empty states, and connector-error surfaces (`needs_reauth` renders a reconnect hint instead of an empty list). Store `value.id`, render `value.label`.
115
115
 
116
116
  ```typescript
117
117
  // app/actions/channels.ts
118
118
  'use server';
119
- import { resolveResource } from '@/lib/nango';
119
+ import { resolveResource } from '@assembly-js/app-foundation/server';
120
120
 
121
121
  export async function searchChannels(args: { query: string; cursor?: string }) {
122
122
  // list-channels returns { conversations, next_cursor? } and has no query
@@ -131,7 +131,7 @@ export async function searchChannels(args: { query: string; cursor?: string }) {
131
131
  ```tsx
132
132
  // client component
133
133
  import { ResourcePicker } from '@/app/components/resource-picker';
134
- import type { ResourceOption } from '@/lib/resource-resolution';
134
+ import type { ResourceOption } from '@assembly-js/app-foundation/client';
135
135
 
136
136
  const [channel, setChannel] = useState<ResourceOption | null>(null);
137
137
  <ResourcePicker fetchPage={searchChannels} value={channel} onValueChange={setChannel} placeholder="Search channels…" />
@@ -139,13 +139,13 @@ const [channel, setChannel] = useState<ResourceOption | null>(null);
139
139
 
140
140
  ## Slack specifics
141
141
 
142
- - **Post messages with `slackPostMessage` from `@/lib/nango` — never hand-roll `chat.postMessage`.** It resolves a `#name` to the channel ID, posts, and on `not_in_channel` joins the channel and retries once. Slack-level failures come back as `{ ok: false, error }`; on `not_in_channel` render "invite the bot to #channel in Slack" (private channels cannot be self-joined).
143
- - **Resolve channel names with `slackFindChannelId` from `@/lib/nango` for every other per-channel call — never hand-roll the lookup.** It handles cursor pagination (a single `conversations.list` call misses channels in large workspaces), name normalization, scope fallback, rate limits, and caching. Use the returned ID for every follow-up call; most Slack methods reject a `#name` with `invalid_arguments`.
142
+ - **Post messages with `slackPostMessage` from `@assembly-js/app-foundation/server` — never hand-roll `chat.postMessage`.** It resolves a `#name` to the channel ID, posts, and on `not_in_channel` joins the channel and retries once. Slack-level failures come back as `{ ok: false, error }`; on `not_in_channel` render "invite the bot to #channel in Slack" (private channels cannot be self-joined).
143
+ - **Resolve channel names with `slackFindChannelId` from `@assembly-js/app-foundation/server` for every other per-channel call — never hand-roll the lookup.** It handles cursor pagination (a single `conversations.list` call misses channels in large workspaces), name normalization, scope fallback, rate limits, and caching. Use the returned ID for every follow-up call; most Slack methods reject a `#name` with `invalid_arguments`.
144
144
  - **Writes need membership, not just visibility.** `slackPostMessage` handles this for posting. For any other write (reactions, uploads), a `not_in_channel` error means join first: the `join-channel` action with the channel ID, then retry once. Private channels cannot be self-joined — they fail `method_not_supported_for_channel_type`, so tell the user to invite the bot instead of retrying.
145
145
  - **Scopes:** `chat:write.public` lets posting reach any public channel without joining it, so only the other writes need `join-channel`. Add `groups:read` if the app must find private channels.
146
146
 
147
147
  ```typescript
148
- import { getIntegration, slackFindChannelId } from '@/lib/nango';
148
+ import { getIntegration, slackFindChannelId } from '@assembly-js/app-foundation/server';
149
149
 
150
150
  const slack = await getIntegration('slack');
151
151
  const channelId = await slackFindChannelId(slack, 'general');
@@ -163,7 +163,7 @@ A connector call throws a **`ConnectorError`** (with `.code`, `.provider`, `.sta
163
163
  - `invalid_request` — malformed relay input (bad action name, unreadable body). Fix the call site.
164
164
 
165
165
  ```typescript
166
- import { getIntegration } from '@/lib/nango';
166
+ import { getIntegration } from '@assembly-js/app-foundation/server';
167
167
 
168
168
  try {
169
169
  const slack = await getIntegration('slack');
@@ -178,10 +178,10 @@ try {
178
178
 
179
179
  ### Checking whether a provider is connected
180
180
 
181
- To gate UI on connectivity ("show a Connect prompt if Slack isn't connected"), use the **`isConnected`** helper from `@/lib/nango`. It asks the broker's read-only status endpoint for the stored connection state — **no provider call is made**, so it is cheap, has no side effects, and an unrelated call failure can never masquerade as "not connected". Do **not** probe by making a provider call and branching on whether it succeeded — a provider `404`/`400`/`500`, `action_failed`, or `connector_unavailable` all mean the connection **exists** and that call merely failed.
181
+ To gate UI on connectivity ("show a Connect prompt if Slack isn't connected"), use the **`isConnected`** helper from `@assembly-js/app-foundation/server`. It asks the broker's read-only status endpoint for the stored connection state — **no provider call is made**, so it is cheap, has no side effects, and an unrelated call failure can never masquerade as "not connected". Do **not** probe by making a provider call and branching on whether it succeeded — a provider `404`/`400`/`500`, `action_failed`, or `connector_unavailable` all mean the connection **exists** and that call merely failed.
182
182
 
183
183
  ```typescript
184
- import { isConnected } from '@/lib/nango';
184
+ import { isConnected } from '@assembly-js/app-foundation/server';
185
185
 
186
186
  const slackConnected = await isConnected('slack');
187
187
  ```
@@ -189,7 +189,7 @@ const slackConnected = await isConnected('slack');
189
189
  When the UI needs to distinguish *why* a provider isn't usable (render "Connect" vs "Reconnect"), read the full state from the client's **`status()`** method instead — it returns `{ provider, connected, status }` where `status` is `connected`, `connection_required`, `needs_reauth`, or `needs_reauth_scope`:
190
190
 
191
191
  ```typescript
192
- import { getIntegration } from '@/lib/nango';
192
+ import { getIntegration } from '@assembly-js/app-foundation/server';
193
193
 
194
194
  const slack = await getIntegration('slack');
195
195
  const { connected, status } = await slack.status();
@@ -106,11 +106,11 @@ await assembly.createNotification({
106
106
  });
107
107
  ```
108
108
 
109
- The examples here are **request-scoped** — a user is present, so `getAssembly()` reads the bearer from the request cookie and acts as that viewer. For a **background send with no user in the loop** (a scheduled job or an incoming webhook), there is no bearer cookie: call `getOfflineAssembly()` from `@/lib/assembly`, which mints an app-scoped **offline bearer** from your API key server-side. In that path `getSessionIdentity()` returns `null`, so choose a `senderId` explicitly (e.g. a system/admin internal user) rather than reading it from identity.
109
+ The examples here are **request-scoped** — a user is present, so `getAssembly()` reads the bearer from the request cookie and acts as that viewer. For a **background send with no user in the loop** (a scheduled job or an incoming webhook), there is no bearer cookie: call `getOfflineAssembly()` from `@assembly-js/app-foundation/server`, which mints an app-scoped **offline bearer** from your API key server-side. In that path `getSessionIdentity()` returns `null`, so choose a `senderId` explicitly (e.g. a system/admin internal user) rather than reading it from identity.
110
110
 
111
111
  ```ts
112
112
  // app/api/cron/reminders/route.ts — background send, no viewer
113
- import { getOfflineAssembly } from '@/lib/assembly';
113
+ import { getOfflineAssembly } from '@assembly-js/app-foundation/server';
114
114
 
115
115
  export async function GET() {
116
116
  const assembly = await getOfflineAssembly(); // app-scoped bearer, no cookie