@zackbart/connecta 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +5 -4
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/agent-routes.js +231 -0
  8. package/dist/auth/clerk-sdk/generated.js +2 -1
  9. package/dist/auth/downstream-oauth/consent.js +519 -0
  10. package/dist/auth/downstream-oauth/grant-store.js +439 -0
  11. package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
  12. package/dist/auth/downstream-oauth/refresh-records.js +447 -0
  13. package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
  14. package/dist/auth/downstream-oauth.js +66 -2516
  15. package/dist/auth/downstream-revocation.js +1 -1
  16. package/dist/auth/native.js +27 -0
  17. package/dist/auth/oauth-grant.js +24 -0
  18. package/dist/auth/oauth-token-response.js +1 -1
  19. package/dist/auth/static-oauth.js +153 -131
  20. package/dist/auth/vercel-agent.js +2 -1
  21. package/dist/auth-elicitation.js +9 -3
  22. package/dist/authorization-handoff.js +11 -5
  23. package/dist/branding.d.ts +1 -15
  24. package/dist/branding.js +2 -2
  25. package/dist/bytes.js +42 -0
  26. package/dist/call-admission-context.js +2 -5
  27. package/dist/call-recovery.js +0 -3
  28. package/dist/catalog-cache.js +130 -43
  29. package/dist/catalog-fingerprint.js +2 -18
  30. package/dist/catalog-policy.js +18 -0
  31. package/dist/catalog-service.js +285 -48
  32. package/dist/catalog.js +18 -2
  33. package/dist/config-defaults.d.ts +1 -0
  34. package/dist/config-defaults.js +1 -0
  35. package/dist/config-schema.d.ts +1 -1
  36. package/dist/config-schema.js +6 -2
  37. package/dist/config.d.ts +57 -25
  38. package/dist/config.js +30 -77
  39. package/dist/connector-caller.d.ts +1 -1
  40. package/dist/connector-health.js +59 -0
  41. package/dist/connector-status.js +117 -0
  42. package/dist/connectors/api-connector.d.ts +7 -2
  43. package/dist/connectors/api-connector.js +11 -7
  44. package/dist/connectors/guarded-fetch.d.ts +0 -6
  45. package/dist/connectors/guarded-fetch.js +1 -11
  46. package/dist/connectors/negotiation-cache.js +22 -4
  47. package/dist/connectors/remote-mcp/call.js +164 -0
  48. package/dist/connectors/remote-mcp/catalog.js +278 -0
  49. package/dist/connectors/remote-mcp/config.js +263 -0
  50. package/dist/connectors/remote-mcp/connection.js +671 -0
  51. package/dist/connectors/remote-mcp/errors.js +488 -0
  52. package/dist/connectors/remote-mcp/oauth.js +469 -0
  53. package/dist/connectors/remote-mcp/resources.js +473 -0
  54. package/dist/connectors/remote-mcp/runtime.js +1 -0
  55. package/dist/connectors/remote-mcp/transport.js +544 -0
  56. package/dist/connectors/remote-mcp.js +61 -2898
  57. package/dist/connectors/resource-uri.js +0 -4
  58. package/dist/credential-contract.d.ts +23 -0
  59. package/dist/credential-reads.js +17 -0
  60. package/dist/credential-verification.js +53 -0
  61. package/dist/credentials.d.ts +7 -0
  62. package/dist/credentials.js +60 -25
  63. package/dist/d1.d.ts +0 -89
  64. package/dist/d1.js +0 -34
  65. package/dist/describe-config.d.ts +12 -2
  66. package/dist/describe-config.js +2 -0
  67. package/dist/discovery-health.js +199 -0
  68. package/dist/downstream-input.js +4 -2
  69. package/dist/errors.d.ts +55 -17
  70. package/dist/errors.js +95 -41
  71. package/dist/execute.js +172 -125
  72. package/dist/executor-result.js +5 -2
  73. package/dist/executors/quickjs-runtime.js +2 -25
  74. package/dist/executors/quickjs.js +1 -1
  75. package/dist/guest-functions.js +83 -0
  76. package/dist/guest-runtime.js +63 -20
  77. package/dist/guest-types.d.ts +24 -10
  78. package/dist/http/argument-problems.js +35 -0
  79. package/dist/http/client.js +1 -1
  80. package/dist/http/errors.js +7 -6
  81. package/dist/http/fields.js +192 -13
  82. package/dist/http/hatch-response.js +73 -0
  83. package/dist/http/hatch.js +88 -23
  84. package/dist/http/json-schema.js +1 -1
  85. package/dist/http/node-transport.js +0 -1
  86. package/dist/http/oauth.js +4 -3
  87. package/dist/http/paging.js +6 -9
  88. package/dist/http/provider.js +33 -5
  89. package/dist/http/safety.js +9 -1
  90. package/dist/http/services.js +1 -8
  91. package/dist/http/tools.js +45 -30
  92. package/dist/http/vendor.js +7 -7
  93. package/dist/identity.js +2 -6
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.js +9 -2
  96. package/dist/invocation-auth.js +2 -1
  97. package/dist/invocation.js +21 -6
  98. package/dist/membership.d.ts +13 -5
  99. package/dist/membership.js +34 -2
  100. package/dist/meta-tools.js +50 -35
  101. package/dist/native-recovery.js +4 -16
  102. package/dist/native-tools.js +6 -2
  103. package/dist/oauth-handoff.js +2 -2
  104. package/dist/oauth-sealing.js +10 -0
  105. package/dist/operation-vocabulary.js +136 -14
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +2 -49
  108. package/dist/operator-ui/contract.d.ts +12 -2
  109. package/dist/operator-ui/generated.js +3 -3
  110. package/dist/operator-ui/model.d.ts +22 -1
  111. package/dist/program-writes.js +3 -8
  112. package/dist/provider-call.js +0 -7
  113. package/dist/provider.d.ts +1 -1
  114. package/dist/providers/_shared/google/kit.js +1 -6
  115. package/dist/providers/_shared/google/service-account.js +1 -3
  116. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  117. package/dist/providers/_shared/guide.js +3 -0
  118. package/dist/providers/_shared/json.js +12 -0
  119. package/dist/providers/_shared/rest/compact-index.js +1 -1
  120. package/dist/providers/_shared/rest/kit.js +1 -3
  121. package/dist/providers/_shared/rest/operation-index.js +49 -53
  122. package/dist/providers/_shared/rest/value-safety.js +106 -28
  123. package/dist/providers/breeze/api.js +9 -8
  124. package/dist/providers/breeze/index.js +4 -2
  125. package/dist/providers/breeze/skill.generated.js +1 -1
  126. package/dist/providers/breeze/tools.js +7 -7
  127. package/dist/providers/ccb/api.js +3 -7
  128. package/dist/providers/ccb/index.js +7 -1
  129. package/dist/providers/ccb/skill.generated.js +1 -1
  130. package/dist/providers/ccb/tools.js +1 -6
  131. package/dist/providers/cloudflare/api.js +10 -3
  132. package/dist/providers/cloudflare/index.js +2 -1
  133. package/dist/providers/cloudflare/safety.js +3 -3
  134. package/dist/providers/cloudflare/skill.generated.js +1 -1
  135. package/dist/providers/docs/lib/schemas.js +24 -22
  136. package/dist/providers/docs/safety.js +2 -0
  137. package/dist/providers/docs/skill.generated.js +1 -1
  138. package/dist/providers/docs/tools.js +4 -14
  139. package/dist/providers/drive/api.js +1 -0
  140. package/dist/providers/drive/lib/contracts.js +8 -5
  141. package/dist/providers/drive/skill.generated.js +1 -1
  142. package/dist/providers/drive/tools.js +23 -41
  143. package/dist/providers/forms/api.js +1 -0
  144. package/dist/providers/forms/skill.generated.js +1 -1
  145. package/dist/providers/forms/tools.js +11 -16
  146. package/dist/providers/github/api.js +1 -0
  147. package/dist/providers/github/lib/discovery.js +1 -1
  148. package/dist/providers/github/skill.generated.js +1 -1
  149. package/dist/providers/github/tools.js +61 -17
  150. package/dist/providers/gmail/lib/schemas.js +60 -50
  151. package/dist/providers/gmail/skill.generated.js +1 -1
  152. package/dist/providers/gmail/tools.js +12 -26
  153. package/dist/providers/granola/index.js +2 -0
  154. package/dist/providers/granola/skill.generated.js +1 -1
  155. package/dist/providers/overflow/api.js +8 -4
  156. package/dist/providers/overflow/index.js +2 -1
  157. package/dist/providers/overflow/skill.generated.js +1 -1
  158. package/dist/providers/overflow/tools.js +3 -7
  159. package/dist/providers/planning-center/api.js +5 -2
  160. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  161. package/dist/providers/planning-center/lib/json-api.js +1 -9
  162. package/dist/providers/planning-center/skill.generated.js +1 -1
  163. package/dist/providers/planning-center/tools.js +2 -1
  164. package/dist/providers/quickbooks/safety.js +80 -39
  165. package/dist/providers/quickbooks/skill.generated.js +1 -1
  166. package/dist/providers/sheets/api.js +1 -0
  167. package/dist/providers/sheets/skill.generated.js +1 -1
  168. package/dist/providers/sheets/tools.js +14 -23
  169. package/dist/providers/slides/skill.generated.js +1 -1
  170. package/dist/providers/slides/tools.js +19 -26
  171. package/dist/providers/stripe/index.js +2 -1
  172. package/dist/providers/stripe/safety.js +2 -2
  173. package/dist/providers/stripe/skill.generated.js +1 -1
  174. package/dist/providers/tithely/api.js +5 -8
  175. package/dist/providers/tithely/index.js +3 -2
  176. package/dist/providers/tithely/skill.generated.js +1 -1
  177. package/dist/providers/tithely/tools.js +7 -5
  178. package/dist/providers/vercel/api.js +2 -7
  179. package/dist/providers/vercel/lib/pin.js +1 -1
  180. package/dist/providers/vercel/safety.js +1 -1
  181. package/dist/providers/vercel/skill.generated.js +1 -1
  182. package/dist/providers/vercel/tools.js +6 -4
  183. package/dist/redaction-marker.js +2 -2
  184. package/dist/registry.d.ts +11 -5
  185. package/dist/registry.js +46 -20
  186. package/dist/request-principal.js +1 -1
  187. package/dist/request-state.js +2 -2
  188. package/dist/routes/access-tokens.js +1 -1
  189. package/dist/routes/activity-disclosure.js +1 -1
  190. package/dist/routes/connect.js +2 -2
  191. package/dist/routes/credentials.js +130 -61
  192. package/dist/routes/dispatch.js +5 -1
  193. package/dist/routes/mcp.js +34 -17
  194. package/dist/routes/oauth-management.js +1 -1
  195. package/dist/routes/operator.js +6 -5
  196. package/dist/routes/shared.d.ts +13 -8
  197. package/dist/routes/shared.js +15 -9
  198. package/dist/routes/ui-config.js +3 -3
  199. package/dist/routes/ui-data.js +126 -20
  200. package/dist/routes/ui.js +17 -1
  201. package/dist/runtime/call-admission.js +1 -1
  202. package/dist/runtime/call.js +8 -3
  203. package/dist/runtime/native-storage.js +9 -0
  204. package/dist/runtime/request-reads.js +182 -0
  205. package/dist/runtime/request-scope.js +16 -0
  206. package/dist/runtime/settlement.js +1 -1
  207. package/dist/runtime/shared-read.js +2 -15
  208. package/dist/runtime/storage.js +12 -0
  209. package/dist/sent-secrets.d.ts +1 -2
  210. package/dist/sent-secrets.js +3 -8
  211. package/dist/server.js +4 -4
  212. package/dist/skills.js +44 -54
  213. package/dist/sqlite.d.ts +0 -19
  214. package/dist/sqlite.js +2 -83
  215. package/dist/storage/keys.js +69 -128
  216. package/dist/storage/memory.js +8 -2
  217. package/dist/storage/sql-native.js +27 -101
  218. package/dist/storage/sql.d.ts +1 -24
  219. package/dist/storage/sql.js +13 -14
  220. package/dist/storage-digest.js +32 -0
  221. package/dist/timeout.js +8 -0
  222. package/dist/tool-error.js +4 -0
  223. package/dist/types.d.ts +33 -0
  224. package/dist/url-safety.js +0 -4
  225. package/dist/usage-guide.js +18 -37
  226. package/dist/validate.d.ts +3 -0
  227. package/dist/validate.js +220 -158
  228. package/dist/version.d.ts +1 -1
  229. package/dist/version.js +1 -1
  230. package/dist/worker.js +1 -1
  231. package/dist/write-outcome.js +0 -3
  232. package/documentation/README.md +19 -55
  233. package/documentation/architecture.md +436 -515
  234. package/documentation/auth.md +229 -88
  235. package/documentation/code-mode.md +201 -94
  236. package/documentation/deploying.md +25 -43
  237. package/documentation/integrating.md +18 -19
  238. package/documentation/meta-tools.md +225 -51
  239. package/documentation/migration-0.33.md +10 -6
  240. package/documentation/migration-0.34.md +5 -5
  241. package/documentation/migration-retired-layouts.md +74 -0
  242. package/documentation/operator-tests.md +20 -120
  243. package/documentation/operator-ui.md +108 -16
  244. package/documentation/pool-membership-upgrade.md +44 -0
  245. package/documentation/provider-auth-migration.md +4 -10
  246. package/documentation/provider-migration-0.29.md +4 -15
  247. package/documentation/providers.md +105 -0
  248. package/examples/worker/AGENTS.md +14 -16
  249. package/examples/worker/README.md +24 -118
  250. package/examples/worker/src/connecta.config.ts +1 -0
  251. package/guides/connecting-agents.md +9 -31
  252. package/guides/how-it-works.md +6 -4
  253. package/guides/safety-and-trust.md +4 -1
  254. package/package.json +19 -19
  255. package/templates/node/AGENTS.md +13 -12
  256. package/templates/node/README.md +9 -26
  257. package/templates/node/package.json +1 -1
  258. package/templates/node/src/connecta.config.ts +1 -0
  259. package/dist/auth/oauth-v2-migration.js +0 -135
  260. package/dist/provider-call-facts.d.ts +0 -115
  261. package/dist/provider-call.d.ts +0 -112
  262. package/dist/storage/d1-copy.js +0 -255
  263. package/dist/storage/sql-schema.d.ts +0 -6
  264. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  265. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
