@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
@@ -1,35 +1,27 @@
1
+ import { authorizationCredential, isMachineCredential } from "./inbound-credential.js";
2
+ import { assertKnownOptions, keys, optionsOf } from "./config-schema.js";
1
3
  import { routeAccessTokens } from "./routes/access-tokens.js";
2
4
  import { validIdentityReference } from "./identity.js";
5
+ import { accessTokenKeys } from "./storage/keys.js";
3
6
  const TOKEN_PREFIX = "cta_";
4
7
  const TOKEN_BYTES = 32;
5
8
  const TOKEN_VALUE_RE = /^cta_[A-Za-z0-9_-]{43}$/;
6
- const RECORD_PREFIX = "access-token:v1:record:";
7
- const LOOKUP_PREFIX = "access-token:v1:lookup:";
8
9
  const MAX_NAME_CHARACTERS = 80;
9
10
  const DEFAULT_MAX_ACTIVE = 100;
10
11
  const MAX_CONFIGURED_ACTIVE = 1_000;
11
- const ACTIVE_KEY = "access-token:v1:active";
12
12
  const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
13
13
  const encoder = new TextEncoder();
14
- function recordKey(id) {
15
- return `${RECORD_PREFIX}${id}`;
16
- }
17
- function lookupKey(hash) {
18
- return `${LOOKUP_PREFIX}${hash}`;
19
- }
14
+ const recordKey = accessTokenKeys.record;
15
+ const lookupKey = accessTokenKeys.lookup;
16
+ const ACTIVE_KEY = accessTokenKeys.active;
20
17
  function bytesToBase64Url(bytes) {
21
18
  let binary = "";
22
19
  for (const byte of bytes)
23
20
  binary += String.fromCharCode(byte);
24
- return btoa(binary)
25
- .replaceAll("+", "-")
26
- .replaceAll("/", "_")
27
- .replace(/=+$/u, "");
21
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
28
22
  }
29
23
  function bytesToHex(bytes) {
30
- return [...bytes]
31
- .map((byte) => byte.toString(16).padStart(2, "0"))
32
- .join("");
24
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
33
25
  }
34
26
  async function hashToken(token) {
35
27
  return bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(token))));
@@ -62,12 +54,10 @@ function parseRecord(raw) {
62
54
  typeof value.createdAt !== "string" ||
63
55
  !Number.isFinite(Date.parse(value.createdAt)) ||
64
56
  typeof value.createdBy !== "string" ||
65
- (value.principal !== undefined &&
66
- !validIdentityReference(value.principal)) ||
57
+ (value.principal !== undefined && !validIdentityReference(value.principal)) ||
67
58
  (value.revokedAt !== undefined &&
68
59
  (typeof value.revokedAt !== "string" || !value.revokedAt || !Number.isFinite(Date.parse(value.revokedAt)))) ||
69
- (value.revokedBy !== undefined &&
70
- typeof value.revokedBy !== "string")) {
60
+ (value.revokedBy !== undefined && typeof value.revokedBy !== "string")) {
71
61
  throw new Error("invalid token record");
72
62
  }
73
63
  return value;
