@cosmoops/connect-sdk 0.7.0 → 0.9.0

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +273 -91
  3. package/dist/cjs/client/components/access-gate.js +39 -4
  4. package/dist/cjs/client/components/ai-format.js +135 -0
  5. package/dist/cjs/client/components/ai-widgets.js +232 -0
  6. package/dist/cjs/client/components/app-shell.js +7 -6
  7. package/dist/cjs/client/components/ask-ai.js +334 -0
  8. package/dist/cjs/client/components/nav.js +1 -1
  9. package/dist/cjs/client/components/side-nav.js +1 -1
  10. package/dist/cjs/client/components/user-menu.js +28 -5
  11. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  12. package/dist/cjs/client/index.js +31 -3
  13. package/dist/cjs/client/provider.js +45 -2
  14. package/dist/cjs/server/client.js +1 -1
  15. package/dist/cjs/server/handler.js +43 -0
  16. package/dist/cjs/server/http.js +46 -24
  17. package/dist/cjs/server/resources/entitlements.js +41 -0
  18. package/dist/cjs/server/resources/identity.js +2 -0
  19. package/dist/esm/client/components/access-gate.js +40 -5
  20. package/dist/esm/client/components/ai-format.js +129 -0
  21. package/dist/esm/client/components/ai-widgets.js +223 -0
  22. package/dist/esm/client/components/app-shell.js +6 -5
  23. package/dist/esm/client/components/ask-ai.js +331 -0
  24. package/dist/esm/client/components/nav.js +1 -1
  25. package/dist/esm/client/components/side-nav.js +1 -1
  26. package/dist/esm/client/components/user-menu.js +28 -5
  27. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  28. package/dist/esm/client/index.js +13 -2
  29. package/dist/esm/client/provider.js +43 -2
  30. package/dist/esm/server/client.js +1 -1
  31. package/dist/esm/server/handler.js +43 -0
  32. package/dist/esm/server/http.js +46 -24
  33. package/dist/esm/server/resources/entitlements.js +41 -0
  34. package/dist/esm/server/resources/identity.js +2 -0
  35. package/dist/theme.css +87 -0
  36. package/dist/types/client/components/access-gate.d.ts +17 -3
  37. package/dist/types/client/components/ai-format.d.ts +24 -0
  38. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  39. package/dist/types/client/components/app-shell.d.ts +21 -5
  40. package/dist/types/client/components/ask-ai.d.ts +73 -0
  41. package/dist/types/client/components/nav.d.ts +3 -3
  42. package/dist/types/client/components/side-nav.d.ts +1 -1
  43. package/dist/types/client/components/user-menu.d.ts +2 -1
  44. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  45. package/dist/types/client/index.d.ts +10 -3
  46. package/dist/types/client/provider.d.ts +44 -1
  47. package/dist/types/server/client.d.ts +1 -1
  48. package/dist/types/server/http.d.ts +6 -1
  49. package/dist/types/server/resources/entitlements.d.ts +33 -1
  50. package/dist/types/types.d.ts +174 -0
  51. package/package.json +8 -2
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # @cosmoops/connect-sdk
2
2
 
