@cosmoops/connect-sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/README.md +273 -91
  3. package/dist/cjs/client/components/access-gate.js +39 -4
  4. package/dist/cjs/client/components/ai-format.js +135 -0
  5. package/dist/cjs/client/components/ai-widgets.js +232 -0
  6. package/dist/cjs/client/components/app-shell.js +7 -6
  7. package/dist/cjs/client/components/ask-ai.js +334 -0
  8. package/dist/cjs/client/components/nav.js +1 -1
  9. package/dist/cjs/client/components/side-nav.js +1 -1
  10. package/dist/cjs/client/components/user-menu.js +28 -5
  11. package/dist/cjs/client/components/workspace-type-gate.js +48 -0
  12. package/dist/cjs/client/index.js +31 -3
  13. package/dist/cjs/client/provider.js +45 -2
  14. package/dist/cjs/server/client.js +1 -1
  15. package/dist/cjs/server/handler.js +43 -0
  16. package/dist/cjs/server/http.js +46 -24
  17. package/dist/cjs/server/resources/entitlements.js +41 -0
  18. package/dist/cjs/server/resources/identity.js +2 -0
  19. package/dist/esm/client/components/access-gate.js +40 -5
  20. package/dist/esm/client/components/ai-format.js +129 -0
  21. package/dist/esm/client/components/ai-widgets.js +223 -0
  22. package/dist/esm/client/components/app-shell.js +6 -5
  23. package/dist/esm/client/components/ask-ai.js +331 -0
  24. package/dist/esm/client/components/nav.js +1 -1
  25. package/dist/esm/client/components/side-nav.js +1 -1
  26. package/dist/esm/client/components/user-menu.js +28 -5
  27. package/dist/esm/client/components/workspace-type-gate.js +45 -0
  28. package/dist/esm/client/index.js +13 -2
  29. package/dist/esm/client/provider.js +43 -2
  30. package/dist/esm/server/client.js +1 -1
  31. package/dist/esm/server/handler.js +43 -0
  32. package/dist/esm/server/http.js +46 -24
  33. package/dist/esm/server/resources/entitlements.js +41 -0
  34. package/dist/esm/server/resources/identity.js +2 -0
  35. package/dist/theme.css +87 -0
  36. package/dist/types/client/components/access-gate.d.ts +17 -3
  37. package/dist/types/client/components/ai-format.d.ts +24 -0
  38. package/dist/types/client/components/ai-widgets.d.ts +42 -0
  39. package/dist/types/client/components/app-shell.d.ts +21 -5
  40. package/dist/types/client/components/ask-ai.d.ts +73 -0
  41. package/dist/types/client/components/nav.d.ts +3 -3
  42. package/dist/types/client/components/side-nav.d.ts +1 -1
  43. package/dist/types/client/components/user-menu.d.ts +2 -1
  44. package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
  45. package/dist/types/client/index.d.ts +10 -3
  46. package/dist/types/client/provider.d.ts +44 -1
  47. package/dist/types/server/client.d.ts +1 -1
  48. package/dist/types/server/http.d.ts +6 -1
  49. package/dist/types/server/resources/entitlements.d.ts +33 -1
  50. package/dist/types/types.d.ts +174 -0
  51. package/package.json +8 -2
@@ -1,7 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
+ import { type ConnectAskAiProps } from "./ask-ai.js";
2
3
  import { type ConnectLinkRenderer, type ConnectNav } from "./nav.js";
3
4
  import type { ThemeMode } from "../../types.js";
