@cosmoops/connect-sdk 0.3.0 → 0.7.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 (137) 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 +24 -22
  7. package/dist/cjs/client/components/app-shell.js +157 -0
  8. package/dist/cjs/client/components/apps-grid.js +4 -3
  9. package/dist/cjs/client/components/feature-gate.js +2 -2
  10. package/dist/cjs/client/components/integrations-modal.js +95 -0
  11. package/dist/cjs/client/components/logo.js +3 -3
  12. package/dist/cjs/client/components/motion.js +27 -0
  13. package/dist/cjs/client/components/nav.js +97 -0
  14. package/dist/cjs/client/components/plan-pricing.js +1 -1
  15. package/dist/cjs/client/components/plans.js +42 -40
  16. package/dist/cjs/client/components/readiness.js +6 -4
  17. package/dist/cjs/client/components/side-nav.js +58 -0
  18. package/dist/cjs/client/components/subscribe-dialog.js +2 -2
  19. package/dist/cjs/client/components/subscription.js +3 -2
  20. package/dist/cjs/client/components/surfaces.js +54 -0
  21. package/dist/cjs/client/components/usage-meter.js +4 -3
  22. package/dist/cjs/client/components/user-menu.js +33 -6
  23. package/dist/cjs/client/index.js +30 -2
  24. package/dist/cjs/client/mark.js +1 -1
  25. package/dist/cjs/client/provider.js +60 -8
  26. package/dist/cjs/client/use-active-path.js +77 -0
  27. package/dist/cjs/client/use-checkout.js +3 -3
  28. package/dist/cjs/errors.js +4 -4
  29. package/dist/cjs/helpers.js +1 -1
  30. package/dist/cjs/index.js +1 -1
  31. package/dist/cjs/next/index.js +96 -15
  32. package/dist/cjs/server/client.js +22 -14
  33. package/dist/cjs/server/handler.js +99 -16
  34. package/dist/cjs/server/http.js +1 -1
  35. package/dist/cjs/server/index.js +8 -1
  36. package/dist/cjs/server/resources/access.js +3 -3
  37. package/dist/cjs/server/resources/ai.js +1 -1
  38. package/dist/cjs/server/resources/analytics.js +66 -0
  39. package/dist/cjs/server/resources/apps.js +4 -4
  40. package/dist/cjs/server/resources/billing.js +5 -5
  41. package/dist/cjs/server/resources/drive.js +4 -4
  42. package/dist/cjs/server/resources/entitlements.js +7 -7
  43. package/dist/cjs/server/resources/identity.js +4 -4
  44. package/dist/cjs/server/resources/integrations.js +66 -0
  45. package/dist/cjs/server/resources/logs.js +3 -3
  46. package/dist/cjs/server/resources/sessions.js +6 -6
  47. package/dist/cjs/server/webhooks.js +127 -0
  48. package/dist/cjs/types.js +1 -1
  49. package/dist/esm/cli/doctor.js +3 -3
  50. package/dist/esm/cli/init.js +3 -3
  51. package/dist/esm/client/components/access-gate.js +158 -0
  52. package/dist/esm/client/components/account-sessions.js +24 -22
  53. package/dist/esm/client/components/app-shell.js +154 -0
  54. package/dist/esm/client/components/apps-grid.js +4 -3
  55. package/dist/esm/client/components/feature-gate.js +2 -2
  56. package/dist/esm/client/components/integrations-modal.js +92 -0
  57. package/dist/esm/client/components/logo.js +3 -3
  58. package/dist/esm/client/components/motion.js +21 -0
  59. package/dist/esm/client/components/nav.js +89 -0
  60. package/dist/esm/client/components/plan-pricing.js +1 -1
  61. package/dist/esm/client/components/plans.js +42 -40
  62. package/dist/esm/client/components/readiness.js +6 -4
  63. package/dist/esm/client/components/side-nav.js +53 -0
  64. package/dist/esm/client/components/subscribe-dialog.js +2 -2
  65. package/dist/esm/client/components/subscription.js +3 -2
  66. package/dist/esm/client/components/surfaces.js +51 -0
  67. package/dist/esm/client/components/usage-meter.js +4 -3
  68. package/dist/esm/client/components/user-menu.js +34 -7
  69. package/dist/esm/client/index.js +16 -2
  70. package/dist/esm/client/mark.js +1 -1
  71. package/dist/esm/client/provider.js +59 -8
  72. package/dist/esm/client/use-active-path.js +74 -0
  73. package/dist/esm/client/use-checkout.js +3 -3
  74. package/dist/esm/errors.js +4 -4
  75. package/dist/esm/helpers.js +1 -1
  76. package/dist/esm/index.js +1 -1
  77. package/dist/esm/next/index.js +95 -15
  78. package/dist/esm/server/client.js +22 -14
  79. package/dist/esm/server/handler.js +99 -16
  80. package/dist/esm/server/http.js +1 -1
  81. package/dist/esm/server/index.js +5 -0
  82. package/dist/esm/server/resources/access.js +3 -3
  83. package/dist/esm/server/resources/ai.js +1 -1
  84. package/dist/esm/server/resources/analytics.js +62 -0
  85. package/dist/esm/server/resources/apps.js +4 -4
  86. package/dist/esm/server/resources/billing.js +5 -5
  87. package/dist/esm/server/resources/drive.js +4 -4
  88. package/dist/esm/server/resources/entitlements.js +7 -7
  89. package/dist/esm/server/resources/identity.js +4 -4
  90. package/dist/esm/server/resources/integrations.js +62 -0
  91. package/dist/esm/server/resources/logs.js +3 -3
  92. package/dist/esm/server/resources/sessions.js +6 -6
  93. package/dist/esm/server/webhooks.js +123 -0
  94. package/dist/esm/types.js +1 -1
  95. package/dist/theme.css +28 -0
  96. package/dist/types/client/components/access-gate.d.ts +79 -0
  97. package/dist/types/client/components/account-sessions.d.ts +1 -1
  98. package/dist/types/client/components/app-shell.d.ts +115 -0
  99. package/dist/types/client/components/apps-grid.d.ts +1 -1
  100. package/dist/types/client/components/feature-gate.d.ts +1 -1
  101. package/dist/types/client/components/integrations-modal.d.ts +21 -0
  102. package/dist/types/client/components/logo.d.ts +5 -5
  103. package/dist/types/client/components/motion.d.ts +22 -0
  104. package/dist/types/client/components/nav.d.ts +110 -0
  105. package/dist/types/client/components/plan-pricing.d.ts +2 -2
  106. package/dist/types/client/components/readiness.d.ts +2 -2
  107. package/dist/types/client/components/side-nav.d.ts +31 -0
  108. package/dist/types/client/components/subscribe-dialog.d.ts +3 -3
  109. package/dist/types/client/components/surfaces.d.ts +51 -0
  110. package/dist/types/client/components/usage-meter.d.ts +1 -1
  111. package/dist/types/client/components/user-menu.d.ts +2 -2
  112. package/dist/types/client/index.d.ts +13 -2
  113. package/dist/types/client/mark.d.ts +1 -1
  114. package/dist/types/client/provider.d.ts +44 -9
  115. package/dist/types/client/use-active-path.d.ts +8 -0
  116. package/dist/types/client/use-checkout.d.ts +3 -3
  117. package/dist/types/errors.d.ts +4 -4
  118. package/dist/types/helpers.d.ts +1 -1
  119. package/dist/types/index.d.ts +1 -1
  120. package/dist/types/next/index.d.ts +40 -0
  121. package/dist/types/server/client.d.ts +17 -11
  122. package/dist/types/server/handler.d.ts +2 -2
  123. package/dist/types/server/index.d.ts +7 -0
  124. package/dist/types/server/resources/access.d.ts +3 -3
  125. package/dist/types/server/resources/ai.d.ts +2 -2
  126. package/dist/types/server/resources/analytics.d.ts +31 -0
  127. package/dist/types/server/resources/apps.d.ts +4 -4
  128. package/dist/types/server/resources/billing.d.ts +4 -4
  129. package/dist/types/server/resources/drive.d.ts +4 -4
  130. package/dist/types/server/resources/entitlements.d.ts +4 -4
  131. package/dist/types/server/resources/identity.d.ts +14 -4
  132. package/dist/types/server/resources/integrations.d.ts +40 -0
  133. package/dist/types/server/resources/logs.d.ts +3 -3
  134. package/dist/types/server/resources/sessions.d.ts +5 -5
  135. package/dist/types/server/webhooks.d.ts +67 -0
  136. package/dist/types/types.d.ts +220 -39
  137. package/package.json +10 -4
