@cosmoops/connect-sdk 0.3.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/README.md +215 -6
  3. package/dist/cjs/cli/doctor.js +3 -3
  4. package/dist/cjs/cli/init.js +3 -3
  5. package/dist/cjs/client/components/access-gate.js +162 -0
  6. package/dist/cjs/client/components/account-sessions.js +21 -19
  7. package/dist/cjs/client/components/app-shell.js +157 -0
  8. package/dist/cjs/client/components/apps-grid.js +2 -1
  9. package/dist/cjs/client/components/integrations-modal.js +95 -0
  10. package/dist/cjs/client/components/motion.js +27 -0
  11. package/dist/cjs/client/components/nav.js +97 -0
  12. package/dist/cjs/client/components/plans.js +39 -37
  13. package/dist/cjs/client/components/readiness.js +3 -1
  14. package/dist/cjs/client/components/side-nav.js +58 -0
  15. package/dist/cjs/client/components/subscription.js +2 -1
  16. package/dist/cjs/client/components/surfaces.js +54 -0
  17. package/dist/cjs/client/components/usage-meter.js +3 -2
  18. package/dist/cjs/client/components/user-menu.js +30 -3
  19. package/dist/cjs/client/index.js +29 -1
  20. package/dist/cjs/client/provider.js +54 -2
  21. package/dist/cjs/client/use-active-path.js +77 -0
  22. package/dist/cjs/errors.js +1 -1
  23. package/dist/cjs/next/index.js +96 -15
  24. package/dist/cjs/server/client.js +10 -2
  25. package/dist/cjs/server/handler.js +89 -6
  26. package/dist/cjs/server/index.js +8 -1
  27. package/dist/cjs/server/resources/analytics.js +66 -0
  28. package/dist/cjs/server/resources/identity.js +1 -1
  29. package/dist/cjs/server/resources/integrations.js +66 -0
  30. package/dist/cjs/server/resources/sessions.js +1 -1
  31. package/dist/cjs/server/webhooks.js +127 -0
  32. package/dist/esm/cli/doctor.js +3 -3
  33. package/dist/esm/cli/init.js +3 -3
  34. package/dist/esm/client/components/access-gate.js +158 -0
  35. package/dist/esm/client/components/account-sessions.js +21 -19
  36. package/dist/esm/client/components/app-shell.js +154 -0
  37. package/dist/esm/client/components/apps-grid.js +2 -1
  38. package/dist/esm/client/components/integrations-modal.js +92 -0
  39. package/dist/esm/client/components/motion.js +21 -0
  40. package/dist/esm/client/components/nav.js +89 -0
  41. package/dist/esm/client/components/plans.js +39 -37
  42. package/dist/esm/client/components/readiness.js +3 -1
  43. package/dist/esm/client/components/side-nav.js +53 -0
  44. package/dist/esm/client/components/subscription.js +2 -1
  45. package/dist/esm/client/components/surfaces.js +51 -0
  46. package/dist/esm/client/components/usage-meter.js +3 -2
  47. package/dist/esm/client/components/user-menu.js +31 -4
  48. package/dist/esm/client/index.js +15 -1
  49. package/dist/esm/client/provider.js +53 -2
  50. package/dist/esm/client/use-active-path.js +74 -0
  51. package/dist/esm/errors.js +1 -1
  52. package/dist/esm/next/index.js +95 -15
  53. package/dist/esm/server/client.js +10 -2
  54. package/dist/esm/server/handler.js +89 -6
  55. package/dist/esm/server/index.js +5 -0
  56. package/dist/esm/server/resources/analytics.js +62 -0
  57. package/dist/esm/server/resources/identity.js +1 -1
  58. package/dist/esm/server/resources/integrations.js +62 -0
  59. package/dist/esm/server/resources/sessions.js +1 -1
  60. package/dist/esm/server/webhooks.js +123 -0
  61. package/dist/theme.css +28 -0
  62. package/dist/types/client/components/access-gate.d.ts +79 -0
  63. package/dist/types/client/components/app-shell.d.ts +115 -0
  64. package/dist/types/client/components/integrations-modal.d.ts +21 -0
  65. package/dist/types/client/components/motion.d.ts +22 -0
  66. package/dist/types/client/components/nav.d.ts +110 -0
  67. package/dist/types/client/components/side-nav.d.ts +31 -0
  68. package/dist/types/client/components/surfaces.d.ts +51 -0
  69. package/dist/types/client/index.d.ts +12 -1
  70. package/dist/types/client/provider.d.ts +38 -3
  71. package/dist/types/client/use-active-path.d.ts +8 -0
  72. package/dist/types/errors.d.ts +1 -1
  73. package/dist/types/next/index.d.ts +40 -0
  74. package/dist/types/server/client.d.ts +8 -2
  75. package/dist/types/server/index.d.ts +7 -0
  76. package/dist/types/server/resources/analytics.d.ts +31 -0
  77. package/dist/types/server/resources/identity.d.ts +12 -2
  78. package/dist/types/server/resources/integrations.d.ts +40 -0
  79. package/dist/types/server/webhooks.d.ts +67 -0
  80. package/dist/types/types.d.ts +182 -1
  81. package/package.json +10 -4
@@ -0,0 +1,62 @@
1
+ import { request } from "../http.js";
2
+ /**
3
+ * Composio-hosted third-party connections (Google Calendar, Slack,
4
+ * Twitter/X, ...) an organization has connected. Connect proxies every call
5
+ * here - your service never sees or holds the underlying OAuth token, which
6
+ * stays with Composio.
7
+ */
8
+ export class IntegrationsResource {
9
+ transport;
10
+ constructor(transport) {
11
+ this.transport = transport;
12
+ }
13
+ /** Enabled toolkits and this organization's connection status for each. */
14
+ async list(organizationId) {
15
+ const result = await request(this.transport, {
16
+ method: "GET",
17
+ path: "/integrations",
18
+ query: { organizationId },
19
+ });
20
+ // `?? []` for the same reason `AppsResource.list` has it - a collection
21
+ // every caller immediately maps or measures.
22
+ return result.toolkits ?? [];
23
+ }
24
+ /**
25
+ * Starts a Composio-hosted connection for one toolkit. The returned
26
+ * `redirectUrl` is where the browser completes the provider's OAuth
27
+ * screen, then Composio bounces it back to `returnUrl`.
28
+ *
29
+ * **The connection is not active when this returns.** Treat the toolkit
30
+ * as `pending` until a later `list()` call reports `active` - only
31
+ * Connect's own webhook from Composio confirms that, never the redirect
32
+ * itself. Reconnecting a disconnected/failed toolkit calls this same
33
+ * method - there is no separate reconnect.
34
+ *
35
+ * Meant to be called on behalf of a signed-in org admin (this is what the
36
+ * bridge's fixed `POST /integrations/connect` route does), not as a bare
37
+ * background action with no admin present to complete the OAuth screen.
38
+ */
39
+ async initiateConnect(organizationId, toolkitSlug, returnUrl) {
40
+ return request(this.transport, {
41
+ method: "POST",
42
+ path: "/integrations/connect",
43
+ json: { organizationId, toolkitSlug, returnUrl },
44
+ });
45
+ }
46
+ /** Idempotent - disconnecting a toolkit that was never connected, or is already disconnected, is a success. */
47
+ async disconnect(organizationId, toolkitSlug) {
48
+ return request(this.transport, {
49
+ method: "POST",
50
+ path: "/integrations/disconnect",
51
+ json: { organizationId, toolkitSlug },
52
+ });
53
+ }
54
+ /** Calls a Composio action against this organization's connected account for the named toolkit. */
55
+ async execute(organizationId, input) {
56
+ return request(this.transport, {
57
+ method: "POST",
58
+ path: "/integrations/actions/execute",
59
+ json: { organizationId, ...input },
60
+ });
61
+ }
62
+ }
@@ -69,7 +69,7 @@ export class SessionsResource {
69
69
  throw new ConnectError({
70
70
  status: 0,
71
71
  code: "missing_client_id",
72
- message: "sessions.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this service's Setup tab once SSO is enabled.",
72
+ message: "sessions.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this service's Integration tab, under Connect client.",
73
73
  endpoint: "sessions",
74
74
  });
75
75
  }
