@zackbart/connecta 0.28.1 → 0.29.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 (319) hide show
  1. package/CHANGELOG.md +788 -5
  2. package/README.md +62 -42
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1584 -1580
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +93 -71
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +59 -7
  52. package/dist/connectors/api-connector.js +106 -19
  53. package/dist/connectors/api.js +4 -1
  54. package/dist/connectors/guarded-fetch.d.ts +144 -0
  55. package/dist/connectors/guarded-fetch.js +84 -35
  56. package/dist/connectors/negotiation-cache.js +46 -0
  57. package/dist/connectors/option-shapes.js +27 -0
  58. package/dist/connectors/remote-mcp.d.ts +30 -4
  59. package/dist/connectors/remote-mcp.js +1774 -320
  60. package/dist/connectors/resource-uri.js +335 -0
  61. package/dist/credential-contract.d.ts +10 -0
  62. package/dist/credential-rules.js +1 -3
  63. package/dist/credential-url.js +108 -0
  64. package/dist/credentials.d.ts +6 -1
  65. package/dist/credentials.js +27 -18
  66. package/dist/d1.d.ts +130 -0
  67. package/dist/d1.js +290 -0
  68. package/dist/describe-config.d.ts +165 -0
  69. package/dist/describe-config.js +315 -0
  70. package/dist/described.js +106 -0
  71. package/dist/downstream-input-context.js +119 -0
  72. package/dist/downstream-input.js +323 -0
  73. package/dist/errors.d.ts +97 -20
  74. package/dist/errors.js +158 -80
  75. package/dist/execute.js +453 -305
  76. package/dist/executor-admission.d.ts +1 -0
  77. package/dist/executor-admission.js +11 -15
  78. package/dist/executor-contract.d.ts +20 -0
  79. package/dist/executor-contract.js +48 -0
  80. package/dist/executor-result.js +24 -9
  81. package/dist/executors/quickjs-child.js +3 -4
  82. package/dist/executors/quickjs-runtime.js +147 -59
  83. package/dist/executors/quickjs.js +29 -35
  84. package/dist/favicon.js +1 -2
  85. package/dist/fix-prompt.js +1 -3
  86. package/dist/guest-runtime.js +188 -0
  87. package/dist/guest-types.d.ts +176 -0
  88. package/dist/guest-types.js +1 -0
  89. package/dist/html-security.js +24 -0
  90. package/dist/identity.js +1 -3
  91. package/dist/inbound-credential.js +52 -0
  92. package/dist/index.d.ts +24 -256
  93. package/dist/index.js +65 -478
  94. package/dist/invocation-auth.js +69 -0
  95. package/dist/invocation.js +230 -73
  96. package/dist/json-schema.d.ts +1 -1
  97. package/dist/mcp-client-context.js +35 -0
  98. package/dist/mcp-result.js +11 -7
  99. package/dist/meta-output.js +76 -0
  100. package/dist/meta-tool-names.d.ts +2 -0
  101. package/dist/meta-tool-names.js +3 -0
  102. package/dist/meta-tools.js +285 -209
  103. package/dist/module-contracts.d.ts +8 -23
  104. package/dist/node.d.ts +0 -2
  105. package/dist/node.js +2 -2
  106. package/dist/oauth-callback-outcome.js +1 -3
  107. package/dist/oauth-handoff.js +117 -0
  108. package/dist/oauth-sealing.js +20 -0
  109. package/dist/operator-record.d.ts +137 -0
  110. package/dist/operator-record.js +420 -0
  111. package/dist/operator-ui/assets.js +31 -0
  112. package/dist/operator-ui/contract.d.ts +72 -0
  113. package/dist/operator-ui/contract.js +1 -0
  114. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  115. package/dist/operator-ui/generated.js +5 -4
  116. package/dist/operator-ui/model.d.ts +12 -87
  117. package/dist/operator-ui/model.js +2 -9
  118. package/dist/operator-ui/setup-commands.js +1 -3
  119. package/dist/operator-ui/view.d.ts +323 -0
  120. package/dist/operator-ui/view.js +62 -189
  121. package/dist/page-styles.js +2 -3
  122. package/dist/program-source.js +14 -2
  123. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  124. package/dist/provider.d.ts +142 -0
  125. package/dist/provider.js +187 -0
  126. package/dist/providers/_shared/google/result-size.js +57 -0
  127. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  128. package/dist/providers/_shared/google/service-account.js +500 -0
  129. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  130. package/dist/providers/_shared/google/workspace.js +518 -0
  131. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  132. package/dist/providers/basecamp/index.js +997 -0
  133. package/dist/providers/basecamp/skill.generated.js +9 -0
  134. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  135. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  136. package/dist/providers/breeze/skill.generated.js +9 -0
  137. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  138. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  139. package/dist/providers/ccb/skill.generated.js +9 -0
  140. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  141. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  142. package/dist/providers/cloudflare/skill.generated.js +10 -0
  143. package/dist/providers/docs/index.d.ts +54 -0
  144. package/dist/providers/docs/index.js +1237 -0
  145. package/dist/providers/docs/skill.generated.js +9 -0
  146. package/dist/providers/drive/index.d.ts +53 -0
  147. package/dist/providers/drive/index.js +1523 -0
  148. package/dist/providers/drive/skill.generated.js +9 -0
  149. package/dist/providers/forms/index.d.ts +54 -0
  150. package/dist/providers/forms/index.js +1217 -0
  151. package/dist/providers/forms/skill.generated.js +11 -0
  152. package/dist/providers/github/auth.d.ts +32 -0
  153. package/dist/providers/github/auth.js +361 -0
  154. package/dist/providers/github/hosted.js +218 -0
  155. package/dist/providers/github/index.d.ts +13 -0
  156. package/dist/providers/github/index.js +140 -0
  157. package/dist/providers/github/key.js +100 -0
  158. package/dist/providers/github/rest.js +321 -0
  159. package/dist/providers/github/scope.d.ts +29 -0
  160. package/dist/providers/github/scope.js +124 -0
  161. package/dist/providers/github/skill.generated.js +8 -0
  162. package/dist/providers/gmail/index.d.ts +51 -0
  163. package/dist/providers/gmail/index.js +1436 -0
  164. package/dist/providers/gmail/skill.generated.js +9 -0
  165. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  166. package/dist/providers/linear/index.js +352 -0
  167. package/dist/providers/linear/skill.generated.js +8 -0
  168. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  169. package/dist/providers/mixpanel/index.js +416 -0
  170. package/dist/providers/mixpanel/skill.generated.js +9 -0
  171. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  172. package/dist/providers/{notion.js → notion/index.js} +226 -239
  173. package/dist/providers/notion/skill.generated.js +10 -0
  174. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  175. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  176. package/dist/providers/overflow/skill.generated.js +8 -0
  177. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  178. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  179. package/dist/providers/planning-center/skill.generated.js +10 -0
  180. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  181. package/dist/providers/revenuecat/index.js +557 -0
  182. package/dist/providers/revenuecat/skill.generated.js +11 -0
  183. package/dist/providers/sheets/index.d.ts +54 -0
  184. package/dist/providers/sheets/index.js +1237 -0
  185. package/dist/providers/sheets/skill.generated.js +10 -0
  186. package/dist/providers/slides/index.d.ts +53 -0
  187. package/dist/providers/slides/index.js +2645 -0
  188. package/dist/providers/slides/skill.generated.js +9 -0
  189. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  190. package/dist/providers/stripe/index.js +266 -0
  191. package/dist/providers/stripe/skill.generated.js +11 -0
  192. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  193. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  194. package/dist/providers/tithely/skill.generated.js +8 -0
  195. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  196. package/dist/providers/vercel/index.js +1038 -0
  197. package/dist/providers/vercel/mcp-ownership.js +212 -0
  198. package/dist/providers/vercel/skill.generated.js +11 -0
  199. package/dist/registry.d.ts +108 -171
  200. package/dist/registry.js +513 -925
  201. package/dist/request-state.js +21 -0
  202. package/dist/resource-read.js +100 -0
  203. package/dist/result-shapes.js +17 -49
  204. package/dist/retired-toolkits.js +1 -1
  205. package/dist/routes/access-tokens.js +3 -6
  206. package/dist/routes/activity-context.js +21 -0
  207. package/dist/routes/activity-disclosure.js +40 -0
  208. package/dist/routes/activity.js +59 -21
  209. package/dist/routes/actor-labels.js +3 -50
  210. package/dist/routes/connect.js +221 -0
  211. package/dist/routes/credentials.js +15 -27
  212. package/dist/routes/mcp.js +356 -130
  213. package/dist/routes/oauth-client-metadata.js +15 -0
  214. package/dist/routes/oauth-management.js +45 -107
  215. package/dist/routes/oauth.js +110 -57
  216. package/dist/routes/operator.js +3 -3
  217. package/dist/routes/shared.d.ts +25 -64
  218. package/dist/routes/shared.js +160 -80
  219. package/dist/routes/skills.js +30 -0
  220. package/dist/routes/ui-config.js +225 -0
  221. package/dist/routes/ui-data.js +40 -35
  222. package/dist/routes/ui.js +66 -56
  223. package/dist/runtime/admission.js +4 -2
  224. package/dist/runtime/call-admission.js +3 -1
  225. package/dist/runtime/connector-scope.js +1 -4
  226. package/dist/runtime/run.js +10 -39
  227. package/dist/runtime/services.js +12 -117
  228. package/dist/runtime/shared-read.js +1 -3
  229. package/dist/runtime/storage.js +8 -1
  230. package/dist/sent-secrets.d.ts +52 -0
  231. package/dist/sent-secrets.js +474 -0
  232. package/dist/server.js +28 -71
  233. package/dist/skills.js +406 -174
  234. package/dist/sqlite.d.ts +48 -0
  235. package/dist/sqlite.js +166 -0
  236. package/dist/storage/keys.js +428 -0
  237. package/dist/storage/memory.js +7 -0
  238. package/dist/storage/sql.d.ts +66 -0
  239. package/dist/storage/sql.js +534 -0
  240. package/dist/timeout.js +4 -2
  241. package/dist/tool-name.js +17 -0
  242. package/dist/tool-safety.d.ts +9 -35
  243. package/dist/tool-safety.js +20 -40
  244. package/dist/types.d.ts +320 -54
  245. package/dist/types.js +0 -1
  246. package/dist/ui.d.ts +7 -19
  247. package/dist/ui.js +76 -90
  248. package/dist/url-safety.js +3 -6
  249. package/dist/usage-guide.js +137 -0
  250. package/dist/validate.d.ts +7 -0
  251. package/dist/validate.js +309 -47
  252. package/dist/version.d.ts +1 -7
  253. package/dist/version.js +3 -7
  254. package/dist/worker.d.ts +11 -0
  255. package/dist/worker.js +434 -0
  256. package/documentation/architecture.md +664 -231
  257. package/documentation/auth.md +936 -546
  258. package/documentation/code-mode.md +501 -312
  259. package/documentation/meta-tools.md +449 -200
  260. package/documentation/operator-tests.md +93 -0
  261. package/documentation/provider-migration-0.29.md +60 -0
  262. package/examples/worker/AGENTS.md +34 -12
  263. package/examples/worker/README.md +286 -255
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  266. package/examples/worker/src/connecta.config.ts +151 -0
  267. package/examples/worker/src/index.ts +14 -197
  268. package/examples/worker/wrangler.jsonc +15 -35
  269. package/package.json +120 -54
  270. package/templates/node/.dockerignore +1 -2
  271. package/templates/node/.env.example +20 -21
  272. package/templates/node/AGENTS.md +21 -17
  273. package/templates/node/Dockerfile +3 -5
  274. package/templates/node/README.md +133 -58
  275. package/templates/node/docker-compose.yml +11 -12
  276. package/templates/node/package.json +4 -2
  277. package/templates/node/src/connecta.config.ts +119 -0
  278. package/templates/node/src/index.ts +9 -154
  279. package/templates/node/src/provision-token.ts +17 -0
  280. package/AGENTS.md +0 -170
  281. package/dist/artifacts/connector.d.ts +0 -58
  282. package/dist/artifacts/connector.js +0 -730
  283. package/dist/artifacts/document.js +0 -86
  284. package/dist/artifacts/guide.js +0 -140
  285. package/dist/artifacts/html-scan.js +0 -329
  286. package/dist/artifacts/json.js +0 -78
  287. package/dist/artifacts/kv-store.d.ts +0 -20
  288. package/dist/artifacts/kv-store.js +0 -140
  289. package/dist/artifacts/markdown.js +0 -478
  290. package/dist/artifacts/module.d.ts +0 -37
  291. package/dist/artifacts/module.js +0 -66
  292. package/dist/artifacts/operations.d.ts +0 -197
  293. package/dist/artifacts/operations.js +0 -1002
  294. package/dist/artifacts/refresh.d.ts +0 -55
  295. package/dist/artifacts/refresh.js +0 -236
  296. package/dist/artifacts/routes.js +0 -206
  297. package/dist/artifacts/types.d.ts +0 -214
  298. package/dist/artifacts/types.js +0 -22
  299. package/dist/artifacts/validate.d.ts +0 -62
  300. package/dist/artifacts/validate.js +0 -651
  301. package/dist/artifacts.d.ts +0 -6
  302. package/dist/artifacts.js +0 -9
  303. package/dist/auth/bearer.d.ts +0 -11
  304. package/dist/auth/bearer.js +0 -50
  305. package/dist/providers/basecamp.js +0 -445
  306. package/dist/providers/linear.js +0 -217
  307. package/dist/providers/mixpanel.js +0 -247
  308. package/dist/providers/revenuecat.js +0 -317
  309. package/dist/providers/stripe.js +0 -253
  310. package/dist/providers/vercel.js +0 -1253
  311. package/dist/storage/file.d.ts +0 -15
  312. package/dist/storage/file.js +0 -455
  313. package/ethos.md +0 -93
  314. package/examples/worker/src/cloudflare-kv.ts +0 -43
  315. package/examples/worker/src/d1-activity-row.ts +0 -155
  316. package/examples/worker/src/d1-activity.ts +0 -147
  317. package/examples/worker/src/d1-storage.ts +0 -138
  318. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  319. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,739 @@
