@zackbart/connecta 0.36.0 → 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 +55 -0
- package/README.md +1 -1
- 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/http/hatch-paging.js +2 -2
- package/dist/http/paging.js +3 -88
- package/dist/http/provider.js +1 -2
- package/dist/http/tools.js +1 -23
- package/dist/program-writes.js +0 -1
- package/dist/providers/_shared/google/service-account.d.ts +1 -1
- package/dist/providers/_shared/google/service-account.js +20 -8
- package/dist/providers/_shared/sensitive-fields.js +0 -16
- package/dist/providers/breeze/api.js +2 -2
- package/dist/providers/ccb/api.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -8
- package/dist/providers/cloudflare/lib/config.d.ts +2 -1
- package/dist/providers/docs/api.js +1 -1
- package/dist/providers/drive/api.js +1 -1
- package/dist/providers/drive/lib/hatch.js +1 -1
- package/dist/providers/drive/tools.js +1 -1
- package/dist/providers/forms/api.js +1 -1
- package/dist/providers/forms/lib/hatch.js +1 -1
- package/dist/providers/forms/tools.js +1 -1
- package/dist/providers/github/api.js +1 -1
- package/dist/providers/github/lib/discovery.js +2 -2
- package/dist/providers/gmail/api.js +1 -1
- package/dist/providers/granola/api.js +155 -0
- package/dist/providers/granola/index.d.ts +10 -0
- package/dist/providers/granola/index.js +68 -0
- package/dist/providers/granola/openapi.generated.js +30 -0
- package/dist/providers/granola/safety.js +37 -0
- package/dist/providers/granola/skill.generated.js +5 -0
- package/dist/providers/granola/tools.js +2 -0
- package/dist/providers/overflow/api.js +26 -26
- package/dist/providers/planning-center/api.js +4 -4
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +2 -2
- package/dist/providers/quickbooks/lib/query.js +1 -1
- package/dist/providers/quickbooks/safety.js +1 -1
- package/dist/providers/sheets/api.js +1 -1
- package/dist/providers/sheets/lib/hatch.js +2 -2
- package/dist/providers/sheets/lib/requests.js +1 -1
- package/dist/providers/slides/api.js +1 -1
- package/dist/providers/stripe/api.js +7 -7
- package/dist/providers/tithely/api.js +2 -2
- package/dist/providers/vercel/api.js +12 -12
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/request-token.d.ts +7 -0
- package/dist/request-token.js +1 -0
- package/dist/routes/mcp.js +1 -1
- package/dist/runtime/admission.js +0 -12
- package/dist/storage/keys.js +2 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/README.md +13 -12
- package/documentation/architecture.md +148 -76
- package/documentation/auth.md +198 -4
- package/documentation/code-mode.md +6 -6
- package/documentation/meta-tools.md +5 -2
- package/documentation/operator-tests.md +11 -2
- package/documentation/operator-ui.md +1 -2
- package/documentation/providers.md +110 -0
- package/package.json +10 -2
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,61 @@
|
|
|
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
|
+
|
|
18
|
+
## 0.36.1 — 2026-10-27
|
|
19
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
The contributor checks now center on real Node/Workers, browser, persistence and installed-package journeys. Every PR runs browser, audit, Eve and Docker package checks. This patch requires no deployment configuration migration. Granola operators supply a workspace key; Eve pool selectors are optional.
|
|
23
|
+
|
|
24
|
+
QuickBooks invoice updates remain unavailable while their delivery contract is unresolved. The workload validation recipe is reproducible locally; live validation requires separate authorization and is not claimed by this release.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Let the Eve helper choose a pool before each model call with an app-owned selector, fence parked calls after pool changes, and cache instructions and failure backoff per endpoint. Instruction requests also accept a custom client name.
|
|
29
|
+
- Add a read-only Granola workspace API provider for meeting notes, details, paginated transcripts, folders and spaces, with encrypted shared credentials and the standard operation search/details/read surface.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Add a locally reproducible Eve 0.84 workload fixture with independently verified audience-exchange controls and retained server cancellation receipts, and clarify that custom-audience workload tokens are refused. Live validation still requires separate authorization for disposable resources. Verify the standard 2 GiB Fluid resource preset and require confirmed cleanup even when evidence persistence fails.
|
|
34
|
+
- Replace invariant/spec test-title bookkeeping with executable evidence file
|
|
35
|
+
references and the existing stateful behavior journeys. Require installed-package,
|
|
36
|
+
generated-deployment and Docker smoke in the shared CI build job, with browser,
|
|
37
|
+
audit and real Eve compatibility on every PR. Remove the path classifier,
|
|
38
|
+
duplicate package workflow and completed migration's cross-commit exemption guard.
|
|
39
|
+
Keep focused runtime probes, provider contracts, build/declaration checks and the
|
|
40
|
+
single fail-closed `check` status.
|
|
41
|
+
- Remove completed provider-migration exemptions, legacy generator fixtures and
|
|
42
|
+
unused internal list/guide adapters. Consolidate contributor provider and release
|
|
43
|
+
workflows around the current standard without changing public tools or guides.
|
|
44
|
+
- Replace lower-layer operator, configuration and storage test mirrors with real browser, SQLite restart and deployed Node/Workers persistence journeys. Preserve focused SQL rollback, expiry, isolation, secrecy and cancellation probes.
|
|
45
|
+
- Replace redundant auth, write, privacy and runtime test matrices with real HTTP/MCP identity, one-send recovery, secrecy and socket-cancellation journeys. Require prompt downstream closure and admission release after client cancellation, before the ordinary request deadline. Retain focused security and lifetime fault probes without changing runtime behavior or public APIs.
|
|
46
|
+
- Clarify why QuickBooks invoice updates remain unavailable: sparse field preservation and request IDs do not establish email suppression for existing send-ready invoices. Record the authoritative contract gaps and a proposed unsent-only update for further review; no accounting write surface is added.
|
|
47
|
+
- Replace the remaining catalog-wide provider failure sweep and redundant
|
|
48
|
+
provider/HTTP agreement checks with shared stateful outcomes and real HTTP
|
|
49
|
+
failure/recovery journeys. Retain vendor-specific privacy, MIME, ownership,
|
|
50
|
+
revision, physical-send and lifetime fault probes; remove five internal paging
|
|
51
|
+
helpers used only by the retired tests and private exports hidden by the old
|
|
52
|
+
source scan. Require successful, complete delivered reconciliation data for the
|
|
53
|
+
Tithe.ly cancellation journey; failed, empty, malformed and incomplete replies
|
|
54
|
+
cannot establish recovery. Public provider behavior is unchanged.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Use permanently unreachable loopback port zero for native transport-failure probes so later test servers cannot answer them. Verify each platform's native connection error while preserving Node/Workers write-outcome, retry, reset and abort assertions.
|
|
59
|
+
|
|
5
60
|
## 0.36.0 — 2026-10-27
|
|
6
61
|
|
|
7
62
|
Connecta 0.36.0 adds a maintained QuickBooks Online Accounting provider for one fixed sandbox or production company per connector. It offers company identity, bounded record searches, individual records and hierarchical financial reports, with contact details and payment credentials withheld. Deployments can opt into customer creation, sparse customer naming updates and unsent US service-invoice creation by setting `readOnly: false` on the connector. Writes need a stable request ID and are never replayed automatically; the API hatch stays read-only, and invoice updates remain unavailable. See [QuickBooks Online](https://github.com/zackbart/connecta/blob/v0.36.0/documentation/providers.md#quickbooks-online).
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Connecta supplies the service credentials, so the agent doesn't need them.
|
|
|
24
24
|
|
|
25
25
|
## What you can connect
|
|
26
26
|
|
|
27
|
-
Maintained connections: <!-- providers:start -->Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Overflow, Planning Center, QuickBooks Online, Stripe, Tithe.ly, and Vercel<!-- providers:end -->.
|
|
27
|
+
Maintained connections: <!-- providers:start -->Breeze ChMS, Church Community Builder, Cloudflare, GitHub, Gmail, Google Docs, Google Drive, Google Forms, Google Sheets, Google Slides, Granola, Overflow, Planning Center, QuickBooks Online, Stripe, Tithe.ly, and Vercel<!-- providers:end -->.
|
|
28
28
|
|
|
29
29
|
Maintained API providers pair reviewed vendor contracts with tools built by
|
|
30
30
|
Connecta. See the [API sources and tool inventory](./documentation/providers.md#api-sources-and-tool-inventory)
|
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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Internal opt-in adapters for the standard hatch.
|
|
2
|
-
//
|
|
1
|
+
// Internal opt-in adapters for the standard hatch. Inputs have crossed
|
|
2
|
+
// connection safety.
|
|
3
3
|
import { ConnectorCallError } from "../errors.js";
|
|
4
4
|
function record(value) {
|
|
5
5
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
package/dist/http/paging.js
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
// Provider kit (decision 0010, B.4). Unreachable from src/index.ts.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// Stream for programs and the hatch. The codec is the only place a cursor is
|
|
7
|
-
// minted: `plain` hands the vendor's cursor through; `bound` ties it to the tool and its
|
|
8
|
-
// arguments so a cursor from another call is refused instead of silently
|
|
9
|
-
// reading the wrong list.
|
|
10
|
-
import { Effect, Option, Schema, Stream } from "effect";
|
|
3
|
+
// List tools take one page at a time. Lookahead detects an extra row; bound
|
|
4
|
+
// cursors authenticate the tool, arguments, connection and credential owner.
|
|
5
|
+
import { Effect, Schema } from "effect";
|
|
11
6
|
import { ConnectorCallError } from "../errors.js";
|
|
12
7
|
import { Refusal } from "./errors.js";
|
|
13
|
-
import { confinementBase } from "../http-policy.js";
|
|
14
8
|
import { canonicalJson } from "../canonical-json.js";
|
|
15
9
|
import { ContinuationBinding, ContinuationStorage } from "../runtime/call.js";
|
|
16
10
|
import { continuationKeys } from "../storage/keys.js";
|
|
17
|
-
/** The `page` object every list tool returns. */
|
|
18
|
-
export const PageInfo = Schema.Struct({
|
|
19
|
-
hasMore: Schema.Boolean,
|
|
20
|
-
nextCursor: Schema.NullOr(Schema.String),
|
|
21
|
-
});
|
|
22
11
|
/**
|
|
23
12
|
* The standard `page` (decision 0011 S2): `nextCursor` is present exactly
|
|
24
13
|
* when `hasMore` is true and continuation is safe. A hatch adapter can report
|
|
25
14
|
* more data without a cursor when its continuation cannot be confined.
|
|
26
|
-
* Step-1 lists keep `PageInfo` until their step 2.
|
|
27
15
|
*/
|
|
28
16
|
export const StandardPage = Schema.Struct({
|
|
29
17
|
hasMore: Schema.Boolean,
|
|
@@ -55,79 +43,6 @@ export function lookahead(options) {
|
|
|
55
43
|
return { rows, page: { hasMore, nextCursor: id } };
|
|
56
44
|
});
|
|
57
45
|
}
|
|
58
|
-
/** Vendor-supplied continuation, read only after the connection made its data safe. */
|
|
59
|
-
export function token(options) {
|
|
60
|
-
return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, hasMore, nextCursor }) => {
|
|
61
|
-
if (hasMore && !nextCursor)
|
|
62
|
-
return badPage("The vendor reported another page without a next cursor.");
|
|
63
|
-
return Effect.succeed({ rows, page: { hasMore, nextCursor: hasMore ? nextCursor : null } });
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/** Offset paging advances by the rows actually returned, with a total when supplied. */
|
|
67
|
-
export function offset(options) {
|
|
68
|
-
return Effect.gen(function* () {
|
|
69
|
-
yield* position(options.offset, 0);
|
|
70
|
-
yield* position(options.limit, 1);
|
|
71
|
-
return yield* Effect.map(options.fetch(options.offset, options.limit), ({ rows, total }) => {
|
|
72
|
-
const next = options.offset + rows.length;
|
|
73
|
-
const hasMore = rows.length > 0 && (total === undefined ? rows.length >= options.limit : next < total);
|
|
74
|
-
return { rows, page: { hasMore, nextCursor: hasMore ? String(next) : null } };
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
/** Page numbers from a vendor's safe body or headers, with full-page fallback. */
|
|
79
|
-
export function page(options) {
|
|
80
|
-
return Effect.gen(function* () {
|
|
81
|
-
yield* position(options.page, 1);
|
|
82
|
-
yield* position(options.limit, 1);
|
|
83
|
-
return yield* Effect.map(options.fetch(options.page, options.limit), ({ rows, hasMore, totalPages }) => {
|
|
84
|
-
const more = hasMore ?? (totalPages === undefined ? rows.length >= options.limit : options.page < totalPages);
|
|
85
|
-
return { rows, page: { hasMore: more, nextCursor: more ? String(options.page + 1) : null } };
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/** Extract only a cursor from a next URL confined to this connection's origin and path. */
|
|
90
|
-
export function link(options) {
|
|
91
|
-
const base = confinementBase("Paging", options.baseUrl);
|
|
92
|
-
const prefix = base.pathname.replace(/\/+$/, "");
|
|
93
|
-
if (!options.param)
|
|
94
|
-
throw new Error("Link paging needs a cursor parameter.");
|
|
95
|
-
return Effect.flatMap(Effect.suspend(options.fetch), ({ rows, next }) => {
|
|
96
|
-
if (!next)
|
|
97
|
-
return Effect.succeed({ rows, page: { hasMore: false, nextCursor: null } });
|
|
98
|
-
let url;
|
|
99
|
-
try {
|
|
100
|
-
url = new URL(next, base);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return badPage("The vendor returned an invalid next-page URL.");
|
|
104
|
-
}
|
|
105
|
-
if (url.origin !== base.origin ||
|
|
106
|
-
!`${url.pathname}/`.startsWith(`${prefix}/`) ||
|
|
107
|
-
url.username ||
|
|
108
|
-
url.password ||
|
|
109
|
-
url.hash)
|
|
110
|
-
return badPage("The vendor returned a next-page URL outside this connection.");
|
|
111
|
-
const cursors = url.searchParams.getAll(options.param);
|
|
112
|
-
if (cursors.length !== 1 || !cursors[0])
|
|
113
|
-
return badPage("The vendor returned no unique next-page cursor.");
|
|
114
|
-
return Effect.succeed({ rows, page: { hasMore: true, nextCursor: cursors[0] } });
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
function position(value, minimum) {
|
|
118
|
-
return Number.isSafeInteger(value) && value >= minimum
|
|
119
|
-
? Effect.void
|
|
120
|
-
: Effect.fail(new Refusal({
|
|
121
|
-
error: new ConnectorCallError("invalid_args", "Paging positions and limits must be whole numbers within their bounds."),
|
|
122
|
-
}));
|
|
123
|
-
}
|
|
124
|
-
function badPage(message) {
|
|
125
|
-
return Effect.fail(new Refusal({ error: new ConnectorCallError("connector_call_failed", message, { retryable: false }) }));
|
|
126
|
-
}
|
|
127
|
-
/** Every row of every page, fetched as the stream is pulled. */
|
|
128
|
-
export function allRows(fetchPage) {
|
|
129
|
-
return Stream.paginate(undefined, (cursor) => Effect.map(fetchPage(cursor), ({ rows, page }) => [rows, Option.fromNullishOr(page.hasMore ? page.nextCursor : null)]));
|
|
130
|
-
}
|
|
131
46
|
const STRAY_CURSOR = "This cursor belongs to a different call. Pass back the nextCursor from the previous page of this same call, with the same arguments.";
|
|
132
47
|
/** Resolved, nonextractable keys only: no request promise is retained between calls. */
|
|
133
48
|
const signingKeys = new WeakMap();
|
package/dist/http/provider.js
CHANGED
|
@@ -191,8 +191,7 @@ export function defineApiProvider(definition) {
|
|
|
191
191
|
// The declared fact the Writes convention states must match what the write builders send.
|
|
192
192
|
if (idempotencyKey !== undefined && [...builtTools, ...hatchTools].some(takesIdempotencyKey) !== idempotencyKey)
|
|
193
193
|
throw new Error(`${metadata.title} declares idempotencyKey: ${idempotencyKey}, but its write tools ${idempotencyKey ? "send no" : "send an"} idempotency key.`);
|
|
194
|
-
const
|
|
195
|
-
const renderedGuide = "content" in usageGuide ? usageGuide : provider.usageGuide(usageGuide);
|
|
194
|
+
const renderedGuide = provider.usageGuide(guide(options));
|
|
196
195
|
const connector = tracksEveryRequest(api(id, {
|
|
197
196
|
...(defaultCallAdmission
|
|
198
197
|
? {
|
package/dist/http/tools.js
CHANGED
|
@@ -22,7 +22,7 @@ import { causeError, runEdge } from "../runtime/run.js";
|
|
|
22
22
|
import { recordUnauthorized, Refusal, RequestRefused, toConnectorCallError, withheldDefect, } from "./errors.js";
|
|
23
23
|
import { fieldsInput, fieldsProblem, selectFields } from "./fields.js";
|
|
24
24
|
import { catalogSchema, Integer } from "./json-schema.js";
|
|
25
|
-
import { boundCursor, Cursor,
|
|
25
|
+
import { boundCursor, Cursor, StandardPage } from "./paging.js";
|
|
26
26
|
import { Classification, Dispatches, IdempotencyKey, Recovery, ReadTool, WriteCapacity, writeRequestKey, fromConnectorContext, } from "./services.js";
|
|
27
27
|
const inputs = new WeakMap();
|
|
28
28
|
const keyed = new WeakSet();
|
|
@@ -118,28 +118,6 @@ export function get(connection, spec) {
|
|
|
118
118
|
export function list(connection, spec) {
|
|
119
119
|
if (spec.cursor !== undefined && spec.cursor !== "plain" && spec.cursor !== "bound")
|
|
120
120
|
throw new Error('A list cursor must be "plain" or "bound".');
|
|
121
|
-
if (!("key" in spec))
|
|
122
|
-
return standardList(connection, spec);
|
|
123
|
-
if (!spec.key || spec.key === "page")
|
|
124
|
-
throw new Error("A list needs a row key distinct from page.");
|
|
125
|
-
return read(connection, {
|
|
126
|
-
...spec,
|
|
127
|
-
output: Schema.Struct({ [spec.key]: Schema.Array(spec.row), page: PageInfo }),
|
|
128
|
-
run: (args) => Effect.gen(function* () {
|
|
129
|
-
const fields = args;
|
|
130
|
-
const supplied = typeof fields.cursor === "string" ? fields.cursor : undefined;
|
|
131
|
-
const cursor = supplied !== undefined && spec.cursor === "bound"
|
|
132
|
-
? yield* boundCursor.decode(spec.name, fields, supplied)
|
|
133
|
-
: supplied;
|
|
134
|
-
const result = yield* spec.page(args, cursor);
|
|
135
|
-
const nextCursor = result.page.nextCursor !== null && spec.cursor === "bound"
|
|
136
|
-
? yield* boundCursor.encode(spec.name, fields, result.page.nextCursor)
|
|
137
|
-
: result.page.nextCursor;
|
|
138
|
-
return { [spec.key]: result.rows, page: { ...result.page, nextCursor } };
|
|
139
|
-
}),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
function standardList(connection, spec) {
|
|
143
121
|
const { default: size, max } = spec.limit;
|
|
144
122
|
if (spec.total !== undefined && spec.total !== true)
|
|
145
123
|
throw new Error(`${spec.name} total must opt in with true for a vendor-stated exact count.`);
|
package/dist/program-writes.js
CHANGED
|
@@ -4,7 +4,6 @@ import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
|
4
4
|
import { Effect } from "effect";
|
|
5
5
|
import { jsonResult } from "./meta-tools.js";
|
|
6
6
|
import { classifyWriteOutcome } from "./write-outcome.js";
|
|
7
|
-
export { classifyWriteOutcome } from "./write-outcome.js";
|
|
8
7
|
/** Writes one program may send (`execute.maxWrites`), unless configured. */
|
|
9
8
|
export const DEFAULT_MAX_WRITES = CONFIG_DEFAULTS.execute.maxWrites;
|
|
10
9
|
/** What a dispatched write's outcome says about whether it landed. */
|
|
@@ -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);
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
// A provider's sensitive fields leave every result unless deployment code
|
|
2
2
|
// returns them; no tool argument can. Shared by the providers whose raw rows
|
|
3
3
|
// and hatches would otherwise reach what their projections drop.
|
|
4
|
-
/** What `raw: true` and the hatches return, under the deployment's `sensitiveFields` choice. */
|
|
5
|
-
export function unprojectedResults(sensitiveFields, names) {
|
|
6
|
-
if (sensitiveFields === "return")
|
|
7
|
-
return (body) => body;
|
|
8
|
-
const dropped = new Set(names);
|
|
9
|
-
const strip = (value) => {
|
|
10
|
-
if (Array.isArray(value))
|
|
11
|
-
return value.map(strip);
|
|
12
|
-
if (!value || typeof value !== "object")
|
|
13
|
-
return value;
|
|
14
|
-
return Object.fromEntries(Object.entries(value)
|
|
15
|
-
.filter(([name]) => !dropped.has(name))
|
|
16
|
-
.map(([name, entry]) => [name, strip(entry)]));
|
|
17
|
-
};
|
|
18
|
-
return (body) => strip(body);
|
|
19
|
-
}
|
|
20
4
|
/** Refuse anything but the two choices, at construction (INV-11). */
|
|
21
5
|
export function sensitiveFieldsOption(provider, value) {
|
|
22
6
|
if (value === undefined)
|
|
@@ -66,7 +66,7 @@ const PATH_PATTERN = /^(\/[a-z0-9_]+)+\/?$/;
|
|
|
66
66
|
* which reads the calendar without a key, and its embed key. Neither hatch
|
|
67
67
|
* returns them.
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
function withoutCalendarFeeds(path, body) {
|
|
70
70
|
if (path !== "/events/calendars/list")
|
|
71
71
|
return body;
|
|
72
72
|
const strip = (value) => {
|
|
@@ -80,7 +80,7 @@ export function withoutCalendarFeeds(path, body) {
|
|
|
80
80
|
};
|
|
81
81
|
return strip(body);
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
function normalizedPath(raw) {
|
|
84
84
|
const path = typeof raw === "string" ? raw : "";
|
|
85
85
|
if (!PATH_PATTERN.test(path)) {
|
|
86
86
|
throw new ConnectorCallError("invalid_args", "A Breeze path is relative to /api, begins with '/', and uses only lowercase letters, digits, underscores, and '/' — for example /tags/list_folders. No query string, host, or dot segments.");
|
|
@@ -238,7 +238,7 @@ export const CCB_INDEX = new OperationIndex({
|
|
|
238
238
|
export const getMe = get("getMe", "/me", { success: Schema.Unknown });
|
|
239
239
|
export const CcbApi = indexedVendor("CCB", CCB_INDEX, [getMe]);
|
|
240
240
|
const safety = valueSafety(CCB_VALUE_SAFETY, () => CCB_INDEX);
|
|
241
|
-
|
|
241
|
+
const CCB_PAGE_HEADERS = ["x-page", "x-next-page", "x-total-pages", "x-total"];
|
|
242
242
|
export function ccbPageInfo(headers, page, size, rows) {
|
|
243
243
|
const number = (name) => {
|
|
244
244
|
const raw = headers[name];
|