@cosmoops/connect-sdk 0.2.0 → 0.6.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 +125 -0
- package/README.md +284 -7
- package/dist/cjs/cli/doctor.js +209 -0
- package/dist/cjs/cli/init.js +20 -8
- package/dist/cjs/client/components/access-gate.js +162 -0
- package/dist/cjs/client/components/account-sessions.js +21 -19
- package/dist/cjs/client/components/app-shell.js +157 -0
- package/dist/cjs/client/components/apps-grid.js +2 -1
- package/dist/cjs/client/components/integrations-modal.js +95 -0
- package/dist/cjs/client/components/motion.js +27 -0
- package/dist/cjs/client/components/nav.js +97 -0
- package/dist/cjs/client/components/plans.js +39 -37
- package/dist/cjs/client/components/readiness.js +3 -1
- package/dist/cjs/client/components/side-nav.js +58 -0
- package/dist/cjs/client/components/subscription.js +2 -1
- package/dist/cjs/client/components/surfaces.js +54 -0
- package/dist/cjs/client/components/usage-meter.js +3 -2
- package/dist/cjs/client/components/user-menu.js +30 -3
- package/dist/cjs/client/index.js +29 -1
- package/dist/cjs/client/provider.js +54 -2
- package/dist/cjs/client/use-active-path.js +77 -0
- package/dist/cjs/errors.js +1 -1
- package/dist/cjs/next/index.js +96 -15
- package/dist/cjs/server/client.js +10 -2
- package/dist/cjs/server/handler.js +89 -6
- package/dist/cjs/server/index.js +8 -1
- package/dist/cjs/server/resources/analytics.js +66 -0
- package/dist/cjs/server/resources/identity.js +1 -1
- package/dist/cjs/server/resources/integrations.js +66 -0
- package/dist/cjs/server/resources/sessions.js +1 -1
- package/dist/cjs/server/webhooks.js +127 -0
- package/dist/cjs/this-entry-is-server-only.js +31 -0
- package/dist/esm/cli/doctor.js +206 -0
- package/dist/esm/cli/init.js +20 -8
- package/dist/esm/client/components/access-gate.js +158 -0
- package/dist/esm/client/components/account-sessions.js +21 -19
- package/dist/esm/client/components/app-shell.js +154 -0
- package/dist/esm/client/components/apps-grid.js +2 -1
- package/dist/esm/client/components/integrations-modal.js +92 -0
- package/dist/esm/client/components/motion.js +21 -0
- package/dist/esm/client/components/nav.js +89 -0
- package/dist/esm/client/components/plans.js +39 -37
- package/dist/esm/client/components/readiness.js +3 -1
- package/dist/esm/client/components/side-nav.js +53 -0
- package/dist/esm/client/components/subscription.js +2 -1
- package/dist/esm/client/components/surfaces.js +51 -0
- package/dist/esm/client/components/usage-meter.js +3 -2
- package/dist/esm/client/components/user-menu.js +31 -4
- package/dist/esm/client/index.js +15 -1
- package/dist/esm/client/provider.js +53 -2
- package/dist/esm/client/use-active-path.js +74 -0
- package/dist/esm/errors.js +1 -1
- package/dist/esm/next/index.js +95 -15
- package/dist/esm/server/client.js +10 -2
- package/dist/esm/server/handler.js +89 -6
- package/dist/esm/server/index.js +5 -0
- package/dist/esm/server/resources/analytics.js +62 -0
- package/dist/esm/server/resources/identity.js +1 -1
- package/dist/esm/server/resources/integrations.js +62 -0
- package/dist/esm/server/resources/sessions.js +1 -1
- package/dist/esm/server/webhooks.js +123 -0
- package/dist/esm/this-entry-is-server-only.js +30 -0
- package/dist/theme.css +28 -0
- package/dist/types/cli/doctor.d.ts +23 -0
- package/dist/types/cli/init.d.ts +2 -3
- package/dist/types/client/components/access-gate.d.ts +79 -0
- package/dist/types/client/components/app-shell.d.ts +115 -0
- package/dist/types/client/components/integrations-modal.d.ts +21 -0
- package/dist/types/client/components/motion.d.ts +22 -0
- package/dist/types/client/components/nav.d.ts +110 -0
- package/dist/types/client/components/side-nav.d.ts +31 -0
- package/dist/types/client/components/surfaces.d.ts +51 -0
- package/dist/types/client/index.d.ts +12 -1
- package/dist/types/client/provider.d.ts +38 -3
- package/dist/types/client/use-active-path.d.ts +8 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/next/index.d.ts +40 -0
- package/dist/types/server/client.d.ts +8 -2
- package/dist/types/server/index.d.ts +17 -2
- package/dist/types/server/resources/analytics.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +12 -2
- package/dist/types/server/resources/integrations.d.ts +40 -0
- package/dist/types/server/webhooks.d.ts +67 -0
- package/dist/types/this-entry-is-server-only.d.ts +1 -0
- package/dist/types/types.d.ts +182 -1
- package/package.json +22 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one elevation model every panel inside a Connect modal follows.
|
|
3
|
+
*
|
|
4
|
+
* This exists because "just use `bg-surface`" is wrong here, and wrong in a
|
|
5
|
+
* way that is invisible until you look at the tokens. `Modal.Dialog` paints
|
|
6
|
+
* itself `bg-overlay`, and in the shared theme (`theme.css`) `--overlay` and
|
|
7
|
+
* `--surface` sit at the *same lightness* in both schemes - 100% against 100%
|
|
8
|
+
* in light, 21% against 21% in dark. A `bg-surface` panel dropped on a modal
|
|
9
|
+
* therefore has no fill contrast at all; only its border separates it from
|
|
10
|
+
* the dialog behind it.
|
|
11
|
+
*
|
|
12
|
+
* That is how the account modal ended up looking half-finished: the identity
|
|
13
|
+
* card used `bg-surface-secondary` and read as a solid block, while the
|
|
14
|
+
* destination tiles and the session list used `bg-surface` and read as
|
|
15
|
+
* hairline outlines floating on nothing. Same nesting depth, two different
|
|
16
|
+
* physical materials, for no reason a user could infer.
|
|
17
|
+
*
|
|
18
|
+
* Three levels, and nesting one inside another always steps the fill:
|
|
19
|
+
*
|
|
20
|
+
* - **L0 - the dialog itself.** `bg-overlay`, owned by HeroUI. Not ours.
|
|
21
|
+
* - **L1 - a panel on the dialog.** `PANEL_FILL` + `border-border`. The
|
|
22
|
+
* identity card, each destination tile, the session list.
|
|
23
|
+
* - **L2 - a medallion inside a panel.** `bg-surface`, which is L0's value
|
|
24
|
+
* and therefore reads as *inset* against L1 rather than raised. Light:
|
|
25
|
+
* white on grey. Dark: near-black on charcoal. Both directions legible.
|
|
26
|
+
*
|
|
27
|
+
* The inversion at L2 is deliberate and is why this is written down. Reaching
|
|
28
|
+
* for a "lighter" fill one step down works in light mode and quietly fails in
|
|
29
|
+
* dark; stepping *back to the dialog's own value* is the trick that reads in
|
|
30
|
+
* both, because the contrast comes from the gap between L1 and L0 rather than
|
|
31
|
+
* from a direction of travel.
|
|
32
|
+
*/
|
|
33
|
+
export const PANEL_FILL = "bg-surface-secondary";
|
|
34
|
+
/**
|
|
35
|
+
* The icon well shared by destination tiles and session rows: a 40px box
|
|
36
|
+
* holding a 20px glyph.
|
|
37
|
+
*
|
|
38
|
+
* Half the well, which is the point. Both surfaces used to run a `size-4`
|
|
39
|
+
* (16px) icon inside a `size-9` (36px) well - a 44% fill that reads as an
|
|
40
|
+
* icon someone forgot to finish, and the specific thing that made the
|
|
41
|
+
* Profile/Billing/Connect tiles look under-drawn. They also disagreed on
|
|
42
|
+
* shape (a circle in the tiles, a rounded square in the session rows) for
|
|
43
|
+
* what is the same component role, so the two surfaces never looked related.
|
|
44
|
+
*
|
|
45
|
+
* Pair with `ICON` on the glyph inside. Session rows override the well to
|
|
46
|
+
* `size-9` for density but keep the same `ICON`, so glyph size stays constant
|
|
47
|
+
* across the whole modal even where the well does not.
|
|
48
|
+
*/
|
|
49
|
+
export const MEDALLION = "size-10";
|
|
50
|
+
/** The glyph inside a `MEDALLION`. Constant across every well in the modal - see `MEDALLION`. */
|
|
51
|
+
export const ICON = "size-5";
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Card, Skeleton } from "@heroui/react";
|
|
4
4
|
import { AlertTriangle } from "lucide-react";
|
|
5
5
|
import { useConnect, useConnectAddonUsage } from "../provider.js";
|
|
6
|
+
import { m, METER_SPRING, MotionProvider } from "./motion.js";
|
|
6
7
|
import { formatPrice } from "./plan-pricing.js";
|
|
7
8
|
function formatUnits(value, unit) {
|
|
8
9
|
return `${value.toLocaleString("en-IN")} ${unit}`;
|
|
@@ -50,7 +51,7 @@ export function ConnectUsageMeter({ addonIds, hideWhenEmpty = true, title = "Usa
|
|
|
50
51
|
return null;
|
|
51
52
|
return (_jsx(Card, { className: className, children: _jsxs(Card.Header, { children: [_jsx(Card.Title, { children: title }), _jsx(Card.Description, { children: "No metered add-ons on this plan." })] }) }));
|
|
52
53
|
}
|
|
53
|
-
return (_jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsx(Card.Title, { children: title }), data.resetsAt && _jsxs(Card.Description, { children: ["Resets on ", formatDate(data.resetsAt), "."] })] }), _jsx(Card.Content, { className: "flex flex-col gap-4", children: addons.map((addon) => (_jsx(AddonRow, { addon: addon }, addon.addonId))) })] }));
|
|
54
|
+
return (_jsx(MotionProvider, { children: _jsxs(Card, { className: className, children: [_jsxs(Card.Header, { children: [_jsx(Card.Title, { children: title }), data.resetsAt && _jsxs(Card.Description, { children: ["Resets on ", formatDate(data.resetsAt), "."] })] }), _jsx(Card.Content, { className: "flex flex-col gap-4", children: addons.map((addon) => (_jsx(AddonRow, { addon: addon }, addon.addonId))) })] }) }));
|
|
54
55
|
}
|
|
55
56
|
function AddonRow({ addon }) {
|
|
56
57
|
const overdrawn = addon.balance < 0;
|
|
@@ -61,5 +62,5 @@ function AddonRow({ addon }) {
|
|
|
61
62
|
const carried = addon.balance > addon.includedPerCycle ? addon.balance - addon.includedPerCycle : 0;
|
|
62
63
|
return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs("div", { className: "flex flex-wrap items-baseline justify-between gap-2 text-sm", children: [_jsx("span", { className: "font-medium text-foreground", children: addon.name }), _jsxs("span", { className: overdrawn ? "text-danger" : "text-muted", children: [overdrawn
|
|
63
64
|
? `${formatUnits(Math.abs(addon.balance), addon.unit)} over`
|
|
64
|
-
: `${formatUnits(addon.balance, addon.unit)} left`, _jsxs("span", { className: "text-muted", children: [" of ", formatUnits(addon.includedPerCycle, addon.unit)] })] })] }), _jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-surface", role: "meter", "aria-label": `${addon.name} remaining`, "aria-valuenow": addon.balance, "aria-valuemin": 0, "aria-valuemax": addon.includedPerCycle, children: _jsx(
|
|
65
|
+
: `${formatUnits(addon.balance, addon.unit)} left`, _jsxs("span", { className: "text-muted", children: [" of ", formatUnits(addon.includedPerCycle, addon.unit)] })] })] }), _jsx("div", { className: "h-1.5 w-full overflow-hidden rounded-full bg-surface", role: "meter", "aria-label": `${addon.name} remaining`, "aria-valuenow": addon.balance, "aria-valuemin": 0, "aria-valuemax": addon.includedPerCycle, children: _jsx(m.div, { className: `h-full rounded-full ${overdrawn ? "bg-danger" : "bg-accent"}`, initial: { width: 0 }, animate: { width: `${overdrawn ? 100 : fraction * 100}%` }, transition: METER_SPRING }) }), carried > 0 && (_jsxs("p", { className: "text-xs text-muted", children: ["Includes ", formatUnits(carried, addon.unit), " carried over from last cycle."] })), overdrawn && addon.overagePricePerUnit !== null && (_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-danger", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), "Billed at ", formatPrice(addon.overagePricePerUnit), " per ", addon.unit, " beyond your allowance."] })), !overdrawn && addon.overagePolicy === "reject" && addon.balance === 0 && (_jsxs("p", { className: "flex items-center gap-1.5 text-xs text-danger", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0", "aria-hidden": "true" }), "Nothing left this cycle - further use is blocked until it resets."] }))] }));
|
|
65
66
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Avatar, Button, Chip, Modal,
|
|
3
|
+
import { Avatar, Button, Chip, Modal, Skeleton, ToggleButton, ToggleButtonGroup, } from "@heroui/react";
|
|
4
4
|
import { CreditCard, ExternalLink, Laptop, LogOut, Moon, ReceiptText, Sun, UserRound, } from "lucide-react";
|
|
5
5
|
import { useConnect } from "../provider.js";
|
|
6
6
|
import { ConnectMark } from "../mark.js";
|
|
7
7
|
import { ConnectAccountSessions } from "./account-sessions.js";
|
|
8
|
+
import { ICON, MEDALLION, PANEL_FILL } from "./surfaces.js";
|
|
8
9
|
const THEME_OPTIONS = [
|
|
9
10
|
{ value: "light", label: "Light", icon: Sun },
|
|
10
11
|
{ value: "dark", label: "Dark", icon: Moon },
|
|
@@ -18,6 +19,20 @@ function initials(name, email) {
|
|
|
18
19
|
function openExternal(url) {
|
|
19
20
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* The member's photo, with their initials underneath it.
|
|
24
|
+
*
|
|
25
|
+
* Both halves always render, which is the whole trick: `<Avatar.Fallback>` is
|
|
26
|
+
* what shows while `src` is still loading and what stays if it 404s or the
|
|
27
|
+
* user has no photo at all, so there is no `photoUrl &&` branch anywhere and
|
|
28
|
+
* no flash of an empty circle on a cold cache. This component previously
|
|
29
|
+
* rendered *only* the fallback - `ConnectUser` carried no photo field at all,
|
|
30
|
+
* so every Connect account showed initials no matter what picture the member
|
|
31
|
+
* had set on their profile.
|
|
32
|
+
*/
|
|
33
|
+
function UserAvatar({ user, size, }) {
|
|
34
|
+
return (_jsxs(Avatar, { size: size, color: "accent", variant: "soft", children: [user.photoUrl && _jsx(Avatar.Image, { alt: "", src: user.photoUrl }), _jsx(Avatar.Fallback, { children: initials(user.displayName, user.email) })] }));
|
|
35
|
+
}
|
|
21
36
|
/** `dispatch_admin` / `billing-viewer` -> `Dispatch admin`. */
|
|
22
37
|
function humanizeRole(role) {
|
|
23
38
|
const words = role.replace(/[_-]+/g, " ").trim();
|
|
@@ -58,9 +73,17 @@ function subscriptionChip(subscription) {
|
|
|
58
73
|
* `w-full` is load-bearing, not decoration: HeroUI's `.button` base is
|
|
59
74
|
* `w-fit`, so a tile dropped into a grid cell shrinks to its own label and
|
|
60
75
|
* the three of them come out ragged and unequal.
|
|
76
|
+
*
|
|
77
|
+
* On the box: `p-3` all round rather than the `px-2 py-4` this used to carry.
|
|
78
|
+
* That old pair was both lopsided (16px of vertical air against 8px of
|
|
79
|
+
* horizontal) and too tall for what it held, so a 16px glyph sat marooned in
|
|
80
|
+
* a ~92px tile. Even padding plus the shared `size-10`/`size-5` medallion
|
|
81
|
+
* (see `MEDALLION`) puts the icon at half the well instead of a stranded
|
|
82
|
+
* 44%, and takes the tile down to ~88px while making its contents read
|
|
83
|
+
* bigger, not smaller.
|
|
61
84
|
*/
|
|
62
85
|
function DestinationTile({ icon, label, url, }) {
|
|
63
|
-
return (_jsxs(Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className:
|
|
86
|
+
return (_jsxs(Button, { variant: "tertiary", "aria-label": `${label} (opens in a new tab)`, className: `relative h-auto w-full flex-col items-center justify-center gap-2 rounded-xl border border-border ${PANEL_FILL} p-3 hover:border-accent hover:bg-surface-secondary`, onPress: () => openExternal(url), children: [_jsx(ExternalLink, { className: "absolute end-2 top-2 size-3.5 text-muted", "aria-hidden": "true" }), _jsx("span", { className: `flex ${MEDALLION} items-center justify-center rounded-xl bg-surface text-muted`, children: icon }), _jsx("span", { className: "text-xs font-medium text-foreground", children: label })] }));
|
|
64
87
|
}
|
|
65
88
|
/**
|
|
66
89
|
* Avatar and name in the trigger; an identity card, the three Connect
|
|
@@ -101,11 +124,15 @@ export function ConnectUserMenu({ compact = false, onSignOut, className, theme,
|
|
|
101
124
|
const label = user.displayName ?? user.email ?? "Account";
|
|
102
125
|
const role = session?.serviceRole?.role;
|
|
103
126
|
const planChip = subscriptionChip(session?.subscription ?? null);
|
|
104
|
-
return (_jsxs(Modal, { children: [_jsxs(Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
|
|
127
|
+
return (_jsxs(Modal, { children: [_jsxs(Button, { variant: "tertiary", "aria-label": `Account: ${label}`,
|
|
128
|
+
// `pl-1` against the default right padding is optical, not sloppy: a
|
|
129
|
+
// circular avatar needs less inset than a text label to look evenly
|
|
130
|
+
// 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) => {
|
|
105
132
|
const [next] = [...keys];
|
|
106
133
|
if (typeof next === "string")
|
|
107
134
|
onThemeChange(next);
|
|
108
|
-
}, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => (_jsxs(ToggleButton, { id: value, children: [index > 0 && _jsx(ToggleButtonGroup.Separator, {}), _jsx(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), _jsx(
|
|
135
|
+
}, children: THEME_OPTIONS.map(({ value, label: optionLabel, icon: Icon }, index) => (_jsxs(ToggleButton, { id: value, children: [index > 0 && _jsx(ToggleButtonGroup.Separator, {}), _jsx(Icon, { className: "size-4", "aria-hidden": "true" }), optionLabel] }, value))) })] })), _jsx(ConnectAccountSessions, {})] }), _jsx(Modal.Footer, { children: _jsxs(Button, { slot: "close", fullWidth: true, variant: "tertiary", className: "text-danger", onPress: () => {
|
|
109
136
|
void signOut().then(onSignOut);
|
|
110
137
|
}, children: [_jsx(LogOut, { className: "size-4 shrink-0" }), "Sign out"] }) })] }) }) })] }));
|
|
111
138
|
}
|
package/dist/esm/client/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* whatever theme the host service 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, useConnectResource, CONNECT_APP_URL, } from "./provider.js";
|
|
14
|
+
export { ConnectProvider, useConnect, useConnectUser, 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.
|
|
@@ -24,7 +24,20 @@ export { ConnectUserMenu, ConnectManageSubscriptionButton, ConnectInvoicesButton
|
|
|
24
24
|
// Built, compiled into `dist/`, and reachable from no entry point until now -
|
|
25
25
|
// `ConnectUserMenu` was the only thing that could render it.
|
|
26
26
|
export { ConnectAccountSessions } from "./components/account-sessions.js";
|
|
27
|
+
// The frame the rest of these components live inside. Exported next to the
|
|
28
|
+
// pieces it composes rather than in a section of its own - an integration
|
|
29
|
+
// reaching for a sidebar wants to find the launcher and the account menu in
|
|
30
|
+
// the same breath, since the shell already mounts both.
|
|
31
|
+
export { ConnectAppShell } from "./components/app-shell.js";
|
|
32
|
+
export { ConnectSideNav } from "./components/side-nav.js";
|
|
33
|
+
export { useActivePath } from "./use-active-path.js";
|
|
34
|
+
// The nav model is plain data and pure functions - a host generating its nav
|
|
35
|
+
// from its own route table needs the types, and `resolveActivePath` is the
|
|
36
|
+
// same longest-prefix rule the sidebar highlights with, so a breadcrumb or a
|
|
37
|
+
// page title derived from it cannot disagree with the rail.
|
|
38
|
+
export { normalizeNav, flattenNav, normalizePath, resolveActivePath, isNavItemActive, gateExemptPaths, } from "./components/nav.js";
|
|
27
39
|
export { ConnectAppsGrid } from "./components/apps-grid.js";
|
|
40
|
+
export { ConnectIntegrationsModal } from "./components/integrations-modal.js";
|
|
28
41
|
export { ConnectPlans } from "./components/plans.js";
|
|
29
42
|
export { ConnectSubscribeDialog } from "./components/subscribe-dialog.js";
|
|
30
43
|
// The subscribe / upgrade / downgrade flow's two moving parts, exported so a
|
|
@@ -34,6 +47,7 @@ export { ConnectSubscribeDialog } from "./components/subscribe-dialog.js";
|
|
|
34
47
|
// Subscription state, gates and the surfaces around the plan grid.
|
|
35
48
|
export { ConnectSubscriptionGate, ConnectNoSubscriptionCard, ConnectSubscriptionCard, ConnectTrialBanner, ConnectSeatUsage, resolveSubscriptionState, } from "./components/subscription.js";
|
|
36
49
|
export { ConnectReadinessBanner, ConnectReadinessGate } from "./components/readiness.js";
|
|
50
|
+
export { ConnectAccessGate, useConnectAccessGate } from "./components/access-gate.js";
|
|
37
51
|
export { ConnectFeatureGate, useConnectFeature } from "./components/feature-gate.js";
|
|
38
52
|
export { ConnectUsageMeter } from "./components/usage-meter.js";
|
|
39
53
|
export { ConnectPlansPage } from "./components/plans-page.js";
|
|
@@ -3,6 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
export const CONNECT_APP_URL = "https://connect.cosmoops.com";
|
|
5
5
|
const ConnectContext = createContext(null);
|
|
6
|
+
/** Marks that this tab has already spent its one silent attempt - see `ConnectProviderProps.silentSignIn`. */
|
|
7
|
+
const SILENT_ATTEMPT_KEY = "connect_silent_attempted";
|
|
6
8
|
/**
|
|
7
9
|
* Wraps the part of your app that shows Connect UI.
|
|
8
10
|
*
|
|
@@ -17,14 +19,44 @@ const ConnectContext = createContext(null);
|
|
|
17
19
|
* </ConnectProvider>
|
|
18
20
|
* ```
|
|
19
21
|
*/
|
|
20
|
-
export function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = CONNECT_APP_URL, fetch: fetchImpl, children, }) {
|
|
22
|
+
export function ConnectProvider({ clientId = "", basePath = "/api/connect", redirectUri, appUrl = CONNECT_APP_URL, fetch: fetchImpl, silentSignIn = false, children, }) {
|
|
21
23
|
// One state cell, not three. `isLoading` is *derived* from "no result yet"
|
|
22
24
|
// rather than set - a `setIsLoading(true)` at the top of an effect is a
|
|
23
25
|
// synchronous setState inside that effect, which cascades an extra render
|
|
24
26
|
// and is what `react-hooks/set-state-in-effect` flags. Everything below
|
|
25
27
|
// writes state only from an async callback.
|
|
26
28
|
const [result, setResult] = useState(null);
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Whether this tab has already spent its one silent attempt, sampled once
|
|
31
|
+
* at mount.
|
|
32
|
+
*
|
|
33
|
+
* A lazy `useState` initializer rather than a read inside the render body:
|
|
34
|
+
* `sessionStorage` is external mutable state, and reading it during render
|
|
35
|
+
* is impure - with the React Compiler on, that is exactly the kind of thing
|
|
36
|
+
* that can be hoisted or memoized into the wrong answer. An initializer
|
|
37
|
+
* runs once, which is all this needs; the value only ever changes as this
|
|
38
|
+
* component navigates the page away.
|
|
39
|
+
*
|
|
40
|
+
* Safe on the server, where it reads `false`: `result` is `null` during SSR
|
|
41
|
+
* and first client render alike, so `isLoading` below is `true` on both
|
|
42
|
+
* regardless of this value, and hydration cannot diverge on it.
|
|
43
|
+
*/
|
|
44
|
+
const [silentAttemptSpent] = useState(() => typeof window !== "undefined" && window.sessionStorage.getItem(SILENT_ATTEMPT_KEY) === "1");
|
|
45
|
+
/**
|
|
46
|
+
* A silent attempt is settled-signed-out *and* about to redirect. Reported
|
|
47
|
+
* as still-loading, which is the honest answer: the question "is this
|
|
48
|
+
* visitor signed in" has not been finally answered yet, and there is a
|
|
49
|
+
* navigation already on its way.
|
|
50
|
+
*
|
|
51
|
+
* Without this the provider publishes `signed out` for the one frame before
|
|
52
|
+
* the effect below fires, so every consumer paints its signed-out state -
|
|
53
|
+
* a "Sign in" button, the plan-picker gate - and then the page vanishes
|
|
54
|
+
* underneath it. Flashing a sales pitch at someone who is about to be
|
|
55
|
+
* silently signed in is the same bug `ConnectSubscriptionGate` exists to
|
|
56
|
+
* prevent, one layer down.
|
|
57
|
+
*/
|
|
58
|
+
const silentPending = silentSignIn && !silentAttemptSpent && result !== null && !result.session.user && !result.error;
|
|
59
|
+
const isLoading = result === null || silentPending;
|
|
28
60
|
const session = result?.session ?? null;
|
|
29
61
|
const error = result?.error ?? null;
|
|
30
62
|
// One entry per path. Held in a ref, not state, because a cache write must
|
|
@@ -104,6 +136,21 @@ export function ConnectProvider({ clientId = "", basePath = "/api/connect", redi
|
|
|
104
136
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
105
137
|
void refresh();
|
|
106
138
|
}, [refresh]);
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
// `silentPending` already encodes settled-and-signed-out, no prior
|
|
141
|
+
// attempt, and no bridge error - a failed bridge read says nothing about
|
|
142
|
+
// whether Connect has a session, and bouncing the browser on a transient
|
|
143
|
+
// network failure would turn a recoverable error into a navigation nobody
|
|
144
|
+
// asked for.
|
|
145
|
+
if (!silentPending || typeof window === "undefined")
|
|
146
|
+
return;
|
|
147
|
+
// Written before navigating, not after: a navigation that begins before
|
|
148
|
+
// the marker lands leaves the tab unmarked and free to probe again on the
|
|
149
|
+
// way back, which is the loop this bounds.
|
|
150
|
+
window.sessionStorage.setItem(SILENT_ATTEMPT_KEY, "1");
|
|
151
|
+
const next = `${window.location.pathname}${window.location.search}`;
|
|
152
|
+
window.location.href = `${basePath}/signin?prompt=none&next=${encodeURIComponent(next)}`;
|
|
153
|
+
}, [silentPending, basePath]);
|
|
107
154
|
const signIn = useCallback((options = {}) => {
|
|
108
155
|
const url = new URL("/sso/authorize", appUrl);
|
|
109
156
|
url.searchParams.set("client_id", clientId);
|
|
@@ -186,6 +233,10 @@ export function useConnectPlans() {
|
|
|
186
233
|
export function useConnectApps() {
|
|
187
234
|
return useConnectResource("/apps");
|
|
188
235
|
}
|
|
236
|
+
/** Enabled Composio-hosted toolkits and this organization's connection status for each. */
|
|
237
|
+
export function useConnectIntegrations() {
|
|
238
|
+
return useConnectResource("/integrations");
|
|
239
|
+
}
|
|
189
240
|
/**
|
|
190
241
|
* This Connect account's own login sessions - account-wide, not scoped to
|
|
191
242
|
* this service, see `SessionsListResponse`'s doc comment. The component
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
const LOCATION_CHANGE = "connect:locationchange";
|
|
4
|
+
let patched = false;
|
|
5
|
+
/**
|
|
6
|
+
* Make `history.pushState`/`replaceState` announce themselves.
|
|
7
|
+
*
|
|
8
|
+
* Neither fires an event - `popstate` covers Back and Forward only - so a
|
|
9
|
+
* client-side router that pushes a new URL moves the address bar and tells
|
|
10
|
+
* nothing outside itself. Without this, a sidebar reading `location.pathname`
|
|
11
|
+
* highlights whatever route the page first loaded on and then never moves
|
|
12
|
+
* again.
|
|
13
|
+
*
|
|
14
|
+
* Both wrappers call through to the original first and return its result, so
|
|
15
|
+
* the patch is transparent to whatever router installed the navigation; it is
|
|
16
|
+
* applied at most once per document, and only when a `ConnectSideNav` is
|
|
17
|
+
* actually mounted without an `activePath`. Pass `activePath` (Next:
|
|
18
|
+
* `usePathname()`) and none of this runs at all - which is the recommended
|
|
19
|
+
* path for any app that has a router to ask.
|
|
20
|
+
*/
|
|
21
|
+
function patchHistory() {
|
|
22
|
+
if (patched || typeof window === "undefined" || typeof window.history === "undefined")
|
|
23
|
+
return;
|
|
24
|
+
patched = true;
|
|
25
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
26
|
+
const original = window.history[method];
|
|
27
|
+
window.history[method] = function patchedHistoryMethod(...args) {
|
|
28
|
+
const result = original.apply(this, args);
|
|
29
|
+
window.dispatchEvent(new Event(LOCATION_CHANGE));
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function subscribe(onStoreChange) {
|
|
35
|
+
patchHistory();
|
|
36
|
+
window.addEventListener("popstate", onStoreChange);
|
|
37
|
+
window.addEventListener(LOCATION_CHANGE, onStoreChange);
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener("popstate", onStoreChange);
|
|
40
|
+
window.removeEventListener(LOCATION_CHANGE, onStoreChange);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function getSnapshot() {
|
|
44
|
+
return window.location.pathname;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The server never has a location, and guessing one is worse than having
|
|
48
|
+
* none: a guess that misses renders a *different* item highlighted on the
|
|
49
|
+
* server than on the client, which is a hydration mismatch. `""` matches no
|
|
50
|
+
* nav item, so the first paint has no highlight and the client's first commit
|
|
51
|
+
* adds it.
|
|
52
|
+
*
|
|
53
|
+
* Doubles as the snapshot when `activePath` *was* supplied - the value is
|
|
54
|
+
* discarded either way, and pairing it with `noopSubscribe` is what keeps a
|
|
55
|
+
* controlled shell from ever touching `history` or `window.location`.
|
|
56
|
+
*/
|
|
57
|
+
function emptySnapshot() {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
function noopSubscribe() {
|
|
61
|
+
return () => { };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The current path, from `activePath` when given and from the browser
|
|
65
|
+
* otherwise.
|
|
66
|
+
*
|
|
67
|
+
* Exported because a host building its own shell out of `ConnectSideNav`
|
|
68
|
+
* needs the same fallback the shell uses.
|
|
69
|
+
*/
|
|
70
|
+
export function useActivePath(activePath) {
|
|
71
|
+
const controlled = activePath !== undefined;
|
|
72
|
+
const live = useSyncExternalStore(controlled ? noopSubscribe : subscribe, controlled ? emptySnapshot : getSnapshot, emptySnapshot);
|
|
73
|
+
return activePath ?? live;
|
|
74
|
+
}
|
package/dist/esm/errors.js
CHANGED
|
@@ -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
|
|
58
|
+
* has to be reissued from the service'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/next/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
1
2
|
import { ConnectClient } from "../server/client.js";
|
|
2
3
|
import { createConnectHandler } from "../server/handler.js";
|
|
3
4
|
/**
|
|
4
5
|
* Plug-and-play wiring for Next.js.
|
|
5
6
|
*
|
|
6
7
|
* Everything below is server-side. Nothing here is *statically* imported from
|
|
7
|
-
* `next
|
|
8
|
-
* and the edge runtime alike
|
|
9
|
-
* that does reach for `next/headers`
|
|
10
|
-
*
|
|
8
|
+
* `next` itself, so this module loads in App Router route handlers, the Node
|
|
9
|
+
* runtime and the edge runtime alike without a `next` peer dependency. The
|
|
10
|
+
* one place that does reach for `next/headers` (or, in `requireConnectSession`,
|
|
11
|
+
* `next/navigation`) does it behind a dynamic import, inside the function that
|
|
12
|
+
* needs it, and says so when it isn't there. `react` is a real, static
|
|
13
|
+
* dependency though - `getConnectSession`'s automatic per-request memoization
|
|
14
|
+
* (below) is built on its `cache()`, which every App Router project already
|
|
15
|
+
* has.
|
|
11
16
|
*
|
|
12
17
|
* The whole integration is two files:
|
|
13
18
|
*
|
|
@@ -33,7 +38,7 @@ import { createConnectHandler } from "../server/handler.js";
|
|
|
33
38
|
* | `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
34
39
|
*
|
|
35
40
|
* Register `{your origin}/api/connect/callback` as a redirect URI in the
|
|
36
|
-
* service's
|
|
41
|
+
* service's Integration tab. That is the only step this cannot do for you.
|
|
37
42
|
*
|
|
38
43
|
* No `next.config.ts` changes needed either, including `transpilePackages`:
|
|
39
44
|
* `dist/` is already ES2022 with `"use client"` hoisted to the top of every
|
|
@@ -198,6 +203,35 @@ async function requestFromHeaders() {
|
|
|
198
203
|
const proto = incoming.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https");
|
|
199
204
|
return new Request(`${proto}://${host}/`, { headers: incoming });
|
|
200
205
|
}
|
|
206
|
+
async function readSession(request, options) {
|
|
207
|
+
const store = options.session ?? cookieSessionStore();
|
|
208
|
+
const token = await store.read(request);
|
|
209
|
+
if (!token)
|
|
210
|
+
return null;
|
|
211
|
+
try {
|
|
212
|
+
return await (options.client ?? connectClient()).identity.getUser(token);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The ambient case - no explicit `request`, default client, default cookie
|
|
220
|
+
* store - wrapped in React's `cache()` so every call in the same request
|
|
221
|
+
* shares one round trip to Connect. A layout reading the session for a nav
|
|
222
|
+
* avatar and the page beneath it reading it again for a role check hit this
|
|
223
|
+
* exact function; without the wrapper that's two live reads of the same
|
|
224
|
+
* answer.
|
|
225
|
+
*
|
|
226
|
+
* Deliberately its own zero-argument function rather than `cache()` wrapping
|
|
227
|
+
* `getConnectSession` directly - `cache()` keys on argument identity, and
|
|
228
|
+
* `getConnectSession`'s default `options = {}` is a fresh object on every
|
|
229
|
+
* call, so wrapping it as-is would never hit the cache for the very call
|
|
230
|
+
* pattern this exists to speed up.
|
|
231
|
+
*/
|
|
232
|
+
const cachedAmbientSession = cache(async () => {
|
|
233
|
+
return readSession(await requestFromHeaders(), {});
|
|
234
|
+
});
|
|
201
235
|
/**
|
|
202
236
|
* The signed-in user for a Server Component, Route Handler or Server Action,
|
|
203
237
|
* or `null`.
|
|
@@ -208,6 +242,9 @@ async function requestFromHeaders() {
|
|
|
208
242
|
* if (!session) redirect("/api/connect/signin?next=/dashboard");
|
|
209
243
|
* ```
|
|
210
244
|
*
|
|
245
|
+
* `requireConnectSession()` below is that exact check in one call, for the
|
|
246
|
+
* common case of "redirect if signed out."
|
|
247
|
+
*
|
|
211
248
|
* Pass a `Request` to use it somewhere `next/headers` does not exist - a
|
|
212
249
|
* framework-agnostic handler, a test, a custom server. Both forms take the
|
|
213
250
|
* same path afterwards; the argument is only how the cookie is found.
|
|
@@ -219,18 +256,61 @@ async function requestFromHeaders() {
|
|
|
219
256
|
*
|
|
220
257
|
* Returns `null` for every failure - an expired session and a missing one both
|
|
221
258
|
* mean "not signed in", and a page guard should not need a try/catch.
|
|
259
|
+
*
|
|
260
|
+
* **Memoized per request for the plain, zero-argument call** - the shape
|
|
261
|
+
* every page, layout and Server Action actually uses - via React's `cache()`.
|
|
262
|
+
* Passing an explicit `request`, `client` or `session` bypasses the cache
|
|
263
|
+
* and always makes a live call: those are for tests and non-ambient
|
|
264
|
+
* callers, where a fresh read every time is the point, not an oversight.
|
|
222
265
|
*/
|
|
223
266
|
export async function getConnectSession(request, options = {}) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
267
|
+
if (!request && !options.client && !options.session)
|
|
268
|
+
return cachedAmbientSession();
|
|
269
|
+
return readSession(request ?? (await requestFromHeaders()), options);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* `getConnectSession()`, minus the boilerplate of checking it and redirecting
|
|
273
|
+
* yourself:
|
|
274
|
+
*
|
|
275
|
+
* ```tsx
|
|
276
|
+
* // app/dashboard/page.tsx
|
|
277
|
+
* const session = await requireConnectSession();
|
|
278
|
+
* ```
|
|
279
|
+
*
|
|
280
|
+
* Redirects to `/signin` under the same `basePath` as `getConnectSession`'s
|
|
281
|
+
* default (`/api/connect`) when signed out - pass `signInPath` if you mounted
|
|
282
|
+
* the bridge somewhere else, and `next` to send the visitor back to where
|
|
283
|
+
* they were once they're signed in. Only sensible in a Server Component,
|
|
284
|
+
* Route Handler or Server Action, the same three places `redirect()` from
|
|
285
|
+
* `next/navigation` itself works.
|
|
286
|
+
*
|
|
287
|
+
* A thin wrapper, not a framework of its own - reach for `getConnectSession`
|
|
288
|
+
* plus your own check when "not signed in" isn't "redirect to sign-in"
|
|
289
|
+
* (an API route returning 401, say). Role and entitlement checks stay
|
|
290
|
+
* outside this function on purpose: `session.serviceRole.granted` and
|
|
291
|
+
* `connect.entitlements.*` are one line each on top of the session this
|
|
292
|
+
* already gives you, and where that line redirects (a plans page, a
|
|
293
|
+
* "forbidden" screen) is a decision only your app can make - see
|
|
294
|
+
* "Authentication & authorization" for the pattern.
|
|
295
|
+
*/
|
|
296
|
+
export async function requireConnectSession(request, options = {}) {
|
|
297
|
+
const session = await getConnectSession(request, options);
|
|
298
|
+
if (session)
|
|
299
|
+
return session;
|
|
300
|
+
const signIn = options.signInPath ?? "/api/connect/signin";
|
|
301
|
+
return redirectTo(options.next ? `${signIn}?next=${encodeURIComponent(options.next)}` : signIn);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* `next/navigation`'s `redirect()`, imported the same dynamic way as
|
|
305
|
+
* `next/headers` above and for the same reason: no static dependency on
|
|
306
|
+
* `next`'s own types at this package's build time. Typed by hand rather than
|
|
307
|
+
* imported, since that's the only way to erase them - `redirect()` itself is
|
|
308
|
+
* typed `never`, which is what lets `requireConnectSession` return here
|
|
309
|
+
* without its own `return` after the call.
|
|
310
|
+
*/
|
|
311
|
+
async function redirectTo(url) {
|
|
312
|
+
const { redirect } = (await import("next/navigation"));
|
|
313
|
+
return redirect(url);
|
|
234
314
|
}
|
|
235
315
|
export { createConnectHandler } from "../server/handler.js";
|
|
236
316
|
export { ConnectClient } from "../server/client.js";
|
|
@@ -5,7 +5,9 @@ import { BillingResource } from "./resources/billing.js";
|
|
|
5
5
|
import { DriveResource } from "./resources/drive.js";
|
|
6
6
|
import { EntitlementsResource } from "./resources/entitlements.js";
|
|
7
7
|
import { IdentityResource } from "./resources/identity.js";
|
|
8
|
+
import { IntegrationsResource } from "./resources/integrations.js";
|
|
8
9
|
import { LogsResource } from "./resources/logs.js";
|
|
10
|
+
import { AnalyticsResource } from "./resources/analytics.js";
|
|
9
11
|
import { OtpResource } from "./resources/otp.js";
|
|
10
12
|
import { SessionsResource } from "./resources/sessions.js";
|
|
11
13
|
export const DEFAULT_BASE_URL = "https://connect.cosmoops.com";
|
|
@@ -76,12 +78,16 @@ export class ConnectClient {
|
|
|
76
78
|
otp;
|
|
77
79
|
/** Feature events into the Console's audit log, filterable to your service. */
|
|
78
80
|
logs;
|
|
81
|
+
/** Emit product events and read back this service's aggregated numbers. */
|
|
82
|
+
analytics;
|
|
79
83
|
/** This Connect account's own login sessions - account-wide, not scoped to this service. Requires the `sessions:manage` scope. */
|
|
80
84
|
sessions;
|
|
85
|
+
/** Composio-hosted third-party connections (Google Calendar, Slack, ...) an organization has connected. */
|
|
86
|
+
integrations;
|
|
81
87
|
constructor(options) {
|
|
82
88
|
assertNotBrowser();
|
|
83
89
|
if (!options.apiKey) {
|
|
84
|
-
throw new TypeError("ConnectClient requires `apiKey` - the plaintext shown once in the
|
|
90
|
+
throw new TypeError("ConnectClient requires `apiKey` - the plaintext shown once in the Integration tab.");
|
|
85
91
|
}
|
|
86
92
|
const appBaseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
87
93
|
const transport = {
|
|
@@ -103,8 +109,10 @@ export class ConnectClient {
|
|
|
103
109
|
this.ai = new AiResource(transport);
|
|
104
110
|
this.otp = new OtpResource(transport);
|
|
105
111
|
this.logs = new LogsResource(transport);
|
|
112
|
+
this.analytics = new AnalyticsResource(transport);
|
|
106
113
|
this.sessions = new SessionsResource(transport, options.clientId ?? null);
|
|
114
|
+
this.integrations = new IntegrationsResource(transport);
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
117
|
/** Kept in step with `package.json` by `sdk.test.ts`. */
|
|
110
|
-
export const SDK_VERSION = "0.
|
|
118
|
+
export const SDK_VERSION = "0.6.0";
|