@@ -0,0 +1,123 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ /**
3
+ * How far out of date a timestamp may be. Five minutes, the same value
4
+ * Connect signs against and the same one Stripe uses - so a backend that
5
+ * already verifies Stripe webhooks needs no new assumptions.
6
+ */
7
+ const TIMESTAMP_TOLERANCE_SECONDS = 300;
8
+ const SIGNATURE_HEADER = "connect-signature";
9
+ /**
10
+ * Verifies a `Connect-Signature` header against the raw request body.
11
+ *
12
+ * Exported on its own for anyone wiring this into a framework
13
+ * `createConnectWebhookHandler` does not cover. The **raw** body matters:
14
+ * verify before parsing, and never re-serialise - `JSON.stringify(await
15
+ * req.json())` produces a different string whenever key order or number
16
+ * formatting differs, and the signature will not match.
17
+ *
18
+ * Returns a boolean and never explains which check failed. Reporting "bad
19
+ * timestamp" separately from "bad signature" tells whoever is probing which
20
+ * half of a forgery to keep working on.
21
+ */
22
+ export function verifyConnectWebhook(rawBody, signatureHeader, secret, now = new Date()) {
23
+ if (!signatureHeader)
24
+ return false;
25
+ const parts = new Map(signatureHeader
26
+ .split(",")
27
+ .map((part) => part.trim().split("="))
28
+ .filter((pair) => pair.length === 2)
29
+ .map(([key, value]) => [key, value]));
30
+ const timestamp = Number(parts.get("t"));
31
+ const presented = parts.get("v1");
32
+ if (!presented || !Number.isFinite(timestamp))
33
+ return false;
34
+ // Absolute difference: a timestamp from the future is as much a sign of a
35
+ // forgery as a stale one, and accepting it would let somebody mint a
36
+ // signature that stays valid indefinitely.
37
+ const nowSeconds = Math.floor(now.getTime() / 1000);
38
+ if (Math.abs(nowSeconds - timestamp) > TIMESTAMP_TOLERANCE_SECONDS)
39
+ return false;
40
+ const expected = createHmac("sha256", secret)
41
+ .update(`${timestamp}.${rawBody}`)
42
+ .digest("hex");
43
+ // `timingSafeEqual` throws on a length mismatch, and a wrong-length hex
44
+ // string is exactly what a probe looks like - so lengths are compared
45
+ // first and only equal-length buffers reach it.
46
+ if (presented.length !== expected.length)
47
+ return false;
48
+ return timingSafeEqual(Buffer.from(presented, "utf8"), Buffer.from(expected, "utf8"));
49
+ }
50
+ /**
51
+ * A ready-made `POST` route for Connect's webhooks.
52
+ *
53
+ * ```ts
54
+ * export const POST = createConnectWebhookHandler({
55
+ * secret: process.env.CONNECT_WEBHOOK_SECRET!,
56
+ * on: {
57
+ * "organization.deletion_scheduled": ({ organizationId, purgeAfter }) =>
58
+ * freeze(organizationId, purgeAfter),
59
+ * "organization.deletion_cancelled": ({ organizationId }) => unfreeze(organizationId),
60
+ * "organization.deleted": ({ organizationId }) => purge(organizationId),
61
+ * },
62
+ * })
63
+ * ```
64
+ *
65
+ * Three behaviours worth knowing, because each of them is a decision rather
66
+ * than an accident:
67
+ *
68
+ * **An unverified request gets a 401 and nothing else happens.** No parsing,
69
+ * no logging of the body, no handler. The endpoint is a public URL, so
70
+ * anything reachable before the signature check is reachable by anyone.
71
+ *
72
+ * **An unhandled event type is a 200.** Connect adds event types over time,
73
+ * and answering 4xx to one you have not written code for would dead-letter
74
+ * it immediately and, after enough of them, disable your endpoint. Silence
75
+ * is the correct answer to "we do not care about this one".
76
+ *
77
+ * **A throwing handler produces a 500, on purpose.** That is what makes
78
+ * Connect retry. Swallowing the error would acknowledge an event you failed
79
+ * to process, and the retry ladder is the only thing standing between a
80
+ * transient failure and a permanently missed deletion.
81
+ *
82
+ * Delivery is at-least-once and unordered. Dedupe on `envelope.id`, and
83
+ * reconcile against the readiness endpoint rather than trusting arrival
84
+ * order - the pull channel is authoritative, this one is fast.
85
+ */
86
+ export function createConnectWebhookHandler(options) {
87
+ return async function POST(request) {
88
+ // Read as text and verify before parsing. `JSON.stringify(await
89
+ // request.json())` is not the same string that was signed - key order and
90
+ // number formatting both differ - so a handler that parses first can
91
+ // never verify.
92
+ const rawBody = await request.text();
93
+ const signature = request.headers.get(SIGNATURE_HEADER);
94
+ if (!verifyConnectWebhook(rawBody, signature, options.secret)) {
95
+ return new Response("Invalid signature", { status: 401 });
96
+ }
97
+ let envelope;
98
+ try {
99
+ envelope = JSON.parse(rawBody);
100
+ }
101
+ catch {
102
+ // Signed by us and still unparseable is not something a retry fixes.
103
+ return new Response("Malformed payload", { status: 400 });
104
+ }
105
+ const handler = options.on[envelope.type];
106
+ if (!handler)
107
+ return new Response(null, { status: 200 });
108
+ try {
109
+ await handler(envelope.data, envelope);
110
+ }
111
+ catch (error) {
112
+ if (options.onError) {
113
+ await options.onError(error, envelope);
114
+ }
115
+ else {
116
+ console.error(`Connect webhook ${envelope.type} (${envelope.id}) failed:`, error);
117
+ }
118
+ // Deliberately a 500: it is what makes Connect retry. See the doc above.
119
+ return new Response("Handler failed", { status: 500 });
120
+ }
121
+ return new Response(null, { status: 200 });
122
+ };
123
+ }
package/dist/theme.css CHANGED
@@ -50,6 +50,31 @@
50
50
  --separator: oklch(92% 0.0067 145.67);
