@zackbart/connecta 0.36.1 → 0.36.2
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 +13 -0
- package/bin/version.mjs +1 -1
- package/dist/auth/downstream-oauth.js +3 -0
- package/dist/auth/oauth-authorization-params.js +52 -0
- package/dist/auth/static-oauth.js +1 -4
- package/dist/connectors/option-shapes.js +1 -0
- package/dist/connectors/remote-mcp.d.ts +4 -1
- package/dist/connectors/remote-mcp.js +12 -1
- package/dist/google.d.ts +13 -0
- package/dist/google.js +42 -0
- package/dist/providers/_shared/google/service-account.d.ts +1 -1
- package/dist/providers/_shared/google/service-account.js +20 -8
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/request-token.d.ts +7 -0
- package/dist/request-token.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +13 -12
- package/documentation/architecture.md +24 -2
- package/documentation/auth.md +189 -2
- package/package.json +5 -1
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.36.2 — 2026-10-27
|
|
6
|
+
|
|
7
|
+
Connecta 0.36.2 makes Google's hosted MCP servers, such as BigQuery, usable through generic `remoteMcp()` connections. The new `@zackbart/connecta/google` entry mints Google service-account tokens for request-local auth, so agent pools can share a service-account connection without a browser consent. The service account acts as itself, or as a Workspace user when a delegation subject is given. A rejected token is dropped and re-minted for the next request; dispatched writes are never replayed.
|
|
8
|
+
|
|
9
|
+
Remote MCP OAuth connections can now add browser-only authorization parameters with `auth.authorizationParams`. Personal Google connections set `access_type: "offline"` and `prompt: "consent"` to receive refresh tokens. Protocol and security fields are reserved, and changing the parameters requires reconnecting. API OAuth keeps its existing `authorizationParams` validation.
|
|
10
|
+
|
|
11
|
+
Both additions are opt-in. Deployments that do not use them need no configuration change. The root bundle cap rises to 403,500 B gzip to fit the new validation. Live Google sign-in and BigQuery calls have not been exercised with deployment credentials.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Mint Google service-account tokens for generic remote MCP request auth through `@zackbart/connecta/google`, with optional Workspace delegation and rejected-token cache invalidation.
|
|
16
|
+
- Let remote MCP OAuth connections configure browser-only authorization parameters, including Google's offline-access and consent settings. Protocol and security fields, including `response_mode`, are reserved in every case variant; changing remote MCP authorization parameters requires reconnecting. API OAuth keeps its existing `authorizationParams` validation.
|
|
17
|
+
|
|
5
18
|
## 0.36.1 — 2026-10-27
|
|
6
19
|
|
|
7
20
|
Connecta 0.36.1 adds read-only Granola workspace API access and lets Eve agents select a pool before each model call. Pool changes keep guidance caches separate and prevent replay of parked calls against a different pool. Existing static-pool configuration and public provider tools keep their behavior.
|
package/bin/version.mjs
CHANGED
|
@@ -1972,6 +1972,9 @@ export class KvOAuthProvider {
|
|
|
1972
1972
|
return;
|
|
1973
1973
|
if (!this.allowAuthorization)
|
|
1974
1974
|
throw this.authorizationRefused();
|
|
1975
|
+
for (const [name, value] of this.clientOptions?.authorizationParams ?? []) {
|
|
1976
|
+
authorizationUrl.searchParams.set(name, value);
|
|
1977
|
+
}
|
|
1975
1978
|
this.assertCredentialFree(authorizationUrl.toString(), (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
1976
1979
|
const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
|
|
1977
1980
|
if (!state)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ConfigError } from "../config-schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Protocol and security fields cannot be supplied through remote MCP extensions.
|
|
4
|
+
* `response_mode` stays reserved: callbacks arrive only as GET query parameters.
|
|
5
|
+
*/
|
|
6
|
+
const RESERVED = new Set([
|
|
7
|
+
"response_type",
|
|
8
|
+
"response_mode",
|
|
9
|
+
"client_id",
|
|
10
|
+
"client_secret",
|
|
11
|
+
"redirect_uri",
|
|
12
|
+
"state",
|
|
13
|
+
"code_challenge",
|
|
14
|
+
"code_challenge_method",
|
|
15
|
+
"scope",
|
|
16
|
+
"resource",
|
|
17
|
+
"request",
|
|
18
|
+
"request_uri",
|
|
19
|
+
]);
|
|
20
|
+
/** Validate and snapshot non-secret browser-only parameters at construction. */
|
|
21
|
+
export function authorizationParamsFor(value, path) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
const invalid = (detail) => {
|
|
25
|
+
throw new ConfigError(`${path} ${detail}`);
|
|
26
|
+
};
|
|
27
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
28
|
+
invalid("must be a plain string map.");
|
|
29
|
+
const entries = Object.entries(value);
|
|
30
|
+
if (entries.length > 32)
|
|
31
|
+
invalid("must contain at most 32 parameters.");
|
|
32
|
+
const names = new Set();
|
|
33
|
+
return entries
|
|
34
|
+
.map(([name, entry]) => {
|
|
35
|
+
if (!/^[A-Za-z0-9_.-]{1,128}$/.test(name))
|
|
36
|
+
invalid("names must be 1–128 ASCII letters, digits, dots, underscores or hyphens.");
|
|
37
|
+
const lower = name.toLowerCase();
|
|
38
|
+
if (RESERVED.has(lower))
|
|
39
|
+
invalid(`may not set "${name}"; the authorization request owns it.`);
|
|
40
|
+
if (names.has(lower))
|
|
41
|
+
invalid("must not contain case-variant duplicate names.");
|
|
42
|
+
names.add(lower);
|
|
43
|
+
if (typeof entry !== "string" ||
|
|
44
|
+
!entry.trim() ||
|
|
45
|
+
entry.length > 2048 ||
|
|
46
|
+
Array.from(entry).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) {
|
|
47
|
+
invalid("values must be non-empty strings of at most 2048 code units without control characters.");
|
|
48
|
+
}
|
|
49
|
+
return [name, entry];
|
|
50
|
+
})
|
|
51
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
52
|
+
}
|
|
@@ -222,7 +222,7 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
222
222
|
super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), viewSignal(ctx, requestOwned),
|
|
223
223
|
// No client binding: the client is configuration, so there is no
|
|
224
224
|
// registration for a restart to carry forward or discard.
|
|
225
|
-
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx) });
|
|
225
|
+
undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx), authorizationParams: settings.authorizationParams });
|
|
226
226
|
this.settings = settings;
|
|
227
227
|
}
|
|
228
228
|
/**
|
|
@@ -341,9 +341,6 @@ class StaticOAuthProvider extends KvOAuthProvider {
|
|
|
341
341
|
}, StaticOAuthProvider.prototype.validateResourceURL);
|
|
342
342
|
registerAuthMethod(StaticOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
|
|
343
343
|
return authFlow(function* () {
|
|
344
|
-
for (const [name, value] of this.settings.authorizationParams) {
|
|
345
|
-
authorizationUrl.searchParams.set(name, value);
|
|
346
|
-
}
|
|
347
344
|
if (!this.settings.pkce) {
|
|
348
345
|
authorizationUrl.searchParams.delete("code_challenge");
|
|
349
346
|
authorizationUrl.searchParams.delete("code_challenge_method");
|
|
@@ -22,6 +22,7 @@ export const REMOTE_MCP_AUTH = variants("type", {
|
|
|
22
22
|
}).shape,
|
|
23
23
|
oauth: optionsOf()({
|
|
24
24
|
...keys("type", "clientMetadataUrl", "scope"),
|
|
25
|
+
authorizationParams: strings(),
|
|
25
26
|
client: optionsOf()(keys("issuer", "clientId", "clientSecret", "tokenEndpointAuthMethod")),
|
|
26
27
|
}).shape,
|
|
27
28
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type RemoteOAuthClient } from "../auth/downstream-client-metadata.js";
|
|
2
2
|
import type { FetchLike, Transport } from "@modelcontextprotocol/client";
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
|
+
import type { RequestToken } from "../request-token.js";
|
|
4
5
|
import type { Connector, ConnectorCallAdmissionPolicy, ConnectorContext, ConnectorCredentialConfig, ConnectorUsageGuide, Logger } from "../types.js";
|
|
5
6
|
/**
|
|
6
7
|
* A static downstream credential supplied through the connection in the
|
|
@@ -37,7 +38,7 @@ interface RemoteMcpCredentialAuth {
|
|
|
37
38
|
export type RemoteMcpAuth = {
|
|
38
39
|
type: "request";
|
|
39
40
|
/** Resolve a Bearer token inside this request. Never persisted or described. */
|
|
40
|
-
token:
|
|
41
|
+
token: RequestToken;
|
|
41
42
|
/** Static protocol/catalog headers, never Authorization. */
|
|
42
43
|
headers?: Record<string, string>;
|
|
43
44
|
} | {
|
|
@@ -51,6 +52,8 @@ export type RemoteMcpAuth = {
|
|
|
51
52
|
client?: RemoteOAuthClient;
|
|
52
53
|
/** Space-separated default scopes when the resource server does not advertise them. */
|
|
53
54
|
scope?: string;
|
|
55
|
+
/** Non-secret browser authorization parameters; protocol fields are reserved. Changes require reconnect. */
|
|
56
|
+
authorizationParams?: Record<string, string>;
|
|
54
57
|
};
|
|
55
58
|
export type RemoteMcpRedirectPolicy = "none" | "same-origin";
|
|
56
59
|
export interface RemoteMcpOptions {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { authorizationParamsFor } from "../auth/oauth-authorization-params.js";
|
|
1
2
|
import { authView } from "../auth/native-call.js";
|
|
2
3
|
import { authFlow, authCall, authInvoke, authValue, authMethod, authCatch } from "../auth/native.js";
|
|
3
4
|
import { isOAuthCredentialRequest } from "../auth/oauth-token-response.js";
|
|
@@ -1101,6 +1102,7 @@ export function remoteMcp(id, opts) {
|
|
|
1101
1102
|
const clientMetadataUrl = opts.auth?.type === "oauth" ? opts.auth.clientMetadataUrl : undefined;
|
|
1102
1103
|
const oauthScope = opts.auth?.type === "oauth" ? opts.auth.scope : undefined;
|
|
1103
1104
|
assertOAuthScope(id, oauthScope);
|
|
1105
|
+
const authorizationParams = authorizationParamsFor(opts.auth?.type === "oauth" ? opts.auth.authorizationParams : undefined, `remoteMcp(${JSON.stringify(id)}).auth.authorizationParams`);
|
|
1104
1106
|
const staticClient = opts.auth?.type === "oauth" && opts.auth.client ? { ...opts.auth.client } : undefined;
|
|
1105
1107
|
assertRemoteOAuthClient(id, staticClient);
|
|
1106
1108
|
if (staticClient && clientMetadataUrl !== undefined)
|
|
@@ -1722,6 +1724,7 @@ export function remoteMcp(id, opts) {
|
|
|
1722
1724
|
url: new URL(opts.url).href,
|
|
1723
1725
|
redirectUri,
|
|
1724
1726
|
clientMetadata: downstreamClientMetadata(redirectUri, oauthScope, ctx.oauthClientName, staticClient ? remoteClientAuthMethod(staticClient) : "none"),
|
|
1727
|
+
...(authorizationParams.length ? { authorizationParams } : {}),
|
|
1725
1728
|
authScope: opts.authScope ?? "shared",
|
|
1726
1729
|
versionNegotiation: opts.versionNegotiation ?? "auto",
|
|
1727
1730
|
redirects: opts.redirects ?? "none",
|
|
@@ -1735,7 +1738,7 @@ export function remoteMcp(id, opts) {
|
|
|
1735
1738
|
},
|
|
1736
1739
|
}
|
|
1737
1740
|
: {}),
|
|
1738
|
-
}), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, { name: ctx.oauthClientName, client: staticClient, secrets: sentSecretsFor(ctx) });
|
|
1741
|
+
}), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, { name: ctx.oauthClientName, client: staticClient, secrets: sentSecretsFor(ctx), authorizationParams });
|
|
1739
1742
|
if (state)
|
|
1740
1743
|
state.provider = provider;
|
|
1741
1744
|
return provider;
|
|
@@ -1856,6 +1859,11 @@ export function remoteMcp(id, opts) {
|
|
|
1856
1859
|
return response;
|
|
1857
1860
|
});
|
|
1858
1861
|
}));
|
|
1862
|
+
if (response.status === 401 && requestAuth?.token.invalidate && credentialValue !== null) {
|
|
1863
|
+
// Invalidate the exact bytes this transport sent, never a newer token.
|
|
1864
|
+
// Preserve the original 401 even if an operator hook fails. No replay.
|
|
1865
|
+
(yield authCatch(authInvoke(requestAuth.token, "invalidate", [credentialValue, ctx]), () => { }));
|
|
1866
|
+
}
|
|
1859
1867
|
if (credentialPost && !isOAuthCredentialRequest(init) && REDIRECT_STATUSES.has(response.status)) {
|
|
1860
1868
|
(yield authCatch(authValue(() => response.body?.cancel()), () => { }));
|
|
1861
1869
|
throw new ConnectorCallError("connector_call_failed", "OAuth registration endpoint redirected; the request was not repeated.", { retryable: false });
|
|
@@ -2408,6 +2416,9 @@ export function remoteMcp(id, opts) {
|
|
|
2408
2416
|
: isOauth
|
|
2409
2417
|
? {
|
|
2410
2418
|
mode: "oauth",
|
|
2419
|
+
...(authorizationParams.length
|
|
2420
|
+
? { authorizationParamNames: authorizationParams.map(([name]) => name) }
|
|
2421
|
+
: {}),
|
|
2411
2422
|
...(oauthScope !== undefined ? { scope: oauthScope } : {}),
|
|
2412
2423
|
...(describedClientMetadataUrl !== undefined ? { clientMetadataUrl: describedClientMetadataUrl } : {}),
|
|
2413
2424
|
}
|
package/dist/google.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RequestToken } from "./request-token.js";
|
|
2
|
+
import { type GoogleServiceAccount } from "./providers/_shared/google/service-account.js";
|
|
3
|
+
export type { GoogleServiceAccount } from "./providers/_shared/google/service-account.js";
|
|
4
|
+
export interface GoogleServiceAccountTokenOptions {
|
|
5
|
+
/** The downloaded JSON key text, or its camelCase fields. Kept in deployment memory only. */
|
|
6
|
+
serviceAccount: GoogleServiceAccount | string;
|
|
7
|
+
/** Non-empty HTTPS OAuth scope URLs for the target API. */
|
|
8
|
+
scopes: readonly string[];
|
|
9
|
+
/** Optional Workspace user for domain-wide delegation; omitted means the account itself. */
|
|
10
|
+
subject?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Mint Google OAuth access tokens for remoteMcp's shared request auth on Node or Workers. */
|
|
13
|
+
export declare function googleServiceAccountToken(options: GoogleServiceAccountTokenOptions): RequestToken;
|
package/dist/google.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { assertKnownOptions, ConfigError, keys, optionsOf } from "./config-schema.js";
|
|
2
|
+
import { delegatedToken, forgetDelegatedToken, parseServiceAccount, } from "./providers/_shared/google/service-account.js";
|
|
3
|
+
const OPTIONS = optionsOf()(keys("serviceAccount", "scopes", "subject"));
|
|
4
|
+
const ACCOUNT = optionsOf()(keys("clientEmail", "privateKey", "clientId"));
|
|
5
|
+
/** Mint Google OAuth access tokens for remoteMcp's shared request auth on Node or Workers. */
|
|
6
|
+
export function googleServiceAccountToken(options) {
|
|
7
|
+
const opts = assertKnownOptions(options, "googleServiceAccountToken", OPTIONS);
|
|
8
|
+
let input = opts.serviceAccount;
|
|
9
|
+
if (typeof input === "object" && input !== null) {
|
|
10
|
+
input = assertKnownOptions(input, "googleServiceAccountToken.serviceAccount", ACCOUNT);
|
|
11
|
+
}
|
|
12
|
+
let account;
|
|
13
|
+
try {
|
|
14
|
+
account = parseServiceAccount("googleServiceAccountToken", input);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// The legacy Workspace parser may quote a JSON credential's type. Never
|
|
18
|
+
// reflect arbitrary credential-file fields through this public helper.
|
|
19
|
+
throw new ConfigError("googleServiceAccountToken.serviceAccount requires a valid Google service-account JSON key or { clientEmail, privateKey, clientId? } with a PKCS#8 RSA key of at least 2048 bits.");
|
|
20
|
+
}
|
|
21
|
+
if (!Array.isArray(opts.scopes) || opts.scopes.length === 0 || !Array.from(opts.scopes).every(validScope)) {
|
|
22
|
+
throw new ConfigError("googleServiceAccountToken.scopes must be a non-empty array of HTTPS OAuth scope URLs.");
|
|
23
|
+
}
|
|
24
|
+
if (opts.subject !== undefined && (typeof opts.subject !== "string" || !/^[^\s@]+@[^\s@]+$/.test(opts.subject))) {
|
|
25
|
+
throw new ConfigError("googleServiceAccountToken.subject must be a Workspace email address when provided.");
|
|
26
|
+
}
|
|
27
|
+
const request = { account, subject: opts.subject, scopes: [...new Set(opts.scopes)] };
|
|
28
|
+
const token = async (ctx) => await delegatedToken(request, ctx);
|
|
29
|
+
token.invalidate = async (rejected) => await forgetDelegatedToken(request, rejected);
|
|
30
|
+
return token;
|
|
31
|
+
}
|
|
32
|
+
function validScope(value) {
|
|
33
|
+
if (typeof value !== "string" || /\s/.test(value))
|
|
34
|
+
return false;
|
|
35
|
+
try {
|
|
36
|
+
const url = new URL(value);
|
|
37
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.hash;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -35,7 +35,7 @@ export declare function parseServiceAccount(owner: string, input: unknown): Serv
|
|
|
35
35
|
/** One delegated token request: whose account, acting as whom, for what. */
|
|
36
36
|
export interface DelegatedTokenRequest {
|
|
37
37
|
account: ServiceAccountKey;
|
|
38
|
-
subject
|
|
38
|
+
subject?: string | undefined;
|
|
39
39
|
scopes: readonly string[];
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Google service account acting for Workspace users through
|
|
3
|
-
* delegation: the key,
|
|
4
|
-
* access tokens
|
|
5
|
-
* own export.
|
|
2
|
+
* A Google service account acting as itself or for Workspace users through
|
|
3
|
+
* domain-wide delegation: the key, signed JWT-bearer assertions and short-lived
|
|
4
|
+
* access tokens. Shared by Workspace providers and the optional /google helper.
|
|
6
5
|
*
|
|
7
6
|
* Why not the vault. One key serves every Workspace connector a deployment
|
|
8
7
|
* declares — Gmail, Drive, Docs — and it is a deployment secret in the same
|
|
@@ -300,9 +299,17 @@ function tokenAnswer(status, code) {
|
|
|
300
299
|
* Map a refused token request to what fixes it. The codes are RFC 6749's and
|
|
301
300
|
* Google documents what each means for a delegated assertion.
|
|
302
301
|
*/
|
|
303
|
-
function tokenFailure(account, scopes, status, headers, payload) {
|
|
302
|
+
function tokenFailure(account, scopes, status, headers, payload, subject) {
|
|
304
303
|
const code = asRecord(payload)["error"];
|
|
305
304
|
const said = tokenAnswer(status, code);
|
|
305
|
+
if (subject === undefined) {
|
|
306
|
+
if (code === "unauthorized_client" || code === "access_denied" || code === "invalid_scope" || status === 403) {
|
|
307
|
+
return new ConnectorCallError("provider_permission_denied", `Google refused a service-account token.${said} Check the requested OAuth scopes and the service account's IAM permissions for the target API and resources.`);
|
|
308
|
+
}
|
|
309
|
+
if (code === "invalid_grant") {
|
|
310
|
+
return resendAdvice(new ConnectorCallError("auth_required", `Google refused the service-account assertion.${said} The key may be deleted or disabled, or this host's clock may be minutes off. Check the key and clock.`), " before retrying");
|
|
311
|
+
}
|
|
312
|
+
}
|
|
306
313
|
const client = account.clientId
|
|
307
314
|
? `client ID ${account.clientId} (${account.clientEmail})`
|
|
308
315
|
: `the client ID of ${account.clientEmail}`;
|
|
@@ -325,7 +332,7 @@ function tokenFailure(account, scopes, status, headers, payload) {
|
|
|
325
332
|
if (status === 403) {
|
|
326
333
|
return new ConnectorCallError("provider_permission_denied", `Google refused domain-wide delegation.${said} Ask a Workspace super admin to review ${client} in the Admin console's Manage Domain Wide Delegation entry and grant exactly these scopes: ${scopes.join(",")}.`);
|
|
327
334
|
}
|
|
328
|
-
return new ConnectorCallError("connector_call_failed", `Google's token endpoint refused the delegated assertion with HTTP ${status}.${said}`, { retryable: false });
|
|
335
|
+
return new ConnectorCallError("connector_call_failed", `Google's token endpoint refused the ${subject === undefined ? "service-account" : "delegated"} assertion with HTTP ${status}.${said}`, { retryable: false });
|
|
329
336
|
}
|
|
330
337
|
async function mint(account, subject, scopes, ctx) {
|
|
331
338
|
const issuedAt = Date.now();
|
|
@@ -339,7 +346,7 @@ async function mint(account, subject, scopes, ctx) {
|
|
|
339
346
|
const parsed = await response.jsonResult();
|
|
340
347
|
const payload = "value" in parsed ? parsed.value : undefined;
|
|
341
348
|
if (!response.ok) {
|
|
342
|
-
throw verdict(tokenFailure(account, scopes, response.status, response.headers, payload));
|
|
349
|
+
throw verdict(tokenFailure(account, scopes, response.status, response.headers, payload, subject));
|
|
343
350
|
}
|
|
344
351
|
const body = asRecord(payload);
|
|
345
352
|
const accessToken = body["access_token"];
|
|
@@ -383,7 +390,12 @@ function keyIdentity(account) {
|
|
|
383
390
|
}
|
|
384
391
|
async function cacheKey({ account, subject, scopes }) {
|
|
385
392
|
// Workspace addresses are case-insensitive; scope order is not a new grant.
|
|
386
|
-
return
|
|
393
|
+
return JSON.stringify([
|
|
394
|
+
account.clientEmail,
|
|
395
|
+
await keyIdentity(account),
|
|
396
|
+
subject?.toLowerCase() ?? null,
|
|
397
|
+
[...new Set(scopes)].sort(),
|
|
398
|
+
]);
|
|
387
399
|
}
|
|
388
400
|
function remember(key, minted) {
|
|
389
401
|
tokens.delete(key);
|
|
@@ -55,4 +55,4 @@ export declare const PLANNING_CENTER_MAX_RESPONSE_BYTES: number;
|
|
|
55
55
|
*/
|
|
56
56
|
export declare const PLANNING_CENTER_ADMISSION: ConnectorCallAdmissionPolicy;
|
|
57
57
|
/** Planning Center refuses a request without a descriptive one (HTTP 403). */
|
|
58
|
-
export declare const DEFAULT_USER_AGENT = "connecta/0.36.
|
|
58
|
+
export declare const DEFAULT_USER_AGENT = "connecta/0.36.2 (+https://github.com/zackbart/connecta)";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConnectorContext } from "./types.js";
|
|
2
|
+
/** A request-local Bearer resolver with optional cache invalidation on HTTP 401. */
|
|
3
|
+
export interface RequestToken {
|
|
4
|
+
(ctx: ConnectorContext): Promise<string>;
|
|
5
|
+
/** Drop only the rejected token. This hook never authorizes replay. */
|
|
6
|
+
invalidate?: (rejected: string, ctx: ConnectorContext) => Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CONNECTA_VERSION = "0.36.
|
|
1
|
+
export declare const CONNECTA_VERSION = "0.36.2";
|
package/dist/version.js
CHANGED
package/documentation/README.md
CHANGED
|
@@ -8,18 +8,19 @@ For a new deployment, start with [Deploying Connecta](./deploying.md).
|
|
|
8
8
|
|
|
9
9
|
## Operator tasks
|
|
10
10
|
|
|
11
|
-
| Task
|
|
12
|
-
|
|
|
13
|
-
| Add or remove a connector
|
|
14
|
-
| Set up a maintained provider
|
|
15
|
-
| Inspect maintained API sources and tools
|
|
16
|
-
| Organize an agent's pools and tiers
|
|
17
|
-
| Add a person (maintainer or pool member)
|
|
18
|
-
| Connect a
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
11
|
+
| Task | Self-contained procedure |
|
|
12
|
+
| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| Add or remove a connector | [Change the connector roster](./integrating.md#add-or-remove-a-connector) |
|
|
14
|
+
| Set up a maintained provider | [Maintained provider setup](./providers.md) |
|
|
15
|
+
| Inspect maintained API sources and tools | [API sources and tool inventory](./providers.md#api-sources-and-tool-inventory) |
|
|
16
|
+
| Organize an agent's pools and tiers | [Organizing an agent's pools](./auth.md#organizing-an-agents-pools) |
|
|
17
|
+
| Add a person (maintainer or pool member) | [Add a person](./auth.md#add-a-person), [member page](./operator-ui.md#use-the-member-page) |
|
|
18
|
+
| Connect Google hosted MCP with a service account | [Google tokens and shared-agent/personal-people setup](./auth.md#google-service-account-tokens-and-hosted-mcp) |
|
|
19
|
+
| Connect a shared credential | [Connect a shared credential](./auth.md#connect-a-shared-credential) |
|
|
20
|
+
| Issue, rotate, or revoke an agent token | [Manage an agent token](./auth.md#issue-rotate-or-revoke-an-agent-token) |
|
|
21
|
+
| Upgrade between versions | [Upgrade an existing deployment](./deploying.md#upgrade-an-existing-deployment), including [migration to 0.33](./migration-0.33.md) |
|
|
22
|
+
| Diagnose with `connecta doctor` | [Verify and diagnose](./deploying.md#verify-and-diagnose) |
|
|
23
|
+
| Connect a client with tool search | [Native tool surface](./meta-tools.md#native-tool-surface), [Eve agents](./meta-tools.md#eve-agents) |
|
|
23
24
|
|
|
24
25
|
For removed hosted presets and the former hosted modes of Stripe, Vercel,
|
|
25
26
|
Cloudflare, and GitHub, use [Migrate hosted MCP presets](./hosted-mcp-migration.md).
|
|
@@ -1199,6 +1199,15 @@ meaning. These facts do not change agent error fields or write outcomes.
|
|
|
1199
1199
|
|
|
1200
1200
|
## Configuration
|
|
1201
1201
|
|
|
1202
|
+
Downstream OAuth browser extensions use `auth.authorizationParams` on
|
|
1203
|
+
`remoteMcp()` and `oauth.authorizationParams` on `api()`. Both snapshot and
|
|
1204
|
+
validate the string map at construction, reserve protocol/security parameters
|
|
1205
|
+
case-insensitively, and add entries only when publishing the consent URL.
|
|
1206
|
+
Remote MCP includes canonical sorted entries in its existing client configuration
|
|
1207
|
+
binding (omitted and empty maps remain equivalent); static API OAuth retains
|
|
1208
|
+
its existing token-endpoint binding. The [auth contract](./auth.md#browser-authorization-parameters)
|
|
1209
|
+
owns limits, reserved names, and reconnect requirements.
|
|
1210
|
+
|
|
1202
1211
|
`ConnectaConfig` is one schema (`src/config.ts`, combinators in
|
|
1203
1212
|
`src/config-schema.ts`). The same value produces the TypeScript type a
|
|
1204
1213
|
deployment writes, the unknown-key walk createConnecta runs before reading any
|
|
@@ -1210,8 +1219,8 @@ is resolved in place of the caller's object, so no getter or Proxy trap runs;
|
|
|
1210
1219
|
an accessor on a config object or array is refused by path, as is an object
|
|
1211
1220
|
that cannot be inspected. A closed object or record slot takes a plain object
|
|
1212
1221
|
only: an array or a class instance there is refused by path, unread. String
|
|
1213
|
-
maps (static `headers`, `api()`
|
|
1214
|
-
`tokenRequestHeaders`) are copied the same way and each value must be a
|
|
1222
|
+
maps (static `headers`, remote MCP and `api()` OAuth `authorizationParams`,
|
|
1223
|
+
and `api()` OAuth `tokenRequestHeaders`) are copied the same way and each value must be a
|
|
1215
1224
|
string. A discriminated union (`remoteMcp()`'s `auth.type`,
|
|
1216
1225
|
a provider's `auth.type`) with a missing or unrecognized discriminant throws
|
|
1217
1226
|
with the valid values. Only plain data is copied. Connectors, modules,
|
|
@@ -1770,6 +1779,19 @@ fetch, streams, Web Crypto, lifetime and request-byte behavior, as listed in
|
|
|
1770
1779
|
|
|
1771
1780
|
## Optional deployment modules
|
|
1772
1781
|
|
|
1782
|
+
`/google` (`src/google.ts`) exports the Promise-based
|
|
1783
|
+
`googleServiceAccountToken` request-token resolver outside the root graph.
|
|
1784
|
+
It reuses `src/providers/_shared/google/service-account.ts` for Web Crypto,
|
|
1785
|
+
in-memory token caching and exact-token invalidation, with an absent subject
|
|
1786
|
+
for service accounts acting as themselves. Workspace providers keep supplying
|
|
1787
|
+
subjects and retain their delegated error guidance. `src/request-token.ts`
|
|
1788
|
+
owns the callable resolver contract and optional HTTP-401 invalidation hook;
|
|
1789
|
+
`remoteMcp()` invokes it without replaying the failed operation.
|
|
1790
|
+
`test/google-service-account-token.test.ts` exercises signing/cache behavior on
|
|
1791
|
+
Node and workerd; `test/google-service-account-remote.node.test.ts` checks
|
|
1792
|
+
real-socket token/MCP requests, invalidation and one physical attempt per write.
|
|
1793
|
+
Operator setup lives in [Google hosted MCP](./auth.md#google-service-account-tokens-and-hosted-mcp).
|
|
1794
|
+
|
|
1773
1795
|
`createConnecta` takes closed typed `ui`, `vault`, and `activity`
|
|
1774
1796
|
slots, with factories at `/ui`, `/credentials`, and `/activity`
|
|
1775
1797
|
and agent tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
|
package/documentation/auth.md
CHANGED
|
@@ -14,7 +14,7 @@ credential contracts; use [deployment setup](./deploying.md) for runnable steps.
|
|
|
14
14
|
| Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [Clerk Organizations](#one-clerk-instance-one-organization-per-deployment), [management](#human-authentication-management) |
|
|
15
15
|
| Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
|
|
16
16
|
| Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
|
|
17
|
-
| Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens)
|
|
17
|
+
| Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens), [Google hosted MCP](#google-service-account-tokens-and-hosted-mcp) |
|
|
18
18
|
| Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
|
|
19
19
|
|
|
20
20
|
[Credential recognition and admission](#credential-recognition-and-admission)
|
|
@@ -2498,6 +2498,48 @@ stored static-client state and reselects the configured identity.
|
|
|
2498
2498
|
Configured `scope` is a fallback; a downstream challenge or protected-resource
|
|
2499
2499
|
declaration takes precedence, and the SDK adds `offline_access` when advertised.
|
|
2500
2500
|
|
|
2501
|
+
### Browser authorization parameters
|
|
2502
|
+
|
|
2503
|
+
Set `auth.authorizationParams` on an OAuth `remoteMcp()` connector to add
|
|
2504
|
+
non-secret provider parameters to the browser consent URL. This works with
|
|
2505
|
+
pre-registered clients, CIMD, and DCR. For example:
|
|
2506
|
+
|
|
2507
|
+
```ts
|
|
2508
|
+
remoteMcp("crm", {
|
|
2509
|
+
url: "https://mcp.example/mcp",
|
|
2510
|
+
auth: {
|
|
2511
|
+
type: "oauth",
|
|
2512
|
+
scope: "records.read",
|
|
2513
|
+
authorizationParams: { access_type: "offline", prompt: "consent" },
|
|
2514
|
+
},
|
|
2515
|
+
});
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
Each configured parameter occurs once, replacing any same-name value already
|
|
2519
|
+
on the authorization endpoint URL. These parameters are never added to token
|
|
2520
|
+
exchange, refresh, registration, revocation, metadata, or MCP requests.
|
|
2521
|
+
Descriptions expose parameter names only. Values are public configuration,
|
|
2522
|
+
visible in the consent URL and stored with it; never put secrets here.
|
|
2523
|
+
|
|
2524
|
+
The map must be a plain string record with at most 32 entries. Names contain
|
|
2525
|
+
1–128 ASCII letters, digits, dots, underscores, or hyphens. Values must be
|
|
2526
|
+
nonempty (not whitespace-only), at most 2048 UTF-16 code units, and contain no
|
|
2527
|
+
ASCII control characters. Case-variant duplicate names are refused. The
|
|
2528
|
+
following names, including every case variant, fail construction with
|
|
2529
|
+
`ConfigError`: `response_type`, `response_mode`, `client_id`, `client_secret`,
|
|
2530
|
+
`redirect_uri`, `state`, `code_challenge`, `code_challenge_method`, `scope`,
|
|
2531
|
+
`resource`, `request`, and `request_uri`. Configure scopes through `auth.scope`.
|
|
2532
|
+
`response_mode` is reserved because Connecta accepts callbacks only as GET query
|
|
2533
|
+
parameters.
|
|
2534
|
+
|
|
2535
|
+
For remote MCP, the map joins the stored client configuration binding, like
|
|
2536
|
+
scope and client identity. Adding, removing, or changing a parameter requires
|
|
2537
|
+
Restart or Disconnect and reconnect to obtain a grant with the new consent
|
|
2538
|
+
settings. The map does not rewrite existing grants or pending consent URLs;
|
|
2539
|
+
as with scope, reconfiguring it alone does not obtain a new grant. A DCR
|
|
2540
|
+
registration created under different parameters cannot be carried through
|
|
2541
|
+
Restart. Key order is irrelevant; an empty map and omission are equivalent.
|
|
2542
|
+
|
|
2501
2543
|
## Downstream OAuth on `api()`
|
|
2502
2544
|
|
|
2503
2545
|
`api()` takes a static authorization-code configuration for APIs without MCP
|
|
@@ -2532,7 +2574,7 @@ No discovery or registration occurs; all URLs are configuration, so the
|
|
|
2532
2574
|
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
2533
2575
|
| Endpoints | Checked at construction: HTTPS, HTTP only on loopback, no credentials or fragment. |
|
|
2534
2576
|
| `pkce` | S256 by default; `false` omits challenge and verifier. |
|
|
2535
|
-
| `authorizationParams` |
|
|
2577
|
+
| `authorizationParams` | Browser-only string parameters. Exact names `response_type`, `client_id`, `redirect_uri`, `state`, `scope`, `code_challenge`, `code_challenge_method` and `resource` are refused; other names and string values are accepted as before. Remote MCP's [stricter rules](#browser-authorization-parameters) do not apply here. |
|
|
2536
2578
|
| `expectedCallbackParameters` | Optional `Readonly<Record<string, string>>` of fixed provider response values. Each configured parameter must occur exactly once with its exact decoded value, after pending-state verification and before issuer validation, flow claims, code exchange, or grant persistence. Missing, duplicate, or mismatched values refuse without exchanging a code or returning parameter values. Omission preserves existing behavior. |
|
|
2537
2579
|
| `revocationEndpoint` | Optional trusted HTTPS revocation URL (loopback HTTP allowed), without credentials, query or fragment. Disconnect removes the local grant before one bounded attempt using the configured client. Failure reports `oauth_revocation_failed`; a repeated Disconnect never retries. |
|
|
2538
2580
|
| `revocationRequestFormat` | `"form"` by default; `"json"` sends JSON without `token_type_hint`. Requires `revocationEndpoint` when set. |
|
|
@@ -2585,6 +2627,15 @@ owner's access token as `Authorization: Bearer` with these rules:
|
|
|
2585
2627
|
- The answer's `.text()` and `.json()`, clones' too, decode its bytes as UTF-8, so
|
|
2586
2628
|
workerd never quotes a downstream's Content-Type in its own log.
|
|
2587
2629
|
|
|
2630
|
+
`api()` already supports `oauth.authorizationParams` through the same OAuth
|
|
2631
|
+
provider and browser-only injection. It keeps its existing, more permissive
|
|
2632
|
+
validation (see the `api()` option table), so existing configurations still
|
|
2633
|
+
construct. Use only GET query response modes with it. Its existing binding remains
|
|
2634
|
+
unchanged: the token endpoint identifies the grant; scope, client identity,
|
|
2635
|
+
and authorization parameters do not add a stored client configuration binding.
|
|
2636
|
+
Changing authorization parameters affects new consents; reconnect to obtain a
|
|
2637
|
+
grant with different consent settings.
|
|
2638
|
+
|
|
2588
2639
|
The grant record and consents, epoch fencing, vault sealing, shared/personal ownership,
|
|
2589
2640
|
callback state/principal checks, `authorize_connector`, Connect, Restart, and
|
|
2590
2641
|
Disconnect match `remoteMcp()`. Status calls report a stored grant for the
|
|
@@ -2603,6 +2654,14 @@ transport, are not stored or described, and a changed token replaces the client.
|
|
|
2603
2654
|
This mode refuses cleartext non-loopback origins. Optional `auth.headers` carry
|
|
2604
2655
|
static protocol/catalog controls and cannot set Authorization.
|
|
2605
2656
|
|
|
2657
|
+
A resolver may expose `token.invalidate(rejectedToken, ctx): Promise<void>` to
|
|
2658
|
+
remove a cached token after a downstream HTTP 401. The hook receives the exact
|
|
2659
|
+
token sent by that transport, so it must leave a newer cached token intact.
|
|
2660
|
+
Hook failures are withheld and preserve the original 401. Request auth never
|
|
2661
|
+
automatically repeats the operation or renews the grant inside the failed
|
|
2662
|
+
scope: authorization failure stays latched for that scope. A new request resolves
|
|
2663
|
+
the callback again. In particular, no dispatched write is replayed.
|
|
2664
|
+
|
|
2606
2665
|
The GitHub provider validates each tool's owner/repo and scope access before
|
|
2607
2666
|
resolving an installation token. It creates one token-bound MCP client for that
|
|
2608
2667
|
operation and closes it before returning. Completed installation/token caches
|
|
@@ -2613,6 +2672,134 @@ is retained across requests, and rejected writes are never replayed. An omitted
|
|
|
2613
2672
|
`describe()` includes neither the key nor the minted tokens. The maintained
|
|
2614
2673
|
provider skill explains App permissions and installation prerequisites.
|
|
2615
2674
|
|
|
2675
|
+
### Google service-account tokens and hosted MCP
|
|
2676
|
+
|
|
2677
|
+
Import `googleServiceAccountToken` from `@zackbart/connecta/google` to connect a
|
|
2678
|
+
Google hosted MCP server using a service-account JSON key on Node or Workers.
|
|
2679
|
+
This optional subpath is outside the root import graph. Supply deployment secrets
|
|
2680
|
+
through the environment or platform secret store:
|
|
2681
|
+
|
|
2682
|
+
```ts
|
|
2683
|
+
import { remoteMcp } from "@zackbart/connecta";
|
|
2684
|
+
import { googleServiceAccountToken } from "@zackbart/connecta/google";
|
|
2685
|
+
|
|
2686
|
+
const warehouse = remoteMcp("warehouse", {
|
|
2687
|
+
url: "https://bigquery.googleapis.com/mcp",
|
|
2688
|
+
authScope: "shared",
|
|
2689
|
+
auth: {
|
|
2690
|
+
type: "request",
|
|
2691
|
+
token: googleServiceAccountToken({
|
|
2692
|
+
serviceAccount: env.GOOGLE_SERVICE_ACCOUNT_JSON,
|
|
2693
|
+
scopes: ["https://www.googleapis.com/auth/bigquery"],
|
|
2694
|
+
}),
|
|
2695
|
+
},
|
|
2696
|
+
});
|
|
2697
|
+
|
|
2698
|
+
const warehousePersonal = remoteMcp("warehouse_personal", {
|
|
2699
|
+
url: "https://bigquery.googleapis.com/mcp",
|
|
2700
|
+
authScope: "personal",
|
|
2701
|
+
auth: {
|
|
2702
|
+
type: "oauth",
|
|
2703
|
+
scope: "https://www.googleapis.com/auth/bigquery",
|
|
2704
|
+
authorizationParams: { access_type: "offline", prompt: "consent" },
|
|
2705
|
+
client: {
|
|
2706
|
+
issuer: "https://accounts.google.com",
|
|
2707
|
+
clientId: env.GOOGLE_CLIENT_ID,
|
|
2708
|
+
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
2709
|
+
tokenEndpointAuthMethod: "client_secret_post",
|
|
2710
|
+
},
|
|
2711
|
+
},
|
|
2712
|
+
});
|
|
2713
|
+
```
|
|
2714
|
+
|
|
2715
|
+
`serviceAccount` accepts downloaded JSON text or
|
|
2716
|
+
`{ clientEmail, privateKey, clientId? }`; `privateKey` is a PKCS#8 RSA PEM of at
|
|
2717
|
+
least 2048 bits. `scopes` must be a non-empty array of HTTPS URLs without
|
|
2718
|
+
credentials, whitespace, or fragments. Unknown options fail construction with
|
|
2719
|
+
`ConfigError`. An optional fixed `subject: "alice@example.com"` requests
|
|
2720
|
+
Workspace domain-wide delegation and requires its admin grant. Omit `subject`
|
|
2721
|
+
for BigQuery: the signed assertion then has no `sub` and acts as the service
|
|
2722
|
+
account itself. See [Google's service-account grant documentation](https://developers.google.com/identity/protocols/oauth2/service-account).
|
|
2723
|
+
|
|
2724
|
+
The helper signs RS256 JWT-bearer assertions and exchanges them at Google's fixed
|
|
2725
|
+
token endpoint. It keeps only bounded in-memory token entries, partitioned by
|
|
2726
|
+
account, key fingerprint, absent-or-present subject and scope set. Concurrent
|
|
2727
|
+
mints share a flight; tokens refresh with a one-minute margin. Keys, assertions
|
|
2728
|
+
and tokens are never logged, persisted or described. Bearer tokens use request
|
|
2729
|
+
auth's existing secret redaction. A 401 evicts only the rejected token; the next
|
|
2730
|
+
request mints again. The failed operation is never automatically repeated.
|
|
2731
|
+
|
|
2732
|
+
#### Separate agent and people connections
|
|
2733
|
+
|
|
2734
|
+
Grant the shared connection to agent pools (including pools used by Eve agents)
|
|
2735
|
+
and the personal connection to pools used by people. Add these entries to the
|
|
2736
|
+
existing deployment configuration, keeping its inbound auth, executor, vault and
|
|
2737
|
+
storage configuration:
|
|
2738
|
+
|
|
2739
|
+
```ts
|
|
2740
|
+
connectors: [warehouse, warehousePersonal],
|
|
2741
|
+
agents: ["assistant"],
|
|
2742
|
+
pools: {
|
|
2743
|
+
warehouse_agents: { tools: ["warehouse"], members: ["agent:assistant"] },
|
|
2744
|
+
warehouse_people: { tools: ["warehouse_personal"], members: ["user_alice"] },
|
|
2745
|
+
},
|
|
2746
|
+
```
|
|
2747
|
+
|
|
2748
|
+
Replace `user_alice` with Alice's admitted principal ID. Agents have no principal;
|
|
2749
|
+
`authScope: "personal"` connectors never appear for them, even if included in a
|
|
2750
|
+
pool's tools. The shared connection uses its fixed service account for every
|
|
2751
|
+
member; the personal connection uses each person's own encrypted OAuth grant.
|
|
2752
|
+
People connect through `authorize_connector` or their member page. Follow
|
|
2753
|
+
[Add a person](#add-a-person), [agent tokens](#agent-tokens-and-upgrading-from-earlier-tokens)
|
|
2754
|
+
and [shared credential ownership](#connect-a-shared-credential) for admission and
|
|
2755
|
+
pool membership. The request-token connection gets its key from deployment
|
|
2756
|
+
configuration and does not need a credential-vault slot.
|
|
2757
|
+
|
|
2758
|
+
#### Google-side setup
|
|
2759
|
+
|
|
2760
|
+
1. Enable the BigQuery API in the Google Cloud project and confirm that the
|
|
2761
|
+
BigQuery MCP service is available under the project's organization policies.
|
|
2762
|
+
Google's current setup uses `https://bigquery.googleapis.com/mcp` and API
|
|
2763
|
+
enablement; follow its current server setup if enablement requirements change.
|
|
2764
|
+
Grant the service account MCP Tool User (`roles/mcp.toolUser`) and BigQuery
|
|
2765
|
+
Job User (`roles/bigquery.jobUser`) on the project where queries run.
|
|
2766
|
+
[BigQuery MCP setup and required roles](https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp).
|
|
2767
|
+
2. Grant BigQuery Data Viewer (`roles/bigquery.dataViewer`) on the datasets the
|
|
2768
|
+
service account may query. Dataset grants restrict data access separately
|
|
2769
|
+
from project-level job execution. Grant further permissions only for the
|
|
2770
|
+
operations you intend to expose; OAuth scopes do not replace IAM.
|
|
2771
|
+
[BigQuery IAM roles and grant levels](https://docs.cloud.google.com/bigquery/docs/access-control).
|
|
2772
|
+
Create a service-account JSON key and store it as
|
|
2773
|
+
`GOOGLE_SERVICE_ACCOUNT_JSON` in the deployment secret store; the helper
|
|
2774
|
+
does not use ADC, metadata-server credentials, or workload identity federation.
|
|
2775
|
+
[Service-account key creation](https://docs.cloud.google.com/iam/docs/keys-create-delete).
|
|
2776
|
+
3. For the personal connection, configure Google Auth Platform branding,
|
|
2777
|
+
audience and data access. Choose **Internal** for an application restricted to
|
|
2778
|
+
people in your Workspace organization, using a project belonging to that
|
|
2779
|
+
organization. Internal consent does not grant dataset access; each person
|
|
2780
|
+
still needs the applicable IAM permissions.
|
|
2781
|
+
[Configure consent and audience](https://developers.google.com/workspace/guides/configure-oauth-consent).
|
|
2782
|
+
4. Create a **Web application** OAuth client under Google Auth Platform → Clients.
|
|
2783
|
+
Register the exact Connecta callback, for example
|
|
2784
|
+
`https://connecta.example/oauth/callback/warehouse_personal`, using the
|
|
2785
|
+
deployment's configured public origin. Store its client ID and secret as
|
|
2786
|
+
deployment secrets. Google's hosted MCP servers do not support DCR or client
|
|
2787
|
+
metadata documents, so configure the static `auth.client` shown above.
|
|
2788
|
+
[Web OAuth client setup](https://docs.cloud.google.com/mcp/set-up-authentication-mcp-servers#web),
|
|
2789
|
+
[Google MCP registration limitations](https://docs.cloud.google.com/mcp/authenticate-mcp#limitations).
|
|
2790
|
+
|
|
2791
|
+
The pre-registered client path supports Google's exact issuer and
|
|
2792
|
+
`client_secret_post`; Google's [discovery document](https://accounts.google.com/.well-known/openid-configuration)
|
|
2793
|
+
advertises that method, PKCE S256, and `https://oauth2.googleapis.com/token`.
|
|
2794
|
+
The personal example requests `access_type=offline` to obtain a refresh token
|
|
2795
|
+
and `prompt=consent` to show consent again when reconnecting. See
|
|
2796
|
+
[Google's offline-access requirements](https://developers.google.com/identity/protocols/oauth2/web-server#offline).
|
|
2797
|
+
Connecta retains the stored refresh token when a successful refresh response
|
|
2798
|
+
omits `refresh_token`, allowing later refreshes with that same token. After
|
|
2799
|
+
adding these parameters to an existing connector, Disconnect and reconnect.
|
|
2800
|
+
Live Google consent and project-specific permissions still need verification
|
|
2801
|
+
against the operator's project.
|
|
2802
|
+
|
|
2616
2803
|
## Contributor reference
|
|
2617
2804
|
|
|
2618
2805
|
### INV-5 output redaction limits
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zackbart/connecta",
|
|
3
|
-
"version": "0.36.
|
|
3
|
+
"version": "0.36.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of six meta-tools.",
|
|
@@ -162,6 +162,10 @@
|
|
|
162
162
|
"./worker": {
|
|
163
163
|
"types": "./dist/worker.d.ts",
|
|
164
164
|
"import": "./dist/worker.js"
|
|
165
|
+
},
|
|
166
|
+
"./google": {
|
|
167
|
+
"types": "./dist/google.d.ts",
|
|
168
|
+
"import": "./dist/google.js"
|
|
165
169
|
}
|
|
166
170
|
},
|
|
167
171
|
"scripts": {
|