@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 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
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.36.1";
3
+ export const CONNECTA_VERSION = "0.36.2";
@@ -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: (ctx: ConnectorContext) => Promise<string>;
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
  }
@@ -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: string;
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 domain-wide
3
- * delegation: the key, the signed JWT-bearer assertion, and the short-lived
4
- * access tokens it buys. Shared by every Workspace product provider; never its
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 [account.clientEmail, await keyIdentity(account), subject.toLowerCase(), [...scopes].sort().join(" ")].join("\n");
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.1 (+https://github.com/zackbart/connecta)";
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";
1
+ export declare const CONNECTA_VERSION = "0.36.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated from package.json by scripts/generate-version.mjs.
2
2
  // Workers-safe: no runtime filesystem read or deployment override.
3
- export const CONNECTA_VERSION = "0.36.1";
3
+ export const CONNECTA_VERSION = "0.36.2";
@@ -8,18 +8,19 @@ For a new deployment, start with [Deploying Connecta](./deploying.md).
8
8
 
9
9
  ## Operator tasks
10
10
 
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 a shared credential | [Connect a shared credential](./auth.md#connect-a-shared-credential) |
19
- | Issue, rotate, or revoke an agent token | [Manage an agent token](./auth.md#issue-rotate-or-revoke-an-agent-token) |
20
- | Upgrade between versions | [Upgrade an existing deployment](./deploying.md#upgrade-an-existing-deployment), including [migration to 0.33](./migration-0.33.md) |
21
- | Diagnose with `connecta doctor` | [Verify and diagnose](./deploying.md#verify-and-diagnose) |
22
- | Connect a client with tool search | [Native tool surface](./meta-tools.md#native-tool-surface), [Eve agents](./meta-tools.md#eve-agents) |
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()`'s `authorizationParams` and
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
@@ -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` | Adds provider parameters; cannot restate the grant's own. |
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.1",
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": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@clerk/backend": "^3.12.0",
21
- "@zackbart/connecta": "0.36.1",
21
+ "@zackbart/connecta": "0.36.2",
22
22
  "quickjs-emscripten": "0.32.0",
23
23
  "undici": "6.29.1"
24
24
  },