@12-apps/mcp 3.2.0 → 3.2.1
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/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/chunk-FYEVBTDU.js +162 -0
- package/dist/chunk-FYEVBTDU.js.map +1 -0
- package/dist/chunk-HAZOPC6U.js +229 -0
- package/dist/chunk-HAZOPC6U.js.map +1 -0
- package/dist/chunk-UIILEGAC.js +1247 -0
- package/dist/chunk-UIILEGAC.js.map +1 -0
- package/dist/chunk-WJJNKKNS.js +63 -0
- package/dist/chunk-WJJNKKNS.js.map +1 -0
- package/dist/coverage-gate/index.d.ts +129 -0
- package/dist/coverage-gate/index.js +163 -0
- package/dist/coverage-gate/index.js.map +1 -0
- package/dist/create-api-mcp-oauth-CwVXKK-A.d.ts +647 -0
- package/dist/generate/index.d.ts +91 -0
- package/dist/generate/index.js +101 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/generate-Dx3cK8th.d.ts +184 -0
- package/dist/guide-DV5MQbCg.d.ts +135 -0
- package/dist/hono/index.d.ts +28 -0
- package/dist/hono/index.js +25 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +331 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/index.d.ts +446 -0
- package/dist/oauth/index.js +279 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/react/index.d.ts +239 -0
- package/dist/react/index.js +1183 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +31 -10
- package/prisma/migrations/20260812150000_add_mcp_oauth_tables/migration.sql +6 -6
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {
|
|
2
|
+
providerForHostId
|
|
3
|
+
} from "../chunk-FYEVBTDU.js";
|
|
4
|
+
import {
|
|
5
|
+
ACCESS_TOKEN_TTL_SECONDS,
|
|
6
|
+
AUTHORIZATION_CODE_AUDIENCE,
|
|
7
|
+
AUTHORIZATION_CODE_TTL_SECONDS,
|
|
8
|
+
AccessTokenError,
|
|
9
|
+
AuthorizationCodeError,
|
|
10
|
+
DEFAULT_MCP_RESOURCE_PATH,
|
|
11
|
+
DEFAULT_OAUTH_PATHS,
|
|
12
|
+
DEFAULT_PROVIDER_ROOTS,
|
|
13
|
+
DEFAULT_SIGNING_KEY_ENV,
|
|
14
|
+
DEFAULT_SIGNING_KEY_ID_ENV,
|
|
15
|
+
MCP_SUPPORTED_SCOPES,
|
|
16
|
+
REFRESH_TOKEN_TTL_MS,
|
|
17
|
+
RefreshTokenError,
|
|
18
|
+
SIGNING_ALG,
|
|
19
|
+
SUPPORTED_CHALLENGE_METHOD,
|
|
20
|
+
UnsupportedChallengeMethodError,
|
|
21
|
+
computeChallenge,
|
|
22
|
+
createApiMcpOauth,
|
|
23
|
+
getRefreshTokenIdentity,
|
|
24
|
+
hashSecret,
|
|
25
|
+
hashToken,
|
|
26
|
+
inProcessCodeReplayStore,
|
|
27
|
+
issueRefreshToken,
|
|
28
|
+
issuer,
|
|
29
|
+
loadSigningKeyFromEnv,
|
|
30
|
+
matchesRedirectUri,
|
|
31
|
+
mintCode,
|
|
32
|
+
originFromRequest,
|
|
33
|
+
providerFromRedirectUris,
|
|
34
|
+
registerClient,
|
|
35
|
+
resolveMcpOauthConfig,
|
|
36
|
+
resolveTrustedOrigin,
|
|
37
|
+
resourceAudience,
|
|
38
|
+
rotateRefreshToken,
|
|
39
|
+
signAccessToken,
|
|
40
|
+
signingKeyProvider,
|
|
41
|
+
trustedOriginsFromEnv,
|
|
42
|
+
verifyAccessToken,
|
|
43
|
+
verifyChallenge,
|
|
44
|
+
verifyCode
|
|
45
|
+
} from "../chunk-UIILEGAC.js";
|
|
46
|
+
import "../chunk-WJJNKKNS.js";
|
|
47
|
+
import {
|
|
48
|
+
__name
|
|
49
|
+
} from "../chunk-7QVYU63E.js";
|
|
50
|
+
|
|
51
|
+
// src/oauth/prisma-stores.ts
|
|
52
|
+
function clientStore(getPrisma) {
|
|
53
|
+
return {
|
|
54
|
+
async create(client) {
|
|
55
|
+
const prisma = await getPrisma();
|
|
56
|
+
return prisma.oAuthClient.create({ data: client });
|
|
57
|
+
},
|
|
58
|
+
async findByClientId(clientId) {
|
|
59
|
+
const prisma = await getPrisma();
|
|
60
|
+
return prisma.oAuthClient.findUnique({ where: { clientId } });
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
__name(clientStore, "clientStore");
|
|
65
|
+
function refreshTokenStore(getPrisma) {
|
|
66
|
+
return {
|
|
67
|
+
async create(token) {
|
|
68
|
+
const prisma = await getPrisma();
|
|
69
|
+
await prisma.oAuthRefreshToken.create({ data: token });
|
|
70
|
+
},
|
|
71
|
+
async findByHash(tokenHash) {
|
|
72
|
+
const prisma = await getPrisma();
|
|
73
|
+
return prisma.oAuthRefreshToken.findUnique({ where: { tokenHash } });
|
|
74
|
+
},
|
|
75
|
+
async hasSuccessor(tokenHash) {
|
|
76
|
+
const prisma = await getPrisma();
|
|
77
|
+
const successor = await prisma.oAuthRefreshToken.findFirst({
|
|
78
|
+
where: { rotatedFrom: tokenHash }
|
|
79
|
+
});
|
|
80
|
+
return successor !== null;
|
|
81
|
+
},
|
|
82
|
+
async listFamily(userEmail, clientId) {
|
|
83
|
+
const prisma = await getPrisma();
|
|
84
|
+
return prisma.oAuthRefreshToken.findMany({ where: { userEmail, clientId } });
|
|
85
|
+
},
|
|
86
|
+
async revokeHashes(tokenHashes, at) {
|
|
87
|
+
if (tokenHashes.length === 0) return;
|
|
88
|
+
const prisma = await getPrisma();
|
|
89
|
+
await prisma.oAuthRefreshToken.updateMany({
|
|
90
|
+
where: { tokenHash: { in: [...tokenHashes] } },
|
|
91
|
+
data: { revokedAt: at }
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
async rotate(successor, parentHash, at) {
|
|
95
|
+
const prisma = await getPrisma();
|
|
96
|
+
return prisma.$transaction(async (tx) => {
|
|
97
|
+
const { count } = await tx.oAuthRefreshToken.updateMany({
|
|
98
|
+
where: { tokenHash: parentHash, revokedAt: null },
|
|
99
|
+
data: { revokedAt: at }
|
|
100
|
+
});
|
|
101
|
+
if (count !== 1) return false;
|
|
102
|
+
await tx.oAuthRefreshToken.create({ data: successor });
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
async revokeLiveForClient(userEmail, clientId) {
|
|
107
|
+
const prisma = await getPrisma();
|
|
108
|
+
const { count } = await prisma.oAuthRefreshToken.updateMany({
|
|
109
|
+
where: { userEmail, clientId, revokedAt: null },
|
|
110
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
111
|
+
});
|
|
112
|
+
return count;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
__name(refreshTokenStore, "refreshTokenStore");
|
|
117
|
+
var CONNECTION_SELECT = {
|
|
118
|
+
oauthClientId: true,
|
|
119
|
+
clientName: true,
|
|
120
|
+
host: true,
|
|
121
|
+
connectedAt: true,
|
|
122
|
+
lastActiveAt: true
|
|
123
|
+
};
|
|
124
|
+
function connectionStore(getPrisma) {
|
|
125
|
+
return {
|
|
126
|
+
async lastActiveAt(userId, oauthClientId) {
|
|
127
|
+
const prisma = await getPrisma();
|
|
128
|
+
const row = await prisma.mcpConnection.findUnique({
|
|
129
|
+
where: { userId_oauthClientId: { userId, oauthClientId } },
|
|
130
|
+
select: { lastActiveAt: true }
|
|
131
|
+
});
|
|
132
|
+
return row?.lastActiveAt ?? null;
|
|
133
|
+
},
|
|
134
|
+
async recordActivity({ userId, oauthClientId, clientName, host, at }) {
|
|
135
|
+
const prisma = await getPrisma();
|
|
136
|
+
await prisma.mcpConnection.upsert({
|
|
137
|
+
where: { userId_oauthClientId: { userId, oauthClientId } },
|
|
138
|
+
create: { userId, oauthClientId, clientName, host, connectedAt: at, lastActiveAt: at },
|
|
139
|
+
// Never blank a known host on refresh — keep the existing attribution
|
|
140
|
+
// when this grant cannot derive one.
|
|
141
|
+
update: {
|
|
142
|
+
clientName,
|
|
143
|
+
lastActiveAt: at,
|
|
144
|
+
revokedAt: null,
|
|
145
|
+
...host ? { host } : {}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
async listActive(userId) {
|
|
150
|
+
const prisma = await getPrisma();
|
|
151
|
+
const rows = await prisma.mcpConnection.findMany({
|
|
152
|
+
where: { userId, revokedAt: null },
|
|
153
|
+
orderBy: { lastActiveAt: "desc" },
|
|
154
|
+
select: { ...CONNECTION_SELECT }
|
|
155
|
+
});
|
|
156
|
+
return rows;
|
|
157
|
+
},
|
|
158
|
+
revokeByHost: /* @__PURE__ */ __name((userId, host) => revokeByHost(getPrisma, userId, host), "revokeByHost"),
|
|
159
|
+
announce: /* @__PURE__ */ __name((userId, host) => announce(getPrisma, userId, host), "announce")
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
__name(connectionStore, "connectionStore");
|
|
163
|
+
async function revokeByHost(getPrisma, userId, host) {
|
|
164
|
+
const prisma = await getPrisma();
|
|
165
|
+
const attributed = await prisma.mcpConnection.findMany({
|
|
166
|
+
where: { userId, revokedAt: null, host },
|
|
167
|
+
select: { id: true, oauthClientId: true }
|
|
168
|
+
});
|
|
169
|
+
const targets = attributed.length > 0 ? attributed : await prisma.mcpConnection.findMany({
|
|
170
|
+
where: { userId, revokedAt: null, host: null },
|
|
171
|
+
select: { id: true, oauthClientId: true }
|
|
172
|
+
});
|
|
173
|
+
if (targets.length === 0) return [];
|
|
174
|
+
await prisma.mcpConnection.updateMany({
|
|
175
|
+
where: { id: { in: targets.map((row) => String(row.id)) } },
|
|
176
|
+
data: { revokedAt: /* @__PURE__ */ new Date() }
|
|
177
|
+
});
|
|
178
|
+
return targets.map((row) => String(row.oauthClientId));
|
|
179
|
+
}
|
|
180
|
+
__name(revokeByHost, "revokeByHost");
|
|
181
|
+
async function announce(getPrisma, userId, host) {
|
|
182
|
+
const prisma = await getPrisma();
|
|
183
|
+
const now = /* @__PURE__ */ new Date();
|
|
184
|
+
const refreshed = await prisma.mcpConnection.updateMany({
|
|
185
|
+
where: { userId, revokedAt: null, host },
|
|
186
|
+
data: { lastActiveAt: now, revokedAt: null }
|
|
187
|
+
});
|
|
188
|
+
if (refreshed.count > 0) return refreshed.count;
|
|
189
|
+
const candidate = await prisma.mcpConnection.findFirst({
|
|
190
|
+
where: { userId, revokedAt: null, host: null },
|
|
191
|
+
orderBy: { lastActiveAt: "desc" },
|
|
192
|
+
select: { id: true }
|
|
193
|
+
});
|
|
194
|
+
if (!candidate) return 0;
|
|
195
|
+
await prisma.mcpConnection.update({
|
|
196
|
+
where: { id: candidate.id },
|
|
197
|
+
data: { host, lastActiveAt: now, revokedAt: null }
|
|
198
|
+
});
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
__name(announce, "announce");
|
|
202
|
+
function createPrismaMcpStores(getPrisma) {
|
|
203
|
+
return {
|
|
204
|
+
clients: clientStore(getPrisma),
|
|
205
|
+
refreshTokens: refreshTokenStore(getPrisma),
|
|
206
|
+
connections: connectionStore(getPrisma)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
__name(createPrismaMcpStores, "createPrismaMcpStores");
|
|
210
|
+
|
|
211
|
+
// src/oauth/connections.ts
|
|
212
|
+
function asProvider(host) {
|
|
213
|
+
return host === null ? null : providerForHostId(host);
|
|
214
|
+
}
|
|
215
|
+
__name(asProvider, "asProvider");
|
|
216
|
+
async function listAiConnections(connections, userId) {
|
|
217
|
+
const rows = await connections.listActive(userId);
|
|
218
|
+
return rows.map((row) => ({ ...row, host: asProvider(row.host) }));
|
|
219
|
+
}
|
|
220
|
+
__name(listAiConnections, "listAiConnections");
|
|
221
|
+
async function disconnectAiHost(stores, caller, host) {
|
|
222
|
+
const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);
|
|
223
|
+
const revoked = await Promise.all(
|
|
224
|
+
disconnectedClientIds.map(
|
|
225
|
+
(clientId) => stores.refreshTokens.revokeLiveForClient(caller.email, clientId)
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
return {
|
|
229
|
+
disconnectedClientIds,
|
|
230
|
+
revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
__name(disconnectAiHost, "disconnectAiHost");
|
|
234
|
+
export {
|
|
235
|
+
ACCESS_TOKEN_TTL_SECONDS,
|
|
236
|
+
AUTHORIZATION_CODE_AUDIENCE,
|
|
237
|
+
AUTHORIZATION_CODE_TTL_SECONDS,
|
|
238
|
+
AccessTokenError,
|
|
239
|
+
AuthorizationCodeError,
|
|
240
|
+
DEFAULT_MCP_RESOURCE_PATH,
|
|
241
|
+
DEFAULT_OAUTH_PATHS,
|
|
242
|
+
DEFAULT_PROVIDER_ROOTS,
|
|
243
|
+
DEFAULT_SIGNING_KEY_ENV,
|
|
244
|
+
DEFAULT_SIGNING_KEY_ID_ENV,
|
|
245
|
+
MCP_SUPPORTED_SCOPES,
|
|
246
|
+
REFRESH_TOKEN_TTL_MS,
|
|
247
|
+
RefreshTokenError,
|
|
248
|
+
SIGNING_ALG,
|
|
249
|
+
SUPPORTED_CHALLENGE_METHOD,
|
|
250
|
+
UnsupportedChallengeMethodError,
|
|
251
|
+
computeChallenge,
|
|
252
|
+
createApiMcpOauth,
|
|
253
|
+
createPrismaMcpStores,
|
|
254
|
+
disconnectAiHost,
|
|
255
|
+
getRefreshTokenIdentity,
|
|
256
|
+
hashSecret,
|
|
257
|
+
hashToken,
|
|
258
|
+
inProcessCodeReplayStore,
|
|
259
|
+
issueRefreshToken,
|
|
260
|
+
issuer,
|
|
261
|
+
listAiConnections,
|
|
262
|
+
loadSigningKeyFromEnv,
|
|
263
|
+
matchesRedirectUri,
|
|
264
|
+
mintCode,
|
|
265
|
+
originFromRequest,
|
|
266
|
+
providerFromRedirectUris,
|
|
267
|
+
registerClient,
|
|
268
|
+
resolveMcpOauthConfig,
|
|
269
|
+
resolveTrustedOrigin,
|
|
270
|
+
resourceAudience,
|
|
271
|
+
rotateRefreshToken,
|
|
272
|
+
signAccessToken,
|
|
273
|
+
signingKeyProvider,
|
|
274
|
+
trustedOriginsFromEnv,
|
|
275
|
+
verifyAccessToken,
|
|
276
|
+
verifyChallenge,
|
|
277
|
+
verifyCode
|
|
278
|
+
};
|
|
279
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/oauth/prisma-stores.ts","../../src/oauth/connections.ts"],"sourcesContent":["import type {\n McpConnectionStore,\n McpOauthStores,\n NewOAuthClient,\n NewRefreshToken,\n OAuthClientStore,\n RefreshTokenStore,\n StoredMcpConnection,\n StoredOAuthClient,\n StoredRefreshToken,\n} from \"./stores\";\n\n/**\n * The ports of `./stores.ts`, filled by Prisma (12-23).\n *\n * The package owns the three models (`prisma/mcp.prisma`), so their delegate\n * shapes are known and this adapter can be exact. A host with Prisma therefore\n * writes ONE line —\n *\n * stores: createPrismaMcpStores(async () => getPrismaClient() as unknown as McpOauthPrisma)\n *\n * — and no host code at all beyond it. The client is duck-typed (only the\n * delegates used, only the arguments used) so this file never imports a project's\n * generated client, and a non-Prisma host fills the ports directly instead.\n */\n\n/** A `where` on the composite unique of `mcp_connections`. */\ninterface ConnectionKey {\n userId_oauthClientId: { userId: string; oauthClientId: string };\n}\n\n/** The minimal Prisma surface the AS needs. Every field is one the surface writes. */\nexport interface McpOauthPrisma {\n oAuthClient: {\n create(args: { data: NewOAuthClient }): Promise<StoredOAuthClient>;\n findUnique(args: { where: { clientId: string } }): Promise<StoredOAuthClient | null>;\n };\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n findUnique(args: { where: { tokenHash: string } }): Promise<StoredRefreshToken | null>;\n findFirst(args: { where: { rotatedFrom: string } }): Promise<{ tokenHash: string } | null>;\n findMany(args: {\n where: { userEmail: string; clientId: string };\n }): Promise<StoredRefreshToken[]>;\n // No single-row `update`: the rotation used to revoke its parent with one and\n // that was the bug (unconditional, so two concurrent rotations both won). Every\n // revoke here is now an `updateMany` with a predicate that says WHICH rows may\n // move, which is also why this delegate list stays honest about what is written.\n updateMany(args: {\n where:\n | { tokenHash: { in: string[] } }\n | { userEmail: string; clientId: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n mcpConnection: {\n findUnique(args: {\n where: ConnectionKey;\n select: { lastActiveAt: true };\n }): Promise<{ lastActiveAt: Date } | null>;\n findFirst(args: {\n where: { userId: string; revokedAt: null; host: null };\n orderBy: { lastActiveAt: \"desc\" };\n select: { id: true };\n }): Promise<{ id: string } | null>;\n findMany(args: {\n where: { userId: string; revokedAt: null; host?: string | null };\n orderBy?: { lastActiveAt: \"desc\" };\n select: Record<string, true>;\n }): Promise<Record<string, unknown>[]>;\n upsert(args: {\n where: ConnectionKey;\n create: Record<string, unknown>;\n update: Record<string, unknown>;\n }): Promise<unknown>;\n update(args: { where: { id: string }; data: Record<string, unknown> }): Promise<unknown>;\n updateMany(args: {\n where: { id: { in: string[] } } | { userId: string; revokedAt: null; host: string };\n data: Record<string, unknown>;\n }): Promise<{ count: number }>;\n };\n /**\n * Prisma's INTERACTIVE transaction, used for the rotation's claim + write. The\n * callback form (not the array form) is required: the successor may only be\n * created once the conditional revoke has reported that it, and not a concurrent\n * sibling, claimed the parent — see `RefreshTokenStore.rotate`.\n */\n $transaction<T>(fn: (tx: McpOauthTx) => Promise<T>): Promise<T>;\n}\n\n/**\n * The delegate subset used INSIDE the rotation transaction. Not exported: it is\n * reachable structurally through `McpOauthPrisma.$transaction`, so no host ever\n * needs to name it, and exporting a type nobody imports is what knip flags.\n */\ninterface McpOauthTx {\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n updateMany(args: {\n where: { tokenHash: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n}\n\n/** A lazily-resolved client, so a host's singleton is awaited per call. */\nexport type McpOauthPrismaProvider = () => Promise<McpOauthPrisma>;\n\nfunction clientStore(getPrisma: McpOauthPrismaProvider): OAuthClientStore {\n return {\n async create(client: NewOAuthClient) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.create({ data: client });\n },\n async findByClientId(clientId: string) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.findUnique({ where: { clientId } });\n },\n };\n}\n\nfunction refreshTokenStore(getPrisma: McpOauthPrismaProvider): RefreshTokenStore {\n return {\n async create(token) {\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.create({ data: token });\n },\n async findByHash(tokenHash) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findUnique({ where: { tokenHash } });\n },\n async hasSuccessor(tokenHash) {\n const prisma = await getPrisma();\n const successor = await prisma.oAuthRefreshToken.findFirst({\n where: { rotatedFrom: tokenHash },\n });\n return successor !== null;\n },\n async listFamily(userEmail, clientId) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findMany({ where: { userEmail, clientId } });\n },\n async revokeHashes(tokenHashes, at) {\n if (tokenHashes.length === 0) return;\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.updateMany({\n where: { tokenHash: { in: [...tokenHashes] } },\n data: { revokedAt: at },\n });\n },\n async rotate(successor, parentHash, at) {\n const prisma = await getPrisma();\n return prisma.$transaction(async (tx) => {\n // CLAIM-ONCE. The `revokedAt: null` predicate is what makes this safe under\n // concurrency, and it is load-bearing rather than defensive: on Postgres's\n // default READ COMMITTED, a second transaction's `updateMany` blocks on the\n // row lock, then re-evaluates this WHERE against the COMMITTED row — which\n // now has a `revokedAt` — and reports 0 rows. So exactly one caller can ever\n // see count 1, and it is the only one that goes on to create a successor.\n // An unconditional `update` would let both through: two live successors of\n // one parent, and replay detection silently defeated (it waits for a third\n // use of the parent that now never comes).\n const { count } = await tx.oAuthRefreshToken.updateMany({\n where: { tokenHash: parentHash, revokedAt: null },\n data: { revokedAt: at },\n });\n // Lost the claim: write NOTHING. The zero-row update commits as the no-op\n // it is, so there is nothing to roll back.\n if (count !== 1) return false;\n // Same transaction as the claim, so a crash cannot leave a live parent AND\n // a live child either.\n await tx.oAuthRefreshToken.create({ data: successor });\n return true;\n });\n },\n async revokeLiveForClient(userEmail, clientId) {\n const prisma = await getPrisma();\n const { count } = await prisma.oAuthRefreshToken.updateMany({\n where: { userEmail, clientId, revokedAt: null },\n data: { revokedAt: new Date() },\n });\n return count;\n },\n };\n}\n\n/** The connection columns the account surface reads. */\nconst CONNECTION_SELECT = {\n oauthClientId: true,\n clientName: true,\n host: true,\n connectedAt: true,\n lastActiveAt: true,\n} as const;\n\nfunction connectionStore(getPrisma: McpOauthPrismaProvider): McpConnectionStore {\n return {\n async lastActiveAt(userId, oauthClientId) {\n const prisma = await getPrisma();\n const row = await prisma.mcpConnection.findUnique({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n select: { lastActiveAt: true },\n });\n return row?.lastActiveAt ?? null;\n },\n async recordActivity({ userId, oauthClientId, clientName, host, at }) {\n const prisma = await getPrisma();\n await prisma.mcpConnection.upsert({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n create: { userId, oauthClientId, clientName, host, connectedAt: at, lastActiveAt: at },\n // Never blank a known host on refresh — keep the existing attribution\n // when this grant cannot derive one.\n update: {\n clientName,\n lastActiveAt: at,\n revokedAt: null,\n ...(host ? { host } : {}),\n },\n });\n },\n async listActive(userId) {\n const prisma = await getPrisma();\n const rows = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { ...CONNECTION_SELECT },\n });\n return rows as unknown as StoredMcpConnection[];\n },\n revokeByHost: (userId, host) => revokeByHost(getPrisma, userId, host),\n announce: (userId, host) => announce(getPrisma, userId, host),\n };\n}\n\n/**\n * Disconnect one provider's connections — and ONLY that provider's.\n *\n * Every read and write is scoped by `userId`: a connection is per-user (an MCP\n * bearer is not tenant-scoped), so the user id IS the isolation here, and the\n * `id` list passed to the update comes from a query that already applied it.\n */\nasync function revokeByHost(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<string[]> {\n const prisma = await getPrisma();\n const attributed = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host },\n select: { id: true, oauthClientId: true },\n });\n // A legacy `host = null` row is claimed only when the provider has no row of\n // its own: pre-attribution connections must stay disconnectable, but a provider\n // that DID attribute can never revoke another assistant's row.\n const targets =\n attributed.length > 0\n ? attributed\n : await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host: null },\n select: { id: true, oauthClientId: true },\n });\n if (targets.length === 0) return [];\n await prisma.mcpConnection.updateMany({\n where: { id: { in: targets.map((row) => String(row.id)) } },\n data: { revokedAt: new Date() },\n });\n return targets.map((row) => String(row.oauthClientId));\n}\n\n/** A provider's self-report: refresh its own row, or claim the unattributed one. */\nasync function announce(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<number> {\n const prisma = await getPrisma();\n const now = new Date();\n const refreshed = await prisma.mcpConnection.updateMany({\n where: { userId, revokedAt: null, host },\n data: { lastActiveAt: now, revokedAt: null },\n });\n if (refreshed.count > 0) return refreshed.count;\n\n // No row for this provider yet — attribute the just-connected one. Scoped by\n // user, so a self-report can never reach another account's connection.\n const candidate = await prisma.mcpConnection.findFirst({\n where: { userId, revokedAt: null, host: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { id: true },\n });\n if (!candidate) return 0;\n await prisma.mcpConnection.update({\n where: { id: candidate.id },\n data: { host, lastActiveAt: now, revokedAt: null },\n });\n return 1;\n}\n\n/** Every port, over one lazily-resolved Prisma client. */\nexport function createPrismaMcpStores(getPrisma: McpOauthPrismaProvider): McpOauthStores {\n return {\n clients: clientStore(getPrisma),\n refreshTokens: refreshTokenStore(getPrisma),\n connections: connectionStore(getPrisma),\n };\n}\n","import { providerForHostId, type AiProvider } from \"../guide\";\nimport type {\n McpConnectionStore,\n RefreshTokenStore,\n StoredMcpConnection,\n} from \"./stores\";\n\n/**\n * The account surface's connection OPERATIONS (12-48) — the half of the\n * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather\n * than host vocabulary.\n *\n * The ROUTE stays in the host on purpose: it mixes the host's session\n * resolution, its response envelope, its published plugin URLs and its logger,\n * and injecting all four here would make the config surface bigger than the\n * handler it replaces. What must NOT stay in each host is the disconnect's\n * both-halves rule, because getting it half right LOOKS right:\n *\n * `connections.revokeByHost` ends the connection rows and returns the OAuth\n * client ids behind them — and a host that stops there has revoked nothing that\n * matters. The assistant still holds a live refresh token for each of those\n * clients, rotates it on schedule, and the very next grant records fresh\n * activity: the card the user just disconnected lights green again on its own.\n * So the rule is one function: revoke the rows AND end every live refresh token\n * of each returned client, in the same call, with no way to import one half\n * without the other.\n *\n * Deliberately NOT invalidated here: the assistant's current ACCESS token.\n * Those are self-contained JWTs the server does not track; a just-disconnected\n * host keeps working for at most their TTL (15 minutes by default) and can then\n * obtain nothing further.\n */\n\n/** An active AI connection, narrowed for display. */\nexport interface AiConnectionSnapshot {\n oauthClientId: string;\n clientName: string | null;\n /** The provider this connection is attributed to (`null` = pre-attribution). */\n host: AiProvider | null;\n connectedAt: Date;\n lastActiveAt: Date;\n}\n\n/** The caller the operations act for — always the session's own user. */\nexport interface AiConnectionCaller {\n /** The host's user id — what `mcp_connections` rows are keyed by. */\n userId: string;\n /** The identity refresh tokens are bound to (the AS binds by email). */\n email: string;\n}\n\n/** What one disconnect actually ended, for the host's log and response. */\nexport interface AiDisconnectResult {\n /** OAuth client ids whose connection rows were revoked. */\n disconnectedClientIds: string[];\n /** Live refresh tokens ended across those clients — the half that cuts access. */\n revokedRefreshTokens: number;\n}\n\n/** Narrow a stored `host` string to a known provider, or `null`. */\nfunction asProvider(host: string | null): AiProvider | null {\n return host === null ? null : providerForHostId(host);\n}\n\n/**\n * A user's active connections, most-recently-active first, with the stored open\n * `host` string narrowed to the package's closed {@link AiProvider} union — the\n * store cannot know which assistants have screens, but the union is this\n * package's own vocabulary (`guide.ts`), so the narrowing lives beside it\n * rather than being re-derived in every host.\n */\nexport async function listAiConnections(\n connections: McpConnectionStore,\n userId: string,\n): Promise<AiConnectionSnapshot[]> {\n const rows: StoredMcpConnection[] = await connections.listActive(userId);\n return rows.map((row) => ({ ...row, host: asProvider(row.host) }));\n}\n\n/**\n * Disconnect one provider for this user — BOTH halves, atomically from the\n * caller's point of view (see the module doc for why one half alone is a\n * disconnect that undoes itself).\n *\n * Idempotent: disconnecting a provider that was never connected returns zero\n * counts rather than failing, so a double-click is harmless. Repeat calls also\n * report zero — `revokeLiveForClient` skips already-revoked tokens by contract.\n */\nexport async function disconnectAiHost(\n stores: { connections: McpConnectionStore; refreshTokens: RefreshTokenStore },\n caller: AiConnectionCaller,\n host: AiProvider,\n): Promise<AiDisconnectResult> {\n const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);\n const revoked = await Promise.all(\n disconnectedClientIds.map((clientId) =>\n stores.refreshTokens.revokeLiveForClient(caller.email, clientId),\n ),\n );\n return {\n disconnectedClientIds,\n revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { OnboardingStore, OnboardingStateSnapshot } from '@12-apps/onboarding';
|
|
2
|
+
import { f as AiHostGuide, h as AiProvider, b as AiCapability, d as AiHostBrand } from '../guide-DV5MQbCg.js';
|
|
3
|
+
export { A as AI_CAPABILITIES, a as AI_PERMISSION_MODEL, c as AiConnectPromptSpec, e as AiHostConfigureStage, g as AiHostLink, i as aiConnectPrompt, j as aiHostGuides, p as providerForHostId } from '../guide-DV5MQbCg.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Revoke a host's access. Returning a promise holds the confirmation popup in
|
|
7
|
+
* its pending state until the write settles, and a rejection keeps it open
|
|
8
|
+
* carrying the reason — so the owner sees the disconnect finish or fail.
|
|
9
|
+
*/
|
|
10
|
+
type DisconnectHandler = (hostId: string) => void | Promise<unknown>;
|
|
11
|
+
/** One assistant's connection state. */
|
|
12
|
+
interface HostStatus {
|
|
13
|
+
host: AiHostGuide;
|
|
14
|
+
connected: boolean;
|
|
15
|
+
/** Freeform activity line for a connected host (e.g. "ativo há 3 min"). */
|
|
16
|
+
detail?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The completed-state status board for the AI integration: every assistant as a
|
|
20
|
+
* box — green when connected, red when still to connect (with a "Conectar"
|
|
21
|
+
* button that re-enters the guided flow for that host). Each connection is
|
|
22
|
+
* attributed to its provider (derived from the OAuth client + confirmed by
|
|
23
|
+
* `announceAiConnection`), so several assistants can show connected at once.
|
|
24
|
+
*
|
|
25
|
+
* A connected box carries two more controls, because the pill it used to show
|
|
26
|
+
* alone was a dead end in both directions: "Instruções" re-enters the host's
|
|
27
|
+
* setup steps (previously reachable only from a RED card, so an all-green board
|
|
28
|
+
* hid them entirely), and "Desconectar" — when the app passes `onDisconnect` —
|
|
29
|
+
* revokes access, which nothing in the UI could do at all.
|
|
30
|
+
*/
|
|
31
|
+
declare function AiStatusBoard({ statuses, onConnect, onDisconnect, }: {
|
|
32
|
+
statuses: readonly HostStatus[];
|
|
33
|
+
onConnect: (hostId: string) => void;
|
|
34
|
+
/** Revoke this host's access. Omit to render a read-only board. */
|
|
35
|
+
onDisconnect?: DisconnectHandler;
|
|
36
|
+
}): React.JSX.Element;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Pure helpers + the connection type shared by the AI-onboarding flow steps
|
|
40
|
+
* (`ai-flow-steps.tsx`) and the orchestration/status board (`ai-steps.tsx`),
|
|
41
|
+
* kept here so neither JSX module has to import the other (no import cycle).
|
|
42
|
+
*/
|
|
43
|
+
/** The live connection surfaced from the server (empty list when none). */
|
|
44
|
+
interface AiConnection {
|
|
45
|
+
clientName: string | null;
|
|
46
|
+
/**
|
|
47
|
+
* The provider this connection is attributed to (derived server-side from the
|
|
48
|
+
* OAuth client's redirect URIs / confirmed by `announceAiConnection`). `null`
|
|
49
|
+
* for a legacy connection recorded before attribution existed — matched
|
|
50
|
+
* best-effort to whichever host the owner is connecting.
|
|
51
|
+
*/
|
|
52
|
+
host: AiProvider | null;
|
|
53
|
+
lastActiveAt: Date;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Props for the reusable AI-connect onboarding flow. */
|
|
57
|
+
interface AiIntegrationOnboardingProps {
|
|
58
|
+
/** Persistence seam — the app wires this to its own backend (server actions). */
|
|
59
|
+
store: OnboardingStore;
|
|
60
|
+
/** The MCP endpoint URL to paste (derived by the app from its public origin). */
|
|
61
|
+
endpointUrl: string;
|
|
62
|
+
/** The owner's saved progress (null → first run / landing). */
|
|
63
|
+
initialState: OnboardingStateSnapshot | null;
|
|
64
|
+
/** The live MCP connections (one per connected assistant; empty when none). */
|
|
65
|
+
connections: readonly AiConnection[];
|
|
66
|
+
/** Onboarding feature key (persistence namespace). @default "ai_integration" */
|
|
67
|
+
featureKey?: string;
|
|
68
|
+
/** Show the dev-only "reset onboarding" button. @default false */
|
|
69
|
+
devReset?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* The platform operating this MCP server, as its OAuth consent button names
|
|
72
|
+
* it. REQUIRED, and it is the reason `hosts` can have a default at all: one
|
|
73
|
+
* ChatGPT step tells the owner which "Sign in with …" button to click, and
|
|
74
|
+
* that button carries whoever runs the server. It used to be a hard-coded
|
|
75
|
+
* name — of a single STORE on one deployment, not even the product — so every
|
|
76
|
+
* other adopter pointed its owners at a button that does not exist.
|
|
77
|
+
*/
|
|
78
|
+
platformName: string;
|
|
79
|
+
/** Assistants offered in the flow. @default aiHostGuides(platformName) */
|
|
80
|
+
hosts?: readonly AiHostGuide[];
|
|
81
|
+
/** Capability cards on the landing. @default the shared AI_CAPABILITIES */
|
|
82
|
+
capabilities?: readonly AiCapability[];
|
|
83
|
+
/** Permission reassurance copy on the landing. @default AI_PERMISSION_MODEL */
|
|
84
|
+
permissionModel?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Message the owner pastes into the assistant on the Conectar step.
|
|
87
|
+
*
|
|
88
|
+
* REQUIRED, with no default, because the useful version of it names TOOLS —
|
|
89
|
+
* one to register the connection, one to read something real — and this
|
|
90
|
+
* package neither defines nor serves any. It shipped a constant naming two
|
|
91
|
+
* tools from one adopter's surface, so another host handed its owner a prompt
|
|
92
|
+
* that called two things that did not exist, and the confirm step then waited
|
|
93
|
+
* forever for a registration that could never happen. Build it with
|
|
94
|
+
* `aiConnectPrompt({ … })`.
|
|
95
|
+
*/
|
|
96
|
+
connectPrompt: string;
|
|
97
|
+
/**
|
|
98
|
+
* Re-check the live connection on the verify step's "Testar conexão" button —
|
|
99
|
+
* apps pass a router refresh (e.g. Next's `router.refresh`). @default a full
|
|
100
|
+
* `window.location.reload()`.
|
|
101
|
+
*/
|
|
102
|
+
onRetest?: () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Revoke a connected assistant's access, from the completed status board. The
|
|
105
|
+
* host ID is the board's card, NOT the connection — `claude` and
|
|
106
|
+
* `claude-desktop` share one provider, so the app resolves which stored
|
|
107
|
+
* connection it owns (`providerForHostId`) and revokes that. Omitted → the
|
|
108
|
+
* board stays read-only, which is the pre-existing behavior.
|
|
109
|
+
*/
|
|
110
|
+
onDisconnect?: DisconnectHandler;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* URL-free, persisted onboarding for the AI/MCP integration — a five-step wizard
|
|
114
|
+
* (pick an assistant, copy the store URL, configure the connector, connect, then
|
|
115
|
+
* verify) whose position + chosen assistant are saved via `@12-apps/onboarding`, so
|
|
116
|
+
* a refresh resumes exactly where the owner left off. The live MCP connection
|
|
117
|
+
* signal drives the verify step and the completed status board.
|
|
118
|
+
*
|
|
119
|
+
* App-agnostic: the app supplies the persistence `store`, the `endpointUrl`, and
|
|
120
|
+
* the live `connections` (one per connected assistant); content (hosts,
|
|
121
|
+
* capabilities, copy) defaults to the shared guide but can be overridden per app.
|
|
122
|
+
*/
|
|
123
|
+
declare function AiIntegrationOnboarding({ store, endpointUrl, initialState, connections, featureKey, devReset, platformName, hosts, capabilities, permissionModel, connectPrompt, onRetest, onDisconnect, }: AiIntegrationOnboardingProps): React.JSX.Element;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Homepage-style marketing landing for the AI integration (shown before the
|
|
127
|
+
* owner starts): a hero, the permission reassurance, a trust/feature strip, and
|
|
128
|
+
* the capability highlights. `onStart` begins the guided flow. `permissionModel`
|
|
129
|
+
* and `capabilities` default to the shared copy; apps can override.
|
|
130
|
+
*/
|
|
131
|
+
declare function AiLanding({ onStart, permissionModel, capabilities, }: {
|
|
132
|
+
onStart: () => void;
|
|
133
|
+
permissionModel?: string;
|
|
134
|
+
capabilities?: readonly AiCapability[];
|
|
135
|
+
}): React.JSX.Element;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Marketing block for the AI integration: a headline + a responsive grid of
|
|
139
|
+
* capability cards. Shown on the onboarding landing (before the owner starts) to
|
|
140
|
+
* sell the feature — "here's what the assistant does for you". `capabilities`
|
|
141
|
+
* defaults to the shared set; apps can pass their own.
|
|
142
|
+
*/
|
|
143
|
+
declare function AiCapabilities({ capabilities, }: {
|
|
144
|
+
capabilities?: readonly AiCapability[];
|
|
145
|
+
}): React.JSX.Element;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Step 1 of the guided AI connect flow: the owner picks a single assistant
|
|
149
|
+
* (Claude.ai, Claude Desktop, ChatGPT, Codex) from a card grid. Picking a card
|
|
150
|
+
* advances straight to the next step — no separate "Próximo" button. Selection
|
|
151
|
+
* is controlled (persisted by the onboarding flow) so a refresh resumes with the
|
|
152
|
+
* same assistant highlighted.
|
|
153
|
+
*/
|
|
154
|
+
declare function HostSelectStep({ hosts, selectedId, onSelect, }: {
|
|
155
|
+
hosts: readonly AiHostGuide[];
|
|
156
|
+
selectedId: string | null;
|
|
157
|
+
onSelect: (hostId: string) => void;
|
|
158
|
+
}): React.JSX.Element;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The store's MCP endpoint URL with a blue "Copiar" button (the display-only
|
|
162
|
+
* field defers the copy to this button). Controlled: `copied` flips the label to
|
|
163
|
+
* "Copiado" and `onCopy` fires after the clipboard write — the wizard uses it to
|
|
164
|
+
* unlock the "Próximo" button on the Copiar-URL step.
|
|
165
|
+
*/
|
|
166
|
+
declare function EndpointCopyBlock({ endpointUrl, copied, onCopy, }: {
|
|
167
|
+
endpointUrl: string;
|
|
168
|
+
copied: boolean;
|
|
169
|
+
onCopy: () => void;
|
|
170
|
+
}): React.JSX.Element;
|
|
171
|
+
/**
|
|
172
|
+
* A copyable multi-line message block (preserves line breaks). Used on the
|
|
173
|
+
* Conectar step for the prompt the owner pastes into the assistant so it
|
|
174
|
+
* identifies itself and calls a tool (which registers the connection).
|
|
175
|
+
*/
|
|
176
|
+
declare function PromptCopyBlock({ title, caption, message, }: {
|
|
177
|
+
title: string;
|
|
178
|
+
caption: string;
|
|
179
|
+
message: string;
|
|
180
|
+
}): React.JSX.Element;
|
|
181
|
+
/** The brand mark + "Conectar no <host>" header shared by the connect sub-steps. */
|
|
182
|
+
declare function HostConnectHeader({ host }: {
|
|
183
|
+
host: AiHostGuide;
|
|
184
|
+
}): React.JSX.Element;
|
|
185
|
+
/**
|
|
186
|
+
* The primary direct-link button to a host's connector settings (nothing when it
|
|
187
|
+
* has none). `onOpen` fires after the link opens so the Configurar step can
|
|
188
|
+
* unlock its "Próximo" only once the owner has actually gone to the connectors.
|
|
189
|
+
*/
|
|
190
|
+
declare function HostOpenButton({ host, onOpen, }: {
|
|
191
|
+
host: AiHostGuide;
|
|
192
|
+
onOpen?: () => void;
|
|
193
|
+
}): React.JSX.Element | null;
|
|
194
|
+
/**
|
|
195
|
+
* A numbered slice of a host's connect steps. `start` keeps the numbering
|
|
196
|
+
* continuous when the steps are split across wizard stages (e.g. the Conectar
|
|
197
|
+
* stage continues at 4 after Configurar showed 1–3).
|
|
198
|
+
*/
|
|
199
|
+
declare function HostStepList({ steps, start, }: {
|
|
200
|
+
steps: readonly string[];
|
|
201
|
+
start?: number;
|
|
202
|
+
}): React.JSX.Element;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The store's MCP endpoint URL. Client component because the copy interaction
|
|
206
|
+
* needs the browser clipboard — the `@12-apps/ui` Code component provides the copy
|
|
207
|
+
* affordance (`copyable`). Pass `copyable={false}` for a display-only field when
|
|
208
|
+
* an external control owns the copy action (e.g. the connect flow's blue Copiar
|
|
209
|
+
* button, which also gates the following steps on the copy).
|
|
210
|
+
*/
|
|
211
|
+
declare function McpEndpointUrl({ url, copyable, }: {
|
|
212
|
+
url: string;
|
|
213
|
+
copyable?: boolean;
|
|
214
|
+
}): React.JSX.Element;
|
|
215
|
+
|
|
216
|
+
/** A round brand chip: the vendor accent + a simple glyph. */
|
|
217
|
+
declare function HostBrandAvatar({ brand, size, }: {
|
|
218
|
+
brand: AiHostBrand;
|
|
219
|
+
size?: number;
|
|
220
|
+
}): React.JSX.Element;
|
|
221
|
+
/** The icon for a capability card, by capability id (falls back to a sparkle). */
|
|
222
|
+
declare function CapabilityIcon({ id, fontSize, }: {
|
|
223
|
+
id: string;
|
|
224
|
+
fontSize?: number;
|
|
225
|
+
}): React.JSX.Element;
|
|
226
|
+
|
|
227
|
+
/** One entry of a landing's trust/feature strip. */
|
|
228
|
+
interface FeatureBadgeItem {
|
|
229
|
+
icon: React.ReactNode;
|
|
230
|
+
label: string;
|
|
231
|
+
caption: string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* One item of an onboarding landing's trust/feature strip: a paper icon tile
|
|
235
|
+
* (primary-coloured glyph) with a two-line label + caption.
|
|
236
|
+
*/
|
|
237
|
+
declare function FeatureBadge({ icon, label, caption }: FeatureBadgeItem): React.JSX.Element;
|
|
238
|
+
|
|
239
|
+
export { AiCapabilities, AiCapability, type AiConnection, AiHostBrand, AiHostGuide, AiIntegrationOnboarding, type AiIntegrationOnboardingProps, AiLanding, AiProvider, AiStatusBoard, CapabilityIcon, type DisconnectHandler, EndpointCopyBlock, FeatureBadge, type FeatureBadgeItem, HostBrandAvatar, HostConnectHeader, HostOpenButton, HostSelectStep, type HostStatus, HostStepList, McpEndpointUrl, PromptCopyBlock };
|