@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.
- package/CHANGELOG.md +125 -0
- package/README.md +215 -6
- package/dist/cjs/cli/doctor.js +3 -3
- package/dist/cjs/cli/init.js +3 -3
- package/dist/cjs/client/components/access-gate.js +162 -0
- package/dist/cjs/client/components/account-sessions.js +24 -22
- package/dist/cjs/client/components/app-shell.js +157 -0
- package/dist/cjs/client/components/apps-grid.js +4 -3
- package/dist/cjs/client/components/feature-gate.js +2 -2
- package/dist/cjs/client/components/integrations-modal.js +95 -0
- package/dist/cjs/client/components/logo.js +3 -3
- package/dist/cjs/client/components/motion.js +27 -0
- package/dist/cjs/client/components/nav.js +97 -0
- package/dist/cjs/client/components/plan-pricing.js +1 -1
- package/dist/cjs/client/components/plans.js +42 -40
- package/dist/cjs/client/components/readiness.js +6 -4
- package/dist/cjs/client/components/side-nav.js +58 -0
- package/dist/cjs/client/components/subscribe-dialog.js +2 -2
- package/dist/cjs/client/components/subscription.js +3 -2
- package/dist/cjs/client/components/surfaces.js +54 -0
- package/dist/cjs/client/components/usage-meter.js +4 -3
- package/dist/cjs/client/components/user-menu.js +33 -6
- package/dist/cjs/client/index.js +30 -2
- package/dist/cjs/client/mark.js +1 -1
- package/dist/cjs/client/provider.js +60 -8
- package/dist/cjs/client/use-active-path.js +77 -0
- package/dist/cjs/client/use-checkout.js +3 -3
- package/dist/cjs/errors.js +4 -4
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/next/index.js +96 -15
- package/dist/cjs/server/client.js +22 -14
- package/dist/cjs/server/handler.js +99 -16
- package/dist/cjs/server/http.js +1 -1
- package/dist/cjs/server/index.js +8 -1
- package/dist/cjs/server/resources/access.js +3 -3
- package/dist/cjs/server/resources/ai.js +1 -1
- package/dist/cjs/server/resources/analytics.js +66 -0
- package/dist/cjs/server/resources/apps.js +4 -4
- package/dist/cjs/server/resources/billing.js +5 -5
- package/dist/cjs/server/resources/drive.js +4 -4
- package/dist/cjs/server/resources/entitlements.js +7 -7
- package/dist/cjs/server/resources/identity.js +4 -4
- package/dist/cjs/server/resources/integrations.js +66 -0
- package/dist/cjs/server/resources/logs.js +3 -3
- package/dist/cjs/server/resources/sessions.js +6 -6
- package/dist/cjs/server/webhooks.js +127 -0
- package/dist/cjs/types.js +1 -1
- package/dist/esm/cli/doctor.js +3 -3
- package/dist/esm/cli/init.js +3 -3
- package/dist/esm/client/components/access-gate.js +158 -0
- package/dist/esm/client/components/account-sessions.js +24 -22
- package/dist/esm/client/components/app-shell.js +154 -0
- package/dist/esm/client/components/apps-grid.js +4 -3
- package/dist/esm/client/components/feature-gate.js +2 -2
- package/dist/esm/client/components/integrations-modal.js +92 -0
- package/dist/esm/client/components/logo.js +3 -3
- package/dist/esm/client/components/motion.js +21 -0
- package/dist/esm/client/components/nav.js +89 -0
- package/dist/esm/client/components/plan-pricing.js +1 -1
- package/dist/esm/client/components/plans.js +42 -40
- package/dist/esm/client/components/readiness.js +6 -4
- package/dist/esm/client/components/side-nav.js +53 -0
- package/dist/esm/client/components/subscribe-dialog.js +2 -2
- package/dist/esm/client/components/subscription.js +3 -2
- package/dist/esm/client/components/surfaces.js +51 -0
- package/dist/esm/client/components/usage-meter.js +4 -3
- package/dist/esm/client/components/user-menu.js +34 -7
- package/dist/esm/client/index.js +16 -2
- package/dist/esm/client/mark.js +1 -1
- package/dist/esm/client/provider.js +59 -8
- package/dist/esm/client/use-active-path.js +74 -0
- package/dist/esm/client/use-checkout.js +3 -3
- package/dist/esm/errors.js +4 -4
- package/dist/esm/helpers.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/next/index.js +95 -15
- package/dist/esm/server/client.js +22 -14
- package/dist/esm/server/handler.js +99 -16
- package/dist/esm/server/http.js +1 -1
- package/dist/esm/server/index.js +5 -0
- package/dist/esm/server/resources/access.js +3 -3
- package/dist/esm/server/resources/ai.js +1 -1
- package/dist/esm/server/resources/analytics.js +62 -0
- package/dist/esm/server/resources/apps.js +4 -4
- package/dist/esm/server/resources/billing.js +5 -5
- package/dist/esm/server/resources/drive.js +4 -4
- package/dist/esm/server/resources/entitlements.js +7 -7
- package/dist/esm/server/resources/identity.js +4 -4
- package/dist/esm/server/resources/integrations.js +62 -0
- package/dist/esm/server/resources/logs.js +3 -3
- package/dist/esm/server/resources/sessions.js +6 -6
- package/dist/esm/server/webhooks.js +123 -0
- package/dist/esm/types.js +1 -1
- package/dist/theme.css +28 -0
- package/dist/types/client/components/access-gate.d.ts +79 -0
- package/dist/types/client/components/account-sessions.d.ts +1 -1
- package/dist/types/client/components/app-shell.d.ts +115 -0
- package/dist/types/client/components/apps-grid.d.ts +1 -1
- package/dist/types/client/components/feature-gate.d.ts +1 -1
- package/dist/types/client/components/integrations-modal.d.ts +21 -0
- package/dist/types/client/components/logo.d.ts +5 -5
- 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/plan-pricing.d.ts +2 -2
- package/dist/types/client/components/readiness.d.ts +2 -2
- package/dist/types/client/components/side-nav.d.ts +31 -0
- package/dist/types/client/components/subscribe-dialog.d.ts +3 -3
- package/dist/types/client/components/surfaces.d.ts +51 -0
- package/dist/types/client/components/usage-meter.d.ts +1 -1
- package/dist/types/client/components/user-menu.d.ts +2 -2
- package/dist/types/client/index.d.ts +13 -2
- package/dist/types/client/mark.d.ts +1 -1
- package/dist/types/client/provider.d.ts +44 -9
- package/dist/types/client/use-active-path.d.ts +8 -0
- package/dist/types/client/use-checkout.d.ts +3 -3
- package/dist/types/errors.d.ts +4 -4
- package/dist/types/helpers.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/next/index.d.ts +40 -0
- package/dist/types/server/client.d.ts +17 -11
- package/dist/types/server/handler.d.ts +2 -2
- package/dist/types/server/index.d.ts +7 -0
- package/dist/types/server/resources/access.d.ts +3 -3
- package/dist/types/server/resources/ai.d.ts +2 -2
- package/dist/types/server/resources/analytics.d.ts +31 -0
- package/dist/types/server/resources/apps.d.ts +4 -4
- package/dist/types/server/resources/billing.d.ts +4 -4
- package/dist/types/server/resources/drive.d.ts +4 -4
- package/dist/types/server/resources/entitlements.d.ts +4 -4
- package/dist/types/server/resources/identity.d.ts +14 -4
- package/dist/types/server/resources/integrations.d.ts +40 -0
- package/dist/types/server/resources/logs.d.ts +3 -3
- package/dist/types/server/resources/sessions.d.ts +5 -5
- package/dist/types/server/webhooks.d.ts +67 -0
- package/dist/types/types.d.ts +220 -39
- package/package.json +10 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { pathSegment, request } from "../http.js";
|
|
2
|
-
/** Files in
|
|
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("
|
|
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: {
|
|
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: {
|
|
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
|
|
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(
|
|
20
|
+
getReadiness(workspaceId) {
|
|
21
21
|
return request(this.transport, {
|
|
22
22
|
method: "GET",
|
|
23
|
-
path: `/onboarding/
|
|
23
|
+
path: `/onboarding/workspaces/${pathSegment(workspaceId, "workspaceId")}`,
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
/** The live plan and subscription, including `plan.features`. */
|
|
27
|
-
getSubscription(
|
|
27
|
+
getSubscription(workspaceId) {
|
|
28
28
|
return request(this.transport, {
|
|
29
29
|
method: "GET",
|
|
30
|
-
path: `/subscriptions/
|
|
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(
|
|
42
|
-
const result = await this.getSubscription(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
9
|
-
* browser's login to Connect, and it fans out to every partner
|
|
10
|
-
* `sessionToken` - Connect has no per-
|
|
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
|
|
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-
|
|
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
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
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(
|
|
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;
|