@@ -1,5 +1,5 @@
1
1
  import { pathSegment, request } from "../http.js";
2
- /** Files in an organization's Drive - upload, and signed URLs to read back. */
2
+ /** Files in a workspace's Drive - upload, and signed URLs to read back. */
3
3
  export class DriveResource {
4
4
  transport;
5
5
  constructor(transport) {
@@ -9,7 +9,7 @@ export class DriveResource {
9
9
  const form = new FormData();
10
10
  const fileName = input.fileName ?? input.file.name ?? "upload";
11
11
  form.set("file", input.file, fileName);
12
- form.set("organizationId", input.organizationId);
12
+ form.set("workspaceId", input.workspaceId);
13
13
  if (input.parentId)
14
14
  form.set("parentId", input.parentId);
15
15
  return request(this.transport, { method: "POST", path: "/drive/files", multipart: form });
@@ -19,7 +19,7 @@ export class DriveResource {
19
19
  const result = await request(this.transport, {
20
20
  method: "GET",
21
21
  path: `/drive/files/${pathSegment(fileId, "fileId")}/url`,
22
- query: { organizationId: input.organizationId },
22
+ query: { workspaceId: input.workspaceId },
23
23
  });
24
24
  return result.url;
25
25
  }
@@ -28,7 +28,7 @@ export class DriveResource {
28
28
  const result = await request(this.transport, {
29
29
  method: "GET",
30
30
  path: `/drive/files/${pathSegment(fileId, "fileId")}/preview-url`,
31
- query: { organizationId: input.organizationId, size: input.size },
31
+ query: { workspaceId: input.workspaceId, size: input.size },
32
32
  });
33
33
  return result.url;
34
34
  }
@@ -9,7 +9,7 @@ export class EntitlementsResource {
9
9
  this.transport = transport;
10
10
  }
11
11
  /**
12
- * Every gate Connect owns between an organization and your service -
12
+ * Every gate Connect owns between a workspace and your application -
13
13
  * subscription, KYC/KYB, a chargeable payment method, seat headroom -
14
14
  * resolved in one call.
15
15
  *
@@ -17,17 +17,17 @@ export class EntitlementsResource {
17
17
  * Connect screen that fixes it, so you can route them somewhere useful
18
18
  * instead of refusing them with nowhere to go.
19
19
  */
20
- getReadiness(organizationId) {
20
+ getReadiness(workspaceId) {
21
21
  return request(this.transport, {
22
22
  method: "GET",
23
- path: `/onboarding/organizations/${pathSegment(organizationId, "organizationId")}`,
23
+ path: `/onboarding/workspaces/${pathSegment(workspaceId, "workspaceId")}`,
24
24
  });
25
25
  }
26
26
  /** The live plan and subscription, including `plan.features`. */
27
- getSubscription(organizationId) {
27
+ getSubscription(workspaceId) {
28
28
  return request(this.transport, {
29
29
  method: "GET",
30
- path: `/subscriptions/organizations/${pathSegment(organizationId, "organizationId")}`,
30
+ path: `/subscriptions/workspaces/${pathSegment(workspaceId, "workspaceId")}`,
31
31
  });
32
32
  }
33
33
  /**
@@ -38,8 +38,8 @@ export class EntitlementsResource {
38
38
  * all: "not entitled" is the same answer either way, and a feature gate
39
39
  * should not need a try/catch around it.
40
40
  */
41
- async hasFeature(organizationId, feature) {
42
- const result = await this.getSubscription(organizationId);
41
+ async hasFeature(workspaceId, feature) {
42
+ const result = await this.getSubscription(workspaceId);
43
43
  return result.hasActiveSubscription && (result.plan?.features.includes(feature) ?? false);
44
44
  }
45
45
  }
@@ -66,7 +66,7 @@ export class IdentityResource {
66
66
  sessionTokenExpiresIn: wire.session_token_expires_in,
67
67
  user: wire.user,
68
68
  subscription: wire.subscription,
69
- serviceRole: wire.serviceRole,
69
+ applicationRole: wire.applicationRole,
70
70
  };
71
71
  }
72
72
  /**
@@ -136,7 +136,7 @@ export class IdentityResource {
136
136
  return {
137
137
  user: wire.user,
138
138
  subscription: wire.subscription,
139
- serviceRole: wire.serviceRole,
139
+ applicationRole: wire.applicationRole,
140
140
  sessionExpiresAt: wire.session_expires_at,
141
141
  };
142
142
  }
@@ -145,7 +145,7 @@ export class IdentityResource {
145
145
  * rest of its ~15-day life. Discard your own copy of `sessionToken` too;
146
146
  * this does not do that for you.
147
147
  *
148
- * `scope` defaults to `"device"`: this sign-in, every other service signed
148
+ * `scope` defaults to `"device"`: this sign-in, every other application signed
149
149
  * into from the same browser, and Connect's own session on that one device.
150
150
  * `"all"` ends every session on every device - an explicit "sign out
151
151
  * everywhere" action only.
@@ -177,7 +177,7 @@ export class IdentityResource {
177
177
  throw new ConnectError({
178
178
  status: 0,
179
179
  code: "missing_client_id",
180
- message: "identity.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this service's Setup tab once SSO is enabled.",
180
+ message: "identity.* needs `clientId`. Pass it to `new ConnectClient({ clientId, apiKey })` - it is shown in this application's Integration tab, under Connect client.",
181
181
  endpoint: "identity",
182
182
  });
183
183
  }
@@ -0,0 +1,62 @@
1
+ import { request } from "../http.js";
2
+ /**
3
+ * Composio-hosted third-party connections (Google Calendar, Slack,
4
+ * Twitter/X, ...) a workspace has connected. Connect proxies every call
5
+ * here - your application 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 workspace's connection status for each. */
14
+ async list(workspaceId) {
15
+ const result = await request(this.transport, {
16
+ method: "GET",
17
+ path: "/integrations",
18
+ query: { workspaceId },
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 workspace 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(workspaceId, toolkitSlug, returnUrl) {
40
+ return request(this.transport, {
41
+ method: "POST",
42
+ path: "/integrations/connect",
43
+ json: { workspaceId, toolkitSlug, returnUrl },
44
+ });
45
+ }
46
+ /** Idempotent - disconnecting a toolkit that was never connected, or is already disconnected, is a success. */
47
+ async disconnect(workspaceId, toolkitSlug) {
48
+ return request(this.transport, {
49
+ method: "POST",
50
+ path: "/integrations/disconnect",
51
+ json: { workspaceId, toolkitSlug },
52
+ });
53
+ }
54
+ /** Calls a Composio action against this workspace's connected account for the named toolkit. */
55
+ async execute(workspaceId, input) {
56
+ return request(this.transport, {
57
+ method: "POST",
58
+ path: "/integrations/actions/execute",
59
+ json: { workspaceId, ...input },
60
+ });
61
+ }
62
+ }
@@ -1,5 +1,5 @@
1
1
  import { request } from "../http.js";
2
- /** Push feature events into the Console's audit log, filterable to your service. */
2
+ /** Push feature events into the Console's audit log, filterable to your application. */
3
3
  export class LogsResource {
4
4
  transport;
5
5
  constructor(transport) {
@@ -11,9 +11,9 @@ export class LogsResource {
11
11
  * reported through the return value.
12
12
  *
13
13
  * Pass `sessionToken` to attribute the event to the end user it belongs to.
14
- * Both scopes on the stored entry come from something verified - the service
14
+ * Both scopes on the stored entry come from something verified - the application
15
15
  * from your API key, the user from that token, which Connect checks was
16
- * minted for your service before attributing anything. `attributed` in the
16
+ * minted for your application before attributing anything. `attributed` in the
17
17
  * result says whether it resolved; a `false` there still means the line was
18
18
  * stored, just without a user on it.
19
19
  */
@@ -5,11 +5,11 @@ import { request } from "../http.js";
5
5
  * timestamps - the same list Connect's own account settings page shows the
6
6
  * user themselves.
7
7
  *
8
- * **Account-wide, not scoped to this service.** A Connect session is one
9
- * browser's login to Connect, and it fans out to every partner service's own
10
- * `sessionToken` - Connect has no per-service session concept to filter by.
8
+ * **Account-wide, not scoped to this application.** A Connect session is one
9
+ * browser's login to Connect, and it fans out to every partner application's own
10
+ * `sessionToken` - Connect has no per-application session concept to filter by.
11
11
  * `list()` returns every session on the account, including ones that
12
- * originated from a completely different, unrelated service. Requires the
12
+ * originated from a completely different, unrelated application. Requires the
13
13
  * `sessions:manage` scope on your API key - unlike `identity`'s base `sso`
14
14
  * capability, this is never granted to a zero-scope legacy key by default.
15
15
  *
@@ -35,7 +35,7 @@ export class SessionsResource {
35
35
  }
36
36
  /**
37
37
  * Ends one specific session by id - not necessarily the one `sessionToken`
38
- * itself was minted from. Cascades to every partner-service sign-in that
38
+ * itself was minted from. Cascades to every partner-application sign-in that
39
39
  * originated from that same browser, same as Connect's own "sign out this
40
40
  * device".
41
41
  */
@@ -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 application'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
+ * "workspace.deletion_scheduled": ({ workspaceId, purgeAfter }) =>
58
+ * freeze(workspaceId, purgeAfter),
59
+ * "workspace.deletion_cancelled": ({ workspaceId }) => unfreeze(workspaceId),
60
+ * "workspace.deleted": ({ workspaceId }) => purge(workspaceId),
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/esm/types.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Wire types for the `/api/v1/service/*` surface - the service-partner API,
2
+ * Wire types for the `/api/v1/application/*` surface - the application-partner API,
3
3
  * and the only thing this SDK covers.
4
4
  *
5
5
  * Hand-written rather than generated so the published package carries no
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
+ /** `workspace_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;
@@ -10,7 +10,7 @@ export interface ConnectAccountSessionsProps {
10
10
  }
11
11
  /**
12
12
  * Every device signed into this Connect account - not scoped to this
13
- * service, see `ConnectUserSession`'s doc comment. Owns the one
13
+ * application, see `ConnectUserSession`'s doc comment. Owns the one
14
14
  * `useConnectSessions()` call for this modal; every revoke action here
15
15
  * mutates through it and re-reads the list itself, since nothing else
16
16
  * invalidates it automatically - see `mutateResource`'s doc comment.
@@ -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 application's name, in the `cosmo·ops VOICE` lockup. See `ConnectLogo`. */
6
+ name: string;
7
+ /** This application'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 application 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 applications 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;
@@ -6,7 +6,7 @@ export interface ConnectAppsGridProps {
6
6
  }
7
7
  /**
8
8
  * The Google-style app launcher: a grid icon in your top bar that opens a
9
- * modal of every Connect service this organization can actually open.
9
+ * modal of every Connect application this workspace can actually open.
10
10
  *
11
11
  * Only launchable apps appear, so no tile is a dead end - a paused
12
12
  * subscription still shows, badged, because a dunning cycle should not make a
@@ -48,7 +48,7 @@ export interface ConnectFeatureGateProps {
48
48
  *
49
49
  * Like every gate in this package, it is a **UX gate, not a security
50
50
  * boundary**: the entitlement is read through your own origin and the browser
51
- * can ignore it. Check `client.entitlements.hasFeature(orgId, feature)` on the
51
+ * can ignore it. Check `client.entitlements.hasFeature(workspaceId, feature)` on the
52
52
  * server before doing the thing the feature unlocks.
53
53
  *
54
54
  * Renders `loading` - not `fallback` - while the answer is unknown, so a paying
@@ -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 workspace 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;