51
51
  --surface: oklch(100% 0.0034 145.67);
52
52
  --surface-secondary: oklch(94% 0.0067 145.67);
53
+ /*
54
+ * The neutral ramp every `secondary`/`tertiary` Button and every default
55
+ * Chip actually paints with - `.button--tertiary` resolves to
56
+ * `--button-bg: var(--default)`, not to `--surface`.
57
+ *
58
+ * Defining these is the difference between "the palette below is the
59
+ * palette" and it merely being most of it. Left out, `--default` and
60
+ * `--surface-tertiary` keep @heroui/styles' stock values, which sit at
61
+ * hue 286 - a cool blue-grey - while every token below is hue 145.67, a
62
+ * warm green-grey. The two ramps are near-neutral enough that neither
63
+ * reads as "blue" or "green" on its own, and precisely similar enough
64
+ * that a grey button next to a grey panel reads as an accident. In dark
65
+ * mode they also disagreed on lightness (27.4% against a 25% panel), so
66
+ * a tertiary button sat a step *above* the panel containing it.
67
+ *
68
+ * Lightness is kept at HeroUI's stock values so component relationships
69
+ * are unchanged; only the hue is brought onto the ramp.
70
+ *
71
+ * Everything ending in `-soft`, plus `--default-hover`/`--default-foreground`,
72
+ * is derived by @heroui/styles via `color-mix` from the base token, so
73
+ * those follow from these and must NOT be redefined here - hardcoding one
74
+ * pins it and stops it tracking a partner's override.
75
+ */
76
+ --default: oklch(94% 0.0067 145.67);
77
+ --surface-tertiary: oklch(93.73% 0.0067 145.67);
53
78
  --danger: oklch(65.32% 0.2359 12.76);
54
79
  --danger-foreground: oklch(99.11% 0 0);
55
80
  --warning: oklch(79% 0.16 76);
@@ -70,6 +95,9 @@
70
95
  --separator: oklch(26% 0.0067 145.67);
71
96
  --surface: oklch(21% 0.0067 145.67);
72
97
  --surface-secondary: oklch(25% 0.0067 145.67);
98
+ /* See the light block above. */
99
+ --default: oklch(27.4% 0.0067 145.67);
100
+ --surface-tertiary: oklch(27.21% 0.0067 145.67);
73
101
  --danger: oklch(65.32% 0.2359 12.76);
74
102
  --danger-foreground: oklch(99.11% 0 0);
75
103
  --warning: oklch(79% 0.16 76);
