@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.
Files changed (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -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
- export interface ResolvedTheme {
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
- export function resolveTheme(theme) {
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
- export function themeCss(theme) {
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
- import { Budget, callScopeKnown, chargeView, recordedService, setViewService } from "./runtime/call.js";
2
- import { chargeCallBudget } from "./runtime/call-admission.js";
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
  }
@@ -43,6 +43,3 @@ export function recordRecovery(ctx, facts) {
43
43
  recordFor(ctx);
44
44
  serviceFor(ctx, Recovery)?.recordNow?.(facts);
45
45
  }
46
- export function recoveryFor(ctx) {
47
- return ctx ? serviceFor(ctx, RecordedRecovery)?.facts : undefined;
48
- }
@@ -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
- if (!raw)
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 && hash(tool.name) && hash(tool.fact)))
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
- const raw = (yield authCall(io, [
111
- authMethod(() => authStorage(storage, "get", [key])),
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
- class CatalogPolicy extends Context.Service()("connecta/operation/CatalogPolicy") {
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
- const current = (yield authCall(io, [
452
- cacheMethod(() => authStorage(storage, "get", [key])),
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
- const prefix = (yield authCall(namespace, [operation]));
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 i = 0; i < m.chunkCount; i++) {
654
+ for (let first = 0; first < m.chunkCount; first += CHUNKS_PER_READ) {
606
655
  if (stopped())
607
656
  return undefined;
608
- const chunk = (yield authCall(io, [
609
- cacheMethod(() => authStorage(storage, "get", [responseCacheKeys.chunk(root, m.revision, i)])),
610
- ]));
611
- if (stopped() || chunk === null || new TextEncoder().encode(chunk).byteLength > MAX_CATALOG_CHUNK_BYTES)
612
- return undefined;
613
- chunks.push(chunk);
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
- if (!stopped()) {
756
- const current = (yield authCall(io, [
757
- cacheMethod(() => authStorage(storage, "get", [responseCacheKeys.generation(connectorId)])),
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
- const generation = (yield authInvoke(operation, "io", [
796
- cacheMethod(() => authStorage(storage, "get", [responseCacheKeys.generation(connectorId)])),
797
- ])) ?? crypto.randomUUID();
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
- const auth = (yield authCall(fingerprintSerializedCatalog, [JSON.stringify(credential ?? null)])).fingerprint;
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 = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");
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
+ }