@zackbart/connecta 0.34.1 → 0.36.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 (441) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/README.md +65 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2044 -1161
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +751 -281
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +29 -7
  53. package/dist/connectors/api-connector.js +145 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/remote-mcp.d.ts +10 -30
  58. package/dist/connectors/remote-mcp.js +955 -563
  59. package/dist/d1.js +10 -243
  60. package/dist/derived-context.js +5 -0
  61. package/dist/describe-config.d.ts +3 -1
  62. package/dist/describe-config.js +2 -0
  63. package/dist/downstream-input.js +347 -260
  64. package/dist/errors.d.ts +87 -6
  65. package/dist/errors.js +114 -4
  66. package/dist/execute.js +87 -50
  67. package/dist/executor-admission.js +3 -0
  68. package/dist/executors/quickjs.js +163 -112
  69. package/dist/guarded-transport.js +50 -0
  70. package/dist/guest-runtime.js +1 -1
  71. package/dist/http/client.js +439 -0
  72. package/dist/http/errors.js +558 -0
  73. package/dist/http/fields.js +49 -0
  74. package/dist/http/hatch-paging.js +318 -0
  75. package/dist/http/hatch.js +1279 -0
  76. package/dist/http/json-schema.js +267 -0
  77. package/dist/http/node-transport.js +241 -0
  78. package/dist/http/oauth.js +162 -0
  79. package/dist/http/paging.js +227 -0
  80. package/dist/http/provider.js +221 -0
  81. package/dist/http/safety.js +82 -0
  82. package/dist/http/services.js +79 -0
  83. package/dist/http/tools.js +379 -0
  84. package/dist/http/vendor-index.js +138 -0
  85. package/dist/http/vendor.js +448 -0
  86. package/dist/http/write-request.js +66 -0
  87. package/dist/http-policy.d.ts +11 -0
  88. package/dist/http-policy.js +45 -0
  89. package/dist/inbound-credential.js +1 -1
  90. package/dist/index.d.ts +2 -2
  91. package/dist/index.js +4 -0
  92. package/dist/invocation-auth.js +30 -19
  93. package/dist/invocation.js +218 -85
  94. package/dist/mcp-result.js +0 -4
  95. package/dist/membership.d.ts +4 -0
  96. package/dist/meta-output.js +1 -1
  97. package/dist/meta-tools.js +531 -406
  98. package/dist/native-recovery.js +52 -0
  99. package/dist/native-tools.js +382 -0
  100. package/dist/node.d.ts +4 -3
  101. package/dist/node.js +45 -8
  102. package/dist/oauth-handoff.js +177 -104
  103. package/dist/oauth-partition.js +6 -20
  104. package/dist/oauth-sealing.js +6 -7
  105. package/dist/operation-vocabulary.js +101 -0
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +17 -4
  108. package/dist/operator-ui/contract.d.ts +8 -3
  109. package/dist/operator-ui/generated.js +4 -4
  110. package/dist/operator-ui/model.d.ts +2 -3
  111. package/dist/program-writes.js +47 -26
  112. package/dist/provider-call-facts.d.ts +115 -0
  113. package/dist/provider-call-facts.js +182 -0
  114. package/dist/provider-call.d.ts +112 -0
  115. package/dist/provider-call.js +271 -0
  116. package/dist/provider.d.ts +65 -31
  117. package/dist/provider.js +116 -39
  118. package/dist/providers/_shared/google/hatch.js +91 -0
  119. package/dist/providers/_shared/google/kit.js +427 -0
  120. package/dist/providers/_shared/google/result-size.js +15 -2
  121. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  122. package/dist/providers/_shared/google/service-account.js +8 -4
  123. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  124. package/dist/providers/_shared/google/workspace.js +6 -425
  125. package/dist/providers/_shared/rest/compact-index.js +86 -0
  126. package/dist/providers/_shared/rest/kit.js +146 -0
  127. package/dist/providers/_shared/rest/operation-index.js +230 -14
  128. package/dist/providers/_shared/rest/response-media.js +11 -0
  129. package/dist/providers/_shared/rest/value-safety.js +353 -195
  130. package/dist/providers/_shared/sensitive-fields.js +27 -0
  131. package/dist/providers/breeze/api.js +584 -0
  132. package/dist/providers/breeze/index.d.ts +1 -3
  133. package/dist/providers/breeze/index.js +67 -1420
  134. package/dist/providers/breeze/openapi.generated.js +60 -0
  135. package/dist/providers/breeze/skill.generated.js +2 -6
  136. package/dist/providers/breeze/tools.js +696 -0
  137. package/dist/providers/ccb/api.js +496 -0
  138. package/dist/providers/ccb/index.d.ts +15 -37
  139. package/dist/providers/ccb/index.js +77 -1702
  140. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  141. package/dist/providers/ccb/lib/constants.js +74 -0
  142. package/dist/providers/ccb/lib/guide.js +9 -0
  143. package/dist/providers/ccb/lib/verdicts.js +387 -0
  144. package/dist/providers/ccb/openapi.generated.js +397 -0
  145. package/dist/providers/ccb/safety.js +295 -0
  146. package/dist/providers/ccb/skill.generated.js +2 -6
  147. package/dist/providers/ccb/tools.js +853 -0
  148. package/dist/providers/cloudflare/api.js +1066 -0
  149. package/dist/providers/cloudflare/index.d.ts +5 -22
  150. package/dist/providers/cloudflare/index.js +79 -197
  151. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  152. package/dist/providers/cloudflare/lib/config.js +2 -0
  153. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  154. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  155. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  156. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  157. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  158. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  159. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  160. package/dist/providers/cloudflare/skill.generated.js +2 -10
  161. package/dist/providers/cloudflare/tools.js +360 -0
  162. package/dist/providers/docs/api.js +51 -0
  163. package/dist/providers/docs/index.d.ts +1 -12
  164. package/dist/providers/docs/index.js +53 -1165
  165. package/dist/providers/docs/lib/constants.d.ts +18 -0
  166. package/dist/providers/docs/lib/constants.js +18 -0
  167. package/dist/providers/docs/lib/discovery.js +26 -0
  168. package/dist/providers/docs/lib/requests.js +48 -0
  169. package/dist/providers/docs/lib/schemas.js +168 -0
  170. package/dist/providers/docs/openapi.generated.js +16 -0
  171. package/dist/providers/docs/safety.js +54 -0
  172. package/dist/providers/docs/skill.generated.js +2 -6
  173. package/dist/providers/docs/tools.js +801 -0
  174. package/dist/providers/drive/api.js +177 -0
  175. package/dist/providers/drive/index.d.ts +1 -12
  176. package/dist/providers/drive/index.js +38 -1448
  177. package/dist/providers/drive/lib/constants.d.ts +6 -0
  178. package/dist/providers/drive/lib/constants.js +6 -0
  179. package/dist/providers/drive/lib/contracts.js +532 -0
  180. package/dist/providers/drive/lib/hatch.js +197 -0
  181. package/dist/providers/drive/openapi.generated.js +77 -0
  182. package/dist/providers/drive/safety.js +90 -0
  183. package/dist/providers/drive/skill.generated.js +2 -6
  184. package/dist/providers/drive/tools.js +1290 -0
  185. package/dist/providers/forms/api.js +71 -0
  186. package/dist/providers/forms/index.d.ts +1 -12
  187. package/dist/providers/forms/index.js +49 -1145
  188. package/dist/providers/forms/lib/constants.d.ts +14 -0
  189. package/dist/providers/forms/lib/constants.js +19 -0
  190. package/dist/providers/forms/lib/hatch.js +72 -0
  191. package/dist/providers/forms/openapi.generated.js +23 -0
  192. package/dist/providers/forms/safety.js +34 -0
  193. package/dist/providers/forms/skill.generated.js +2 -8
  194. package/dist/providers/forms/tools.js +1023 -0
  195. package/dist/providers/github/api.js +150 -0
  196. package/dist/providers/github/index.d.ts +4 -5
  197. package/dist/providers/github/index.js +30 -87
  198. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  199. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  200. package/dist/providers/github/lib/discovery.js +132 -0
  201. package/dist/providers/github/lib/operations.js +58 -0
  202. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  203. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  204. package/dist/providers/github/openapi.generated.js +1216 -0
  205. package/dist/providers/github/safety.js +216 -0
  206. package/dist/providers/github/skill.generated.js +2 -5
  207. package/dist/providers/github/tools.js +506 -0
  208. package/dist/providers/gmail/api.js +114 -0
  209. package/dist/providers/gmail/index.d.ts +1 -12
  210. package/dist/providers/gmail/index.js +73 -1621
  211. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  212. package/dist/providers/gmail/lib/constants.js +16 -0
  213. package/dist/providers/gmail/lib/index.js +173 -0
  214. package/dist/providers/gmail/lib/schemas.js +176 -0
  215. package/dist/providers/gmail/lib/threading.js +120 -0
  216. package/dist/providers/gmail/openapi.generated.js +92 -0
  217. package/dist/providers/gmail/safety.js +21 -0
  218. package/dist/providers/gmail/skill.generated.js +2 -6
  219. package/dist/providers/gmail/tools.js +1435 -0
  220. package/dist/providers/overflow/api.js +1008 -0
  221. package/dist/providers/overflow/index.d.ts +1 -17
  222. package/dist/providers/overflow/index.js +60 -1488
  223. package/dist/providers/overflow/skill.generated.js +2 -5
  224. package/dist/providers/overflow/tools.js +873 -0
  225. package/dist/providers/planning-center/api.js +467 -0
  226. package/dist/providers/planning-center/index.d.ts +4 -35
  227. package/dist/providers/planning-center/index.js +69 -2445
  228. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  229. package/dist/providers/planning-center/lib/constants.js +83 -0
  230. package/dist/providers/planning-center/lib/discovery.js +40 -0
  231. package/dist/providers/planning-center/lib/guide.js +39 -0
  232. package/dist/providers/planning-center/lib/json-api.js +83 -0
  233. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  234. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  235. package/dist/providers/planning-center/safety.js +188 -0
  236. package/dist/providers/planning-center/skill.generated.js +2 -27
  237. package/dist/providers/planning-center/tools.js +1010 -0
  238. package/dist/providers/quickbooks/api.js +299 -0
  239. package/dist/providers/quickbooks/index.d.ts +13 -0
  240. package/dist/providers/quickbooks/index.js +93 -0
  241. package/dist/providers/quickbooks/lib/query.js +118 -0
  242. package/dist/providers/quickbooks/lib/writes.js +168 -0
  243. package/dist/providers/quickbooks/safety.js +222 -0
  244. package/dist/providers/quickbooks/skill.generated.js +5 -0
  245. package/dist/providers/quickbooks/tools.js +117 -0
  246. package/dist/providers/sheets/api.js +133 -0
  247. package/dist/providers/sheets/index.d.ts +4 -16
  248. package/dist/providers/sheets/index.js +61 -1172
  249. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  250. package/dist/providers/sheets/lib/constants.js +14 -0
  251. package/dist/providers/sheets/lib/hatch.js +163 -0
  252. package/dist/providers/sheets/lib/requests.js +105 -0
  253. package/dist/providers/sheets/lib/schemas.js +138 -0
  254. package/dist/providers/sheets/openapi.generated.js +30 -0
  255. package/dist/providers/sheets/safety.js +27 -0
  256. package/dist/providers/sheets/skill.generated.js +2 -7
  257. package/dist/providers/sheets/tools.js +733 -0
  258. package/dist/providers/slides/api.js +147 -0
  259. package/dist/providers/slides/index.d.ts +2 -10
  260. package/dist/providers/slides/index.js +48 -2555
  261. package/dist/providers/slides/lib/constants.d.ts +11 -0
  262. package/dist/providers/slides/lib/constants.js +11 -0
  263. package/dist/providers/slides/lib/requests.js +60 -0
  264. package/dist/providers/slides/lib/schemas.js +565 -0
  265. package/dist/providers/slides/openapi.generated.js +18 -0
  266. package/dist/providers/slides/skill.generated.js +2 -6
  267. package/dist/providers/slides/tools.js +1968 -0
  268. package/dist/providers/stripe/api.js +401 -0
  269. package/dist/providers/stripe/index.d.ts +5 -27
  270. package/dist/providers/stripe/index.js +74 -210
  271. package/dist/providers/stripe/lib/config.d.ts +4 -0
  272. package/dist/providers/stripe/lib/config.js +2 -0
  273. package/dist/providers/stripe/openapi.generated.js +4 -3
  274. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  275. package/dist/providers/stripe/skill.generated.js +2 -11
  276. package/dist/providers/stripe/tools.js +77 -0
  277. package/dist/providers/tithely/api.js +531 -0
  278. package/dist/providers/tithely/index.d.ts +17 -0
  279. package/dist/providers/tithely/index.js +86 -965
  280. package/dist/providers/tithely/skill.generated.js +2 -5
  281. package/dist/providers/tithely/tools.js +426 -0
  282. package/dist/providers/vercel/api.js +551 -0
  283. package/dist/providers/vercel/index.d.ts +9 -25
  284. package/dist/providers/vercel/index.js +65 -260
  285. package/dist/providers/vercel/lib/config.d.ts +2 -0
  286. package/dist/providers/vercel/lib/config.js +2 -0
  287. package/dist/providers/vercel/lib/index.js +117 -0
  288. package/dist/providers/vercel/lib/logs.js +100 -0
  289. package/dist/providers/vercel/lib/pin.js +270 -0
  290. package/dist/providers/vercel/openapi.generated.js +10 -5
  291. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  292. package/dist/providers/vercel/skill.generated.js +2 -7
  293. package/dist/providers/vercel/tools.js +642 -0
  294. package/dist/redaction-identity.js +47 -0
  295. package/dist/redaction-marker.js +125 -0
  296. package/dist/registry.d.ts +21 -15
  297. package/dist/registry.js +409 -376
  298. package/dist/routes/access-tokens.js +1 -1
  299. package/dist/routes/actor-labels.js +1 -1
  300. package/dist/routes/connect.js +1 -1
  301. package/dist/routes/dispatch.js +103 -0
  302. package/dist/routes/mcp.js +170 -79
  303. package/dist/routes/oauth.js +6 -1
  304. package/dist/routes/operator.js +2 -2
  305. package/dist/routes/shared.d.ts +5 -4
  306. package/dist/routes/shared.js +244 -177
  307. package/dist/routes/skills.js +10 -8
  308. package/dist/routes/ui-config.js +27 -3
  309. package/dist/routes/ui-data.js +21 -20
  310. package/dist/routes/ui.js +1 -1
  311. package/dist/runtime/call-admission.js +0 -26
  312. package/dist/runtime/call-entry.js +30 -0
  313. package/dist/runtime/call.js +552 -0
  314. package/dist/runtime/connector-scope.js +7 -0
  315. package/dist/runtime/credential-probe.js +34 -0
  316. package/dist/runtime/executor.js +40 -0
  317. package/dist/runtime/native-storage.js +63 -0
  318. package/dist/runtime/registry.js +27 -0
  319. package/dist/runtime/request-scope.js +72 -0
  320. package/dist/runtime/run.js +15 -7
  321. package/dist/runtime/shared-read.js +34 -5
  322. package/dist/runtime/storage.js +34 -21
  323. package/dist/sent-secrets.d.ts +194 -37
  324. package/dist/sent-secrets.js +1743 -340
  325. package/dist/server.js +5 -56
  326. package/dist/skills.js +322 -223
  327. package/dist/sqlite.js +15 -14
  328. package/dist/storage/d1-copy.js +255 -0
  329. package/dist/storage/d1-driver.js +15 -0
  330. package/dist/storage/keys.js +28 -0
  331. package/dist/storage/sql-driver.js +88 -0
  332. package/dist/storage/sql-native.js +544 -0
  333. package/dist/storage/sql-schema.d.ts +6 -0
  334. package/dist/storage/sql-schema.js +15 -0
  335. package/dist/storage/sql.d.ts +14 -20
  336. package/dist/storage/sql.js +29 -535
  337. package/dist/timeout.js +0 -26
  338. package/dist/tool-safety.js +20 -10
  339. package/dist/types.d.ts +9 -109
  340. package/dist/ui.d.ts +1 -1
  341. package/dist/ui.js +0 -1
  342. package/dist/upward.js +3 -0
  343. package/dist/usage-guide.js +3 -1
  344. package/dist/validate.d.ts +4 -2
  345. package/dist/validate.js +86 -9
  346. package/dist/version.d.ts +1 -1
  347. package/dist/version.js +1 -1
  348. package/dist/worker.js +312 -292
  349. package/dist/workspace-subject.js +5 -1
  350. package/dist/write-outcome.js +454 -34
  351. package/documentation/README.md +28 -7
  352. package/documentation/architecture.md +2409 -255
  353. package/documentation/auth.md +867 -114
  354. package/documentation/code-mode.md +49 -16
  355. package/documentation/deploying.md +164 -20
  356. package/documentation/hosted-mcp-migration.md +460 -0
  357. package/documentation/integrating.md +129 -144
  358. package/documentation/meta-tools.md +409 -32
  359. package/documentation/operating.md +27 -3
  360. package/documentation/operator-tests.md +3 -0
  361. package/documentation/operator-ui.md +84 -48
  362. package/documentation/provider-auth-migration.md +16 -63
  363. package/documentation/provider-migration-0.29.md +10 -5
  364. package/documentation/provider-migration-0.35.md +975 -0
  365. package/documentation/providers.md +1347 -0
  366. package/examples/worker/AGENTS.md +23 -9
  367. package/examples/worker/README.md +254 -135
  368. package/examples/worker/alchemy.run.ts +116 -0
  369. package/examples/worker/gitignore +10 -0
  370. package/examples/worker/src/connecta.config.ts +23 -21
  371. package/examples/worker/wrangler.jsonc +14 -5
  372. package/guides/connecting-agents.md +157 -0
  373. package/guides/how-it-works.md +156 -0
  374. package/guides/safety-and-trust.md +153 -0
  375. package/package.json +30 -50
  376. package/templates/node/.env.example +3 -0
  377. package/templates/node/README.md +22 -2
  378. package/templates/node/docker-compose.yml +1 -0
  379. package/templates/node/package.json +3 -2
  380. package/templates/node/src/connecta.config.ts +8 -0
  381. package/dist/catalog-drift.d.ts +0 -138
  382. package/dist/catalog-drift.js +0 -578
  383. package/dist/operator-ui/agents.d.ts +0 -74
  384. package/dist/operator-ui/agents.js +0 -104
  385. package/dist/operator-ui/config-summary.js +0 -565
  386. package/dist/operator-ui/connection-map.js +0 -376
  387. package/dist/operator-ui/endpoints.js +0 -221
  388. package/dist/operator-ui/fix-prompts.d.ts +0 -18
  389. package/dist/operator-ui/fix-prompts.js +0 -87
  390. package/dist/operator-ui/health.js +0 -52
  391. package/dist/operator-ui/member-page.js +0 -97
  392. package/dist/operator-ui/overview.js +0 -123
  393. package/dist/operator-ui/setup-commands.js +0 -51
  394. package/dist/operator-ui/view.d.ts +0 -401
  395. package/dist/operator-ui/view.js +0 -705
  396. package/dist/providers/_shared/request-token.js +0 -83
  397. package/dist/providers/_shared/rest/dispatch.js +0 -41
  398. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  399. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  400. package/dist/providers/_shared/rest/tools.js +0 -569
  401. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  402. package/dist/providers/basecamp/index.d.ts +0 -45
  403. package/dist/providers/basecamp/index.js +0 -997
  404. package/dist/providers/basecamp/skill.generated.js +0 -9
  405. package/dist/providers/cloudflare/rest.d.ts +0 -37
  406. package/dist/providers/cloudflare/rest.js +0 -1097
  407. package/dist/providers/github/hosted.js +0 -218
  408. package/dist/providers/github/rest.js +0 -321
  409. package/dist/providers/infisical/index.d.ts +0 -7
  410. package/dist/providers/infisical/index.js +0 -713
  411. package/dist/providers/infisical/skill.generated.js +0 -8
  412. package/dist/providers/intercom/index.d.ts +0 -24
  413. package/dist/providers/intercom/index.js +0 -121
  414. package/dist/providers/intercom/skill.generated.js +0 -8
  415. package/dist/providers/linear/index.d.ts +0 -55
  416. package/dist/providers/linear/index.js +0 -537
  417. package/dist/providers/linear/skill.generated.js +0 -8
  418. package/dist/providers/mixpanel/index.d.ts +0 -59
  419. package/dist/providers/mixpanel/index.js +0 -472
  420. package/dist/providers/mixpanel/operations.js +0 -20
  421. package/dist/providers/mixpanel/rest.js +0 -603
  422. package/dist/providers/mixpanel/skill.generated.js +0 -10
  423. package/dist/providers/mixpanel/value-safety.js +0 -16
  424. package/dist/providers/notion/index.d.ts +0 -67
  425. package/dist/providers/notion/index.js +0 -368
  426. package/dist/providers/notion/openapi.generated.js +0 -75
  427. package/dist/providers/notion/rest.d.ts +0 -24
  428. package/dist/providers/notion/rest.js +0 -1226
  429. package/dist/providers/notion/skill.generated.js +0 -11
  430. package/dist/providers/notion/value-safety.js +0 -56
  431. package/dist/providers/revenuecat/index.d.ts +0 -51
  432. package/dist/providers/revenuecat/index.js +0 -697
  433. package/dist/providers/revenuecat/skill.generated.js +0 -11
  434. package/dist/providers/stripe/rest.d.ts +0 -14
  435. package/dist/providers/stripe/rest.js +0 -402
  436. package/dist/providers/vercel/rest.d.ts +0 -20
  437. package/dist/providers/vercel/rest.js +0 -1020
  438. package/dist/providers/wispr-flow/index.d.ts +0 -14
  439. package/dist/providers/wispr-flow/index.js +0 -117
  440. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  441. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,1646 +1,13 @@
