@cosmoops/connect-sdk 0.9.0 → 0.16.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 (155) hide show
  1. package/CHANGELOG.md +566 -0
  2. package/README.md +113 -42
  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 +20 -1
  6. package/dist/cjs/client/components/ai-screens.js +73 -0
  7. package/dist/cjs/client/components/ai-widgets.js +37 -0
  8. package/dist/cjs/client/components/app-shell.js +75 -41
  9. package/dist/cjs/client/components/apps-grid.js +3 -2
  10. package/dist/cjs/client/components/ask-ai.js +50 -5
  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 +41 -0
  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 +49 -23
  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 +96 -37
  27. package/dist/cjs/client/components/workspace-switcher.js +100 -0
  28. package/dist/cjs/client/index.js +72 -2
  29. package/dist/cjs/client/provider.js +57 -2
  30. package/dist/cjs/client/use-checkout.js +9 -5
  31. package/dist/cjs/errors.js +1 -1
  32. package/dist/cjs/index.js +16 -1
  33. package/dist/cjs/mcp/index.js +90 -0
  34. package/dist/cjs/mcp/tools.js +369 -0
  35. package/dist/cjs/next/index.js +2 -2
  36. package/dist/cjs/server/client.js +25 -2
  37. package/dist/cjs/server/handler.js +203 -18
  38. package/dist/cjs/server/http.js +6 -2
  39. package/dist/cjs/server/resources/acting-as.js +16 -0
  40. package/dist/cjs/server/resources/ai.js +22 -2
  41. package/dist/cjs/server/resources/data.js +374 -0
  42. package/dist/cjs/server/resources/identity.js +7 -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-schema.js +66 -0
  48. package/dist/cjs/store.js +152 -0
  49. package/dist/cjs/ui/empty-state.js +28 -0
  50. package/dist/cjs/ui/index.js +44 -0
  51. package/dist/cjs/ui/page-header.js +27 -0
  52. package/dist/cjs/ui/page-shell.js +44 -0
  53. package/dist/cjs/ui/section.js +20 -0
  54. package/dist/cjs/ui/setting-row.js +14 -0
  55. package/dist/cjs/ui/status-chip.js +17 -0
  56. package/dist/esm/cli/doctor.js +3 -3
  57. package/dist/esm/cli/init.js +3 -3
  58. package/dist/esm/client/components/access-gate.js +20 -1
  59. package/dist/esm/client/components/ai-screens.js +69 -0
  60. package/dist/esm/client/components/ai-widgets.js +37 -1
  61. package/dist/esm/client/components/app-shell.js +81 -47
  62. package/dist/esm/client/components/apps-grid.js +3 -2
  63. package/dist/esm/client/components/ask-ai.js +51 -6
  64. package/dist/esm/client/components/confirm-dialog.js +37 -0
  65. package/dist/esm/client/components/cursor-pagination.js +19 -0
  66. package/dist/esm/client/components/filter-tabs.js +21 -0
  67. package/dist/esm/client/components/grievance-notice.js +27 -0
  68. package/dist/esm/client/components/link-button.js +17 -10
  69. package/dist/esm/client/components/logo.js +4 -3
  70. package/dist/esm/client/components/nav.js +40 -0
  71. package/dist/esm/client/components/plan-pricing.js +25 -0
  72. package/dist/esm/client/components/plans.js +2 -2
  73. package/dist/esm/client/components/platform-gate.js +45 -0
  74. package/dist/esm/client/components/shell-context.js +141 -0
  75. package/dist/esm/client/components/side-nav.js +48 -21
  76. package/dist/esm/client/components/sidebar.js +120 -0
  77. package/dist/esm/client/components/subscribe-dialog.js +3 -3
  78. package/dist/esm/client/components/subscription.js +2 -2
  79. package/dist/esm/client/components/user-menu.js +95 -38
  80. package/dist/esm/client/components/workspace-switcher.js +97 -0
  81. package/dist/esm/client/index.js +47 -4
  82. package/dist/esm/client/provider.js +53 -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 +4 -0
  86. package/dist/esm/mcp/index.js +85 -0
  87. package/dist/esm/mcp/tools.js +366 -0
  88. package/dist/esm/next/index.js +2 -2
  89. package/dist/esm/server/client.js +25 -2
  90. package/dist/esm/server/handler.js +203 -18
  91. package/dist/esm/server/http.js +6 -2
  92. package/dist/esm/server/resources/acting-as.js +13 -0
  93. package/dist/esm/server/resources/ai.js +22 -2
  94. package/dist/esm/server/resources/data.js +369 -0
  95. package/dist/esm/server/resources/identity.js +7 -3
  96. package/dist/esm/server/resources/kv.js +80 -0
  97. package/dist/esm/server/resources/platform.js +39 -0
  98. package/dist/esm/server/resources/sessions.js +1 -1
  99. package/dist/esm/server/resources/workspaces.js +70 -0
  100. package/dist/esm/store-schema.js +62 -0
  101. package/dist/esm/store.js +141 -0
  102. package/dist/esm/ui/empty-state.js +25 -0
  103. package/dist/esm/ui/index.js +34 -0
  104. package/dist/esm/ui/page-header.js +24 -0
  105. package/dist/esm/ui/page-shell.js +41 -0
  106. package/dist/esm/ui/section.js +17 -0
  107. package/dist/esm/ui/setting-row.js +11 -0
  108. package/dist/esm/ui/status-chip.js +14 -0
  109. package/dist/types/client/components/ai-screens.d.ts +25 -0
  110. package/dist/types/client/components/ai-widgets.d.ts +34 -1
  111. package/dist/types/client/components/app-shell.d.ts +65 -27
  112. package/dist/types/client/components/apps-grid.d.ts +11 -2
  113. package/dist/types/client/components/ask-ai.d.ts +47 -1
  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 +24 -0
  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 +27 -13
  125. package/dist/types/client/components/sidebar.d.ts +95 -0
  126. package/dist/types/client/components/user-menu.d.ts +68 -22
  127. package/dist/types/client/components/workspace-switcher.d.ts +49 -0
  128. package/dist/types/client/index.d.ts +29 -5
  129. package/dist/types/client/provider.d.ts +109 -6
  130. package/dist/types/client/use-checkout.d.ts +8 -4
  131. package/dist/types/errors.d.ts +1 -1
  132. package/dist/types/index.d.ts +6 -0
  133. package/dist/types/mcp/index.d.ts +74 -0
  134. package/dist/types/mcp/tools.d.ts +107 -0
  135. package/dist/types/next/index.d.ts +1 -1
  136. package/dist/types/server/client.d.ts +25 -2
  137. package/dist/types/server/http.d.ts +8 -2
  138. package/dist/types/server/index.d.ts +4 -0
  139. package/dist/types/server/resources/acting-as.d.ts +11 -0
  140. package/dist/types/server/resources/ai.d.ts +20 -2
  141. package/dist/types/server/resources/data.d.ts +258 -0
  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-schema.d.ts +52 -0
  146. package/dist/types/store.d.ts +96 -0
  147. package/dist/types/types.d.ts +369 -16
  148. package/dist/types/ui/empty-state.d.ts +34 -0
  149. package/dist/types/ui/index.d.ts +40 -0
  150. package/dist/types/ui/page-header.d.ts +34 -0
  151. package/dist/types/ui/page-shell.d.ts +39 -0
  152. package/dist/types/ui/section.d.ts +26 -0
  153. package/dist/types/ui/setting-row.d.ts +18 -0
  154. package/dist/types/ui/status-chip.d.ts +36 -0
  155. package/package.json +15 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,572 @@
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.16.0
7
+
8
+ ### Added
9
+
10
+ - **`@cosmoops/connect-sdk/mcp` - your store declaration, as MCP tools.**
11
+
12
+ ```ts
13
+ import { createStoreToolset } from "@cosmoops/connect-sdk/mcp";
14
+
15
+ const toolset = createStoreToolset({
16
+ collections: STORE_SCHEMA, // the array you already seed to Connect
17
+ data: connect.data,
18
+ workspaceId: session.workspaceId,
19
+ asUser: session.sessionToken, // every call runs as this user
20
+ });
21
+
22
+ server.setRequestHandler(ListToolsRequestSchema, () => toolset.listTools());
23
+ server.setRequestHandler(CallToolRequestSchema, (request) =>
24
+ toolset.callTool(request.params.name, request.params.arguments ?? {}),
25
+ );
26
+ ```
27
+
28
+ `Application.storeSchema` already says what each collection holds, which
29
+ fields may be filtered on, which query shapes the database can serve, and
30
+ whether it refuses everything but a create. Writing tools over that by hand
31
+ means keeping a second copy of all of it, and the copy is wrong the first time
32
+ a filter is added to a screen. So they are derived: adding a queryable field
33
+ and re-seeding widens the tools on the next boot, with no tool code to touch.
34
+
35
+ Three read tools per collection (`_get`, `_list`, `_aggregate`), and writes
36
+ only if you ask - `writes: "create" | "all"`, defaulting to `"none"`. An
37
+ `appendOnly` collection never derives an update or a delete, because Connect
38
+ would refuse them anyway and a tool that can only fail is worse than no tool.
39
+
40
+ Each tool's description carries the collection's queryable fields and its
41
+ declared composite indexes, and an unservable query is refused locally with
42
+ the index it would need - so a model adapts instead of spending a turn
43
+ discovering the shape.
44
+
45
+ **Every call carries an end user's session token.** Connect resolves it to a
46
+ member and checks the collection's declared `readPermission`/`writePermission`
47
+ against that member's live roles, so a toolset never reaches further than the
48
+ person it runs for, and a permission revoked in the console is gone from the
49
+ next call. Build one per request from `getConnectSession()`, or pass a
50
+ function for `asUser`.
51
+
52
+ There is deliberately **no scan tool**. `data.scan` reads across every
53
+ workspace and for that reason takes no `asUser` - deriving one would mean that
54
+ single call escalating to your API key while every tool beside it ran as the
55
+ user. Cross-tenant reads belong behind their own gate.
56
+
57
+ Server-only, like `/server` and `/next`: a browser import resolves to the
58
+ guard stub and fails the build. **No new dependency** - `listTools()` and
59
+ `callTool()` answer in the shapes the MCP protocol defines, so the official
60
+ MCP SDK stays yours to install and version.
61
+
62
+ **Pass `include`.** Measured on the reference applications, deriving over
63
+ every collection yields 45 read tools for club and 21 for CRM - whole-catalogue
64
+ numbers, and a model offered them chooses worse than one offered a handful.
65
+ Name the collections the surface at hand is about. Where a collection declares
66
+ more index shapes than are worth listing (club's ledger declares around
67
+ forty), the description states the rule and leaves the specifics to the
68
+ refusal, which names the exact index a rejected query needed.
69
+
70
+ - **`StoreCollectionDeclaration`, `describeQueryRejection` and
71
+ `storeIndexCovers`** on the root entry. The declaration type is the shape
72
+ Connect stores on `Application.storeSchema`, so an app deriving its schema
73
+ from its own query matrix can stop declaring a local copy of it.
74
+
75
+ ## 0.15.0
76
+
77
+ Your application's own database stops being a toy. Real queries, real
78
+ aggregations, and real atomicity - without ever handing you a Firebase
79
+ credential.
80
+
81
+ Everything here is `data.*`. The rest of the SDK is unchanged.
82
+
83
+ ### Added
84
+
85
+ - **`data.aggregate()` - count, sum and average in the database.**
86
+
87
+ ```ts
88
+ const { values } = await connect.data.aggregate({
89
+ workspaceId,
90
+ collection: "deals",
91
+ filters: [{ field: "outcome", op: "==", value: "won" }],
92
+ aggregations: [
93
+ { op: "count", alias: "count" },
94
+ { op: "sum", field: "value", alias: "total" },
95
+ ],
96
+ });
97
+ ```
98
+
99
+ Firestore bills an aggregation by the index entries it scans rather than by
100
+ the documents it would have returned, so a dashboard tile over ten thousand
101
+ rows costs a handful of reads and stays flat as the collection grows. Every
102
+ integration that had no aggregate call to reach for was paging the rows into
103
+ its own process and adding them up, which is an unbounded read wearing a
104
+ different name.
105
+
106
+ A `sum` or `avg` over nothing answers `null`, not `0` - "nothing matched"
107
+ and "the total is zero" are different facts.
108
+
109
+ - **`data.commit()` - a set of writes that all land or none do**, across as
110
+ many of your collections as they name.
111
+
112
+ You never hold a transaction: a handle passed across an HTTP boundary is a
113
+ server-side lock whose duration your network controls. Instead you read,
114
+ decide, and commit conditional on nothing having moved. Connect runs the
115
+ whole set as one short transaction on its side.
116
+
117
+ ```ts
118
+ import { increment, unchangedSince } from "@cosmoops/connect-sdk";
119
+
120
+ await connect.data.commit({
121
+ workspaceId,
122
+ operations: [
123
+ { op: "create", collection: "ledger", id, data: entry },
124
+ {
125
+ op: "merge",
126
+ collection: "funds",
127
+ id: entry.fundId,
128
+ data: { balancePaise: increment(entry.amountPaise) },
129
+ },
130
+ ],
131
+ });
132
+ ```
133
+
134
+ A stale precondition throws `ConnectError` with status 409 and writes
135
+ nothing.
136
+
137
+ - **`data.transaction(build)`** - the read/decide/commit loop with the retry
138
+ written once. `build` re-runs against fresh reads when a precondition went
139
+ stale, and only a 409 is retried: a 403 means the operation was wrong rather
140
+ than late.
141
+
142
+ - **Field transforms**: `increment`, `serverTimestamp`, `arrayUnion`,
143
+ `arrayRemove`, `deleteField`, plus the `unchangedSince(document)` and
144
+ `documentExists()` precondition builders. All exported from the root entry,
145
+ so a Server Component and a client component build the same write the same
146
+ way.
147
+
148
+ `increment` is the one that genuinely cannot be done any other way: it is
149
+ commutative, so two concurrent increments both land - where read-add-write
150
+ from your own process loses one of them however carefully it retries.
151
+
152
+ - **Composite indexes.** Declare them per collection on your application's
153
+ **Store** tab and Connect builds them in your database, reconciling on every
154
+ save - including removing ones you no longer declare, which would otherwise
155
+ be paid for on every document write forever.
156
+
157
+ This is what lifts the old one-filter ceiling. A query with no matching
158
+ index is still refused, but the refusal now names the index to declare
159
+ rather than telling you the shape is impossible.
160
+
161
+ - **`_updateTime`** on every document that came from a read - Firestore's own
162
+ write timestamp, and the value a `{ updateTime }` precondition is compared
163
+ against. Not `_updatedAt`: a transform in the same commit can move that
164
+ field, and cannot touch the write timestamp.
165
+
166
+ ### Changed - breaking
167
+
168
+ - **`data.query({ filter })` is now `data.query({ filters })`**, a list, ANDed.
169
+ `data.scan` takes the same. One filter becomes a one-element array:
170
+
171
+ ```ts
172
+ // Before
173
+ await connect.data.query({ workspaceId, collection: "tickets", filter: f });
174
+ // After
175
+ await connect.data.query({ workspaceId, collection: "tickets", filters: [f] });
176
+ ```
177
+
178
+ The server still accepts the singular `filter` query parameter, so an
179
+ integration on 0.14 or earlier keeps working against an upgraded Connect -
180
+ but the SDK method has moved and this one is a code change.
181
+
182
+ - **`Plan.billingInterval` and `SubscribedPlan.billingInterval` are now
183
+ `BillingInterval`**, which adds `"one-time"`. Connect has been able to
184
+ return it; the SDK type could not represent it, so a one-time plan was typed
185
+ as monthly and rendered "per month" - a false price rather than a cosmetic
186
+ slip. `ConnectPlans`, `ConnectSubscription` and the subscribe dialog now
187
+ label it "once".
188
+
189
+ Exhaustive `switch`es on a billing interval will need a third arm.
190
+
191
+ ### Added - operators
192
+
193
+ - `not-in` joins the query operators.
194
+
195
+ ## 0.14.0
196
+
197
+ One session answers "who is this?" once, and the subscribe button goes where
198
+ it says it does.
199
+
200
+ ### Changed - breaking
201
+
202
+ - **`applicationRole` and `platformAccess` are now `role`, `permissions` and
203
+ `staff`**, three flat fields on `exchangeCode()` and `getUser()` (and on the
204
+ `/session` bridge read).
205
+
206
+ The two blocks they replace were mutually exclusive by construction. A
207
+ Connect platform account belongs to no workspace - Connect enforces it - so
208
+ a member session always carried `platformAccess: null` and a staff session
209
+ always carried `applicationRole: { role: null, granted: [] }`. Two shapes
210
+ for one question meant every integration wrote the same null-check twice,
211
+ and the block that did not apply still had to be built and sent.
212
+
213
+ ```ts
214
+ // Before
215
+ const { applicationRole, platformAccess } = await connect.identity.getUser(t);
216
+ if (platformAccess) {
217
+ if (!platformAccess.roles.includes("platform_admin")) return forbidden();
218
+ return renderOperatorConsole();
219
+ }
220
+ if (!applicationRole.granted.includes("invoice:write")) return forbidden();
221
+
222
+ // After
223
+ const { role, permissions, staff } = await connect.identity.getUser(t);
224
+ if (staff) {
225
+ if (role !== "platform_admin") return forbidden();
226
+ return renderOperatorConsole();
227
+ }
228
+ if (!permissions.includes("invoice:write")) return forbidden();
229
+ ```
230
+
231
+ Read `staff` first: it decides which vocabulary the other two speak - your
232
+ own Permission Catalog's ids for a member, Connect's `platform:*` ids for an
233
+ operator. A staff session reports one `role` rather than a list, and it is
234
+ the highest-authority role your application accepts; the platform roles nest
235
+ (`developer` ⊆ `platform_manager` ⊆ `platform_admin`), so nothing is lost,
236
+ and `permissions` is still the union over every accepted role.
237
+
238
+ `ApplicationRole` and `PlatformAccess` are gone as exported types, replaced
239
+ by `SessionAccess`. `useConnectPlatformAccess()` keeps its shape -
240
+ `roles` is now a one-element array on a staff session, `[]` otherwise - so
241
+ `<ConnectPlatformGate accept={[...]}>` is unchanged.
242
+
243
+ The signed-out `/session` response carries the same three fields
244
+ (`role: null, permissions: [], staff: false`) rather than a bare
245
+ `applicationRole: null`, so a client never tells "signed out" apart from
246
+ "signed in with no role" by which keys happen to be present.
247
+
248
+ - **Workspace role ids lost their `org_` prefix** - `org_admin` is `admin`,
249
+ and the same for `finance`, `manager` and `member`. This is visible to you
250
+ only through `access.getUserRoles()` and `access.listRoles()`, which report
251
+ a *member's* resolved role on your application rather than these; if you
252
+ hardcoded a Connect workspace role id anywhere, it needs updating. Grants
253
+ stored under the old ids keep resolving.
254
+
255
+ ### Fixed
256
+
257
+ - **Subscribing no longer lands on a 404.** `buildCheckoutUrl` - and so
258
+ `<ConnectPlans>`'s subscribe button - built
259
+ `/applications/{id}/subscribe/{planId}`. The portal route customers can
260
+ actually open is `/apps/...`; `/applications` is a Console-only prefix. Every
261
+ first-time subscriber sent from a partner's own pricing page hit a 404. The
262
+ matching `resolveUrl` on the `subscription_missing` readiness blocker was
263
+ wrong the same way and is fixed with it.
264
+
265
+ - **`staff` actually reaches you.** Both `/sso/token` and `/sso/user` resolved
266
+ the staff block, typed it and documented it, then dropped it while building
267
+ the JSON response - the handlers are hand-built rather than schema-parsed,
268
+ so a forgotten field was silent. Staff access has been invisible to every
269
+ partner since it shipped in 0.11.0. If you turned it on and concluded it did
270
+ not work, it did not.
271
+
272
+ - `SDK_VERSION`, sent in the `User-Agent`, said `0.12.0` on a 0.13.0 package.
273
+
274
+ ## 0.13.0
275
+
276
+ The platform's grievance channel is now something you read rather than
277
+ something you copy.
278
+
279
+ ### Added
280
+
281
+ - **`connect.platform.getCompanyProfile()`** - the registered entity operating
282
+ Connect, and the grievance officer s.13 of India's DPDP Act 2023 requires a
283
+ Data Fiduciary to publish. Backed by the new
284
+ `GET /api/v1/application/company-profile`, which needs the new
285
+ **`company:read`** scope on your API key.
286
+
287
+ Your application is a Data Processor on that notice, not a second Data
288
+ Fiduciary, so the channel your privacy page names is this one. The shape it
289
+ replaces is four lines in an `.env`:
290
+
291
+ ```bash
292
+ # Before - a statutory contact that can go stale without anyone finding out.
293
+ APP_DATA_FIDUCIARY_NAME="..."
294
+ APP_GRIEVANCE_OFFICER_EMAIL="..."
295
+ ```
296
+
297
+ An officer resigns, and every application holding a copy is now publishing a
298
+ redressal channel that reaches nobody. Reading it removes that failure mode
299
+ outright, which is the entire reason it is an endpoint.
300
+
301
+ Every field is resolved server-side against the platform's published
302
+ defaults, so only `website` and `gstNumber` are nullable - there is nothing
303
+ here for you to write a fallback for.
304
+
305
+ - **`useConnectCompanyProfile()`** - the same read through the browser bridge.
306
+ The one hook here that does not depend on a session, so it resolves for a
307
+ signed-out visitor too: the moment a grievance channel most needs to be
308
+ readable is on a public privacy page.
309
+
310
+ - **`<ConnectGrievanceNotice />`** - the notice, rendered. The wording lives in
311
+ the SDK rather than being retyped per application, so a user comparing two
312
+ applications' privacy pages does not find two different accounts of who
313
+ their Data Fiduciary is.
314
+
315
+ ```tsx
316
+ import { ConnectGrievanceNotice } from "@cosmoops/connect-sdk/client";
317
+
318
+ <ConnectGrievanceNotice applicationName="CosmoOps CRM" />
319
+ ```
320
+
321
+ It renders `null` while loading and `null` on failure rather than a skeleton
322
+ or an error box - a red error in the middle of a legal notice reads as the
323
+ notice itself being broken. Call the hook directly if you want those states.
324
+
325
+ ### Upgrading
326
+
327
+ Existing API keys do not carry `company:read`. Tick it on the key's scopes in
328
+ the Console's Integration tab, or re-run the seed script for that application,
329
+ which widens scopes in place.
330
+
331
+ ## 0.12.0
332
+
333
+ The application shell is one column now. The top bar is gone, the rail holds
334
+ everything it used to, and the rail starts collapsed.
335
+
336
+ ### Changed - breaking
337
+
338
+ - **Two renames, both with deprecated aliases kept**, so nothing breaks on
339
+ upgrade:
340
+
341
+ | Was | Now | Why |
342
+ |---|---|---|
343
+ | `ConnectSideNav` | `ConnectSidebarNav` | The rail is a `ConnectSidebar` now; its parts are named after it |
344
+ | `ConnectUserMenu` | `ConnectUserButton` | It is a button that opens a modal. "Menu" had people looking for a dropdown that never existed |
345
+
346
+ **`AppShell` keeps its name.** It stays the one export in `/client` without
347
+ the `Connect` prefix, exactly as 0.8.0 decided. A host whose own layout
348
+ component is also called `AppShell` aliases it on import - `import {
349
+ AppShell as ConnectAppShell }` - which is the documented answer to that
350
+ clash and what both reference integrations do.
351
+
352
+ - **The top bar is removed.** With it go two props:
353
+
354
+ - **`title`** - a page's heading belongs to the page. Move it to
355
+ `ConnectPageShell`, from `@cosmoops/connect-sdk/ui`, which renders it next
356
+ to that page's own breadcrumbs and actions:
357
+
358
+ ```tsx
359
+ // before // after
360
+ <AppShell title="Billing"> <AppShell>
361
+ <BillingPage /> <ConnectPageShell title="Billing">…</ConnectPageShell>
362
+ </AppShell> </AppShell>
363
+ ```
364
+
365
+ - **`headerActions`** - there is no header to put them in. A control that
366
+ belongs to a page goes in that page's `ConnectPageShell`; a control that
367
+ belongs to the *application* goes in `sidebarFooter`, which now accepts
368
+ sidebar rows (see `ConnectSidebarRow`).
369
+
370
+ Below `md` a slim bar remains, carrying the drawer trigger and the lockup and
371
+ nothing else. A phone has no rail to hold that trigger.
372
+
373
+ - **`defaultCollapsed` now defaults to `true`.** The icon rail is the resting
374
+ state and expanding is the deliberate act - the rail's job is to be *there*,
375
+ not to be read. A stored preference from the user's last visit still wins.
376
+ Pass `defaultCollapsed={false}` to keep 0.11's behaviour.
377
+
378
+ - **`ConnectUserMenu`'s `compact` prop** is superseded by
379
+ `ConnectUserButton`'s `variant`. The alias maps `compact` to
380
+ `variant="compact"` and the old default to `variant="inline"`, so existing
381
+ call sites are unchanged.
382
+
383
+ - **`ConnectAppsGrid` now renders as a sidebar row by default**
384
+ (`variant="row"`). Pass `variant="icon"` for the square accent button it used
385
+ to be.
386
+
387
+ ### Added
388
+
389
+ - **`secondaryNav`** on `AppShell` - a second `ConnectNav`, pinned to
390
+ the foot of the rail above the account row. For the routes a product has but
391
+ does not want competing with its own: settings, help, plans, status. Same
392
+ shape, same active resolution, same audience filtering, same `gateExempt`
393
+ handling as `nav`.
394
+
395
+ - **The workspace switcher moved into the rail's header**, directly under the
396
+ brand, and is now collapse-aware: a 32px initials tile when the rail is
397
+ narrow, tile plus name plus chevron when it is open. It still hides itself
398
+ for an account with one workspace.
399
+
400
+ - **The account row is the last thing in the rail.** Circular avatar, name over
401
+ email, opening the same account modal as before. It collapses to the avatar
402
+ alone, which is still a complete account control.
403
+
404
+ - **`ConnectProfileModal`** - the account modal with no trigger of its own,
405
+ controlled by `isOpen`/`onOpenChange`. For an application that wants to keep
406
+ its own account control and put Connect's modal behind it.
407
+
408
+ - **The rail's pieces are exported**: `ConnectSidebar`, `ConnectSidebarHeader`,
409
+ `ConnectSidebarBody`, `ConnectSidebarFooter`, `ConnectSidebarRow`, plus
410
+ `ConnectShellProvider` / `useConnectShell` / `sidebarRowClass` and the
411
+ `RAIL_COLLAPSED` / `RAIL_EXPANDED` constants. `AppShell` composes
412
+ exactly these, so an application whose chrome the prop API cannot express
413
+ rebuilds the same rail from the same parts rather than forking it.
414
+
415
+ - **`ConnectLogo` takes `variant`** - `"full"` | `"mark"` | `"wordmark"`.
416
+ `collapsed` is deprecated in favour of `variant="mark"`.
417
+
418
+ ### Fixed
419
+
420
+ - **The collapsed Ask AI panel occupied 400px at `lg` and up.** Its shell
421
+ carried `lg:w-[400px]` and `lg:w-0` at once; Tailwind emits arbitrary values
422
+ after scale values, so the override never applied and the closed panel sat
423
+ permanently in the layout, eating that much of the content pane on every page
424
+ with `askAi` on. Only the `translate-x` hid it below `lg`, which is why it
425
+ looked right on a phone and wrong on a laptop.
426
+
427
+ - **Sidebar contrast, in both directions.** Resting nav rows were
428
+ `--foreground` - 17.6:1 against the rail in light, 15.8:1 in dark - so eight
429
+ destinations shouted as loudly as body copy and the selected row had to win
430
+ on background alone. They are `--muted` now (4.8:1 / 6.2:1, past AA), with
431
+ `--foreground` on hover. The *active* row was `text-accent` on
432
+ `bg-accent-soft`, which measures **2.8:1 in light and 4.0:1 in dark - both
433
+ below AA**, on the one row a user most needs to read; it now uses HeroUI's
434
+ derived `--accent-soft-foreground` (4.8:1 / 5.6:1), so a host that re-themes
435
+ `--accent` keeps the contrast.
436
+
437
+ - **The account row and the app launcher looked permanently selected.** Both
438
+ are HeroUI `<Button variant="tertiary">` because they trigger overlays, and
439
+ `.button--tertiary` sets a real `--button-bg`. Next to the plain anchors the
440
+ nav renders, three rows in the rail appeared highlighted at once.
441
+
442
+ - **The account avatar is a circle.** HeroUI's `.avatar` is `rounded-2xl`/
443
+ `rounded-3xl` - right for the media component, wrong for a person, and it
444
+ read as a rendering fault beside circular status dots.
445
+
446
+ ### Motion
447
+
448
+ The rail animates its **width and nothing else**, via `motion`. Every row is
449
+ laid out so its glyph sits on the same 32px horizontal centre in both states,
450
+ and every label is clipped by the shrinking rail rather than re-measured - so
451
+ collapsing costs no reflow on any row, and no child moves sideways. Section
452
+ headings hold their box and cross-fade a rule in place of their text, so
453
+ nothing jumps vertically either. A tween rather than a spring: an overshooting
454
+ *width* drags every clip edge past its resting place and back, which reads as
455
+ the labels wobbling.
456
+
457
+ ## 0.11.0
458
+
459
+ ### Changed - breaking
460
+
461
+ - **`ConnectUser.workspaceId`, `workspaceName` and `workspaceType` are now
462
+ nullable.** They are `null` for exactly one thing: a **Connect platform
463
+ staff** session, described below. Every member session carries them as
464
+ before.
465
+
466
+ If you never turn staff access on for your application - it is off by
467
+ default, and only a Connect platform admin can turn it on - this is a type
468
+ narrow and nothing else. Reading `user.workspaceId` where a `string` is
469
+ wanted becomes a compile error, and `?? ""` is the wrong fix: decide what
470
+ your tenanting does with a session that has no tenant before you enable the
471
+ feature.
472
+
473
+ The honest alternative was a synthesized placeholder workspace, which would
474
+ have kept the types quiet while flowing a fabricated tenant id into the ID
475
+ token's `org_id` claim and into whatever you key your own rows on. A
476
+ compile error is cheaper than that.
477
+
478
+ ### Added
479
+
480
+ - **Connect platform staff sign-in.** Connect's own operators
481
+ (`platform_admin`, `platform_manager`, `developer`) can now sign into your
482
+ application, so an operator console can live inside your product and be
483
+ authenticated by Connect rather than by a second login you maintain.
484
+
485
+ These accounts belong to no workspace - Connect enforces that, they cannot
486
+ own or join one - which is why the session is shaped differently: no
487
+ `workspaceId`, `subscription: null`, and `applicationRole.granted: []`.
488
+ Platform authority is deliberately *not* reported through the member-role
489
+ field. What staff hold is the new block:
490
+
491
+ ```ts
492
+ const { platformAccess } = await client.identity.getUser(sessionToken);
493
+ if (!platformAccess) return forbidden();
494
+ if (!platformAccess.roles.includes("platform_admin")) return forbidden();
495
+ ```
496
+
497
+ `roles` is the intersection of what the account holds and what your
498
+ application accepts - never Connect's full role list for that person.
499
+
500
+ Off by default and per-application. Ask a Connect platform admin to enable it
501
+ on your catalogue entry, naming the roles you accept; until then
502
+ `platformAccess` is always `null` and nothing below does anything.
503
+
504
+ - **`platformAccess` on `exchangeCode()` and `getUser()`** - `PlatformAccess |
505
+ null`. Live on `getUser()`, like `subscription` and `applicationRole`: a
506
+ platform role revoked, or staff access turned off for your application,
507
+ comes back `null` there and Connect has already revoked the session.
508
+
509
+ - **`<ConnectPlatformGate>`** and **`useConnectPlatformAccess()`** in
510
+ `/client`. The gate renders its children only for a staff session, and takes
511
+ an optional `roles` allowlist when your console has tiers of its own.
512
+
513
+ Like every gate in this package it is a **UX gate, not a security
514
+ boundary** - and that matters more here than anywhere else, because what
515
+ sits behind it is usually an operator console. Re-check `platformAccess`
516
+ server-side on every route it reveals.
517
+
518
+ - **`ConnectNavItem.audience`** - `"staff"` or `"member"`. `AppShell` filters
519
+ the rail by it off the live session, so your operator routes appear only for
520
+ staff and your billing/team/usage routes disappear for them. A parent whose
521
+ children all disappear is dropped with them. `filterNavForAudience` is
522
+ exported for a rail you render yourself.
523
+
524
+ - `ConnectAccessGate` passes a staff session straight through. Every state it
525
+ reports is a fact about a workspace, and `subscription_missing` would
526
+ otherwise send an operator to your plan picker to buy a plan for a workspace
527
+ that does not exist.
528
+
529
+ ### Note
530
+
531
+ Workspace-scoped bridge routes (`/entitlements`, `/addons`, `/integrations`,
532
+ `/ai/chat`, `/data/*`, `/kv/*`) answer `403 no_workspace` for a staff session.
533
+ There is no tenant to read, and the only other option would be letting the
534
+ request name one - which is precisely what `createConnectHandler` exists to
535
+ prevent. An operator console reads `platformAccess` and talks to your own
536
+ backend for anything tenant-shaped.
537
+
538
+ ## 0.10.0
539
+
540
+ ### Added
541
+
542
+ - **`@cosmoops/connect-sdk/ui`** - the page-composition layer, and a new entry
543
+ point. `ConnectPageShell` (with `.Header` and `.Async`), `ConnectPageHeader`,
544
+ `ConnectSection`, `ConnectEmptyState`, `ConnectSettingRow`,
545
+ `ConnectStatusChip`.
546
+
547
+ The SDK already shipped the *application* chrome - the shell, the launcher,
548
+ the account menu, the plan grid - and nothing between that and raw HeroUI. So
549
+ every integration re-derived the same six primitives, and they drifted the
550
+ same way each time: the page action ending up below the description instead
551
+ of beside the title, section headings rendered as `h2` so every block became
552
+ a sibling of its own page, and one empty state doing duty for both "you have
553
+ none" and "none match this filter". Those are the three bugs these components
554
+ are shaped to make unrepresentable, not merely to style consistently.
555
+
556
+ **A separate entry from `/client`, deliberately.** That module's barrel
557
+ carries a `"use client"` directive, so re-exporting a presentational `<Card>`
558
+ wrapper through it would turn every page shell into a client boundary. Nothing
559
+ in `/ui` has a hook or state, so it renders in a Server Component and stays
560
+ out of the browser bundle.
561
+
562
+ ```tsx
563
+ import { ConnectPageShell, ConnectSection } from "@cosmoops/connect-sdk/ui";
564
+ ```
565
+
566
+ - **`ConnectConfirmDialog`, `ConnectCursorPagination`, `ConnectFilterTabs`** in
567
+ `@cosmoops/connect-sdk/client` - the interactive counterparts, which hold
568
+ state and so belong on the browser side. `ConnectCursorPagination` has no page
569
+ numbers on purpose: a keyset-paginated list cannot know its page count without
570
+ a second billed index scan over the same filter, for a number nobody acts on.
571
+
6
572
  ## 0.9.0
7
573
 
8
574
  ### Added