@cosmoops/connect-sdk 0.7.0 → 0.15.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 (154) hide show
  1. package/CHANGELOG.md +662 -0
  2. package/README.md +376 -124
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +59 -5
  6. package/dist/cjs/client/components/ai-format.js +135 -0
  7. package/dist/cjs/client/components/ai-widgets.js +232 -0
  8. package/dist/cjs/client/components/app-shell.js +80 -45
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +353 -0
  11. package/dist/cjs/client/components/confirm-dialog.js +40 -0
  12. package/dist/cjs/client/components/cursor-pagination.js +22 -0
  13. package/dist/cjs/client/components/filter-tabs.js +24 -0
  14. package/dist/cjs/client/components/grievance-notice.js +30 -0
  15. package/dist/cjs/client/components/link-button.js +17 -10
  16. package/dist/cjs/client/components/logo.js +4 -3
  17. package/dist/cjs/client/components/nav.js +42 -1
  18. package/dist/cjs/client/components/plan-pricing.js +27 -0
  19. package/dist/cjs/client/components/plans.js +1 -1
  20. package/dist/cjs/client/components/platform-gate.js +48 -0
  21. package/dist/cjs/client/components/shell-context.js +149 -0
  22. package/dist/cjs/client/components/side-nav.js +50 -24
  23. package/dist/cjs/client/components/sidebar.js +127 -0
  24. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  25. package/dist/cjs/client/components/subscription.js +1 -1
  26. package/dist/cjs/client/components/user-menu.js +122 -40
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  29. package/dist/cjs/client/index.js +98 -3
  30. package/dist/cjs/client/provider.js +100 -2
  31. package/dist/cjs/client/use-checkout.js +9 -5
  32. package/dist/cjs/errors.js +1 -1
  33. package/dist/cjs/index.js +11 -1
  34. package/dist/cjs/next/index.js +2 -2
  35. package/dist/cjs/server/client.js +25 -2
  36. package/dist/cjs/server/handler.js +237 -16
  37. package/dist/cjs/server/http.js +52 -26
  38. package/dist/cjs/server/resources/acting-as.js +16 -0
  39. package/dist/cjs/server/resources/ai.js +15 -2
  40. package/dist/cjs/server/resources/data.js +374 -0
  41. package/dist/cjs/server/resources/entitlements.js +41 -0
  42. package/dist/cjs/server/resources/identity.js +9 -3
  43. package/dist/cjs/server/resources/kv.js +84 -0
  44. package/dist/cjs/server/resources/platform.js +43 -0
  45. package/dist/cjs/server/resources/sessions.js +1 -1
  46. package/dist/cjs/server/resources/workspaces.js +74 -0
  47. package/dist/cjs/store.js +152 -0
  48. package/dist/cjs/ui/empty-state.js +28 -0
  49. package/dist/cjs/ui/index.js +44 -0
  50. package/dist/cjs/ui/page-header.js +27 -0
  51. package/dist/cjs/ui/page-shell.js +44 -0
  52. package/dist/cjs/ui/section.js +20 -0
  53. package/dist/cjs/ui/setting-row.js +14 -0
  54. package/dist/cjs/ui/status-chip.js +17 -0
  55. package/dist/esm/cli/doctor.js +3 -3
  56. package/dist/esm/cli/init.js +3 -3
  57. package/dist/esm/client/components/access-gate.js +60 -6
  58. package/dist/esm/client/components/ai-format.js +129 -0
  59. package/dist/esm/client/components/ai-widgets.js +223 -0
  60. package/dist/esm/client/components/app-shell.js +85 -50
  61. package/dist/esm/client/components/apps-grid.js +3 -2
  62. package/dist/esm/client/components/ask-ai.js +350 -0
  63. package/dist/esm/client/components/confirm-dialog.js +37 -0
  64. package/dist/esm/client/components/cursor-pagination.js +19 -0
  65. package/dist/esm/client/components/filter-tabs.js +21 -0
  66. package/dist/esm/client/components/grievance-notice.js +27 -0
  67. package/dist/esm/client/components/link-button.js +17 -10
  68. package/dist/esm/client/components/logo.js +4 -3
  69. package/dist/esm/client/components/nav.js +41 -1
  70. package/dist/esm/client/components/plan-pricing.js +25 -0
  71. package/dist/esm/client/components/plans.js +2 -2
  72. package/dist/esm/client/components/platform-gate.js +45 -0
  73. package/dist/esm/client/components/shell-context.js +141 -0
  74. package/dist/esm/client/components/side-nav.js +49 -22
  75. package/dist/esm/client/components/sidebar.js +120 -0
  76. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  77. package/dist/esm/client/components/subscription.js +2 -2
  78. package/dist/esm/client/components/user-menu.js +121 -41
  79. package/dist/esm/client/components/workspace-switcher.js +97 -0
  80. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  81. package/dist/esm/client/index.js +58 -5
  82. package/dist/esm/client/provider.js +94 -2
  83. package/dist/esm/client/use-checkout.js +9 -5
  84. package/dist/esm/errors.js +1 -1
  85. package/dist/esm/index.js +1 -0
  86. package/dist/esm/next/index.js +2 -2
  87. package/dist/esm/server/client.js +25 -2
  88. package/dist/esm/server/handler.js +237 -16
  89. package/dist/esm/server/http.js +52 -26
  90. package/dist/esm/server/resources/acting-as.js +13 -0
  91. package/dist/esm/server/resources/ai.js +15 -2
  92. package/dist/esm/server/resources/data.js +369 -0
  93. package/dist/esm/server/resources/entitlements.js +41 -0
  94. package/dist/esm/server/resources/identity.js +9 -3
  95. package/dist/esm/server/resources/kv.js +80 -0
  96. package/dist/esm/server/resources/platform.js +39 -0
  97. package/dist/esm/server/resources/sessions.js +1 -1
  98. package/dist/esm/server/resources/workspaces.js +70 -0
  99. package/dist/esm/store.js +141 -0
  100. package/dist/esm/ui/empty-state.js +25 -0
  101. package/dist/esm/ui/index.js +34 -0
  102. package/dist/esm/ui/page-header.js +24 -0
  103. package/dist/esm/ui/page-shell.js +41 -0
  104. package/dist/esm/ui/section.js +17 -0
  105. package/dist/esm/ui/setting-row.js +11 -0
  106. package/dist/esm/ui/status-chip.js +14 -0
  107. package/dist/theme.css +87 -0
  108. package/dist/types/client/components/access-gate.d.ts +17 -3
  109. package/dist/types/client/components/ai-format.d.ts +24 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  111. package/dist/types/client/components/app-shell.d.ts +85 -31
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +73 -0
  114. package/dist/types/client/components/confirm-dialog.d.ts +34 -0
  115. package/dist/types/client/components/cursor-pagination.d.ts +27 -0
  116. package/dist/types/client/components/filter-tabs.d.ts +26 -0
  117. package/dist/types/client/components/grievance-notice.d.ts +31 -0
  118. package/dist/types/client/components/link-button.d.ts +32 -9
  119. package/dist/types/client/components/logo.d.ts +18 -2
  120. package/dist/types/client/components/nav.d.ts +27 -3
  121. package/dist/types/client/components/plan-pricing.d.ts +10 -0
  122. package/dist/types/client/components/platform-gate.d.ts +53 -0
  123. package/dist/types/client/components/shell-context.d.ts +116 -0
  124. package/dist/types/client/components/side-nav.d.ts +28 -14
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +70 -23
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  129. package/dist/types/client/index.d.ts +36 -7
  130. package/dist/types/client/provider.d.ts +149 -3
  131. package/dist/types/client/use-checkout.d.ts +8 -4
  132. package/dist/types/errors.d.ts +1 -1
  133. package/dist/types/index.d.ts +2 -0
  134. package/dist/types/next/index.d.ts +1 -1
  135. package/dist/types/server/client.d.ts +25 -2
  136. package/dist/types/server/http.d.ts +14 -3
  137. package/dist/types/server/index.d.ts +4 -0
  138. package/dist/types/server/resources/acting-as.d.ts +11 -0
  139. package/dist/types/server/resources/ai.d.ts +9 -1
  140. package/dist/types/server/resources/data.d.ts +258 -0
  141. package/dist/types/server/resources/entitlements.d.ts +33 -1
  142. package/dist/types/server/resources/kv.d.ts +63 -0
  143. package/dist/types/server/resources/platform.d.ts +32 -0
  144. package/dist/types/server/resources/workspaces.d.ts +55 -0
  145. package/dist/types/store.d.ts +96 -0
  146. package/dist/types/types.d.ts +491 -13
  147. package/dist/types/ui/empty-state.d.ts +34 -0
  148. package/dist/types/ui/index.d.ts +40 -0
  149. package/dist/types/ui/page-header.d.ts +34 -0
  150. package/dist/types/ui/page-shell.d.ts +39 -0
  151. package/dist/types/ui/section.d.ts +26 -0
  152. package/dist/types/ui/setting-row.d.ts +18 -0
  153. package/dist/types/ui/status-chip.d.ts +36 -0
  154. package/package.json +13 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,668 @@
