@zackbart/connecta 0.37.1 → 0.39.0
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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/branding.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConnectaBranding, ConnectaTheme, UiAuthConfig } from "./types.js";
|
|
2
2
|
/** Connecta's default monochrome "C" mark. */
|
|
3
3
|
export declare const CONNECTA_FAVICON_SVG = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 32 32\">\n <style>\n .fg { fill: #000 }\n @media (prefers-color-scheme: dark) { .fg { fill: #fff } }\n </style>\n <path class=\"fg\" d=\"M27 9.4A13 13 0 1 0 27 22.6l-4.4-2.5a8 8 0 1 1 0-8.2z\"/>\n</svg>";
|
|
4
|
-
|
|
4
|
+
interface ResolvedTheme {
|
|
5
5
|
accent?: string;
|
|
6
6
|
radius?: string;
|
|
7
7
|
fontFamily?: string;
|
|
@@ -28,26 +28,12 @@ interface ResolvedBranding {
|
|
|
28
28
|
theme: ResolvedTheme;
|
|
29
29
|
}
|
|
30
30
|
export declare function resolveBranding(branding?: ConnectaBranding): ResolvedBranding;
|
|
31
|
-
/**
|
|
32
|
-
* Read a theme the way branding URLs are read: gate every field, drop what
|
|
33
|
-
* fails, never throw. This runs during `createConnecta`, so a malformed value
|
|
34
|
-
* has to fall back to the stylesheet default instead of refusing to serve the
|
|
35
|
-
* page. `droppedThemeTokens` names the drops for the startup warning.
|
|
36
|
-
*/
|
|
37
|
-
export declare function resolveTheme(theme?: ConnectaTheme): ResolvedTheme;
|
|
38
31
|
/**
|
|
39
32
|
* Names of the theme tokens the operator set that failed their gate. Same
|
|
40
33
|
* contract as `droppedBrandingUrls`: rendering falls back silently, so this is
|
|
41
34
|
* the only place an operator learns their value never reached the page.
|
|
42
35
|
*/
|
|
43
36
|
export declare function droppedThemeTokens(theme?: ConnectaTheme): string[];
|
|
44
|
-
/**
|
|
45
|
-
* The resolved theme as a `:root` block, or "" when a deployment configured
|
|
46
|
-
* nothing. It is emitted after the stylesheet so it overrides the defaults.
|
|
47
|
-
* There is no escaping here: every value has already passed a gate above, and
|
|
48
|
-
* anything that would need escaping is dropped rather than rewritten.
|
|
49
|
-
*/
|
|
50
|
-
export declare function themeCss(theme: ResolvedTheme): string;
|
|
51
37
|
/** Text or attribute value, escaped for any position in an HTML document. */
|
|
52
38
|
export declare function escapeHtml(value: string): string;
|
|
53
39
|
export interface PageLayout {
|
package/dist/branding.js
CHANGED
|
@@ -71,7 +71,7 @@ const COLOR_SCHEMES = ["system", "light", "dark"];
|
|
|
71
71
|
* has to fall back to the stylesheet default instead of refusing to serve the
|
|
72
72
|
* page. `droppedThemeTokens` names the drops for the startup warning.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
function resolveTheme(theme) {
|
|
75
75
|
const accent = trimmedString(theme?.accent);
|
|
76
76
|
const fontFamily = trimmedString(theme?.fontFamily);
|
|
77
77
|
const monoFamily = trimmedString(theme?.monoFamily);
|
|
@@ -175,7 +175,7 @@ function accentTokens(accent) {
|
|
|
175
175
|
* There is no escaping here: every value has already passed a gate above, and
|
|
176
176
|
* anything that would need escaping is dropped rather than rewritten.
|
|
177
177
|
*/
|
|
178
|
-
|
|
178
|
+
function themeCss(theme) {
|
|
179
179
|
const derived = theme.accent ? accentTokens(theme.accent) : undefined;
|
|
180
180
|
const declarations = [
|
|
181
181
|
theme.accent ? `--accent:${theme.accent}` : "",
|
package/dist/bytes.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Lowercase hexadecimal, preserving each byte's leading zero. */
|
|
2
|
+
export function bytesToHex(bytes) {
|
|
3
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
4
|
+
}
|
|
5
|
+
export async function sha256Hex(value) {
|
|
6
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))));
|
|
7
|
+
}
|
|
8
|
+
/** UTF-8 byte count without allocating encoded bytes; stops once the limit is exceeded. */
|
|
9
|
+
export function utf8Length(value, limit = Infinity) {
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
for (let i = 0; i < value.length; i++) {
|
|
12
|
+
const code = value.charCodeAt(i);
|
|
13
|
+
if (code < 0x80)
|
|
14
|
+
bytes++;
|
|
15
|
+
else if (code < 0x800)
|
|
16
|
+
bytes += 2;
|
|
17
|
+
else if (code >= 0xd800 &&
|
|
18
|
+
code <= 0xdbff &&
|
|
19
|
+
value.charCodeAt(i + 1) >= 0xdc00 &&
|
|
20
|
+
value.charCodeAt(i + 1) <= 0xdfff) {
|
|
21
|
+
bytes += 4;
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
else
|
|
25
|
+
bytes += 3;
|
|
26
|
+
if (bytes > limit)
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
export function decodeBase64(value) {
|
|
32
|
+
const binary = atob(value);
|
|
33
|
+
const bytes = new Uint8Array(binary.length);
|
|
34
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
35
|
+
bytes[index] = binary.charCodeAt(index);
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
/** Optional padding preserves callers that pad before handing input to atob. */
|
|
39
|
+
export function decodeBase64url(value, pad = false) {
|
|
40
|
+
const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
41
|
+
return decodeBase64(pad ? base64 + "=".repeat((4 - (base64.length % 4)) % 4) : base64);
|
|
42
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
export function attachCallAdmission(ctx, permit) {
|
|
4
|
-
setViewService(ctx, Budget, { charge: chargeCallBudget(permit, ctx.signal) });
|
|
5
|
-
}
|
|
1
|
+
// The Promise view of the call's admission service; authority stays internal.
|
|
2
|
+
import { Budget, callScopeKnown, chargeView, recordedService } from "./runtime/call.js";
|
|
6
3
|
export function chargeConnectorCall(ctx) {
|
|
7
4
|
return recordedService(ctx, Budget) || callScopeKnown(ctx) ? chargeView(ctx) : Promise.resolve();
|
|
8
5
|
}
|
package/dist/call-recovery.js
CHANGED
package/dist/catalog-cache.js
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import { detach, runEdge } from "./runtime/run.js";
|
|
2
2
|
import { Deferred, Effect } from "effect";
|
|
3
|
-
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authMap, authAwait, } from "./auth/native.js";
|
|
3
|
+
import { authFlow, authCall, authInvoke, authValue, authMethod, authProgram, registerAuth, authStorage, authStorageMany, authMap, authAwait, } from "./auth/native.js";
|
|
4
4
|
import { MAX_CACHE_TTL_MS, specTypeSchemas } from "@modelcontextprotocol/client";
|
|
5
5
|
import { advanceCatalogRevision, catalogRevision } from "./catalog-revision.js";
|
|
6
6
|
import { callerOf } from "./connector-caller.js";
|
|
7
7
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
8
8
|
import { Context } from "effect";
|
|
9
|
-
import { RequestScope } from "./runtime/request-scope.js";
|
|
9
|
+
import { ownerReads, RequestScope } from "./runtime/request-scope.js";
|
|
10
10
|
import { serviceFor, setViewService, viewServices } from "./runtime/call.js";
|
|
11
11
|
import { ConnectorCallError } from "./errors.js";
|
|
12
12
|
import { fingerprintSerializedCatalog } from "./catalog-fingerprint.js";
|
|
13
13
|
import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
|
|
14
14
|
import { redactCatalog, sentSecretsForRequest } from "./sent-secrets.js";
|
|
15
15
|
import { failureRecord, logFailure } from "./operator-record.js";
|
|
16
|
+
import { secretDigest } from "./storage-digest.js";
|
|
16
17
|
import { responseCacheKeys } from "./storage/keys.js";
|
|
18
|
+
import { CatalogPolicy } from "./catalog-policy.js";
|
|
19
|
+
/**
|
|
20
|
+
* An unchanged refresh leaves a baseline younger than this in place instead of
|
|
21
|
+
* rewriting it, so a baseline outlives every refresh by at least the baseline
|
|
22
|
+
* TTL minus this interval.
|
|
23
|
+
*/
|
|
24
|
+
const BASELINE_REWRITE_AGE_MS = 60 * 60 * 1000;
|
|
25
|
+
const refreshHash = (v) => typeof v === "string" && /^sha256:[0-9]+:[0-9a-f]{64}$/.test(v);
|
|
26
|
+
function refreshManifest(raw) {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const value = JSON.parse(raw);
|
|
31
|
+
if (refreshHash(value.digest) &&
|
|
32
|
+
refreshHash(value.fingerprint) &&
|
|
33
|
+
/^[0-9a-f-]{36}$/.test(value.revision) &&
|
|
34
|
+
Number.isInteger(value.chunkCount) &&
|
|
35
|
+
value.chunkCount >= 1 &&
|
|
36
|
+
value.chunkCount <= Math.ceil(MAX_SERIALIZED_CATALOG_BYTES / MAX_CATALOG_CHUNK_BYTES))
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
17
41
|
/** Publish one accepted catalog refresh, with its partitioned drift baseline. */
|
|
18
42
|
export function observeCompletedCatalogRefresh(ctx, refresh) {
|
|
19
43
|
return runEdge(authProgram(observeCompletedCatalogRefreshNative, [ctx, refresh]));
|
|
@@ -41,18 +65,10 @@ function refreshBaseline(storage, key, raw, io) {
|
|
|
41
65
|
return runEdge(authProgram(refreshBaselineNative, [storage, key, raw, io]));
|
|
42
66
|
}
|
|
43
67
|
const refreshBaselineNative = /* @__PURE__ */ registerAuth(refreshBaseline, (storage, key, raw, io) => authFlow(function* () {
|
|
44
|
-
|
|
68
|
+
const value = refreshManifest(raw);
|
|
69
|
+
if (!value)
|
|
45
70
|
return;
|
|
46
71
|
try {
|
|
47
|
-
const value = JSON.parse(raw);
|
|
48
|
-
const hash = (v) => typeof v === "string" && /^sha256:[0-9]+:[0-9a-f]{64}$/.test(v);
|
|
49
|
-
if (!hash(value.digest) ||
|
|
50
|
-
!hash(value.fingerprint) ||
|
|
51
|
-
!/^[0-9a-f-]{36}$/.test(value.revision) ||
|
|
52
|
-
!Number.isInteger(value.chunkCount) ||
|
|
53
|
-
value.chunkCount < 1 ||
|
|
54
|
-
value.chunkCount > Math.ceil(MAX_SERIALIZED_CATALOG_BYTES / MAX_CATALOG_CHUNK_BYTES))
|
|
55
|
-
return;
|
|
56
72
|
const chunks = [];
|
|
57
73
|
for (let index = 0; index < value.chunkCount; index++) {
|
|
58
74
|
const chunk = (yield authCall(io, [
|
|
@@ -68,7 +84,7 @@ const refreshBaselineNative = /* @__PURE__ */ registerAuth(refreshBaseline, (sto
|
|
|
68
84
|
const tools = JSON.parse(serialized);
|
|
69
85
|
if (Array.isArray(tools) &&
|
|
70
86
|
tools.length <= MAX_CATALOG_TOOLS &&
|
|
71
|
-
tools.every((tool) => tool &&
|
|
87
|
+
tools.every((tool) => tool && refreshHash(tool.name) && refreshHash(tool.fact)))
|
|
72
88
|
return { digest: value.digest, tools };
|
|
73
89
|
}
|
|
74
90
|
catch { }
|
|
@@ -86,6 +102,18 @@ function acceptCatalogRefresh(storage, key, connectorId, digest, tools, privateC
|
|
|
86
102
|
]));
|
|
87
103
|
}
|
|
88
104
|
const acceptCatalogRefreshNative = /* @__PURE__ */ registerAuth(acceptCatalogRefresh, (storage, key, connectorId, digest, tools, privateCatalog, current, io) => authFlow(function* () {
|
|
105
|
+
// An identical catalog cannot drift, and its baseline, written within
|
|
106
|
+
// the last hour, needs no rewrite: such a refresh writes nothing. The
|
|
107
|
+
// stored chunks always outlive their manifest, since a manifest is only
|
|
108
|
+
// ever written with fresh chunks.
|
|
109
|
+
const stored = (yield authCall(io, [
|
|
110
|
+
authMethod(() => authStorage(storage, "get", [key])),
|
|
111
|
+
]));
|
|
112
|
+
const baseline = refreshManifest(stored);
|
|
113
|
+
if (baseline?.digest === digest &&
|
|
114
|
+
baseline.writtenAt !== undefined &&
|
|
115
|
+
Date.now() - baseline.writtenAt < BASELINE_REWRITE_AGE_MS)
|
|
116
|
+
return;
|
|
89
117
|
const next = (yield authCall(catalogFingerprints, [tools]));
|
|
90
118
|
const serialized = JSON.stringify(next);
|
|
91
119
|
const revision = crypto.randomUUID();
|
|
@@ -103,13 +131,17 @@ const acceptCatalogRefreshNative = /* @__PURE__ */ registerAuth(acceptCatalogRef
|
|
|
103
131
|
])),
|
|
104
132
|
]));
|
|
105
133
|
}
|
|
106
|
-
const manifest = { digest, revision, chunkCount, fingerprint };
|
|
134
|
+
const manifest = { digest, revision, chunkCount, fingerprint, writtenAt: Date.now() };
|
|
107
135
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
108
136
|
if (!(yield authCall(current, [])))
|
|
109
137
|
return;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
// The first attempt publishes against the manifest read above; a lost
|
|
139
|
+
// compare-and-set reads it again.
|
|
140
|
+
const raw = attempt === 0
|
|
141
|
+
? stored
|
|
142
|
+
: (yield authCall(io, [
|
|
143
|
+
authMethod(() => authStorage(storage, "get", [key])),
|
|
144
|
+
]));
|
|
113
145
|
const previous = (yield authCall(refreshBaseline, [storage, key, raw, io]));
|
|
114
146
|
if (!(yield authCall(current, [])))
|
|
115
147
|
return;
|
|
@@ -204,8 +236,8 @@ export function observeCatalogExpiry(ctx, method, ttlMs, expiresAt, staleFallbac
|
|
|
204
236
|
// failure for five minutes. Resources never use stale inventories. A fallback
|
|
205
237
|
// carries its original expired deadline and cannot authorize auth recovery.
|
|
206
238
|
const STALE_FALLBACK_MS = 300_000;
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
/** Catalog chunks one storage read fetches together. */
|
|
240
|
+
const CHUNKS_PER_READ = 8;
|
|
209
241
|
export function attachCatalogCache(ctx, value) {
|
|
210
242
|
setViewService(ctx, CatalogPolicy, value);
|
|
211
243
|
}
|
|
@@ -381,7 +413,7 @@ const invalidateCatalogCacheNative = /* @__PURE__ */ registerAuth(invalidateCata
|
|
|
381
413
|
}
|
|
382
414
|
}));
|
|
383
415
|
/** Keys ignore self-reported serverInfo and bind the configured connector instead. */
|
|
384
|
-
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) {
|
|
416
|
+
export function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) {
|
|
385
417
|
return runEdge(authProgram(catalogClientOptionsNative, [
|
|
386
418
|
ctx,
|
|
387
419
|
connectorId,
|
|
@@ -391,10 +423,20 @@ export function catalogClientOptions(ctx, connectorId, config, authPartition, co
|
|
|
391
423
|
owner,
|
|
392
424
|
sharing,
|
|
393
425
|
resolveCredentialIdentity,
|
|
426
|
+
connectionless,
|
|
394
427
|
]));
|
|
395
428
|
}
|
|
396
|
-
const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOptions, (ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) => authFlow(function* () {
|
|
429
|
+
const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOptions, (ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity, connectionless = false) => authFlow(function* () {
|
|
397
430
|
const request = Context.get(viewServices(ctx), RequestScope);
|
|
431
|
+
// Generation reads that only locate an entry are memoized for this
|
|
432
|
+
// request; every acceptance or publication check reads it fresh. A
|
|
433
|
+
// generation is never reused once replaced, so a stale one only
|
|
434
|
+
// addresses an entry that the fresh check then refuses.
|
|
435
|
+
const reads = ownerReads(request);
|
|
436
|
+
const generationRead = (memoized) => {
|
|
437
|
+
const key = responseCacheKeys.generation(connectorId);
|
|
438
|
+
return reads.read(storage, key, authStorage(storage, "get", [key]), !memoized);
|
|
439
|
+
};
|
|
398
440
|
// SDK callbacks start at their own Promise edge. Reprovide only their
|
|
399
441
|
// canonical request owner, never the initiating call's live services.
|
|
400
442
|
const cacheMethod = (program) => authMethod((...args) => program(...args).pipe(Effect.provideService(RequestScope, request)));
|
|
@@ -443,18 +485,21 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
443
485
|
};
|
|
444
486
|
let lastStamp = 0;
|
|
445
487
|
let staleTools;
|
|
446
|
-
const currentGeneration = cacheMethod(({ stopped, io, generation }) => authFlow(function* () {
|
|
488
|
+
const currentGeneration = cacheMethod(({ stopped, io, generation }, memoized = false) => authFlow(function* () {
|
|
447
489
|
const key = responseCacheKeys.generation(connectorId);
|
|
448
490
|
for (let attempt = 0; attempt < 16; attempt++) {
|
|
449
491
|
if (stopped())
|
|
450
492
|
return undefined;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
]));
|
|
493
|
+
// A retry after a lost claim always reads fresh.
|
|
494
|
+
const current = (yield authCall(io, [cacheMethod(() => generationRead(memoized && attempt === 0))]));
|
|
454
495
|
if (stopped())
|
|
455
496
|
return undefined;
|
|
456
497
|
if (current)
|
|
457
498
|
return current;
|
|
499
|
+
// A connectionless reader never claims or recreates a generation: a
|
|
500
|
+
// missing one is a miss at every check, never a reused namespace.
|
|
501
|
+
if (connectionless)
|
|
502
|
+
return undefined;
|
|
458
503
|
const next = generation.value ?? crypto.randomUUID();
|
|
459
504
|
const claimed = (yield authCall(io, [
|
|
460
505
|
cacheMethod(() => authStorage(storage, "compareAndSet", [
|
|
@@ -471,12 +516,12 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
471
516
|
}
|
|
472
517
|
throw new Error("Catalog cache generation is busy.");
|
|
473
518
|
}));
|
|
474
|
-
const namespace = cacheMethod((operation) => authFlow(function* () {
|
|
519
|
+
const namespace = cacheMethod((operation, memoized = false) => authFlow(function* () {
|
|
475
520
|
const { stopped, generation, io } = operation;
|
|
476
521
|
if (resolveCredentialIdentity &&
|
|
477
522
|
(yield authCall(io, [resolveCredentialIdentity])) !== operation.credentialIdentity)
|
|
478
523
|
return undefined;
|
|
479
|
-
const current = (yield authCall(currentGeneration, [operation]));
|
|
524
|
+
const current = (yield authCall(currentGeneration, [operation, memoized]));
|
|
480
525
|
if (!current || stopped())
|
|
481
526
|
return undefined;
|
|
482
527
|
generation.value ??= current;
|
|
@@ -499,7 +544,8 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
499
544
|
pair.length !== 2 ||
|
|
500
545
|
(pair[1] !== partition && (pair[1] !== "" || sharing !== "shared")))
|
|
501
546
|
return undefined;
|
|
502
|
-
|
|
547
|
+
// Locating only: get's fence and set's publication re-check fresh.
|
|
548
|
+
const prefix = (yield authCall(namespace, [operation, true]));
|
|
503
549
|
if (!prefix || stopped())
|
|
504
550
|
return undefined;
|
|
505
551
|
const digest = (yield authCall(fingerprintSerializedCatalog, [
|
|
@@ -601,16 +647,20 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
601
647
|
(m.expiresAt <= Date.now() &&
|
|
602
648
|
(key.method !== "tools/list" || m.expiresAt + STALE_FALLBACK_MS <= Date.now())))
|
|
603
649
|
return undefined;
|
|
650
|
+
// Up to CHUNKS_PER_READ chunks per round trip, so one answer stays a
|
|
651
|
+
// few megabytes; the generation fence below still decides whether
|
|
652
|
+
// what was read may be served.
|
|
604
653
|
const chunks = [];
|
|
605
|
-
for (let
|
|
654
|
+
for (let first = 0; first < m.chunkCount; first += CHUNKS_PER_READ) {
|
|
606
655
|
if (stopped())
|
|
607
656
|
return undefined;
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
657
|
+
const keys = Array.from({ length: Math.min(CHUNKS_PER_READ, m.chunkCount - first) }, (_, i) => responseCacheKeys.chunk(root, m.revision, first + i));
|
|
658
|
+
const read = (yield authCall(io, [cacheMethod(() => authStorageMany(storage, keys))]));
|
|
659
|
+
for (const chunk of read) {
|
|
660
|
+
if (stopped() || chunk === null || new TextEncoder().encode(chunk).byteLength > MAX_CATALOG_CHUNK_BYTES)
|
|
661
|
+
return undefined;
|
|
662
|
+
chunks.push(chunk);
|
|
663
|
+
}
|
|
614
664
|
}
|
|
615
665
|
const value = chunks.join("");
|
|
616
666
|
const fingerprint = (yield authCall(fingerprintSerializedCatalog, [value]));
|
|
@@ -752,10 +802,11 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
752
802
|
};
|
|
753
803
|
try {
|
|
754
804
|
const { stopped, io } = capture({ ctx, generation: connectionGeneration, ended: false });
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
805
|
+
// A connectionless reader only lists, and each listing reads its own
|
|
806
|
+
// generation; there are no connection-owned writes to fence.
|
|
807
|
+
if (!connectionless && !stopped()) {
|
|
808
|
+
// Memoized: a connection's writes publish only after a fresh check.
|
|
809
|
+
const current = (yield authCall(io, [cacheMethod(() => generationRead(true))]));
|
|
759
810
|
if (!stopped())
|
|
760
811
|
connectionGeneration.value = current ?? crypto.randomUUID();
|
|
761
812
|
}
|
|
@@ -792,9 +843,13 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
792
843
|
listing.credentialIdentity = yield authInvoke(operation, "io", [resolveCredentialIdentity]);
|
|
793
844
|
if (!operation.stopped()) {
|
|
794
845
|
try {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
846
|
+
// Memoized: it locates the listing's entries, which the
|
|
847
|
+
// fresh fence in get accepts or refuses.
|
|
848
|
+
const stored = (yield authInvoke(operation, "io", [
|
|
849
|
+
cacheMethod(() => generationRead(true)),
|
|
850
|
+
]));
|
|
851
|
+
const generation = stored ?? crypto.randomUUID();
|
|
852
|
+
listing.generationStored = stored !== null;
|
|
798
853
|
listing.generation.value = generation;
|
|
799
854
|
connectionGeneration.value = generation;
|
|
800
855
|
}
|
|
@@ -820,6 +875,34 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
820
875
|
void previous.then(release);
|
|
821
876
|
}
|
|
822
877
|
}));
|
|
878
|
+
// A complete, unexpired tools/list entry, read before any connection
|
|
879
|
+
// exists. This is the SDK's own two-probe read: the private slot
|
|
880
|
+
// `[serverIdentity, cachePartition]`, then a shared connector's public
|
|
881
|
+
// slot `[serverIdentity, ""]`, served only when written as public.
|
|
882
|
+
// `address` binds the configured connector rather than the self-reported
|
|
883
|
+
// identity, so the pre-connect `""` identity selects the same entries.
|
|
884
|
+
// Expired, partial, refused (`ttlMs: 0`) and invalidated entries miss,
|
|
885
|
+
// and the read writes nothing.
|
|
886
|
+
const cachedTools = cacheMethod((listingCtx) => authCall(withListing, [
|
|
887
|
+
listingCtx,
|
|
888
|
+
cacheMethod(() => authFlow(function* () {
|
|
889
|
+
// No stored generation means no entry, and claiming one is a write.
|
|
890
|
+
if (!active?.generationStored)
|
|
891
|
+
return undefined;
|
|
892
|
+
const read = (slot) => authInvoke(store, "get", [
|
|
893
|
+
{ method: "tools/list", params: "", partition: JSON.stringify(["", slot]) },
|
|
894
|
+
]);
|
|
895
|
+
let entry = (yield read(partition));
|
|
896
|
+
if (entry === undefined && sharing === "shared") {
|
|
897
|
+
const shared = (yield read(""));
|
|
898
|
+
entry = shared?.scope === "public" ? shared : undefined;
|
|
899
|
+
}
|
|
900
|
+
if (entry?.expiresAt === undefined || !(entry.expiresAt > Date.now()))
|
|
901
|
+
return undefined;
|
|
902
|
+
const result = JSON.parse(entry.value);
|
|
903
|
+
return Array.isArray(result.tools) ? result : undefined;
|
|
904
|
+
})),
|
|
905
|
+
]));
|
|
823
906
|
const fallbackTools = cacheMethod(() => authFlow(function* () {
|
|
824
907
|
const operation = capture();
|
|
825
908
|
if (!staleTools ||
|
|
@@ -837,6 +920,7 @@ const catalogClientOptionsNative = /* @__PURE__ */ registerAuth(catalogClientOpt
|
|
|
837
920
|
intake,
|
|
838
921
|
completeCatalogRefresh,
|
|
839
922
|
fallbackTools,
|
|
923
|
+
cachedTools,
|
|
840
924
|
responseCacheStore: store,
|
|
841
925
|
cachePartition: partition,
|
|
842
926
|
defaultCacheTtlMs: policy?.defaultTtlMs ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds * 1000,
|
|
@@ -855,7 +939,10 @@ function customCatalogCache(ctx, connectorId) {
|
|
|
855
939
|
}
|
|
856
940
|
const customCatalogCacheNative = /* @__PURE__ */ registerAuth(customCatalogCache, (ctx, connectorId) => authFlow(function* () {
|
|
857
941
|
const credential = (yield authValue(() => ctx.credential?.getAll()));
|
|
858
|
-
|
|
942
|
+
// Credential values enter the stored partition only through a keyed digest.
|
|
943
|
+
const auth = credential == null
|
|
944
|
+
? (yield authCall(fingerprintSerializedCatalog, ["null"])).fingerprint
|
|
945
|
+
: (yield authCall(secretDigest, [ctx, JSON.stringify(credential)]));
|
|
859
946
|
const cache = (yield authCall(catalogClientOptions, [ctx, connectorId, "custom-connector", auth]));
|
|
860
947
|
const key = {
|
|
861
948
|
method: "tools/list",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { bytesToHex } from "./bytes.js";
|
|
1
2
|
const encoder = new TextEncoder();
|
|
2
3
|
async function fingerprintBytes(bytes) {
|
|
3
4
|
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)));
|
|
4
|
-
const hex =
|
|
5
|
+
const hex = bytesToHex(digest);
|
|
5
6
|
return `sha256:${bytes.byteLength}:${hex}`;
|
|
6
7
|
}
|
|
7
8
|
/** Fingerprint an already serialized catalog without parsing/reserializing it. */
|
|
@@ -12,20 +13,3 @@ export async function fingerprintSerializedCatalog(serializedTools) {
|
|
|
12
13
|
byteLength: bytes.byteLength,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Capture every persisted/discovered field in one deterministic snapshot.
|
|
17
|
-
* Object key order is allowed to cause a conservative extra write; changing
|
|
18
|
-
* any serialized field must change the digest.
|
|
19
|
-
*/
|
|
20
|
-
export async function snapshotCatalog(tools) {
|
|
21
|
-
const serializedTools = JSON.stringify(tools);
|
|
22
|
-
if (serializedTools === undefined) {
|
|
23
|
-
throw new TypeError("Tool catalog is not JSON-serializable.");
|
|
24
|
-
}
|
|
25
|
-
const bytes = encoder.encode(serializedTools);
|
|
26
|
-
return {
|
|
27
|
-
tools,
|
|
28
|
-
fingerprint: await fingerprintBytes(bytes),
|
|
29
|
-
serializedBytes: bytes,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The catalog cache's per-view policy: its store, owner partition and TTL
|
|
2
|
+
// bounds, attached to each connector context by the registry. Kept apart from
|
|
3
|
+
// the cache itself so bounded facts stored beside it (connector health, the
|
|
4
|
+
// operator page's status observations) reach the partition without the MCP
|
|
5
|
+
// client the cache adapter imports.
|
|
6
|
+
import { Context } from "effect";
|
|
7
|
+
import { callerOf } from "./connector-caller.js";
|
|
8
|
+
import { serviceFor } from "./runtime/call.js";
|
|
9
|
+
export class CatalogPolicy extends Context.Service()("connecta/operation/CatalogPolicy") {
|
|
10
|
+
}
|
|
11
|
+
/** The store and owner partition this connector's catalog cache uses, for bounded facts kept beside it. */
|
|
12
|
+
export function catalogOwnerPartition(ctx) {
|
|
13
|
+
const policy = serviceFor(ctx, CatalogPolicy);
|
|
14
|
+
return {
|
|
15
|
+
storage: policy?.storage ?? ctx.storage,
|
|
16
|
+
partition: JSON.stringify([policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null), ctx.baseUrl, ctx.publicUrl]),
|
|
17
|
+
};
|
|
18
|
+
}
|