@@ -1,4 +1,5 @@
1
- // Value safety for every REST vendor (decision 0005): no tool response,
1
+ import { isRecord } from "../json.js";
2
+ // Value safety for every REST vendor: no tool response,
2
3
  // success or error, returns a credential (API, service, or bypass tokens,
3
4
  // signing secrets, bearer URLs, private keys, temporary credentials, auth
4
5
  // headers, authorization verifiers) or a stored secret value (environment
@@ -34,7 +35,7 @@
34
35
  // Sent-secret redaction runs first, in the provider kit's parse pass, so a
35
36
  // rule that decides on a value can find the placeholder where the vendor
36
37
  // wrote what the rule matches. Every such decision input fails closed
37
- // (#703): an altered type discriminator applies every resource rule; an
38
+ //: an altered type discriminator applies every resource rule; an
38
39
  // altered label or type makes a secret record or a header rule; an altered
39
40
  // URL scheme or parameter name withholds the whole URL or its value; an
40
41
  // altered key withholds its value; a placeholder that may stand for a
@@ -204,10 +205,7 @@ export function credentialName(name) {
204
205
  const normalized = normalizedName(name);
205
206
  return containsCredential(normalized) || CREDENTIAL_SUFFIX.test(normalized);
206
207
  }
207
- function isRecord(value) {
208
- return typeof value === "object" && value !== null && !Array.isArray(value);
209
- }
210
- /** A rule's test over one decision input; an input redaction altered passes it (#703). */
208
+ /** A rule's test over one decision input; an input redaction altered passes it. */
211
209
  function decides(input, test) {
212
210
  return typeof input === "string" && (redactionAltered(input) || test(input));
213
211
  }