3
3
  `@cosmoops/connect-sdk` follows semver, with the 0.x convention that **minor
4
4
  versions may carry breaking changes** and patch versions never do.
5
5
 
6
+ ## 0.15.0
7
+
8
+ Your application's own database stops being a toy. Real queries, real
9
+ aggregations, and real atomicity - without ever handing you a Firebase
10
+ credential.
11
+
12
+ Everything here is `data.*`. The rest of the SDK is unchanged.
13
+
14
+ ### Added
15
+
16
+ - **`data.aggregate()` - count, sum and average in the database.**
17
+
18
+ ```ts
19
+ const { values } = await connect.data.aggregate({
20
+ workspaceId,
21
+ collection: "deals",
22
+ filters: [{ field: "outcome", op: "==", value: "won" }],
23
+ aggregations: [
24
+ { op: "count", alias: "count" },
25
+ { op: "sum", field: "value", alias: "total" },
26
+ ],
27
+ });
28
+ ```
29
+
30
+ Firestore bills an aggregation by the index entries it scans rather than by
31
+ the documents it would have returned, so a dashboard tile over ten thousand
32
+ rows costs a handful of reads and stays flat as the collection grows. Every
33
+ integration that had no aggregate call to reach for was paging the rows into
34
+ its own process and adding them up, which is an unbounded read wearing a
35
+ different name.
36
+
37
+ A `sum` or `avg` over nothing answers `null`, not `0` - "nothing matched"
38
+ and "the total is zero" are different facts.
39
+
40
+ - **`data.commit()` - a set of writes that all land or none do**, across as
41
+ many of your collections as they name.
42
+
43
+ You never hold a transaction: a handle passed across an HTTP boundary is a
44
+ server-side lock whose duration your network controls. Instead you read,
45
+ decide, and commit conditional on nothing having moved. Connect runs the
46
+ whole set as one short transaction on its side.
47
+
48
+ ```ts
49
+ import { increment, unchangedSince } from "@cosmoops/connect-sdk";
50
+
51
+ await connect.data.commit({
52
+ workspaceId,
53
+ operations: [
54
+ { op: "create", collection: "ledger", id, data: entry },
55
+ {
56
+ op: "merge",
57
+ collection: "funds",
58
+ id: entry.fundId,
59
+ data: { balancePaise: increment(entry.amountPaise) },
60
+ },
61
+ ],
62
+ });
63
+ ```
64
+
65
+ A stale precondition throws `ConnectError` with status 409 and writes
66
+ nothing.
67
+
68
+ - **`data.transaction(build)`** - the read/decide/commit loop with the retry
69
+ written once. `build` re-runs against fresh reads when a precondition went
70
+ stale, and only a 409 is retried: a 403 means the operation was wrong rather
71
+ than late.
72
+
73
+ - **Field transforms**: `increment`, `serverTimestamp`, `arrayUnion`,
74
+ `arrayRemove`, `deleteField`, plus the `unchangedSince(document)` and
75
+ `documentExists()` precondition builders. All exported from the root entry,
76
+ so a Server Component and a client component build the same write the same
77
+ way.
78
+
79
+ `increment` is the one that genuinely cannot be done any other way: it is
80
+ commutative, so two concurrent increments both land - where read-add-write
81
+ from your own process loses one of them however carefully it retries.
82
+
83
+ - **Composite indexes.** Declare them per collection on your application's
84
+ **Store** tab and Connect builds them in your database, reconciling on every
85
+ save - including removing ones you no longer declare, which would otherwise
86
+ be paid for on every document write forever.
87
+
88
+ This is what lifts the old one-filter ceiling. A query with no matching
89
+ index is still refused, but the refusal now names the index to declare
90
+ rather than telling you the shape is impossible.
91
+
92
+ - **`_updateTime`** on every document that came from a read - Firestore's own
93
+ write timestamp, and the value a `{ updateTime }` precondition is compared
94
+ against. Not `_updatedAt`: a transform in the same commit can move that
95
+ field, and cannot touch the write timestamp.
96
+
97
+ ### Changed - breaking
98
+
99
+ - **`data.query({ filter })` is now `data.query({ filters })`**, a list, ANDed.
100
+ `data.scan` takes the same. One filter becomes a one-element array:
101
+
102
+ ```ts
103
+ // Before
104
+ await connect.data.query({ workspaceId, collection: "tickets", filter: f });
105
+ // After
106
+ await connect.data.query({ workspaceId, collection: "tickets", filters: [f] });
107
+ ```
108
+
109
+ The server still accepts the singular `filter` query parameter, so an
110
+ integration on 0.14 or earlier keeps working against an upgraded Connect -
111
+ but the SDK method has moved and this one is a code change.
112
+
113
+ - **`Plan.billingInterval` and `SubscribedPlan.billingInterval` are now
114
+ `BillingInterval`**, which adds `"one-time"`. Connect has been able to
115
+ return it; the SDK type could not represent it, so a one-time plan was typed
116
+ as monthly and rendered "per month" - a false price rather than a cosmetic
117
+ slip. `ConnectPlans`, `ConnectSubscription` and the subscribe dialog now
118
+ label it "once".
119
+
120
+ Exhaustive `switch`es on a billing interval will need a third arm.
121
+
122
+ ### Added - operators
123
+
124
+ - `not-in` joins the query operators.
125
+
126
+ ## 0.14.0
127
+
128
+ One session answers "who is this?" once, and the subscribe button goes where
129
+ it says it does.
130
+
131
+ ### Changed - breaking
132
+
133
+ - **`applicationRole` and `platformAccess` are now `role`, `permissions` and
134
+ `staff`**, three flat fields on `exchangeCode()` and `getUser()` (and on the
135
+ `/session` bridge read).
136
+
137
+ The two blocks they replace were mutually exclusive by construction. A
138
+ Connect platform account belongs to no workspace - Connect enforces it - so
139
+ a member session always carried `platformAccess: null` and a staff session
140
+ always carried `applicationRole: { role: null, granted: [] }`. Two shapes
141
+ for one question meant every integration wrote the same null-check twice,
142
+ and the block that did not apply still had to be built and sent.
143
+
144
+ ```ts
145
+ // Before
146
+ const { applicationRole, platformAccess } = await connect.identity.getUser(t);
147
+ if (platformAccess) {
148
+ if (!platformAccess.roles.includes("platform_admin")) return forbidden();
149
+ return renderOperatorConsole();
150
+ }
151
+ if (!applicationRole.granted.includes("invoice:write")) return forbidden();
152
+
153
+ // After
154
+ const { role, permissions, staff } = await connect.identity.getUser(t);
155
+ if (staff) {
156
+ if (role !== "platform_admin") return forbidden();
157
+ return renderOperatorConsole();
158
+ }
159
+ if (!permissions.includes("invoice:write")) return forbidden();
160
+ ```
161
+
162
+ Read `staff` first: it decides which vocabulary the other two speak - your
163
+ own Permission Catalog's ids for a member, Connect's `platform:*` ids for an
164
+ operator. A staff session reports one `role` rather than a list, and it is
165
+ the highest-authority role your application accepts; the platform roles nest
166
+ (`developer` ⊆ `platform_manager` ⊆ `platform_admin`), so nothing is lost,
167
+ and `permissions` is still the union over every accepted role.
168
+
169
+ `ApplicationRole` and `PlatformAccess` are gone as exported types, replaced
170
+ by `SessionAccess`. `useConnectPlatformAccess()` keeps its shape -
171
+ `roles` is now a one-element array on a staff session, `[]` otherwise - so
172
+ `<ConnectPlatformGate accept={[...]}>` is unchanged.
173
+
174
+ The signed-out `/session` response carries the same three fields
175
+ (`role: null, permissions: [], staff: false`) rather than a bare
176
+ `applicationRole: null`, so a client never tells "signed out" apart from
177
+ "signed in with no role" by which keys happen to be present.
178
+
179
+ - **Workspace role ids lost their `org_` prefix** - `org_admin` is `admin`,
180
+ and the same for `finance`, `manager` and `member`. This is visible to you
181
+ only through `access.getUserRoles()` and `access.listRoles()`, which report
182
+ a *member's* resolved role on your application rather than these; if you
183
+ hardcoded a Connect workspace role id anywhere, it needs updating. Grants
184
+ stored under the old ids keep resolving.
185
+
186
+ ### Fixed
187
+
188
+ - **Subscribing no longer lands on a 404.** `buildCheckoutUrl` - and so
189
+ `<ConnectPlans>`'s subscribe button - built
190
+ `/applications/{id}/subscribe/{planId}`. The portal route customers can
191
+ actually open is `/apps/...`; `/applications` is a Console-only prefix. Every
192
+ first-time subscriber sent from a partner's own pricing page hit a 404. The
193
+ matching `resolveUrl` on the `subscription_missing` readiness blocker was
194
+ wrong the same way and is fixed with it.
195
+
196
+ - **`staff` actually reaches you.** Both `/sso/token` and `/sso/user` resolved
197
+ the staff block, typed it and documented it, then dropped it while building
198
+ the JSON response - the handlers are hand-built rather than schema-parsed,
199
+ so a forgotten field was silent. Staff access has been invisible to every
200
+ partner since it shipped in 0.11.0. If you turned it on and concluded it did
201
+ not work, it did not.
202
+
203
+ - `SDK_VERSION`, sent in the `User-Agent`, said `0.12.0` on a 0.13.0 package.
204
+
205
+ ## 0.13.0
206
+
207
+ The platform's grievance channel is now something you read rather than
208
+ something you copy.
209
+
210
+ ### Added
211
+
212
+ - **`connect.platform.getCompanyProfile()`** - the registered entity operating
213
+ Connect, and the grievance officer s.13 of India's DPDP Act 2023 requires a
214
+ Data Fiduciary to publish. Backed by the new
215
+ `GET /api/v1/application/company-profile`, which needs the new
216
+ **`company:read`** scope on your API key.
217
+
218
+ Your application is a Data Processor on that notice, not a second Data
219
+ Fiduciary, so the channel your privacy page names is this one. The shape it
220
+ replaces is four lines in an `.env`:
221
+
222
+ ```bash
223
+ # Before - a statutory contact that can go stale without anyone finding out.
224
+ APP_DATA_FIDUCIARY_NAME="..."
225
+ APP_GRIEVANCE_OFFICER_EMAIL="..."
226
+ ```
227
+
228
+ An officer resigns, and every application holding a copy is now publishing a
229
+ redressal channel that reaches nobody. Reading it removes that failure mode
230
+ outright, which is the entire reason it is an endpoint.
231
+
232
+ Every field is resolved server-side against the platform's published
233
+ defaults, so only `website` and `gstNumber` are nullable - there is nothing
234
+ here for you to write a fallback for.
235
+
236
+ - **`useConnectCompanyProfile()`** - the same read through the browser bridge.
237
+ The one hook here that does not depend on a session, so it resolves for a
238
+ signed-out visitor too: the moment a grievance channel most needs to be
239
+ readable is on a public privacy page.
240
+
241
+ - **`<ConnectGrievanceNotice />`** - the notice, rendered. The wording lives in
242
+ the SDK rather than being retyped per application, so a user comparing two
243
+ applications' privacy pages does not find two different accounts of who
244
+ their Data Fiduciary is.
245
+
246
+ ```tsx
247
+ import { ConnectGrievanceNotice } from "@cosmoops/connect-sdk/client";
248
+
249
+ <ConnectGrievanceNotice applicationName="CosmoOps CRM" />
250
+ ```
251
+
252
+ It renders `null` while loading and `null` on failure rather than a skeleton
253
+ or an error box - a red error in the middle of a legal notice reads as the
254
+ notice itself being broken. Call the hook directly if you want those states.
255
+
256
+ ### Upgrading
257
+
258
+ Existing API keys do not carry `company:read`. Tick it on the key's scopes in
259
+ the Console's Integration tab, or re-run the seed script for that application,
260
+ which widens scopes in place.
261
+
262
+ ## 0.12.0
263
+
264
+ The application shell is one column now. The top bar is gone, the rail holds
265
+ everything it used to, and the rail starts collapsed.
266
+
267
+ ### Changed - breaking
268
+
269
+ - **Two renames, both with deprecated aliases kept**, so nothing breaks on
270
+ upgrade:
271
+
272
+ | Was | Now | Why |
273
+ |---|---|---|
274
+ | `ConnectSideNav` | `ConnectSidebarNav` | The rail is a `ConnectSidebar` now; its parts are named after it |
275
+ | `ConnectUserMenu` | `ConnectUserButton` | It is a button that opens a modal. "Menu" had people looking for a dropdown that never existed |
276
+
277
+ **`AppShell` keeps its name.** It stays the one export in `/client` without
278
+ the `Connect` prefix, exactly as 0.8.0 decided. A host whose own layout
279
+ component is also called `AppShell` aliases it on import - `import {
280
+ AppShell as ConnectAppShell }` - which is the documented answer to that
281
+ clash and what both reference integrations do.
282
+
283
+ - **The top bar is removed.** With it go two props:
284
+
285
+ - **`title`** - a page's heading belongs to the page. Move it to
286
+ `ConnectPageShell`, from `@cosmoops/connect-sdk/ui`, which renders it next
287
+ to that page's own breadcrumbs and actions:
288
+
289
+ ```tsx
290
+ // before // after
291
+ <AppShell title="Billing"> <AppShell>
292
+ <BillingPage /> <ConnectPageShell title="Billing">…</ConnectPageShell>
293
+ </AppShell> </AppShell>
294
+ ```
295
+
296
+ - **`headerActions`** - there is no header to put them in. A control that
297
+ belongs to a page goes in that page's `ConnectPageShell`; a control that
298
+ belongs to the *application* goes in `sidebarFooter`, which now accepts
299
+ sidebar rows (see `ConnectSidebarRow`).
300
+
301
+ Below `md` a slim bar remains, carrying the drawer trigger and the lockup and
302
+ nothing else. A phone has no rail to hold that trigger.
303
+
304
+ - **`defaultCollapsed` now defaults to `true`.** The icon rail is the resting
305
+ state and expanding is the deliberate act - the rail's job is to be *there*,
306
+ not to be read. A stored preference from the user's last visit still wins.
307
+ Pass `defaultCollapsed={false}` to keep 0.11's behaviour.
308
+
309
+ - **`ConnectUserMenu`'s `compact` prop** is superseded by
310
+ `ConnectUserButton`'s `variant`. The alias maps `compact` to
311
+ `variant="compact"` and the old default to `variant="inline"`, so existing
312
+ call sites are unchanged.
313
+
314
+ - **`ConnectAppsGrid` now renders as a sidebar row by default**
315
+ (`variant="row"`). Pass `variant="icon"` for the square accent button it used
316
+ to be.
317
+
318
+ ### Added
319
+
320
+ - **`secondaryNav`** on `AppShell` - a second `ConnectNav`, pinned to
321
+ the foot of the rail above the account row. For the routes a product has but
322
+ does not want competing with its own: settings, help, plans, status. Same
323
+ shape, same active resolution, same audience filtering, same `gateExempt`
324
+ handling as `nav`.
325
+
326
+ - **The workspace switcher moved into the rail's header**, directly under the
327
+ brand, and is now collapse-aware: a 32px initials tile when the rail is
328
+ narrow, tile plus name plus chevron when it is open. It still hides itself
329
+ for an account with one workspace.
330
+
331
+ - **The account row is the last thing in the rail.** Circular avatar, name over
332
+ email, opening the same account modal as before. It collapses to the avatar
333
+ alone, which is still a complete account control.
334
+
335
+ - **`ConnectProfileModal`** - the account modal with no trigger of its own,
336
+ controlled by `isOpen`/`onOpenChange`. For an application that wants to keep
337
+ its own account control and put Connect's modal behind it.
338
+
339
+ - **The rail's pieces are exported**: `ConnectSidebar`, `ConnectSidebarHeader`,
340
+ `ConnectSidebarBody`, `ConnectSidebarFooter`, `ConnectSidebarRow`, plus
341
+ `ConnectShellProvider` / `useConnectShell` / `sidebarRowClass` and the
342
+ `RAIL_COLLAPSED` / `RAIL_EXPANDED` constants. `AppShell` composes
343
+ exactly these, so an application whose chrome the prop API cannot express
344
+ rebuilds the same rail from the same parts rather than forking it.
345
+
346
+ - **`ConnectLogo` takes `variant`** - `"full"` | `"mark"` | `"wordmark"`.
347
+ `collapsed` is deprecated in favour of `variant="mark"`.
348
+
349
+ ### Fixed
350
+
351
+ - **The collapsed Ask AI panel occupied 400px at `lg` and up.** Its shell
352
+ carried `lg:w-[400px]` and `lg:w-0` at once; Tailwind emits arbitrary values
353
+ after scale values, so the override never applied and the closed panel sat
354
+ permanently in the layout, eating that much of the content pane on every page
355
+ with `askAi` on. Only the `translate-x` hid it below `lg`, which is why it
356
+ looked right on a phone and wrong on a laptop.
357
+
358
+ - **Sidebar contrast, in both directions.** Resting nav rows were
359
+ `--foreground` - 17.6:1 against the rail in light, 15.8:1 in dark - so eight
360
+ destinations shouted as loudly as body copy and the selected row had to win
361
+ on background alone. They are `--muted` now (4.8:1 / 6.2:1, past AA), with
362
+ `--foreground` on hover. The *active* row was `text-accent` on
363
+ `bg-accent-soft`, which measures **2.8:1 in light and 4.0:1 in dark - both
364
+ below AA**, on the one row a user most needs to read; it now uses HeroUI's
365
+ derived `--accent-soft-foreground` (4.8:1 / 5.6:1), so a host that re-themes
366
+ `--accent` keeps the contrast.
367
+
368
+ - **The account row and the app launcher looked permanently selected.** Both
369
+ are HeroUI `<Button variant="tertiary">` because they trigger overlays, and
370
+ `.button--tertiary` sets a real `--button-bg`. Next to the plain anchors the
371
+ nav renders, three rows in the rail appeared highlighted at once.
372
+
373
+ - **The account avatar is a circle.** HeroUI's `.avatar` is `rounded-2xl`/
374
+ `rounded-3xl` - right for the media component, wrong for a person, and it
375
+ read as a rendering fault beside circular status dots.
376
+
377
+ ### Motion
378
+
379
+ The rail animates its **width and nothing else**, via `motion`. Every row is
380
+ laid out so its glyph sits on the same 32px horizontal centre in both states,
381
+ and every label is clipped by the shrinking rail rather than re-measured - so
382
+ collapsing costs no reflow on any row, and no child moves sideways. Section
383
+ headings hold their box and cross-fade a rule in place of their text, so
384
+ nothing jumps vertically either. A tween rather than a spring: an overshooting
385
+ *width* drags every clip edge past its resting place and back, which reads as
386
+ the labels wobbling.
387
+
388
+ ## 0.11.0
389
+
390
+ ### Changed - breaking
391
+
392
+ - **`ConnectUser.workspaceId`, `workspaceName` and `workspaceType` are now
393
+ nullable.** They are `null` for exactly one thing: a **Connect platform
394
+ staff** session, described below. Every member session carries them as
395
+ before.
396
+
397
+ If you never turn staff access on for your application - it is off by
398
+ default, and only a Connect platform admin can turn it on - this is a type
399
+ narrow and nothing else. Reading `user.workspaceId` where a `string` is
400
+ wanted becomes a compile error, and `?? ""` is the wrong fix: decide what
401
+ your tenanting does with a session that has no tenant before you enable the
402
+ feature.
403
+
404
+ The honest alternative was a synthesized placeholder workspace, which would
405
+ have kept the types quiet while flowing a fabricated tenant id into the ID
406
+ token's `org_id` claim and into whatever you key your own rows on. A
407
+ compile error is cheaper than that.
408
+
409
+ ### Added
410
+
411
+ - **Connect platform staff sign-in.** Connect's own operators
412
+ (`platform_admin`, `platform_manager`, `developer`) can now sign into your
413
+ application, so an operator console can live inside your product and be
414
+ authenticated by Connect rather than by a second login you maintain.
415
+
416
+ These accounts belong to no workspace - Connect enforces that, they cannot
417
+ own or join one - which is why the session is shaped differently: no
418
+ `workspaceId`, `subscription: null`, and `applicationRole.granted: []`.
419
+ Platform authority is deliberately *not* reported through the member-role
420
+ field. What staff hold is the new block:
421
+
422
+ ```ts
423
+ const { platformAccess } = await client.identity.getUser(sessionToken);
424
+ if (!platformAccess) return forbidden();
425
+ if (!platformAccess.roles.includes("platform_admin")) return forbidden();
426
+ ```
427
+
428
+ `roles` is the intersection of what the account holds and what your
429
+ application accepts - never Connect's full role list for that person.
430
+
431
+ Off by default and per-application. Ask a Connect platform admin to enable it
432
+ on your catalogue entry, naming the roles you accept; until then
433
+ `platformAccess` is always `null` and nothing below does anything.
434
+
435
+ - **`platformAccess` on `exchangeCode()` and `getUser()`** - `PlatformAccess |
436
+ null`. Live on `getUser()`, like `subscription` and `applicationRole`: a
437
+ platform role revoked, or staff access turned off for your application,
438
+ comes back `null` there and Connect has already revoked the session.
439
+
440
+ - **`<ConnectPlatformGate>`** and **`useConnectPlatformAccess()`** in
441
+ `/client`. The gate renders its children only for a staff session, and takes
442
+ an optional `roles` allowlist when your console has tiers of its own.
443
+
444
+ Like every gate in this package it is a **UX gate, not a security
445
+ boundary** - and that matters more here than anywhere else, because what
446
+ sits behind it is usually an operator console. Re-check `platformAccess`
447
+ server-side on every route it reveals.
448
+
449
+ - **`ConnectNavItem.audience`** - `"staff"` or `"member"`. `AppShell` filters
450
+ the rail by it off the live session, so your operator routes appear only for
451
+ staff and your billing/team/usage routes disappear for them. A parent whose
452
+ children all disappear is dropped with them. `filterNavForAudience` is
453
+ exported for a rail you render yourself.
454
+
455
+ - `ConnectAccessGate` passes a staff session straight through. Every state it
456
+ reports is a fact about a workspace, and `subscription_missing` would
457
+ otherwise send an operator to your plan picker to buy a plan for a workspace
458
+ that does not exist.
459
+
460
+ ### Note
461
+
462
+ Workspace-scoped bridge routes (`/entitlements`, `/addons`, `/integrations`,
463
+ `/ai/chat`, `/data/*`, `/kv/*`) answer `403 no_workspace` for a staff session.
464
+ There is no tenant to read, and the only other option would be letting the
465
+ request name one - which is precisely what `createConnectHandler` exists to
466
+ prevent. An operator console reads `platformAccess` and talks to your own
467
+ backend for anything tenant-shaped.
468
+
469
+ ## 0.10.0
470
+
471
+ ### Added
472
+
473
+ - **`@cosmoops/connect-sdk/ui`** - the page-composition layer, and a new entry
474
+ point. `ConnectPageShell` (with `.Header` and `.Async`), `ConnectPageHeader`,
475
+ `ConnectSection`, `ConnectEmptyState`, `ConnectSettingRow`,
476
+ `ConnectStatusChip`.
477
+
478
+ The SDK already shipped the *application* chrome - the shell, the launcher,
479
+ the account menu, the plan grid - and nothing between that and raw HeroUI. So
480
+ every integration re-derived the same six primitives, and they drifted the
481
+ same way each time: the page action ending up below the description instead
482
+ of beside the title, section headings rendered as `h2` so every block became
483
+ a sibling of its own page, and one empty state doing duty for both "you have
484
+ none" and "none match this filter". Those are the three bugs these components
485
+ are shaped to make unrepresentable, not merely to style consistently.
486
+
487
+ **A separate entry from `/client`, deliberately.** That module's barrel
488
+ carries a `"use client"` directive, so re-exporting a presentational `<Card>`
489
+ wrapper through it would turn every page shell into a client boundary. Nothing
490
+ in `/ui` has a hook or state, so it renders in a Server Component and stays
491
+ out of the browser bundle.
492
+
493
+ ```tsx
494
+ import { ConnectPageShell, ConnectSection } from "@cosmoops/connect-sdk/ui";
495
+ ```
496
+
497
+ - **`ConnectConfirmDialog`, `ConnectCursorPagination`, `ConnectFilterTabs`** in
498
+ `@cosmoops/connect-sdk/client` - the interactive counterparts, which hold
499
+ state and so belong on the browser side. `ConnectCursorPagination` has no page
500
+ numbers on purpose: a keyset-paginated list cannot know its page count without
501
+ a second billed index scan over the same filter, for a number nobody acts on.
502
+
503
+ ## 0.9.0
504
+
505
+ ### Added
506
+
507
+ - **`user.workspaceType`** on `exchangeCode()` and `getUser()` - `"individual"`
508
+ (a one-seat personal account, KYC) or `"business"` (a team, KYB). The one
509
+ fact about the signed-in workspace a partner could not look up on its own:
510
+ the application API has no workspace read, so an application serving both
511
+ audiences had to render one UI for everybody. An `"individual"` workspace
512
+ has exactly one seat and can never gain another, so a members table, an
513
+ invite button and a seat meter are all dead controls there.
514
+
515
+ Re-read it rather than caching it past sign-in: an individual workspace can
516
+ convert to a business one under a live session (the reverse never happens).
517
+
518
+ - **`audience`** on the same two responses - what your application is *sold*
519
+ to, joined with the account in front of you:
520
+
521
+ ```ts
522
+ const { user, audience } = await connect.identity.exchangeCode({ code, redirectUri });
523
+ // user.workspaceType -> "individual"
524
+ // audience.application -> "b2b"
525
+ // audience.matches -> false
526
+ ```
527
+
528
+ `matches: false` is a hard block, not a hint: readiness reports an
529
+ `audience_mismatch` blocker and any attempt to subscribe is refused. It ships
530
+ alongside `workspaceType` so the check stays `!audience.matches` instead of a
531
+ hardcoded audience constant in your own code that goes stale the moment the
532
+ catalogue entry changes. `audience.enforcedAtSignIn` tells you whether
533
+ Connect refuses the sign-in itself, in which case a mismatch can only reach
534
+ you on a session minted before that setting was turned on.
535
+
536
+ - **`<ConnectWorkspaceTypeGate>`**, plus `useConnectWorkspaceType()` and
537
+ `useConnectAudience()`:
538
+
539
+ ```tsx
540
+ <ConnectWorkspaceTypeGate type="business">
541
+ <TeamMembersTable />
542
+ </ConnectWorkspaceTypeGate>
543
+ ```
544
+
545
+ This is *not* the audience gate - it does not know what your application is
546
+ sold to, and is for varying what each account type sees *inside* an
547
+ application both audiences can reach. `<ConnectAccessGate>` remains the one
548
+ that blocks a mismatched account outright, and already did.
549
+
550
+ Like every gate in this package: a UX gate, not a security boundary.
551
+
552
+ - **`<ConnectAskAi>`**, and `askAi` on `AppShell` - a chat panel grounded in
553
+ the signed-in workspace's Context Index. **On by default**, like `gate`:
554
+
555
+ ```tsx
556
+ <AppShell name="Dispatch" nav={nav}>
557
+ {children}
558
+ </AppShell>
559
+ ```
560
+
561
+ It stays inert until it can work - nothing renders for a signed-out visitor,
562
+ and a workspace whose Context Index is off gets a message in the panel rather
563
+ than a broken answer. Pass `askAi={false}` for an application that should not
564
+ offer it, or an object to override the heading, copy or markdown plugins.
565
+
566
+ It holds no credential, like everything else in `./client`: it POSTs to
567
+ `${basePath}/ai/chat`, a new bridge route on `createConnectHandler` that
568
+ resolves the workspace from the caller's own session and streams Connect's
569
+ answer straight through. A browser-supplied `workspaceId` would be another
570
+ tenant's documents delivered as a chat reply, so there isn't one.
571
+
572
+ The panel parses the server-sent event stream itself rather than pulling in
573
+ `ai`/`@ai-sdk/react`.
574
+
575
+ - **Answers that draw, not just write.** Connect's Ask AI can call a small set
576
+ of *presentation tools*, and this panel renders each one natively:
577
+
578
+ | Tool | Rendered as |
579
+ |---|---|
580
+ | `showTable` | a sortable table with a CSV download |
581
+ | `showChart` | a bar / line / area chart (inline SVG), or a labelled proportion list for `pie` |
582
+ | `showMetrics` | up to six metric tiles with period-over-period deltas |
583
+ | `showTimeline` | dated events on a rail |
584
+ | `showDetails` | one entity's fields as a fact sheet |
585
+ | `showSteps` | a numbered procedure |
586
+ | `showSources` | the documents a grounded answer rested on |
587
+
588
+ Every value arrives **raw with a format tag** - a currency is the number
589
+ `12500`, never the string `"₹12,500"` - so it is formatted in the viewer's
590
+ locale here, and a column still sorts numerically. The seven widgets and
591
+ `formatAiValue` are exported for a partner rendering Connect's chat somewhere
592
+ other than this panel.
593
+
594
+ No charting library ships in this package: the chart is hand-drawn SVG. A
595
+ 400px side panel does not need pan, zoom or brushes, and a charting
596
+ dependency is not a reasonable thing to make every platform install.
597
+
598
+ - **Markdown, via `streamdown`** - tables, fenced code, task lists, and a
599
+ word-by-word `blurIn` reveal with a caret while the answer streams. Three
600
+ host steps, all one-time:
601
+
602
+ 1. `npm i streamdown` (a new **optional** peer dependency - the panel is the
603
+ only thing that uses it).
604
+ 2. `@source "…/node_modules/streamdown/dist/*.js";` in your Tailwind CSS, so
605
+ its utility classes are generated.
606
+ 3. `import "streamdown/styles.css";` for the streaming animation.
607
+
608
+ The colour tokens Streamdown expects (it paints with shadcn/ui's names) are
609
+ mapped onto HeroUI's in this SDK's `theme.css`, which you already import - so
610
+ answers inherit your platform's theme with no further work.
611
+
612
+ Syntax highlighting, KaTeX, Mermaid and CJK typography are Streamdown plugin
613
+ packages, off unless you pass them - several are large, and prose, tables and
614
+ plain code fences render without any of them:
615
+
616
+ ```tsx
617
+ import { code } from "@streamdown/code";
618
+ const PLUGINS = { code }; // module scope: Streamdown compares this by reference
619
+
620
+ <AppShell name="Dispatch" nav={nav} askAi={{ markdownPlugins: PLUGINS }}>
621
+ ```
622
+
623
+ - `streamResource()` on `useConnect()` - POST through the bridge and get the
624
+ raw `Response` back, unread and uncached. The seam `<ConnectAskAi>` streams
625
+ over, and the one to use for any future streaming route.
626
+
627
+ ### Changed
628
+
629
+ - `ConnectSession` (the value behind `useConnect()`) carries `audience`,
630
+ `null` when signed out alongside the three fields that already were.
631
+
632
+ ### Known gaps
633
+
634
+ - No SDK methods for the channel and call endpoints (`GET /channels`,
635
+ `GET`/`PUT`/`DELETE /channels/{channelId}/routing`, `GET /calls`,
636
+ `GET /calls/{callId}`). They are documented in the partner API and callable
637
+ over HTTP; they just have no typed wrapper here yet. The conformance suite
638
+ names them, and stays red until they land.
639
+
640
+ ### Fixed
641
+
642
+ - `ai.chat()` no longer has its stream cut off mid-answer. The transport's
643
+ `timeoutMs` (20s by default) was handed to `fetch` as an `AbortSignal`, and a
644
+ `fetch` signal keeps governing the *body* long after the headers arrive - so
645
+ any conversation that took longer than the timeout to finish generating was
646
+ torn down. The timeout is now cleared once an attempt is done reading, which
647
+ for a `raw` (streaming) request means once the headers land. Pass your own
648
+ `signal` to bound the stream itself.
649
+
650
+ ## 0.8.0
651
+
652
+ *Never published - `0.9.0` is the first release carrying these changes.*
653
+
654
+ ### Changed (breaking)
655
+
656
+ - **`ConnectAppShell` renamed to `AppShell`.** No other change - same props,
657
+ same behaviour. Every other component in `/client` keeps its `Connect`
658
+ prefix; this is the one exception, by request. Update the import:
659
+
660
+ ```diff
661
+ - import { ConnectAppShell, type ConnectAppShellProps } from "@cosmoops/connect-sdk/client";
662
+ + import { AppShell, type AppShellProps } from "@cosmoops/connect-sdk/client";
663
+ ```
664
+
665
+ If your own layout component is also called `AppShell` (a common name for
666
+ one), alias the import: `import { AppShell as ConnectAppShell } from "..."`.
667
+
6
668
  ## 0.6.0
7
669
 
8
670
  ### Added