1
+ import { MAX_CACHE_TTL_MS, specTypeSchemas } from "@modelcontextprotocol/client";
2
+ import { callerOf } from "./connector-caller.js";
3
+ import { CONFIG_DEFAULTS } from "./config-defaults.js";
4
+ import { ConnectorCallError } from "./errors.js";
5
+ import { fingerprintSerializedCatalog } from "./catalog-fingerprint.js";
6
+ import { MAX_CATALOG_CHUNK_BYTES, MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "./catalog-limits.js";
7
+ import { redactCatalog, sentSecretsForRequest } from "./sent-secrets.js";
8
+ import { failureRecord, logFailure } from "./operator-record.js";
9
+ import { responseCacheKeys } from "./storage/keys.js";
10
+ /** Publish one accepted catalog refresh, with its partitioned drift baseline. */
11
+ export async function observeCompletedCatalogRefresh(ctx, refresh) {
12
+ await settings.get(ctx)?.onCompletedCatalogRefresh?.(refresh, ctx);
13
+ }
14
+ /** Hash-only facts preserve change counts without storing tool names or schemas. */
15
+ async function catalogFingerprints(tools) {
16
+ const facts = [];
17
+ for (let offset = 0; offset < tools.length; offset += 128) {
18
+ facts.push(...(await Promise.all(tools.slice(offset, offset + 128).map(async (tool) => ({
19
+ name: (await fingerprintSerializedCatalog(tool.name)).fingerprint,
20
+ fact: (await fingerprintSerializedCatalog(JSON.stringify(tool))).fingerprint,
21
+ })))));
22
+ }
23
+ return facts;
24
+ }
25
+ async function refreshBaseline(storage, key, raw, io) {
26
+ if (!raw)
27
+ return;
28
+ try {
29
+ const value = JSON.parse(raw);
30
+ const hash = (v) => typeof v === "string" && /^sha256:[0-9]+:[0-9a-f]{64}$/.test(v);
31
+ if (!hash(value.digest) ||
32
+ !hash(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;
38
+ const chunks = [];
39
+ for (let index = 0; index < value.chunkCount; index++) {
40
+ const chunk = await io(() => storage.get(responseCacheKeys.chunk(key, value.revision, index)));
41
+ if (chunk === null || new TextEncoder().encode(chunk).byteLength > MAX_CATALOG_CHUNK_BYTES)
42
+ return;
43
+ chunks.push(chunk);
44
+ }
45
+ const serialized = chunks.join("");
46
+ if ((await fingerprintSerializedCatalog(serialized)).fingerprint !== value.fingerprint)
47
+ return;
48
+ const tools = JSON.parse(serialized);
49
+ if (Array.isArray(tools) &&
50
+ tools.length <= MAX_CATALOG_TOOLS &&
51
+ tools.every((tool) => tool && hash(tool.name) && hash(tool.fact)))
52
+ return { digest: value.digest, tools };
53
+ }
54
+ catch {
55
+ /* Retired or torn baselines establish a new count baseline. */
56
+ }
57
+ }
58
+ async function acceptCatalogRefresh(storage, key, connectorId, digest, tools, privateCatalog, current, io) {
59
+ const next = await catalogFingerprints(tools);
60
+ const serialized = JSON.stringify(next);
61
+ const revision = crypto.randomUUID();
62
+ const fingerprint = (await fingerprintSerializedCatalog(serialized)).fingerprint;
63
+ // Fingerprints are ASCII. Chunk them under the same platform value ceiling
64
+ // as catalogs, then publish their manifest in one compare-and-set.
65
+ const chunkCount = Math.ceil(serialized.length / MAX_CATALOG_CHUNK_BYTES);
66
+ for (let index = 0; index < chunkCount; index++) {
67
+ if (!(await current()))
68
+ return;
69
+ const chunk = serialized.slice(index * MAX_CATALOG_CHUNK_BYTES, (index + 1) * MAX_CATALOG_CHUNK_BYTES);
70
+ await io(() => storage.set(responseCacheKeys.chunk(key, revision, index), chunk, { ttlSeconds: GENERATION_TTL_SECONDS + 300 }));
71
+ }
72
+ const manifest = { digest, revision, chunkCount, fingerprint };
73
+ for (let attempt = 0; attempt < 16; attempt++) {
74
+ if (!(await current()))
75
+ return;
76
+ const raw = await io(() => storage.get(key));
77
+ const previous = await refreshBaseline(storage, key, raw, io);
78
+ if (!(await current()))
79
+ return;
80
+ const published = await io(() => storage.compareAndSet(key, raw, JSON.stringify(manifest), { ttlSeconds: GENERATION_TTL_SECONDS }));
81
+ if (!(await current()))
82
+ return;
83
+ if (published)
84
+ return {
85
+ connectorId,
86
+ digest,
87
+ next,
88
+ private: privateCatalog,
89
+ ...(previous ? { previousDigest: previous.digest, previous: previous.tools } : {}),
90
+ };
91
+ }
92
+ throw new Error("Catalog refresh baseline is busy.");
93
+ }
94
+ /** Custom dynamic connectors have no SDK cache; publish their complete intake here. */
95
+ export async function observeUncachedCatalogRefresh(ctx, connectorId, tools) {
96
+ const policy = settings.get(ctx);
97
+ if (!policy?.onCompletedCatalogRefresh)
98
+ return;
99
+ const config = (await fingerprintSerializedCatalog("custom-connector")).fingerprint;
100
+ const partition = (await fingerprintSerializedCatalog(JSON.stringify([policy.partition, ctx.baseUrl, ctx.publicUrl])))
101
+ .fingerprint;
102
+ const digest = (await fingerprintSerializedCatalog(JSON.stringify(tools))).fingerprint;
103
+ const refresh = await acceptCatalogRefresh(policy.storage, responseCacheKeys.refreshDigest(connectorId, config, partition), connectorId, digest, tools, true, async () => !ctx.signal?.aborted, (read) => read());
104
+ if (refresh)
105
+ await observeCompletedCatalogRefresh(ctx, refresh);
106
+ }
107
+ export function catalogItems(method, result) {
108
+ if (method === "tools/list")
109
+ return result.tools;
110
+ if (method === "resources/list")
111
+ return result.resources;
112
+ return result.resourceTemplates;
113
+ }
114
+ function catalogSchema(method) {
115
+ return method === "tools/list"
116
+ ? specTypeSchemas.ListToolsResult
117
+ : method === "resources/list"
118
+ ? specTypeSchemas.ListResourcesResult
119
+ : specTypeSchemas.ListResourceTemplatesResult;
120
+ }
121
+ const expiry = new WeakMap();
122
+ const fetches = new WeakMap();
123
+ export function observeCatalogFetch(ctx, at, method = "tools/list") {
124
+ const methods = fetches.get(ctx) ?? new Map();
125
+ methods.set(method, at);
126
+ fetches.set(ctx, methods);
127
+ }
128
+ export function catalogFetchedAt(ctx, method = "tools/list") {
129
+ return fetches.get(ctx)?.get(method);
130
+ }
131
+ export function catalogExpiry(ctx, method = "tools/list") {
132
+ return expiry.get(ctx)?.get(method);
133
+ }
134
+ export function observeCatalogExpiry(ctx, method, ttlMs, expiresAt, staleFallback = false) {
135
+ const fetchedAt = catalogFetchedAt(ctx, method) ?? Date.now();
136
+ const methods = expiry.get(ctx) ?? new Map();
137
+ methods.set(method, {
138
+ fetchedAt,
139
+ ttlMs,
140
+ expiresAt: Math.min(expiresAt ?? Infinity, fetchedAt + ttlMs),
141
+ staleFallback,
142
+ });
143
+ expiry.set(ctx, methods);
144
+ }
145
+ // Tool facts remain available for ordinary calls after a transient listing
146
+ // failure for five minutes. Resources never use stale inventories. A fallback
147
+ // carries its original expired deadline and cannot authorize auth recovery.
148
+ const STALE_FALLBACK_MS = 300_000;
149
+ const settings = new WeakMap();
150
+ export function attachCatalogCache(ctx, value) {
151
+ settings.set(ctx, value);
152
+ }
153
+ const defaultOwner = {};
154
+ const scopes = new WeakMap();
155
+ function cacheScope(ctx, connectorId, owner = defaultOwner) {
156
+ const key = ctx.requestScope ?? ctx;
157
+ let connectors = scopes.get(key);
158
+ if (!connectors) {
159
+ connectors = new Map();
160
+ scopes.set(key, connectors);
161
+ }
162
+ let owners = connectors.get(connectorId);
163
+ if (!owners) {
164
+ owners = new WeakMap();
165
+ connectors.set(connectorId, owners);
166
+ }
167
+ let scope = owners.get(owner);
168
+ if (!scope) {
169
+ scope = { closed: false, abort: new AbortController() };
170
+ owners.set(owner, scope);
171
+ }
172
+ return scope;
173
+ }
174
+ /** Start the fence before SDK eviction; teardown joins already-started I/O. */
175
+ export function observeCatalogChange(ctx, connectorId, connectionSignal, owner) {
176
+ const scope = cacheScope(ctx, connectorId, owner);
177
+ if (scope.closed || connectionSignal?.aborted)
178
+ return;
179
+ const writing = invalidateCatalogCache(settings.get(ctx)?.storage ?? ctx.storage, connectorId).catch((error) => {
180
+ logFailure(ctx.logger, "catalog invalidation failed", failureRecord({ connector: connectorId }, error));
181
+ });
182
+ scope.invalidating = scope.invalidating ? Promise.all([scope.invalidating, writing]).then(() => { }) : writing;
183
+ }
184
+ export function closeCatalogCacheScope(ctx, connectorId, owner) {
185
+ const scope = cacheScope(ctx, connectorId, owner);
186
+ scope.closed = true;
187
+ scope.abort.abort();
188
+ return scope.invalidating;
189
+ }
190
+ /** Zero is an explicit refusal to reuse. Positive hints use the configured bounds. */
191
+ function catalogTtlMs(ctx, hint) {
192
+ const policy = settings.get(ctx);
193
+ const ttl = hint === undefined ? Math.floor(policy?.defaultTtlMs ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds * 1000) : hint;
194
+ if (typeof ttl !== "number" || !Number.isSafeInteger(ttl) || ttl <= 0)
195
+ return 0;
196
+ return Math.floor(Math.min(Math.max(ttl, policy?.minTtlMs ?? 0), policy?.maxTtlMs ?? MAX_CACHE_TTL_MS, MAX_CACHE_TTL_MS));
197
+ }
198
+ /** Sanitize before SDK automatic writes and before its derived tool index. */
199
+ export function catalogIntake(ctx, result) {
200
+ const tools = redactCatalog(ctx, result.tools).map(({ _meta: _ignored, icons, ...tool }) => ({
201
+ ...tool,
202
+ ...(icons ? { icons: icons.filter((icon) => !/^data:/i.test(icon.src)) } : {}),
203
+ }));
204
+ if (tools.length > MAX_CATALOG_TOOLS)
205
+ throw catalogCeiling();
206
+ const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redact({ ...result, tools });
207
+ if (new TextEncoder().encode(JSON.stringify(clean)).byteLength > MAX_SERIALIZED_CATALOG_BYTES)
208
+ throw catalogCeiling();
209
+ for (const tool of clean.tools)
210
+ assertHeaderDeclarations(tool.inputSchema);
211
+ return {
212
+ ...clean,
213
+ ttlMs: catalogTtlMs(ctx, result.ttlMs),
214
+ cacheScope: result.cacheScope === "public" ? "public" : "private",
215
+ };
216
+ }
217
+ /** Refuse before SDK 2.3.1's header exclusion path logs downstream names/reasons.
218
+ * Match its schema keyword walk; errors carry no downstream text (INV-6/8). */
219
+ function assertHeaderDeclarations(schema) {
220
+ const names = new Set();
221
+ const maps = new Set(["patternProperties", "dependentSchemas", "$defs", "definitions"]);
222
+ const branches = [
223
+ "items",
224
+ "prefixItems",
225
+ "contains",
226
+ "additionalProperties",
227
+ "unevaluatedProperties",
228
+ "unevaluatedItems",
229
+ "propertyNames",
230
+ ...maps,
231
+ "oneOf",
232
+ "anyOf",
233
+ "allOf",
234
+ "not",
235
+ "if",
236
+ "then",
237
+ "else",
238
+ ];
239
+ const invalid = () => {
240
+ throw new ConnectorCallError("connector_call_failed", "Downstream tool has an invalid x-mcp-header declaration.", {
241
+ retryable: false,
242
+ });
243
+ };
244
+ const visit = (node, reachable, property) => {
245
+ if (!node || typeof node !== "object")
246
+ return;
247
+ const value = node;
248
+ if ("x-mcp-header" in value) {
249
+ const header = value["x-mcp-header"];
250
+ if (!reachable ||
251
+ !property ||
252
+ typeof header !== "string" ||
253
+ !/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(header) ||
254
+ !["string", "integer", "boolean", "number"].includes(value.type) ||
255
+ names.has(header.toLowerCase()))
256
+ invalid();
257
+ names.add(header.toLowerCase());
258
+ }
259
+ if (value.properties && typeof value.properties === "object") {
260
+ for (const child of Object.values(value.properties))
261
+ visit(child, reachable, true);
262
+ }
263
+ for (const keyword of branches) {
264
+ const sub = value[keyword];
265
+ const children = Array.isArray(sub)
266
+ ? sub
267
+ : sub && typeof sub === "object" && maps.has(keyword)
268
+ ? Object.values(sub)
269
+ : [sub];
270
+ for (const child of children)
271
+ visit(child, false, false);
272
+ }
273
+ };
274
+ visit(schema, true, false);
275
+ }
276
+ function catalogCeiling() {
277
+ return new ConnectorCallError("connector_call_failed", "Downstream catalog exceeds the complete-catalog ceiling.", {
278
+ retryable: false,
279
+ });
280
+ }
281
+ const GENERATION_TTL_SECONDS = (MAX_CACHE_TTL_MS * 2) / 1000;
282
+ export async function invalidateCatalogCache(storage, connectorId) {
283
+ await storage.set(responseCacheKeys.generation(connectorId), crypto.randomUUID(), {
284
+ ttlSeconds: GENERATION_TTL_SECONDS,
285
+ });
286
+ }
287
+ /** Keys ignore self-reported serverInfo and bind the configured connector instead. */
288
+ export async function catalogClientOptions(ctx, connectorId, config, authPartition, connectionSignal, owner, sharing = "private", resolveCredentialIdentity) {
289
+ const policy = settings.get(ctx);
290
+ const partition = JSON.stringify([
291
+ policy?.partition ?? JSON.stringify(callerOf(ctx) ?? null),
292
+ ctx.baseUrl,
293
+ ctx.publicUrl,
294
+ authPartition,
295
+ ]);
296
+ const sharedPartition = JSON.stringify([
297
+ "host-shared",
298
+ policy?.sharedPartition ?? JSON.stringify([ctx.baseUrl, ctx.publicUrl, callerOf(ctx)?.pool]),
299
+ authPartition,
300
+ ]);
301
+ const storage = policy?.storage ?? ctx.storage;
302
+ const configHash = (await fingerprintSerializedCatalog(JSON.stringify([
303
+ config,
304
+ policy?.defaultTtlMs ?? 300_000,
305
+ policy?.minTtlMs ?? 0,
306
+ policy?.maxTtlMs ?? MAX_CACHE_TTL_MS,
307
+ ]))).fingerprint;
308
+ const scope = cacheScope(ctx, connectorId, owner);
309
+ let unavailable = false;
310
+ const connectionGeneration = {};
311
+ let active;
312
+ // ResponseCacheStore has no per-call options. A method snapshots its binding
313
+ // before any await; an abandoned storage promise never adopts a later listing.
314
+ const capture = (listing = active) => {
315
+ const signals = [scope.abort.signal, connectionSignal, listing?.ctx.signal].filter((signal) => signal !== undefined);
316
+ const ended = () => scope.closed || listing?.ended === true || signals.some((signal) => signal.aborted);
317
+ const reason = () => signals.find((signal) => signal.aborted)?.reason ??
318
+ new ConnectorCallError("connector_call_failed", "Catalog cache operation ended.");
319
+ return {
320
+ ctx: listing?.ctx ?? ctx,
321
+ generation: listing?.generation ?? { ...connectionGeneration },
322
+ credentialIdentity: listing?.credentialIdentity,
323
+ listing,
324
+ stopped: () => unavailable || ended(),
325
+ io: (read) => new Promise((resolve, reject) => {
326
+ const cleanup = () => {
327
+ for (const signal of signals)
328
+ signal.removeEventListener("abort", abort);
329
+ };
330
+ const abort = () => {
331
+ cleanup();
332
+ reject(reason());
333
+ };
334
+ if (ended()) {
335
+ abort();
336
+ return;
337
+ }
338
+ for (const signal of signals)
339
+ signal.addEventListener("abort", abort, { once: true });
340
+ // Storage cannot cancel dispatched I/O. Stop waiting, but attach both
341
+ // outcomes so its eventual completion cannot continue cache publication.
342
+ try {
343
+ read().then((value) => {
344
+ cleanup();
345
+ resolve(value);
346
+ }, (error) => {
347
+ cleanup();
348
+ reject(error);
349
+ });
350
+ }
351
+ catch (error) {
352
+ cleanup();
353
+ reject(error);
354
+ }
355
+ }),
356
+ };
357
+ };
358
+ let lastStamp = 0;
359
+ let staleTools;
360
+ const currentGeneration = async ({ stopped, io, generation }) => {
361
+ const key = responseCacheKeys.generation(connectorId);
362
+ for (let attempt = 0; attempt < 16; attempt++) {
363
+ if (stopped())
364
+ return undefined;
365
+ const current = await io(() => storage.get(key));
366
+ if (stopped())
367
+ return undefined;
368
+ if (current)
369
+ return current;
370
+ const next = generation.value ?? crypto.randomUUID();
371
+ const claimed = await io(() => storage.compareAndSet(key, null, next, { ttlSeconds: GENERATION_TTL_SECONDS }));
372
+ if (stopped())
373
+ return undefined;
374
+ if (claimed)
375
+ return next;
376
+ }
377
+ throw new Error("Catalog cache generation is busy.");
378
+ };
379
+ const namespace = async (operation) => {
380
+ const { stopped, generation, io } = operation;
381
+ if (resolveCredentialIdentity && (await io(resolveCredentialIdentity)) !== operation.credentialIdentity)
382
+ return undefined;
383
+ const current = await currentGeneration(operation);
384
+ if (!current || stopped())
385
+ return undefined;
386
+ generation.value ??= current;
387
+ return generation.value === current
388
+ ? responseCacheKeys.namespace(connectorId, configHash, generation.value)
389
+ : undefined;
390
+ };
391
+ const address = async (key, operation) => {
392
+ const { stopped } = operation;
393
+ if (stopped() || !isCatalogMethod(key.method) || key.params)
394
+ return undefined;
395
+ let pair;
396
+ try {
397
+ pair = JSON.parse(key.partition ?? "");
398
+ }
399
+ catch {
400
+ return undefined;
401
+ }
402
+ if (!Array.isArray(pair) ||
403
+ pair.length !== 2 ||
404
+ (pair[1] !== partition && (pair[1] !== "" || sharing !== "shared")))
405
+ return undefined;
406
+ const prefix = await namespace(operation);
407
+ if (!prefix || stopped())
408
+ return undefined;
409
+ // Hash the admitted principal/pool/auth partition; no identities or server text in keys.
410
+ const digest = (await fingerprintSerializedCatalog(JSON.stringify([
411
+ key.method,
412
+ pair[1] === "" ? sharedPartition : partition,
413
+ operation.credentialIdentity ?? null,
414
+ ]))).fingerprint;
415
+ return stopped() ? undefined : responseCacheKeys.entry(prefix, digest);
416
+ };
417
+ const manifest = (raw) => {
418
+ if (!raw)
419
+ return undefined;
420
+ try {
421
+ const m = JSON.parse(raw);
422
+ return Number.isSafeInteger(m.stamp) &&
423
+ m.stamp > 0 &&
424
+ Number.isFinite(m.expiresAt) &&
425
+ Number.isFinite(m.fetchedAt) &&
426
+ m.fetchedAt >= 0 &&
427
+ m.fetchedAt <= m.expiresAt &&
428
+ (m.scope === "public" || m.scope === "private") &&
429
+ typeof m.revision === "string" &&
430
+ /^[0-9a-f-]{36}$/.test(m.revision) &&
431
+ Number.isInteger(m.chunkCount) &&
432
+ m.chunkCount > 0 &&
433
+ m.chunkCount <= Math.ceil(MAX_SERIALIZED_CATALOG_BYTES / MAX_CATALOG_CHUNK_BYTES) + 1 &&
434
+ /^sha256:[0-9]+:[0-9a-f]{64}$/.test(m.fingerprint)
435
+ ? m
436
+ : undefined;
437
+ }
438
+ catch {
439
+ return undefined;
440
+ }
441
+ };
442
+ const intake = (ctx, method, result) => {
443
+ const body = sharing === "private" ? { ...result, cacheScope: "private" } : result;
444
+ if (method === "tools/list")
445
+ return catalogIntake(ctx, body);
446
+ const field = method === "resources/list" ? "resources" : "resourceTemplates";
447
+ const items = catalogItems(method, body).map(({ _meta: _ignored, ...item }) => item);
448
+ if (items.length > MAX_CATALOG_TOOLS)
449
+ throw catalogCeiling();
450
+ const clean = sentSecretsForRequest(ctx.requestScope ?? ctx).redact({ ...body, [field]: items });
451
+ if (new TextEncoder().encode(JSON.stringify(clean)).byteLength > MAX_SERIALIZED_CATALOG_BYTES)
452
+ throw catalogCeiling();
453
+ return {
454
+ ...clean,
455
+ ttlMs: catalogTtlMs(ctx, body.ttlMs),
456
+ cacheScope: body.cacheScope === "public" ? "public" : "private",
457
+ };
458
+ };
459
+ const completeCatalogRefresh = async (result) => {
460
+ const listing = active;
461
+ const operation = capture();
462
+ const { stopped, io } = operation;
463
+ if (!listing || stopped())
464
+ return;
465
+ const clean = intake(listing.ctx, "tools/list", result);
466
+ const digest = (await fingerprintSerializedCatalog(JSON.stringify(clean))).fingerprint;
467
+ const partitionDigest = (await fingerprintSerializedCatalog(JSON.stringify([
468
+ clean.cacheScope === "public" ? sharedPartition : partition,
469
+ operation.credentialIdentity ?? null,
470
+ ]))).fingerprint;
471
+ try {
472
+ return await acceptCatalogRefresh(storage, responseCacheKeys.refreshDigest(connectorId, configHash, partitionDigest), connectorId, digest, clean.tools, clean.cacheScope !== "public", async () => !stopped() && !!(await namespace(operation)) && !stopped(), io);
473
+ }
474
+ catch (error) {
475
+ if (!stopped())
476
+ logFailure(listing.ctx.logger, "catalog refresh observation failed", failureRecord({ connector: connectorId }, error));
477
+ }
478
+ };
479
+ const store = {
480
+ async get(key) {
481
+ const operation = capture();
482
+ const { ctx, stopped, io } = operation;
483
+ const root = await address(key, operation);
484
+ if (!root || stopped())
485
+ return undefined;
486
+ const m = manifest(await io(() => storage.get(root)));
487
+ if (stopped() ||
488
+ !m ||
489
+ (m.expiresAt <= Date.now() && (key.method !== "tools/list" || m.expiresAt + STALE_FALLBACK_MS <= Date.now())))
490
+ return undefined;
491
+ const chunks = [];
492
+ for (let i = 0; i < m.chunkCount; i++) {
493
+ if (stopped())
494
+ return undefined;
495
+ const chunk = await io(() => storage.get(responseCacheKeys.chunk(root, m.revision, i)));
496
+ if (stopped() || chunk === null || new TextEncoder().encode(chunk).byteLength > MAX_CATALOG_CHUNK_BYTES)
497
+ return undefined;
498
+ chunks.push(chunk);
499
+ }
500
+ const value = chunks.join("");
501
+ const fingerprint = await fingerprintSerializedCatalog(value);
502
+ if (stopped() ||
503
+ fingerprint.byteLength > MAX_SERIALIZED_CATALOG_BYTES ||
504
+ fingerprint.fingerprint !== m.fingerprint ||
505
+ !(await namespace(operation)) ||
506
+ stopped())
507
+ return undefined;
508
+ const result = JSON.parse(value);
509
+ const method = key.method;
510
+ const items = catalogItems(method, result);
511
+ if (!Array.isArray(items) ||
512
+ items.length > MAX_CATALOG_TOOLS ||
513
+ "nextCursor" in result ||
514
+ (result.resultType !== undefined && result.resultType !== "complete"))
515
+ return undefined;
516
+ const validated = await catalogSchema(method)["~standard"].validate(result);
517
+ if (stopped() || validated.issues)
518
+ return undefined;
519
+ const clean = intake(ctx, method, validated.value);
520
+ if (m.expiresAt <= Date.now()) {
521
+ if (method === "tools/list" && (!staleTools || staleTools.manifest.fetchedAt < m.fetchedAt))
522
+ staleTools = { result: clean, manifest: m };
523
+ return undefined;
524
+ }
525
+ observeCatalogFetch(ctx, m.fetchedAt, method);
526
+ observeCatalogExpiry(ctx, method, clean.ttlMs, m.expiresAt);
527
+ return { value: JSON.stringify(clean), stamp: m.stamp, expiresAt: m.expiresAt, scope: m.scope };
528
+ },
529
+ async set(key, entry) {
530
+ const operation = capture();
531
+ const { ctx, stopped, io } = operation;
532
+ const root = await address(key, operation);
533
+ if (!root || stopped())
534
+ return 0;
535
+ const result = JSON.parse(entry.value);
536
+ const method = key.method;
537
+ if (catalogTtlMs(ctx, result.ttlMs) === 0) {
538
+ if (!stopped() && (await namespace(operation)) && !stopped())
539
+ await io(() => storage.delete(root));
540
+ return 0;
541
+ }
542
+ if (!Array.isArray(catalogItems(method, result)) ||
543
+ "nextCursor" in result ||
544
+ (result.resultType !== undefined && result.resultType !== "complete"))
545
+ return 0;
546
+ // Defence in depth: the SDK catches store failures, so intake also runs on
547
+ // the request path, where a secret-bearing name refuses the entire list.
548
+ const value = JSON.stringify(intake(ctx, method, result));
549
+ const fingerprint = await fingerprintSerializedCatalog(value);
550
+ const expiresAt = Math.min(entry.expiresAt ?? Date.now(), (catalogFetchedAt(ctx, method) ?? Date.now()) + catalogTtlMs(ctx, result.ttlMs));
551
+ if (stopped() || expiresAt <= Date.now() || !(await namespace(operation)) || stopped())
552
+ return 0;
553
+ const revision = crypto.randomUUID();
554
+ const bytes = new TextEncoder().encode(value);
555
+ const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
556
+ let offset = 0;
557
+ let chunkCount = 0;
558
+ const ttlSeconds = () => Math.max(0.001, (expiresAt + (method === "tools/list" ? STALE_FALLBACK_MS : 0) - Date.now()) / 1000);
559
+ while (offset < bytes.length) {
560
+ if (stopped() || expiresAt <= Date.now())
561
+ return 0;
562
+ let end = Math.min(offset + MAX_CATALOG_CHUNK_BYTES, bytes.length);
563
+ while (end < bytes.length && (bytes[end] & 0xc0) === 0x80)
564
+ end--;
565
+ const chunk = responseCacheKeys.chunk(root, revision, chunkCount++);
566
+ await io(() => storage.set(chunk, decoder.decode(bytes.subarray(offset, end)), { ttlSeconds: ttlSeconds() }));
567
+ if (stopped())
568
+ return 0;
569
+ offset = end;
570
+ }
571
+ for (let attempt = 0; attempt < 16; attempt++) {
572
+ if (stopped() || expiresAt <= Date.now() || !(await namespace(operation)) || stopped())
573
+ return 0;
574
+ const previous = await io(() => storage.get(root));
575
+ if (stopped() || expiresAt <= Date.now())
576
+ return 0;
577
+ const stamp = Math.max(Date.now(), (manifest(previous)?.stamp ?? 0) + 1, lastStamp + 1);
578
+ const m = {
579
+ stamp,
580
+ expiresAt,
581
+ fetchedAt: catalogFetchedAt(ctx, method) ?? Date.now(),
582
+ scope: sharing === "shared" && result.cacheScope === "public" ? "public" : "private",
583
+ revision,
584
+ chunkCount,
585
+ fingerprint: fingerprint.fingerprint,
586
+ };
587
+ const published = await io(() => storage.compareAndSet(root, previous, JSON.stringify(m), { ttlSeconds: ttlSeconds() }));
588
+ if (stopped())
589
+ return 0;
590
+ if (published) {
591
+ lastStamp = stamp;
592
+ return stamp;
593
+ }
594
+ }
595
+ throw new Error("Catalog cache publication is busy.");
596
+ },
597
+ async delete(key) {
598
+ const operation = capture();
599
+ const root = await address(key, operation);
600
+ if (root && !operation.stopped())
601
+ await operation.io(() => storage.delete(root));
602
+ },
603
+ async evict(method) {
604
+ if (isCatalogMethod(method))
605
+ await store.clear();
606
+ },
607
+ async clear() {
608
+ const operation = capture();
609
+ if (!operation.stopped())
610
+ await operation.io(() => invalidateCatalogCache(storage, connectorId));
611
+ },
612
+ };
613
+ // Refresh/bypass listings may never call get(). Pin before any wire I/O so
614
+ // their first late publication cannot adopt a post-notification generation.
615
+ // Reading an absent generation pins a nonce without mutating storage before
616
+ // negotiation/auth succeeds. The first cache operation may claim that nonce.
617
+ try {
618
+ const { stopped, io } = capture({ ctx, generation: connectionGeneration, ended: false });
619
+ if (!stopped()) {
620
+ const current = await io(() => storage.get(responseCacheKeys.generation(connectorId)));
621
+ if (!stopped())
622
+ connectionGeneration.value = current ?? crypto.randomUUID();
623
+ }
624
+ }
625
+ catch (error) {
626
+ unavailable = true;
627
+ logFailure(ctx.logger, "catalog read failed", failureRecord({ connector: connectorId }, error));
628
+ }
629
+ let tail = Promise.resolve();
630
+ const withListing = (listingCtx, read) => {
631
+ const pending = tail.then(async () => {
632
+ if (scope.closed || connectionSignal?.aborted)
633
+ throw new ConnectorCallError("connector_call_failed", "Catalog cache scope ended.");
634
+ if (listingCtx.signal?.aborted)
635
+ throw listingCtx.signal.reason;
636
+ if (policy && !settings.has(listingCtx))
637
+ settings.set(listingCtx, policy);
638
+ const listing = { ctx: listingCtx, generation: {}, ended: false };
639
+ active = listing;
640
+ staleTools = undefined;
641
+ const operation = capture();
642
+ try {
643
+ if (resolveCredentialIdentity)
644
+ listing.credentialIdentity = await operation.io(resolveCredentialIdentity);
645
+ // Each new listing pins before even a refresh's first wire page. A
646
+ // notification fences this listing, while a later listing can refresh.
647
+ if (!operation.stopped()) {
648
+ try {
649
+ listing.generation.value =
650
+ (await operation.io(() => storage.get(responseCacheKeys.generation(connectorId)))) ?? crypto.randomUUID();
651
+ connectionGeneration.value = listing.generation.value;
652
+ }
653
+ catch (error) {
654
+ if (operation.stopped())
655
+ throw error;
656
+ unavailable = true;
657
+ logFailure(ctx.logger, "catalog read failed", failureRecord({ connector: connectorId }, error));
658
+ }
659
+ }
660
+ return await read();
661
+ }
662
+ finally {
663
+ listing.ended = true;
664
+ active = undefined;
665
+ }
666
+ });
667
+ // Keep the binding through the entire SDK method, including its opposite
668
+ // partition delete even when set() failed. Only then may a sibling bind.
669
+ tail = pending.then(() => { }, () => { });
670
+ return pending;
671
+ };
672
+ const fallbackTools = async () => {
673
+ const operation = capture();
674
+ if (!staleTools ||
675
+ operation.stopped() ||
676
+ !(await namespace(operation)) ||
677
+ operation.stopped() ||
678
+ staleTools.manifest.expiresAt + STALE_FALLBACK_MS <= Date.now())
679
+ return;
680
+ const { result, manifest } = staleTools;
681
+ observeCatalogFetch(operation.ctx, manifest.fetchedAt);
682
+ observeCatalogExpiry(operation.ctx, "tools/list", typeof result.ttlMs === "number" ? result.ttlMs : 0, manifest.expiresAt, true);
683
+ return result;
684
+ };
685
+ return {
686
+ intake,
687
+ completeCatalogRefresh,
688
+ fallbackTools,
689
+ responseCacheStore: store,
690
+ cachePartition: partition,
691
+ defaultCacheTtlMs: policy?.defaultTtlMs ?? CONFIG_DEFAULTS.discovery.catalogTtlSeconds * 1000,
692
+ withListing,
693
+ currentContext: () => active?.ctx ?? ctx,
694
+ };
695
+ }
696
+ function isCatalogMethod(method) {
697
+ return method === "tools/list" || method === "resources/list" || method === "resources/templates/list";
698
+ }
699
+ async function customCatalogCache(ctx, connectorId) {
700
+ const credential = await ctx.credential?.getAll();
701
+ const auth = (await fingerprintSerializedCatalog(JSON.stringify(credential ?? null))).fingerprint;
702
+ const cache = await catalogClientOptions(ctx, connectorId, "custom-connector", auth);
703
+ const key = { method: "tools/list", partition: JSON.stringify(["custom-connector", cache.cachePartition]) };
704
+ return { cache, key };
705
+ }
706
+ /** Custom listings always fetch. Only an authenticated owner may retain a
707
+ * complete private fallback; unscoped/operator reads start no cache I/O. */
708
+ export async function storeCustomCatalogFallback(ctx, connectorId, tools) {
709
+ const ttlMs = catalogTtlMs(ctx, undefined);
710
+ observeCatalogFetch(ctx, Date.now());
711
+ observeCatalogExpiry(ctx, "tools/list", ttlMs);
712
+ if (!ttlMs || !callerOf(ctx)?.authenticated)
713
+ return;
714
+ const { cache, key } = await customCatalogCache(ctx, connectorId);
715
+ await cache.withListing(ctx, async () => {
716
+ const result = catalogIntake(ctx, {
717
+ tools: tools.map((tool) => ({
718
+ ...tool,
719
+ inputSchema: tool.inputSchema ?? { type: "object" },
720
+ })),
721
+ ttlMs,
722
+ cacheScope: "private",
723
+ });
724
+ await cache.responseCacheStore.set(key, {
725
+ value: JSON.stringify(result),
726
+ expiresAt: catalogExpiry(ctx).expiresAt,
727
+ scope: "private",
728
+ });
729
+ });
730
+ }
731
+ export async function customCatalogFallback(ctx, connectorId) {
732
+ if (!callerOf(ctx)?.authenticated)
733
+ return;
734
+ const { cache, key } = await customCatalogCache(ctx, connectorId);
735
+ return cache.withListing(ctx, async () => {
736
+ await cache.responseCacheStore.get(key);
737
+ return (await cache.fallbackTools())?.tools;
738
+ });
739
+ }