1
+ import { CCB_ENVIRONMENTS, CCB_MEDIA_TYPE } from "./lib/constants.js";
1
2
  import { skill } from "./skill.generated.js";
2
- /**
3
- * Church Community Builder — sold today as Pushpay ChMS — through its v2 JSON
4
- * REST API, hand-written over `api()`'s downstream OAuth grant.
5
- *
6
- * Why `ccb()` and not `pushpayChms()`: every wire contract this file depends
7
- * on says CCB — the `api.ccbchurch.com` host, the
8
- * `application/vnd.ccbchurch.v2+json` media type, the OAuth hosts — and so do
9
- * the churches that use it. "Pushpay" also names a separate giving platform
10
- * with its own API, so a `pushpay()` export would collide with the provider
11
- * that one day wraps it. The title says both names, so neither reader guesses.
12
- *
13
- * Considered and rejected: Pushpay hosts ReadMe "docs MCP" servers
14
- * (https://pushpay-chmsv2.readme.io/mcp). They search the documentation and
15
- * proxy raw HAR-shaped requests; they own no OAuth grant, project nothing,
16
- * classify nothing, and would hand an agent a generic request tool whose
17
- * safety class it chooses per call. This connector is the opposite trade:
18
- * named reads with projections, and raw access split by safety.
19
- *
20
- * Out of scope on purpose: transaction and batch listing exists only in the
21
- * legacy v1 XML API (`<church>.ccbchurch.com/api.php`, per-church API users,
22
- * a daily cap). v2 exposes giving only as per-person and per-family metrics,
23
- * pledges, scheduled gifts, and financial settings, and that is all this
24
- * connector reaches.
25
- *
26
- * Drift is handled the Vercel way: `src/providers/ccb/drift.json` records
27
- * every endpoint a named tool touches, and
28
- * `pnpm run providers:check --provider ccb` compares those rows with the
29
- * published, credential-free OpenAPI document at
30
- * https://docs.pushpay.io/chms-v2/openapi/chms-api-specs.json.
31
- */
32
- import { api } from "../../connectors/api.js";
33
- import { defined } from "../../connectors/api-connector.js";
34
- import { guardedFetch, oauthBearer, retryAfterMs, } from "../../connectors/guarded-fetch.js";
35
- import { ConnectorCallError } from "../../errors.js";
36
3
  import { keys, optionsOf } from "../../config-schema.js";
37
4
  import { CALL_ADMISSION } from "../../connectors/option-shapes.js";