3
- Official client for the **CosmoOps Connect service-partner API** (`/api/v1/service/*`).
3
+ Official client for the **CosmoOps Connect application-partner API** (`/api/v1/application/*`).
4
+
5
+ **Current version: `0.8.0`** · [Release notes](#release-notes) · [Full changelog](./CHANGELOG.md)
6
+ — follows semver with the 0.x convention that a **minor** version may carry a
7
+ breaking change (a patch never does). Read the release notes for your target
8
+ version before upgrading past a minor bump.
4
9
 
5
10
  ```bash
6
11
  npm install @cosmoops/connect-sdk
12
+ npm install streamdown # optional — Ask AI's markdown renderer, see step 4
7
13
  ```
8
14
 
9
15
  Two entry points, and the split is a security boundary, not a preference:
@@ -23,9 +29,114 @@ Browser ──► your backend ──► Connect
23
29
  (client) (createConnectHandler + server client, holds the key)
24
30
  ```
25
31
 
32
+ ## Quickstart (Next.js)
33
+
34
+ The fast path — most integrations are this and nothing else. Not on Next.js?
35
+ Skip to [Other frameworks](#other-frameworks); the shape is the same, you
36
+ just wire the session cookie yourself.
37
+
38
+ **1. Install and get credentials.** From your application's Integration tab
39
+ in Connect, copy the API key and OAuth client id.
40
+
41
+ ```bash
42
+ npm install @cosmoops/connect-sdk
43
+ ```
44
+
45
+ **2. Set environment variables.**
46
+
47
+ | Variable | Required | Purpose |
48
+ | --- | --- | --- |
49
+ | `CONNECT_API_KEY` | yes | server-side credential |
50
+ | `CONNECT_CLIENT_ID` | yes | OAuth client id |
51
+ | `CONNECT_APP_URL` | no | non-production Connect instance |
52
+ | `CONNECT_SESSION_COOKIE` | no | session cookie name |
53
+
54
+ **3. Mount the bridge route.** One file, no arguments — this is your entire backend integration:
55
+
56
+ ```ts
57
+ // app/api/connect/[...connect]/route.ts
58
+ export { GET, POST } from "@cosmoops/connect-sdk/next";
59
+ ```
60
+
61
+ **4. Wrap your layout and import the theme.**
62
+
63
+ ```tsx
64
+ // app/layout.tsx
65
+ import { ConnectProvider } from "@cosmoops/connect-sdk/client";
66
+
67
+ <ConnectProvider>{children}</ConnectProvider>
68
+ ```
69
+
70
+ ```css
71
+ /* app/globals.css */
72
+ @import "tailwindcss";
73
+ @import "@heroui/styles";
74
+ @import "@cosmoops/connect-sdk/theme.css";
75
+
76
+ /* Ask AI's markdown renderer. Tailwind skips node_modules unless told, and
77
+ Streamdown's utility classes live inside its compiled bundle. Adjust the
78
+ `../` count to reach your own node_modules. */
79
+ @source "../node_modules/streamdown/dist/*.js";
80
+ ```
81
+
82
+ ```tsx
83
+ // app/layout.tsx — Streamdown's streaming animation keyframes
84
+ import "streamdown/styles.css";
85
+ ```
86
+
87
+ Skip the last two if you pass `askAi={false}` to `AppShell`. The colour tokens
88
+ Streamdown expects are already mapped onto HeroUI's in `theme.css`.
89
+
90
+ **5. Register the redirect URI.** In the application's Integration tab, add
91
+ `{your origin}/api/connect/callback`. This is the one step the SDK cannot do
92
+ for you.
93
+
94
+ **6. Read the session wherever you need it.**
95
+
96
+ ```tsx
97
+ // app/dashboard/page.tsx
98
+ import { requireConnectSession } from "@cosmoops/connect-sdk/next";
99
+
100
+ export default async function DashboardPage() {
101
+ const session = await requireConnectSession(); // UserResponse — redirects if signed out
102
+ return <h1>{session.user.workspaceName}</h1>;
103
+ }
104
+ ```
105
+
106
+ **7. Verify the wiring.**
107
+
108
+ ```bash
109
+ npx @cosmoops/connect-sdk doctor
110
+ ```
111
+
112
+ That's the whole round trip — `/signin`, `/callback`, an httpOnly session
113
+ cookie, `/session`, `/entitlements`, `/plans`, `/addons`, `/apps`,
114
+ `/sessions`, `/logout` — with no `next.config.ts` changes, including
115
+ `transpilePackages`: `dist/` is already ES2022 with `"use client"` hoisted,
116
+ so Turbopack and webpack both consume it as-is.
117
+
118
+ `npx @cosmoops/connect-sdk init` writes step 3's route file for you, plus a
119
+ `/plans` page and an `.env.local` template for step 2 — then prints steps 4
120
+ and 5 as the ones it won't do on your behalf, since those touch your own
121
+ layout and Connect's dashboard rather than files it can safely write. See
122
+ [Scaffolding](#scaffolding) below.
123
+
124
+ Two things most integrations add next:
125
+
126
+ - **A shell** — `AppShell` from `/client` is the sidebar, top bar and access
127
+ gate every application platform needs. See [App shell](#app-shell).
128
+ - **Route protection before render** — see [Route protection and
129
+ `proxy.ts`](#route-protection-and-proxyts) below; `requireConnectSession()`
130
+ above already redirects a signed-out visitor, so you only need this for
131
+ visitors you want bounced *before* a page starts rendering.
132
+
133
+ The sections below this point cover the manual path — rolling the OAuth round
134
+ trip yourself for a non-Next backend, or understanding what `/next` is doing
135
+ for you under the hood.
136
+
26
137
  ## Server setup
27
138
 
28
- One credential does three jobs — the API key plaintext from your service's Integration tab is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
139
+ One credential does three jobs — the API key plaintext from your application's Integration tab is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
29
140
 
30
141
  ```ts
31
142
  import { ConnectClient } from "@cosmoops/connect-sdk/server";
@@ -81,6 +192,49 @@ const { user, serviceRole } = await connect.identity.getUser(sessionToken);
81
192
  if (!serviceRole.granted.includes("invoice:write")) return forbidden();
82
193
  ```
83
194
 
195
+ ## Know which kind of account signed in
196
+
197
+ `user.workspaceType` is `"individual"` (a one-seat personal account) or
198
+ `"business"` (a team). It is the one fact about the workspace you cannot look
199
+ up yourself — this API has no workspace read — and it decides whether half
200
+ your UI is real: an individual workspace has exactly one seat and can never
201
+ gain another, so a members table, an invite button and a seat meter can only
202
+ ever fail there.
203
+
204
+ `audience` answers the other half — what Connect's catalogue says your
205
+ application is *sold* to, and whether this account qualifies:
206
+
207
+ ```ts
208
+ const { user, audience } = await connect.identity.getUser(sessionToken);
209
+
210
+ user.workspaceType; // "individual"
211
+ audience.application; // "b2b"
212
+ audience.matches; // false — readiness will block, subscribing is refused
213
+ ```
214
+
215
+ Both ship together so the check stays `!audience.matches` rather than a
216
+ hardcoded audience constant in your own code that goes stale the moment an
217
+ operator changes the catalogue entry. `audience.enforcedAtSignIn` says whether
218
+ Connect refuses the hand-off itself; when it does, a mismatch only ever reaches
219
+ you on a session minted before that setting was turned on.
220
+
221
+ A mismatch is not a reason to sign anyone out. They have a valid Connect
222
+ account — just the wrong kind of one for this application. `<ConnectAccessGate>`
223
+ already blocks the shell on it.
224
+
225
+ In the browser, the same two facts are `useConnectWorkspaceType()` and
226
+ `useConnectAudience()`, and `<ConnectWorkspaceTypeGate>` is the declarative form:
227
+
228
+ ```tsx
229
+ <ConnectWorkspaceTypeGate type="business">
230
+ <TeamMembersTable />
231
+ </ConnectWorkspaceTypeGate>
232
+ ```
233
+
234
+ That gate varies what each account type sees *inside* an application both
235
+ audiences can reach. It is not the audience gate and does not replace
236
+ `<ConnectAccessGate>`.
237
+
84
238
  **4. On logout.**
85
239
 
86
240
  ```ts
@@ -135,14 +289,14 @@ default: worth it for an app shell, less so for a marketing page.
135
289
  ```ts
136
290
  import { blockingBlockers } from "@cosmoops/connect-sdk"; // isomorphic
137
291
 
138
- const readiness = await connect.entitlements.getReadiness(orgId);
292
+ const readiness = await connect.entitlements.getReadiness(workspaceId);
139
293
 
140
294
  if (!readiness.ready) {
141
295
  const [blocker] = blockingBlockers(readiness);
142
296
  return showBlocked(blocker.message, blocker.resolveUrl); // resolveUrl links into Connect
143
297
  }
144
298
 
145
- if (await connect.entitlements.hasFeature(orgId, "campaign-module")) {
299
+ if (await connect.entitlements.hasFeature(workspaceId, "campaign-module")) {
146
300
  // ...
147
301
  }
148
302
  ```
@@ -153,7 +307,7 @@ Branch on `blocker.code`, never on `message`. Treat an unrecognised code as bloc
153
307
 
154
308
  ```ts
155
309
  const { balance } = await connect.billing.consumeAddon({
156
- organizationId: orgId,
310
+ workspaceId,
157
311
  addonId: "api-calls",
158
312
  quantity: 1,
159
313
  });
@@ -204,7 +358,7 @@ await client.logs.write({
204
358
  });
205
359
  ```
206
360
 
207
- Both scopes on the stored entry come from something verified, never from your payload: `serviceId` from your API key, and `userId` from this token — which Connect checks is unrevoked, unexpired, and **minted for your service** before attributing anything. **There is no `userId` field to send.** An actor a caller can type is not an actor worth recording, and a conformance test fails the build if one ever appears in the server schema.
361
+ Both scopes on the stored entry come from something verified, never from your payload: `applicationId` from your API key, and `userId` from this token — which Connect checks is unrevoked, unexpired, and **minted for your application** before attributing anything. **There is no `userId` field to send.** An actor a caller can type is not an actor worth recording, and a conformance test fails the build if one ever appears in the server schema.
208
362
 
209
363
  `write` returns `{ delivered, attributed }` and never throws. `attributed: false` with `delivered: true` means the token had expired (~15 days) or been revoked and the line was stored *without* a user — deliberately kept rather than rejected, because losing telemetry over a failed attribution is the worse of the two outcomes.
210
364
 
@@ -221,73 +375,20 @@ new ConnectClient({
221
375
  });
222
376
  ```
223
377
 
224
- ## Next.js — plug and play
378
+ ## Next.js — reading the session
225
379
 
226
- Two files, no arguments.
380
+ The setup is [Quickstart](#quickstart-nextjs) above; this is the detail behind
381
+ its step 6 — `getConnectSession()` / `requireConnectSession()` beyond the
382
+ one-liner shown there.
227
383
 
228
- ```ts
229
- // app/api/connect/[...connect]/route.ts
230
- export { GET, POST } from "@cosmoops/connect-sdk/next";
231
- ```
384
+ `requireConnectSession()` is `getConnectSession()` plus the check-and-redirect.
385
+ Reach for the plain version when a missing session isn't an error — an
386
+ optional account badge in a header, say:
232
387
 
233
388
  ```tsx
234
- // app/layout.tsx
235
- import { ConnectProvider } from "@cosmoops/connect-sdk/client";
236
-
237
- <ConnectProvider>{children}</ConnectProvider>
238
- ```
239
-
240
- ```css
241
- /* app/globals.css */
242
- @import "tailwindcss";
243
- @import "@heroui/styles";
244
- @import "@cosmoops/connect-sdk/theme.css";
245
- ```
246
-
247
- `npx @cosmoops/connect-sdk init` writes the route file and a `/plans` page for
248
- you; the provider and the stylesheet are the two it won't edit on your behalf.
249
-
250
- That gives you the whole round trip — `/signin`, `/callback`, an httpOnly session
251
- cookie, `/session`, `/entitlements`, `/plans`, `/addons`, `/apps`, `/sessions`,
252
- `/logout`. Config comes from the environment:
253
-
254
- | Variable | Required | Purpose |
255
- | --- | --- | --- |
256
- | `CONNECT_API_KEY` | yes | server-side credential |
257
- | `CONNECT_CLIENT_ID` | yes | OAuth client id |
258
- | `CONNECT_APP_URL` | no | non-production Connect instance |
259
- | `CONNECT_SESSION_COOKIE` | no | session cookie name |
260
-
261
- Register `{your origin}/api/connect/callback` as a redirect URI in the service's
262
- Integration tab — the one step this cannot do for you.
263
-
264
- No `next.config.ts` changes needed, including `transpilePackages`: `dist/` is
265
- already ES2022 with `"use client"` hoisted, so Turbopack and webpack both
266
- consume it as-is.
267
-
268
- Reading the session server-side — no argument, no `headers()` dance:
269
-
270
- ```tsx
271
- // app/dashboard/page.tsx
272
389
  import { getConnectSession } from "@cosmoops/connect-sdk/next";
273
- import { redirect } from "next/navigation";
274
390
 
275
- export default async function DashboardPage() {
276
- const session = await getConnectSession(); // UserResponse | null
277
- if (!session) redirect("/api/connect/signin?next=/dashboard");
278
- return <h1>{session.user.organizationName}</h1>;
279
- }
280
- ```
281
-
282
- Or skip the check-and-redirect entirely — `requireConnectSession()` does both:
283
-
284
- ```tsx
285
- import { requireConnectSession } from "@cosmoops/connect-sdk/next";
286
-
287
- export default async function DashboardPage() {
288
- const session = await requireConnectSession(); // UserResponse — redirects if signed out
289
- return <h1>{session.user.organizationName}</h1>;
290
- }
391
+ const session = await getConnectSession(); // UserResponse | null
291
392
  ```
292
393
 
293
394
  Pass a `Request` — `getConnectSession(request)` — anywhere `next/headers` does
@@ -303,9 +404,6 @@ It reads `cookies()` transitively, so under `cacheComponents` it belongs inside
303
404
  a `<Suspense>` boundary like any other request-scoped read. A page that awaits
304
405
  it above every boundary gives up its static shell.
305
406
 
306
- You do **not** need `transpilePackages`. `dist/` is ES2022 with `"use client"`
307
- already hoisted, so Turbopack and webpack consume it as-is.
308
-
309
407
  ## Route protection and `proxy.ts`
310
408
 
311
409
  Nothing here is auto-wired, and nothing needs to be: this SDK ships no proxy and
@@ -340,11 +438,24 @@ export const config = { matcher: ["/dashboard/:path*", "/settings/:path*"] };
340
438
 
341
439
  **A cookie is not a session, and this check is not authorization.** The cookie
342
440
  holds an opaque Connect `sessionToken`; deciding whether it is still valid means
343
- asking Connect, and proxy runs on every matched request — so a real check here
344
- would put a network round trip in front of each one. Next's own guidance says
345
- the same thing: proxy is for [optimistic
346
- checks](https://nextjs.org/docs/app/guides/authentication#optimistic-checks-with-proxy-optional),
347
- "not ... a full session management or authorization solution."
441
+ asking Connect, and proxy runs on every matched request — including
442
+ [prefetched](https://nextjs.org/docs/app/getting-started/linking-and-navigating#prefetching)
443
+ ones — so a real check here would put a network round trip in front of far
444
+ more requests than the visitor actually makes.
445
+
446
+ This isn't just this SDK's opinion — it's what Next.js's own [Authentication
447
+ guide](https://nextjs.org/docs/app/guides/authentication#optimistic-checks-with-proxy-optional)
448
+ recommends, word for word (verified against the docs bundled with the Next
449
+ version in this repo's `node_modules`, since the hosted docs move faster than
450
+ any one install):
451
+
452
+ > Since Proxy runs on every route, including prefetched routes, it's important
453
+ > to only read the session from the cookie (optimistic checks), and avoid
454
+ > database checks to prevent performance issues.
455
+
456
+ The snippet above is exactly that: a presence check, nothing more. Next is
457
+ equally direct that this is as far as it goes — optimistic checks are "not
458
+ \[...\] a full session management or authorization solution."
348
459
 
349
460
  An expired or revoked token still arrives as a cookie and still passes the
350
461
  snippet above. The authoritative check is `getConnectSession()` in the Server
@@ -405,13 +516,13 @@ that URL yourself instead.
405
516
 
406
517
  ```tsx
407
518
  import {
408
- ConnectAppShell,
519
+ AppShell,
409
520
  ConnectLoginButton, ConnectRegisterButton,
410
521
  ConnectUserMenu, ConnectAppsGrid, ConnectPlans,
411
522
  } from "@cosmoops/connect-sdk/client";
412
523
 
413
- // The whole frame — sidebar, top bar, access gate. See "App shell" below.
414
- <ConnectAppShell name="Dispatch" nav={nav}>{children}</ConnectAppShell>
524
+ // The whole frame — sidebar, top bar, access gate, Ask AI. See "App shell" below.
525
+ <AppShell name="Dispatch" nav={nav}>{children}</AppShell>
415
526
 
416
527
  // Or the pieces, if you already have a layout. The pair swaps automatically
417
528
  // on sign-in state.
@@ -425,7 +536,7 @@ import {
425
536
 
426
537
  | Component | What it does |
427
538
  | --- | --- |
428
- | `ConnectAppShell` | **The whole application frame** — collapsible sidebar, top bar with the launcher and account menu, and your pages in the middle. Mounts `ConnectAccessGate` for you. |
539
+ | `AppShell` | **The whole application frame** — collapsible sidebar, top bar with the launcher and account menu, and your pages in the middle. Mounts `ConnectAccessGate` for you. |
429
540
  | `ConnectSideNav` | The sidebar's nav column on its own — sections, one level of children, active-route highlight — for an app that already has its own chrome. |
430
541
  | `ConnectLoginButton` · `ConnectRegisterButton` | "Continue with Connect", in the shape people know from Google. Render nothing when signed in. |
431
542
  | `ConnectUserMenu` | Avatar, name, workspace. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. |
@@ -433,11 +544,14 @@ import {
433
544
  | `ConnectPlans` | Monthly/annual toggle, features, add-ons, live discount, coupon field, and the subscribe / upgrade / downgrade / cancel flow. |
434
545
  | `ConnectPlansPage` | A whole `/plans` page — readiness banner, current subscription, and the grid. What `connect-sdk init` scaffolds. |
435
546
  | `ConnectSubscribeDialog` | The review step `ConnectPlans` opens — plan, GST, what happens to the current cycle. Exported so you can drive the flow from your own pricing page. |
547
+ | `ConnectAskAi` | **Ask AI** — a chat panel grounded in the workspace's Context Index, streamed through your own bridge. Answers draw as well as write: tables, charts, metric tiles. Mounted by `AppShell` by default. |
548
+ | `AiTable` · `AiChart` · `AiMetrics` · `AiTimeline` · `AiDetails` · `AiSteps` · `AiSources` | The seven widgets an Ask AI answer can draw, exported for rendering Connect's chat somewhere other than the panel. |
436
549
  | `ConnectAccessGate` | **The one gate to mount if you mount one.** Composes suspended / no-plan / onboarding-pending / payment-due into a single priority-ordered decision. Wrap your app content once. |
437
550
  | `ConnectSubscriptionGate` | Renders your module for a subscribed workspace, and `ConnectNoSubscriptionCard` for everyone else. Never flashes the upsell while loading. |
438
551
  | `ConnectNoSubscriptionCard` · `ConnectSubscriptionCard` | The same slot, unsubscribed and subscribed. Plan, status, renewal date, seats, and one way into Connect. |
439
- | `ConnectReadinessBanner` · `ConnectReadinessGate` | Every gate between this workspace and your service — KYC, payment method, seat headroom — each with the Connect screen that fixes it. |
552
+ | `ConnectReadinessBanner` · `ConnectReadinessGate` | Every gate between this workspace and your application — KYC, payment method, seat headroom — each with the Connect screen that fixes it. |
440
553
  | `ConnectFeatureGate` | Renders a module only when `plan.features` includes its key. |
554
+ | `ConnectWorkspaceTypeGate` | Renders a subtree only for an `"individual"` or `"business"` workspace — for dead controls, not access. |
441
555
  | `ConnectTrialBanner` | A running trial and the date it converts. Renders nothing otherwise. |
442
556
  | `ConnectAccountSessions` | Every device signed into this Connect account, with revoke. |
443
557
  | `ConnectUsageMeter` | Remaining balance of each consumable add-on, with overage and carry-forward. |
@@ -446,12 +560,12 @@ import {
446
560
 
447
561
  ### Hooks
448
562
 
449
- `useConnect` · `useConnectUser` · `useActivePath` · `useConnectAccessGate` · `useConnectEntitlements` · `useConnectPlans` · `useConnectApps` · `useConnectAddonUsage` · `useConnectSessions` · `useConnectResource` · `useConnectFeature` · `useConnectCheckout`
563
+ `useConnect` · `useConnectUser` · `useConnectWorkspaceType` · `useConnectAudience` · `useActivePath` · `useConnectAccessGate` · `useConnectEntitlements` · `useConnectPlans` · `useConnectApps` · `useConnectAddonUsage` · `useConnectSessions` · `useConnectResource` · `useConnectFeature` · `useConnectCheckout`
450
564
 
451
565
  ### Metering
452
566
 
453
567
  ```tsx
454
- await client.billing.consumeAddon({ organizationId, addonId: "addon_sms", quantity: 1 }); // server
568
+ await client.billing.consumeAddon({ workspaceId, addonId: "addon_sms", quantity: 1 }); // server
455
569
  <ConnectUsageMeter /> // browser
456
570
  ```
457
571
 
@@ -461,7 +575,7 @@ Invoices stay a deep link (`ConnectInvoicesButton`). A GST tax invoice is a lega
461
575
 
462
576
  ### Plan features: label vs key
463
577
 
464
- The catalogue read (`useConnectPlans`, `<ConnectPlans />`) reports `features` as `{ key, name, description }` — **render `name`, gate on `key`**. The subscription read (`useConnectEntitlements`, `hasFeature`, `<ConnectFeatureGate>`) reports `features` as a bare `string[]` of keys, and stays that way: that one is a contract your backend compares against, and a display label is neither stable nor comparable. `name` falls back to the key for a plan older than its service's feature catalog.
578
+ The catalogue read (`useConnectPlans`, `<ConnectPlans />`) reports `features` as `{ key, name, description }` — **render `name`, gate on `key`**. The subscription read (`useConnectEntitlements`, `hasFeature`, `<ConnectFeatureGate>`) reports `features` as a bare `string[]` of keys, and stays that way: that one is a contract your backend compares against, and a display label is neither stable nor comparable. `name` falls back to the key for a plan older than its application's feature catalog.
465
579
 
466
580
  ### Gates
467
581
 
@@ -481,7 +595,7 @@ the order yourself:
481
595
  | State | What renders |
482
596
  | --- | --- |
483
597
  | `suspended` | A hard stop. Nothing the customer does clears it — only a Connect admin reactivating the workspace. |
484
- | `no_subscription` | "Choose a plan", linking to Connect's subscribe page for your service. |
598
+ | `no_subscription` | "Choose a plan", linking to Connect's subscribe page for your application. |
485
599
  | `onboarding_pending` | Waiting on a reviewer. No action offered, because there is none. |
486
600
  | `blocked` | Any other blocking readiness code — verification, audience, seats — each with its own fix. |
487
601
  | `payment_retry` | **Your app still renders.** A dismissible banner counts down to the dunning deadline. |
@@ -519,7 +633,7 @@ The narrower gates still exist for gating *within* a cleared workspace:
519
633
 
520
634
  All of them render `loading` — never the fallback — while the answer is unknown, so a paying customer is never flashed an upsell for something they already have.
521
635
 
522
- **They are UX gates, not security boundaries.** Everything they read reaches your own origin and a browser can ignore it. The authoritative check is on your server: `client.entitlements.getReadiness(orgId)` and `client.entitlements.hasFeature(orgId, feature)`.
636
+ **They are UX gates, not security boundaries.** Everything they read reaches your own origin and a browser can ignore it. The authoritative check is on your server: `client.entitlements.getReadiness(workspaceId)` and `client.entitlements.hasFeature(workspaceId, feature)`.
523
637
 
524
638
  ### Scaffolding
525
639
 
@@ -579,12 +693,12 @@ Cancelling uses the same machinery via `buildCancelUrl`. There is no *resume*: C
579
693
  "use client";
580
694
  import Link from "next/link";
581
695
  import { usePathname } from "next/navigation";
582
- import { ConnectAppShell } from "@cosmoops/connect-sdk/client";
696
+ import { AppShell } from "@cosmoops/connect-sdk/client";
583
697
  import { LayoutDashboard, Receipt, Settings, Truck } from "lucide-react";
584
698
 
585
699
  export function Shell({ children }: { children: React.ReactNode }) {
586
700
  return (
587
- <ConnectAppShell
701
+ <AppShell
588
702
  name="Dispatch"
589
703
  icon={<Truck />}
590
704
  activePath={usePathname()}
@@ -610,12 +724,12 @@ export function Shell({ children }: { children: React.ReactNode }) {
610
724
  ]}
611
725
  >
612
726
  {children}
613
- </ConnectAppShell>
727
+ </AppShell>
614
728
  );
615
729
  }
616
730
  ```
617
731
 
618
- That is the entire layout of a service platform. Everything that should look
732
+ That is the entire layout of a application platform. Everything that should look
619
733
  identical across the family — the lockup, the rail geometry, the active-route
620
734
  treatment, the app launcher, the account menu, the mobile drawer — comes from
621
735
  here; only `name`, `icon` and `nav` differ per platform.
@@ -627,7 +741,7 @@ plain `<a>`, which works everywhere and costs a full page load. Pass
627
741
  `window.location` and patches `history.pushState` to hear about client-side
628
742
  navigations, which is guesswork your router never has to do.
629
743
 
630
- **The access gate is on by default.** `ConnectAppShell` is exactly the "wrap
744
+ **The access gate is on by default.** `AppShell` is exactly the "wrap
631
745
  your app content once" place `ConnectAccessGate` asks for, so it mounts one
632
746
  around `children` — an app that ships a shell and forgets the gate is an app
633
747
  that lets a suspended workspace keep working. The chrome is never gated: the
@@ -644,6 +758,17 @@ page that sells them one, and is told to choose a plan. Pass `gate={false}` to
644
758
  opt out entirely, or `gateExemptRoutes` for a path that is not in the sidebar
645
759
  at all.
646
760
 
761
+ **Ask AI is on by default too**, for the same reason: a family of application
762
+ platforms should answer questions the same way, and a feature every partner has
763
+ to remember to switch on is a feature most of them do not have. It stays inert
764
+ until it can work — nothing renders for a signed-out visitor, and a workspace
765
+ whose Context Index is off gets a message in the panel rather than a broken
766
+ answer. Pass `askAi={false}` to leave it out, or an object to override its
767
+ heading, copy or markdown plugins.
768
+
769
+ It needs two things you control: the workspace's Context Index turned on in
770
+ Connect, and `ai:chat` on the API key behind your bridge.
771
+
647
772
  Other things worth knowing:
648
773
 
649
774
  - **The rail collapses to icons** and remembers the choice per browser
@@ -686,7 +811,7 @@ Each platform passes only what differs — `name` and `icon`. Mark geometry, wor
686
811
  @import "@cosmoops/connect-sdk/theme.css";
687
812
  ```
688
813
 
689
- One shared **HeroUI v3** theme for every service platform. The components paint with semantic tokens only — `accent`, `muted`, `surface`, `separator`, `danger` — never a hardcoded colour, so one edit to the theme restyles all of them everywhere at once. To diverge on brand, redefine individual tokens *after* the import rather than forking the file.
814
+ One shared **HeroUI v3** theme for every application platform. The components paint with semantic tokens only — `accent`, `muted`, `surface`, `separator`, `danger` — never a hardcoded colour, so one edit to the theme restyles all of them everywhere at once. To diverge on brand, redefine individual tokens *after* the import rather than forking the file.
690
815
 
691
816
  Requires `@heroui/react`, `lucide-react` and `motion` as peers (optional — server-only consumers need none of them). `motion` drives the components' entrance and state transitions, and respects `prefers-reduced-motion`.
692
817
 
@@ -696,6 +821,63 @@ The *flow* is in your app; the *payment* is not. A plan change is not one write
696
821
 
697
822
  So the hand-off is a URL, and Connect renders it as a modal over the relevant page (an intercepted parallel route) — the customer lands on the confirmation for the plan they picked, and returns to a subscription that is already correct.
698
823
 
824
+ ## Release notes
825
+
826
+ Full history: [CHANGELOG.md](./CHANGELOG.md). Latest first:
827
+
828
+ ### 0.9.0
829
+
830
+ - **Ask AI**, mounted by `AppShell` by default — a chat panel grounded in the
831
+ workspace's Context Index, streamed through a new `POST /ai/chat` route on
832
+ your own bridge. The workspace comes from the caller's session, never the
833
+ browser.
834
+ - **Answers draw as well as write.** Connect's model calls presentation tools
835
+ and the panel renders them natively: a sortable table with CSV export, a
836
+ chart, metric tiles, timelines, fact sheets, procedures, citations. Values
837
+ arrive raw with a format tag, so they are formatted in the viewer's locale
838
+ and a column still sorts numerically. The seven widgets are exported.
839
+ - **`streamdown` is a new optional peer dependency** — see step 1 and step 4 of
840
+ the quickstart. Two lines of host setup; skip both with `askAi={false}`.
841
+ - **`user.workspaceType`, `audience`**, `<ConnectWorkspaceTypeGate>`,
842
+ `useConnectWorkspaceType()`, `useConnectAudience()`.
843
+ - **`streamResource()`** on `useConnect()` — POST through the bridge and get
844
+ the raw streaming `Response` back.
845
+ - **Fixed:** `ai.chat()` no longer has its stream cut off mid-answer by the
846
+ transport's 20s timeout.
847
+
848
+ *0.8.0 was never published; its one breaking change is in this release.*
849
+
850
+ ### 0.8.0 — breaking (folded into 0.9.0)
851
+
852
+ - **`ConnectAppShell` renamed to `AppShell`.** Same props, same behaviour —
853
+ only the name changed. Every other `/client` export keeps its `Connect`
854
+ prefix; this is the one deliberate exception.
855
+
856
+ ```diff
857
+ - import { ConnectAppShell, type ConnectAppShellProps } from "@cosmoops/connect-sdk/client";
858
+ + import { AppShell, type AppShellProps } from "@cosmoops/connect-sdk/client";
859
+ ```
860
+
861
+ If your own layout component is also named `AppShell`, alias the import:
862
+ `import { AppShell as ConnectAppShell } from "..."`.
863
+
864
+ ### 0.6.0
865
+
866
+ - **`AppShell`** (then `ConnectAppShell`) — the application frame every
867
+ platform was otherwise writing by hand: collapsible sidebar, top bar with
868
+ the app launcher and account menu, mobile drawer, `ConnectAccessGate`
869
+ mounted around your content by default.
870
+ - **`ConnectSideNav`** — the sidebar's nav column alone, for an app with
871
+ chrome it already likes.
872
+ - The nav model as data — `ConnectNav` / `ConnectNavItem` / `ConnectNavSection`
873
+ — plus the pure functions the rail resolves the active route with.
874
+
875
+ See [CHANGELOG.md](./CHANGELOG.md) for 0.5.0 and earlier.
876
+
877
+ Upgrading past a minor version? Read that version's entry first — per the 0.x
878
+ convention at the top of this file, a minor bump here is allowed to break
879
+ something, and the changelog says what and how to migrate.
880
+
699
881
  ## Development
700
882
 
701
883
  ```bash
@@ -25,9 +25,17 @@ function formatDate(iso) {
25
25
  * `ConnectReadinessGate`/`ConnectReadinessBanner` already use. `ConnectAccessGate`
26
26
  * is a priority ordering over the same data, not a second opinion about it.
27
27
  *
28
- * Priority, first match wins: `suspended` > `no_subscription` >
29
- * `onboarding_pending` > any other blocking code (`blocked`) >
30
- * `payment_retry` (advisory - `children` still render) > `clear`.
28
+ * Priority, first match wins: `suspended` > `audience_mismatch` >
29
+ * `no_subscription` > `onboarding_pending` > any other blocking code
30
+ * (`blocked`) > `payment_retry` (advisory - `children` still render) >
31
+ * `clear`.
32
+ *
33
+ * `audience_mismatch` outranks `no_subscription` deliberately, and the
34
+ * ordering is the whole point of it: a workspace of the wrong account type
35
+ * carries BOTH blockers at once, and showing the subscription one first sent
36
+ * the customer to a plan picker whose subscribe call
37
+ * (`ActivateSubscriptionUseCase`) rejects them for the audience anyway. The
38
+ * account type is the reason; the missing subscription is a consequence.
31
39
  */
32
40
  function useConnectAccessGate() {
33
41
  const { session, isLoading: sessionLoading } = (0, provider_js_1.useConnect)();
@@ -50,6 +58,10 @@ function useConnectAccessGate() {
50
58
  const suspended = blocking.find((b) => b.code === "workspace_suspended");
51
59
  if (suspended)
52
60
  return { state: "suspended", blocker: suspended, paymentRetry: null };
61
+ const audienceMismatch = blocking.find((b) => b.code === "audience_mismatch");
62
+ if (audienceMismatch) {
63
+ return { state: "audience_mismatch", blocker: audienceMismatch, paymentRetry: null };
64
+ }
53
65
  const noSubscription = blocking.find((b) => b.code === "subscription_missing");
54
66
  if (noSubscription)
55
67
  return { state: "no_subscription", blocker: noSubscription, paymentRetry: null };
@@ -109,6 +121,29 @@ function ConnectAccessGate({ children, fallback, loading, className }) {
109
121
  return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: fallback ?? (0, jsx_runtime_1.jsx)(AccessGateFallback, { state: gate.state, blocker: gate.blocker, className: className }) }));
110
122
  }
111
123
  function AccessGateFallback({ state, blocker, className, }) {
124
+ if (state === "audience_mismatch") {
125
+ // `requiredWorkspaceType` rather than the message, which is documented as
126
+ // human-facing and subject to rewording - the title has to name the right
127
+ // account type even after someone rephrases the sentence below it.
128
+ const required = blocker?.requiredWorkspaceType ?? null;
129
+ return ((0, jsx_runtime_1.jsx)(GateCard, { icon: required === "individual" ? lucide_react_1.User : lucide_react_1.Building2, iconClassName: "text-muted", title: required === "business"
130
+ ? "Business account required"
131
+ : required === "individual"
132
+ ? "Individual account required"
133
+ : "Not available for this account type",
134
+ // The server names the application in its own message, which reads
135
+ // better than anything composable here; the fallback covers a blocker
136
+ // that arrived without one.
137
+ message: blocker?.message ??
138
+ (required
139
+ ? `This application is only available to ${required} accounts.`
140
+ : "This application isn't available to this account's type."),
141
+ // Deliberately no action, even if one were sent: `Workspace.type` is
142
+ // set at sign-up and no screen changes it, so any button here would
143
+ // only move the dead end somewhere else. Switching means a different
144
+ // account, which is a conversation with support, not a link.
145
+ resolveUrl: null, className: className }));
146
+ }
112
147
  if (state === "no_subscription") {
113
148
  return ((0, jsx_runtime_1.jsx)(GateCard, { icon: lucide_react_1.CreditCard, iconClassName: "text-accent", title: "Choose a plan to continue", message: blocker?.message ?? "This workspace isn't subscribed to this application yet.", actionLabel: "See plans", resolveUrl: blocker?.resolveUrl ?? null, className: className }));
114
149
  }
@@ -118,7 +153,7 @@ function AccessGateFallback({ state, blocker, className, }) {
118
153
  if (state === "onboarding_pending") {
119
154
  return ((0, jsx_runtime_1.jsx)(GateCard, { icon: lucide_react_1.Clock, iconClassName: "text-muted", title: "Waiting on approval", message: blocker?.message ?? "This workspace is waiting on approval before it can use this application.", resolveUrl: null, className: className }));
120
155
  }
121
- // "blocked" - any other blocking code (verification, audience, seats, ...).
156
+ // "blocked" - any other blocking code (verification, seats, ...).
122
157
  // Full fidelity for a code this component doesn't special-case: the same
123
158
  // banner-in-a-card `ConnectReadinessGate` falls back to, rather than
124
159
  // `GateCard`'s single-blocker shape - there can be more than one of these