4
- export interface ConnectAppShellProps {
5
+ export interface AppShellProps {
5
6
  /** This application's name, in the `cosmo·ops VOICE` lockup. See `ConnectLogo`. */
6
7
  name: string;
7
8
  /** This application's glyph, drawn on the CosmoOps badge. Falls back to the Connect rings. */
@@ -61,6 +62,21 @@ export interface ConnectAppShellProps {
61
62
  * say. Prefix-matched, same as the nav's active resolution.
62
63
  */
63
64
  gateExemptRoutes?: string[];
65
+ /**
66
+ * Mount `ConnectAskAi` - a chat panel grounded in the signed-in workspace's
67
+ * Context Index, streamed through your own bridge. Pass an object to
68
+ * override its heading, copy or markdown plugins.
69
+ *
70
+ * **On by default**, like `gate`, and for the same reason: a family of
71
+ * application platforms should answer questions the same way, and a feature
72
+ * every partner has to remember to switch on is a feature most of them do
73
+ * not have. It stays inert until it can actually work - the trigger renders
74
+ * nothing for a signed-out visitor, and a workspace whose Context Index is
75
+ * off gets a message in the panel rather than a broken answer.
76
+ *
77
+ * Set `false` for an application that should not offer it at all.
78
+ */
79
+ askAi?: boolean | ConnectAskAiProps;
64
80
  className?: string;
65
81
  /** Classes on the scrolling content pane - override the default max-width and padding here. */
66
82
  contentClassName?: string;
@@ -82,12 +98,12 @@ export interface ConnectAppShellProps {
82
98
  * "use client";
83
99
  * import Link from "next/link";
84
100
  * import { usePathname } from "next/navigation";
85
- * import { ConnectAppShell } from "@cosmoops/connect-sdk/client";
101
+ * import { AppShell } from "@cosmoops/connect-sdk/client";
86
102
  * import { LayoutDashboard, Truck } from "lucide-react";
87
103
  *
88
104
  * export function Shell({ children }: { children: React.ReactNode }) {
89
105
  * return (
90
- * <ConnectAppShell
106
+ * <AppShell
91
107
  * name="Dispatch"
92
108
  * icon={<Truck />}
93
109
  * activePath={usePathname()}
@@ -98,7 +114,7 @@ export interface ConnectAppShellProps {
98
114
  * ]}
99
115
  * >
100
116
  * {children}
101
- * </ConnectAppShell>
117
+ * </AppShell>
102
118
  * );
103
119
  * }
104
120
  * ```
@@ -112,4 +128,4 @@ export interface ConnectAppShellProps {
112
128
  * so a page with a shell, a plan table and a usage meter still makes one
113
129
  * session read.
114
130
  */
115
- export declare function ConnectAppShell({ name, icon, homeHref, nav, activePath, renderLink, title, headerActions, sidebarFooter, footer, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, collapsible, showAppsGrid, showAuthButtons, theme, onThemeChange, gate, gateExemptRoutes, className, contentClassName, children, }: ConnectAppShellProps): import("react").JSX.Element;
131
+ export declare function AppShell({ name, icon, homeHref, nav, activePath, renderLink, title, headerActions, sidebarFooter, footer, defaultCollapsed, collapsed: controlledCollapsed, onCollapsedChange, collapsible, showAppsGrid, showAuthButtons, theme, onThemeChange, gate, gateExemptRoutes, askAi, className, contentClassName, children, }: AppShellProps): import("react").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { type StreamdownProps } from "streamdown";
2
+ export interface ConnectAskAiProps {
3
+ /** Panel heading. Defaults to `"Ask AI"`. */
4
+ title?: string;
5
+ /** Byline over each assistant turn. Defaults to `"Assistant"`. */
6
+ assistantName?: string;
7
+ /** Composer placeholder. */
8
+ placeholder?: string;
9
+ /** Shown once, above an empty conversation. */
10
+ hint?: string;
11
+ /** Start open. The panel owns its state after that. */
12
+ defaultOpen?: boolean;
13
+ /**
14
+ * Streamdown plugins - syntax highlighting, KaTeX, Mermaid, CJK.
15
+ *
16
+ * Off unless you pass them, because each is a package of its own and several
17
+ * are large (Mermaid alone dwarfs this SDK). Prose, GFM tables, task lists
18
+ * and plain code fences all render without any of them.
19
+ *
20
+ * ```tsx
21
+ * import { code } from "@streamdown/code";
22
+ * import { mermaid } from "@streamdown/mermaid";
23
+ *
24
+ * <ConnectAskAi markdownPlugins={{ code, mermaid }} />
25
+ * ```
26
+ *
27
+ * Hoist the object to module scope: Streamdown compares this prop by
28
+ * reference, so an inline literal defeats its block memoisation.
29
+ */
30
+ markdownPlugins?: StreamdownProps["plugins"];
31
+ }
32
+ /**
33
+ * Ask AI - a chat panel grounded in the signed-in workspace's Context Index,
34
+ * streamed through your own bridge.
35
+ *
36
+ * Answers are not only prose. Connect's model can call a small set of
37
+ * presentation tools - `showTable`, `showChart`, `showMetrics`, `showTimeline`,
38
+ * `showDetails`, `showSteps`, `showSources` - and this panel renders each one
39
+ * natively: a sortable table with a CSV download, a chart, a row of metric
40
+ * tiles. Values arrive raw with a format tag, so they are formatted here rather
41
+ * than by the model, which is what lets a column sort numerically.
42
+ *
43
+ * Nothing here holds a credential: it POSTs to `${basePath}/ai/chat` on your
44
+ * own origin, and `createConnectHandler` is what reaches Connect. The workspace
45
+ * is resolved from the caller's session there, never sent from the browser - so
46
+ * a user can only ever ask about the workspace they are signed into.
47
+ *
48
+ * ```tsx
49
+ * <AppShell name="Dispatch" nav={nav}>
50
+ * {children}
51
+ * </AppShell>
52
+ * ```
53
+ *
54
+ * `AppShell` mounts it by default. Mount it directly only when you are not
55
+ * using the shell: it renders a fixed corner trigger plus a panel that is an
56
+ * *in-flow flex sibling* at `lg` and up, so opening it pushes your content
57
+ * aside rather than covering it. That requires it to be the last child of a
58
+ * `flex` row owning the viewport height - which is exactly what `AppShell` is.
59
+ * Below `lg` it is a full-screen sheet with a backdrop instead; there is no
60
+ * room to push anything on a phone.
61
+ *
62
+ * **Host setup:** markdown rendering needs `streamdown` installed, one Tailwind
63
+ * `@source "…/node_modules/streamdown/dist/*.js"` line so its utility classes
64
+ * are generated, and `import "streamdown/styles.css"` for the streaming
65
+ * animation. The colour tokens Streamdown expects are already mapped in this
66
+ * SDK's `theme.css`.
67
+ *
68
+ * Answers require the workspace's Context Index to be turned on (a workspace
69
+ * admin toggle in Connect) and your API key to carry the `ai:chat` scope.
70
+ * Neither is something the browser can discover, so both surface as a message
71
+ * in the panel rather than as a hidden button.
72
+ */
73
+ export declare function ConnectAskAi({ title, assistantName, placeholder, hint, defaultOpen, markdownPlugins, }: ConnectAskAiProps): import("react").JSX.Element | null;
@@ -28,7 +28,7 @@ export interface ConnectNavItem {
28
28
  items?: ConnectNavItem[];
29
29
  /**
30
30
  * Leave this route *outside* `ConnectAccessGate` - see
31
- * `ConnectAppShellProps.gate`. Declared here, next to the route it applies
31
+ * `AppShellProps.gate`. Declared here, next to the route it applies
32
32
  * to, rather than as a second list somewhere else that has to be kept in
33
33
  * step with this one.
34
34
  */
@@ -40,7 +40,7 @@ export interface ConnectNavSection {
40
40
  items: ConnectNavItem[];
41
41
  }
42
42
  /**
43
- * What `ConnectAppShell`/`ConnectSideNav` accept for `nav`: a flat list of
43
+ * What `AppShell`/`ConnectSideNav` accept for `nav`: a flat list of
44
44
  * items, a list of sections, or a mix. Consecutive bare items fold into one
45
45
  * untitled section, so the common case is just an array of items.
46
46
  */
@@ -106,5 +106,5 @@ export declare function resolveActivePath(pathname: string, nav: ConnectNav): st
106
106
  * lights up on every app's home route. External items are never active.
107
107
  */
108
108
  export declare function isNavItemActive(item: ConnectNavItem, activeHref: string | null): boolean;
109
- /** Hrefs declared `gateExempt` - the routes `ConnectAppShell` renders outside its `ConnectAccessGate`. */
109
+ /** Hrefs declared `gateExempt` - the routes `AppShell` renders outside its `ConnectAccessGate`. */
110
110
  export declare function gateExemptPaths(nav: ConnectNav): string[];
@@ -23,7 +23,7 @@ export interface ConnectSideNavProps {
23
23
  * The sidebar's nav column on its own - sections, items, one level of
24
24
  * children, and the active highlight.
25
25
  *
26
- * `ConnectAppShell` renders this inside its rail and its mobile drawer, and it
26
+ * `AppShell` renders this inside its rail and its mobile drawer, and it
27
27
  * is exported for a host that wants the shell's navigation without its
28
28
  * chrome: the same items, the same active resolution, dropped into a layout
29
29
  * you already have.
@@ -70,7 +70,8 @@ export declare function ConnectInvoicesButton({ className }: Readonly<{
70
70
  * `ConnectHomeButton`.
71
71
  *
72
72
  * It was reachable only from inside `ConnectUserMenu`, which meant an app that
73
- * builds its own account menu had to hardcode `${appUrl}/profile` to offer it.
73
+ * builds its own account menu had to hardcode `${appUrl}/profile` to offer it -
74
+ * and got no `return_to`, so the user had no way back.
74
75
  */
75
76
  export declare function ConnectProfileButton({ className }: Readonly<{
76
77
  className?: string;
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from "react";
2
+ import type { WorkspaceType } from "../../types.js";
3
+ export interface ConnectWorkspaceTypeGateProps {
4
+ /**
5
+ * The workspace type this subtree is for. Pass an array to accept either -
6
+ * which is the same as not using the gate at all, so it exists only to keep
7
+ * a `type={someVariable}` call site from having to special-case "both".
8
+ */
9
+ type: WorkspaceType | WorkspaceType[];
10
+ children: ReactNode;
11
+ /**
12
+ * Rendered for a workspace of the other type. Defaults to `null` - hiding
13
+ * the subtree, which is what this gate is normally for.
14
+ */
15
+ fallback?: ReactNode;
16
+ /** Shown until the session read settles. Defaults to nothing. */
17
+ loading?: ReactNode;
18
+ }
19
+ /**
20
+ * Renders `children` only for a workspace of the given type.
21
+ *
22
+ * ```tsx
23
+ * <ConnectWorkspaceTypeGate type="business">
24
+ * <TeamMembersTable />
25
+ * </ConnectWorkspaceTypeGate>
26
+ * ```
27
+ *
28
+ * The point is dead controls, not access: an `"individual"` workspace has
29
+ * exactly one seat and can never have another, so a members table, an invite
30
+ * button and a seat meter are all things that can only ever fail there.
31
+ * Hiding them is a better answer than letting someone press one and read an
32
+ * error about an account shape they did not choose.
33
+ *
34
+ * This is emphatically **not** the audience gate. It does not know what your
35
+ * application is sold to and will happily render a business-only subtree to a
36
+ * business workspace that has no business being in your application at all -
37
+ * that is `<ConnectAccessGate>`'s job, off the `audience_mismatch` readiness
38
+ * blocker. Use this one *inside* an application both audiences can reach, to
39
+ * vary what each sees.
40
+ *
41
+ * Like every gate in this package it is a **UX gate, not a security
42
+ * boundary**: the browser can ignore it. Anything that must not happen for an
43
+ * individual workspace has to be refused on your server too.
44
+ *
45
+ * Renders `loading` - not `fallback` - while the session is unknown, so a
46
+ * business account never flickers through the individual-account layout.
47
+ */
48
+ export declare function ConnectWorkspaceTypeGate({ type, children, fallback, loading, }: ConnectWorkspaceTypeGateProps): import("react").JSX.Element;
@@ -10,7 +10,7 @@
10
10
  * whatever theme the host application platform already configured, and one theme
11
11
  * change lands identically in all of them.
12
12
  */
13
- export { ConnectProvider, useConnect, useConnectUser, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
13
+ export { ConnectProvider, useConnect, useConnectUser, useConnectWorkspaceType, useConnectAudience, useConnectEntitlements, useConnectPlans, useConnectApps, useConnectAddonUsage, useConnectSessions, useConnectIntegrations, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
14
14
  export type { ConnectProviderProps, ConnectContextValue, ConnectSession } from "./provider.js";
15
15
  export { blockingBlockers, normalizePlanFeatures } from "../helpers.js";
16
16
  export type { WirePlanFeature } from "../helpers.js";
@@ -25,8 +25,8 @@ export { ConnectUserMenu, ConnectManageSubscriptionButton, ConnectInvoicesButton
25
25
  export { ConnectAccountSessions } from "./components/account-sessions.js";
26
26
  export type { ConnectAccountSessionsProps } from "./components/account-sessions.js";
27
27
  export type { ConnectUserMenuProps } from "./components/user-menu.js";
28
- export { ConnectAppShell } from "./components/app-shell.js";
29
- export type { ConnectAppShellProps } from "./components/app-shell.js";
28
+ export { AppShell } from "./components/app-shell.js";
29
+ export type { AppShellProps } from "./components/app-shell.js";
30
30
  export { ConnectSideNav } from "./components/side-nav.js";
31
31
  export type { ConnectSideNavProps } from "./components/side-nav.js";
32
32
  export { useActivePath } from "./use-active-path.js";
@@ -34,6 +34,11 @@ export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemAc
34
34
  export type { ConnectNav, ConnectNavItem, ConnectNavSection, ConnectNavLinkProps, ConnectLinkRenderer, } from "./components/nav.js";
35
35
  export { ConnectAppsGrid } from "./components/apps-grid.js";
36
36
  export type { ConnectAppsGridProps } from "./components/apps-grid.js";
37
+ export { ConnectAskAi } from "./components/ask-ai.js";
38
+ export type { ConnectAskAiProps } from "./components/ask-ai.js";
39
+ export { AiTable, AiChart, AiMetrics, AiTimeline, AiDetails, AiSteps, AiSources, } from "./components/ai-widgets.js";
40
+ export { formatAiValue, isNumericAiFormat, aiStatusTone, AI_EMPTY_VALUE, AI_TONE_CHIP, AI_TONE_DOT, } from "./components/ai-format.js";
41
+ export type { AiStatusTone } from "./components/ai-format.js";
37
42
  export { ConnectIntegrationsModal } from "./components/integrations-modal.js";
38
43
  export type { ConnectIntegrationsModalProps } from "./components/integrations-modal.js";
39
44
  export { ConnectPlans } from "./components/plans.js";
@@ -48,6 +53,8 @@ export { ConnectAccessGate, useConnectAccessGate } from "./components/access-gat
48
53
  export type { ConnectAccessGateProps, ConnectAccessGateState, ConnectAccessGateResult, } from "./components/access-gate.js";
49
54
  export { ConnectFeatureGate, useConnectFeature } from "./components/feature-gate.js";
50
55
  export type { ConnectFeatureGateProps } from "./components/feature-gate.js";
56
+ export { ConnectWorkspaceTypeGate } from "./components/workspace-type-gate.js";
57
+ export type { ConnectWorkspaceTypeGateProps } from "./components/workspace-type-gate.js";
51
58
  export { ConnectUsageMeter } from "./components/usage-meter.js";
52
59
  export type { ConnectUsageMeterProps } from "./components/usage-meter.js";
53
60
  export { ConnectPlansPage } from "./components/plans-page.js";
@@ -1,10 +1,16 @@
1
1
  import type { ReactNode } from "react";
2
- import type { AddonUsageResponse, ConnectUser, IntegrationsListResponse, WorkspaceApp, PlansResponse, ReadinessResponse, ApplicationRole, SessionsListResponse, SessionSubscription, SubscriptionResponse } from "../types.js";
2
+ import type { AddonUsageResponse, ConnectUser, IntegrationsListResponse, WorkspaceApp, PlansResponse, ReadinessResponse, ApplicationRole, SessionsListResponse, SessionSubscription, SessionAudience, SubscriptionResponse, WorkspaceType } from "../types.js";
3
3
  export declare const CONNECT_APP_URL = "https://connect.cosmoops.com";
4
4
  export interface ConnectSession {
5
5
  user: ConnectUser | null;
6
6
  subscription: SessionSubscription | null;
7
7
  applicationRole: ApplicationRole | null;
8
+ /**
9
+ * `null` when signed out, for the same reason the three above are: there is
10
+ * no workspace to compare an audience against yet. Never `null` on a
11
+ * signed-in session.
12
+ */
13
+ audience: SessionAudience | null;
8
14
  }
9
15
  export interface ConnectContextValue {
10
16
  /**
@@ -58,6 +64,22 @@ export interface ConnectContextValue {
58
64
  * page.
59
65
  */
60
66
  invalidateResource(path: string): void;
67
+ /**
68
+ * POST through the bridge and hand back the raw `Response`, unread and
69
+ * uncached - the seam `<ConnectAskAi>` streams server-sent events over.
70
+ *
71
+ * Separate from `mutateResource` rather than a flag on it, because the two
72
+ * differ in every respect that matters: this one must not `await
73
+ * response.json()` (that buffers the whole stream and defeats the point),
74
+ * must not touch the resource cache (a conversation is not a resource), and
75
+ * takes an `AbortSignal` so a "Stop" button can actually stop generation.
76
+ *
77
+ * Throws on a non-2xx, having read the error envelope - which is safe,
78
+ * because a failure never has a stream behind it.
79
+ */
80
+ streamResource(path: string, body: unknown, options?: {
81
+ signal?: AbortSignal;
82
+ }): Promise<Response>;
61
83
  }
62
84
  export interface ConnectProviderProps {
63
85
  /**
@@ -128,6 +150,27 @@ export declare function ConnectProvider({ clientId, basePath, redirectUri, appUr
128
150
  export declare function useConnect(): ConnectContextValue;
129
151
  /** The signed-in user, or `null`. Sugar over `useConnect().session`. */
130
152
  export declare function useConnectUser(): ConnectUser | null;
153
+ /**
154
+ * Whether the signed-in workspace is a one-seat personal account or a team -
155
+ * `null` while the session is loading or signed out.
156
+ *
157
+ * Branch on this to stop rendering team UI at a solo account: a members
158
+ * table, an invite button and a seat meter are all dead controls on an
159
+ * `"individual"` workspace, which has exactly one seat and cannot gain
160
+ * another. `<ConnectWorkspaceTypeGate>` is the declarative form.
161
+ */
162
+ export declare function useConnectWorkspaceType(): WorkspaceType | null;
163
+ /**
164
+ * Whether this account is the kind your application is sold to - `null` while
165
+ * the session is loading or signed out.
166
+ *
167
+ * `audience.matches === false` is a hard block, not a hint: readiness reports
168
+ * `audience_mismatch` and subscribing is refused. `<ConnectAccessGate>`
169
+ * already blocks the shell on it, so reach for this only when you need the
170
+ * verdict somewhere the gate does not run - a marketing page, say, or a
171
+ * sign-up CTA you would rather not show to an account that cannot complete it.
172
+ */
173
+ export declare function useConnectAudience(): SessionAudience | null;
131
174
  /** Readiness gates and the live subscription, fetched on first use. */
132
175
  export declare function useConnectEntitlements(): {
133
176
  data: {
@@ -86,4 +86,4 @@ export declare class ConnectClient {
86
86
  constructor(options: ConnectClientOptions);
87
87
  }
88
88
  /** Kept in step with `package.json` by `sdk.test.ts`. */
89
- export declare const SDK_VERSION = "0.7.0";
89
+ export declare const SDK_VERSION = "0.9.0";
@@ -12,7 +12,12 @@ export interface RequestSpec {
12
12
  headers?: Record<string, string>;
13
13
  /** Bearer API key (default), or none - the `/sso/*` endpoints authenticate inside the form body instead. */
14
14
  auth?: "bearer" | "none";
15
- /** Skip JSON parsing and hand back the raw `Response` - for streaming endpoints. */
15
+ /**
16
+ * Skip JSON parsing and hand back the raw `Response` - for streaming
17
+ * endpoints.
18
+ *
19
+ * Also stops `timeoutMs` from applying to the *body*: see `request`.
20
+ */
16
21
  raw?: boolean;
17
22
  signal?: AbortSignal;
18
23
  }
@@ -1,5 +1,5 @@
1
1
  import { type TransportOptions } from "../http.js";
2
- import type { ReadinessResponse, SubscriptionResponse } from "../../types.js";
2
+ import type { ApplicationSubscriber, ReadinessResponse, SubscribersResponse, SubscriptionResponse } from "../../types.js";
3
3
  /**
4
4
  * The two checks worth making on every request: may this workspace be here at
5
5
  * all, and what is it paying for.
@@ -28,4 +28,36 @@ export declare class EntitlementsResource {
28
28
  * should not need a try/catch around it.
29
29
  */
30
30
  hasFeature(workspaceId: string, feature: string): Promise<boolean>;
31
+ /**
32
+ * One page of the workspaces subscribed to your application, newest first.
33
+ *
34
+ * The inverse of `getSubscription`, which asks about one workspace you
35
+ * already know. This asks who your subscribers *are* - what a partner
36
+ * dashboard, an MRR total or a churn report is built on.
37
+ *
38
+ * Rate-limited far more tightly than the per-workspace read (10/min per
39
+ * key, against 60): it walks your whole subscriber base and is a reporting
40
+ * call, not a per-request gate. Use `getSubscription` in a hot path.
41
+ *
42
+ * Your application is resolved from the API key, so there is no parameter
43
+ * for it and no way to ask about anyone else's.
44
+ */
45
+ listSubscribers(options?: {
46
+ status?: ApplicationSubscriber["status"];
47
+ /** Cursor from a previous page's `nextBefore`. */
48
+ before?: string;
49
+ /** 1..100, default 25. */
50
+ limit?: number;
51
+ }): Promise<SubscribersResponse>;
52
+ /**
53
+ * Every subscriber, paged through for you.
54
+ *
55
+ * An async generator rather than an array: a large subscriber base is
56
+ * exactly where an accidental full materialisation hurts, and `for await`
57
+ * reads the same as a loop over one page.
58
+ */
59
+ iterateSubscribers(options?: {
60
+ status?: ApplicationSubscriber["status"];
61
+ pageSize?: number;
62
+ }): AsyncGenerator<ApplicationSubscriber>;
31
63
  }
@@ -27,6 +27,54 @@ export interface ConnectUser {
27
27
  photoUrl: string | null;
28
28
  workspaceId: string;
29
29
  workspaceName: string;
30
+ /**
31
+ * Whether this is a one-seat personal account or a team.
32
+ *
33
+ * The one fact about the signed-in workspace you cannot look up yourself -
34
+ * the application API has no workspace read - so branch on it rather than
35
+ * inferring an account's shape from whether it happens to have a second
36
+ * member. An `"individual"` workspace has exactly one seat and can never
37
+ * have another, so a members table, an invite button or a seat-count meter
38
+ * is dead UI for it.
39
+ *
40
+ * Re-read it from `identity.getUser()`; do not cache it past the sign-in.
41
+ * An `"individual"` workspace can convert to `"business"` under a live
42
+ * session (the reverse never happens).
43
+ */
44
+ workspaceType: WorkspaceType;
45
+ }
46
+ export type WorkspaceType = "individual" | "business";
47
+ export type ApplicationAudience = "b2b" | "b2c" | "both";
48
+ /**
49
+ * Whether the signed-in workspace is inside your application's audience -
50
+ * returned alongside `user` by both `exchangeCode()` and `getUser()`.
51
+ *
52
+ * `user.workspaceType` answers what the account *is*; this answers what your
53
+ * application is *sold to*, and whether the two agree. Both ship together so
54
+ * the check stays `!audience.matches` rather than a hardcoded audience
55
+ * constant in your own code that goes stale the moment the catalogue entry
56
+ * changes.
57
+ *
58
+ * `matches: false` means readiness will report an `audience_mismatch` blocker
59
+ * and any attempt to subscribe will be refused, so treat it as a hard block:
60
+ * `<ConnectAccessGate>` already does. What it is NOT is a reason to sign the
61
+ * user out - they have a valid Connect account, just the wrong kind of one
62
+ * for this application.
63
+ */
64
+ export interface SessionAudience {
65
+ /** Your catalogue entry's setting - `"both"` if you have never narrowed it. */
66
+ application: ApplicationAudience;
67
+ /** The single `workspaceType` `application` demands - `null` for `"both"`, which accepts either. */
68
+ requiredWorkspaceType: WorkspaceType | null;
69
+ /** `false` means this user is outside your audience. */
70
+ matches: boolean;
71
+ /**
72
+ * Whether Connect refuses the sign-in itself on a mismatch. When `true`,
73
+ * a `matches: false` can only reach you on a session minted before the
74
+ * setting was turned on. When `false`, gating is entirely yours to do -
75
+ * which is what `<ConnectAccessGate>` is for.
76
+ */
77
+ enforcedAtSignIn: boolean;
30
78
  }
31
79
  export type SubscriptionStatus = "created" | "authenticated" | "active" | "paused" | "cancelled";
32
80
  /** The compact subscription block returned alongside identity - `null` when the workspace has never subscribed to this application. */
@@ -58,11 +106,15 @@ export interface TokenResponse {
58
106
  user: ConnectUser;
59
107
  subscription: SessionSubscription | null;
60
108
  applicationRole: ApplicationRole;
109
+ /** Whether this account is the kind your application serves - see `SessionAudience`. */
110
+ audience: SessionAudience;
61
111
  }
62
112
  export interface UserResponse {
63
113
  user: ConnectUser;
64
114
  subscription: SessionSubscription | null;
65
115
  applicationRole: ApplicationRole;
116
+ /** Live, like `subscription` and `applicationRole` - a workspace that converted to a business since sign-in matches here without another sign-in. */
117
+ audience: SessionAudience;
66
118
  /** ISO 8601. Refresh proactively off this rather than waiting for a 401. */
67
119
  sessionExpiresAt: string;
68
120
  }
@@ -119,6 +171,8 @@ export interface ReadinessBlocker {
119
171
  resolveUrl: string | null;
120
172
  /** When this blocker resolves itself automatically - currently only set on `payment_retry_pending`, the deadline dunning suspends the subscription. `undefined`/`null` for every other code. */
121
173
  graceEndsAt?: string | null;
174
+ /** The account type this application requires - set only on `audience_mismatch`, `undefined`/`null` otherwise. Branch on this rather than reading it out of `message`. */
175
+ requiredWorkspaceType?: "individual" | "business" | null;
122
176
  }
123
177
  export interface ReadinessResponse {
124
178
  workspaceId: string;
@@ -261,6 +315,33 @@ export interface SubscriptionResponse {
261
315
  /** `null` exactly when `subscription` is. */
262
316
  plan: SubscribedPlan | null;
263
317
  }
318
+ /** One row of `subscribers.list()` - a workspace subscribed to your application. */
319
+ export interface ApplicationSubscriber {
320
+ subscriptionId: string;
321
+ workspaceId: string;
322
+ /** `null` when the workspace has been purged and only its tombstone remains. */
323
+ workspaceName: string | null;
324
+ /** `"active"`, `"suspended"`, `"pending_deletion"`, `"deleted"` - or `null` alongside a null `workspaceName`. */
325
+ workspaceStatus: string | null;
326
+ planId: string;
327
+ /** `null` if the plan was hard-deleted rather than archived. */
328
+ planName: string | null;
329
+ status: "created" | "authenticated" | "active" | "paused" | "cancelled";
330
+ seatCount: number;
331
+ /** ISO 8601. */
332
+ subscribedAt: string;
333
+ /** ISO 8601, or `null` for a cancelled/paused subscription. */
334
+ nextBillingDate: string | null;
335
+ }
336
+ export interface SubscribersResponse {
337
+ items: ApplicationSubscriber[];
338
+ /**
339
+ * Pass back as `before` for the next page. `null` on the last page - which
340
+ * is the only reliable end-of-list signal, since a full page can still be
341
+ * the last one.
342
+ */
343
+ nextBefore: string | null;
344
+ }
264
345
  /** One consumable add-on's remaining balance on a workspace's subscription. */
265
346
  export interface AddonUsage {
266
347
  addonId: string;
@@ -613,3 +694,96 @@ export interface ConnectWebhookEnvelope<T extends ConnectWebhookEventType = Conn
613
694
  apiVersion: string;
614
695
  data: ConnectWebhookEventMap[T];
615
696
  }
697
+ /** How a scalar should be presented. See `AiValue` for what each one expects. */
698
+ export type AiValueFormat = "text" | "number"
699
+ /** A number in rupees - `12500`, not paise and not a formatted string. */
700
+ | "currency"
701
+ /** A fraction - `0.42` renders as 42%. */
702
+ | "percent"
703
+ /** An ISO 8601 date string. */
704
+ | "date"
705
+ /** An ISO 8601 datetime string. */
706
+ | "datetime"
707
+ /** A number of seconds. */
708
+ | "duration"
709
+ /** A number of bytes. */
710
+ | "bytes"
711
+ /** A short state word, drawn as a chip. */
712
+ | "status";
713
+ export type AiTone = "neutral" | "positive" | "warning" | "danger";
714
+ /** One cell. `null` is "no value" and renders as an em dash. */
715
+ export type AiValue = string | number | null;
716
+ export interface AiTableInput {
717
+ title?: string;
718
+ columns: {
719
+ label: string;
720
+ format: AiValueFormat;
721
+ }[];
722
+ /** Positional: one array per row, values in the same order as `columns`. */
723
+ rows: AiValue[][];
724
+ caption?: string;
725
+ }
726
+ export interface AiChartInput {
727
+ title?: string;
728
+ kind: "bar" | "line" | "area" | "pie";
729
+ xLabel?: string;
730
+ yLabel?: string;
731
+ valueFormat: AiValueFormat;
732
+ /** Every series shares the same x values, in the same order. */
733
+ series: {
734
+ name: string;
735
+ points: {
736
+ x: string;
737
+ y: number | null;
738
+ }[];
739
+ }[];
740
+ caption?: string;
741
+ }
742
+ export interface AiMetricsInput {
743
+ title?: string;
744
+ metrics: {
745
+ label: string;
746
+ value: AiValue;
747
+ format: AiValueFormat;
748
+ /** Signed change against the previous period, as a fraction. */
749
+ delta?: number | null;
750
+ /** Whether a rise is good - churn is `"down-is-good"`. Colouring a delta by sign alone gets this wrong half the time. */
751
+ deltaMeaning: "up-is-good" | "down-is-good" | "neutral";
752
+ hint?: string;
753
+ }[];
754
+ }
755
+ export interface AiTimelineInput {
756
+ title?: string;
757
+ /** Oldest first. */
758
+ events: {
759
+ at: string;
760
+ title: string;
761
+ detail?: string;
762
+ tone: AiTone;
763
+ }[];
764
+ }
765
+ export interface AiDetailsInput {
766
+ title?: string;
767
+ fields: {
768
+ label: string;
769
+ value: AiValue;
770
+ format: AiValueFormat;
771
+ tone: AiTone;
772
+ }[];
773
+ }
774
+ export interface AiStepsInput {
775
+ title?: string;
776
+ steps: {
777
+ title: string;
778
+ detail?: string;
779
+ }[];
780
+ }
781
+ export interface AiSourcesInput {
782
+ sources: {
783
+ title: string;
784
+ uri?: string;
785
+ snippet?: string;
786
+ }[];
787
+ }
788
+ /** The tools this client knows how to draw. Anything else in the stream is ignored. */
789
+ export type AiToolName = "showTable" | "showChart" | "showMetrics" | "showTimeline" | "showDetails" | "showSteps" | "showSources";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmoops/connect-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Official server-side SDK for the CosmoOps Connect platform API - Login with Connect, entitlements, billing and metering.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
@@ -73,6 +73,7 @@
73
73
  "build:storybook": "storybook build -o storybook-static"
74
74
  },
75
75
  "devDependencies": {
76
+ "@connect/ai": "workspace:*",
76
77
  "@connect/data": "workspace:*",
77
78
  "@heroui/react": "^3.2.2",
78
79
  "@heroui/styles": "^3.2.2",
@@ -87,6 +88,7 @@
87
88
  "react": "19.2.8",
88
89
  "react-dom": "19.2.8",
89
90
  "storybook": "^10.5.10",
91
+ "streamdown": "^2.5.0",
90
92
  "tailwindcss": "^4.3.3",
91
93
  "typedoc": "^0.28.20",
92
94
  "typedoc-plugin-markdown": "^4.12.0",
@@ -98,7 +100,8 @@
98
100
  "lucide-react": ">=0.400.0",
99
101
  "motion": ">=12",
100
102
  "react": ">=19",
101
- "react-dom": ">=19"
103
+ "react-dom": ">=19",
104
+ "streamdown": ">=2.5.0"
102
105
  },
103
106
  "peerDependenciesMeta": {
104
107
  "@heroui/react": {
@@ -115,6 +118,9 @@
115
118
  },
116
119
  "react-dom": {
117
120
  "optional": true
121
+ },
122
+ "streamdown": {
123
+ "optional": true
118
124
  }
119
125
  }
120
126
  }