38
- import { asProviderFactory } from "../../provider.js";
39
- /** CCB's v2 media type. The API and its token endpoint both require it. */
40
- export const CCB_MEDIA_TYPE = "application/vnd.ccbchurch.v2+json";
41
- /**
42
- * The two published environments. Credentials do not cross: sandbox clients
43
- * work only against the sandbox hosts and production clients only against
44
- * production, so the environment is part of the client, not a preference.
45
- */
46
- export const CCB_ENVIRONMENTS = {
47
- production: {
48
- apiOrigin: "https://api.ccbchurch.com",
49
- authorizationEndpoint: "https://oauth.ccbchurch.com/oauth/authorize",
50
- },
51
- sandbox: {
52
- apiOrigin: "https://api-beta.ccbchurch.com",
53
- authorizationEndpoint: "https://beta-oauth.ccbchurch.com/oauth/authorize",
54
- },
55
- };
56
- /**
57
- * The read scopes the named tools and the search hatch need, from the
58
- * `security` requirements of the operations they call (spec revision recorded
59
- * in the drift manifest). `read:campuses` and `read:church` are there so the
60
- * ids every projection carries — campus, membership type — can be resolved
61
- * through `ccb_api_get`. Request these on Pushpay's API access form.
62
- */
63
- export const CCB_READ_SCOPES = [
64
- "read:advanced_searches",
65
- "read:campuses",
66
- "read:church",
67
- "read:event_attendance",
68
- "read:events",
69
- "read:forms",
70
- "read:group_members",
71
- "read:groups",
72
- "read:individual_groups",
73
- "read:individual_notes",
74
- "read:individual_pledges",
75
- "read:individuals",
76
- "read:individuals_metrics",
77
- "read:process_queues",
78
- "read:queue_individuals",
79
- "read:scheduling",
80
- ];
81
- /**
82
- * What `access: "read-write"` adds: the writes of the everyday staff jobs —
83
- * profiles, groups, events and attendance, form responses, notes, process
84
- * queues, scheduling. No `delete:` scope, nothing financial, no background
85
- * checks, and no privacy settings: those are deliberate, so they are only
86
- * ever requested through an explicit `scopes` list.
87
- */
88
- export const CCB_WRITE_SCOPES = [
89
- "write:event_attendance",
90
- "write:events",
91
- "write:form_responses",
92
- "write:group_members",
93
- "write:groups",
94
- "write:individual_groups",
95
- "write:individual_notes",
96
- "write:individuals",
97
- "write:notes",
98
- "write:queue_individuals",
99
- "write:scheduling",
100
- ];
101
- const SCOPE_SHAPE = /^(read|write|delete):[a-z_]+$/;
102
- const PER_PAGE_VALUES = [25, 50, 75, 100];
103
- /** CCB's own minimum, which is also the cheapest honest first read. */
104
- const DEFAULT_PER_PAGE = 25;
105
- /**
106
- * A ceiling on absurdity rather than a budget: at 100 rows per page the
107
- * fattest documented list (individuals with addresses) stays far below it.
108
- */
109
- const CCB_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
110
- /**
111
- * CCB meters the v2 API per endpoint and per API client: one request a second
112
- * sustained with a burst of 60 (docs.pushpay.io/chms-v2/docs/general). Three
113
- * endpoints are stricter and allow no burst — `/individuals/{id}` at one a
114
- * second, `/scheduling/categories/{id}/schedules` at one per two seconds, and
115
- * `/search/individuals/results` at one per five.
116
- *
117
- * What ships is one rolling window of 60 calls per 60 seconds, partitioned by
118
- * endpoint (method plus path template), which is the documented default
119
- * transcribed as faithfully as one rule can: it never admits more than the
120
- * token bucket would in any minute, because a bucket of 60 refilling at one a
121
- * second can pass up to 120 in a minute and the window passes 60. Partitioning
122
- * matters — a single connector-wide window would throttle a healthy program
123
- * that touches ten endpoints to a tenth of what CCB allows.
124
- *
125
- * What it cannot express is the three no-burst overrides: this release
126
- * enforces one rule, and a rule has one budget for every partition. Those are
127
- * left to CCB's own 429, which carries `retry-after` and maps to a
128
- * `rate_limited` failure with that wait; the guide says to space those calls.
129
- * No `maxConcurrency` is declared: per-endpoint concurrency would not keep a
130
- * fast loop under one a second, and the window already bounds the burst.
131
- *
132
- * Per runtime, and per API client only as far as one runtime is the whole
133
- * client: other deployments, isolates, or integrations using the same client
134
- * spend from CCB's bucket without spending from this window.
135
- */
136
- const CCB_ADMISSION = {
137
- rules: [
138
- {
139
- budget: { kind: "rolling-window", maxCalls: 60, windowMs: 60_000 },
140
- partitionKey: ({ toolName, args }) => endpointPartition(toolName, args),
141
- },
142
- ],
143
- };
144
- function asRecord(value) {
145
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
146
- }
147
- function asArray(value) {
148
- return Array.isArray(value) ? value : [];
149
- }
150
- function compact(value) {
151
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null));
152
- }
153
- /** An object that is empty after compaction reads as absent. */
154
- function present(value) {
155
- return Object.keys(value).length > 0 ? value : undefined;
156
- }
157
- // ---------------------------------------------------------------------------
158
- // Admission partitions
159
- // ---------------------------------------------------------------------------
160
- /** The method and path template each named tool calls. */
161
- const NAMED_ENDPOINTS = {
162
- get_me: "GET /me",
163
- list_individuals: "GET /individuals",
164
- get_individual: "GET /individuals/{id}",
165
- list_families: "GET /families",
166
- get_family: "GET /families/{id}",
167
- list_groups: "GET /groups",
168
- get_group: "GET /groups/{id}",
169
- list_group_members: "GET /groups/{id}/members",
170
- list_individual_groups: "GET /individuals/{id}/groups",
171
- list_events: "GET /events",
172
- get_event: "GET /events/{id}",
173
- get_event_attendance: "GET /events/{id}/attendance/{occurrence}",
174
- list_event_attendees: "GET /events/{id}/attendance/{occurrence}/attendees",
175
- list_forms: "GET /forms",
176
- list_form_responses: "GET /forms/{id}/responses",
177
- list_processes: "GET /processes",
178
- list_queues: "GET /queues",
179
- list_queue_individuals: "GET /queues_individuals",
180
- list_scheduling_categories: "GET /scheduling/categories",
181
- list_schedules: "GET /scheduling/categories/{id}/schedules",
182
- list_individual_assignments: "GET /individuals/{id}/assignments",
183
- list_individual_notes: "GET /individuals/{id}/notes",
184
- get_giving_metrics: (args) => args["familyId"] !== undefined ? "GET /families/{id}/metrics/giving" : "GET /individuals/{id}/metrics/giving",
185
- list_individual_pledges: "GET /individuals/{id}/pledges",
186
- ccb_api_search: (args) => `POST /search/${typeof args["domain"] === "string" ? args["domain"] : "?"}/results`,
187
- };
188
- /** `/individuals/123/notes` → `/individuals/{id}/notes`; the key never holds an id. */
189
- function pathTemplate(path) {
190
- return path
191
- .split("/")
192
- .map((segment) => (/^\d+$/.test(segment) ? "{id}" : segment))
193
- .join("/");
194
- }
195
- const MAX_PARTITION_KEY_CHARS = 120;
196
- /**
197
- * The CCB endpoint one call will spend from, as a bounded, id-free key. Runs
198
- * before argument validation, so it reads arguments defensively and never
199
- * throws: a malformed hatch path falls into one shared partition and is then
200
- * refused by the handler.
201
- */
202
- function endpointPartition(toolName, raw) {
203
- const args = asRecord(raw);
204
- const named = NAMED_ENDPOINTS[toolName];
205
- let key;
206
- if (typeof named === "string")
207
- key = named;
208
- else if (named)
209
- key = named(args);
210
- else {
211
- const method = toolName === "ccb_api_get" ? "GET" : typeof args["method"] === "string" ? args["method"] : "?";
212
- const path = typeof args["path"] === "string" ? args["path"] : "";
213
- key = `${method} ${pathTemplate(path)}`;
214
- }
215
- // ASCII-only after templating in every realistic case; the slice keeps an
216
- // absurd path inside the admission controller's 128-byte key limit anyway.
217
- return [...key]
218
- .filter((c) => c.charCodeAt(0) < 128)
219
- .join("")
220
- .slice(0, MAX_PARTITION_KEY_CHARS);
221
- }
222
- // ---------------------------------------------------------------------------
223
- // Transport and typed failures
224
- // ---------------------------------------------------------------------------
225
- function errorDetail(payload, status) {
226
- const root = asRecord(payload);
227
- const parts = [];
228
- for (const key of ["message", "error_description", "error"]) {
229
- const value = root[key];
230
- if (typeof value === "string" && value.trim()) {
231
- parts.push(value.trim());
232
- break;
233
- }
234
- }
235
- for (const item of asArray(root["errors"]).slice(0, 5)) {
236
- if (typeof item === "string" && item.trim())
237
- parts.push(item.trim());
238
- else {
239
- const message = asRecord(item)["message"];
240
- if (typeof message === "string" && message.trim())
241
- parts.push(message.trim());
242
- }
243
- }
244
- const detail = parts.join("; ").slice(0, 500);
245
- return detail ? `CCB HTTP ${status}: ${detail}` : `CCB returned HTTP ${status}.`;
246
- }
247
- /** The wait CCB states: `retry-after`, else the `x-ratelimit-reset` epoch. */
248
- function rateLimitWaitMs(headers, payload) {
249
- const stated = retryAfterMs(headers);
250
- if (stated !== undefined)
251
- return stated;
252
- const reset = Number(headers.get("x-ratelimit-reset"));
253
- if (headers.get("x-ratelimit-reset") && Number.isFinite(reset)) {
254
- return Math.max(0, Math.trunc(reset * 1_000 - Date.now()));
255
- }
256
- const body = Number(asRecord(payload)["retry_after"]);
257
- return Number.isFinite(body) && body >= 0 ? Math.trunc(body * 1_000) : undefined;
258
- }
259
- /**
260
- * Map a CCB failure by what the caller does next.
261
- *
262
- * A 401 rarely reaches here: `ctx.oauth.fetch` answers one with a refresh and
263
- * a replay and throws `auth_required` itself when that fails. A 403 is not
264
- * `auth_required`, because re-consenting with the same scopes cannot fix it:
265
- * either the client was not granted the scope (an operator widens `scopes`
266
- * and restarts authorization) or, in Identity Auth, the signed-in person
267
- * lacks the permission in CCB. CCB answers a permission gap with 403 and an
268
- * absence with 404, so 404 is a genuine `not_found`.
269
- */
270
- function ccbFailure(status, headers, payload) {
271
- const detail = errorDetail(payload, status);
272
- if (status === 429) {
273
- const wait = rateLimitWaitMs(headers, payload);
274
- return new ConnectorCallError("rate_limited", `${detail} CCB limits each endpoint per API client; wait before calling this endpoint again.`, wait === undefined ? {} : { retryAfterMs: wait });
275
- }
276
- if (status === 401) {
277
- return new ConnectorCallError("auth_required", `${detail} CCB rejected the connection's access token. Call authorize_connector for this connector, then retry.`);
278
- }
279
- if (status === 403) {
280
- return new ConnectorCallError("provider_permission_denied", `${detail} The grant lacks this endpoint's scope, or (Identity Auth) the signed-in person lacks the permission in CCB. Ask the CCB administrator to grant this endpoint's scope or the signed-in person's permission. Re-authorizing alone will not fix it.`, { retryable: false });
281
- }
282
- if (status === 404) {
283
- return new ConnectorCallError("not_found", `${detail} Re-read the id from its list tool; CCB reports a permission gap as 403, so this record does not exist.`);
284
- }
285
- if (status === 409)
286
- return new ConnectorCallError("conflict", detail);
287
- if (status === 400 || status === 412 || status === 422) {
288
- return new ConnectorCallError("invalid_args", detail);
289
- }
290
- if (status >= 500) {
291
- const wait = retryAfterMs(headers);
292
- return new ConnectorCallError("unavailable", `${detail} CCB is failing upstream.`, wait === undefined ? {} : { retryAfterMs: wait });
293
- }
294
- return new ConnectorCallError("connector_call_failed", detail, { retryable: false });
295
- }
296
- function ccbTransport(apiOrigin) {
297
- return guardedFetch({
298
- provider: "CCB",
299
- baseUrl: apiOrigin,
300
- headers: { Accept: CCB_MEDIA_TYPE },
301
- maxResponseBytes: CCB_MAX_RESPONSE_BYTES,
302
- ...oauthBearer("CCB"),
303
- });
304
- }
305
- async function callCcb(send, request, ctx) {
306
- return await send(request, ctx, async (response) => {
307
- const parsed = await response.jsonResult();
308
- if (!response.ok) {
309
- throw ccbFailure(response.status, response.headers, "value" in parsed ? parsed.value : undefined);
310
- }
311
- if (!("value" in parsed)) {
312
- throw new ConnectorCallError("connector_call_failed", "CCB returned a successful response that is not JSON.", {
313
- retryable: false,
314
- });
315
- }
316
- return { body: parsed.value, headers: response.headers };
317
- });
318
- }
319
- // ---------------------------------------------------------------------------
320
- // Pagination
321
- // ---------------------------------------------------------------------------
322
- function headerInt(headers, name) {
323
- const raw = headers.get(name);
324
- if (raw === null || raw.trim() === "")
325
- return undefined;
326
- const value = Number(raw);
327
- return Number.isInteger(value) && value >= 0 ? value : undefined;
328
- }
329
- /**
330
- * One branch for every list: `hasMore`, from `x-next-page` when CCB sends it
331
- * (empty on the last page), else `x-page` against `x-total-pages`, else — a
332
- * list that sent neither — a full page is read as "maybe more".
333
- */
334
- function pageInfo(headers, page, perPage, rows) {
335
- const current = headerInt(headers, "x-page") ?? page;
336
- const totalPages = headerInt(headers, "x-total-pages");
337
- let nextPage;
338
- if (headers.has("x-next-page"))
339
- nextPage = headerInt(headers, "x-next-page") ?? null;
340
- else if (totalPages !== undefined)
341
- nextPage = current < totalPages ? current + 1 : null;
342
- else
343
- nextPage = rows >= perPage ? current + 1 : null;
344
- return {
345
- hasMore: nextPage !== null,
346
- nextPage,
347
- total: headerInt(headers, "x-total") ?? null,
348
- };
349
- }
350
- // ---------------------------------------------------------------------------
351
- // Projections
352
- //
353
- // One mechanical rule: CCB's snake_case becomes camelCase, nested references
354
- // flatten to an id and a name, and permission flags (`actions`), images,
355
- // creator and modifier records are dropped everywhere. Sensitive fields are
356
- // dropped too — allergies, giving numbers, envelope ids, giving dates and
357
- // statement dates, prayer requests and meeting notes on attendance, payment
358
- // details on form responses. `raw: true` returns CCB's row untouched.
359
- // ---------------------------------------------------------------------------
360
- function phones(value) {
361
- const phone = asRecord(value);
362
- return present(compact({
363
- mobile: phone["mobile"] || undefined,
364
- home: phone["home"] || undefined,
365
- work: phone["work"] || undefined,
366
- }));
367
- }
368
- function address(value) {
369
- const row = asRecord(value);
370
- return present(compact({
371
- street: row["street"] || undefined,
372
- city: row["city"] || undefined,
373
- state: row["state"] || undefined,
374
- zip: row["zip"] || undefined,
375
- country: row["country_iso"] || undefined,
376
- }));
377
- }
378
- function projectPerson(value) {
379
- const row = asRecord(value);
380
- return compact({
381
- id: row["id"],
382
- name: row["name"],
383
- firstName: row["first_name"],
384
- lastName: row["last_name"],
385
- email: row["email"] || undefined,
386
- phones: phones(row["phone"]),
387
- birthday: row["birthday"],
388
- familyId: row["family_id"],
389
- familyPosition: row["family_position"],
390
- campusId: row["campus_id"],
391
- membershipTypeId: row["membership_type_id"],
392
- active: row["active"],
393
- deceased: row["deceased"],
394
- });
395
- }
396
- function projectIndividual(value) {
397
- const row = asRecord(value);
398
- const addresses = asRecord(row["addresses"]);
399
- return compact({
400
- ...projectPerson(row),
401
- gender: row["gender"],
402
- maritalStatus: row["marital_status"],
403
- homeAddress: address(addresses["home"]),
404
- mailingAddress: address(addresses["mailing"]),
405
- campusName: row["campus_name"],
406
- membershipTypeName: row["membership_type_name"],
407
- membershipDate: row["membership_date"],
408
- schoolGrade: row["school_grade"],
409
- baptized: row["baptized"],
410
- baptizedDate: row["baptized_date"],
411
- lastAttendedDate: row["last_attended_date"],
412
- limitedAccessUser: row["limited_access_user"],
413
- listed: row["listed"],
414
- created: row["created"],
415
- modified: row["modified"],
416
- customFields: present(Object.fromEntries(asArray(row["custom_fields"])
417
- .map(asRecord)
418
- .filter((field) => field["id"] !== undefined)
419
- .map((field) => [String(field["id"]), field["value"]]))),
420
- });
421
- }
422
- function projectFamily(value) {
423
- const row = asRecord(value);
424
- return compact({
425
- id: row["id"],
426
- lastName: row["last_name"],
427
- address: address(row["address"]),
428
- members: Array.isArray(row["members"]) ? row["members"].map(projectPerson) : undefined,
429
- });
430
- }
431
- function ref(value) {
432
- const row = asRecord(value);
433
- return row["id"] === undefined && row["name"] === undefined
434
- ? undefined
435
- : compact({ id: row["id"], name: row["name"] });
436
- }
437
- function projectGroup(value) {
438
- const row = asRecord(value);
439
- return compact({
440
- id: row["id"],
441
- name: row["name"],
442
- campusId: row["campus_id"],
443
- inactive: row["inactive"],
444
- groupType: asRecord(row["group_type"])["name"],
445
- interactionType: row["interaction_type"],
446
- membershipType: row["membership_type"],
447
- mainLeader: ref(row["main_leader"]),
448
- departmentId: asRecord(row["department"])["id"],
449
- description: row["description"],
450
- memberCount: row["member_count"],
451
- childcare: row["childcare"],
452
- listed: row["listed"],
453
- full: row["full"],
454
- meetDay: asRecord(row["meet_day"])["name"],
455
- meetTime: asRecord(row["meet_time"])["name"],
456
- area: asRecord(row["area"])["name"],
457
- address: address(row["address"]),
458
- });
459
- }
460
- function projectMembership(value) {
461
- const row = asRecord(value);
462
- const person = asRecord(row["individual"]);
463
- const group = asRecord(row["group"]);
464
- return compact({
465
- groupId: row["group_id"],
466
- individualId: row["individual_id"],
467
- status: row["status"],
468
- dateAdded: row["date_added"],
469
- name: person["name"],
470
- email: person["email"] || undefined,
471
- phones: phones(person["phone"]),
472
- groupName: group["name"],
473
- groupType: asRecord(group["group_type"])["name"],
474
- groupInactive: group["inactive"],
475
- });
476
- }
477
- function projectOccurrence(value) {
478
- const row = asRecord(value);
479
- const event = asRecord(row["event"]);
480
- const group = asRecord(event["group"]);
481
- return compact({
482
- eventId: row["event_id"] ?? event["id"],
483
- occurrence: row["occurrence"],
484
- start: row["start"],
485
- end: row["end"],
486
- name: event["name"],
487
- campusId: event["campus_id"],
488
- recurs: event["recurs"],
489
- groupId: group["id"] === undefined ? undefined : Number(group["id"]),
490
- groupName: group["name"],
491
- });
492
- }
493
- function projectEvent(value) {
494
- const row = asRecord(value);
495
- const attendance = asRecord(row["attendance"]);
496
- return compact({
497
- id: row["id"],
498
- name: row["name"],
499
- description: row["description"],
500
- campusId: row["campus_id"],
501
- start: row["start"],
502
- end: row["end"],
503
- recurs: row["recurs"],
504
- recurEndDate: row["recur_end_date"],
505
- approvalStatus: row["approval_status"],
506
- address: address(row["address"]),
507
- guestCounts: present(compact({ ...asRecord(row["guest_counts"]) })),
508
- group: ref(row["group"]),
509
- attendance: present(compact({
510
- status: attendance["status"],
511
- totalAttendance: attendance["total_attendance"],
512
- visitors: attendance["visitors"],
513
- })),
514
- });
515
- }
516
- function projectAttendance(value) {
517
- const row = asRecord(value);
518
- return compact({
519
- eventId: row["event_id"],
520
- occurrence: row["occurrence"],
521
- status: row["status"],
522
- totalAttendance: row["total_attendance"],
523
- visitors: row["visitors"],
524
- topic: row["topic"],
525
- });
526
- }
527
- function projectAttendee(value) {
528
- const row = asRecord(value);
529
- const person = asRecord(row["individual"]);
530
- return compact({
531
- individualId: row["individual_id"] ?? person["id"],
532
- name: person["name"],
533
- familyId: person["family_id"],
534
- occurrenceDate: row["occurrence_date"],
535
- });
536
- }
537
- function projectForm(value) {
538
- const row = asRecord(value);
539
- return compact({
540
- id: row["id"],
541
- title: row["title"],
542
- status: row["status"],
543
- campusId: row["campus_id"],
544
- start: row["start"],
545
- end: row["end"],
546
- public: row["public"],
547
- url: row["url"],
548
- });
549
- }
550
- function projectResponse(value) {
551
- const row = asRecord(value);
552
- return compact({
553
- id: row["id"],
554
- formId: row["form_id"],
555
- individualId: row["individual_id"],
556
- created: row["created"],
557
- paymentStatus: row["payment_status"],
558
- answers: asArray(row["answers"]).map((answer) => {
559
- const item = asRecord(answer);
560
- return compact({ questionId: item["question_id"], answer: item["answer"] });
561
- }),
562
- });
563
- }
564
- function projectProcess(value) {
565
- const row = asRecord(value);
566
- return compact({
567
- id: row["id"],
568
- name: row["name"],
569
- campusId: row["campus_id"],
570
- ownerId: row["owner_id"],
571
- hidden: row["hidden"],
572
- archived: row["archived"],
573
- managers: Array.isArray(row["process_managers"]) ? row["process_managers"].map(ref).filter(Boolean) : undefined,
574
- });
575
- }
576
- function projectQueue(value) {
577
- const row = asRecord(value);
578
- return compact({
579
- id: row["id"],
580
- name: row["name"],
581
- processId: row["process_id"],
582
- processName: row["process_name"] ?? asRecord(row["process"])["name"],
583
- description: row["description"] || undefined,
584
- assignedCount: row["assigned_count"],
585
- unassignedCount: row["unassigned_count"],
586
- });
587
- }
588
- function projectQueueIndividual(value) {
589
- const row = asRecord(value);
590
- const queue = asRecord(row["queue"]);
591
- const process = asRecord(row["process"]);
592
- return compact({
593
- queueIndividualId: row["queue_individual_id"],
594
- queueId: row["queue_id"],
595
- queueName: queue["name"],
596
- processId: process["id"] ?? queue["process_id"],
597
- processName: process["name"] ?? queue["process_name"],
598
- individualId: row["individual_id"],
599
- name: asRecord(row["individual"])["name"],
600
- status: row["status"],
601
- due: row["due"],
602
- created: row["created"],
603
- completed: row["completed"],
604
- });
605
- }
606
- function projectCategory(value) {
607
- const row = asRecord(value);
608
- return compact({
609
- id: row["id"],
610
- name: row["name"],
611
- campus: ref(row["campus"]),
612
- organizer: ref(row["organizer"]),
613
- archived: row["archived"],
614
- teams: Array.isArray(row["teams"]) ? row["teams"].map(ref).filter(Boolean) : undefined,
615
- });
616
- }
617
- function projectSchedule(value) {
618
- const row = asRecord(value);
619
- return compact({
620
- id: row["id"],
621
- categoryId: row["category_id"],
622
- name: row["name"],
623
- start: row["start"],
624
- end: row["end"],
625
- needed: row["needed"],
626
- events: Array.isArray(row["events"])
627
- ? row["events"].map((event) => {
628
- const item = asRecord(event);
629
- return compact({ id: item["id"], name: item["name"], start: item["start"], end: item["end"] });
630
- })
631
- : undefined,
632
- metrics: present(compact({ ...asRecord(row["metrics"]) })),
633
- });
634
- }
635
- function projectAssignment(value) {
636
- const row = asRecord(value);
637
- const volunteer = asRecord(row["volunteer"]);
638
- const person = asRecord(volunteer["individual"]);
639
- return compact({
640
- id: row["id"],
641
- categoryId: row["category_id"],
642
- scheduleId: row["schedule_id"],
643
- eventId: row["event_id"],
644
- eventPositionId: row["event_position_id"],
645
- status: row["status"],
646
- statusReason: row["status_reason"] || undefined,
647
- dateNotified: row["date_notified"],
648
- individualId: person["id"],
649
- name: person["name"],
650
- });
651
- }
652
- function projectNote(value) {
653
- const row = asRecord(value);
654
- return compact({
655
- id: row["id"],
656
- individualId: row["individual_id"],
657
- note: row["note"],
658
- date: row["date"],
659
- sharingLevel: row["sharing_level"],
660
- context: row["context"],
661
- contextId: row["context_id"],
662
- creator: ref(row["creator"]),
663
- });
664
- }
665
- function projectPledge(value) {
666
- const row = asRecord(value);
667
- const recurrence = asRecord(row["recurrence"]);
668
- return compact({
669
- id: row["id"],
670
- campusId: row["campus_id"],
671
- fund: asRecord(row["coa"])["name"],
672
- pledged: row["pledged"],
673
- paid: row["paid"],
674
- expected: row["expected"],
675
- total: row["total"],
676
- frequency: recurrence["frequency"],
677
- start: row["start"],
678
- end: recurrence["end"],
679
- });
680
- }
681
- // ---------------------------------------------------------------------------
682
- // Schemas
683
- // ---------------------------------------------------------------------------
684
- const INT = { type: "integer" };
685
- const STR = { type: "string" };
686
- const BOOL = { type: "boolean" };
687
- const DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
688
- function obj(properties) {
689
- return { type: "object", properties };
690
- }
691
- function idProperty(description) {
692
- return { type: "integer", minimum: 1, description };
693
- }
694
- const RAW = {
695
- type: "boolean",
696
- description: "Return CCB's untouched rows, including the fields the projection drops.",
697
- };
698
- const PAGE = {
699
- type: "integer",
700
- minimum: 1,
701
- description: "1-based page number; pass page.nextPage from the previous result.",
702
- };
703
- function perPageProperty(defaultPerPage) {
704
- return {
705
- type: "integer",
706
- enum: [...PER_PAGE_VALUES],
707
- description: `Rows per page. CCB accepts only these values; defaults to ${defaultPerPage}.`,
708
- };
709
- }
710
- const PAGE_SCHEMA = obj({
711
- hasMore: BOOL,
712
- nextPage: { type: ["integer", "null"] },
713
- total: { type: ["integer", "null"] },
714
- });
715
- function listOutput(key, item) {
716
- return {
717
- type: "object",
718
- properties: { [key]: { type: "array", items: item }, page: PAGE_SCHEMA },
719
- required: [key, "page"],
720
- };
721
- }
722
- function rowsOutput(key, item) {
723
- return {
724
- type: "object",
725
- properties: { [key]: { type: "array", items: item } },
726
- required: [key],
727
- };
728
- }
729
- function input(properties, required = []) {
730
- return { type: "object", properties, required, additionalProperties: false };
731
- }
732
- const PHONES = obj({ mobile: STR, home: STR, work: STR });
733
- const PERSON_SCHEMA = obj({
734
- id: INT,
735
- name: STR,
736
- firstName: STR,
737
- lastName: STR,
738
- email: STR,
739
- phones: PHONES,
740
- birthday: STR,
741
- familyId: INT,
742
- familyPosition: STR,
743
- campusId: INT,
744
- membershipTypeId: INT,
745
- active: BOOL,
746
- deceased: STR,
747
- });
748
- const ADDRESS = obj({ street: STR, city: STR, state: STR, zip: STR, country: STR });
749
- const INDIVIDUAL_SCHEMA = obj({
750
- ...PERSON_SCHEMA.properties,
751
- gender: STR,
752
- maritalStatus: STR,
753
- homeAddress: ADDRESS,
754
- mailingAddress: ADDRESS,
755
- campusName: STR,
756
- membershipTypeName: STR,
757
- membershipDate: STR,
758
- baptized: BOOL,
759
- lastAttendedDate: STR,
760
- listed: BOOL,
761
- customFields: { type: "object" },
762
- });
763
- const FAMILY_SCHEMA = obj({
764
- id: INT,
765
- lastName: STR,
766
- address: ADDRESS,
767
- members: { type: "array", items: PERSON_SCHEMA },
768
- });
769
- const REF = obj({ id: INT, name: STR });
770
- const GROUP_SCHEMA = obj({
771
- id: INT,
772
- name: STR,
773
- campusId: INT,
774
- inactive: BOOL,
775
- groupType: STR,
776
- interactionType: STR,
777
- membershipType: STR,
778
- mainLeader: REF,
779
- departmentId: INT,
780
- });
781
- const GROUP_DETAIL_SCHEMA = obj({
782
- ...GROUP_SCHEMA.properties,
783
- description: STR,
784
- memberCount: INT,
785
- childcare: BOOL,
786
- listed: BOOL,
787
- full: BOOL,
788
- meetDay: STR,
789
- meetTime: STR,
790
- area: STR,
791
- address: ADDRESS,
792
- });
793
- const MEMBERSHIP_SCHEMA = obj({
794
- groupId: INT,
795
- individualId: INT,
796
- status: STR,
797
- dateAdded: STR,
798
- name: STR,
799
- email: STR,
800
- phones: PHONES,
801
- groupName: STR,
802
- groupType: STR,
803
- groupInactive: BOOL,
804
- });
805
- const OCCURRENCE_SCHEMA = obj({
806
- eventId: INT,
807
- occurrence: STR,
808
- start: STR,
809
- end: STR,
810
- name: STR,
811
- campusId: INT,
812
- recurs: BOOL,
813
- groupId: INT,
814
- groupName: STR,
815
- });
816
- const EVENT_SCHEMA = obj({
817
- id: INT,
818
- name: STR,
819
- description: STR,
820
- campusId: INT,
821
- start: STR,
822
- end: STR,
823
- recurs: BOOL,
824
- recurEndDate: STR,
825
- approvalStatus: STR,
826
- address: ADDRESS,
827
- guestCounts: { type: "object" },
828
- group: REF,
829
- attendance: obj({ status: STR, totalAttendance: INT, visitors: INT }),
830
- });
831
- const ATTENDANCE_SCHEMA = obj({
832
- eventId: INT,
833
- occurrence: STR,
834
- status: STR,
835
- totalAttendance: INT,
836
- visitors: INT,
837
- topic: STR,
838
- });
839
- const ATTENDEE_SCHEMA = obj({ individualId: INT, name: STR, familyId: INT, occurrenceDate: STR });
840
- const FORM_SCHEMA = obj({
841
- id: INT,
842
- title: STR,
843
- status: STR,
844
- campusId: INT,
845
- start: STR,
846
- end: STR,
847
- public: BOOL,
848
- url: STR,
849
- });
850
- const RESPONSE_SCHEMA = obj({
851
- id: INT,
852
- formId: INT,
853
- individualId: INT,
854
- created: STR,
855
- paymentStatus: STR,
856
- answers: { type: "array", items: obj({ questionId: INT, answer: STR }) },
857
- });
858
- const PROCESS_SCHEMA = obj({
859
- id: INT,
860
- name: STR,
861
- campusId: INT,
862
- ownerId: INT,
863
- hidden: BOOL,
864
- archived: BOOL,
865
- managers: { type: "array", items: REF },
866
- });
867
- const QUEUE_SCHEMA = obj({
868
- id: INT,
869
- name: STR,
870
- processId: INT,
871
- processName: STR,
872
- description: STR,
873
- assignedCount: INT,
874
- unassignedCount: INT,
875
- });
876
- const QUEUE_INDIVIDUAL_SCHEMA = obj({
877
- queueIndividualId: INT,
878
- queueId: INT,
879
- queueName: STR,
880
- processId: INT,
881
- processName: STR,
882
- individualId: INT,
883
- name: STR,
884
- status: STR,
885
- due: STR,
886
- created: STR,
887
- completed: STR,
888
- });
889
- const CATEGORY_SCHEMA = obj({
890
- id: INT,
891
- name: STR,
892
- campus: REF,
893
- organizer: REF,
894
- archived: BOOL,
895
- teams: { type: "array", items: REF },
896
- });
897
- const SCHEDULE_SCHEMA = obj({
898
- id: INT,
899
- categoryId: INT,
900
- name: STR,
901
- start: STR,
902
- end: STR,
903
- needed: INT,
904
- events: { type: "array", items: obj({ id: INT, name: STR, start: STR, end: STR }) },
905
- metrics: { type: "object" },
906
- });
907
- const ASSIGNMENT_SCHEMA = obj({
908
- id: INT,
909
- categoryId: INT,
910
- scheduleId: INT,
911
- eventId: INT,
912
- eventPositionId: INT,
913
- status: STR,
914
- statusReason: STR,
915
- dateNotified: STR,
916
- individualId: INT,
917
- name: STR,
918
- });
919
- const NOTE_SCHEMA = obj({
920
- id: INT,
921
- individualId: INT,
922
- note: STR,
923
- date: STR,
924
- sharingLevel: STR,
925
- context: STR,
926
- contextId: INT,
927
- creator: REF,
928
- });
929
- const PLEDGE_SCHEMA = obj({
930
- id: INT,
931
- campusId: INT,
932
- fund: STR,
933
- pledged: { type: "number" },
934
- paid: { type: "number" },
935
- expected: { type: "number" },
936
- total: { type: "number" },
937
- frequency: STR,
938
- start: STR,
939
- end: STR,
940
- });
941
- const ME_SCHEMA = obj({
942
- id: INT,
943
- name: STR,
944
- email: STR,
945
- username: STR,
946
- familyId: INT,
947
- campusId: INT,
948
- userTypes: { type: "array", items: STR },
949
- });
950
- /** Domains with a `POST /search/{domain}/results` endpoint in the v2 spec. */
951
- const SEARCH_DOMAINS = [
952
- "assignments",
953
- "categories",
954
- "department_detail",
955
- "groups",
956
- "individual_event_attendance",
957
- "individual_groups",
958
- "individuals",
959
- "organizers",
960
- "positions",
961
- "schedules",
962
- "schedules_totals",
963
- "scheduling_files",
964
- "scheduling_individuals",
965
- "songs",
966
- "volunteers",
967
- ];
968
- const HATCH_PATH = {
969
- type: "string",
970
- minLength: 2,
971
- maxLength: 512,
972
- pattern: "^/[^?#]*$",
973
- description: "Path below the API origin beginning with '/', such as /campuses. No query string; no /oauth paths.",
974
- };
975
- const QUERY = {
976
- type: "object",
977
- description: "Query parameters by name, one value each, such as { per_page: 50 }.",
978
- additionalProperties: { type: ["string", "number", "boolean"] },
979
- };
980
- const RESULT_OUTPUT = {
981
- type: "object",
982
- properties: {
983
- result: { description: "CCB's untouched response body, or null when empty." },
984
- page: PAGE_SCHEMA,
985
- },
986
- required: ["result"],
987
- };
988
- /**
989
- * The hatch path, refused locally where it could only fail or should not be
990
- * sent: the token endpoint shares the API origin, and sending the grant's
991
- * bearer token to `/oauth/*` is never what a call meant.
992
- */
993
- function hatchPath(value) {
994
- const path = String(value);
995
- // Judged after the same normalization the transport applies, and after
996
- // percent-decoding, so `/groups/../oauth/token` and `/%6Fauth/token` are
997
- // the token endpoint they resolve to.
998
- let resolved = path;
999
- try {
1000
- resolved = new URL(`https://ccb.invalid${path.startsWith("/") ? path : `/${path}`}`).pathname;
1001
- resolved = decodeURIComponent(resolved);
1002
- }
1003
- catch {
1004
- // An unparseable path is refused by the transport's own confinement.
1005
- }
1006
- if (/^\/+oauth(\/|$)/i.test(path) || /^\/+oauth(\/|$)/i.test(resolved)) {
1007
- throw new ConnectorCallError("invalid_args", "CCB /oauth paths belong to the connector's grant, not to API calls.");
1008
- }
1009
- return path;
1010
- }
1011
- /** Scalar query values only; the schema admits nothing else. */
1012
- function hatchQuery(value) {
1013
- const query = {};
1014
- for (const [name, item] of Object.entries(asRecord(value))) {
1015
- if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
1016
- query[name] = item;
1017
- }
1018
- }
1019
- return query;
1020
- }
1021
- function pageQuery(args, defaultPerPage) {
1022
- return {
1023
- page: typeof args["page"] === "number" ? args["page"] : 1,
1024
- per_page: typeof args["perPage"] === "number" ? args["perPage"] : defaultPerPage,
1025
- };
1026
- }
1027
- function compactDate(value) {
1028
- return typeof value === "string" ? value.replace(/-/g, "") : undefined;
1029
- }
1030
- const OCCURRENCE = {
1031
- type: "string",
1032
- pattern: "^\\d{4}-?\\d{2}-?\\d{2}$",
1033
- description: "Occurrence date, YYYYMMDD or YYYY-MM-DD, from list_events.",
1034
- };
1035
- function dateProperty(description) {
1036
- return { type: "string", pattern: DATE_PATTERN, description };
1037
- }
1038
- const DAY_MS = 86_400_000;
1039
- // ---------------------------------------------------------------------------
1040
- // Tools
1041
- // ---------------------------------------------------------------------------
1042
- function tools(send, defaultPerPage, writable) {
1043
- const readOnly = { readOnlyHint: true };
1044
- const perPage = perPageProperty(defaultPerPage);
1045
- /** A page-numbered list: one GET, projected rows, and the page signal. */
1046
- const list = (path, args, ctx, key, project, query = {}) => callCcb(send, { method: "GET", path, query: { ...query, ...pageQuery(args, defaultPerPage) } }, ctx).then(({ body, headers }) => {
1047
- const rows = asArray(body);
1048
- const paging = pageQuery(args, defaultPerPage);
1049
- return {
1050
- [key]: args["raw"] === true ? rows : rows.map(project),
1051
- page: pageInfo(headers, paging.page, paging.per_page, rows.length),
1052
- };
1053
- });
1054
- /** An unpaged GET whose body is one record. */
1055
- const one = async (path, args, ctx, project, query = {}) => {
1056
- const { body } = await callCcb(send, { method: "GET", path, query }, ctx);
1057
- return args["raw"] === true ? body : project(body);
1058
- };
1059
- const id = (value) => encodeURIComponent(String(value));
1060
- const named = [
1061
- {
1062
- name: "get_me",
1063
- description: "Get the CCB individual the access token belongs to. Under Identity Auth that is the signed-in person whose permissions every call carries.",
1064
- annotations: readOnly,
1065
- inputSchema: input({}),
1066
- outputSchema: ME_SCHEMA,
1067
- handler: async (_args, ctx) => {
1068
- const { body } = await callCcb(send, { method: "GET", path: "/me" }, ctx);
1069
- const row = asRecord(body);
1070
- return compact({
1071
- id: row["id"],
1072
- name: row["name"],
1073
- email: row["email"] || undefined,
1074
- username: row["username"],
1075
- familyId: row["family_id"],
1076
- campusId: row["campus_id"],
1077
- userTypes: Array.isArray(row["user_types"]) ? row["user_types"] : undefined,
1078
- });
1079
- },
1080
- },
1081
- {
1082
- name: "list_individuals",
1083
- description: "Find CCB individuals by partial name, phone, or email. Returns lean profiles with family and campus ids; not the advanced search.",
1084
- annotations: readOnly,
1085
- inputSchema: input({
1086
- query: {
1087
- type: "string",
1088
- minLength: 1,
1089
- maxLength: 200,
1090
- description: "Partial name, phone number, or email. Omit to page everyone.",
1091
- },
1092
- includeInactive: { type: "boolean", description: "Include inactive individuals." },
1093
- includeDeceased: { type: "boolean", description: "Include deceased individuals." },
1094
- campusScope: {
1095
- type: "string",
1096
- enum: ["all", "current", "other"],
1097
- description: "Campuses to search relative to the connection's own.",
1098
- },
1099
- page: PAGE,
1100
- perPage,
1101
- raw: RAW,
1102
- }),
1103
- outputSchema: listOutput("individuals", PERSON_SCHEMA),
1104
- handler: (args, ctx) => list("/individuals", args, ctx, "individuals", projectPerson, {
1105
- name: args["query"],
1106
- include_inactive: args["includeInactive"],
1107
- include_deceased: args["includeDeceased"],
1108
- campus_scope: args["campusScope"],
1109
- }),
1110
- },
1111
- {
1112
- name: "get_individual",
1113
- description: "Get one CCB individual's profile: contact, addresses, membership, and custom fields. CCB allows one call a second; prefer list_individuals for many people.",
1114
- annotations: readOnly,
1115
- inputSchema: input({ individualId: idProperty("Individual id from list_individuals."), raw: RAW }, [
1116
- "individualId",
1117
- ]),
1118
- outputSchema: INDIVIDUAL_SCHEMA,
1119
- handler: (args, ctx) => one(`/individuals/${id(args["individualId"])}`, args, ctx, projectIndividual),
1120
- },
1121
- {
1122
- name: "list_families",
1123
- description: "Find CCB families by partial last name, email, or mobile phone. Returns family ids and addresses; get_family lists the members.",
1124
- annotations: readOnly,
1125
- inputSchema: input({
1126
- lastName: { type: "string", minLength: 1, maxLength: 200, description: "Partial last name." },
1127
- email: { type: "string", minLength: 1, maxLength: 200, description: "Partial email." },
1128
- phone: { type: "string", minLength: 1, maxLength: 50, description: "Partial mobile phone." },
1129
- page: PAGE,
1130
- perPage,
1131
- raw: RAW,
1132
- }),
1133
- outputSchema: listOutput("families", FAMILY_SCHEMA),
1134
- handler: (args, ctx) => list("/families", args, ctx, "families", projectFamily, {
1135
- last_name: args["lastName"],
1136
- email: args["email"],
1137
- phone: args["phone"],
1138
- }),
1139
- },
1140
- {
1141
- name: "get_family",
1142
- description: "Get one CCB family with every member's lean profile and family position.",
1143
- annotations: readOnly,
1144
- inputSchema: input({
1145
- familyId: idProperty("Family id from an individual's familyId or list_families."),
1146
- includeInactive: { type: "boolean", description: "Include inactive members." },
1147
- includeDeceased: { type: "boolean", description: "Include deceased members." },
1148
- raw: RAW,
1149
- }, ["familyId"]),
1150
- outputSchema: FAMILY_SCHEMA,
1151
- handler: (args, ctx) => one(`/families/${id(args["familyId"])}`, args, ctx, projectFamily, {
1152
- include_inactive: args["includeInactive"],
1153
- include_deceased: args["includeDeceased"],
1154
- }),
1155
- },
1156
- {
1157
- name: "list_groups",
1158
- description: "List or search CCB groups by partial name, optionally for one campus. Returns group type, leader, and membership mode.",
1159
- annotations: readOnly,
1160
- inputSchema: input({
1161
- name: { type: "string", minLength: 1, maxLength: 200, description: "Partial group name." },
1162
- campusId: idProperty("Only groups at this campus."),
1163
- page: PAGE,
1164
- perPage,
1165
- raw: RAW,
1166
- }),
1167
- outputSchema: listOutput("groups", GROUP_SCHEMA),
1168
- handler: (args, ctx) => list("/groups", args, ctx, "groups", projectGroup, {
1169
- name: args["name"],
1170
- campus_ids: args["campusId"],
1171
- }),
1172
- },
1173
- {
1174
- name: "get_group",
1175
- description: "Get one CCB group: description, member count, meeting day, time, area, and address.",
1176
- annotations: readOnly,
1177
- inputSchema: input({ groupId: idProperty("Group id from list_groups."), raw: RAW }, ["groupId"]),
1178
- outputSchema: GROUP_DETAIL_SCHEMA,
1179
- handler: (args, ctx) => one(`/groups/${id(args["groupId"])}`, args, ctx, projectGroup),
1180
- },
1181
- {
1182
- name: "list_group_members",
1183
- description: "List one CCB group's participants with role and date added. Filter by leader or member status, or partial name.",
1184
- annotations: readOnly,
1185
- inputSchema: input({
1186
- groupId: idProperty("Group id from list_groups."),
1187
- status: { type: "string", enum: ["all", "leader", "member"], description: "Participant role filter." },
1188
- name: { type: "string", minLength: 1, maxLength: 200, description: "Partial participant name." },
1189
- page: PAGE,
1190
- perPage,
1191
- raw: RAW,
1192
- }, ["groupId"]),
1193
- outputSchema: listOutput("members", MEMBERSHIP_SCHEMA),
1194
- handler: (args, ctx) => list(`/groups/${id(args["groupId"])}/members`, args, ctx, "members", projectMembership, {
1195
- status: args["status"],
1196
- name: args["name"],
1197
- }),
1198
- },
1199
- {
1200
- name: "list_individual_groups",
1201
- description: "List the CCB groups one individual belongs to, with their role. Defaults to member and leader roles.",
1202
- annotations: readOnly,
1203
- inputSchema: input({
1204
- individualId: idProperty("Individual id."),
1205
- includeInactive: { type: "boolean", description: "List inactive groups instead of active ones." },
1206
- page: PAGE,
1207
- perPage,
1208
- raw: RAW,
1209
- }, ["individualId"]),
1210
- outputSchema: listOutput("groups", MEMBERSHIP_SCHEMA),
1211
- handler: (args, ctx) => list(`/individuals/${id(args["individualId"])}/groups`, args, ctx, "groups", projectMembership, {
1212
- inactive: args["includeInactive"],
1213
- }),
1214
- },
1215
- {
1216
- name: "list_events",
1217
- description: "List CCB event occurrences by name and time range. Each row's eventId and occurrence feed the attendance tools.",
1218
- annotations: readOnly,
1219
- inputSchema: input({
1220
- name: { type: "string", minLength: 1, maxLength: 200, description: "Partial event name." },
1221
- range: {
1222
- type: "string",
1223
- enum: ["ALL", "PAST", "CURRENT", "FUTURE"],
1224
- description: "Time range relative to now.",
1225
- },
1226
- page: PAGE,
1227
- perPage,
1228
- raw: RAW,
1229
- }),
1230
- outputSchema: listOutput("occurrences", OCCURRENCE_SCHEMA),
1231
- handler: (args, ctx) => list("/events", args, ctx, "occurrences", projectOccurrence, {
1232
- name: args["name"],
1233
- range_type: args["range"],
1234
- }),
1235
- },
1236
- {
1237
- name: "get_event",
1238
- description: "Get one CCB event's details, optionally for one occurrence with its attendance totals.",
1239
- annotations: readOnly,
1240
- inputSchema: input({
1241
- eventId: idProperty("Event id from list_events."),
1242
- occurrence: { ...OCCURRENCE, description: "Occurrence date, YYYYMMDD or YYYY-MM-DD. Omit for the series." },
1243
- withAttendance: { type: "boolean", description: "Add attendance totals for the occurrence." },
1244
- raw: RAW,
1245
- }, ["eventId"]),
1246
- outputSchema: EVENT_SCHEMA,
1247
- handler: (args, ctx) => one(`/events/${id(args["eventId"])}`, args, ctx, projectEvent, {
1248
- occurrence: compactDate(args["occurrence"]),
1249
- attendance_info: args["withAttendance"],
1250
- }),
1251
- },
1252
- {
1253
- name: "get_event_attendance",
1254
- description: "Get one CCB event occurrence's attendance summary: met or not, totals, visitors, topic. Drops leader notes and prayer requests unless raw.",
1255
- annotations: readOnly,
1256
- inputSchema: input({ eventId: idProperty("Event id from list_events."), occurrence: OCCURRENCE, raw: RAW }, [
1257
- "eventId",
1258
- "occurrence",
1259
- ]),
1260
- outputSchema: ATTENDANCE_SCHEMA,
1261
- handler: (args, ctx) => one(`/events/${id(args["eventId"])}/attendance/${compactDate(args["occurrence"])}`, args, ctx, projectAttendance),
1262
- },
1263
- {
1264
- name: "list_event_attendees",
1265
- description: "List who attended one CCB event occurrence. Returns individual ids and names.",
1266
- annotations: readOnly,
1267
- inputSchema: input({ eventId: idProperty("Event id from list_events."), occurrence: OCCURRENCE, page: PAGE, perPage, raw: RAW }, ["eventId", "occurrence"]),
1268
- outputSchema: listOutput("attendees", ATTENDEE_SCHEMA),
1269
- handler: (args, ctx) => list(`/events/${id(args["eventId"])}/attendance/${compactDate(args["occurrence"])}/attendees`, args, ctx, "attendees", projectAttendee),
1270
- },
1271
- {
1272
- name: "list_forms",
1273
- description: "List or search CCB forms by partial title, status, or campus. Returns form ids for list_form_responses.",
1274
- annotations: readOnly,
1275
- inputSchema: input({
1276
- title: { type: "string", minLength: 1, maxLength: 200, description: "Partial form title." },
1277
- status: {
1278
- type: "string",
1279
- enum: ["ACTIVE", "SCHEDULED", "UNPUBLISHED", "EXPIRED", "ARCHIVED"],
1280
- description: "Form status.",
1281
- },
1282
- campusId: idProperty("Only forms at this campus."),
1283
- page: PAGE,
1284
- perPage,
1285
- raw: RAW,
1286
- }),
1287
- outputSchema: listOutput("forms", FORM_SCHEMA),
1288
- handler: (args, ctx) => list("/forms", args, ctx, "forms", projectForm, {
1289
- title: args["title"],
1290
- status: args["status"],
1291
- campus_id: args["campusId"],
1292
- }),
1293
- },
1294
- {
1295
- name: "list_form_responses",
1296
- description: "List one CCB form's responses with answers by question id and payment status. Drops payment details unless raw.",
1297
- annotations: readOnly,
1298
- inputSchema: input({ formId: idProperty("Form id from list_forms."), page: PAGE, perPage, raw: RAW }, ["formId"]),
1299
- outputSchema: listOutput("responses", RESPONSE_SCHEMA),
1300
- handler: (args, ctx) => list(`/forms/${id(args["formId"])}/responses`, args, ctx, "responses", projectResponse),
1301
- },
1302
- {
1303
- name: "list_processes",
1304
- description: "List CCB processes (assimilation and follow-up pipelines) by name or status. Returns process ids for list_queues.",
1305
- annotations: readOnly,
1306
- inputSchema: input({
1307
- name: { type: "string", minLength: 1, maxLength: 200, description: "Process name search." },
1308
- status: { type: "string", enum: ["HIDDEN", "ACTIVE", "ARCHIVED"], description: "Process status." },
1309
- managedByMe: { type: "boolean", description: "Only processes the connection's individual manages." },
1310
- page: PAGE,
1311
- perPage,
1312
- raw: RAW,
1313
- }),
1314
- outputSchema: listOutput("processes", PROCESS_SCHEMA),
1315
- handler: (args, ctx) => list("/processes", args, ctx, "processes", projectProcess, {
1316
- name: args["name"],
1317
- statuses: args["status"],
1318
- managed_by_me: args["managedByMe"],
1319
- }),
1320
- },
1321
- {
1322
- name: "list_queues",
1323
- description: "List CCB process queues, optionally for one process, with assigned and unassigned counts.",
1324
- annotations: readOnly,
1325
- inputSchema: input({
1326
- processId: idProperty("Only queues in this process."),
1327
- search: { type: "string", minLength: 1, maxLength: 200, description: "Queue name search." },
1328
- page: PAGE,
1329
- perPage,
1330
- raw: RAW,
1331
- }),
1332
- outputSchema: listOutput("queues", QUEUE_SCHEMA),
1333
- handler: (args, ctx) => list("/queues", args, ctx, "queues", projectQueue, {
1334
- process_ids: args["processId"],
1335
- search_term: args["search"],
1336
- }),
1337
- },
1338
- {
1339
- name: "list_queue_individuals",
1340
- description: "List people in CCB process queues the connection can see, by status or assignment. CCB offers no per-queue filter here; filter on queueId.",
1341
- annotations: readOnly,
1342
- inputSchema: input({
1343
- status: {
1344
- type: "string",
1345
- enum: ["NOT_STARTED", "WAITING", "IN_PROCESS", "DONE"],
1346
- description: "Queue status.",
1347
- },
1348
- assignment: { type: "string", enum: ["ASSIGNED_TO_ME", "UNASSIGNED"], description: "Omit for both." },
1349
- page: PAGE,
1350
- perPage,
1351
- raw: RAW,
1352
- }),
1353
- outputSchema: listOutput("queueIndividuals", QUEUE_INDIVIDUAL_SCHEMA),
1354
- handler: (args, ctx) => list("/queues_individuals", args, ctx, "queueIndividuals", projectQueueIndividual, {
1355
- statuses: args["status"],
1356
- assignment: args["assignment"],
1357
- }),
1358
- },
1359
- {
1360
- name: "list_scheduling_categories",
1361
- description: "List CCB scheduling categories (serving ministries) with teams and organizer. Category ids feed list_schedules.",
1362
- annotations: readOnly,
1363
- inputSchema: input({
1364
- search: { type: "string", minLength: 1, maxLength: 200, description: "Category name search." },
1365
- status: {
1366
- type: "string",
1367
- enum: ["ACTIVE", "ARCHIVED", "ACTIVE_AND_ARCHIVED"],
1368
- description: "Defaults to CCB's own (active).",
1369
- },
1370
- raw: RAW,
1371
- }),
1372
- outputSchema: rowsOutput("categories", CATEGORY_SCHEMA),
1373
- handler: async (args, ctx) => {
1374
- const { body } = await callCcb(send, {
1375
- method: "GET",
1376
- path: "/scheduling/categories",
1377
- query: { search_term: args["search"], status: args["status"] },
1378
- }, ctx);
1379
- const rows = Array.isArray(body) ? body : body && typeof body === "object" ? [body] : [];
1380
- return { categories: args["raw"] === true ? rows : rows.map(projectCategory) };
1381
- },
1382
- },
1383
- {
1384
- name: "list_schedules",
1385
- description: "List one CCB scheduling category's schedules with events and assignment metrics. CCB allows one call per two seconds here.",
1386
- annotations: readOnly,
1387
- inputSchema: input({
1388
- categoryId: idProperty("Category id from list_scheduling_categories."),
1389
- after: dateProperty("Only schedules ending after this date, YYYY-MM-DD."),
1390
- before: dateProperty("Only schedules ending before this date, YYYY-MM-DD."),
1391
- page: PAGE,
1392
- perPage,
1393
- raw: RAW,
1394
- }, ["categoryId"]),
1395
- outputSchema: listOutput("schedules", SCHEDULE_SCHEMA),
1396
- handler: (args, ctx) => list(`/scheduling/categories/${id(args["categoryId"])}/schedules`, args, ctx, "schedules", projectSchedule, {
1397
- after: args["after"],
1398
- before: args["before"],
1399
- withMetrics: true,
1400
- }),
1401
- },
1402
- {
1403
- name: "list_individual_assignments",
1404
- description: "List one CCB individual's serving assignments with status, schedule, and event ids.",
1405
- annotations: readOnly,
1406
- inputSchema: input({
1407
- individualId: idProperty("Individual id."),
1408
- startDate: dateProperty("Earliest assignment date, YYYY-MM-DD."),
1409
- endDate: dateProperty("Latest assignment date, YYYY-MM-DD."),
1410
- page: PAGE,
1411
- perPage,
1412
- raw: RAW,
1413
- }, ["individualId"]),
1414
- outputSchema: listOutput("assignments", ASSIGNMENT_SCHEMA),
1415
- // The spec declares start_date and end_date as this GET's request body
1416
- // (getIndividualAssignments), which fetch cannot send; they ride the
1417
- // query, as list_scheduling_categories' body fields do.
1418
- handler: (args, ctx) => list(`/individuals/${id(args["individualId"])}/assignments`, args, ctx, "assignments", projectAssignment, {
1419
- start_date: args["startDate"],
1420
- end_date: args["endDate"],
1421
- }),
1422
- },
1423
- {
1424
- name: "list_individual_notes",
1425
- description: "List the notes on one CCB individual that the connection may read, with sharing level and context. Pastoral content; reduce before returning.",
1426
- annotations: readOnly,
1427
- inputSchema: input({
1428
- individualId: idProperty("Individual id."),
1429
- context: {
1430
- type: "string",
1431
- enum: ["GROUP", "DEPARTMENT", "PROCESS_QUEUE", "GENERAL"],
1432
- description: "Only notes in this context.",
1433
- },
1434
- page: PAGE,
1435
- perPage,
1436
- raw: RAW,
1437
- }, ["individualId"]),
1438
- outputSchema: listOutput("notes", NOTE_SCHEMA),
1439
- handler: (args, ctx) => list(`/individuals/${id(args["individualId"])}/notes`, args, ctx, "notes", projectNote, {
1440
- context: args["context"],
1441
- include_creator: true,
1442
- }),
1443
- },
1444
- {
1445
- name: "get_giving_metrics",
1446
- description: "Get gift counts by period for one CCB individual or family, over at most 12 months. Counts only; v2 exposes no amounts or transactions.",
1447
- annotations: readOnly,
1448
- inputSchema: input({
1449
- individualId: idProperty("Individual id. Exactly one of individualId or familyId."),
1450
- familyId: idProperty("Family id. Exactly one of individualId or familyId."),
1451
- start: dateProperty("Start date, YYYY-MM-DD. CCB caps the range at 12 months."),
1452
- end: dateProperty("End date, YYYY-MM-DD."),
1453
- raw: RAW,
1454
- }),
1455
- outputSchema: obj({
1456
- periods: {
1457
- type: "array",
1458
- items: obj({
1459
- start: STR,
1460
- count: INT,
1461
- members: { type: "array", items: obj({ individualId: INT, count: INT }) },
1462
- }),
1463
- },
1464
- }),
1465
- handler: async (args, ctx) => {
1466
- const individual = args["individualId"] !== undefined;
1467
- if (individual === (args["familyId"] !== undefined)) {
1468
- throw new ConnectorCallError("invalid_args", "Pass exactly one of individualId or familyId.");
1469
- }
1470
- if (typeof args["start"] === "string" &&
1471
- typeof args["end"] === "string" &&
1472
- Date.parse(args["end"]) - Date.parse(args["start"]) > 366 * DAY_MS) {
1473
- throw new ConnectorCallError("invalid_args", "CCB caps giving metrics at a 12-month range; narrow start and end.");
1474
- }
1475
- const path = individual
1476
- ? `/individuals/${id(args["individualId"])}/metrics/giving`
1477
- : `/families/${id(args["familyId"])}/metrics/giving`;
1478
- const { body } = await callCcb(send, { method: "GET", path, query: { start: args["start"], end: args["end"] } }, ctx);
1479
- const rows = asArray(body);
1480
- if (args["raw"] === true)
1481
- return { periods: rows };
1482
- return {
1483
- periods: rows.map((value) => {
1484
- const row = asRecord(value);
1485
- return compact({
1486
- start: row["start"] ?? row["date"],
1487
- count: row["count"],
1488
- members: Array.isArray(row["members"])
1489
- ? row["members"].map((member) => {
1490
- const item = asRecord(member);
1491
- return compact({ individualId: item["id"], count: item["count"] });
1492
- })
1493
- : undefined,
1494
- });
1495
- }),
1496
- };
1497
- },
1498
- },
1499
- {
1500
- name: "list_individual_pledges",
1501
- description: "List one CCB individual's pledges: fund, pledged, paid, and expected amounts, and recurrence.",
1502
- annotations: readOnly,
1503
- inputSchema: input({ individualId: idProperty("Individual id."), page: PAGE, perPage, raw: RAW }, [
1504
- "individualId",
1505
- ]),
1506
- outputSchema: listOutput("pledges", PLEDGE_SCHEMA),
1507
- handler: (args, ctx) => list(`/individuals/${id(args["individualId"])}/pledges`, args, ctx, "pledges", projectPledge),
1508
- },
1509
- ];
1510
- const hatches = [
1511
- {
1512
- name: "ccb_api_get",
1513
- description: "Call any CCB v2 GET endpoint and return its untouched body. Use named reads first; they project and page for you.",
1514
- annotations: readOnly,
1515
- inputSchema: input({ path: HATCH_PATH, query: QUERY }, ["path"]),
1516
- outputSchema: RESULT_OUTPUT,
1517
- handler: async (args, ctx) => {
1518
- const scalars = hatchQuery(args["query"]);
1519
- const { body, headers } = await callCcb(send, { method: "GET", path: hatchPath(args["path"]), query: scalars }, ctx);
1520
- const page = typeof scalars["page"] === "number" ? scalars["page"] : 1;
1521
- const per = typeof scalars["per_page"] === "number" ? scalars["per_page"] : DEFAULT_PER_PAGE;
1522
- return defined({
1523
- result: body ?? null,
1524
- page: Array.isArray(body) && (headers.has("x-page") || headers.has("x-next-page"))
1525
- ? pageInfo(headers, page, per, body.length)
1526
- : undefined,
1527
- });
1528
- },
1529
- },
1530
- {
1531
- name: "ccb_api_search",
1532
- description: "Run a CCB advanced search (POST /search/{domain}/results) and return untouched rows. Read-only; individuals allows one call per five seconds.",
1533
- annotations: readOnly,
1534
- inputSchema: input({
1535
- // A pattern, not an enum: fifteen domain names render past the
1536
- // 256-byte annotation budget and would truncate the whole tool in
1537
- // search (H7). The handler refuses anything outside the list.
1538
- domain: {
1539
- type: "string",
1540
- pattern: "^[a-z_]{1,40}$",
1541
- description: `Search domain: ${SEARCH_DOMAINS.join(", ")}.`,
1542
- },
1543
- body: {
1544
- type: "object",
1545
- description: "Search body as CCB documents it for the domain, e.g. { configuration, filters }.",
1546
- },
1547
- page: PAGE,
1548
- perPage,
1549
- }, ["domain", "body"]),
1550
- outputSchema: RESULT_OUTPUT,
1551
- handler: async (args, ctx) => {
1552
- if (!SEARCH_DOMAINS.includes(args["domain"])) {
1553
- throw new ConnectorCallError("invalid_args", `CCB has no advanced search domain "${String(args["domain"]).slice(0, 40)}"; use one of ${SEARCH_DOMAINS.join(", ")}.`);
1554
- }
1555
- const paging = pageQuery(args, defaultPerPage);
1556
- const { body, headers } = await callCcb(send, { method: "POST", path: `/search/${args["domain"]}/results`, query: paging, body: args["body"] }, ctx);
1557
- return {
1558
- result: body ?? null,
1559
- page: pageInfo(headers, paging.page, paging.per_page, asArray(body).length),
1560
- };
1561
- },
1562
- },
1563
- ];
1564
- if (writable) {
1565
- hatches.push({
1566
- name: "ccb_api_mutate",
1567
- description: "Call any CCB v2 JSON POST, PUT, PATCH, or DELETE endpoint. The approval-gated hatch for every write; no file uploads.",
1568
- annotations: { readOnlyHint: false, destructiveHint: true },
1569
- inputSchema: input({
1570
- method: {
1571
- type: "string",
1572
- enum: ["POST", "PUT", "PATCH", "DELETE"],
1573
- description: "HTTP method the endpoint documents.",
1574
- },
1575
- path: HATCH_PATH,
1576
- query: QUERY,
1577
- body: {
1578
- type: ["object", "array", "string", "number", "boolean", "null"],
1579
- description: "JSON body exactly as CCB documents it. Omit when the endpoint takes none.",
1580
- },
1581
- }, ["method", "path"]),
1582
- outputSchema: RESULT_OUTPUT,
1583
- handler: async (args, ctx) => {
1584
- const { body } = await callCcb(send, {
1585
- method: args["method"],
1586
- path: hatchPath(args["path"]),
1587
- query: hatchQuery(args["query"]),
1588
- ...(args["body"] !== undefined ? { body: args["body"] } : {}),
1589
- }, ctx);
1590
- return { result: body ?? null };
1591
- },
1592
- });
1593
- }
1594
- return [...named, ...hatches];
1595
- }
1596
- // ---------------------------------------------------------------------------
1597
- // Guide
1598
- // ---------------------------------------------------------------------------
1599
- function routingLine(environment, mode, writable) {
1600
- return [
1601
- environment === "sandbox" ? "Sandbox church data" : "Production church data",
1602
- mode === "identity"
1603
- ? "acting as each signed-in person with their own CCB permissions (Identity Auth)"
1604
- : "church-wide under one administrator-approved grant (System Auth)",
1605
- writable ? "reads plus approval-gated writes" : "read-only",
1606
- ].join(", ");
1607
- }
1608
- function usageGuide(purpose, environment, mode, scopes, writable, instructions) {
1609
- const extra = instructions?.trim();
1610
- return `# Church Community Builder (Pushpay ChMS)
1611
-
1612
- ${routingLine(environment, mode, writable)}: ${purpose}${skill.fragments.guide_0}${mode === "identity" ? "- Results are what the signed-in person may see in CCB. A 403 means that person lacks the permission, not that the connection is broken.\n" : ""}${skill.fragments.guide_1}${writable ? " `ccb_api_mutate` reaches every POST, PUT, PATCH, and DELETE and always needs approval." : " This connection is read-only, so it ships no write tool."} Paths are below the API origin, such as \`/church/membership_types\`.
1613
- Granted scopes: ${scopes.join(" ")}. A call outside them fails with 403.
1614
- ${extra ? `\n## ${skill.instructionsHeading}\n\n${extra}\n` : ""}`;
1615
- }
1616
- // ---------------------------------------------------------------------------
1617
- // Constructor
1618
- // ---------------------------------------------------------------------------
1619
- function resolveScopes(options) {
1620
- if (options.scopes !== undefined && options.access !== undefined) {
1621
- throw new Error("ccb() takes access or scopes, not both: scopes is the exact list and replaces access.");
1622
- }
1623
- if (options.scopes === undefined) {
1624
- const access = options.access ?? "read";
1625
- if (access !== "read" && access !== "read-write") {
1626
- throw new Error('ccb() access must be "read" or "read-write".');
1627
- }
1628
- return access === "read" ? CCB_READ_SCOPES : [...CCB_READ_SCOPES, ...CCB_WRITE_SCOPES];
1629
- }
1630
- if (!Array.isArray(options.scopes) || options.scopes.length === 0) {
1631
- throw new Error("ccb() scopes must be a non-empty list.");
1632
- }
1633
- const seen = new Set();
1634
- for (const scope of options.scopes) {
1635
- if (typeof scope !== "string" || !SCOPE_SHAPE.test(scope)) {
1636
- throw new Error(`ccb() scope "${String(scope)}" is not a CCB scope; CCB scopes look like read:individuals.`);
1637
- }
1638
- if (seen.has(scope))
1639
- throw new Error(`ccb() scope "${scope}" is listed twice.`);
1640
- seen.add(scope);
1641
- }
1642
- return [...options.scopes];
1643
- }
5
+ import { defineApiProvider } from "../../http/provider.js";
6
+ import { sensitiveFieldsOption } from "../_shared/sensitive-fields.js";
7
+ import { getMe, authenticate, CCB_MAX_RESPONSE_BYTES, CCB_ADMISSION, CcbApi, ccbSafety, errors, oauth, resolveScopes, CCB_INDEX, CCB_VERDICTS, CCB_PAGES, CCB_NAMED, ccbBinding, } from "./api.js";
8
+ import { ccbTools } from "./tools.js";
9
+ import { routingLine } from "./lib/guide.js";
10
+ export { CCB_ENVIRONMENTS, CCB_MEDIA_TYPE, CCB_READ_SCOPES, CCB_WRITE_SCOPES, CCB_SENSITIVE_FIELDS, } from "./lib/constants.js";
1644
11
  function defaultTitle(environment, mode, writable) {
1645
12
  const qualifiers = [
1646
13
  ...(environment === "sandbox" ? ["sandbox"] : []),
@@ -1651,75 +18,83 @@ function defaultTitle(environment, mode, writable) {
1651
18
  }
1652
19
  /** The closed options ccb() accepts; see `assertKnownOptions`. */
1653
20
  const CCB_OPTIONS = optionsOf()({
1654
- ...keys("purpose", "title", "instructions", "environment", "mode", "clientId", "clientSecret", "access", "scopes", "subdomain", "defaultPerPage", "maxResultBytes", "readOnly"),
21
+ ...keys("purpose", "title", "instructions", "environment", "mode", "clientId", "clientSecret", "access", "scopes", "subdomain", "defaultPerPage", "maxResultBytes", "readOnly", "sensitiveFields"),
1655
22
  callAdmission: CALL_ADMISSION,
1656
23
  });
1657
- /** A maintained Church Community Builder (Pushpay ChMS) v2 connection. */
1658
- export const ccb = asProviderFactory({
24
+ function validatedOrigin(options) {
25
+ if (options.environment !== "production" && options.environment !== "sandbox")
26
+ throw new Error('ccb() requires environment: "production" or "sandbox" — the one the OAuth client was issued for. Credentials do not cross environments.');
27
+ if (options.mode !== "system" && options.mode !== "identity")
28
+ throw new Error('ccb() requires mode: "system" (one church-wide grant an administrator approves) or "identity" (each person signs in as themselves).');
29
+ const subdomain = options.subdomain?.trim();
30
+ if (subdomain !== undefined && !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(subdomain))
31
+ throw new Error("ccb() subdomain must be the bare CCB subdomain, such as mychurch.");
32
+ if (![25, 50, 75, 100].includes(options.defaultPerPage ?? 25))
33
+ throw new Error("ccb() defaultPerPage must be 25, 50, 75, or 100 — the only page sizes CCB accepts.");
34
+ sensitiveFieldsOption("ccb", options.sensitiveFields);
35
+ resolveScopes(options);
36
+ return CCB_ENVIRONMENTS[options.environment].apiOrigin;
37
+ }
38
+ const writable = (options) => resolveScopes(options).some((scope) => !scope.startsWith("read:"));
39
+ /** A maintained Church Community Builder (Pushpay ChMS) v2 connection on the provider kit. */
40
+ export const ccb = defineApiProvider({
1659
41
  name: "ccb",
1660
42
  title: "Church Community Builder",
1661
- kind: "api",
1662
43
  readme: "Church Community Builder",
1663
44
  bundle: {
1664
- "baselineGzip": 120479,
1665
- "maxGzip": 180479,
1666
- "note": "./providers/ccb starts at 120,479 B gzip: the hand-written surface plus api()'s static OAuth grant over the SDK's auth client, with no remoteMcp() transport. The cap uses the baseline + 60,000 B policy.",
45
+ baselineGzip: 391649,
46
+ 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).",
1667
47
  },
1668
48
  skill,
1669
49
  options: CCB_OPTIONS,
1670
- create: ccbConnector,
50
+ displayTitle: (options) => defaultTitle(options.environment, options.mode, writable(options)),
51
+ description: (options) => `Church Community Builder (Pushpay ChMS) — ${routingLine(options.environment, options.mode, writable(options))}: ${options.purpose}`,
52
+ authScope: (options) => (options.mode === "identity" ? "personal" : "shared"),
53
+ vendor: CcbApi,
54
+ baseUrl: validatedOrigin,
55
+ maxResponseBytes: CCB_MAX_RESPONSE_BYTES,
56
+ headers: { Accept: CCB_MEDIA_TYPE },
57
+ defaultCallAdmission: CCB_ADMISSION,
58
+ authenticate,
59
+ oauth,
60
+ oauthWrite401: "grant",
61
+ errors,
62
+ safety: ccbSafety,
63
+ probe: { operation: getMe, request: {}, describe: () => "Connected to CCB." },
64
+ guide: (options) => ({
65
+ context: [
66
+ `${routingLine(options.environment, options.mode, writable(options))}: ${options.purpose}`,
67
+ "The OAuth client is configured for this environment; use the connection's existing grant. Resolve a person's identity with get_me when needed.",
68
+ `Sensitive fields: ${options.sensitiveFields ?? "drop"}. Named projections stay narrow; credentials and private file destinations are always withheld.`,
69
+ `Configured scopes: ${resolveScopes(options).join(" ")}.`,
70
+ ...(!writable(options)
71
+ ? ["Configured scopes permit only reads: ccb_api_write is omitted and writes are refused."]
72
+ : []),
73
+ ...(options.readOnly ? ["This connection is read-only: every write is refused."] : []),
74
+ ...(options.defaultPerPage !== undefined
75
+ ? [`Named lists default to ${options.defaultPerPage} rows on this connection.`]
76
+ : []),
77
+ ],
78
+ summary: `${options.environment === "sandbox" ? "Sandbox" : "Production"} CCB, ${options.mode === "identity" ? "per person" : "church-wide"}: id resolution, sensitive data, paging, rate limits, hatches.`,
79
+ required: true,
80
+ }),
81
+ pageSize: { default: 25, max: 100 },
82
+ idempotencyKey: false,
83
+ hatch: {
84
+ name: "ccb",
85
+ title: "CCB",
86
+ index: CCB_INDEX,
87
+ verdicts: CCB_VERDICTS,
88
+ named: CCB_NAMED,
89
+ pages: CCB_PAGES,
90
+ pageCursor: "argument",
91
+ binding: ccbBinding,
92
+ writeEnabled: writable,
93
+ pathExample: "/individuals/42",
94
+ notes: () => ({
95
+ read: "Read people and individual profiles, households and families, groups and memberships, events and attendance, campuses, forms and responses, processes and queues, schedules and assignments, notes, pledges, donations, contributions and giving metrics. Discover the exact operation with ccb_api_search, then inspect ccb_api_details.",
96
+ write: "Update, edit or change a person's email or phone; create people and new individual profiles, pastoral notes, group memberships and events; add person to group, change group role, record attendance and change phone number. Discover the exact operation with ccb_api_search, then inspect ccb_api_details. Use ccb_api_write for the requested change; the host may ask the user to confirm. Writes require authorized configured scopes.",
97
+ }),
98
+ },
99
+ tools: ccbTools,
1671
100
  });
1672
- function ccbConnector(id, options) {
1673
- const purpose = typeof options.purpose === "string" ? options.purpose.trim() : "";
1674
- if (!purpose)
1675
- throw new Error("ccb() requires a non-empty church purpose.");
1676
- if (options.environment !== "production" && options.environment !== "sandbox") {
1677
- throw new Error('ccb() requires environment: "production" or "sandbox" — the one the OAuth client was issued for. Credentials do not cross environments.');
1678
- }
1679
- if (options.mode !== "system" && options.mode !== "identity") {
1680
- throw new Error('ccb() requires mode: "system" (one church-wide grant an administrator approves) or "identity" (each person signs in as themselves).');
1681
- }
1682
- const subdomain = options.subdomain?.trim();
1683
- if (subdomain !== undefined && !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(subdomain)) {
1684
- throw new Error("ccb() subdomain must be the bare CCB subdomain, such as mychurch.");
1685
- }
1686
- const defaultPerPage = options.defaultPerPage ?? DEFAULT_PER_PAGE;
1687
- if (!PER_PAGE_VALUES.includes(defaultPerPage)) {
1688
- throw new Error("ccb() defaultPerPage must be 25, 50, 75, or 100 — the only page sizes CCB accepts.");
1689
- }
1690
- const scopes = resolveScopes(options);
1691
- const writable = scopes.some((scope) => !scope.startsWith("read:"));
1692
- const { environment, mode } = options;
1693
- const endpoints = CCB_ENVIRONMENTS[environment];
1694
- return api(id, {
1695
- title: options.title ?? defaultTitle(environment, mode, writable),
1696
- description: `Church Community Builder (Pushpay ChMS) — ${routingLine(environment, mode, writable)}: ${purpose}`,
1697
- // System Auth is one administrator's approval for the whole church, so
1698
- // the grant is the deployment's; a personal one would send every user to
1699
- // find a Master Administrator. Identity Auth acts as the person who
1700
- // signed in, so sharing it would let everyone act as whoever connected
1701
- // first. The mode decides, and there is no separate override to contradict it.
1702
- authScope: mode === "identity" ? "personal" : "shared",
1703
- oauth: {
1704
- authorizationEndpoint: endpoints.authorizationEndpoint,
1705
- tokenEndpoint: `${endpoints.apiOrigin}/oauth/token`,
1706
- clientId: options.clientId,
1707
- clientSecret: options.clientSecret,
1708
- scope: scopes.join(" "),
1709
- authorizationParams: {
1710
- ...(mode === "identity" ? { resource_owner_auth: "true" } : {}),
1711
- ...(subdomain ? { subdomain } : {}),
1712
- },
1713
- tokenRequestHeaders: { Accept: CCB_MEDIA_TYPE },
1714
- apiOrigins: [endpoints.apiOrigin],
1715
- },
1716
- usageGuide: {
1717
- content: usageGuide(purpose, environment, mode, scopes, writable, options.instructions),
1718
- summary: `${environment === "sandbox" ? "Sandbox" : "Production"} CCB, ${mode === "identity" ? "per person" : "church-wide"}: id resolution, sensitive data, paging, rate limits, hatches.`,
1719
- required: true,
1720
- },
1721
- callAdmission: options.callAdmission === undefined ? CCB_ADMISSION : options.callAdmission,
1722
- ...(options.maxResultBytes !== undefined ? { maxResultBytes: options.maxResultBytes } : {}),
1723
- tools: tools(ccbTransport(endpoints.apiOrigin), defaultPerPage, writable),
1724
- });
1725
- }