@@ -0,0 +1,79 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ReadinessBlocker } from "../../types.js";
3
+ export type ConnectAccessGateState = "loading"
4
+ /** No signed-in user - not this component's concern, see the doc comment on `ConnectAccessGate`. */
5
+ | "signed_out"
6
+ /** `organization_suspended` - a platform admin took this workspace offline. Hard block, no dismiss. */
7
+ | "suspended"
8
+ /** `subscription_missing` - nothing to subscribe to yet. Hard block. */
9
+ | "no_subscription"
10
+ /** `onboarding_approval_pending` - waiting on a reviewer. Hard block, no action to offer. */
11
+ | "onboarding_pending"
12
+ /** Any other `blocking` readiness code (verification, audience, seats, ...). Hard block. */
13
+ | "blocked"
14
+ /** `payment_retry_pending` - an automatic dunning retry is scheduled. Not blocking - `children` still render. */
15
+ | "payment_retry"
16
+ /** Nothing outstanding. */
17
+ | "clear";
18
+ export interface ConnectAccessGateResult {
19
+ state: ConnectAccessGateState;
20
+ /** The blocker driving a hard-block state (`suspended`/`no_subscription`/`onboarding_pending`/`blocked`). `null` otherwise. */
21
+ blocker: ReadinessBlocker | null;
22
+ /** The `payment_retry_pending` blocker, only when `state === "payment_retry"`. Carries `graceEndsAt`. */
23
+ paymentRetry: ReadinessBlocker | null;
24
+ }
25
+ /**
26
+ * The single composed answer to "can this workspace use my app right now,
27
+ * and if not, which of the four things is it": choose a plan, wait on
28
+ * approval, resolve an overdue payment, or - a suspended workspace - none of
29
+ * the above, because nothing short of a platform admin fixes that one.
30
+ *
31
+ * Reuses the existing single sources of truth rather than re-deriving them:
32
+ * `blockingBlockers()` (`helpers.ts`) for severity, the same readiness read
33
+ * `ConnectReadinessGate`/`ConnectReadinessBanner` already use. `ConnectAccessGate`
34
+ * is a priority ordering over the same data, not a second opinion about it.
35
+ *
36
+ * Priority, first match wins: `suspended` > `no_subscription` >
37
+ * `onboarding_pending` > any other blocking code (`blocked`) >
38
+ * `payment_retry` (advisory - `children` still render) > `clear`.
39
+ */
40
+ export declare function useConnectAccessGate(): ConnectAccessGateResult;
41
+ export interface ConnectAccessGateProps {
42
+ children: ReactNode;
43
+ /** Replaces the default card for every hard-block state (`suspended`/`no_subscription`/`onboarding_pending`/`blocked`) uniformly. For a different card per state, call `useConnectAccessGate()` directly instead. */
44
+ fallback?: ReactNode;
45
+ /** Shown while the first read is in flight. */
46
+ loading?: ReactNode;
47
+ className?: string;
48
+ }
49
+ /**
50
+ * Wrap your app's content once - typically in your root layout, alongside
51
+ * (not instead of) your own header/nav - and it renders exactly one of: the
52
+ * hard-block card for a suspended workspace, one with no plan, or one
53
+ * waiting on approval; your `children` plus a dismissible payment-due banner
54
+ * during an automatic dunning retry; or just `children` once everything is
55
+ * clear.
56
+ *
57
+ * **Do not wrap your own plans/pricing page with this.** The `no_subscription`
58
+ * card's button sends someone to Connect's subscribe page directly (see
59
+ * `GateCard`'s doc comment on why - it used to link to your in-app `/plans`
60
+ * instead, which is a dead end once this gate hides that very page too), but
61
+ * a customer can still navigate to your plans page on their own (a nav link,
62
+ * a bookmark) and must actually see it there. Since the gate is mounted once
63
+ * around every route, carving out that exception is your router's job, not
64
+ * a prop on this component: check the current path in your layout/shell and
65
+ * skip wrapping `children` in `<ConnectAccessGate>` for that one route.
66
+ *
67
+ * **Signed-out renders `children`, not a fallback** - deliberately different
68
+ * from `ConnectReadinessGate`/`ConnectSubscriptionGate`, which show nothing
69
+ * until signed in. None of the four states this component composes is a
70
+ * property of a signed-out visitor, and a component mounted around your
71
+ * entire app must not hide a public landing page behind an empty gate. Your
72
+ * own sign-in flow decides what a signed-out visitor sees.
73
+ *
74
+ * **This is a UX gate, not a security boundary** - same caveat as every
75
+ * other gate in this package. The authoritative check is
76
+ * `client.entitlements.getReadiness` on your server, before you do the
77
+ * thing.
78
+ */
79
+ export declare function ConnectAccessGate({ children, fallback, loading, className }: ConnectAccessGateProps): import("react").JSX.Element;
@@ -0,0 +1,115 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ConnectLinkRenderer, type ConnectNav } from "./nav.js";
3
+ import type { ThemeMode } from "../../types.js";
4
+ export interface ConnectAppShellProps {
5
+ /** This service's name, in the `cosmo·ops VOICE` lockup. See `ConnectLogo`. */
6
+ name: string;
7
+ /** This service's glyph, drawn on the CosmoOps badge. Falls back to the Connect rings. */
8
+ icon?: ReactNode;
9
+ /** Where the lockup links. Defaults to `/`. */
10
+ homeHref?: string;
11
+ /** The sidebar's destinations - items, sections, or a mix. See `ConnectNav`. */
12
+ nav: ConnectNav;
13
+ /** Current path. Pass `usePathname()` in Next; see `ConnectSideNavProps.activePath`. */
14
+ activePath?: string;
15
+ /** See `ConnectLinkRenderer` - how this app renders a link. Defaults to a plain `<a>`. */
16
+ renderLink?: ConnectLinkRenderer;
17
+ /**
18
+ * Page heading in the top bar. A string renders as the page's `<h1>`, so
19
+ * don't also put one at the top of the page; pass a node for breadcrumbs or
20
+ * anything else.
21
+ */
22
+ title?: ReactNode;
23
+ /** Your own controls in the top bar, left of the Connect ones. */
24
+ headerActions?: ReactNode;
25
+ /** Extra content pinned to the bottom of the sidebar, above the fold-away. */
26
+ sidebarFooter?: ReactNode;
27
+ /** Rendered below `children`, inside the scrolling content pane. */
28
+ footer?: ReactNode;
29
+ /** Start the rail collapsed. Ignored when `collapsed` is supplied. */
30
+ defaultCollapsed?: boolean;
31
+ /** Drive the rail yourself. Supply `onCollapsedChange` with it. */
32
+ collapsed?: boolean;
33
+ onCollapsedChange?: (collapsed: boolean) => void;
34
+ /** Remove the collapse control and pin the rail expanded. */
35
+ collapsible?: boolean;
36
+ /** The app launcher in the top bar. On by default. */
37
+ showAppsGrid?: boolean;
38
+ /** "Continue with Connect" / "Create account" in the top bar when signed out. On by default. */
39
+ showAuthButtons?: boolean;
40
+ /** Passed to `ConnectUserMenu` - the SDK has no theme provider, so this is host-controlled. */
41
+ theme?: ThemeMode;
42
+ onThemeChange?: (theme: ThemeMode) => void;
43
+ /**
44
+ * Wrap `children` in `ConnectAccessGate` - suspended, no plan, onboarding
45
+ * pending, payment due, decided once for the whole app. **On by default**,
46
+ * because the shell is exactly the "wrap your app content once" place that
47
+ * gate asks for, and an app that mounts a shell and forgets the gate is an
48
+ * app that lets a suspended workspace keep working.
49
+ *
50
+ * The chrome is never gated: the sidebar, the lockup, the user menu and
51
+ * sign-out stay reachable in every blocked state, so a blocked customer can
52
+ * always get to the page that unblocks them.
53
+ *
54
+ * Routes flagged `gateExempt` on their nav item render outside it - see
55
+ * `ConnectNavItem.gateExempt` and `gateExemptRoutes`.
56
+ */
57
+ gate?: boolean;
58
+ /**
59
+ * Extra ungated paths, on top of the ones flagged `gateExempt` in `nav`.
60
+ * For a route that is not in the sidebar at all - a `/checkout/callback`,
61
+ * say. Prefix-matched, same as the nav's active resolution.
62
+ */
63
+ gateExemptRoutes?: string[];
64
+ className?: string;
65
+ /** Classes on the scrolling content pane - override the default max-width and padding here. */
66
+ contentClassName?: string;
67
+ children: ReactNode;
68
+ }
69
+ /**
70
+ * The application frame every CosmoOps service platform runs inside: a
71
+ * collapsible sidebar, a top bar carrying the app launcher and the account
72
+ * menu, and your pages in the middle.
73
+ *
74
+ * This is the piece each partner was otherwise writing by hand, and writing
75
+ * slightly differently - which is how a family of services stops looking like
76
+ * one product. Everything that should be identical across platforms (the
77
+ * lockup, the rail geometry, the active-route treatment, the launcher, the
78
+ * account menu, the access gate) lives here; everything that genuinely
79
+ * differs is a prop:
80
+ *
81
+ * ```tsx
82
+ * "use client";
83
+ * import Link from "next/link";
84
+ * import { usePathname } from "next/navigation";
85
+ * import { ConnectAppShell } from "@cosmoops/connect-sdk/client";
86
+ * import { LayoutDashboard, Truck } from "lucide-react";
87
+ *
88
+ * export function Shell({ children }: { children: React.ReactNode }) {
89
+ * return (
90
+ * <ConnectAppShell
91
+ * name="Dispatch"
92
+ * icon={<Truck />}
93
+ * activePath={usePathname()}
94
+ * renderLink={(props) => <Link {...props} />}
95
+ * nav={[
96
+ * { label: "Overview", href: "/", icon: <LayoutDashboard className="size-5" /> },
97
+ * { label: "Plans", href: "/plans", icon: <Truck className="size-5" />, gateExempt: true },
98
+ * ]}
99
+ * >
100
+ * {children}
101
+ * </ConnectAppShell>
102
+ * );
103
+ * }
104
+ * ```
105
+ *
106
+ * There is no `isSignedIn` branch anywhere in here, and there should be none
107
+ * in yours: the auth buttons render nothing once a session exists, the user
108
+ * menu renders nothing without one, and the launcher renders nothing when
109
+ * signed out. The swap happens inside the components.
110
+ *
111
+ * It renders no provider of its own - mount `ConnectProvider` above it, once,
112
+ * so a page with a shell, a plan table and a usage meter still makes one
113
+ * session read.
114
+ */
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;
@@ -0,0 +1,21 @@
1
+ export interface ConnectIntegrationsModalProps {
2
+ /** Extra classes on the trigger button. */
3
+ className?: string;
4
+ /** Trigger label, both visible and for screen readers. */
5
+ label?: string;
6
+ }
7
+ /**
8
+ * A grid of every Composio-hosted toolkit this organization can connect -
9
+ * Google Calendar, Slack, Twitter/X, whatever a platform admin enabled in
10
+ * Connect's own Console. Each tile carries its own connect/reconnect/
11
+ * disconnect action and its own pending state, independent of every other
12
+ * tile - see the doc comment on `pendingSlug` below for why that matters.
13
+ *
14
+ * Not optimistic: a tile stays whatever it already was after "Connect" is
15
+ * pressed, because the connection isn't settled until Composio's webhook
16
+ * confirms it - `mutateResource` here only ever gets as far as `pending`
17
+ * before the browser navigates away to complete the provider's OAuth screen.
18
+ *
19
+ * Renders nothing when signed out, same as `ConnectAppsGrid`.
20
+ */
21
+ export declare function ConnectIntegrationsModal({ className, label }: ConnectIntegrationsModalProps): import("react").JSX.Element | null;
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ export { m, AnimatePresence } from "motion/react";
3
+ /** HeroUI's --ease-out-quart, transcribed for motion transitions. */
4
+ export declare const EASE_OUT_QUART: readonly [0.165, 0.84, 0.44, 1];
5
+ export declare const ENTER: {
6
+ readonly duration: 0.3;
7
+ readonly ease: readonly [0.165, 0.84, 0.44, 1];
8
+ };
9
+ export declare const METER_SPRING: {
10
+ readonly type: "spring";
11
+ readonly stiffness: 170;
12
+ readonly damping: 26;
13
+ };
14
+ /**
15
+ * Per-component motion root. domAnimation keeps the feature bundle small,
16
+ * `strict` restricts rendering to `m.*` components, and reducedMotion="user"
17
+ * drops transform animation (keeping opacity) under prefers-reduced-motion -
18
+ * the same contract HeroUI's own `motion-reduce` styles follow.
19
+ */
20
+ export declare function MotionProvider({ children }: Readonly<{
21
+ children: ReactNode;
22
+ }>): import("react").JSX.Element;
@@ -0,0 +1,110 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * One destination in the shell's sidebar.
4
+ *
5
+ * `href` is a path in *your* app, not a Connect URL - the shell never
6
+ * routes anywhere itself, it hands `href` to `renderLink` and lets your
7
+ * router do the work.
8
+ */
9
+ export interface ConnectNavItem {
10
+ label: string;
11
+ href: string;
12
+ /**
13
+ * A pre-rendered glyph, e.g. `<LayoutDashboard className="size-5" />`.
14
+ * Required in practice for anything in the primary rail: the collapsed rail
15
+ * is icon-only, and an item with no icon collapses to an empty box.
16
+ */
17
+ icon?: ReactNode;
18
+ /** Trailing content on the row - a count `<Chip>`, a status dot. Hidden while collapsed. */
19
+ badge?: ReactNode;
20
+ /**
21
+ * Match this route only when the path is exactly `href`, never as a prefix.
22
+ * `/` is always treated as exact regardless, since every path starts with it.
23
+ */
24
+ exact?: boolean;
25
+ /** Opens in a new tab, with a trailing arrow. For docs, status pages, anything outside the app. */
26
+ external?: boolean;
27
+ /** One level of children, revealed beneath the parent when the parent or a child is active. */
28
+ items?: ConnectNavItem[];
29
+ /**
30
+ * Leave this route *outside* `ConnectAccessGate` - see
31
+ * `ConnectAppShellProps.gate`. Declared here, next to the route it applies
32
+ * to, rather than as a second list somewhere else that has to be kept in
33
+ * step with this one.
34
+ */
35
+ gateExempt?: boolean;
36
+ }
37
+ /** A titled group of items. Untitled groups render as a plain divider-free block. */
38
+ export interface ConnectNavSection {
39
+ title?: string;
40
+ items: ConnectNavItem[];
41
+ }
42
+ /**
43
+ * What `ConnectAppShell`/`ConnectSideNav` accept for `nav`: a flat list of
44
+ * items, a list of sections, or a mix. Consecutive bare items fold into one
45
+ * untitled section, so the common case is just an array of items.
46
+ */
47
+ export type ConnectNav = ReadonlyArray<ConnectNavItem | ConnectNavSection>;
48
+ /** Props the shell hands to `renderLink`. Spread them onto whatever your router's link component is. */
49
+ export interface ConnectNavLinkProps {
50
+ href: string;
51
+ className: string;
52
+ children: ReactNode;
53
+ onClick?: () => void;
54
+ "aria-current"?: "page";
55
+ /**
56
+ * Always set, and the only name the link has while the rail is collapsed -
57
+ * spread it through rather than dropping it, or an icon-only rail reads as
58
+ * a column of unlabelled links.
59
+ */
60
+ "aria-label": string;
61
+ target?: string;
62
+ rel?: string;
63
+ }
64
+ /**
65
+ * How this app renders a link.
66
+ *
67
+ * The SDK ships into any React app and cannot import `next/link`,
68
+ * `react-router` or anything else framework-specific, so routing is a prop.
69
+ * The default is a plain `<a>`, which is correct but costs a full page load;
70
+ * an app with a client router should pass its own:
71
+ *
72
+ * ```tsx
73
+ * renderLink={(props) => <Link {...props} />} // next/link
74
+ * ```
75
+ */
76
+ export type ConnectLinkRenderer = (props: ConnectNavLinkProps) => ReactNode;
77
+ /** Folds a mixed `ConnectNav` into sections, gathering consecutive bare items into one untitled group. */
78
+ export declare function normalizeNav(nav: ConnectNav): ConnectNavSection[];
79
+ /** Every item in the tree, parents and children alike, in render order. */
80
+ export declare function flattenNav(nav: ConnectNav): ConnectNavItem[];
81
+ /**
82
+ * `https://x.test/billing/?tab=1#top` -> `/billing`.
83
+ *
84
+ * Trailing slashes go too, so `/billing/` and `/billing` are the same route -
85
+ * they are, to every router, and treating them as different is how a nav item
86
+ * ends up unhighlighted on exactly one of the two URLs a user can arrive at.
87
+ */
88
+ export declare function normalizePath(path: string): string;
89
+ /**
90
+ * The href of the nav item the current path belongs to, or `null`.
91
+ *
92
+ * Longest prefix wins, so `/settings/billing` lights up Billing and not also
93
+ * its parent Settings - a plain `startsWith` over an unordered list lights up
94
+ * both, and the sidebar grows two highlights.
95
+ *
96
+ * External items never match: they leave the app, so no path inside it is
97
+ * ever "on" them.
98
+ */
99
+ export declare function resolveActivePath(pathname: string, nav: ConnectNav): string | null;
100
+ /**
101
+ * Whether `item` is the destination `resolveActivePath` picked.
102
+ *
103
+ * Not just `normalizePath(item.href) === activeHref`, which is the trap this
104
+ * exists to close: `normalizePath` strips the origin off an absolute URL, so
105
+ * an external item pointed at `https://docs.example.com` normalizes to `/` and
106
+ * lights up on every app's home route. External items are never active.
107
+ */
108
+ export declare function isNavItemActive(item: ConnectNavItem, activeHref: string | null): boolean;
109
+ /** Hrefs declared `gateExempt` - the routes `ConnectAppShell` renders outside its `ConnectAccessGate`. */
110
+ export declare function gateExemptPaths(nav: ConnectNav): string[];
@@ -0,0 +1,31 @@
1
+ import { type ConnectLinkRenderer, type ConnectNav } from "./nav.js";
2
+ /** A plain anchor - correct anywhere, and a full page load. See `ConnectLinkRenderer`. */
3
+ export declare const defaultRenderLink: ConnectLinkRenderer;
4
+ export interface ConnectSideNavProps {
5
+ /** Items, sections, or a mix - see `ConnectNav`. */
6
+ nav: ConnectNav;
7
+ /**
8
+ * The current path. Pass your router's own (`usePathname()` in Next) -
9
+ * without it the nav reads `window.location` and patches `history` to hear
10
+ * about client-side navigations, which works but is guesswork the router
11
+ * never has to do. See `useActivePath`.
12
+ */
13
+ activePath?: string;
14
+ /** See `ConnectLinkRenderer`. Defaults to a plain `<a>`. */
15
+ renderLink?: ConnectLinkRenderer;
16
+ /** Icon-only rail. Labels move into tooltips and children are hidden. */
17
+ collapsed?: boolean;
18
+ /** Called after any nav link is pressed - the mobile drawer closes itself with this. */
19
+ onNavigate?: () => void;
20
+ className?: string;
21
+ }
22
+ /**
23
+ * The sidebar's nav column on its own - sections, items, one level of
24
+ * children, and the active highlight.
25
+ *
26
+ * `ConnectAppShell` renders this inside its rail and its mobile drawer, and it
27
+ * is exported for a host that wants the shell's navigation without its
28
+ * chrome: the same items, the same active resolution, dropped into a layout
29
+ * you already have.
30
+ */
31
+ export declare function ConnectSideNav({ nav, activePath, renderLink, collapsed, onNavigate, className, }: ConnectSideNavProps): import("react").JSX.Element;