@agent-native/core 0.174.0 → 0.174.1-nightly-20260825180834

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.
@@ -9,6 +9,7 @@ import {
9
9
  listProviderApiCatalog,
10
10
  } from "@agent-native/core/provider-api";
11
11
  import {
12
+ CredentialStoreUnavailableError,
12
13
  hasWorkspaceProviderOAuthCredentials,
13
14
  isGoogleWorkspaceOAuthProvider,
14
15
  } from "@agent-native/core/server";
@@ -50,7 +51,7 @@ type GrantSummary = {
50
51
  lastUsedAt?: string | null;
51
52
  };
52
53
 
53
- type GoogleOAuthAvailability = "configured" | "unconfigured";
54
+ type GoogleOAuthAvailability = "configured" | "unconfigured" | "unavailable";
54
55
 
55
56
  function unique(values: string[]) {
56
57
  return Array.from(
@@ -129,10 +130,17 @@ export default defineAction({
129
130
  | WorkspaceConnectionTemplateUse
130
131
  | undefined,
131
132
  });
132
- const googleOAuthAvailability: GoogleOAuthAvailability =
133
- (await hasWorkspaceProviderOAuthCredentials("gmail"))
133
+ let googleOAuthAvailability: GoogleOAuthAvailability;
134
+ try {
135
+ googleOAuthAvailability = (await hasWorkspaceProviderOAuthCredentials(
136
+ "gmail",
137
+ ))
134
138
  ? "configured"
135
139
  : "unconfigured";
140
+ } catch (error) {
141
+ if (!(error instanceof CredentialStoreUnavailableError)) throw error;
142
+ googleOAuthAvailability = "unavailable";
143
+ }
136
144
  const googleOAuthConfigured = googleOAuthAvailability === "configured";
137
145
  const providers = catalogProviders.filter(
138
146
  (provider) =>
@@ -274,7 +282,7 @@ export default defineAction({
274
282
  availability: {
275
283
  googleOAuth: {
276
284
  status: googleOAuthAvailability,
277
- retryable: false,
285
+ retryable: googleOAuthAvailability === "unavailable",
278
286
  },
279
287
  },
280
288
  providers: providersWithReadiness,
@@ -221,6 +221,12 @@ interface WorkspaceConnectionGrantSummary {
221
221
  }
222
222
 
223
223
  interface WorkspaceConnectionsResponse {
224
+ availability?: {
225
+ googleOAuth: {
226
+ status: "configured" | "unconfigured" | "unavailable";
227
+ retryable: boolean;
228
+ };
229
+ };
224
230
  providers: WorkspaceConnectionProvider[];
225
231
  connections: WorkspaceConnection[];
226
232
  grants: WorkspaceConnectionGrant[];
@@ -2719,6 +2725,9 @@ export default function WorkspaceIntegrationsRoute() {
2719
2725
  EMPTY_RESPONSE) as WorkspaceConnectionsResponse;
2720
2726
  const providers = data.providers;
2721
2727
  const connections = data.connections;
2728
+ const googleOAuthUnavailable =
2729
+ data.availability?.googleOAuth.status === "unavailable" &&
2730
+ data.availability.googleOAuth.retryable;
2722
2731
  const apps = (appsQuery.data ?? []) as WorkspaceAppSummary[];
2723
2732
  const groups = (groupsQuery.data ?? []) as WorkspaceUserGroup[];
2724
2733
  const providersById = useMemo(
@@ -3152,6 +3161,9 @@ export default function WorkspaceIntegrationsRoute() {
3152
3161
  }}
3153
3162
  />
3154
3163
  ) : null}
3164
+ {googleOAuthUnavailable ? (
3165
+ <ActionQueryError onRetry={() => void connectionsQuery.refetch()} />
3166
+ ) : null}
3155
3167
  {!connectionsQuery.isError && !appsQuery.isError ? (
3156
3168
  <>
3157
3169
  {connectionsQuery.isLoading ? (
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
62
62
  error: string;
63
63
  states?: undefined;
64
64
  } | {
65
- error?: undefined;
66
65
  states: {
67
66
  clientId: number;
68
67
  state: string;
69
68
  }[];
69
+ error?: undefined;
70
70
  }>>;
71
71
  /**
72
72
  * GET /_agent-native/collab/:docId/users
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
77
77
  error: string;
78
78
  users?: undefined;
79
79
  } | {
80
- error?: undefined;
81
80
  users: {
82
81
  clientId: number;
83
82
  lastSeen: number;
84
83
  }[];
84
+ error?: undefined;
85
85
  }>>;
@@ -13,8 +13,8 @@
13
13
  * Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
14
14
  */
15
15
  export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
16
- ok?: undefined;
17
16
  error: string;
17
+ ok?: undefined;
18
18
  } | {
19
19
  error?: undefined;
20
20
  ok: boolean;
@@ -24,7 +24,17 @@ export interface McpOAuthRoutesOptions {
24
24
  }
25
25
  export declare function redirectWithStagedCookies(event: H3Event, location: string): Response;
26
26
  export declare function mountMcpOAuthRoutes(nitroApp: any, options: McpOAuthRoutesOptions): void;
27
- export declare function resolveMcpOAuthScope(serverUrl: URL, requestedScope: unknown): RemoteMcpScope | null;
27
+ export declare function resolveMcpOAuthStartError(error: unknown): {
28
+ status: 400 | 503;
29
+ body: {
30
+ error: string;
31
+ errorCode?: string;
32
+ retryable?: boolean;
33
+ };
34
+ };
35
+ export declare function resolveMcpOAuthScope(serverUrl: URL, requestedScope: unknown, options?: {
36
+ allowManagedOrgReconnect?: boolean;
37
+ }): RemoteMcpScope | null;
28
38
  export declare function stripMcpOAuthAppBasePath(path: string, basePath: string): string;
29
39
  export declare function resolveManagedMcpOAuthClient(serverUrl: URL): Promise<StoredOAuthClientInformation | undefined>;
30
40
  export declare function setMcpOAuthFlowCookie(event: H3Event, flow: McpOAuthFlow, secure: boolean): void;
@@ -3,7 +3,7 @@ import { deleteCookie, defineEventHandler, getChunkedCookie, getCookie, getMetho
3
3
  import { getOrgContext } from "../org/context.js";
4
4
  import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
5
5
  import { getSession, safeReturnPath } from "../server/auth.js";
6
- import { resolveSecret } from "../server/credential-provider.js";
6
+ import { CredentialStoreUnavailableError, resolveSecretPair, } from "../server/credential-provider.js";
7
7
  import { getH3App } from "../server/framework-request-handler.js";
8
8
  import { getAppBasePath, getAppUrl, resolveOAuthRedirectUri, } from "../server/google-oauth.js";
9
9
  import { runWithRequestContext } from "../server/request-context.js";
@@ -16,17 +16,25 @@ const FLOW_COOKIE_MAX_CHUNKS = 8;
16
16
  const CHUNKED_COOKIE_PREFIX = "__chunked__";
17
17
  const MANAGED_MCP_OAUTH_CLIENTS = [
18
18
  {
19
- serverOrigin: "https://mcp.hubspot.com",
20
- clientIdKeys: [
21
- "HUBSPOT_MCP_CLIENT_ID",
22
- "HUBSPOT_INTEGRATION_CLIENT_ID",
23
- "HUBSPOT_CLIENT_ID",
19
+ serverOrigins: ["https://mcp.hubspot.com"],
20
+ credentialPairs: [
21
+ ["HUBSPOT_MCP_CLIENT_ID", "HUBSPOT_MCP_CLIENT_SECRET"],
22
+ ["HUBSPOT_INTEGRATION_CLIENT_ID", "HUBSPOT_INTEGRATION_CLIENT_SECRET"],
23
+ ["HUBSPOT_CLIENT_ID", "HUBSPOT_CLIENT_SECRET"],
24
24
  ],
25
- clientSecretKeys: [
26
- "HUBSPOT_MCP_CLIENT_SECRET",
27
- "HUBSPOT_INTEGRATION_CLIENT_SECRET",
28
- "HUBSPOT_CLIENT_SECRET",
25
+ },
26
+ {
27
+ serverOrigins: [
28
+ "https://gmailmcp.googleapis.com",
29
+ "https://drivemcp.googleapis.com",
30
+ "https://docsmcp.googleapis.com",
31
+ "https://sheetsmcp.googleapis.com",
32
+ "https://slidesmcp.googleapis.com",
33
+ "https://calendarmcp.googleapis.com",
34
+ "https://chatmcp.googleapis.com",
35
+ "https://people.googleapis.com",
29
36
  ],
37
+ credentialPairs: [["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"]],
30
38
  },
31
39
  ];
32
40
  export function redirectWithStagedCookies(event, location) {
@@ -111,7 +119,9 @@ async function handleMcpOAuthStart(event) {
111
119
  setResponseStatus(event, 400);
112
120
  return { error: "MCP server name is invalid." };
113
121
  }
114
- const requestedScope = resolveMcpOAuthScope(urlCheck.url, query.scope);
122
+ const requestedScope = resolveMcpOAuthScope(urlCheck.url, query.scope, {
123
+ allowManagedOrgReconnect: reconnectScope === "org" && Boolean(reconnectServer),
124
+ });
115
125
  if (!requestedScope) {
116
126
  setResponseStatus(event, 400);
117
127
  return {
@@ -165,7 +175,7 @@ async function handleMcpOAuthStart(event) {
165
175
  if (!started) {
166
176
  setResponseStatus(event, 400);
167
177
  return {
168
- error: "HubSpot personal MCP connect is not configured for this workspace. A workspace owner must register the HubSpot MCP Auth App once; after that, any workspace member can connect a personal account.",
178
+ error: "Managed MCP OAuth is not configured for this workspace. A workspace owner must register the OAuth client once; after that, any workspace member can connect a personal account.",
169
179
  };
170
180
  }
171
181
  const flow = {
@@ -190,18 +200,37 @@ async function handleMcpOAuthStart(event) {
190
200
  setMcpOAuthFlowCookie(event, flow, redirectUri.startsWith("https://"));
191
201
  return redirectWithStagedCookies(event, started.authorizationUrl.href);
192
202
  }
193
- catch {
194
- setResponseStatus(event, 400);
203
+ catch (error) {
204
+ const failure = resolveMcpOAuthStartError(error);
205
+ setResponseStatus(event, failure.status);
206
+ return failure.body;
207
+ }
208
+ }
209
+ export function resolveMcpOAuthStartError(error) {
210
+ if (error instanceof CredentialStoreUnavailableError) {
195
211
  return {
196
- error: "This MCP server could not start OAuth. It may not support standard MCP OAuth discovery or dynamic client registration.",
212
+ status: 503,
213
+ body: {
214
+ error: error.message,
215
+ errorCode: error.errorCode,
216
+ retryable: error.retryable,
217
+ },
197
218
  };
198
219
  }
220
+ return {
221
+ status: 400,
222
+ body: {
223
+ error: "This MCP server could not start OAuth. It may not support standard MCP OAuth discovery or dynamic client registration.",
224
+ },
225
+ };
199
226
  }
200
227
  function isManagedMcpOAuthServer(serverUrl) {
201
- return MANAGED_MCP_OAUTH_CLIENTS.some((client) => client.serverOrigin === serverUrl.origin);
228
+ return MANAGED_MCP_OAUTH_CLIENTS.some((client) => client.serverOrigins.includes(serverUrl.origin));
202
229
  }
203
- export function resolveMcpOAuthScope(serverUrl, requestedScope) {
204
- if (isManagedMcpOAuthServer(serverUrl) && requestedScope === "org") {
230
+ export function resolveMcpOAuthScope(serverUrl, requestedScope, options) {
231
+ if (isManagedMcpOAuthServer(serverUrl) &&
232
+ requestedScope === "org" &&
233
+ !options?.allowManagedOrgReconnect) {
205
234
  return null;
206
235
  }
207
236
  return requestedScope === "org" ? "org" : "user";
@@ -220,15 +249,13 @@ export function stripMcpOAuthAppBasePath(path, basePath) {
220
249
  : path;
221
250
  }
222
251
  export async function resolveManagedMcpOAuthClient(serverUrl) {
223
- const client = MANAGED_MCP_OAUTH_CLIENTS.find((candidate) => candidate.serverOrigin === serverUrl.origin);
252
+ const client = MANAGED_MCP_OAUTH_CLIENTS.find((candidate) => candidate.serverOrigins.includes(serverUrl.origin));
224
253
  if (!client)
225
254
  return undefined;
226
- for (let index = 0; index < client.clientIdKeys.length; index += 1) {
227
- const [clientId, clientSecret] = await Promise.all([
228
- resolveSecret(client.clientIdKeys[index]),
229
- resolveSecret(client.clientSecretKeys[index]),
230
- ]);
231
- if (clientId && clientSecret) {
255
+ for (const [clientIdKey, clientSecretKey] of client.credentialPairs) {
256
+ const credentials = await resolveSecretPair([clientIdKey, clientSecretKey]);
257
+ if (credentials) {
258
+ const [clientId, clientSecret] = credentials;
232
259
  return {
233
260
  client_id: clientId,
234
261
  client_secret: clientSecret,
@@ -62,6 +62,6 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
62
62
  summary?: undefined;
63
63
  spans?: undefined;
64
64
  id?: undefined;
65
- error?: undefined;
66
65
  ok: boolean;
66
+ error?: undefined;
67
67
  }>>;
@@ -401,6 +401,11 @@ export declare function prefetchSecrets(keys: readonly string[]): Promise<void>;
401
401
  * Resolving several keys in one request? Call `prefetchSecrets` first.
402
402
  */
403
403
  export declare function resolveSecret(key: string): Promise<string | null>;
404
+ /**
405
+ * Resolve two related secrets from one scope/source. A complete lower-
406
+ * precedence pair wins over mixing a partial override with another source.
407
+ */
408
+ export declare function resolveSecretPair(keys: readonly [string, string]): Promise<[string, string] | null>;
404
409
  /**
405
410
  * `resolveSecret` without the throw: reports whether the miss is definitive
406
411
  * (`lookupFailed: false` — no such row anywhere the caller can reach) or just
@@ -1235,6 +1235,90 @@ export async function resolveSecret(key) {
1235
1235
  assertCredentialStoreReadable(resolved);
1236
1236
  return null;
1237
1237
  }
1238
+ /**
1239
+ * Resolve two related secrets from one scope/source. A complete lower-
1240
+ * precedence pair wins over mixing a partial override with another source.
1241
+ */
1242
+ export async function resolveSecretPair(keys) {
1243
+ const [firstKey, secondKey] = keys;
1244
+ const readPair = async (scope, scopeId) => {
1245
+ const { readAppSecrets } = await import("../secrets/storage.js");
1246
+ const secrets = await readAppSecrets({ keys, scope, scopeId });
1247
+ const first = secrets.get(firstKey)?.value;
1248
+ const second = secrets.get(secondKey)?.value;
1249
+ return first && second ? [first, second] : null;
1250
+ };
1251
+ const readEnvironmentPair = () => {
1252
+ if (!canUseDeployCredentialFallbackForRequest(firstKey) ||
1253
+ !canUseDeployCredentialFallbackForRequest(secondKey)) {
1254
+ return null;
1255
+ }
1256
+ const first = process.env[firstKey];
1257
+ const second = process.env[secondKey];
1258
+ return first && second ? [first, second] : null;
1259
+ };
1260
+ const email = getRequestUserEmail();
1261
+ if (!email)
1262
+ return readEnvironmentPair();
1263
+ let lookupFailed = false;
1264
+ let cause;
1265
+ try {
1266
+ let pair = await readPair("user", email);
1267
+ if (pair)
1268
+ return pair;
1269
+ let orgId = getRequestOrgId();
1270
+ if (!orgId) {
1271
+ const resolved = await resolveOrgIdForRequestEmail(email);
1272
+ cause = resolved.cause;
1273
+ lookupFailed = cause !== undefined;
1274
+ orgId = resolved.orgId;
1275
+ }
1276
+ if (lookupFailed) {
1277
+ const environmentPair = readEnvironmentPair();
1278
+ if (environmentPair)
1279
+ return environmentPair;
1280
+ assertCredentialStoreReadable({ lookupFailed, cause });
1281
+ return null;
1282
+ }
1283
+ if (orgId) {
1284
+ pair = await readPair("org", orgId);
1285
+ if (pair)
1286
+ return pair;
1287
+ pair = await readPair("workspace", orgId);
1288
+ if (pair)
1289
+ return pair;
1290
+ }
1291
+ pair = await readPair("workspace", `solo:${email}`);
1292
+ if (pair)
1293
+ return pair;
1294
+ const vaultOrgId = process.env.AGENT_VAULT_ORG_ID?.trim();
1295
+ if (vaultOrgId && vaultOrgId !== orgId) {
1296
+ const access = await Promise.all(keys.map((key) => canReadDesignatedVaultFallback(vaultOrgId, key)));
1297
+ const unavailable = access.find((result) => result.status === "unavailable");
1298
+ if (unavailable?.status === "unavailable") {
1299
+ lookupFailed = true;
1300
+ cause = unavailable.cause;
1301
+ }
1302
+ else if (access.every((result) => result.status === "allowed")) {
1303
+ pair = await readPair("org", vaultOrgId);
1304
+ if (pair)
1305
+ return pair;
1306
+ pair = await readPair("workspace", vaultOrgId);
1307
+ if (pair)
1308
+ return pair;
1309
+ }
1310
+ }
1311
+ }
1312
+ catch (error) {
1313
+ lookupFailed = true;
1314
+ cause = error;
1315
+ }
1316
+ const environmentPair = readEnvironmentPair();
1317
+ if (environmentPair)
1318
+ return environmentPair;
1319
+ assertCredentialStoreReadable({ lookupFailed, cause });
1320
+ return null;
1321
+ }
1238
1322
  /**
1239
1323
  * `resolveSecret` without the throw: reports whether the miss is definitive
1240
1324
  * (`lookupFailed: false` — no such row anywhere the caller can reach) or just
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
26
26
  expiresAt?: undefined;
27
27
  ttlSeconds?: undefined;
28
28
  } | {
29
- error?: undefined;
30
29
  token: string;
31
30
  expiresAt: string;
32
31
  ttlSeconds: number;
32
+ error?: undefined;
33
33
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.174.0",
3
+ "version": "0.174.1-nightly-20260825180834",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {