@cosmoops/connect-sdk 0.6.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 (138) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +273 -91
  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 +42 -7
  6. package/dist/cjs/client/components/account-sessions.js +3 -3
  7. package/dist/cjs/client/components/ai-format.js +135 -0
  8. package/dist/cjs/client/components/ai-widgets.js +232 -0
  9. package/dist/cjs/client/components/app-shell.js +9 -8
  10. package/dist/cjs/client/components/apps-grid.js +3 -3
  11. package/dist/cjs/client/components/ask-ai.js +334 -0
  12. package/dist/cjs/client/components/feature-gate.js +2 -2
  13. package/dist/cjs/client/components/integrations-modal.js +1 -1
  14. package/dist/cjs/client/components/logo.js +3 -3
  15. package/dist/cjs/client/components/nav.js +1 -1
  16. package/dist/cjs/client/components/plan-pricing.js +1 -1
  17. package/dist/cjs/client/components/plans.js +6 -6
  18. package/dist/cjs/client/components/readiness.js +3 -3
  19. package/dist/cjs/client/components/side-nav.js +1 -1
  20. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  21. package/dist/cjs/client/components/subscription.js +1 -1
  22. package/dist/cjs/client/components/usage-meter.js +1 -1
  23. package/dist/cjs/client/components/user-menu.js +31 -8
  24. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  25. package/dist/cjs/client/index.js +32 -4
  26. package/dist/cjs/client/mark.js +1 -1
  27. package/dist/cjs/client/provider.js +50 -7
  28. package/dist/cjs/client/use-checkout.js +3 -3
  29. package/dist/cjs/errors.js +4 -4
  30. package/dist/cjs/helpers.js +1 -1
  31. package/dist/cjs/index.js +1 -1
  32. package/dist/cjs/next/index.js +2 -2
  33. package/dist/cjs/server/client.js +15 -15
  34. package/dist/cjs/server/handler.js +56 -13
  35. package/dist/cjs/server/http.js +47 -25
  36. package/dist/cjs/server/resources/access.js +3 -3
  37. package/dist/cjs/server/resources/ai.js +1 -1
  38. package/dist/cjs/server/resources/analytics.js +2 -2
  39. package/dist/cjs/server/resources/apps.js +4 -4
  40. package/dist/cjs/server/resources/billing.js +5 -5
  41. package/dist/cjs/server/resources/drive.js +4 -4
  42. package/dist/cjs/server/resources/entitlements.js +48 -7
  43. package/dist/cjs/server/resources/identity.js +6 -4
  44. package/dist/cjs/server/resources/integrations.js +13 -13
  45. package/dist/cjs/server/resources/logs.js +3 -3
  46. package/dist/cjs/server/resources/sessions.js +6 -6
  47. package/dist/cjs/server/webhooks.js +4 -4
  48. package/dist/cjs/types.js +1 -1
  49. package/dist/esm/cli/doctor.js +3 -3
  50. package/dist/esm/cli/init.js +3 -3
  51. package/dist/esm/client/components/access-gate.js +43 -8
  52. package/dist/esm/client/components/account-sessions.js +3 -3
  53. package/dist/esm/client/components/ai-format.js +129 -0
  54. package/dist/esm/client/components/ai-widgets.js +223 -0
  55. package/dist/esm/client/components/app-shell.js +8 -7
  56. package/dist/esm/client/components/apps-grid.js +3 -3
  57. package/dist/esm/client/components/ask-ai.js +331 -0
  58. package/dist/esm/client/components/feature-gate.js +2 -2
  59. package/dist/esm/client/components/integrations-modal.js +1 -1
  60. package/dist/esm/client/components/logo.js +3 -3
  61. package/dist/esm/client/components/nav.js +1 -1
  62. package/dist/esm/client/components/plan-pricing.js +1 -1
  63. package/dist/esm/client/components/plans.js +6 -6
  64. package/dist/esm/client/components/readiness.js +3 -3
  65. package/dist/esm/client/components/side-nav.js +1 -1
  66. package/dist/esm/client/components/subscribe-dialog.js +2 -2
  67. package/dist/esm/client/components/subscription.js +1 -1
  68. package/dist/esm/client/components/usage-meter.js +1 -1
  69. package/dist/esm/client/components/user-menu.js +31 -8
  70. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  71. package/dist/esm/client/index.js +14 -3
  72. package/dist/esm/client/mark.js +1 -1
  73. package/dist/esm/client/provider.js +48 -7
  74. package/dist/esm/client/use-checkout.js +3 -3
  75. package/dist/esm/errors.js +4 -4
  76. package/dist/esm/helpers.js +1 -1
  77. package/dist/esm/index.js +1 -1
  78. package/dist/esm/next/index.js +2 -2
  79. package/dist/esm/server/client.js +15 -15
  80. package/dist/esm/server/handler.js +56 -13
  81. package/dist/esm/server/http.js +47 -25
  82. package/dist/esm/server/resources/access.js +3 -3
  83. package/dist/esm/server/resources/ai.js +1 -1
  84. package/dist/esm/server/resources/analytics.js +2 -2
  85. package/dist/esm/server/resources/apps.js +4 -4
  86. package/dist/esm/server/resources/billing.js +5 -5
  87. package/dist/esm/server/resources/drive.js +4 -4
  88. package/dist/esm/server/resources/entitlements.js +48 -7
  89. package/dist/esm/server/resources/identity.js +6 -4
  90. package/dist/esm/server/resources/integrations.js +13 -13
  91. package/dist/esm/server/resources/logs.js +3 -3
  92. package/dist/esm/server/resources/sessions.js +6 -6
  93. package/dist/esm/server/webhooks.js +4 -4
  94. package/dist/esm/types.js +1 -1
  95. package/dist/theme.css +87 -0
  96. package/dist/types/client/components/access-gate.d.ts +18 -4
  97. package/dist/types/client/components/account-sessions.d.ts +1 -1
  98. package/dist/types/client/components/ai-format.d.ts +24 -0
  99. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  100. package/dist/types/client/components/app-shell.d.ts +25 -9
  101. package/dist/types/client/components/apps-grid.d.ts +1 -1
  102. package/dist/types/client/components/ask-ai.d.ts +73 -0
  103. package/dist/types/client/components/feature-gate.d.ts +1 -1
  104. package/dist/types/client/components/integrations-modal.d.ts +1 -1
  105. package/dist/types/client/components/logo.d.ts +5 -5
  106. package/dist/types/client/components/nav.d.ts +3 -3
  107. package/dist/types/client/components/plan-pricing.d.ts +2 -2
  108. package/dist/types/client/components/readiness.d.ts +2 -2
  109. package/dist/types/client/components/side-nav.d.ts +1 -1
  110. package/dist/types/client/components/subscribe-dialog.d.ts +3 -3
  111. package/dist/types/client/components/usage-meter.d.ts +1 -1
  112. package/dist/types/client/components/user-menu.d.ts +4 -3
  113. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  114. package/dist/types/client/index.d.ts +11 -4
  115. package/dist/types/client/mark.d.ts +1 -1
  116. package/dist/types/client/provider.d.ts +52 -9
  117. package/dist/types/client/use-checkout.d.ts +3 -3
  118. package/dist/types/errors.d.ts +4 -4
  119. package/dist/types/helpers.d.ts +1 -1
  120. package/dist/types/index.d.ts +1 -1
  121. package/dist/types/next/index.d.ts +1 -1
  122. package/dist/types/server/client.d.ts +13 -13
  123. package/dist/types/server/handler.d.ts +2 -2
  124. package/dist/types/server/http.d.ts +6 -1
  125. package/dist/types/server/resources/access.d.ts +3 -3
  126. package/dist/types/server/resources/ai.d.ts +2 -2
  127. package/dist/types/server/resources/analytics.d.ts +2 -2
  128. package/dist/types/server/resources/apps.d.ts +4 -4
  129. package/dist/types/server/resources/billing.d.ts +4 -4
  130. package/dist/types/server/resources/drive.d.ts +4 -4
  131. package/dist/types/server/resources/entitlements.d.ts +37 -5
  132. package/dist/types/server/resources/identity.d.ts +2 -2
  133. package/dist/types/server/resources/integrations.d.ts +9 -9
  134. package/dist/types/server/resources/logs.d.ts +3 -3
  135. package/dist/types/server/resources/sessions.d.ts +5 -5
  136. package/dist/types/server/webhooks.d.ts +4 -4
  137. package/dist/types/types.d.ts +235 -61
  138. package/package.json +8 -2