@@ -215,7 +213,7 @@ function decides(input, test) {
215
213
  const PLACEHOLDER_KEYS = new WeakSet();
216
214
  /**
217
215
  * Whether redaction altered one of a record's keys, so no key-based decision
218
- * about it can be read (#703). Parsed JSON keeps its keys; text the kit
216
+ * about it can be read. Parsed JSON keeps its keys; text the kit
219
217
  * redacted whole and a result hook parsed after it (NDJSON, log streams) can
220
218
  * carry the placeholder in one.
221
219
  */
@@ -226,7 +224,7 @@ function keysAltered(value) {
226
224
  const SCHEME_PREFIX = /^(?:[a-z][a-z0-9+.-]*(?::\/{0,2})?)?$/i;
227
225
  /**
228
226
  * Whether redaction altered the scheme a whole-string URL decision reads
229
- * (#703): the text is no longer URL-shaped, everything before its first
227
+ *: the text is no longer URL-shaped, everything before its first
230
228
  * placeholder could begin a URL, and more than placeholders follows it. A
231
229
  * URL a vendor sends holds no whitespace.
232
230
  */
@@ -434,7 +432,7 @@ function endsValue(text, at) {
434
432
  /**
435
433
  * Where a word at `start` holds a placeholder in place of a link's scheme
436
434
  * and host (`[redacted]/pkg?token=…`, `https:[redacted]…`), or `-1`:
437
- * redaction took the authority that makes text a link (#703). A Markdown
435
+ * redaction took the authority that makes text a link. A Markdown
438
436
  * label (`[redacted](…)`) is not one; its destination is read as a link.
439
437
  */
440
438
  function markerLed(text, start) {
@@ -524,7 +522,7 @@ function linkSpan(text, start, host, inline) {
524
522
  /**
525
523
  * Visit each destination once, respecting its CommonMark close and encoded
526
524
  * delimiters. Redaction (`failClosed`) also reads a link whose scheme and
527
- * host redaction took (#703); admission never does.
525
+ * host redaction took; admission never does.
528
526
  */
529
527
  function* linkSpans(text, failClosed = false) {
530
528
  const markers = failClosed && redactionAltered(text);
@@ -585,7 +583,7 @@ function credentialQuery(text) {
585
583
  const fragment = decoded.indexOf("#");
586
584
  if (fragment >= 0) {
587
585
  const words = decodeLinkText(decoded.slice(fragment + 1));
588
- // A fragment redaction altered cannot be read (#703).
586
+ // A fragment redaction altered cannot be read.
589
587
  if (/token|secret|key|code|auth/i.test(words) || alteredFragment(words))
590
588
  return true;
591
589
  }
@@ -597,7 +595,7 @@ function credentialQuery(text) {
597
595
  const equals = pair.indexOf("=");
598
596
  at = end + 1;
599
597
  // A component redaction altered with no readable name (its name, or its `=`, taken) may be a
600
- // credential parameter's value, however encoded (#703).
598
+ // credential parameter's value, however encoded.
601
599
  if (pair !== REDACTED && redactionAltered(equals < 0 ? pair : pair.slice(0, equals)))
602
600
  return true;
603
601
  if (equals < 0)
@@ -615,7 +613,7 @@ function withholdLinkQueries(text) {
615
613
  let out = "";
616
614
  let copied = 0;
617
615
  for (const { cut, open, end, start, host } of linkSpans(text, true)) {
618
- // A parameter name redaction altered cannot be read, so the whole link goes (#703).
616
+ // A parameter name redaction altered cannot be read, so the whole link goes.
619
617
  const whole = alteredLink(text.slice(host, end));
620
618
  if (!whole && (cut < 0 || !credentialQuery(text.slice(cut, end))))
621
619
  continue;
@@ -640,7 +638,7 @@ function withholdLinkQueries(text) {
640
638
  }
641
639
  return copied === 0 ? text : out + text.slice(copied);
642
640
  }
643
- /** Whether redaction altered a parameter name in a link's own path, query, or fragment (#703). */
641
+ /** Whether redaction altered a parameter name in a link's own path, query, or fragment. */
644
642
  function alteredLink(link) {
645
643
  return redactionAltered(link) && parameterNames(decodeLinkText(link)).some(redactionAltered);
646
644
  }
@@ -676,7 +674,7 @@ function withholdSigningParams(anchored) {
676
674
  }
677
675
  const NAME_CHAR = /[\w.%~;-]/;
678
676
  const NAME_ESCAPE = new RegExp(Array.from("abcdefghijklmnopqrstuvwxyz0123456789_.~-", signingName).join("|"), "iy");
679
- /** Where a parameter's value ends, reading placeholders and character references whole (#703). */
677
+ /** Where a parameter's value ends, reading placeholders and character references whole. */
680
678
  function valueEnd(text, at, inferred) {
681
679
  while (at < text.length) {
682
680
  const reference = Math.max(characterReferenceEnd(text, at, inferred), bareReferenceEnd(text, at, inferred));
@@ -693,7 +691,7 @@ function valueEnd(text, at, inferred) {
693
691
  }
694
692
  /**
695
693
  * The next placeholder that may hide a signing parameter, at or after `from`,
696
- * and its value boundary when a name is recognized (#703). Redaction may have taken a signing name, its
694
+ * and its value boundary when a name is recognized. Redaction may have taken a signing name, its
697
695
  * `=`, and the start of its value, so a placeholder followed by any of a
698
696
  * value's base64 or base64url characters (`+`, `/`, `=`, `-`, `_` included)
699
697
  * or `%` escapes goes with them, however short. References count when they
@@ -956,7 +954,7 @@ function labelsOf(value) {
956
954
  }
957
955
  /**
958
956
  * Whether a record's label names a credential, so its value or args are one.
959
- * A label redaction altered, or keys that may hide one, does too (#703).
957
+ * A label redaction altered, or keys that may hide one, does too.
960
958
  */
961
959
  function credentialLabelled(value) {
962
960
  if (!isRecord(value))
@@ -966,17 +964,17 @@ function credentialLabelled(value) {
966
964
  (isRecord(target) && keysAltered(target)) ||
967
965
  labelsOf(value).some((label) => decides(label, credentialName)));
968
966
  }
969
- /** A record typed as a secret (`secret_text`); a type redaction altered may have been one (#703). */
967
+ /** A record typed as a secret (`secret_text`); a type redaction altered may have been one. */
970
968
  function typedSecret(value) {
971
969
  return keysAltered(value) || decides(value["type"], (type) => /secret/i.test(type));
972
970
  }
973
- /** A list item typed as an environment variable; a type redaction altered may have been one (#703). */
971
+ /** A list item typed as an environment variable; a type redaction altered may have been one. */
974
972
  function envTyped(value) {
975
973
  if (!isRecord(value))
976
974
  return false;
977
975
  return keysAltered(value) || decides(String(value["type"]), (type) => /env/i.test(type));
978
976
  }
979
- /** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …); likewise (#703). */
977
+ /** A header, cookie, or query rule (`request.headers`, `header`, `cookie`, `query`, …); likewise. */
980
978
  function headerLike(value) {
981
979
  return isRecord(value) && (keysAltered(value) || decides(value["type"], (type) => /header|cookie|query/i.test(type)));
982
980
  }
@@ -989,7 +987,7 @@ function headerValues(value) {
989
987
  if (isRecord(value))
990
988
  return Object.fromEntries(Object.keys(value).map((name) => [name, REDACTED]));
991
989
  if (Array.isArray(value)) {
992
- // A pair whose keys redaction altered may hold its value under any of them (#703).
990
+ // A pair whose keys redaction altered may hold its value under any of them.
993
991
  return value.map((item) => !isRecord(item)
994
992
  ? item
995
993
  : keysAltered(item)
@@ -1120,7 +1118,7 @@ function scrubEnv(value, depth) {
1120
1118
  if (depth > 32)
1121
1119
  return REDACTED;
1122
1120
  if (Array.isArray(value)) {
1123
- // A `NAME=value` string, or one whose name and `=` redaction may have taken (#703).
1121
+ // A `NAME=value` string, or one whose name and `=` redaction may have taken.
1124
1122
  return value.map((item) => typeof item === "string" && (item.includes("=") || redactionAltered(item)) ? REDACTED : scrubEnv(item, depth + 1));
1125
1123
  }
1126
1124
  if (!isRecord(value))
@@ -1159,7 +1157,7 @@ function scrubValues(value, depth) {
1159
1157
  }
1160
1158
  return out;
1161
1159
  }
1162
- /** Every rule's paths and withheld fields: the rule for an object whose type redaction altered (#703). */
1160
+ /** Every rule's paths and withheld fields: the rule for an object whose type redaction altered. */
1163
1161
  const ANY_RULE = new WeakMap();
1164
1162
  function anyRule(resources) {
1165
1163
  let rule = ANY_RULE.get(resources);
@@ -1172,7 +1170,7 @@ function anyRule(resources) {
1172
1170
  }
1173
1171
  /**
1174
1172
  * What a record's type discriminator selects. A type redaction altered, or
1175
- * one an altered key may hide, selects every rule at once (#703).
1173
+ * one an altered key may hide, selects every rule at once.
1176
1174
  */
1177
1175
  function typed(record, key, rules, any) {
1178
1176
  const type = record[key];
@@ -1247,7 +1245,7 @@ function heuristic(value, exempt) {
1247
1245
  if (!exempt.reviewedUrls)
1248
1246
  return URL_SHAPE.test(current) ? sanitizeUrl(current) : current;
1249
1247
  // A placeholder's own brackets are not Markdown; a URL whose scheme redaction
1250
- // altered may still carry userinfo, so it is withheld whole (#703).
1248
+ // altered may still carry userinfo, so it is withheld whole.
1251
1249
  const altered = redactionAltered(current);
1252
1250
  if (TEXT.test(altered ? current.replaceAll(REDACTED, "") : current))
1253
1251
  return current;
@@ -1269,7 +1267,7 @@ function heuristic(value, exempt) {
1269
1267
  const normalized = normalizedName(key);
1270
1268
  const review = Object.hasOwn(exempt.fields, key) ? exempt.fields[key] : undefined;
1271
1269
  if (!placeholders && redactionAltered(key)) {
1272
- // A key redaction altered may have named a credential (#703).
1270
+ // A key redaction altered may have named a credential.
1273
1271
  out[key] = withheld(item);
1274
1272
  }
1275
1273
  else if (review?.verdict === "redact") {
@@ -1313,7 +1311,7 @@ function heuristic(value, exempt) {
1313
1311
  typeof item === "string" &&
1314
1312
  (URL_SHAPE.test(item) || alteredScheme(item))) {
1315
1313
  // An external destination can carry its secret in the path; relative paths stay.
1316
- // One whose scheme redaction altered does not parse, so it is withheld (#703).
1314
+ // One whose scheme redaction altered does not parse, so it is withheld.
1317
1315
  out[key] = originOnly(item);
1318
1316
  }
1319
1317
  else if (typeof item === "string" && verbatim?.has(key)) {
@@ -1340,6 +1338,53 @@ function acceptsCredential(schema, depth) {
1340
1338
  acceptsCredential(node.items, depth + 1) ||
1341
1339
  (node.anyOf ?? []).some((branch) => acceptsCredential(branch, depth + 1)));
1342
1340
  }
1341
+ const ENV_VALUE_NAMES = ENV_VALUE_FIELDS.map(normalizedName);
1342
+ /** `data[].id` segments as tokens, a list's items as `[]`: `["data", "[]", "id"]`. */
1343
+ function tokensOf(segments) {
1344
+ return segments.flatMap((segment) => segment === "[]"
1345
+ ? ["[]"]
1346
+ : segment.endsWith("[]")
1347
+ ? [normalizedName(segment.slice(0, -2)), "[]"]
1348
+ : [normalizedName(segment)]);
1349
+ }
1350
+ /**
1351
+ * A best-effort hint for discovery over rules that change every non-null value
1352
+ * they reach: a path is withheld when it sits at or inside a reviewed redaction
1353
+ * path (matched from the body's root, or at any depth with `anyDepth`), or any
1354
+ * of its names satisfies the name rule, which also sees the parent's name.
1355
+ * Paths carry their list markers: a reviewed `[]` needs a declared list there,
1356
+ * while a named segment also reaches every item of a list. Rules that can leave
1357
+ * a value unchanged (`url:`, `origin:` or `#url` sanitizing, `[?…]`, `@keys`)
1358
+ * are not hints. Names compare normalized, and `{}` or `*` matches any name.
1359
+ */
1360
+ export function pathWithholds(rules) {
1361
+ const patterns = (rules.paths ?? []).flatMap((raw) => {
1362
+ const { mode, segments } = parsePath(raw);
1363
+ if (mode !== "redact" || segments.some((segment) => segment.startsWith("[?") || segment === "@keys"))
1364
+ return [];
1365
+ return [segments.map((segment) => (segment === "[]" ? "[]" : segment === "{}" ? "*" : normalizedName(segment)))];
1366
+ });
1367
+ const reaches = (pattern, tokens, from) => {
1368
+ let at = from;
1369
+ for (const part of pattern) {
1370
+ if (part !== "[]")
1371
+ while (tokens[at] === "[]")
1372
+ at += 1;
1373
+ if (at >= tokens.length || (part === "[]" ? tokens[at] !== "[]" : part !== "*" && part !== tokens[at]))
1374
+ return false;
1375
+ at += 1;
1376
+ }
1377
+ return true;
1378
+ };
1379
+ return (path) => {
1380
+ const tokens = tokensOf(path);
1381
+ const names = tokens.filter((token) => token !== "[]");
1382
+ if (rules.names && names.some((name, at) => rules.names(name, names[at - 1] ?? "")))
1383
+ return true;
1384
+ const starts = rules.anyDepth ? tokens.flatMap((token, at) => (token === "[]" ? [] : [at])) : [0];
1385
+ return patterns.some((pattern) => starts.some((at) => reaches(pattern, tokens, at)));
1386
+ };
1387
+ }
1343
1388
  /** A vendor's value-safety mechanism over its reviewed table and pinned index. */
1344
1389
  export function valueSafety(table, index) {
1345
1390
  const fields = table.fields ?? {};
@@ -1349,9 +1394,42 @@ export function valueSafety(table, index) {
1349
1394
  const key = `${method} ${template}`;
1350
1395
  return Object.hasOwn(table.operations, key) ? table.operations[key] : undefined;
1351
1396
  };
1397
+ const hints = new Map();
1398
+ const redactedNames = Object.entries(fields)
1399
+ .filter(([, review]) => review.verdict === "redact")
1400
+ .map(([name]) => normalizedName(name));
1401
+ const keptNames = Object.entries(fields)
1402
+ .filter(([, review]) => review.verdict === "keep")
1403
+ .map(([name]) => normalizedName(name));
1404
+ const scopeParents = scopeMaps.map(normalizedName);
1352
1405
  return {
1353
1406
  table,
1354
1407
  verdict,
1408
+ withholds(method, template) {
1409
+ const key = `${method} ${template}`;
1410
+ let hint = hints.get(key);
1411
+ if (!hint) {
1412
+ const reviewed = verdict(method, template);
1413
+ const env = table.envBodies?.test(template) === true;
1414
+ const kept = new Set([
1415
+ ...keptNames,
1416
+ ...(reviewed && reviewed.verdict !== "refuse" ? (reviewed.keep ?? []) : []).map((path) => trailOf(path).map(normalizedName).at(-1)),
1417
+ ]);
1418
+ hint =
1419
+ reviewed?.verdict === "refuse"
1420
+ ? () => true
1421
+ : pathWithholds({
1422
+ paths: reviewed?.verdict === "redact" ? reviewed.paths : [],
1423
+ names: (name, parent) => redactedNames.includes(name) ||
1424
+ (env && ENV_VALUE_NAMES.includes(name)) ||
1425
+ (parent === "deployhooks" && name === "url") ||
1426
+ // The suffix rule scrubs only strings, so only the whole-value credential rule hints.
1427
+ (!kept.has(name) && !scopeParents.includes(parent) && containsCredential(name)),
1428
+ });
1429
+ hints.set(key, hint);
1430
+ }
1431
+ return hint;
1432
+ },
1355
1433
  refusal(method, template) {
1356
1434
  const reviewed = verdict(method, template);
1357
1435
  return reviewed?.verdict === "refuse" ? `Connecta refuses ${method} ${template}. ${reviewed.reason}` : undefined;
@@ -1367,7 +1445,7 @@ export function valueSafety(table, index) {
1367
1445
  }
1368
1446
  const resources = table.embeddedUrls.resources;
1369
1447
  if (resources) {
1370
- // A type redaction altered takes every resource's reviewed text paths (#703).
1448
+ // A type redaction altered takes every resource's reviewed text paths.
1371
1449
  const all = [...new Set(Object.values(resources.paths).flat())];
1372
1450
  const any = () => all;
1373
1451
  const scan = (value, depth) => {
@@ -1,3 +1,6 @@
1
+ import { Routes, routePhrases } from "../../agent-routes.js";
2
+ import { pathWithholds } from "../_shared/rest/value-safety.js";
3
+ import { asRecord } from "../_shared/json.js";
1
4
  // Breeze's reviewed GET-only vendor. HTTP methods do not establish safety: see tools.ts.
2
5
  import { Effect, Schema } from "effect";
3
6
  import { ConnectorCallError, resendAdvice } from "../../errors.js";
@@ -9,12 +12,6 @@ import { openapi } from "./openapi.generated.js";
9
12
  export const DEFAULT_PAGE_SIZE = 100;
10
13
  export const MAX_PAGE_SIZE = 1000;
11
14
  export const BREEZE_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
12
- export function asRecord(value) {
13
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
14
- }
15
- export function asArray(value) {
16
- return Array.isArray(value) ? value : [];
17
- }
18
15
  // --- The read hatch's allowlist ----------------------------------------------
19
16
  /** Reviewed observational endpoints and their documented query names. GET alone establishes nothing. */
20
17
  export const READ_ENDPOINTS = new Map([
@@ -472,10 +469,11 @@ export const admit = (url) => {
472
469
  };
473
470
  export const authenticate = Effect.gen(function* () {
474
471
  const values = yield* (yield* Credentials).values.pipe(Effect.mapError((error) => new Refusal({ error })));
472
+ const routes = yield* Routes;
475
473
  const key = values?.["value"]?.trim();
476
474
  if (!key)
477
475
  return yield* Effect.fail(new Refusal({
478
- error: new ConnectorCallError("auth_required", "No Breeze API key is configured for this connector. Call authorize_connector for recovery options. When available, an operator can add the key in this connection in the operator UI."),
476
+ error: new ConnectorCallError("auth_required", `No Breeze API key is configured for this connector. ${routePhrases(routes).authorizeFor("recovery options")}. When available, an operator can add the key in this connection in the operator UI.`),
479
477
  }));
480
478
  return { "Api-Key": key };
481
479
  });
@@ -542,6 +540,9 @@ export const breezeSafety = {
542
540
  })(),
543
541
  }),
544
542
  redact: (body, op) => withoutCalendarFeeds(op.template, body),
543
+ withholds: (op) => op.template === "/events/calendars/list"
544
+ ? pathWithholds({ names: (name) => name === "address" || name === "embedkey" })
545
+ : () => false,
545
546
  withholdsErrors: () => false,
546
547
  },
547
548
  sensitiveFields: { names: [], choice: "drop" },
@@ -551,7 +552,7 @@ function shortText(value, secrets) {
551
552
  return value.trim() || undefined;
552
553
  if (value === undefined || value === null)
553
554
  return undefined;
554
- // The kit redacted the answer's values and kept its keys (#703); serialized, a key is text
555
+ // The kit redacted the answer's values and kept its keys; serialized, a key is text
555
556
  // the cut below could split, so the whole text is checked before it is cut.
556
557
  const text = secrets.redact(JSON.stringify(value));
557
558
  return text.length > 300 ? `${text.slice(0, 300)}…` : text;
@@ -1,8 +1,10 @@
1
+ import { readOnlyNotice } from "../_shared/guide.js";
2
+ import { asRecord } from "../_shared/json.js";
1
3
  import { skill } from "./skill.generated.js";
2
4
  import { keys, optionsOf } from "../../config-schema.js";
3
5
  import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
4
6
  import { defineApiProvider } from "../../http/provider.js";
5
- import { accountSummary, admit, asRecord, authenticate, BreezeApi, breezeSafety, BREEZE_MAX_RESPONSE_BYTES, BREEZE_INDEX, BREEZE_VERDICTS, BREEZE_NAMED, breezePeoplePage, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, errors, result, } from "./api.js";
7
+ import { accountSummary, admit, authenticate, BreezeApi, breezeSafety, BREEZE_MAX_RESPONSE_BYTES, BREEZE_INDEX, BREEZE_VERDICTS, BREEZE_NAMED, breezePeoplePage, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, errors, result, } from "./api.js";
6
8
  import { breezeTools } from "./tools.js";
7
9
  export const BREEZE_HOST_SUFFIX = ".breezechms.com";
8
10
  const SUBDOMAIN_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -68,7 +70,7 @@ export const breeze = defineApiProvider({
68
70
  `Church database: ${normalizeSubdomain(options.subdomain)}${BREEZE_HOST_SUFFIX}.`,
69
71
  `Church purpose: ${options.purpose}`,
70
72
  "Calendar feed addresses and embed keys are always removed; profile details follow the requested vendor detail level.",
71
- ...(options.readOnly ? ["This connection is read-only: every write is refused."] : []),
73
+ ...readOnlyNotice(options.readOnly),
72
74
  ...(options.defaultPageSize !== undefined
73
75
  ? [`Paged lists return ${options.defaultPageSize} items by default on this connection.`]
74
76
  : []),
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## Church records\n\nBreeze ChMS (now Tithely Church Management) holds people, tags, events,\nattendance, forms, volunteers and giving. People carry contact information,\nbirthdates and family ties; giving reveals donors and amounts. Reduce records\nin code and return only the names, counts or totals the task needs.\n\n## Ids and joins\n\nIds are numeric strings. Resolve them from the relevant inventory before\nwriting. Events have an occurrence id and a series `eventId`; attendance,\nvolunteers and check-in use the occurrence id. Event and pledge lists can lag\nup to 15 minutes behind the site. An all-zero MySQL date means no time was\nrecorded, including no check-out.\n\nProfile `details` use field ids, not names. Resolve field and option ids from\n`list_profile_fields`; multiple-choice filters join option ids with `-`.\nA person-name lookup requires listing people and matching in code. Form entry\nresponses similarly join to that form's field ids.\n\n## Giving\n\nGiving, funds and pledges use archival endpoints removed from the public\nreference in 2023. They can change or vanish without notice. Amounts are\nexact decimal strings: reduce them with integer-cent arithmetic, and read the\ncurrency from `get_account_summary`. A split gift's fund amounts belong to\nits fund ids. Tithe.ly deposits sync in batches, so recent gifts can be absent.\nEditing a giving transaction may return a new payment id.\nUse `limit: 1000` when reducing broad giving ranges: each local page refetches\nthe bounded vendor list, so larger windows avoid repeated full-list fetches.\n\n## Required sequences\n\nCheck for an existing person before adding one: creation does not deduplicate.\nResolve profile fields before updating them. Text, dates (M/D/YYYY) and option\nids use `response`; email, phone, address and family-role values use `details`.\nNamed person writes take these entries in `values`.\nFamily changes can remove previous associations. Attendance check-out records\na departure time; removing attendance deletes the record instead.\n\nBreeze sends writes as GET requests too. Query names ending in `_json` take\nJSON-encoded strings. The named person writes encode their typed profile values\nfor you. The account-log `object_json` is a serialized affected-object value;\nits free-text description has no stable schema. Events and account logs have\nvendor caps: narrow the date range when the result fills its limit.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `breeze_api_details`, `breeze_api_search`, `list_attendance`, `list_contributions`, `list_funds`, `list_people`, `list_profile_fields`, `list_tags`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `breeze_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 100, at most 1000). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `breeze_api_search` finds this API's operations and reads no account data, `breeze_api_details` shows one operation's contract, and `breeze_api_read` calls a reviewed read. `breeze_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## Church records\n\nBreeze ChMS (now Tithely Church Management) holds people, tags, events,\nattendance, forms, volunteers and giving. People carry contact information,\nbirthdates and family ties; giving reveals donors and amounts. Reduce records\nin code and return only the names, counts or totals the task needs.\n\n## Ids and joins\n\nIds are numeric strings. Resolve them from the relevant inventory before\nwriting. Events have an occurrence id and a series `eventId`; attendance,\nvolunteers and check-in use the occurrence id. Event and pledge lists can lag\nup to 15 minutes behind the site. An all-zero MySQL date means no time was\nrecorded, including no check-out.\n\nProfile `details` use field ids, not names. Resolve field and option ids from\n`list_profile_fields`; multiple-choice filters join option ids with `-`.\nA person-name lookup requires listing people and matching in code. Form entry\nresponses similarly join to that form's field ids.\n\n## Giving\n\nGiving, funds and pledges use archival endpoints removed from the public\nreference in 2023. They can change or vanish without notice. Amounts are\nexact decimal strings: reduce them with integer-cent arithmetic, and read the\ncurrency from `get_account_summary`. A split gift's fund amounts belong to\nits fund ids. Tithe.ly deposits sync in batches, so recent gifts can be absent.\nEditing a giving transaction may return a new payment id.\nUse `limit: 1000` when reducing broad giving ranges: each local page refetches\nthe bounded vendor list, so larger windows avoid repeated full-list fetches.\n\n## Required sequences\n\nCheck for an existing person before adding one: creation does not deduplicate.\nResolve profile fields before updating them. Text, dates (M/D/YYYY) and option\nids use `response`; email, phone, address and family-role values use `details`.\nNamed person writes take these entries in `values`.\nFamily changes can remove previous associations. Attendance check-out records\na departure time; removing attendance deletes the record instead.\n\nBreeze sends writes as GET requests too. Query names ending in `_json` take\nJSON-encoded strings. The named person writes encode their typed profile values\nfor you. The account-log `object_json` is a serialized affected-object value;\nits free-text description has no stable schema. Events and account logs have\nvendor caps: narrow the date range when the result fills its limit.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `breeze_api_details`, `breeze_api_search`, `list_attendance`, `list_contributions`, `list_funds`, `list_people`, `list_profile_fields`, `list_tags`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. For `breeze_api_read`, put it in `query[page.param]` or `body[page.param]`, as `page.in` specifies. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 100, at most 1000). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `breeze_api_search` finds this API's operations and reads no account data, `breeze_api_details` shows one operation's contract, and `breeze_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `breeze_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Church instructions"
5
5
  };
@@ -1,17 +1,16 @@
1
+ import { asArray, asRecord } from "../_shared/json.js";
2
+ import { compact } from "../_shared/json.js";
1
3
  // Breeze's curated surface: projections and profile encoders earn named tools; the standard hatch supplies coverage.
2
4
  import { Effect, Schema } from "effect";
3
5
  import { ConnectorCallError } from "../../errors.js";
4
6
  import { Refusal } from "../../http/errors.js";
5
7
  import { Integer, isMinChars, isMaxChars } from "../../http/json-schema.js";
6
8
  import { RequestSecrets, Classification } from "../../http/services.js";
7
- import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
9
+ import { fieldsContract, selectFields } from "../../http/fields.js";
8
10
  import { read, list, write as writeTool } from "../../http/tools.js";
9
- import { asArray, asRecord, READ_ENDPOINTS } from "./api.js";
11
+ import { READ_ENDPOINTS } from "./api.js";
10
12
  const DEFAULT_EVENT_LIMIT = 100;
11
13
  const ObjectValue = Schema.Record(Schema.String, Schema.Unknown);
12
- function compact(value) {
13
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
14
- }
15
14
  function timestamp(value) {
16
15
  if (value === undefined)
17
16
  return undefined;
@@ -219,6 +218,7 @@ const Result_list_events = Schema.Struct({
219
218
  });
220
219
  const Row_list_events = Result_list_events.fields.events.value;
221
220
  const SelectedEvent = Schema.Struct(Object.fromEntries(Object.entries(Row_list_events.fields).map(([name, schema]) => [name, Schema.optionalKey(schema)])));
221
+ const EVENT_FIELDS = fieldsContract("item", SelectedEvent);
222
222
  const Result_list_attendance = Schema.Struct({
223
223
  "attendance": Schema.Array(Schema.Struct({
224
224
  "personId": Schema.String,
@@ -528,7 +528,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
528
528
  description: "Instances returned, 1 to Breeze's 1000. Defaults to 100.",
529
529
  default: 100,
530
530
  })),
531
- fields: Schema.optionalKey(fieldsInput("Dot paths to keep in each event item.")),
531
+ fields: Schema.optionalKey(EVENT_FIELDS.input),
532
532
  }),
533
533
  output: Schema.Struct({
534
534
  items: Schema.Array(SelectedEvent),
@@ -536,7 +536,7 @@ export function breezeTools({ connection, pageSize: defaultPageSize, }) {
536
536
  truncated: Schema.optionalKey(Schema.Array(Schema.String)),
537
537
  }),
538
538
  run: (args) => Effect.gen(function* () {
539
- const error = fieldsProblem(args.fields, new Set(Object.keys(Row_list_events.fields)), "an event item");
539
+ const error = EVENT_FIELDS.problem(args.fields);
540
540
  if (error)
541
541
  return yield* Effect.fail(new Refusal({ error }));
542
542
  const result = yield* load_list_events(args);
@@ -1,3 +1,5 @@
1
+ import { asRecord, asArray } from "../_shared/json.js";
2
+ import { routePhrases } from "../../agent-routes.js";
1
3
  import { CCB_MEDIA_TYPE, CCB_ENVIRONMENTS, CCB_READ_SCOPES, CCB_WRITE_SCOPES, CCB_SENSITIVE_FIELDS, } from "./lib/constants.js";
2
4
  // Pushpay ChMS v2 JSON contracts, OAuth, status meanings, admission and privacy.
3
5
  // The legacy per-church XML API is outside this provider's scope.
@@ -56,12 +58,6 @@ export const CCB_ADMISSION = {
56
58
  },
57
59
  ],
58
60
  };
59
- function asRecord(value) {
60
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
61
- }
62
- function asArray(value) {
63
- return Array.isArray(value) ? value : [];
64
- }
65
61
  // ---------------------------------------------------------------------------
66
62
  // Admission partitions
67
63
  // ---------------------------------------------------------------------------
@@ -470,7 +466,7 @@ export const errors = {
470
466
  statuses: {
471
467
  401: () => ({
472
468
  code: "auth_required",
473
- words: (detail) => `${detail} CCB rejected the connection's access token. Call authorize_connector for this connector.`,
469
+ words: (detail, routes) => `${detail} CCB rejected the connection's access token. ${routePhrases(routes).authorizeFor("this connector")}.`,
474
470
  advice: ", then retry",
475
471
  }),
476
472
  403: () => ({
@@ -1,3 +1,4 @@
1
+ import { readOnlyNotice } from "../_shared/guide.js";
1
2
  import { CCB_ENVIRONMENTS, CCB_MEDIA_TYPE } from "./lib/constants.js";
2
3
  import { skill } from "./skill.generated.js";
3
4
  import { keys, optionsOf } from "../../config-schema.js";
@@ -46,6 +47,11 @@ export const ccb = defineApiProvider({
46
47
  note: "./providers/ccb merged GitHub/Cloudflare/CCB step-2 snapshot measures 391,649 B standalone gzip-9 before recording these bundle facts, Node 26.12.0 with zlib-ng 1.3.1; recomputed with the combined opt-in hatch support. Earlier step-2 standalone gzip snapshot at ffd64b04, CI run 40080865116 static job, Node 22.23.3, against main ce0c7492 (345,272 B). Includes the reviewed 385-operation request/response index. Shared Effect is paid once per deployment; provider entries are reported, uncapped and non-additive (#703) (#899).",
47
48
  },
48
49
  skill,
50
+ // The guide resolves a person from individuals and a household from families.
51
+ searchSynonyms: [
52
+ ["person", "individual"],
53
+ ["household", "family"],
54
+ ],
49
55
  options: CCB_OPTIONS,
50
56
  displayTitle: (options) => defaultTitle(options.environment, options.mode, writable(options)),
51
57
  description: (options) => `Church Community Builder (Pushpay ChMS) — ${routingLine(options.environment, options.mode, writable(options))}: ${options.purpose}`,
@@ -70,7 +76,7 @@ export const ccb = defineApiProvider({
70
76
  ...(!writable(options)
71
77
  ? ["Configured scopes permit only reads: ccb_api_write is omitted and writes are refused."]
72
78
  : []),
73
- ...(options.readOnly ? ["This connection is read-only: every write is refused."] : []),
79
+ ...readOnlyNotice(options.readOnly),
74
80
  ...(options.defaultPerPage !== undefined
75
81
  ? [`Named lists default to ${options.defaultPerPage} rows on this connection.`]
76
82
  : []),
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/generate-providers.mjs; do not edit.
2
2
  export const skill = {
3
- "content": "## People, households and groups\n\nCCB v2 addresses records by integer ids. Resolve a person from the individual\nlist and a household from families; never guess ids. Family membership and\nchurch group membership join by individual id. A membership role belongs to\nthat group, rather than being a property of the person everywhere.\n\nIdentity Auth acts with the signed-in person's permissions. A permission\nrefusal can mean that person lacks access even when the connection is healthy.\n\n## Events and attendance\n\nAn event is a series; its dated occurrence is a separate identity. Attendance\njoins need both the event id and occurrence. Preserve the vendor's date and\ntime-zone meaning instead of treating every occurrence as UTC midnight.\n\n## Pastoral and financial records\n\nNotes, background checks, pledges and giving metrics can be sensitive. Read\nonly what the task needs and reduce those records inside execute_code before\nreturning an answer. Gift metrics are counts by period, with a range of at\nmost twelve months. Per-gift transactions and amounts belong to the legacy\nv1 XML API, which this v2 connector does not reach.\n\n## Vendor call rates and recovery\n\nCCB meters endpoints separately per API client. Individual profile reads have\nno burst; individual advanced searches allow one call per five seconds, and\ncategory schedule lists allow one per two seconds. Prefer list pages over a\nloop of individual gets. Other deployments sharing the client spend from the\nsame vendor allowance.\n\nFor a profile edit, resolve the individual first and preserve fields outside\nthe requested change. Use `ccb_api_write` for the edit; the host may ask the\nuser to confirm. After an ambiguous update, read that profile to determine\nwhat actually changed before deciding whether another write is needed.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `ccb_api_details`, `ccb_api_read`, `ccb_api_search`, `list_event_attendees`, `list_events`, `list_families`, `list_group_members`, `list_groups`, `list_individual_groups`, `list_individual_notes`, `list_individuals`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `ccb_api_search` finds this API's operations and reads no account data, `ccb_api_details` shows one operation's contract, and `ccb_api_read` calls a reviewed read. `ccb_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 14 operations are refused by design; use `ccb_api_details` with the method and path to inspect the original operation summary and its separate refusal. Use details for refused methods outside the read/write call schemas; those methods remain unavailable.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
3
+ "content": "## People, households and groups\n\nCCB v2 addresses records by integer ids. Resolve a person from the individual\nlist and a household from families; never guess ids. Family membership and\nchurch group membership join by individual id. A membership role belongs to\nthat group, rather than being a property of the person everywhere.\n\nIdentity Auth acts with the signed-in person's permissions. A permission\nrefusal can mean that person lacks access even when the connection is healthy.\n\n## Events and attendance\n\nAn event is a series; its dated occurrence is a separate identity. Attendance\njoins need both the event id and occurrence. Preserve the vendor's date and\ntime-zone meaning instead of treating every occurrence as UTC midnight.\n\n## Pastoral and financial records\n\nNotes, background checks, pledges and giving metrics can be sensitive. Read\nonly what the task needs and reduce those records inside execute_code before\nreturning an answer. Gift metrics are counts by period, with a range of at\nmost twelve months. Per-gift transactions and amounts belong to the legacy\nv1 XML API, which this v2 connector does not reach.\n\n## Vendor call rates and recovery\n\nCCB meters endpoints separately per API client. Individual profile reads have\nno burst; individual advanced searches allow one call per five seconds, and\ncategory schedule lists allow one per two seconds. Prefer list pages over a\nloop of individual gets. Other deployments sharing the client spend from the\nsame vendor allowance.\n\nFor a profile edit, resolve the individual first and preserve fields outside\nthe requested change. Use `ccb_api_write` for the edit; the host may ask the\nuser to confirm. After an ambiguous update, read that profile to determine\nwhat actually changed before deciding whether another write is needed.\n\n## Conventions\n\n- **Paging:** while `page.hasMore` is true, continue with `page.nextCursor` unchanged. For `ccb_api_details`, `ccb_api_read`, `ccb_api_search`, `list_event_attendees`, `list_events`, `list_families`, `list_group_members`, `list_groups`, `list_individual_groups`, `list_individual_notes`, `list_individuals`, `search_records`, pass it as the top-level `cursor`; omit raw paging anchors on continuations. Keep the original method, path, filters, page size, fields and headers. Paged `list_*` and `search_*` reads take `limit` (default 25, at most 100). The last page has no `nextCursor`. If `page.hasMore` is true without a cursor, the data is incomplete and continuation was withheld. When approaching execute_code's per-program call limit, return page.nextCursor before the call that would exceed it and continue in the next program; don't restart.\n- **Results:** `fields` narrows a read's result to the dot paths you name. `truncated` lists the dot paths a result cut for size; the ids that fetch what was cut stay in the result.\n- **Hatch:** `ccb_api_search` finds this API's operations and reads no account data, `ccb_api_details` shows one operation's contract, and `ccb_api_read` calls a reviewed read. Details' `response.fields` promises selection only; `withheld` hints are incomplete, and absence never means safe or readable. `ccb_api_write` calls a reviewed write; the host may ask the user to confirm it. An operation without a reviewed verdict is refused. 14 operations are refused by design; use `ccb_api_details` with the method and path to see each summary and refusal, even for methods outside the read/write schemas.\n- **Writes:** a write is sent once and never retried. When its outcome is unknown, read the target before repeating it. Writes take no idempotency key.\n- **Errors:** codes come from the vendor's statuses and error facts, never its prose. `not_found` means the record does not exist; this API tells absence from a permission gap. It documents no conflict.\n- **Withheld values:** a value withheld for safety reads `\"[redacted]\"`. Writes refuse whole marker values and withheld links; longer prose may mention the marker. Paths and queries refuse any containment.",
4
4
  "instructionsHeading": "Church instructions"
5
5
  };
@@ -1,3 +1,4 @@
1
+ import { asRecord, asArray } from "../_shared/json.js";
1
2
  // CCB v2 curated projections, bound numbered pages and reviewed POST data search.
2
3
  import { Effect, Schema } from "effect";
3
4
  import { ConnectorCallError } from "../../errors.js";
@@ -7,12 +8,6 @@ import { Refusal } from "../../http/errors.js";
7
8
  import { Integer, isMinChars, isMaxChars } from "../../http/json-schema.js";
8
9
  import { read, list } from "../../http/tools.js";
9
10
  import { callCcb, DEFAULT_PER_PAGE, SEARCH_DOMAINS, CCB_INDEX, ccbPageInfo, } from "./api.js";
10
- function asRecord(value) {
11
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
12
- }
13
- function asArray(value) {
14
- return Array.isArray(value) ? value : [];
15
- }
16
11
  function compact(value) {
17
12
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null));
18
13
  }
@@ -1,3 +1,4 @@
1
+ import { decodeBase64 } from "../../bytes.js";
1
2
  // Cloudflare's pinned REST contracts, confinement, framing and failure semantics.
2
3
  import { responseMedia } from "../_shared/rest/response-media.js";
3
4
  import { EXCLUSIVE_PATH_IDS } from "./lib/path-bounds.js";
@@ -396,8 +397,7 @@ export class RawUpload {
396
397
  }
397
398
  export function bytesFromBase64(value, field) {
398
399
  try {
399
- const binary = atob(value);
400
- return Uint8Array.from(binary, (character) => character.charCodeAt(0));
400
+ return decodeBase64(value);
401
401
  }
402
402
  catch {
403
403
  return invalid(`${field} must be valid base64.`);
@@ -433,7 +433,7 @@ class CloudflareIndex extends OperationIndex {
433
433
  for (const name of EXCLUSIVE_PATH_IDS[`${match.op.method} ${match.op.path}`] ?? []) {
434
434
  if (Number(decodeURIComponent(match.params[name])) < 1) {
435
435
  throw new ConnectorCallError("invalid_args", `Not sent: Cloudflare path parameter ${name} must be at least 1. Read cloudflare_api_details for the pinned contract.`, {
436
- validation: { issues: [{ path: `/path/${name}`, code: "minimum", expected: "at least 1" }] },
436
+ validation: { issues: [{ path: `/path/${name}`, expected: "at least 1" }] },
437
437
  });
438
438
  }
439
439
  }
@@ -528,6 +528,13 @@ export const safety = () => {
528
528
  }
529
529
  : SAFETY.redact(body, operation.method, canonical(operation)),
530
530
  withholdsErrors: (operation) => operation.method === "POST" && operation.template === "/graphql" ? false : reviewed.withholdsErrors(operation),
531
+ // The table applies to the envelope and again to its result, as redact does.
532
+ withholds: (operation) => {
533
+ const hint = reviewed.withholds(operation);
534
+ return (path) => hint(path) ||
535
+ (path[0] === "result" && hint(path.slice(1))) ||
536
+ (path[0] === "result[]" && hint(["[]", ...path.slice(1)]));
537
+ },
531
538
  },
532
539
  sensitiveFields: { names: [], choice: "drop" },
533
540
  };
@@ -1,3 +1,4 @@
1
+ import { readOnlyNotice } from "../_shared/guide.js";
1
2
  import { skill } from "./skill.generated.js";
2
3
  import { keys, optionsOf, variants } from "../../config-schema.js";
3
4
  import { CALL_ADMISSION } from "../../provider.js";
@@ -90,7 +91,7 @@ function usageGuide(options) {
90
91
  : ["No additional deployment pin: the credential's vendor permissions define its reachable estate."]),
91
92
  ...(accountId ? [`Default account id: ${accountId}.`] : []),
92
93
  ...(zoneId ? [`Default zone id: ${zoneId}.`] : []),
93
- ...(options.readOnly ? ["This connection is read-only: every write is refused."] : []),
94
+ ...readOnlyNotice(options.readOnly),
94
95
  ],
95
96
  required: true,
96
97
  };
@@ -1,4 +1,4 @@
1
- // Cloudflare's reviewed value-safety table (#801), on the shared mechanism in
1
+ // Cloudflare's reviewed value-safety table, on the shared mechanism in
2
2
  // `../_shared/rest/value-safety.ts`: every operation the detector flags in
3
3
  // the pinned spec (`value-safety.candidates.json`) has one verdict here, and
4
4
  // `value-safety.node.test.ts` runs the shared harness over it.
@@ -16,7 +16,7 @@
16
16
  // Paths are dot paths into the data a tool returns (Cloudflare's `result`,
17
17
  // or the whole body when it is not an envelope); lists are traversed
18
18
  // implicitly and `*` matches every key of an object.
19
- // Embedded URL verdict (#812): reviewed paths + global signing parameters.
19
+ // Embedded URL verdict: reviewed paths + global signing parameters.
20
20
  // Evidence: digest-verified schema revision 843eae2fbf4fe774ef9c91021dd34017ac9c970a:
21
21
  // builds_BuildLogsResponse.lines (timestamp/log-message tuples),
22
22
  // pages_deployment_log.data.line and workers-observability_query_results
@@ -869,7 +869,7 @@ const OPERATIONS = {
869
869
  key,
870
870
  safe(DESCRIBED_ONLY, "geo_restrictions", "policy_restrictions", "certificates.geo_restrictions"),
871
871
  ])),
872
- // Round 3 (#801): candidates the consolidated detector surfaced (the union
872
+ // Round 3: candidates the consolidated detector surfaced (the union
873
873
  // of the two former detectors, with transfer, claim, invite, key, signing,
874
874
  // and webhook families).
875
875
  [`GET ${A}/abuse-reports/submitted/{report_id}`]: safe("An abuse report; the authorization statement is the reporter's text."),