@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.
- package/CHANGELOG.md +165 -0
- package/README.md +273 -91
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +42 -7
- package/dist/cjs/client/components/account-sessions.js +3 -3
- package/dist/cjs/client/components/ai-format.js +135 -0
- package/dist/cjs/client/components/ai-widgets.js +232 -0
- package/dist/cjs/client/components/app-shell.js +9 -8
- package/dist/cjs/client/components/apps-grid.js +3 -3
- package/dist/cjs/client/components/ask-ai.js +334 -0
- package/dist/cjs/client/components/feature-gate.js +2 -2
- package/dist/cjs/client/components/integrations-modal.js +1 -1
- package/dist/cjs/client/components/logo.js +3 -3
- package/dist/cjs/client/components/nav.js +1 -1
- package/dist/cjs/client/components/plan-pricing.js +1 -1
- package/dist/cjs/client/components/plans.js +6 -6
- package/dist/cjs/client/components/readiness.js +3 -3
- package/dist/cjs/client/components/side-nav.js +1 -1
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +1 -1
- package/dist/cjs/client/components/usage-meter.js +1 -1
- package/dist/cjs/client/components/user-menu.js +31 -8
- package/dist/cjs/client/components/workspace-type-gate.js +48 -0
- package/dist/cjs/client/index.js +32 -4
- package/dist/cjs/client/mark.js +1 -1
- package/dist/cjs/client/provider.js +50 -7
- package/dist/cjs/client/use-checkout.js +3 -3
- package/dist/cjs/errors.js +4 -4
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/next/index.js +2 -2
- package/dist/cjs/server/client.js +15 -15
- package/dist/cjs/server/handler.js +56 -13
- package/dist/cjs/server/http.js +47 -25
- package/dist/cjs/server/resources/access.js +3 -3
- package/dist/cjs/server/resources/ai.js +1 -1
- package/dist/cjs/server/resources/analytics.js +2 -2
- package/dist/cjs/server/resources/apps.js +4 -4
- package/dist/cjs/server/resources/billing.js +5 -5
- package/dist/cjs/server/resources/drive.js +4 -4
- package/dist/cjs/server/resources/entitlements.js +48 -7
- package/dist/cjs/server/resources/identity.js +6 -4
- package/dist/cjs/server/resources/integrations.js +13 -13
- package/dist/cjs/server/resources/logs.js +3 -3
- package/dist/cjs/server/resources/sessions.js +6 -6
- package/dist/cjs/server/webhooks.js +4 -4
- package/dist/cjs/types.js +1 -1
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +43 -8
- package/dist/esm/client/components/account-sessions.js +3 -3
- package/dist/esm/client/components/ai-format.js +129 -0
- package/dist/esm/client/components/ai-widgets.js +223 -0
- package/dist/esm/client/components/app-shell.js +8 -7
- package/dist/esm/client/components/apps-grid.js +3 -3
- package/dist/esm/client/components/ask-ai.js +331 -0
- package/dist/esm/client/components/feature-gate.js +2 -2
- package/dist/esm/client/components/integrations-modal.js +1 -1
- package/dist/esm/client/components/logo.js +3 -3
- package/dist/esm/client/components/nav.js +1 -1
- package/dist/esm/client/components/plan-pricing.js +1 -1
- package/dist/esm/client/components/plans.js +6 -6
- package/dist/esm/client/components/readiness.js +3 -3
- package/dist/esm/client/components/side-nav.js +1 -1
- package/dist/esm/client/components/subscribe-dialog.js +2 -2
- package/dist/esm/client/components/subscription.js +1 -1
- package/dist/esm/client/components/usage-meter.js +1 -1
- package/dist/esm/client/components/user-menu.js +31 -8
- package/dist/esm/client/components/workspace-type-gate.js +45 -0
- package/dist/esm/client/index.js +14 -3
- package/dist/esm/client/mark.js +1 -1
- package/dist/esm/client/provider.js +48 -7
- package/dist/esm/client/use-checkout.js +3 -3
- package/dist/esm/errors.js +4 -4
- package/dist/esm/helpers.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/next/index.js +2 -2
- package/dist/esm/server/client.js +15 -15
- package/dist/esm/server/handler.js +56 -13
- package/dist/esm/server/http.js +47 -25
- package/dist/esm/server/resources/access.js +3 -3
- package/dist/esm/server/resources/ai.js +1 -1
- package/dist/esm/server/resources/analytics.js +2 -2
- package/dist/esm/server/resources/apps.js +4 -4
- package/dist/esm/server/resources/billing.js +5 -5
- package/dist/esm/server/resources/drive.js +4 -4
- package/dist/esm/server/resources/entitlements.js +48 -7
- package/dist/esm/server/resources/identity.js +6 -4
- package/dist/esm/server/resources/integrations.js +13 -13
- package/dist/esm/server/resources/logs.js +3 -3
- package/dist/esm/server/resources/sessions.js +6 -6
- package/dist/esm/server/webhooks.js +4 -4
- package/dist/esm/types.js +1 -1
- package/dist/theme.css +87 -0
- package/dist/types/client/components/access-gate.d.ts +18 -4
- package/dist/types/client/components/account-sessions.d.ts +1 -1
- package/dist/types/client/components/ai-format.d.ts +24 -0
- package/dist/types/client/components/ai-widgets.d.ts +42 -0
- package/dist/types/client/components/app-shell.d.ts +25 -9
- package/dist/types/client/components/apps-grid.d.ts +1 -1
- package/dist/types/client/components/ask-ai.d.ts +73 -0
- package/dist/types/client/components/feature-gate.d.ts +1 -1
- package/dist/types/client/components/integrations-modal.d.ts +1 -1
- package/dist/types/client/components/logo.d.ts +5 -5
- package/dist/types/client/components/nav.d.ts +3 -3
- package/dist/types/client/components/plan-pricing.d.ts +2 -2
- package/dist/types/client/components/readiness.d.ts +2 -2
- package/dist/types/client/components/side-nav.d.ts +1 -1
- package/dist/types/client/components/subscribe-dialog.d.ts +3 -3
- package/dist/types/client/components/usage-meter.d.ts +1 -1
- package/dist/types/client/components/user-menu.d.ts +4 -3
- package/dist/types/client/components/workspace-type-gate.d.ts +48 -0
- package/dist/types/client/index.d.ts +11 -4
- package/dist/types/client/mark.d.ts +1 -1
- package/dist/types/client/provider.d.ts +52 -9
- package/dist/types/client/use-checkout.d.ts +3 -3
- package/dist/types/errors.d.ts +4 -4
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/next/index.d.ts +1 -1
- package/dist/types/server/client.d.ts +13 -13
- package/dist/types/server/handler.d.ts +2 -2
- package/dist/types/server/http.d.ts +6 -1
- package/dist/types/server/resources/access.d.ts +3 -3
- package/dist/types/server/resources/ai.d.ts +2 -2
- package/dist/types/server/resources/analytics.d.ts +2 -2
- package/dist/types/server/resources/apps.d.ts +4 -4
- package/dist/types/server/resources/billing.d.ts +4 -4
- package/dist/types/server/resources/drive.d.ts +4 -4
- package/dist/types/server/resources/entitlements.d.ts +37 -5
- package/dist/types/server/resources/identity.d.ts +2 -2
- package/dist/types/server/resources/integrations.d.ts +9 -9
- package/dist/types/server/resources/logs.d.ts +3 -3
- package/dist/types/server/resources/sessions.d.ts +5 -5
- package/dist/types/server/webhooks.d.ts +4 -4
- package/dist/types/types.d.ts +235 -61
- 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
|
|
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?.
|
|
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.
|
|
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/
|
|
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(
|
|
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
|
+
}
|
package/dist/esm/client/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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";
|
package/dist/esm/client/mark.js
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
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` -
|
|
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
|
|
269
|
+
/** This application's plan catalog. */
|
|
229
270
|
export function useConnectPlans() {
|
|
230
271
|
return useConnectResource("/plans");
|
|
231
272
|
}
|
|
232
|
-
/** The apps this
|
|
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
|
|
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
|
|
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/(.)
|
|
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,
|
|
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
|
-
: `/
|
|
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());
|
package/dist/esm/errors.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Every failure the Connect
|
|
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/
|
|
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
|
|
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
|
|
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.
|
package/dist/esm/helpers.js
CHANGED
|
@@ -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
|
|
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
|
-
* **
|
|
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
|
package/dist/esm/next/index.js
CHANGED
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
43
|
+
* The CosmoOps Connect application-partner API.
|
|
44
44
|
*
|
|
45
|
-
* Covers `/api/v1/
|
|
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.
|
|
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
|
|
65
|
+
/** What a signed-in member may do on your application. */
|
|
66
66
|
access;
|
|
67
|
-
/** The
|
|
67
|
+
/** The applications a workspace can open - for a launcher grid. */
|
|
68
68
|
apps;
|
|
69
|
-
/** Whether a workspace may use your
|
|
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
|
|
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
|
|
79
|
+
/** Feature events into the Console's audit log, filterable to your application. */
|
|
80
80
|
logs;
|
|
81
|
-
/** Emit product events and read back this
|
|
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
|
|
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, ...)
|
|
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}${
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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.
|
|
180
|
-
client.entitlements.getSubscription(user.
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
267
|
+
await client.integrations.disconnect(user.workspaceId, body.toolkitSlug);
|
|
225
268
|
return json({ ok: true });
|
|
226
269
|
},
|
|
227
|
-
// Account-wide, not scoped to this
|
|
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
|