@@ -119,18 +109,20 @@ export class AccessTokenManager {
119
109
  maxActive;
120
110
  constructor(storage, options = {}) {
121
111
  this.storage = storage;
122
- if (!storage.list) {
123
- throw new Error("accessTokens requires a storage adapter that implements list(prefix)");
112
+ // Issuance claims capacity by compare-and-set and listing enumerates
113
+ // records; refuse a store without either here, not at the first create.
114
+ if (typeof storage?.list !== "function" || typeof storage.compareAndSet !== "function") {
115
+ throw new Error("accessTokens requires storage that implements list and compareAndSet " +
116
+ "(d1Storage, sqliteStorage, or memoryStorage)");
124
117
  }
125
118
  const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
126
- if (!Number.isInteger(maxActive) ||
127
- maxActive < 1 ||
128
- maxActive > MAX_CONFIGURED_ACTIVE) {
119
+ if (!Number.isInteger(maxActive) || maxActive < 1 || maxActive > MAX_CONFIGURED_ACTIVE) {
129
120
  throw new Error(`accessTokens.maxActive must be a whole number from 1 to ${MAX_CONFIGURED_ACTIVE}`);
130
121
  }
131
122
  this.maxActive = maxActive;
132
123
  this.auth = {
133
124
  kind: "access_token",
125
+ recognizesCredential: isMachineCredential,
134
126
  activityActorNamespace: "connecta:access-tokens:v1",
135
127
  activityActorLabel: async (id) => {
136
128
  try {
@@ -153,7 +145,7 @@ export class AccessTokenManager {
153
145
  return record;
154
146
  }
155
147
  async list() {
156
- const keys = await this.storage.list(RECORD_PREFIX);
148
+ const keys = await this.storage.list(accessTokenKeys.recordPrefix);
157
149
  const records = await Promise.all(keys.map(async (key) => {
158
150
  const raw = await this.storage.get(key);
159
151
  return raw ? parseRecord(raw) : null;
@@ -165,9 +157,6 @@ export class AccessTokenManager {
165
157
  }
166
158
  async create(name, createdBy) {
167
159
  const normalizedName = normalizeName(name);
168
- if (!this.storage.compareAndSet) {
169
- throw new Error("Creating access tokens requires atomic compareAndSet storage");
170
- }
171
160
  if (typeof createdBy !== "string" && !validIdentityReference(createdBy)) {
172
161
  throw new Error("Invalid token owner");
173
162
  }
@@ -184,12 +173,8 @@ export class AccessTokenManager {
184
173
  tokenHash: hash,
185
174
  tokenPrefix: token.slice(0, 12),
186
175
  createdAt: new Date().toISOString(),
187
- createdBy: typeof createdBy === "string"
188
- ? createdBy
189
- : `${createdBy.namespace}:${createdBy.id}`,
190
- ...(typeof createdBy === "string"
191
- ? {}
192
- : { principal: { ...createdBy } }),
176
+ createdBy: typeof createdBy === "string" ? createdBy : `${createdBy.namespace}:${createdBy.id}`,
177
+ ...(typeof createdBy === "string" ? {} : { principal: { ...createdBy } }),
193
178
  };
194
179
  try {
195
180
  await this.updateActive(record.id, true);
@@ -211,7 +196,7 @@ export class AccessTokenManager {
211
196
  }
212
197
  async rename(id, name) {
213
198
  const normalized = normalizeName(name);
214
- const record = await this.updateRecord(id, current => ({ ...current, name: normalized }));
199
+ const record = await this.updateRecord(id, (current) => ({ ...current, name: normalized }));
215
200
  return record ? metadata(record) : null;
216
201
  }
217
202
  async revoke(id, revokedBy) {
@@ -221,11 +206,14 @@ export class AccessTokenManager {
221
206
  // Remove admission first. A metadata failure cannot leave a successful
222
207
  // revocation's lookup alive. Retry deletion even if already marked revoked.
223
208
  await this.storage.delete(lookupKey(record.tokenHash));
224
- const updated = await this.updateRecord(id, current => current.revokedAt ? current : {
225
- ...current, revokedAt: new Date().toISOString(), revokedBy,
226
- });
227
- if (this.storage.compareAndSet)
228
- await this.updateActive(id, false);
209
+ const updated = await this.updateRecord(id, (current) => current.revokedAt
210
+ ? current
211
+ : {
212
+ ...current,
213
+ revokedAt: new Date().toISOString(),
214
+ revokedBy,
215
+ });
216
+ await this.updateActive(id, false);
229
217
  return updated ? metadata(updated) : null;
230
218
  }
231
219
  async updateRecord(id, update) {
@@ -239,45 +227,36 @@ export class AccessTokenManager {
239
227
  if (record.id !== id)
240
228
  throw new Error("Stored access token id mismatch");
241
229
  const next = update(record);
242
- if (this.storage.compareAndSet) {
243
- // A rename racing revocation must retain revokedAt, including when
244
- // an in-flight create has not yet published its lookup.
245
- if (!await this.storage.compareAndSet(recordKey(id), raw, JSON.stringify(next)))
246
- continue;
247
- }
248
- else {
249
- // Legacy adapters cannot create, so no late lookup writer exists.
250
- // Revocation's deleted lookup still refuses a stale metadata write.
251
- await this.storage.set(recordKey(id), JSON.stringify(next));
252
- }
230
+ // A rename racing revocation must retain revokedAt, including when
231
+ // an in-flight create has not yet published its lookup.
232
+ if (!(await this.storage.compareAndSet(recordKey(id), raw, JSON.stringify(next))))
233
+ continue;
253
234
  return next;
254
235
  }
255
236
  throw new Error("Access token metadata is busy; retry the operation");
256
237
  }
257
238
  async updateActive(id, adding) {
258
- const cas = this.storage.compareAndSet;
259
- if (!cas)
260
- throw new Error("Creating access tokens requires atomic compareAndSet storage");
261
239
  for (let attempt = 0; attempt < 32; attempt++) {
262
240
  const raw = await this.storage.get(ACTIVE_KEY);
263
241
  const ids = raw === null
264
- ? (await this.list()).filter(token => !token.revokedAt).map(token => token.id)
242
+ ? (await this.list()).filter((token) => !token.revokedAt).map((token) => token.id)
265
243
  : JSON.parse(raw);
266
- if (!Array.isArray(ids) || !ids.every(value => typeof value === "string" && ID_RE.test(value)) || new Set(ids).size !== ids.length) {
244
+ if (!Array.isArray(ids) ||
245
+ !ids.every((value) => typeof value === "string" && ID_RE.test(value)) ||
246
+ new Set(ids).size !== ids.length) {
267
247
  throw new Error("Stored access token capacity is invalid");
268
248
  }
269
249
  if (adding && ids.length >= this.maxActive)
270
250
  throw new Error(`This deployment already has the maximum of ${this.maxActive} active access tokens`);
271
- const next = adding ? [...ids, id] : ids.filter(value => value !== id);
272
- if (await cas.call(this.storage, ACTIVE_KEY, raw, JSON.stringify(next)))
251
+ const next = adding ? [...ids, id] : ids.filter((value) => value !== id);
252
+ if (await this.storage.compareAndSet(ACTIVE_KEY, raw, JSON.stringify(next)))
273
253
  return;
274
254
  }
275
255
  throw new Error("Access token capacity is busy; retry the operation");
276
256
  }
277
257
  async authorize(request) {
278
- const header = request.headers.get("authorization") ?? "";
279
- const match = /^Bearer\s+(.+)$/iu.exec(header);
280
- const token = match?.[1];
258
+ const credential = authorizationCredential(request);
259
+ const token = credential.kind === "bearer" ? credential.token : undefined;
281
260
  if (!token || !TOKEN_VALUE_RE.test(token))
282
261
  return unauthorized();
283
262
  const hash = await hashToken(token);
@@ -298,8 +277,19 @@ export class AccessTokenManager {
298
277
  };
299
278
  }
300
279
  }
280
+ /** The closed options accessTokens() accepts; see `assertKnownOptions`. */
281
+ const ACCESS_TOKENS_OPTIONS = optionsOf()(keys("maxActive"));
301
282
  /** Opt in using the same storage namespace that held the v0.23 records. */
302
283
  export function accessTokens(storage, options = {}) {
284
+ options = assertKnownOptions(options, "accessTokens()", ACCESS_TOKENS_OPTIONS);
303
285
  const manager = new AccessTokenManager(storage, options);
304
- return { auth: manager.auth, handle: context => routeAccessTokens(context, manager) };
286
+ const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
287
+ const optionSources = Object.freeze({
288
+ maxActive: options.maxActive === undefined ? "default" : "config",
289
+ });
290
+ return {
291
+ auth: manager.auth,
292
+ handle: (context) => routeAccessTokens(context, manager),
293
+ describe: () => ({ maxActive, optionSources }),
294
+ };
305
295
  }
@@ -0,0 +1,8 @@
1
+ // Weak ownership keeps the grouping context within the request's lifetime.
2
+ const requests = new WeakMap();
3
+ export function bindActivityRequest(scope, context) {
4
+ requests.set(scope, context);
5
+ }
6
+ export function activityRequest(scope) {
7
+ return scope ? requests.get(scope) : undefined;
8
+ }
@@ -1,6 +1,11 @@
1
1
  import type { ActivityModule } from "./module-contracts.js";
2
- import type { CatalogDriftCounts, Logger } from "./types.js";
3
- export type ActivityCallSource = "call_tool" | "call_destructive_tool" | "batch_call" | "execute_code" | "resume_execution";
2
+ import { type ClassificationCode } from "./errors.js";
3
+ import type { Logger } from "./types.js";
4
+ /** One policy for the record builder, SQL write/read boundaries, and UI. */
5
+ export declare function activityClientFact(value: unknown, field: "name" | "version"): string | undefined;
6
+ /** Stored package versions use a bounded release-version grammar, including prereleases. */
7
+ export declare function activityPackageVersion(value: unknown): string | undefined;
8
+ export type ActivityCallSource = "catalog_refresh" | "call_tool" | "call_destructive_tool" | "batch_call" | "execute_code" | "resume_execution";
4
9
  export type ActivityOutcome = "success" | "error" | "timeout" | "cancelled" | "paused" | "approved";
5
10
  /** Read-only history: how much one `resume_execution` approved (#672). */
6
11
  export type ActivityApproval = "call" | "tool";
@@ -28,11 +33,37 @@ export interface ActivityActor {
28
33
  * by construction. Deployments that need a safe human summary can add a
29
34
  * separate, explicit connector-level feature later.
30
35
  */
36
+ export interface ActivityCatalogChange {
37
+ kind: "catalog_changed";
38
+ addedTools: number;
39
+ removedTools: number;
40
+ changedTools: number;
41
+ }
42
+ /** Integer metadata is checked at every write and read boundary. */
43
+ export declare function activityCount(value: unknown): number | undefined;
44
+ export declare function activityBehaviorFacts(event: {
45
+ classification?: unknown;
46
+ resultBytes?: unknown;
47
+ kind?: unknown;
48
+ drift?: unknown;
49
+ pool?: unknown;
50
+ actorBasis?: unknown;
51
+ }): Pick<ToolCallActivityEvent, "classification" | "resultBytes" | "kind" | "drift" | "pool" | "actorBasis">;
31
52
  export interface ToolCallActivityEvent {
32
53
  schemaVersion: 1;
33
54
  id: string;
34
55
  occurredAt: string;
35
56
  requestId: string;
57
+ /** Absent on legacy tool rows. Catalog changes use the same paging envelope. */
58
+ kind?: "catalog_drift";
59
+ drift?: ActivityCatalogChange;
60
+ /** The final registry verdict captured when resolution succeeded. */
61
+ classification?: "read" | "write";
62
+ /** UTF-8 bytes of the downstream value before result paging or truncation. */
63
+ resultBytes?: number;
64
+ pool?: string;
65
+ /** Personal ownership is provable only for rows recorded from the admitted principal. */
66
+ actorBasis?: "principal";
36
67
  actor: ActivityActor;
37
68
  connectorId: string;
38
69
  toolName: string;
@@ -55,28 +86,13 @@ export interface ToolCallActivityEvent {
55
86
  * here — the approved arguments were never recorded.
56
87
  */
57
88
  approval?: ActivityApproval;
89
+ /** Real build version. Absent only in history written before telemetry existed. */
90
+ packageVersion?: string;
58
91
  serverName: string;
59
92
  serverVersion: string;
60
- deploymentId?: string;
61
- }
62
- /**
63
- * A downstream catalog moved away from the manifest a release reviewed.
64
- *
65
- * Four integers and an id: the type has nowhere to put a tool name, a schema,
66
- * an argument, a result, or a downstream error string, which is the same
67
- * construction guarantee `ToolCallActivityEvent` makes. Which tool drifted is
68
- * deliberately absent — the runtime reports that something did, and the
69
- * maintainer-run check with a live catalog in front of it names names
70
- * ([#343](https://github.com/zackbart/connecta/issues/343),
71
- * [#351](https://github.com/zackbart/connecta/issues/351)).
72
- */
73
- export interface CatalogDriftActivityEvent extends CatalogDriftCounts {
74
- schemaVersion: 1;
75
- id: string;
76
- occurredAt: string;
77
- connectorId: string;
78
- serverName: string;
79
- serverVersion: string;
93
+ /** Self-declared client identity checked by activityClientFact; invalid facts are absent. */
94
+ clientName?: string;
95
+ clientVersion?: string;
80
96
  deploymentId?: string;
81
97
  }
82
98
  export interface ActivityPage {
@@ -97,13 +113,6 @@ export interface ActivityReadPage {
97
113
  /** Write-only deployments can implement only this small, vendor-neutral seam. */
98
114
  export interface ActivitySink {
99
115
  record(event: ToolCallActivityEvent): void | Promise<void>;
100
- /**
101
- * Optional catalog-drift channel. Optional rather than a widened `record`,
102
- * because a store written before drift existed already decided what a row
103
- * looks like: a sink that does not implement this simply never hears about
104
- * drift, and the operator still reads the same counts from connector status.
105
- */
106
- recordCatalogDrift?(event: CatalogDriftActivityEvent): void | Promise<void>;
107
116
  }
108
117
  /** Optional read side used by Connecta's authenticated Activity UI. */
109
118
  export interface ActivityReader {
@@ -114,6 +123,14 @@ export interface ActivityReader {
114
123
  }
115
124
  export interface ActivityStore extends ActivitySink {
116
125
  list?: ActivityReader["list"];
126
+ /**
127
+ * The adapter and its retention, for `Connecta.describeConfig()`. Kinds
128
+ * other than the shipped `"d1"` and `"sqlite"` are described as `"custom"`.
129
+ */
130
+ describe?(): {
131
+ kind: string;
132
+ retentionDays?: number;
133
+ };
117
134
  }
118
135
  /** Reader implementations throw this for an opaque cursor they cannot decode. */
119
136
  export declare class InvalidActivityCursorError extends Error {
@@ -122,17 +139,21 @@ export declare class InvalidActivityCursorError extends Error {
122
139
  }
123
140
  export type ActivityReadGate = (actor: ActivityActor) => boolean | Promise<boolean>;
124
141
  /**
125
- * Deployment-scoped destination for drift observations. Not request-scoped:
126
- * a drifting catalog is something the deployment saw, not something a caller
127
- * did, so it carries no actor and no request id to attribute it to one.
142
+ * Default destination for catalog changes. Request attribution overrides it
143
+ * when a publication belongs to an admitted caller.
128
144
  */
129
145
  export interface CatalogDriftActivityContext {
130
- recordDrift?: typeof recordCatalogDriftActivity;
146
+ recordChange?: typeof recordCatalogChangeActivity;
147
+ defer?: (promise: Promise<unknown>) => void;
131
148
  sink: ActivitySink;
132
149
  serverInfo: {
133
150
  name: string;
134
151
  version: string;
135
152
  };
153
+ clientInfo?: {
154
+ name: string;
155
+ version: string;
156
+ };
136
157
  deploymentId?: string;
137
158
  logger: Logger;
138
159
  }
@@ -141,34 +162,47 @@ export interface ActivityRequestContext {
141
162
  recordTool?: typeof recordToolActivity | undefined;
142
163
  sink: ActivitySink;
143
164
  actor: ActivityActor;
165
+ /** Personal rows identify their admitted owner in the existing actor fields. */
166
+ principalActor?: ActivityActor;
144
167
  requestId: string;
168
+ pool?: string;
145
169
  serverInfo: {
146
170
  name: string;
147
171
  version: string;
148
172
  };
173
+ clientInfo?: {
174
+ name: string;
175
+ version: string;
176
+ };
149
177
  deploymentId?: string;
150
178
  defer?: (promise: Promise<unknown>) => void;
151
179
  logger: Logger;
152
180
  }
153
- export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "errorCode" | "friction">;
181
+ export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "friction" | "classification" | "resultBytes" | "kind" | "drift"> & {
182
+ /**
183
+ * A code connecta assigns. Rows written by older deployments may hold
184
+ * others, so the stored event keeps `string`; a new row never does.
185
+ */
186
+ errorCode?: ClassificationCode;
187
+ personal?: boolean;
188
+ privateCatalog?: boolean;
189
+ };
154
190
  /**
155
191
  * Best-effort by design: activity storage can never change a tool result.
156
192
  * Workers attach async sinks to waitUntil; synchronous sinks such as Analytics
157
193
  * Engine complete inline; Node promises remain detached from the response.
158
194
  */
159
195
  export declare function recordToolActivity(context: ActivityRequestContext | undefined, input: ActivityEventInput): void;
160
- /**
161
- * Record one catalog-drift observation, best-effort like every other activity
162
- * write: a store that is down, or a sink that never implemented the channel,
163
- * can never change what a refresh returned to the caller who paid for it.
164
- */
165
- export declare function recordCatalogDriftActivity(context: CatalogDriftActivityContext | undefined, input: {
196
+ /** A discrete catalog change, with no catalog names, descriptions or schemas. */
197
+ export declare function recordCatalogChangeActivity(context: CatalogDriftActivityContext | undefined, input: {
166
198
  connectorId: string;
167
- } & CatalogDriftCounts): void;
199
+ drift: ActivityCatalogChange;
200
+ personal?: boolean;
201
+ privateCatalog?: boolean;
202
+ }, request?: ActivityRequestContext): void;
168
203
  export interface ActivityHistoryOptions {
169
204
  store: ActivityStore;
170
205
  deploymentId?: string;
171
206
  readGate?: ActivityReadGate;
172
207
  }
173
- /** Attach payload-free history without changing tool results on store failure. */
174
208
  export declare function activityHistory(options: ActivityHistoryOptions): ActivityModule;
package/dist/activity.js CHANGED
@@ -1,5 +1,8 @@
1
+ import { CONNECTA_VERSION } from "./version.js";
2
+ import { assertKnownOptions, keys, optionsOf } from "./config-schema.js";
1
3
  import { routeActivity } from "./routes/activity.js";
2
- import { boundedEchoText } from "./errors.js";
4
+ import { boundedEchoText, classificationCode } from "./errors.js";
5
+ import { failureRecord, logFailure } from "./operator-record.js";
3
6
  /**
4
7
  * How long an identity field may be before the store stops believing it.
5
8
  *
@@ -15,10 +18,72 @@ import { boundedEchoText } from "./errors.js";
15
18
  * 40,000 would. The `…` marker keeps a clamped value from reading as a real one.
16
19
  */
17
20
  const MAX_ACTIVITY_NAME_BYTES = 128;
21
+ /**
22
+ * Client facts use bounded ASCII grammars, never truncation or escaping.
23
+ * Names are 1 to 64 characters: alphanumeric or @ first (scoped package names
24
+ * such as `@modelcontextprotocol/inspector`), then alphanumerics, space, dot,
25
+ * underscore, @, /, + or -. Versions are 1 to 32 characters: alphanumeric
26
+ * first, then alphanumerics, dot, underscore, + or -. Prototype-key names are
27
+ * withheld too. The end assertion rejects even a final CR/LF, unlike `$`.
28
+ */
29
+ const CLIENT_FACT_GRAMMARS = {
30
+ name: /^[A-Za-z0-9@][A-Za-z0-9 ._@/+-]{0,63}(?![\s\S])/,
31
+ version: /^[A-Za-z0-9][A-Za-z0-9._+-]{0,31}(?![\s\S])/,
32
+ };
33
+ const RESERVED_CLIENT_FACTS = new Set(["__proto__", "constructor", "prototype"]);
34
+ /** One policy for the record builder, SQL write/read boundaries, and UI. */
35
+ export function activityClientFact(value, field) {
36
+ return typeof value === "string" && !RESERVED_CLIENT_FACTS.has(value) && CLIENT_FACT_GRAMMARS[field].test(value)
37
+ ? value
38
+ : undefined;
39
+ }
40
+ /** Stored package versions use a bounded release-version grammar, including prereleases. */
41
+ export function activityPackageVersion(value) {
42
+ return typeof value === "string" &&
43
+ value.length <= 128 &&
44
+ /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?(?![\s\S])/.test(value)
45
+ ? value
46
+ : undefined;
47
+ }
18
48
  /** Two names and the dot between them. */
19
49
  const MAX_ACTIVITY_ADDRESS_BYTES = MAX_ACTIVITY_NAME_BYTES * 2 + 1;
20
50
  import { agentFrictionForCode } from "./activity-friction.js";
21
51
  export { agentFrictionForCode } from "./activity-friction.js";
52
+ /** Integer metadata is checked at every write and read boundary. */
53
+ export function activityCount(value) {
54
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
55
+ }
56
+ export function activityBehaviorFacts(event) {
57
+ const classification = event.classification === "read" || event.classification === "write" ? event.classification : undefined;
58
+ const resultBytes = activityCount(event.resultBytes);
59
+ const change = event.drift;
60
+ const drift = event.kind === "catalog_drift" &&
61
+ change?.kind === "catalog_changed" &&
62
+ activityCount(change.addedTools) !== undefined &&
63
+ activityCount(change.removedTools) !== undefined &&
64
+ activityCount(change.changedTools) !== undefined
65
+ ? {
66
+ kind: "catalog_changed",
67
+ addedTools: change.addedTools,
68
+ removedTools: change.removedTools,
69
+ changedTools: change.changedTools,
70
+ }
71
+ : undefined;
72
+ // Pool names come from matched deployment config, whose grammar has no size
73
+ // ceiling. Invalid stored scope stays withheld rather than becoming root scope.
74
+ const pool = event.pool === undefined || event.pool === null
75
+ ? undefined
76
+ : typeof event.pool === "string" && /^[a-z0-9_-]+(?![\s\S])/.test(event.pool)
77
+ ? event.pool
78
+ : "<withheld>";
79
+ return {
80
+ ...(event.actorBasis === "principal" ? { actorBasis: "principal" } : {}),
81
+ ...(classification !== undefined ? { classification } : {}),
82
+ ...(resultBytes !== undefined ? { resultBytes } : {}),
83
+ ...(drift ? { kind: "catalog_drift", drift } : {}),
84
+ ...(pool !== undefined ? { pool } : {}),
85
+ };
86
+ }
22
87
  /** Reader implementations throw this for an opaque cursor they cannot decode. */
23
88
  export class InvalidActivityCursorError extends Error {
24
89
  name = "InvalidActivityCursorError";
@@ -36,13 +101,24 @@ export function recordToolActivity(context, input) {
36
101
  return;
37
102
  // A caller-supplied class wins because it knows something the code table
38
103
  // cannot: friction that belongs to a call which did not fail.
39
- const friction = input.friction ?? agentFrictionForCode(input.errorCode);
104
+ // Checked here as well as by type: a handler can put any string in a
105
+ // `ConnectorCallError`'s code, and a downstream's own code is not recorded.
106
+ const errorCode = classificationCode(input.errorCode);
107
+ const friction = input.friction ?? agentFrictionForCode(errorCode);
108
+ // Re-check client grammars at the record boundary; never spread the envelope.
109
+ const clientName = activityClientFact(context.clientInfo?.name, "name");
110
+ const clientVersion = activityClientFact(context.clientInfo?.version, "version");
40
111
  const event = {
41
112
  schemaVersion: 1,
42
113
  id: crypto.randomUUID(),
43
114
  occurredAt: new Date().toISOString(),
44
115
  requestId: context.requestId,
45
- actor: context.actor,
116
+ actor: input.personal || input.privateCatalog ? (context.principalActor ?? { kind: context.actor.kind }) : context.actor,
117
+ ...activityBehaviorFacts({
118
+ ...input,
119
+ pool: context.pool,
120
+ actorBasis: input.privateCatalog || (input.personal && context.principalActor) ? "principal" : undefined,
121
+ }),
46
122
  connectorId: boundedEchoText(input.connectorId, MAX_ACTIVITY_NAME_BYTES),
47
123
  toolName: boundedEchoText(input.toolName, MAX_ACTIVITY_NAME_BYTES),
48
124
  address: boundedEchoText(input.address, MAX_ACTIVITY_ADDRESS_BYTES),
@@ -50,13 +126,14 @@ export function recordToolActivity(context, input) {
50
126
  outcome: input.outcome,
51
127
  durationMs: Math.max(0, Math.trunc(input.durationMs)),
52
128
  attempts: Math.max(1, Math.trunc(input.attempts)),
53
- ...(input.errorCode ? { errorCode: input.errorCode } : {}),
129
+ ...(errorCode ? { errorCode } : {}),
54
130
  ...(friction ? { friction } : {}),
131
+ packageVersion: CONNECTA_VERSION,
55
132
  serverName: context.serverInfo.name,
56
133
  serverVersion: context.serverInfo.version,
57
- ...(context.deploymentId
58
- ? { deploymentId: context.deploymentId }
59
- : {}),
134
+ ...(clientName !== undefined ? { clientName } : {}),
135
+ ...(clientVersion !== undefined ? { clientVersion } : {}),
136
+ ...(context.deploymentId ? { deploymentId: context.deploymentId } : {}),
60
137
  };
61
138
  try {
62
139
  const result = context.sink.record(event);
@@ -64,51 +141,42 @@ export function recordToolActivity(context, input) {
64
141
  return;
65
142
  }
66
143
  const pending = Promise.resolve(result).catch((error) => {
67
- context.logger.warn("[connecta] activity record failed", error);
144
+ logFailure(context.logger, "activity record failed", failureRecord({}, error));
68
145
  });
69
146
  if (context.defer)
70
147
  context.defer(pending);
71
148
  }
72
149
  catch (error) {
73
- context.logger.warn("[connecta] activity record failed", error);
150
+ logFailure(context.logger, "activity record failed", failureRecord({}, error));
74
151
  }
75
152
  }
76
- /**
77
- * Record one catalog-drift observation, best-effort like every other activity
78
- * write: a store that is down, or a sink that never implemented the channel,
79
- * can never change what a refresh returned to the caller who paid for it.
80
- */
81
- export function recordCatalogDriftActivity(context, input) {
82
- if (!context?.sink.recordCatalogDrift)
153
+ /** A discrete catalog change, with no catalog names, descriptions or schemas. */
154
+ export function recordCatalogChangeActivity(context, input, request) {
155
+ if (!context)
83
156
  return;
84
- const event = {
85
- schemaVersion: 1,
86
- id: crypto.randomUUID(),
87
- occurredAt: new Date().toISOString(),
88
- connectorId: boundedEchoText(input.connectorId, MAX_ACTIVITY_NAME_BYTES),
89
- unclassifiedTools: input.unclassifiedTools,
90
- unservedTools: input.unservedTools,
91
- annotationConflicts: input.annotationConflicts,
92
- schemaChanges: input.schemaChanges,
93
- serverName: context.serverInfo.name,
94
- serverVersion: context.serverInfo.version,
95
- ...(context.deploymentId ? { deploymentId: context.deploymentId } : {}),
96
- };
97
- try {
98
- const result = context.sink.recordCatalogDrift(event);
99
- if (!result || typeof result.then !== "function") {
100
- return;
101
- }
102
- void Promise.resolve(result).catch((error) => {
103
- context.logger.warn("[connecta] catalog drift record failed", error);
104
- });
105
- }
106
- catch (error) {
107
- context.logger.warn("[connecta] catalog drift record failed", error);
108
- }
157
+ recordToolActivity(request ?? {
158
+ ...context,
159
+ actor: { kind: "system" },
160
+ requestId: crypto.randomUUID(),
161
+ }, {
162
+ connectorId: input.connectorId,
163
+ toolName: "<catalog>",
164
+ address: `${input.connectorId}.<catalog>`,
165
+ source: "catalog_refresh",
166
+ outcome: "success",
167
+ durationMs: 0,
168
+ attempts: 1,
169
+ kind: "catalog_drift",
170
+ drift: input.drift,
171
+ ...(input.personal ? { personal: true } : {}),
172
+ ...(input.privateCatalog ? { privateCatalog: true } : {}),
173
+ });
109
174
  }
110
175
  /** Attach payload-free history without changing tool results on store failure. */
176
+ /** The closed options activityHistory() accepts; see `assertKnownOptions`. */
177
+ const ACTIVITY_HISTORY_OPTIONS = optionsOf()(keys("store", "deploymentId", "readGate"));
111
178
  export function activityHistory(options) {
179
+ options = assertKnownOptions(options, "activityHistory()", ACTIVITY_HISTORY_OPTIONS);
112
180
  if (!options || typeof options.store?.record !== "function") {
113
181
  throw new Error("activityHistory.store must implement record(event)");
114
182
  }
@@ -116,6 +184,6 @@ export function activityHistory(options) {
116
184
  ...options,
117
185
  handle: routeActivity,
118
186
  recordTool: recordToolActivity,
119
- recordDrift: recordCatalogDriftActivity,
187
+ recordChange: recordCatalogChangeActivity,
120
188
  };
121
189
  }