@@ -19,6 +19,28 @@ function initials(name, email) {
19
19
  function openExternal(url) {
20
20
  window.open(url, "_blank", "noopener,noreferrer");
21
21
  }
22
+ /**
23
+ * Connect's profile page, with the context it needs to offer a way back here.
24
+ *
25
+ * Connect answers `/profile` for any signed-in user, so the bare URL works on
26
+ * its own - these two params only add the "Back to <your app>" banner. It
27
+ * validates `return_to` against this `client_id`'s registered redirect
28
+ * origins and silently drops the banner if they don't match, so a wrong or
29
+ * stale `appUrl`/`clientId` pairing degrades to what it did before rather
30
+ * than erroring.
31
+ *
32
+ * `window.location.href` rather than a configured home URL: the point is to
33
+ * come back to the page they left, and only the browser knows which one that
34
+ * is.
35
+ */
36
+ function profileUrl(appUrl, clientId) {
37
+ const url = new URL("/profile", appUrl);
38
+ url.searchParams.set("client_id", clientId);
39
+ if (typeof window !== "undefined") {
40
+ url.searchParams.set("return_to", window.location.href);
41
+ }
42
+ return url.toString();
43
+ }
22
44
  /**
23
45
  * The member's photo, with their initials underneath it.
24
46
  *
@@ -97,7 +119,7 @@ function DestinationTile({ icon, label, url, }) {
97
119
  * in place, which is why it sits alone in the footer rather than as a fourth
98
120
  * item in the same list as three navigations.
99
121
  *
100
- * The session list is account-wide, not scoped to this service - see
122
+ * The session list is account-wide, not scoped to this application - see
101
123
  * `ConnectUserSession`'s doc comment in `types.ts`. Appearance only renders
102
124
  * when both `theme` and `onThemeChange` are supplied.
103
125
  *
@@ -114,7 +136,7 @@ function DestinationTile({ icon, label, url, }) {
114
136
  * the modal programmatically; sign-out uses `slot="close"` instead.
115
137
  */
116
138
  export function ConnectUserMenu({ compact = false, onSignOut, className, theme, onThemeChange, }) {
117
- const { session, isLoading, signOut, appUrl } = useConnect();
139
+ const { session, isLoading, signOut, appUrl, clientId } = useConnect();
118
140
  if (isLoading) {
119
141
  return (_jsxs("div", { className: `flex items-center gap-2 ${className ?? ""}`, children: [_jsx(Skeleton, { className: "size-8 shrink-0 rounded-full" }), !compact && _jsx(Skeleton, { className: "h-4 w-24" })] }));
120
142
  }
@@ -122,13 +144,13 @@ export function ConnectUserMenu({ compact = false, onSignOut, className, theme,
122
144
  if (!user)
123
145
  return null;
124
146
  const label = user.displayName ?? user.email ?? "Account";
125
- const role = session?.serviceRole?.role;
147
+ const role = session?.applicationRole?.role;
126
148
  const planChip = subscriptionChip(session?.subscription ?? null);
127
149
  return (_jsxs(Modal, { children: [_jsxs(Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
128
150
  // `pl-1` against the default right padding is optical, not sloppy: a
129
151
  // circular avatar needs less inset than a text label to look evenly
130
152
  // seated in the button.
131
- className: `gap-2 pl-1 ${className ?? ""}`, children: [_jsx(UserAvatar, { user: user, size: "sm" }), !compact && _jsx("span", { className: "text-sm font-medium text-foreground", children: label })] }), _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { size: "md", scroll: "inside", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Account" }) }), _jsxs(Modal.Body, { className: "flex flex-col gap-5", children: [_jsxs("div", { className: `flex items-start gap-3 rounded-xl border border-border ${PANEL_FILL} p-4`, children: [_jsx(UserAvatar, { user: user, size: "lg" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && _jsx("p", { className: "truncate text-xs text-muted", children: user.email }), _jsx("p", { className: "truncate text-xs text-muted", children: user.organizationName }), (role || planChip || !user.emailVerified) && (_jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && (_jsx(Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && (_jsx(Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(DestinationTile, { icon: _jsx(UserRound, { className: ICON, "aria-hidden": "true" }), label: "Profile", url: `${appUrl}/profile` }), _jsx(DestinationTile, { icon: _jsx(CreditCard, { className: ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), _jsx(DestinationTile, { icon: _jsx(ConnectMark, { className: ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), _jsx(ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
153
+ className: `gap-2 pl-1 ${className ?? ""}`, children: [_jsx(UserAvatar, { user: user, size: "sm" }), !compact && _jsx("span", { className: "text-sm font-medium text-foreground", children: label })] }), _jsx(Modal.Backdrop, { children: _jsx(Modal.Container, { size: "md", scroll: "inside", children: _jsxs(Modal.Dialog, { children: [_jsx(Modal.CloseTrigger, {}), _jsx(Modal.Header, { children: _jsx(Modal.Heading, { children: "Account" }) }), _jsxs(Modal.Body, { className: "flex flex-col gap-5", children: [_jsxs("div", { className: `flex items-start gap-3 rounded-xl border border-border ${PANEL_FILL} p-4`, children: [_jsx(UserAvatar, { user: user, size: "lg" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("p", { className: "truncate text-sm font-semibold text-foreground", children: label }), user.email && _jsx("p", { className: "truncate text-xs text-muted", children: user.email }), _jsx("p", { className: "truncate text-xs text-muted", children: user.workspaceName }), (role || planChip || !user.emailVerified) && (_jsxs("div", { className: "mt-1.5 flex flex-wrap items-center gap-1.5", children: [role && (_jsx(Chip, { size: "sm", variant: "soft", color: "accent", children: humanizeRole(role) })), planChip, !user.emailVerified && (_jsx(Chip, { size: "sm", variant: "soft", color: "warning", children: "Email unverified" }))] }))] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-2", children: [_jsx(DestinationTile, { icon: _jsx(UserRound, { className: ICON, "aria-hidden": "true" }), label: "Profile", url: profileUrl(appUrl, clientId) }), _jsx(DestinationTile, { icon: _jsx(CreditCard, { className: ICON, "aria-hidden": "true" }), label: "Billing", url: `${appUrl}/billing` }), _jsx(DestinationTile, { icon: _jsx(ConnectMark, { className: ICON }), label: "Connect", url: appUrl })] }), theme && onThemeChange && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("p", { className: "text-sm font-medium text-foreground", children: "Appearance" }), _jsx(ToggleButtonGroup, { fullWidth: true, "aria-label": "Theme", selectionMode: "single", disallowEmptySelection: true, selectedKeys: [theme], onSelectionChange: (keys) => {
132
154
  const [next] = [...keys];
133
155
  if (typeof next === "string")
134
156
  onThemeChange(next);
@@ -149,7 +171,7 @@ export function ConnectManageSubscriptionButton({ className }) {
149
171
  * Invoices and payment history, on Connect.
150
172
  *
151
173
  * A deep link rather than a component that lists them, and that is not a
152
- * placeholder: nothing in `/api/v1/service/*` exposes invoices, so an SDK-side
174
+ * placeholder: nothing in `/api/v1/application/*` exposes invoices, so an SDK-side
153
175
  * list would have nothing to read. It is also the right answer regardless -
154
176
  * a GST tax invoice is a legal document with a fixed format, downloadable and
155
177
  * emailed from one place, and a partner-rendered copy of one would be a second
@@ -167,13 +189,14 @@ export function ConnectInvoicesButton({ className }) {
167
189
  * `ConnectHomeButton`.
168
190
  *
169
191
  * It was reachable only from inside `ConnectUserMenu`, which meant an app that
170
- * builds its own account menu had to hardcode `${appUrl}/profile` to offer it.
192
+ * builds its own account menu had to hardcode `${appUrl}/profile` to offer it -
193
+ * and got no `return_to`, so the user had no way back.
171
194
  */
172
195
  export function ConnectProfileButton({ className }) {
173
- const { session, appUrl } = useConnect();
196
+ const { session, appUrl, clientId } = useConnect();
174
197
  if (!session?.user)
175
198
  return null;
176
- return (_jsxs(Button, { variant: "tertiary", className: className, onPress: () => openExternal(`${appUrl}/profile`), children: [_jsx(UserRound, { className: "size-4 shrink-0" }), "Profile", _jsx(ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
199
+ return (_jsxs(Button, { variant: "tertiary", className: className, onPress: () => openExternal(profileUrl(appUrl, clientId)), children: [_jsx(UserRound, { className: "size-4 shrink-0" }), "Profile", _jsx(ExternalLink, { className: "size-3.5 shrink-0 text-muted" })] }));
177
200
  }
178
201
  /** A plain "Open Connect" button, for a footer or a help menu. */
179
202
  export function ConnectHomeButton({ className }) {
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useConnect } from "../provider.js";
4
+ /**
5
+ * Renders `children` only for a workspace of the given type.
6
+ *
7
+ * ```tsx
8
+ * <ConnectWorkspaceTypeGate type="business">
9
+ * <TeamMembersTable />
10
+ * </ConnectWorkspaceTypeGate>
11
+ * ```
12
+ *
13
+ * The point is dead controls, not access: an `"individual"` workspace has
14
+ * exactly one seat and can never have another, so a members table, an invite
15
+ * button and a seat meter are all things that can only ever fail there.
16
+ * Hiding them is a better answer than letting someone press one and read an
17
+ * error about an account shape they did not choose.
18
+ *
19
+ * This is emphatically **not** the audience gate. It does not know what your
20
+ * application is sold to and will happily render a business-only subtree to a
21
+ * business workspace that has no business being in your application at all -
22
+ * that is `<ConnectAccessGate>`'s job, off the `audience_mismatch` readiness
23
+ * blocker. Use this one *inside* an application both audiences can reach, to
24
+ * vary what each sees.
25
+ *
26
+ * Like every gate in this package it is a **UX gate, not a security
27
+ * boundary**: the browser can ignore it. Anything that must not happen for an
28
+ * individual workspace has to be refused on your server too.
29
+ *
30
+ * Renders `loading` - not `fallback` - while the session is unknown, so a
31
+ * business account never flickers through the individual-account layout.
32
+ */
33
+ export function ConnectWorkspaceTypeGate({ type, children, fallback = null, loading = null, }) {
34
+ const { session, isLoading } = useConnect();
35
+ const workspaceType = session?.user?.workspaceType ?? null;
36
+ if (isLoading)
37
+ return _jsx(_Fragment, { children: loading });
38
+ // Signed out is not "the other type" - there is no workspace to be a type
39
+ // of yet, and rendering the fallback here would put an individual-account
40
+ // layout in front of a visitor who has not signed in.
41
+ if (!workspaceType)
42
+ return _jsx(_Fragment, { children: loading });
43
+ const accepted = Array.isArray(type) ? type : [type];
44
+ return _jsx(_Fragment, { children: accepted.includes(workspaceType) ? children : fallback });
45
+ }
@@ -8,10 +8,10 @@
8
8
  *
9
9
  * Styling is HeroUI v3 semantic tokens only (`accent`, `muted`, `surface`,
10
10
  * `separator`, ...), never a hardcoded colour - so these components inherit
11
- * whatever theme the host service platform already configured, and one theme
11
+ * whatever theme the host application platform already configured, and one theme
12
12
  * change lands identically in all of them.
13
13
  */
14
- export { ConnectProvider, useConnect, useConnectUser, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
14
+ export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
15
15
  // Re-exported from the root entry rather than kept there alone: it is a pure
16
16
  // helper, but every caller is UI code, and making a component file reach two
17
17
  // entry points for one function is friction with nothing behind it.
@@ -28,7 +28,7 @@ export { ConnectAccountSessions } from "./components/account-sessions.js";
28
28
  // pieces it composes rather than in a section of its own - an integration
29
29
  // reaching for a sidebar wants to find the launcher and the account menu in
30
30
  // the same breath, since the shell already mounts both.
31
- export { ConnectAppShell } from "./components/app-shell.js";
31
+ export { AppShell } from "./components/app-shell.js";
32
32
  export { ConnectSideNav } from "./components/side-nav.js";
33
33
  export { useActivePath } from "./use-active-path.js";
34
34
  // The nav model is plain data and pure functions - a host generating its nav
@@ -37,6 +37,16 @@ export { useActivePath } from "./use-active-path.js";
37
37
  // page title derived from it cannot disagree with the rail.
38
38
  export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemActive, gateExemptPaths, } from "./components/nav.js";
39
39
  export { ConnectAppsGrid } from "./components/apps-grid.js";
40
+ // `AppShell askAi` mounts this; exported for an app that wants the panel
41
+ // without the frame, or wants it somewhere other than the shell's flex row
42
+ // (at which point it overlays rather than pushes - see its doc comment).
43
+ export { ConnectAskAi } from "./components/ask-ai.js";
44
+ // The seven widgets an Ask AI answer can draw, and the formatter they share -
45
+ // exported so a partner rendering Connect's chat somewhere else entirely (a
46
+ // support console, an embedded assistant) gets the same table, chart and metric
47
+ // tiles rather than reimplementing them from the wire types.
48
+ export { AiTable, AiChart, AiMetrics, AiTimeline, AiDetails, AiSteps, AiSources, } from "./components/ai-widgets.js";
49
+ export { formatAiValue, isNumericAiFormat, aiStatusTone, AI_EMPTY_VALUE, AI_TONE_CHIP, AI_TONE_DOT, } from "./components/ai-format.js";
40
50
  export { ConnectIntegrationsModal } from "./components/integrations-modal.js";
41
51
  export { ConnectPlans } from "./components/plans.js";
42
52
  export { ConnectSubscribeDialog } from "./components/subscribe-dialog.js";
@@ -49,6 +59,7 @@ export { ConnectSubscriptionGate, ConnectNoSubscriptionCard, ConnectSubscription
49
59
  export { ConnectReadinessBanner, ConnectReadinessGate } from "./components/readiness.js";
50
60
  export { ConnectAccessGate, useConnectAccessGate } from "./components/access-gate.js";
51
61
  export { ConnectFeatureGate, useConnectFeature } from "./components/feature-gate.js";
62
+ export { ConnectWorkspaceTypeGate } from "./components/workspace-type-gate.js";
52
63
  export { ConnectUsageMeter } from "./components/usage-meter.js";
53
64
  export { ConnectPlansPage } from "./components/plans-page.js";
54
65
  export { useConnectCheckout, buildCheckoutUrl } from "./use-checkout.js";
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * The Connect mark - concentric rings on a solid accent badge.
5
5
  *
6
6
  * Painted from `--accent` / `--accent-foreground`, the same HeroUI tokens the
7
- * host app themes, so the badge tracks whatever palette a service platform has
7
+ * host app themes, so the badge tracks whatever palette an application platform has
8
8
  * configured rather than carrying a hardcoded CosmoOps green into someone
9
9
  * else's brand.
10
10
  */
@@ -111,6 +111,20 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
111
111
  const invalidateResource = useCallback((path) => {
112
112
  cache.current.delete(`${basePath}${path}`);
113
113
  }, [basePath]);
114
+ const streamResource = useCallback(async (path, body, options) => {
115
+ const response = await (fetchImpl ?? globalThis.fetch)(`${basePath}${path}`, {
116
+ method: "POST",
117
+ credentials: "same-origin",
118
+ headers: { accept: "text/event-stream", "content-type": "application/json" },
119
+ body: JSON.stringify(body),
120
+ signal: options?.signal,
121
+ });
122
+ if (!response.ok) {
123
+ const envelope = await response.json().catch(() => null);
124
+ throw new Error(envelope?.error?.message ?? `Connect bridge returned ${response.status}`);
125
+ }
126
+ return response;
127
+ }, [basePath, fetchImpl]);
114
128
  const refresh = useCallback(async () => {
115
129
  cache.current.clear();
116
130
  try {
@@ -122,7 +136,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
122
136
  // error, so a Login button still renders rather than the UI hanging on
123
137
  // skeletons forever.
124
138
  setResult({
125
- session: { user: null, subscription: null, serviceRole: null },
139
+ session: { user: null, subscription: null, applicationRole: null, audience: null },
126
140
  error: cause instanceof Error ? cause.message : "Could not reach Connect",
127
141
  });
128
142
  }
@@ -171,7 +185,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
171
185
  credentials: "same-origin",
172
186
  }).catch(() => { });
173
187
  cache.current.clear();
174
- setResult({ session: { user: null, subscription: null, serviceRole: null }, error: null });
188
+ setResult({ session: { user: null, subscription: null, applicationRole: null, audience: null }, error: null });
175
189
  }, [basePath, fetchImpl]);
176
190
  const value = useMemo(() => ({
177
191
  clientId,
@@ -185,6 +199,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
185
199
  fetchResource,
186
200
  mutateResource,
187
201
  invalidateResource,
202
+ streamResource,
188
203
  }), [
189
204
  clientId,
190
205
  appUrl,
@@ -197,6 +212,7 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
197
212
  fetchResource,
198
213
  mutateResource,
199
214
  invalidateResource,
215
+ streamResource,
200
216
  ]);
201
217
  return _jsx(ConnectContext.Provider, { value: value, children: children });
202
218
  }
@@ -211,6 +227,31 @@ export function useConnect() {
211
227
  export function useConnectUser() {
212
228
  return useConnect().session?.user ?? null;
213
229
  }
230
+ /**
231
+ * Whether the signed-in workspace is a one-seat personal account or a team -
232
+ * `null` while the session is loading or signed out.
233
+ *
234
+ * Branch on this to stop rendering team UI at a solo account: a members
235
+ * table, an invite button and a seat meter are all dead controls on an
236
+ * `"individual"` workspace, which has exactly one seat and cannot gain
237
+ * another. `<ConnectWorkspaceTypeGate>` is the declarative form.
238
+ */
239
+ export function useConnectWorkspaceType() {
240
+ return useConnect().session?.user?.workspaceType ?? null;
241
+ }
242
+ /**
243
+ * Whether this account is the kind your application is sold to - `null` while
244
+ * the session is loading or signed out.
245
+ *
246
+ * `audience.matches === false` is a hard block, not a hint: readiness reports
247
+ * `audience_mismatch` and subscribing is refused. `<ConnectAccessGate>`
248
+ * already blocks the shell on it, so reach for this only when you need the
249
+ * verdict somewhere the gate does not run - a marketing page, say, or a
250
+ * sign-up CTA you would rather not show to an account that cannot complete it.
251
+ */
252
+ export function useConnectAudience() {
253
+ return useConnect().session?.audience ?? null;
254
+ }
214
255
  /** Readiness gates and the live subscription, fetched on first use. */
215
256
  export function useConnectEntitlements() {
216
257
  return useConnectResource("/entitlements");
@@ -218,28 +259,28 @@ export function useConnectEntitlements() {
218
259
  /**
219
260
  * What is left of every consumable add-on on this workspace's subscription.
220
261
  *
221
- * The read half of `client.billing.consumeAddon` - a service that meters its
262
+ * The read half of `client.billing.consumeAddon` - an application that meters its
222
263
  * own end users had no way to show them what remained until this existed.
223
264
  * Empty `addons` means there is nothing to meter, which is not an error.
224
265
  */
225
266
  export function useConnectAddonUsage() {
226
267
  return useConnectResource("/addons");
227
268
  }
228
- /** This service's plan catalog. */
269
+ /** This application's plan catalog. */
229
270
  export function useConnectPlans() {
230
271
  return useConnectResource("/plans");
231
272
  }
232
- /** The apps this organization can open. */
273
+ /** The apps this workspace can open. */
233
274
  export function useConnectApps() {
234
275
  return useConnectResource("/apps");
235
276
  }
236
- /** Enabled Composio-hosted toolkits and this organization's connection status for each. */
277
+ /** Enabled Composio-hosted toolkits and this workspace's connection status for each. */
237
278
  export function useConnectIntegrations() {
238
279
  return useConnectResource("/integrations");
239
280
  }
240
281
  /**
241
282
  * This Connect account's own login sessions - account-wide, not scoped to
242
- * this service, see `SessionsListResponse`'s doc comment. The component
283
+ * this application, see `SessionsListResponse`'s doc comment. The component
243
284
  * that calls this is also the one that must own any revoke actions and call
244
285
  * the returned `refetch()` afterward - see `mutateResource`'s doc comment
245
286
  * for why nothing does that automatically.
@@ -152,15 +152,15 @@ export function buildCancelUrl({ appUrl, subscriptionId }) {
152
152
  * The Connect URL that completes a subscribe or a plan change.
153
153
  *
154
154
  * Both land on a modal Connect renders over the relevant page (an intercepted
155
- * parallel route - `app/@modal/(.)services/...` and
155
+ * parallel route - `app/@modal/(.)applications/...` and
156
156
  * `app/@modal/(.)billing/...`), so the customer arrives already looking at the
157
157
  * confirmation for the plan they picked here, not at a catalogue they have to
158
158
  * find it in again.
159
159
  */
160
- export function buildCheckoutUrl({ appUrl, serviceId, planId, subscriptionId, coupon, }) {
160
+ export function buildCheckoutUrl({ appUrl, applicationId, planId, subscriptionId, coupon, }) {
161
161
  const path = subscriptionId
162
162
  ? `/billing/subscriptions/${encodeURIComponent(subscriptionId)}/change-plan/${encodeURIComponent(planId)}`
163
- : `/services/${encodeURIComponent(serviceId)}/subscribe/${encodeURIComponent(planId)}`;
163
+ : `/applications/${encodeURIComponent(applicationId)}/subscribe/${encodeURIComponent(planId)}`;
164
164
  const url = new URL(path, appUrl);
165
165
  if (coupon?.trim())
166
166
  url.searchParams.set("coupon", coupon.trim());
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Every failure the Connect service API can report, as one class.
2
+ * Every failure the Connect application API can report, as one class.
3
3
  *
4
4
  * The API speaks two error dialects and this collapses them: the
5
- * `/api/v1/service/*` endpoints use `{ error: { code, message } }`, while the
5
+ * `/api/v1/application/*` endpoints use `{ error: { code, message } }`, while the
6
6
  * three `/sso/*` ones use OAuth2's `{ error, error_description }` (RFC 6749
7
7
  * §5.2). Both land here with `code` populated, so integrating code branches on
8
8
  * one field either way.
@@ -41,7 +41,7 @@ export class ConnectError extends Error {
41
41
  * Whether the user's Connect sign-in is over and they need to go back
42
42
  * through `identity.authorizeUrl()`. Collapses every session-side failure,
43
43
  * matching how the API reports them - unknown, expired, revoked, or issued
44
- * to a different service all arrive as one `invalid_token`.
44
+ * to a different application all arrive as one `invalid_token`.
45
45
  */
46
46
  get isSessionExpired() {
47
47
  return this.code === "invalid_token";
@@ -55,7 +55,7 @@ export class ConnectError extends Error {
55
55
  * guessing keys. These two are the exception: reaching them required
56
56
  * presenting a key whose hash matched, so you already hold it, and what you
57
57
  * need to know is that no retry and no scope change will fix this - the key
58
- * has to be reissued from the service's Integration tab.
58
+ * has to be reissued from the application's Integration tab.
59
59
  *
60
60
  * Worth alerting on. It is the one failure that will not resolve itself and
61
61
  * takes down every call your integration makes.
@@ -12,7 +12,7 @@ export function blockingBlockers(readiness) {
12
12
  * Coerces either wire shape into `PlanFeature[]`.
13
13
  *
14
14
  * A bare key becomes `{ key, name: key, description: "" }` - the same
15
- * fallback the server applies for a plan older than its service's feature
15
+ * fallback the server applies for a plan older than its application's feature
16
16
  * catalog, so an un-upgraded Connect renders exactly what it always did
17
17
  * rather than a column of blanks.
18
18
  *
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `@cosmoops/connect-sdk` - the official client for the CosmoOps Connect
3
- * **service-partner API** (`/api/v1/service/*`).
3
+ * **application-partner API** (`/api/v1/application/*`).
4
4
  *
5
5
  * This root entry is the isomorphic surface: wire types, the error class, and
6
6
  * pure helpers. It pulls in neither Node-only code nor React, so it is safe to
@@ -38,7 +38,7 @@ import { createConnectHandler } from "../server/handler.js";
38
38
  * | `CONNECT_SESSION_COOKIE` | no | session cookie name |
39
39
  *
40
40
  * Register `{your origin}/api/connect/callback` as a redirect URI in the
41
- * service's Integration tab. That is the only step this cannot do for you.
41
+ * application's Integration tab. That is the only step this cannot do for you.
42
42
  *
43
43
  * No `next.config.ts` changes needed either, including `transpilePackages`:
44
44
  * `dist/` is already ES2022 with `"use client"` hoisted to the top of every
@@ -287,7 +287,7 @@ export async function getConnectSession(request, options = {}) {
287
287
  * A thin wrapper, not a framework of its own - reach for `getConnectSession`
288
288
  * plus your own check when "not signed in" isn't "redirect to sign-in"
289
289
  * (an API route returning 401, say). Role and entitlement checks stay
290
- * outside this function on purpose: `session.serviceRole.granted` and
290
+ * outside this function on purpose: `session.applicationRole.granted` and
291
291
  * `connect.entitlements.*` are one line each on top of the session this
292
292
  * already gives you, and where that line redirects (a plans page, a
293
293
  * "forbidden" screen) is a decision only your app can make - see
@@ -11,7 +11,7 @@ import { AnalyticsResource } from "./resources/analytics.js";
11
11
  import { OtpResource } from "./resources/otp.js";
12
12
  import { SessionsResource } from "./resources/sessions.js";
13
13
  export const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
14
- const SERVICE_API_PREFIX = "/api/v1/service";
14
+ const APPLICATION_API_PREFIX = "/api/v1/application";
15
15
  /**
16
16
  * Refuses to build a client in a browser.
17
17
  *
@@ -40,9 +40,9 @@ function assertNotBrowser() {
40
40
  "in browser code instead; it talks to your own origin and holds no credential.");
41
41
  }
42
42
  /**
43
- * The CosmoOps Connect service-partner API.
43
+ * The CosmoOps Connect application-partner API.
44
44
  *
45
- * Covers `/api/v1/service/*` and nothing else - the surface a service platform
45
+ * Covers `/api/v1/application/*` and nothing else - the surface an application platform
46
46
  * integrates against. Connect's internal API is not part of this package and
47
47
  * never will be.
48
48
  *
@@ -53,7 +53,7 @@ function assertNotBrowser() {
53
53
  * });
54
54
  *
55
55
  * const session = await connect.identity.exchangeCode({ code, redirectUri });
56
- * const { ready } = await connect.entitlements.getReadiness(session.user.organizationId);
56
+ * const { ready } = await connect.entitlements.getReadiness(session.user.workspaceId);
57
57
  * ```
58
58
  *
59
59
  * Every failure throws a `ConnectError` carrying a stable `code`. Nothing here
@@ -62,27 +62,27 @@ function assertNotBrowser() {
62
62
  export class ConnectClient {
63
63
  /** "Login with Connect" - the OAuth2 handshake and the session after it. */
64
64
  identity;
65
- /** What a signed-in member may do on your service. */
65
+ /** What a signed-in member may do on your application. */
66
66
  access;
67
- /** The services an organization can open - for a launcher grid. */
67
+ /** The applications a workspace can open - for a launcher grid. */
68
68
  apps;
69
- /** Whether a workspace may use your service, and on what plan. */
69
+ /** Whether a workspace may use your application, and on what plan. */
70
70
  entitlements;
71
71
  /** Your plan catalog, and metering usage against add-on balances. */
72
72
  billing;
73
- /** Files in an organization's Drive. */
73
+ /** Files in a workspace's Drive. */
74
74
  drive;
75
75
  /** RAG chat and structured extraction. */
76
76
  ai;
77
77
  /** WhatsApp/SMS phone verification. */
78
78
  otp;
79
- /** Feature events into the Console's audit log, filterable to your service. */
79
+ /** Feature events into the Console's audit log, filterable to your application. */
80
80
  logs;
81
- /** Emit product events and read back this service's aggregated numbers. */
81
+ /** Emit product events and read back this application's aggregated numbers. */
82
82
  analytics;
83
- /** This Connect account's own login sessions - account-wide, not scoped to this service. Requires the `sessions:manage` scope. */
83
+ /** This Connect account's own login sessions - account-wide, not scoped to this application. Requires the `sessions:manage` scope. */
84
84
  sessions;
85
- /** Composio-hosted third-party connections (Google Calendar, Slack, ...) an organization has connected. */
85
+ /** Composio-hosted third-party connections (Google Calendar, Slack, ...) a workspace has connected. */
86
86
  integrations;
87
87
  constructor(options) {
88
88
  assertNotBrowser();
@@ -91,7 +91,7 @@ export class ConnectClient {
91
91
  }
92
92
  const appBaseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
93
93
  const transport = {
94
- baseUrl: `${appBaseUrl}${SERVICE_API_PREFIX}`,
94
+ baseUrl: `${appBaseUrl}${APPLICATION_API_PREFIX}`,
95
95
  apiKey: options.apiKey,
96
96
  timeoutMs: options.timeoutMs ?? 20_000,
97
97
  maxRetries: options.maxRetries ?? 2,
@@ -99,7 +99,7 @@ export class ConnectClient {
99
99
  userAgent: `cosmoops-connect-sdk/${SDK_VERSION}`,
100
100
  };
101
101
  // `authorizeUrl` points at the app, not the API prefix - it is a browser
102
- // redirect, not a service call, so identity takes both.
102
+ // redirect, not an application call, so identity takes both.
103
103
  this.identity = new IdentityResource(transport, options.clientId ?? null, appBaseUrl);
104
104
  this.access = new AccessResource(transport);
105
105
  this.apps = new AppsResource(transport);
@@ -115,4 +115,4 @@ export class ConnectClient {
115
115
  }
116
116
  }
117
117
  /** Kept in step with `package.json` by `sdk.test.ts`. */
118
- export const SDK_VERSION = "0.6.0";
118
+ export const SDK_VERSION = "0.9.0";
@@ -91,9 +91,9 @@ function redirect(location, cookies = []) {
91
91
  * export { handler as GET, handler as POST };
92
92
  * ```
93
93
  *
94
- * Every route resolves the organization from the caller's *own* session, never
94
+ * Every route resolves the workspace from the caller's *own* session, never
95
95
  * from a query parameter. A browser must not be able to ask about an
96
- * organization it is not signed into, and a client-supplied `organizationId`
96
+ * workspace it is not signed into, and a client-supplied `workspaceId`
97
97
  * is exactly that hole.
98
98
  */
99
99
  export function createConnectHandler(options) {
@@ -160,7 +160,7 @@ export function createConnectHandler(options) {
160
160
  }
161
161
  return token;
162
162
  }
163
- /** Signed-in identity plus the organization it belongs to - the first read every other route needs. */
163
+ /** Signed-in identity plus the workspace it belongs to - the first read every other route needs. */
164
164
  async function whoami(request) {
165
165
  return client.identity.getUser(await requireToken(request));
166
166
  }
@@ -170,32 +170,75 @@ export function createConnectHandler(options) {
170
170
  // Signed out is a normal state for a page rendering a Login button, not
171
171
  // an error - `user: null` rather than a 401 the client must catch.
172
172
  if (!token)
173
- return json({ user: null, subscription: null, serviceRole: null });
173
+ return json({ user: null, subscription: null, applicationRole: null });
174
174
  return json(await client.identity.getUser(token));
175
175
  },
176
176
  "GET /entitlements": async (request) => {
177
177
  const { user } = await whoami(request);
178
178
  const [readiness, subscription] = await Promise.all([
179
- client.entitlements.getReadiness(user.organizationId),
180
- client.entitlements.getSubscription(user.organizationId),
179
+ client.entitlements.getReadiness(user.workspaceId),
180
+ client.entitlements.getSubscription(user.workspaceId),
181
181
  ]);
182
182
  return json({ readiness, subscription });
183
183
  },
184
184
  "GET /plans": async () => json(await client.billing.listPlans()),
185
- // Organization comes from the caller's own session, same as every other
185
+ // Workspace comes from the caller's own session, same as every other
186
186
  // route here - a browser must not be able to ask what another workspace
187
187
  // has left.
188
188
  "GET /addons": async (request) => {
189
189
  const { user } = await whoami(request);
190
- return json(await client.billing.getAddonUsage(user.organizationId));
190
+ return json(await client.billing.getAddonUsage(user.workspaceId));
191
191
  },
192
192
  "GET /apps": async (request) => {
193
193
  const { user } = await whoami(request);
194
- return json({ apps: await client.apps.list(user.organizationId) });
194
+ return json({ apps: await client.apps.list(user.workspaceId) });
195
+ },
196
+ /**
197
+ * Ask AI. The one streaming route here, and the only one that does not
198
+ * answer JSON - it forwards Connect's server-sent event stream through
199
+ * untouched so `<ConnectAskAi>` can render tokens as they arrive.
200
+ *
201
+ * Same rule as every other route: the workspace comes from the caller's
202
+ * own session. It matters more here than anywhere else, because the
203
+ * answers are *retrieved from that workspace's Context Index* - a
204
+ * client-supplied `workspaceId` would be a read of another tenant's
205
+ * documents, laundered through a chat reply.
206
+ *
207
+ * Messages are forwarded as-is (part-based `UIMessage`s), and only their
208
+ * shape is checked: the conversation is the caller's own, and Connect
209
+ * validates it again on arrival.
210
+ */
211
+ "POST /ai/chat": async (request) => {
212
+ const { user } = await whoami(request);
213
+ const body = (await request.json().catch(() => null));
214
+ if (!Array.isArray(body?.messages)) {
215
+ throw new ConnectError({
216
+ status: 400,
217
+ code: "validation_error",
218
+ message: "messages is required",
219
+ endpoint: "POST /ai/chat",
220
+ });
221
+ }
222
+ const upstream = await client.ai.chat({
223
+ workspaceId: user.workspaceId,
224
+ messages: body.messages,
225
+ });
226
+ // `upstream.body` is piped, never buffered - awaiting it would collect
227
+ // the whole answer server-side and deliver it in one lump, which is the
228
+ // only thing streaming was for. The browser cancelling its read
229
+ // propagates down this pipe and stops generation upstream.
230
+ return new Response(upstream.body, {
231
+ status: upstream.status,
232
+ headers: {
233
+ "content-type": upstream.headers.get("content-type") ?? "text/event-stream",
234
+ "cache-control": "no-store",
235
+ "x-content-type-options": "nosniff",
236
+ },
237
+ });
195
238
  },
196
239
  "GET /integrations": async (request) => {
197
240
  const { user } = await whoami(request);
198
- return json({ toolkits: await client.integrations.list(user.organizationId) });
241
+ return json({ toolkits: await client.integrations.list(user.workspaceId) });
199
242
  },
200
243
  "POST /integrations/connect": async (request) => {
201
244
  const { user } = await whoami(request);
@@ -208,7 +251,7 @@ export function createConnectHandler(options) {
208
251
  endpoint: "POST /integrations/connect",
209
252
  });
210
253
  }
211
- return json(await client.integrations.initiateConnect(user.organizationId, body.toolkitSlug, body.returnUrl));
254
+ return json(await client.integrations.initiateConnect(user.workspaceId, body.toolkitSlug, body.returnUrl));
212
255
  },
213
256
  "POST /integrations/disconnect": async (request) => {
214
257
  const { user } = await whoami(request);
@@ -221,10 +264,10 @@ export function createConnectHandler(options) {
221
264
  endpoint: "POST /integrations/disconnect",
222
265
  });
223
266
  }
224
- await client.integrations.disconnect(user.organizationId, body.toolkitSlug);
267
+ await client.integrations.disconnect(user.workspaceId, body.toolkitSlug);
225
268
  return json({ ok: true });
226
269
  },
227
- // Account-wide, not scoped to this service - see `SessionsResource`'s
270
+ // Account-wide, not scoped to this application - see `SessionsResource`'s
228
271
  // doc comment. `requireToken` alone identifies the caller; the target
229
272
  // `sessionId` on the revoke routes travels in the JSON body, not the
230
273
  // URL, so the exact-match routes table above never needs a dynamic