@zackbart/connecta 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/CHANGELOG.md +961 -5
  2. package/README.md +62 -39
  3. package/bin/connecta.mjs +110 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  12. package/dist/auth/clerk-sdk/client.js +5 -0
  13. package/dist/auth/clerk-sdk/generated.js +1845 -0
  14. package/dist/auth/clerk-transport.js +19 -0
  15. package/dist/auth/clerk.d.ts +17 -7
  16. package/dist/auth/clerk.js +189 -107
  17. package/dist/auth/cloudflare-access.js +25 -26
  18. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  19. package/dist/auth/downstream-client-metadata.js +133 -0
  20. package/dist/auth/downstream-oauth.js +1632 -1509
  21. package/dist/auth/downstream-revocation.js +106 -0
  22. package/dist/auth/oauth-token-response.js +231 -0
  23. package/dist/auth/oauth-v2-migration.js +116 -0
  24. package/dist/auth/static-oauth.js +526 -0
  25. package/dist/auth-elicitation.js +283 -0
  26. package/dist/branding.d.ts +8 -16
  27. package/dist/branding.js +26 -59
  28. package/dist/byte-read-response.js +39 -0
  29. package/dist/call-admission.js +12 -16
  30. package/dist/catalog-cache.js +739 -0
  31. package/dist/catalog-drift.d.ts +50 -15
  32. package/dist/catalog-drift.js +365 -128
  33. package/dist/catalog-fingerprint.js +2 -4
  34. package/dist/catalog-freshness.js +13 -0
  35. package/dist/catalog-limits.js +2 -2
  36. package/dist/catalog-service.js +368 -224
  37. package/dist/catalog.js +43 -84
  38. package/dist/concurrency.js +3 -4
  39. package/dist/config-defaults.d.ts +39 -0
  40. package/dist/config-defaults.js +46 -0
  41. package/dist/config-schema.d.ts +181 -0
  42. package/dist/config-schema.js +435 -0
  43. package/dist/config-value-sources.js +102 -0
  44. package/dist/config.d.ts +631 -0
  45. package/dist/config.js +503 -0
  46. package/dist/connector-access.d.ts +1 -0
  47. package/dist/connector-access.js +13 -15
  48. package/dist/connector-caller.d.ts +8 -0
  49. package/dist/connector-caller.js +15 -6
  50. package/dist/connector-scope.js +1 -1
  51. package/dist/connectors/api-connector.d.ts +220 -0
  52. package/dist/connectors/api-connector.js +214 -0
  53. package/dist/connectors/api.d.ts +8 -71
  54. package/dist/connectors/api.js +13 -99
  55. package/dist/connectors/guarded-fetch.d.ts +144 -0
  56. package/dist/connectors/guarded-fetch.js +108 -25
  57. package/dist/connectors/negotiation-cache.js +46 -0
  58. package/dist/connectors/option-shapes.js +27 -0
  59. package/dist/connectors/remote-mcp.d.ts +30 -4
  60. package/dist/connectors/remote-mcp.js +1780 -351
  61. package/dist/connectors/resource-uri.js +335 -0
  62. package/dist/credential-contract.d.ts +10 -0
  63. package/dist/credential-rules.js +1 -3
  64. package/dist/credential-url.js +108 -0
  65. package/dist/credentials.d.ts +6 -1
  66. package/dist/credentials.js +27 -18
  67. package/dist/d1.d.ts +130 -0
  68. package/dist/d1.js +290 -0
  69. package/dist/describe-config.d.ts +165 -0
  70. package/dist/describe-config.js +315 -0
  71. package/dist/described.js +106 -0
  72. package/dist/downstream-input-context.js +119 -0
  73. package/dist/downstream-input.js +323 -0
  74. package/dist/errors.d.ts +97 -20
  75. package/dist/errors.js +158 -80
  76. package/dist/execute.js +453 -305
  77. package/dist/executor-admission.d.ts +1 -0
  78. package/dist/executor-admission.js +11 -15
  79. package/dist/executor-contract.d.ts +20 -0
  80. package/dist/executor-contract.js +48 -0
  81. package/dist/executor-result.js +24 -9
  82. package/dist/executors/quickjs-child.js +3 -4
  83. package/dist/executors/quickjs-runtime.js +147 -59
  84. package/dist/executors/quickjs.js +29 -35
  85. package/dist/favicon.js +1 -2
  86. package/dist/fix-prompt.js +1 -3
  87. package/dist/guest-runtime.js +188 -0
  88. package/dist/guest-types.d.ts +176 -0
  89. package/dist/guest-types.js +1 -0
  90. package/dist/html-security.js +24 -0
  91. package/dist/identity.js +1 -3
  92. package/dist/inbound-credential.js +52 -0
  93. package/dist/index.d.ts +25 -257
  94. package/dist/index.js +65 -478
  95. package/dist/invocation-auth.js +69 -0
  96. package/dist/invocation.js +230 -73
  97. package/dist/json-schema.d.ts +1 -1
  98. package/dist/mcp-client-context.js +35 -0
  99. package/dist/mcp-result.js +11 -7
  100. package/dist/meta-output.js +76 -0
  101. package/dist/meta-tool-names.d.ts +2 -0
  102. package/dist/meta-tool-names.js +3 -0
  103. package/dist/meta-tools.js +285 -209
  104. package/dist/module-contracts.d.ts +8 -23
  105. package/dist/node.d.ts +0 -2
  106. package/dist/node.js +2 -2
  107. package/dist/oauth-callback-outcome.js +1 -3
  108. package/dist/oauth-handoff.js +117 -0
  109. package/dist/oauth-partition.js +17 -0
  110. package/dist/oauth-sealing.js +20 -0
  111. package/dist/operator-record.d.ts +137 -0
  112. package/dist/operator-record.js +420 -0
  113. package/dist/operator-ui/assets.js +31 -0
  114. package/dist/operator-ui/contract.d.ts +72 -0
  115. package/dist/operator-ui/contract.js +1 -0
  116. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  117. package/dist/operator-ui/generated.js +5 -4
  118. package/dist/operator-ui/model.d.ts +12 -87
  119. package/dist/operator-ui/model.js +2 -9
  120. package/dist/operator-ui/setup-commands.js +1 -3
  121. package/dist/operator-ui/view.d.ts +323 -0
  122. package/dist/operator-ui/view.js +62 -189
  123. package/dist/page-styles.js +2 -3
  124. package/dist/program-source.js +14 -2
  125. package/dist/{exempt-writes.js → program-writes.js} +55 -18
  126. package/dist/provider.d.ts +142 -0
  127. package/dist/provider.js +187 -0
  128. package/dist/providers/_shared/google/result-size.js +57 -0
  129. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  130. package/dist/providers/_shared/google/service-account.js +500 -0
  131. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  132. package/dist/providers/_shared/google/workspace.js +518 -0
  133. package/dist/providers/basecamp/index.d.ts +43 -0
  134. package/dist/providers/basecamp/index.js +997 -0
  135. package/dist/providers/basecamp/skill.generated.js +9 -0
  136. package/dist/providers/breeze/index.d.ts +34 -0
  137. package/dist/providers/breeze/index.js +1446 -0
  138. package/dist/providers/breeze/skill.generated.js +9 -0
  139. package/dist/providers/ccb/index.d.ts +103 -0
  140. package/dist/providers/ccb/index.js +1725 -0
  141. package/dist/providers/ccb/skill.generated.js +9 -0
  142. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  143. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  144. package/dist/providers/cloudflare/skill.generated.js +10 -0
  145. package/dist/providers/docs/index.d.ts +54 -0
  146. package/dist/providers/docs/index.js +1237 -0
  147. package/dist/providers/docs/skill.generated.js +9 -0
  148. package/dist/providers/drive/index.d.ts +53 -0
  149. package/dist/providers/drive/index.js +1523 -0
  150. package/dist/providers/drive/skill.generated.js +9 -0
  151. package/dist/providers/forms/index.d.ts +54 -0
  152. package/dist/providers/forms/index.js +1217 -0
  153. package/dist/providers/forms/skill.generated.js +11 -0
  154. package/dist/providers/github/auth.d.ts +32 -0
  155. package/dist/providers/github/auth.js +361 -0
  156. package/dist/providers/github/hosted.js +218 -0
  157. package/dist/providers/github/index.d.ts +13 -0
  158. package/dist/providers/github/index.js +140 -0
  159. package/dist/providers/github/key.js +100 -0
  160. package/dist/providers/github/rest.js +321 -0
  161. package/dist/providers/github/scope.d.ts +29 -0
  162. package/dist/providers/github/scope.js +124 -0
  163. package/dist/providers/github/skill.generated.js +8 -0
  164. package/dist/providers/gmail/index.d.ts +51 -0
  165. package/dist/providers/gmail/index.js +1436 -0
  166. package/dist/providers/gmail/skill.generated.js +9 -0
  167. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  168. package/dist/providers/linear/index.js +352 -0
  169. package/dist/providers/linear/skill.generated.js +8 -0
  170. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  171. package/dist/providers/mixpanel/index.js +416 -0
  172. package/dist/providers/mixpanel/skill.generated.js +9 -0
  173. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  174. package/dist/providers/{notion.js → notion/index.js} +226 -239
  175. package/dist/providers/notion/skill.generated.js +10 -0
  176. package/dist/providers/overflow/index.d.ts +60 -0
  177. package/dist/providers/overflow/index.js +1535 -0
  178. package/dist/providers/overflow/skill.generated.js +8 -0
  179. package/dist/providers/planning-center/index.d.ts +59 -0
  180. package/dist/providers/planning-center/index.js +2199 -0
  181. package/dist/providers/planning-center/skill.generated.js +10 -0
  182. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  183. package/dist/providers/revenuecat/index.js +557 -0
  184. package/dist/providers/revenuecat/skill.generated.js +11 -0
  185. package/dist/providers/sheets/index.d.ts +54 -0
  186. package/dist/providers/sheets/index.js +1237 -0
  187. package/dist/providers/sheets/skill.generated.js +10 -0
  188. package/dist/providers/slides/index.d.ts +53 -0
  189. package/dist/providers/slides/index.js +2645 -0
  190. package/dist/providers/slides/skill.generated.js +9 -0
  191. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  192. package/dist/providers/stripe/index.js +266 -0
  193. package/dist/providers/stripe/skill.generated.js +11 -0
  194. package/dist/providers/tithely/index.d.ts +49 -0
  195. package/dist/providers/tithely/index.js +1003 -0
  196. package/dist/providers/tithely/skill.generated.js +8 -0
  197. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  198. package/dist/providers/vercel/index.js +1038 -0
  199. package/dist/providers/vercel/mcp-ownership.js +212 -0
  200. package/dist/providers/vercel/skill.generated.js +11 -0
  201. package/dist/registry.d.ts +108 -171
  202. package/dist/registry.js +513 -925
  203. package/dist/request-state.js +21 -0
  204. package/dist/resource-read.js +100 -0
  205. package/dist/result-shapes.js +17 -49
  206. package/dist/retired-toolkits.js +1 -1
  207. package/dist/routes/access-tokens.js +3 -6
  208. package/dist/routes/activity-context.js +21 -0
  209. package/dist/routes/activity-disclosure.js +40 -0
  210. package/dist/routes/activity.js +59 -21
  211. package/dist/routes/actor-labels.js +3 -50
  212. package/dist/routes/connect.js +221 -0
  213. package/dist/routes/credentials.js +15 -27
  214. package/dist/routes/mcp.js +356 -130
  215. package/dist/routes/oauth-client-metadata.js +15 -0
  216. package/dist/routes/oauth-management.js +45 -107
  217. package/dist/routes/oauth.js +136 -57
  218. package/dist/routes/operator.js +3 -3
  219. package/dist/routes/shared.d.ts +25 -64
  220. package/dist/routes/shared.js +160 -80
  221. package/dist/routes/skills.js +30 -0
  222. package/dist/routes/ui-config.js +225 -0
  223. package/dist/routes/ui-data.js +40 -35
  224. package/dist/routes/ui.js +66 -56
  225. package/dist/runtime/admission.js +4 -2
  226. package/dist/runtime/call-admission.js +3 -1
  227. package/dist/runtime/connector-scope.js +1 -4
  228. package/dist/runtime/run.js +10 -39
  229. package/dist/runtime/services.js +12 -117
  230. package/dist/runtime/shared-read.js +1 -3
  231. package/dist/runtime/storage.js +8 -1
  232. package/dist/sent-secrets.d.ts +52 -0
  233. package/dist/sent-secrets.js +474 -0
  234. package/dist/server.js +28 -71
  235. package/dist/skills.js +406 -174
  236. package/dist/sqlite.d.ts +48 -0
  237. package/dist/sqlite.js +166 -0
  238. package/dist/storage/keys.js +428 -0
  239. package/dist/storage/memory.js +7 -0
  240. package/dist/storage/sql.d.ts +66 -0
  241. package/dist/storage/sql.js +534 -0
  242. package/dist/timeout.js +4 -2
  243. package/dist/tool-name.js +17 -0
  244. package/dist/tool-safety.d.ts +9 -35
  245. package/dist/tool-safety.js +20 -40
  246. package/dist/types.d.ts +320 -54
  247. package/dist/types.js +0 -1
  248. package/dist/ui.d.ts +7 -19
  249. package/dist/ui.js +76 -90
  250. package/dist/url-safety.js +3 -6
  251. package/dist/usage-guide.js +137 -0
  252. package/dist/validate.d.ts +7 -0
  253. package/dist/validate.js +309 -47
  254. package/dist/version.d.ts +1 -7
  255. package/dist/version.js +3 -7
  256. package/dist/worker.d.ts +11 -0
  257. package/dist/worker.js +434 -0
  258. package/documentation/architecture.md +665 -230
  259. package/documentation/auth.md +962 -458
  260. package/documentation/code-mode.md +501 -312
  261. package/documentation/meta-tools.md +449 -200
  262. package/documentation/operator-tests.md +93 -0
  263. package/documentation/provider-migration-0.29.md +60 -0
  264. package/examples/worker/AGENTS.md +34 -12
  265. package/examples/worker/README.md +286 -255
  266. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  267. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  268. package/examples/worker/src/connecta.config.ts +151 -0
  269. package/examples/worker/src/index.ts +14 -197
  270. package/examples/worker/wrangler.jsonc +15 -35
  271. package/package.json +125 -35
  272. package/templates/node/.dockerignore +1 -2
  273. package/templates/node/.env.example +20 -21
  274. package/templates/node/AGENTS.md +21 -17
  275. package/templates/node/Dockerfile +3 -5
  276. package/templates/node/README.md +133 -58
  277. package/templates/node/docker-compose.yml +11 -12
  278. package/templates/node/package.json +4 -2
  279. package/templates/node/src/connecta.config.ts +119 -0
  280. package/templates/node/src/index.ts +9 -154
  281. package/templates/node/src/provision-token.ts +17 -0
  282. package/AGENTS.md +0 -170
  283. package/dist/artifacts/connector.d.ts +0 -58
  284. package/dist/artifacts/connector.js +0 -730
  285. package/dist/artifacts/document.js +0 -86
  286. package/dist/artifacts/guide.js +0 -140
  287. package/dist/artifacts/html-scan.js +0 -329
  288. package/dist/artifacts/json.js +0 -78
  289. package/dist/artifacts/kv-store.d.ts +0 -20
  290. package/dist/artifacts/kv-store.js +0 -140
  291. package/dist/artifacts/markdown.js +0 -478
  292. package/dist/artifacts/module.d.ts +0 -37
  293. package/dist/artifacts/module.js +0 -66
  294. package/dist/artifacts/operations.d.ts +0 -197
  295. package/dist/artifacts/operations.js +0 -1002
  296. package/dist/artifacts/refresh.d.ts +0 -55
  297. package/dist/artifacts/refresh.js +0 -236
  298. package/dist/artifacts/routes.js +0 -206
  299. package/dist/artifacts/types.d.ts +0 -214
  300. package/dist/artifacts/types.js +0 -22
  301. package/dist/artifacts/validate.d.ts +0 -62
  302. package/dist/artifacts/validate.js +0 -651
  303. package/dist/artifacts.d.ts +0 -6
  304. package/dist/artifacts.js +0 -9
  305. package/dist/auth/bearer.d.ts +0 -11
  306. package/dist/auth/bearer.js +0 -50
  307. package/dist/providers/linear.js +0 -217
  308. package/dist/providers/mixpanel.js +0 -247
  309. package/dist/providers/revenuecat.js +0 -317
  310. package/dist/providers/stripe.js +0 -253
  311. package/dist/providers/vercel.js +0 -1253
  312. package/dist/storage/file.d.ts +0 -15
  313. package/dist/storage/file.js +0 -455
  314. package/ethos.md +0 -93
  315. package/examples/worker/src/cloudflare-kv.ts +0 -43
  316. package/examples/worker/src/d1-activity-row.ts +0 -155
  317. package/examples/worker/src/d1-activity.ts +0 -147
  318. package/examples/worker/src/d1-storage.ts +0 -138
  319. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  320. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,1535 @@
1
+ import { skill } from "./skill.generated.js";
2
+ /**
3
+ * Overflow (overflow.co) — church and nonprofit giving: cash, stock, crypto,
4
+ * and donor-advised-fund gifts, the deposits that settle them, the donors who
5
+ * gave them, and the recurring gifts that will.
6
+ *
7
+ * No SDK on purpose, and Overflow publishes none for this API anyway: v3 is
8
+ * header-authenticated JSON over thirty-eight operations, so `fetch` through
9
+ * the shared guarded transport keeps this subpath Workers-clean and adds
10
+ * nothing to install. Every tool below is hand-written. Overflow's OpenAPI
11
+ * document is drift evidence only — `src/providers/overflow/drift.json`
12
+ * records the operations the named tools touch, and
13
+ * `npm run providers:check -- --provider overflow` compares them with the
14
+ * published document without a credential. That document is served only by
15
+ * staging (`server.stage.overflow.co/api/docs/openapi.json`; production's
16
+ * equivalent answers 404), so the check reads staging's contract and assumes
17
+ * production runs the same v3 surface — Overflow documents one API at two
18
+ * base URLs, not two APIs.
19
+ *
20
+ * The surface is small enough to cover whole: named reads for the jobs a
21
+ * finance or ministry team actually asks about — what was given, what settled
22
+ * into the bank, who gave, what recurs, which campaign, and why a webhook
23
+ * missed — and two guarded hatches so every other operation stays reachable.
24
+ * Every write Overflow offers either moves money (charges, payment
25
+ * authorization, refunds, recurring-gift changes) or edits a donor's record,
26
+ * so none gets a named tool: each crosses `overflow_api_mutate`, which is
27
+ * destructive by declaration and lands on the host's approval prompt with the
28
+ * exact method, path, and body a human is being asked to allow. Overflow has
29
+ * no upload endpoint, so there is no upload hatch.
30
+ */
31
+ import { apiConnector as api, defined } from "../../connectors/api-connector.js";
32
+ import { guardedFetch, retryAfterMs, } from "../../connectors/guarded-fetch.js";
33
+ import { ConnectorCallError } from "../../errors.js";
34
+ import { keys, optionsOf } from "../../config-schema.js";
35
+ import { PROVIDER_COMMON } from "../../connectors/option-shapes.js";
36
+ import { asProviderFactory } from "../../provider.js";
37
+ /**
38
+ * Overflow's two published environments. They are separate deployments with
39
+ * separate credentials: a staging key works only against staging and a
40
+ * production key only against production.
41
+ */
42
+ export const OVERFLOW_API_BASE_URLS = {
43
+ production: "https://server.overflow.co",
44
+ staging: "https://server.stage.overflow.co",
45
+ };
46
+ /** Every v3 operation lives below this path on either origin. */
47
+ const API_PATH = "/api/v3";
48
+ /** Overflow's cap on `limit` for every page-numbered collection. */
49
+ const MAX_PAGE_SIZE = 100;
50
+ /** Overflow's own default; already a quarter of the cap, so a first read is cheap. */
51
+ const DEFAULT_PAGE_SIZE = 25;
52
+ /** Webhook event logs page by cursor and cap `limit` lower, at 50. */
53
+ const MAX_EVENT_LOG_PAGE_SIZE = 50;
54
+ const DEFAULT_EVENT_LOG_PAGE_SIZE = 25;
55
+ /**
56
+ * A ceiling on absurdity, not a budget. A full page of 100 contributions with
57
+ * their embedded donor blocks is a few hundred kilobytes; anything near four
58
+ * mebibytes is a proxy misbehaving.
59
+ */
60
+ const OVERFLOW_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
61
+ /** Overflow's documented fixed window: 120 requests per 60 seconds per client. */
62
+ const RATE_LIMIT_WINDOW_MS = 60_000;
63
+ /**
64
+ * Overflow documents 120 requests per minute per API client, metered in a
65
+ * fixed 60-second window that opens on the first request
66
+ * (docs.overflow.co/api-reference/rate-limiting). A rolling 60-second window
67
+ * of 120 calls is the conservative reading of that: no 60-second span it
68
+ * admits can exceed the fixed window's allowance. It is still a per-runtime
69
+ * approximation, not an enforcement — every isolate or process keeps its own
70
+ * counter, and any other integration using the same client id spends the same
71
+ * allowance without this counter seeing it. `maxConcurrency: 4` is connecta's
72
+ * choice rather than Overflow's: an averaged budget cannot stop one
73
+ * `execute_code` program from firing a whole minute's allowance in one tick,
74
+ * and the concurrency bound is what keeps a fan-out from tripping the 429.
75
+ */
76
+ const OVERFLOW_ADMISSION = {
77
+ rules: [
78
+ {
79
+ maxConcurrency: 4,
80
+ budget: { kind: "rolling-window", maxCalls: 120, windowMs: RATE_LIMIT_WINDOW_MS },
81
+ maxQueueSize: 32,
82
+ queueTimeoutMs: 5_000,
83
+ retryAfterMs: 1_000,
84
+ },
85
+ ],
86
+ };
87
+ function asRecord(value) {
88
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
89
+ }
90
+ function asArray(value) {
91
+ return Array.isArray(value) ? value : [];
92
+ }
93
+ function compact(value) {
94
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
95
+ }
96
+ /** A `{ id, name }` reference, or undefined when Overflow sent none. */
97
+ function reference(value) {
98
+ const record = asRecord(value);
99
+ return Object.keys(record).length === 0 ? undefined : compact({ id: record["id"], name: record["name"] });
100
+ }
101
+ // --- Credentials and transport ----------------------------------------------
102
+ const API_KEY_CREDENTIAL = {
103
+ label: "Overflow API credentials",
104
+ description: "The client id and API key Overflow issued for this environment (Overflow dashboard, or Overflow's developer team). Staging and production credentials are separate and not interchangeable. Server-side only: connecta sends them to Overflow's API and nowhere else.",
105
+ fields: [
106
+ {
107
+ name: "clientId",
108
+ label: "Client ID",
109
+ description: "Overflow's public client identifier, sent as x-client-id.",
110
+ placeholder: "c77c8d57-7e1c-4a7e-ad46-e756e5f8dabd",
111
+ inputType: "text",
112
+ },
113
+ {
114
+ name: "apiKey",
115
+ label: "API key",
116
+ description: "The secret key paired with that client id, sent as x-api-key.",
117
+ placeholder: "Paste Overflow API key",
118
+ inputType: "password",
119
+ },
120
+ ],
121
+ };
122
+ /**
123
+ * One credential source today: the operator's client id and API key. This is
124
+ * the only place that knows what proves identity to Overflow, so an OAuth
125
+ * bearer source — if Overflow ever offers one and `api()` grows the slot — is
126
+ * a second function selected at construction, not a change to any tool.
127
+ */
128
+ async function apiKeyHeaders(ctx) {
129
+ const values = await ctx.credential?.getAll();
130
+ const clientId = values?.["clientId"]?.trim();
131
+ const apiKey = values?.["apiKey"]?.trim();
132
+ if (!clientId || !apiKey) {
133
+ throw new ConnectorCallError("auth_required", "No Overflow client id and API key are configured for this connector. Call authorize_connector for recovery options; an operator adds both fields in this connection in the operator UI.");
134
+ }
135
+ return { "x-client-id": clientId, "x-api-key": apiKey };
136
+ }
137
+ function overflowTransport(origin) {
138
+ return guardedFetch({
139
+ provider: "Overflow",
140
+ baseUrl: `${origin.replace(/\/+$/, "")}${API_PATH}`,
141
+ headers: { Accept: "application/json" },
142
+ maxResponseBytes: OVERFLOW_MAX_RESPONSE_BYTES,
143
+ authenticate: apiKeyHeaders,
144
+ });
145
+ }
146
+ // --- Failures ----------------------------------------------------------------
147
+ /** NestJS framing: `{ statusCode, message: string | string[], error }`, or `{ error }`. */
148
+ function detailFor(payload, status) {
149
+ const root = asRecord(payload);
150
+ const message = root["message"];
151
+ if (typeof message === "string" && message.trim())
152
+ return message.trim();
153
+ if (Array.isArray(message)) {
154
+ const lines = message.filter((line) => typeof line === "string" && line.trim() !== "");
155
+ if (lines.length > 0)
156
+ return lines.join("; ");
157
+ }
158
+ if (typeof root["error"] === "string" && root["error"].trim()) {
159
+ return root["error"].trim();
160
+ }
161
+ if (typeof payload === "string" && payload.trim()) {
162
+ return payload.trim().slice(0, 300);
163
+ }
164
+ return `Overflow returned HTTP ${status}.`;
165
+ }
166
+ /**
167
+ * Overflow's own wait: `Retry-After` if a proxy supplies one, otherwise
168
+ * `x-ratelimit-reset`, which Overflow documents as the seconds left in the
169
+ * current window (a delta, not an epoch).
170
+ */
171
+ function windowResetMs(headers) {
172
+ const retryAfter = retryAfterMs(headers);
173
+ if (retryAfter !== undefined)
174
+ return retryAfter;
175
+ const raw = headers.get("x-ratelimit-reset");
176
+ if (!raw)
177
+ return undefined;
178
+ const seconds = Number(raw.trim());
179
+ return Number.isFinite(seconds) && seconds >= 0 ? Math.trunc(seconds * 1_000) : undefined;
180
+ }
181
+ /**
182
+ * Map an Overflow failure by what the caller does next (H11).
183
+ *
184
+ * Three readings here are Overflow-specific. A bad client id or key answers
185
+ * 403 "Forbidden resource" (observed against staging; the docs table says
186
+ * 401), and so does an attempt to edit a donor profile that a donor account
187
+ * owns — the status cannot tell those apart, so a write's message says so.
188
+ * A 404 is an absence: credentials are per nonprofit and Overflow scopes every
189
+ * read to it, so a missing id is missing from this nonprofit's view and the
190
+ * next move is to re-resolve it. And a 5xx on a write is never retryable from
191
+ * here: Overflow's own contract returns 500 from `POST /contributions` after
192
+ * work has started, so the charge may exist, and a blind retry could charge a
193
+ * donor twice.
194
+ */
195
+ function overflowFailure(status, headers, payload, method) {
196
+ const detail = detailFor(payload, status);
197
+ if (status === 429) {
198
+ return new ConnectorCallError("rate_limited", `Overflow rate limit reached (HTTP 429): ${detail} Overflow allows 120 requests per minute per API client in a fixed 60-second window, shared by every integration using this client id.`, { retryAfterMs: windowResetMs(headers) ?? RATE_LIMIT_WINDOW_MS });
199
+ }
200
+ if (status === 401 || status === 403) {
201
+ const ambiguity = method !== "GET" && status === 403
202
+ ? " Overflow also answers 403 when a donor profile owned by a donor account cannot be edited through the API; if reads succeed with this credential, that refusal is the cause and no credential change will fix it."
203
+ : "";
204
+ return new ConnectorCallError(status === 403 ? "provider_permission_denied" : "auth_required", `Overflow rejected the request (HTTP ${status}): ${detail} The client id and API key may be wrong, revoked, or issued for the other environment (staging and production credentials are not interchangeable); an operator must check them and ask the account administrator to grant resource access if they are valid.${ambiguity}`);
205
+ }
206
+ if (status === 404) {
207
+ return new ConnectorCallError("not_found", `Overflow found no such resource for this nonprofit (HTTP 404): ${detail} Re-resolve the id with its list tool; for a raw call, confirm the path below /api/v3.`);
208
+ }
209
+ if (status === 400) {
210
+ return new ConnectorCallError("invalid_args", `Overflow rejected the request (HTTP 400): ${detail}`);
211
+ }
212
+ if (status === 409 || status === 422) {
213
+ return new ConnectorCallError("connector_call_failed", `Overflow refused the operation (HTTP ${status}): ${detail} Repeating the same call will not change the outcome.`, { retryable: false });
214
+ }
215
+ if (status >= 500) {
216
+ if (method !== "GET") {
217
+ return new ConnectorCallError("connector_call_failed", `Overflow failed while processing a write (HTTP ${status}): ${detail} The write may or may not have taken effect. Read the affected contribution, donor, or recurring gift before retrying; repeating a charge, refund, or subscription change can apply it twice.`, { retryable: false });
218
+ }
219
+ const wait = windowResetMs(headers);
220
+ return new ConnectorCallError("unavailable", `Overflow is failing upstream (HTTP ${status}): ${detail}`, wait === undefined ? {} : { retryAfterMs: wait });
221
+ }
222
+ return new ConnectorCallError("connector_call_failed", `Overflow request failed (HTTP ${status}): ${detail}`, {
223
+ retryable: false,
224
+ });
225
+ }
226
+ function parseBody(text) {
227
+ if (!text.trim())
228
+ return undefined;
229
+ try {
230
+ return JSON.parse(text);
231
+ }
232
+ catch {
233
+ return text;
234
+ }
235
+ }
236
+ /**
237
+ * Send one request. Named tools read strictly — a successful response that is
238
+ * not JSON is a malformed answer — while the hatches return Overflow's body
239
+ * as it arrived, text included (`/health` answers `ok`).
240
+ */
241
+ async function callOverflow(send, request, ctx, mode = "strict") {
242
+ return await send(request, ctx, async (response) => {
243
+ const text = await response.text();
244
+ if (!response.ok) {
245
+ throw overflowFailure(response.status, response.headers, parseBody(text), request.method);
246
+ }
247
+ const body = parseBody(text);
248
+ if (mode === "strict" && typeof body === "string") {
249
+ throw new ConnectorCallError("connector_call_failed", "Overflow returned a non-JSON body for a successful read.", { retryable: false });
250
+ }
251
+ return body;
252
+ });
253
+ }
254
+ function segment(value) {
255
+ return encodeURIComponent(String(value));
256
+ }
257
+ /**
258
+ * Overflow pages by number and reports `totalCount`, so "is there another
259
+ * page?" is arithmetic over what was asked for. A response with no usable
260
+ * count reports `hasMore` from whether the page came back full — honest in
261
+ * the common case, and never a confident `false` on a page that was full.
262
+ */
263
+ function pageInfo(payload, page, limit, rows) {
264
+ const total = asRecord(payload)["totalCount"];
265
+ if (typeof total === "number" && Number.isFinite(total)) {
266
+ return { page, limit, totalCount: total, hasMore: page * limit < total };
267
+ }
268
+ return { page, limit, totalCount: (page - 1) * limit + rows, hasMore: rows >= limit };
269
+ }
270
+ // --- Projections -------------------------------------------------------------
271
+ /**
272
+ * The donor as it rides inside a contribution: identity only. Overflow embeds
273
+ * the whole profile — email, phone, mailing address, giving aggregates — in
274
+ * every contribution row; `get_donor` is where that belongs.
275
+ */
276
+ function projectDonorReference(value) {
277
+ const donor = asRecord(value);
278
+ if (Object.keys(donor).length === 0)
279
+ return undefined;
280
+ return compact({
281
+ id: donor["id"],
282
+ firstName: donor["firstName"],
283
+ lastName: donor["lastName"],
284
+ });
285
+ }
286
+ function projectContribution(value) {
287
+ const contribution = asRecord(value);
288
+ const paymentMethod = asRecord(contribution["paymentMethod"]);
289
+ const stocks = asRecord(contribution["stocks"]);
290
+ const crypto = asRecord(contribution["crypto"]);
291
+ return compact({
292
+ id: contribution["id"],
293
+ type: contribution["type"],
294
+ status: contribution["status"],
295
+ amount: contribution["amount"],
296
+ frequency: contribution["frequency"] ?? undefined,
297
+ contributionDate: contribution["contributionDate"],
298
+ donor: projectDonorReference(contribution["donor"]),
299
+ anonymous: contribution["anonymous"],
300
+ donorCoveredFees: contribution["donorCoveredFees"],
301
+ campaign: reference(contribution["campaign"]),
302
+ subcampaign: reference(contribution["subcampaign"]),
303
+ locationId: contribution["locationId"],
304
+ depositId: contribution["depositId"] ?? undefined,
305
+ subscriptionId: contribution["subscriptionId"],
306
+ givingLinkId: contribution["givingLinkId"] ?? undefined,
307
+ pledgeId: contribution["pledgeId"] ?? undefined,
308
+ paymentMethod: Object.keys(paymentMethod).length === 0
309
+ ? undefined
310
+ : compact({ type: paymentMethod["type"], last4: paymentMethod["last4"] }),
311
+ stocks: Object.keys(stocks).length === 0
312
+ ? undefined
313
+ : compact({ quantity: stocks["quantity"], tickers: stocks["tickers"] }),
314
+ crypto: Object.keys(crypto).length === 0 ? undefined : compact({ quantity: crypto["quantity"], token: crypto["token"] }),
315
+ dedication: contribution["dedication"] ?? undefined,
316
+ donorNotes: contribution["donorNotes"] ?? undefined,
317
+ metadata: contribution["metadata"] ?? undefined,
318
+ createdAt: contribution["createdAt"],
319
+ updatedAt: contribution["updatedAt"],
320
+ });
321
+ }
322
+ function projectDonor(value, full) {
323
+ const donor = asRecord(value);
324
+ return compact({
325
+ id: donor["id"],
326
+ firstName: donor["firstName"],
327
+ lastName: donor["lastName"],
328
+ email: donor["email"],
329
+ phone: full ? donor["phone"] : undefined,
330
+ address: full ? (donor["address"] ?? undefined) : undefined,
331
+ locationIds: donor["locationIds"],
332
+ totalContributionsCount: donor["totalContributionsCount"],
333
+ activeRecurringCount: donor["activeRecurringCount"],
334
+ latestContributionDate: donor["latestContributionDate"],
335
+ createdAt: donor["createdAt"],
336
+ updatedAt: donor["updatedAt"],
337
+ });
338
+ }
339
+ function projectDeposit(value, withLineItems) {
340
+ const deposit = asRecord(value);
341
+ return compact({
342
+ id: deposit["id"],
343
+ status: deposit["status"],
344
+ type: deposit["type"],
345
+ amountInCents: deposit["amountInCents"],
346
+ arrivalAt: deposit["arrivalAt"],
347
+ name: deposit["name"] ?? undefined,
348
+ bankName: deposit["bankName"] ?? undefined,
349
+ bankLast4: deposit["bankLast4"] ?? undefined,
350
+ statementDescriptor: deposit["statementDescriptor"] ?? undefined,
351
+ paymentMethodTypes: deposit["paymentMethodType"],
352
+ startingBalanceInCents: deposit["startingBalanceInCents"] ?? undefined,
353
+ endingBalanceInCents: deposit["endingBalanceInCents"] ?? undefined,
354
+ reconciledAt: deposit["reconciledAt"] ?? null,
355
+ createdAt: deposit["createdAt"],
356
+ updatedAt: deposit["updatedAt"],
357
+ lineItems: withLineItems
358
+ ? asArray(deposit["lineItems"]).map((row) => {
359
+ const item = asRecord(row);
360
+ return compact({
361
+ type: item["type"],
362
+ grossValueInCents: item["grossValueInCents"],
363
+ grossFeeValueInCents: item["grossFeeValueInCents"],
364
+ referenceId: item["referenceId"],
365
+ description: item["description"],
366
+ });
367
+ })
368
+ : undefined,
369
+ });
370
+ }
371
+ function projectRefund(value) {
372
+ const refund = asRecord(value);
373
+ return compact({
374
+ id: refund["id"],
375
+ contributionId: refund["contributionId"],
376
+ status: refund["status"],
377
+ reason: refund["reason"] ?? undefined,
378
+ amountInCents: refund["amountInCents"],
379
+ netValueInCents: refund["netValueInCents"],
380
+ feeValueInCents: refund["feeValueInCents"] ?? undefined,
381
+ depositId: refund["depositId"] ?? undefined,
382
+ createdAt: refund["createdAt"],
383
+ updatedAt: refund["updatedAt"],
384
+ });
385
+ }
386
+ function projectChargeback(value) {
387
+ const chargeback = asRecord(value);
388
+ return compact({
389
+ id: chargeback["id"],
390
+ contributionId: chargeback["contributionId"],
391
+ status: chargeback["status"],
392
+ type: chargeback["type"],
393
+ reason: chargeback["reason"] ?? undefined,
394
+ amountInCents: chargeback["amountInCents"],
395
+ netValueInCents: chargeback["netValueInCents"] ?? undefined,
396
+ feeValueInCents: chargeback["feeValueInCents"] ?? undefined,
397
+ depositId: chargeback["depositId"] ?? undefined,
398
+ createdAt: chargeback["createdAt"],
399
+ updatedAt: chargeback["updatedAt"],
400
+ });
401
+ }
402
+ function projectSubscription(value) {
403
+ const subscription = asRecord(value);
404
+ const paymentMethod = asRecord(subscription["paymentMethod"]);
405
+ return compact({
406
+ id: subscription["id"],
407
+ donorId: subscription["donorId"],
408
+ status: subscription["status"],
409
+ amount: subscription["amount"],
410
+ frequency: subscription["frequency"],
411
+ startDate: subscription["startDate"],
412
+ nextPaymentDate: subscription["nextPaymentDate"] ?? null,
413
+ anonymous: subscription["anonymous"],
414
+ donorCoveredFees: subscription["donorCoveredFees"],
415
+ campaign: reference(subscription["campaign"]),
416
+ subcampaign: reference(subscription["subcampaign"]),
417
+ locationId: subscription["locationId"] ?? undefined,
418
+ paymentMethod: Object.keys(paymentMethod).length === 0
419
+ ? undefined
420
+ : compact({
421
+ id: paymentMethod["id"],
422
+ type: paymentMethod["type"],
423
+ last4: paymentMethod["last4"],
424
+ expiration: paymentMethod["expiration"],
425
+ }),
426
+ metadata: subscription["metadata"] ?? undefined,
427
+ createdAt: subscription["createdAt"],
428
+ updatedAt: subscription["updatedAt"],
429
+ });
430
+ }
431
+ function projectPaymentMethod(value) {
432
+ const method = asRecord(value);
433
+ return compact({
434
+ id: method["id"],
435
+ type: method["type"],
436
+ last4: method["last4"],
437
+ expirationDate: method["expirationDate"],
438
+ isExpired: method["isExpired"],
439
+ updatedAt: method["updatedAt"],
440
+ });
441
+ }
442
+ function projectCampaign(value) {
443
+ const campaign = asRecord(value);
444
+ const refs = (list) => {
445
+ const rows = asArray(list)
446
+ .map(reference)
447
+ .filter((row) => row !== undefined);
448
+ return rows.length === 0 ? undefined : rows;
449
+ };
450
+ return compact({
451
+ id: campaign["id"],
452
+ name: campaign["name"],
453
+ status: campaign["status"],
454
+ isSubcampaign: campaign["isSubcampaign"],
455
+ isActive: campaign["isActive"],
456
+ startDate: campaign["startDate"],
457
+ endDate: campaign["endDate"] ?? null,
458
+ hideFromDonors: campaign["hideFromDonors"],
459
+ archivedAt: campaign["archivedAt"] ?? undefined,
460
+ locationIds: campaign["locationIds"],
461
+ parentCampaigns: refs(campaign["parentCampaigns"]),
462
+ subcampaigns: refs(campaign["subcampaigns"]),
463
+ totalContributionCount: campaign["totalContributionCount"],
464
+ totalContributionValue: campaign["totalContributionValue"],
465
+ uniqueDonorCount: campaign["uniqueDonorCount"],
466
+ });
467
+ }
468
+ function projectLocation(value) {
469
+ const location = asRecord(value);
470
+ return compact({
471
+ id: location["id"],
472
+ name: location["name"],
473
+ isDefaultLocation: location["isDefaultLocation"],
474
+ hideFromDonors: location["hideFromDonors"],
475
+ archivedAt: location["archivedAt"] ?? null,
476
+ createdAt: location["createdAt"],
477
+ updatedAt: location["updatedAt"],
478
+ });
479
+ }
480
+ function projectTapEvent(value) {
481
+ const event = asRecord(value);
482
+ const device = asRecord(event["device"]);
483
+ const group = asRecord(event["group"]);
484
+ const destination = asRecord(event["destination"]);
485
+ return compact({
486
+ id: event["id"],
487
+ createdAt: event["createdAt"],
488
+ deviceId: event["deviceId"],
489
+ deviceSerialNumber: device["serialNumber"],
490
+ groupId: event["groupId"],
491
+ groupName: group["name"],
492
+ destinationId: event["destinationId"],
493
+ destinationName: destination["name"],
494
+ destinationType: destination["type"] ?? undefined,
495
+ });
496
+ }
497
+ function projectWebhook(value) {
498
+ const webhook = asRecord(value);
499
+ return compact({
500
+ id: webhook["id"],
501
+ name: webhook["name"],
502
+ description: webhook["description"] ?? undefined,
503
+ destinationUrl: webhook["destinationUrl"],
504
+ enabledEvents: webhook["enabledEvents"],
505
+ status: webhook["status"],
506
+ createdAt: webhook["createdAt"],
507
+ updatedAt: webhook["updatedAt"],
508
+ });
509
+ }
510
+ /**
511
+ * Deliberately drops `request.body` and `response.body`. The request body is
512
+ * the event payload Overflow delivered — donor names, emails, and amounts —
513
+ * and the response body is whatever the receiving server echoed. Diagnosing a
514
+ * missed delivery needs the status, the code, and the error, not the payload.
515
+ */
516
+ function projectEventLog(value) {
517
+ const log = asRecord(value);
518
+ const request = asRecord(log["request"]);
519
+ const response = asRecord(log["response"]);
520
+ return compact({
521
+ id: log["id"],
522
+ eventId: log["eventId"],
523
+ webhookEventName: log["webhookEventName"],
524
+ status: log["status"],
525
+ attemptNumber: log["attemptNumber"],
526
+ deliverySource: log["deliverySource"],
527
+ originatedAt: log["originatedAt"],
528
+ attemptedAt: log["attemptedAt"],
529
+ url: request["url"],
530
+ responseCode: response["code"],
531
+ responseDurationMs: response["durationMs"],
532
+ errorMessage: response["errorMessage"],
533
+ });
534
+ }
535
+ // --- Schema fragments --------------------------------------------------------
536
+ function namedInput(properties, required = []) {
537
+ return { type: "object", properties, required, additionalProperties: false };
538
+ }
539
+ const RAW_PROPERTY = {
540
+ type: "boolean",
541
+ description: "Return Overflow's untouched rows instead of the lean projection.",
542
+ };
543
+ const OBJECT_ID = "^[0-9a-fA-F]{24}$";
544
+ function idProperty(source) {
545
+ return {
546
+ type: "string",
547
+ pattern: OBJECT_ID,
548
+ description: `24-hex id from ${source}.`,
549
+ };
550
+ }
551
+ const DATE_PROPERTY = (meaning) => ({
552
+ type: "string",
553
+ pattern: "^\\d{4}-\\d{2}-\\d{2}",
554
+ description: `${meaning}; ISO date or date-time.`,
555
+ });
556
+ const SORT_DIRECTION = {
557
+ type: "string",
558
+ enum: ["ASC", "DESC"],
559
+ description: "Sort direction.",
560
+ };
561
+ const ID_LIST = (source) => ({
562
+ type: "array",
563
+ minItems: 1,
564
+ items: { type: "string", pattern: OBJECT_ID, description: `Id from ${source}.` },
565
+ description: `Ids from ${source}; matches any.`,
566
+ });
567
+ const PAGE_PROPERTY = {
568
+ type: "integer",
569
+ minimum: 1,
570
+ description: "1-based page number. Defaults to 1.",
571
+ };
572
+ function limitProperty(defaultPageSize) {
573
+ return {
574
+ type: "integer",
575
+ minimum: 1,
576
+ maximum: MAX_PAGE_SIZE,
577
+ description: `Rows per page, up to Overflow's cap of ${MAX_PAGE_SIZE}. Defaults to ${defaultPageSize}.`,
578
+ };
579
+ }
580
+ const PAGE_OUTPUT = {
581
+ type: "object",
582
+ properties: {
583
+ page: { type: "integer" },
584
+ limit: { type: "integer" },
585
+ totalCount: { type: "integer" },
586
+ hasMore: { type: "boolean", description: "True when page + 1 exists." },
587
+ },
588
+ required: ["page", "limit", "totalCount", "hasMore"],
589
+ };
590
+ function listOutput(key, item) {
591
+ return {
592
+ type: "object",
593
+ properties: { [key]: { type: "array", items: item }, page: PAGE_OUTPUT },
594
+ required: [key, "page"],
595
+ };
596
+ }
597
+ const REFERENCE = {
598
+ type: "object",
599
+ properties: { id: { type: "string" }, name: { type: "string" } },
600
+ };
601
+ const CONTRIBUTION_SCHEMA = {
602
+ type: "object",
603
+ properties: {
604
+ id: { type: "string" },
605
+ type: { type: "string" },
606
+ status: { type: "string" },
607
+ amount: { type: "number" },
608
+ frequency: { type: "string" },
609
+ contributionDate: { type: "string" },
610
+ donor: { type: "object" },
611
+ campaign: REFERENCE,
612
+ subcampaign: REFERENCE,
613
+ locationId: { type: "string" },
614
+ depositId: { type: "string" },
615
+ subscriptionId: { type: "string" },
616
+ paymentMethod: { type: "object" },
617
+ stocks: { type: "object" },
618
+ crypto: { type: "object" },
619
+ metadata: { type: "object" },
620
+ createdAt: { type: "string" },
621
+ updatedAt: { type: "string" },
622
+ },
623
+ required: ["id", "type", "status", "amount"],
624
+ };
625
+ const DONOR_SCHEMA = {
626
+ type: "object",
627
+ properties: {
628
+ id: { type: "string" },
629
+ firstName: { type: "string" },
630
+ lastName: { type: "string" },
631
+ email: { type: "string" },
632
+ phone: { type: "string" },
633
+ address: { type: "object" },
634
+ locationIds: { type: "array", items: { type: "string" } },
635
+ totalContributionsCount: { type: "number" },
636
+ activeRecurringCount: { type: "number" },
637
+ latestContributionDate: { type: "string" },
638
+ createdAt: { type: "string" },
639
+ updatedAt: { type: "string" },
640
+ },
641
+ required: ["id"],
642
+ };
643
+ const DEPOSIT_SCHEMA = {
644
+ type: "object",
645
+ properties: {
646
+ id: { type: "string" },
647
+ status: { type: "string" },
648
+ type: { type: "string" },
649
+ amountInCents: { type: "number" },
650
+ arrivalAt: { type: "string" },
651
+ name: { type: "string" },
652
+ bankName: { type: "string" },
653
+ bankLast4: { type: "string" },
654
+ paymentMethodTypes: { type: "array", items: { type: "string" } },
655
+ reconciledAt: { type: ["string", "null"] },
656
+ lineItems: { type: "array", items: { type: "object" } },
657
+ createdAt: { type: "string" },
658
+ updatedAt: { type: "string" },
659
+ },
660
+ required: ["id", "status", "amountInCents"],
661
+ };
662
+ const MONEY_ROLLUP = {
663
+ type: "object",
664
+ properties: {
665
+ count: { type: "number" },
666
+ grossInCents: { type: "number" },
667
+ feesInCents: { type: "number" },
668
+ totalInCents: { type: "number" },
669
+ },
670
+ };
671
+ const REFUND_SCHEMA = {
672
+ type: "object",
673
+ properties: {
674
+ id: { type: "string" },
675
+ contributionId: { type: "string" },
676
+ status: { type: "string" },
677
+ reason: { type: "string" },
678
+ amountInCents: { type: "number" },
679
+ netValueInCents: { type: "number" },
680
+ feeValueInCents: { type: "number" },
681
+ depositId: { type: "string" },
682
+ createdAt: { type: "string" },
683
+ updatedAt: { type: "string" },
684
+ },
685
+ required: ["id", "contributionId", "status", "amountInCents"],
686
+ };
687
+ const CHARGEBACK_SCHEMA = {
688
+ type: "object",
689
+ properties: {
690
+ id: { type: "string" },
691
+ contributionId: { type: "string" },
692
+ status: { type: "string" },
693
+ type: { type: "string" },
694
+ reason: { type: "string" },
695
+ amountInCents: { type: "number" },
696
+ netValueInCents: { type: "number" },
697
+ depositId: { type: "string" },
698
+ createdAt: { type: "string" },
699
+ updatedAt: { type: "string" },
700
+ },
701
+ required: ["id", "contributionId", "status", "amountInCents"],
702
+ };
703
+ const SUBSCRIPTION_SCHEMA = {
704
+ type: "object",
705
+ properties: {
706
+ id: { type: "string" },
707
+ donorId: { type: "string" },
708
+ status: { type: "string" },
709
+ amount: { type: "number" },
710
+ frequency: { type: "string" },
711
+ startDate: { type: "string" },
712
+ nextPaymentDate: { type: ["string", "null"] },
713
+ campaign: REFERENCE,
714
+ subcampaign: REFERENCE,
715
+ locationId: { type: "string" },
716
+ paymentMethod: { type: "object" },
717
+ createdAt: { type: "string" },
718
+ updatedAt: { type: "string" },
719
+ },
720
+ required: ["id", "donorId", "status", "amount", "frequency"],
721
+ };
722
+ const PAYMENT_METHOD_SCHEMA = {
723
+ type: "object",
724
+ properties: {
725
+ id: { type: "string" },
726
+ type: { type: "string" },
727
+ last4: { type: "string" },
728
+ expirationDate: { type: "string" },
729
+ isExpired: { type: "boolean" },
730
+ updatedAt: { type: "string" },
731
+ },
732
+ required: ["id", "type", "last4"],
733
+ };
734
+ const CAMPAIGN_SCHEMA = {
735
+ type: "object",
736
+ properties: {
737
+ id: { type: "string" },
738
+ name: { type: "string" },
739
+ status: { type: "string" },
740
+ isSubcampaign: { type: "boolean" },
741
+ isActive: { type: "boolean" },
742
+ startDate: { type: "string" },
743
+ endDate: { type: ["string", "null"] },
744
+ locationIds: { type: "array", items: { type: "string" } },
745
+ parentCampaigns: { type: "array", items: REFERENCE },
746
+ subcampaigns: { type: "array", items: REFERENCE },
747
+ totalContributionCount: { type: "number" },
748
+ totalContributionValue: { type: "number" },
749
+ uniqueDonorCount: { type: "number" },
750
+ },
751
+ required: ["id", "name", "status"],
752
+ };
753
+ const LOCATION_SCHEMA = {
754
+ type: "object",
755
+ properties: {
756
+ id: { type: "string" },
757
+ name: { type: "string" },
758
+ isDefaultLocation: { type: "boolean" },
759
+ hideFromDonors: { type: "boolean" },
760
+ archivedAt: { type: ["string", "null"] },
761
+ createdAt: { type: "string" },
762
+ },
763
+ required: ["id", "name"],
764
+ };
765
+ const TAP_EVENT_SCHEMA = {
766
+ type: "object",
767
+ properties: {
768
+ id: { type: "string" },
769
+ createdAt: { type: "string" },
770
+ deviceId: { type: "string" },
771
+ deviceSerialNumber: { type: "number" },
772
+ groupId: { type: "string" },
773
+ groupName: { type: "string" },
774
+ destinationId: { type: "string" },
775
+ destinationName: { type: "string" },
776
+ destinationType: { type: "string" },
777
+ },
778
+ required: ["id", "createdAt"],
779
+ };
780
+ const WEBHOOK_SCHEMA = {
781
+ type: "object",
782
+ properties: {
783
+ id: { type: "string" },
784
+ name: { type: "string" },
785
+ destinationUrl: { type: "string" },
786
+ enabledEvents: { type: "array", items: { type: "string" } },
787
+ status: { type: "string" },
788
+ createdAt: { type: "string" },
789
+ },
790
+ required: ["id", "name", "status"],
791
+ };
792
+ const EVENT_LOG_SCHEMA = {
793
+ type: "object",
794
+ properties: {
795
+ id: { type: "string" },
796
+ eventId: { type: "string" },
797
+ webhookEventName: { type: "string" },
798
+ status: { type: "string" },
799
+ attemptNumber: { type: "number" },
800
+ attemptedAt: { type: "string" },
801
+ url: { type: "string" },
802
+ responseCode: { type: "number" },
803
+ errorMessage: { type: "string" },
804
+ },
805
+ required: ["id", "status"],
806
+ };
807
+ // --- Raw hatches ---------------------------------------------------------------
808
+ const RAW_PATH_PROPERTY = {
809
+ type: "string",
810
+ minLength: 1,
811
+ description: "Path below /api/v3 beginning with '/', e.g. /refunds/<id>. No query string.",
812
+ };
813
+ const RAW_QUERY_PROPERTY = {
814
+ type: "array",
815
+ maxItems: 50,
816
+ description: "Query parameters as name/value pairs; repeat a name to send several values.",
817
+ items: {
818
+ type: "object",
819
+ properties: {
820
+ name: { type: "string", minLength: 1, description: "Parameter name, e.g. statusBucket[]." },
821
+ value: {
822
+ type: ["string", "number", "boolean"],
823
+ description: "Parameter value.",
824
+ },
825
+ },
826
+ required: ["name", "value"],
827
+ additionalProperties: false,
828
+ },
829
+ };
830
+ /** Name/value pairs to a query, repeated names becoming repeated keys. */
831
+ function rawQuery(value) {
832
+ const query = {};
833
+ for (const row of asArray(value)) {
834
+ const pair = asRecord(row);
835
+ const name = pair["name"];
836
+ const item = pair["value"];
837
+ if (typeof name !== "string")
838
+ continue;
839
+ if (typeof item === "string" || typeof item === "number" || typeof item === "boolean") {
840
+ (query[name] ??= []).push(item);
841
+ }
842
+ }
843
+ return Object.fromEntries(Object.entries(query).map(([name, values]) => [name, values.length === 1 ? values[0] : values]));
844
+ }
845
+ function tools(send, defaultPageSize) {
846
+ const readOnly = { readOnlyHint: true };
847
+ const LIMIT = limitProperty(defaultPageSize);
848
+ /** One page-numbered collection read: `{ data, totalCount }` in, `{ <key>, page }` out. */
849
+ async function list(spec, ctx) {
850
+ const page = spec.args["page"] ?? 1;
851
+ const limit = spec.args["limit"] ?? defaultPageSize;
852
+ const payload = await callOverflow(send, { method: "GET", path: spec.path, query: { ...spec.query, page, limit } }, ctx);
853
+ const rows = asArray(asRecord(payload)["data"]);
854
+ return {
855
+ [spec.key]: spec.args["raw"] === true ? rows : rows.map(spec.project),
856
+ page: pageInfo(payload, page, limit, rows.length),
857
+ };
858
+ }
859
+ /** One single-resource read: Overflow wraps it as `{ data }`. */
860
+ async function one(path, ctx) {
861
+ const payload = await callOverflow(send, { method: "GET", path }, ctx);
862
+ const record = asRecord(payload)["data"];
863
+ if (record === undefined || record === null) {
864
+ throw new ConnectorCallError("connector_call_failed", "Overflow answered without the requested record.", {
865
+ retryable: false,
866
+ });
867
+ }
868
+ return asRecord(record);
869
+ }
870
+ return [
871
+ {
872
+ name: "overflow_api_get",
873
+ description: "Call any Overflow v3 GET endpoint and return its untouched body. Prefer a named read; this covers single refunds, chargebacks, campaigns, and Tap devices.",
874
+ annotations: readOnly,
875
+ inputSchema: namedInput({ path: RAW_PATH_PROPERTY, query: RAW_QUERY_PROPERTY }, ["path"]),
876
+ outputSchema: {
877
+ type: "object",
878
+ properties: { result: { description: "Overflow's untouched response body." } },
879
+ required: ["result"],
880
+ },
881
+ handler: async (args, ctx) => ({
882
+ result: (await callOverflow(send, { method: "GET", path: String(args["path"]), query: rawQuery(args["query"]) }, ctx, "raw")) ?? null,
883
+ }),
884
+ },
885
+ {
886
+ name: "overflow_api_mutate",
887
+ description: "Send a JSON POST, PATCH, or DELETE to Overflow v3. Every Overflow write — charges, refunds, recurring gifts, donor edits — goes here, behind approval.",
888
+ annotations: { readOnlyHint: false, destructiveHint: true },
889
+ inputSchema: namedInput({
890
+ method: {
891
+ type: "string",
892
+ enum: ["POST", "PATCH", "DELETE"],
893
+ description: "Mutation method the Overflow endpoint requires.",
894
+ },
895
+ path: RAW_PATH_PROPERTY,
896
+ query: RAW_QUERY_PROPERTY,
897
+ body: {
898
+ type: ["object", "array"],
899
+ description: "JSON body exactly as Overflow documents it. Units differ: subscription `amount` is dollars, " +
900
+ "/payments/authorize takes `amountInCents`, /contributions `amount` states none. Omit when the endpoint takes none.",
901
+ },
902
+ }, ["method", "path"]),
903
+ outputSchema: {
904
+ type: "object",
905
+ properties: {
906
+ result: { description: "Overflow's untouched response body, or null when it sent none." },
907
+ },
908
+ required: ["result"],
909
+ },
910
+ handler: async (args, ctx) => ({
911
+ result: (await callOverflow(send, {
912
+ method: args["method"],
913
+ path: String(args["path"]),
914
+ query: rawQuery(args["query"]),
915
+ ...(args["body"] !== undefined ? { body: args["body"] } : {}),
916
+ }, ctx, "raw")) ?? null,
917
+ }),
918
+ },
919
+ {
920
+ name: "list_contributions",
921
+ description: "List gifts (cash, stock, crypto, DAF, manual) with status, amount, campaign, deposit, and donor name. Donor contact details stay in get_donor.",
922
+ annotations: readOnly,
923
+ inputSchema: namedInput({
924
+ minimumUpdatedDate: DATE_PROPERTY("Updated on or after"),
925
+ maximumUpdatedDate: DATE_PROPERTY("Updated on or before"),
926
+ minimumInitiatedDate: DATE_PROPERTY("Initiated on or after"),
927
+ maximumInitiatedDate: DATE_PROPERTY("Initiated on or before"),
928
+ statusBucket: {
929
+ type: "array",
930
+ minItems: 1,
931
+ uniqueItems: true,
932
+ items: {
933
+ type: "string",
934
+ enum: ["PENDING", "CONFIRMED", "FAILED", "CANCELED"],
935
+ description: "Status bucket.",
936
+ },
937
+ description: "Overflow's coarse status buckets; matches any.",
938
+ },
939
+ campaignId: idProperty("list_campaigns"),
940
+ subcampaignId: idProperty("list_campaigns with isSubcampaign"),
941
+ locationIds: ID_LIST("list_locations"),
942
+ givingLinkId: { type: "string", minLength: 1, description: "Giving link id." },
943
+ pledgeId: { type: "string", minLength: 1, description: "Pledge id." },
944
+ page: PAGE_PROPERTY,
945
+ limit: LIMIT,
946
+ raw: RAW_PROPERTY,
947
+ }),
948
+ outputSchema: listOutput("contributions", CONTRIBUTION_SCHEMA),
949
+ handler: (args, ctx) => list({
950
+ args,
951
+ path: "/contributions",
952
+ key: "contributions",
953
+ project: projectContribution,
954
+ query: {
955
+ minimumUpdatedDate: args["minimumUpdatedDate"],
956
+ maximumUpdatedDate: args["maximumUpdatedDate"],
957
+ minimumInitiatedDate: args["minimumInitiatedDate"],
958
+ maximumInitiatedDate: args["maximumInitiatedDate"],
959
+ "statusBucket[]": args["statusBucket"],
960
+ campaignId: args["campaignId"],
961
+ subcampaignId: args["subcampaignId"],
962
+ "locationIds[]": args["locationIds"],
963
+ givingLinkId: args["givingLinkId"],
964
+ pledgeId: args["pledgeId"],
965
+ },
966
+ }, ctx),
967
+ },
968
+ {
969
+ name: "get_contribution",
970
+ description: "Get one contribution by id with its status, amount, campaign, deposit, and donor name. Refunds and chargebacks are separate lists.",
971
+ annotations: readOnly,
972
+ inputSchema: namedInput({ contributionId: idProperty("list_contributions"), raw: RAW_PROPERTY }, [
973
+ "contributionId",
974
+ ]),
975
+ outputSchema: CONTRIBUTION_SCHEMA,
976
+ handler: async (args, ctx) => {
977
+ const record = await one(`/contributions/${segment(args["contributionId"])}`, ctx);
978
+ return args["raw"] === true ? record : projectContribution(record);
979
+ },
980
+ },
981
+ {
982
+ name: "list_deposits",
983
+ description: "List deposits (payouts from Overflow to the nonprofit's bank) with status, net amount in cents, arrival date, and reconciliation state.",
984
+ annotations: readOnly,
985
+ inputSchema: namedInput({
986
+ startDate: DATE_PROPERTY("Arrived on or after"),
987
+ endDate: DATE_PROPERTY("Arrived on or before"),
988
+ types: {
989
+ type: "array",
990
+ minItems: 1,
991
+ uniqueItems: true,
992
+ items: { type: "string", enum: ["AUTOMATED", "MANUAL", "IMPORT"], description: "Deposit type." },
993
+ description: "Deposit types; matches any.",
994
+ },
995
+ reconciled: { type: "boolean", description: "Filter by reconciliation state." },
996
+ minimumAmountInCents: { type: "integer", minimum: 0, description: "Net amount at least this, in cents." },
997
+ maximumAmountInCents: { type: "integer", minimum: 0, description: "Net amount at most this, in cents." },
998
+ search: { type: "string", minLength: 1, description: "Overflow's free-text search across deposit fields." },
999
+ sortDirection: { ...SORT_DIRECTION, description: "Arrival-date order. Defaults to DESC." },
1000
+ page: PAGE_PROPERTY,
1001
+ limit: LIMIT,
1002
+ raw: RAW_PROPERTY,
1003
+ }),
1004
+ outputSchema: listOutput("deposits", DEPOSIT_SCHEMA),
1005
+ handler: (args, ctx) => list({
1006
+ args,
1007
+ path: "/deposits",
1008
+ key: "deposits",
1009
+ project: (row) => projectDeposit(row, false),
1010
+ query: {
1011
+ startDate: args["startDate"],
1012
+ endDate: args["endDate"],
1013
+ "types[]": args["types"],
1014
+ reconciled: args["reconciled"],
1015
+ minimumEstimatedValueInCents: args["minimumAmountInCents"],
1016
+ maximumEstimatedValueInCents: args["maximumAmountInCents"],
1017
+ search: args["search"],
1018
+ sortDirection: args["sortDirection"],
1019
+ },
1020
+ }, ctx),
1021
+ },
1022
+ {
1023
+ name: "get_deposit",
1024
+ description: "Get one deposit with its line items: each payment, refund, chargeback, adjustment, or transfer it settled, in cents. Totals are get_deposit_summary.",
1025
+ annotations: readOnly,
1026
+ inputSchema: namedInput({ depositId: idProperty("list_deposits"), raw: RAW_PROPERTY }, ["depositId"]),
1027
+ outputSchema: DEPOSIT_SCHEMA,
1028
+ handler: async (args, ctx) => {
1029
+ const record = await one(`/deposits/${segment(args["depositId"])}`, ctx);
1030
+ return args["raw"] === true ? record : projectDeposit(record, true);
1031
+ },
1032
+ },
1033
+ {
1034
+ name: "get_deposit_summary",
1035
+ description: "Get one deposit's totals by kind — contributions, refunds, chargebacks, adjustments — as count, gross, fees, and net in cents.",
1036
+ annotations: readOnly,
1037
+ inputSchema: namedInput({ depositId: idProperty("list_deposits") }, ["depositId"]),
1038
+ outputSchema: {
1039
+ type: "object",
1040
+ properties: {
1041
+ depositId: { type: "string" },
1042
+ contributions: MONEY_ROLLUP,
1043
+ refunds: MONEY_ROLLUP,
1044
+ chargebacks: MONEY_ROLLUP,
1045
+ adjustments: MONEY_ROLLUP,
1046
+ },
1047
+ required: ["depositId", "contributions", "refunds", "chargebacks", "adjustments"],
1048
+ },
1049
+ handler: async (args, ctx) => {
1050
+ const summary = await one(`/deposits/${segment(args["depositId"])}/summary`, ctx);
1051
+ const rollup = (value) => {
1052
+ const row = asRecord(value);
1053
+ return compact({
1054
+ count: row["count"] ?? 0,
1055
+ grossInCents: row["grossInCents"] ?? 0,
1056
+ feesInCents: row["feesInCents"] ?? 0,
1057
+ totalInCents: row["totalInCents"] ?? 0,
1058
+ });
1059
+ };
1060
+ return {
1061
+ depositId: args["depositId"],
1062
+ contributions: rollup(summary["contributions"]),
1063
+ refunds: rollup(summary["refunds"]),
1064
+ chargebacks: rollup(summary["chargebacks"]),
1065
+ adjustments: rollup(summary["adjustments"]),
1066
+ };
1067
+ },
1068
+ },
1069
+ {
1070
+ name: "list_refunds",
1071
+ description: "List refunds with status, reason, amount, and net in cents, optionally for one contribution. Reads only; issuing a refund is a mutate call.",
1072
+ annotations: readOnly,
1073
+ inputSchema: namedInput({
1074
+ contributionId: idProperty("list_contributions"),
1075
+ minimumUpdatedDate: DATE_PROPERTY("Updated on or after"),
1076
+ maximumUpdatedDate: DATE_PROPERTY("Updated on or before"),
1077
+ sortDirection: { ...SORT_DIRECTION, description: "Created-date order. Defaults to DESC." },
1078
+ page: PAGE_PROPERTY,
1079
+ limit: LIMIT,
1080
+ raw: RAW_PROPERTY,
1081
+ }),
1082
+ outputSchema: listOutput("refunds", REFUND_SCHEMA),
1083
+ handler: (args, ctx) => list({
1084
+ args,
1085
+ path: "/refunds",
1086
+ key: "refunds",
1087
+ project: projectRefund,
1088
+ query: {
1089
+ contributionId: args["contributionId"],
1090
+ minimumUpdatedDate: args["minimumUpdatedDate"],
1091
+ maximumUpdatedDate: args["maximumUpdatedDate"],
1092
+ sortDirection: args["sortDirection"],
1093
+ },
1094
+ }, ctx),
1095
+ },
1096
+ {
1097
+ name: "list_chargebacks",
1098
+ description: "List chargebacks (card disputes and ACH returns) with status — pending, won, lost — and amounts in cents, optionally for one contribution.",
1099
+ annotations: readOnly,
1100
+ inputSchema: namedInput({
1101
+ contributionId: idProperty("list_contributions"),
1102
+ minimumUpdatedDate: DATE_PROPERTY("Updated on or after"),
1103
+ maximumUpdatedDate: DATE_PROPERTY("Updated on or before"),
1104
+ sortDirection: { ...SORT_DIRECTION, description: "Created-date order. Defaults to DESC." },
1105
+ page: PAGE_PROPERTY,
1106
+ limit: LIMIT,
1107
+ raw: RAW_PROPERTY,
1108
+ }),
1109
+ outputSchema: listOutput("chargebacks", CHARGEBACK_SCHEMA),
1110
+ handler: (args, ctx) => list({
1111
+ args,
1112
+ path: "/chargebacks",
1113
+ key: "chargebacks",
1114
+ project: projectChargeback,
1115
+ query: {
1116
+ contributionId: args["contributionId"],
1117
+ minimumUpdatedDate: args["minimumUpdatedDate"],
1118
+ maximumUpdatedDate: args["maximumUpdatedDate"],
1119
+ sortDirection: args["sortDirection"],
1120
+ },
1121
+ }, ctx),
1122
+ },
1123
+ {
1124
+ name: "list_donors",
1125
+ description: "List donors with name, email, gift count, and recurring count. Cannot search by name or email; omits phone and address, which get_donor returns.",
1126
+ annotations: readOnly,
1127
+ inputSchema: namedInput({
1128
+ locationIds: ID_LIST("list_locations"),
1129
+ minimumUpdatedDate: DATE_PROPERTY("Updated on or after"),
1130
+ maximumUpdatedDate: DATE_PROPERTY("Updated on or before"),
1131
+ sortBy: {
1132
+ type: "string",
1133
+ enum: ["updatedAt", "createdAt", "lastName", "firstName", "email", "totalContributionsCount"],
1134
+ description: "Sort field. Defaults to updatedAt.",
1135
+ },
1136
+ sortDirection: { ...SORT_DIRECTION, description: "Defaults to DESC." },
1137
+ page: PAGE_PROPERTY,
1138
+ limit: LIMIT,
1139
+ raw: RAW_PROPERTY,
1140
+ }),
1141
+ outputSchema: listOutput("donors", DONOR_SCHEMA),
1142
+ handler: (args, ctx) => list({
1143
+ args,
1144
+ path: "/donors",
1145
+ key: "donors",
1146
+ project: (row) => projectDonor(row, false),
1147
+ query: {
1148
+ "locationIds[]": args["locationIds"],
1149
+ minimumUpdatedDate: args["minimumUpdatedDate"],
1150
+ maximumUpdatedDate: args["maximumUpdatedDate"],
1151
+ sortBy: args["sortBy"],
1152
+ sortDirection: args["sortDirection"],
1153
+ },
1154
+ }, ctx),
1155
+ },
1156
+ {
1157
+ name: "get_donor",
1158
+ description: "Get one donor's full profile: contact details, mailing address, locations, and giving counts. Their gifts are list_contributions rows.",
1159
+ annotations: readOnly,
1160
+ inputSchema: namedInput({ donorId: idProperty("list_donors or a contribution's donor.id") }, ["donorId"]),
1161
+ outputSchema: DONOR_SCHEMA,
1162
+ handler: async (args, ctx) => projectDonor(await one(`/donors/${segment(args["donorId"])}`, ctx), true),
1163
+ },
1164
+ {
1165
+ name: "list_subscriptions",
1166
+ description: "List recurring gifts with status, amount, frequency, next payment date, and campaign — for the nonprofit or, given donorId, one donor.",
1167
+ annotations: readOnly,
1168
+ inputSchema: namedInput({
1169
+ donorId: idProperty("list_donors; omit for every donor"),
1170
+ status: {
1171
+ type: "array",
1172
+ minItems: 1,
1173
+ uniqueItems: true,
1174
+ items: {
1175
+ type: "string",
1176
+ enum: ["active", "paused", "pending", "failed", "inactive", "recreated"],
1177
+ description: "Recurring-gift status.",
1178
+ },
1179
+ description: "Statuses; matches any.",
1180
+ },
1181
+ locationIds: ID_LIST("list_locations"),
1182
+ minimumUpdatedDate: DATE_PROPERTY("Updated on or after"),
1183
+ maximumUpdatedDate: DATE_PROPERTY("Updated on or before"),
1184
+ sortBy: {
1185
+ type: "string",
1186
+ enum: ["createdAt", "amount", "frequency", "nextContributionAt"],
1187
+ description: "Sort field. Defaults to createdAt.",
1188
+ },
1189
+ sortDirection: { ...SORT_DIRECTION, description: "Defaults to DESC." },
1190
+ page: PAGE_PROPERTY,
1191
+ limit: LIMIT,
1192
+ raw: RAW_PROPERTY,
1193
+ }),
1194
+ outputSchema: listOutput("subscriptions", SUBSCRIPTION_SCHEMA),
1195
+ handler: (args, ctx) => list({
1196
+ args,
1197
+ path: args["donorId"] === undefined ? "/subscriptions" : `/subscriptions/${segment(args["donorId"])}`,
1198
+ key: "subscriptions",
1199
+ project: projectSubscription,
1200
+ query: {
1201
+ "status[]": args["status"],
1202
+ "locationIds[]": args["locationIds"],
1203
+ minimumUpdatedDate: args["minimumUpdatedDate"],
1204
+ maximumUpdatedDate: args["maximumUpdatedDate"],
1205
+ sortBy: args["sortBy"],
1206
+ sortDirection: args["sortDirection"],
1207
+ },
1208
+ }, ctx),
1209
+ },
1210
+ {
1211
+ name: "list_payment_methods",
1212
+ description: "List one donor's saved payment methods (type, last 4, expiry) and their ids. Unpaginated. Never returns account or card numbers.",
1213
+ annotations: readOnly,
1214
+ inputSchema: namedInput({
1215
+ donorId: idProperty("list_donors"),
1216
+ showExpired: { type: "boolean", description: "Include expired methods. Defaults to false." },
1217
+ raw: RAW_PROPERTY,
1218
+ }, ["donorId"]),
1219
+ outputSchema: {
1220
+ type: "object",
1221
+ properties: { paymentMethods: { type: "array", items: PAYMENT_METHOD_SCHEMA } },
1222
+ required: ["paymentMethods"],
1223
+ },
1224
+ handler: async (args, ctx) => {
1225
+ const payload = await callOverflow(send, {
1226
+ method: "GET",
1227
+ path: `/payment-methods/${segment(args["donorId"])}`,
1228
+ query: { showExpired: args["showExpired"] },
1229
+ }, ctx);
1230
+ const rows = asArray(asRecord(payload)["data"]);
1231
+ return { paymentMethods: args["raw"] === true ? rows : rows.map(projectPaymentMethod) };
1232
+ },
1233
+ },
1234
+ {
1235
+ name: "list_campaigns",
1236
+ description: "List campaigns or subcampaigns with status, dates, and giving totals. Ids feed the campaignId filters and gift bodies.",
1237
+ annotations: readOnly,
1238
+ inputSchema: namedInput({
1239
+ isSubcampaign: { type: "boolean", description: "True lists subcampaigns instead. Defaults to false." },
1240
+ parentCampaignId: idProperty("list_campaigns; lists its subcampaigns"),
1241
+ search: { type: "string", minLength: 1, description: "Campaign-name search." },
1242
+ includeArchived: { type: "boolean", description: "Include archived campaigns." },
1243
+ excludeInactive: { type: "boolean", description: "Exclude campaigns that have ended." },
1244
+ sortBy: {
1245
+ type: "string",
1246
+ enum: [
1247
+ "startDate",
1248
+ "endDate",
1249
+ "name",
1250
+ "displayOrder",
1251
+ "totalContributionCount",
1252
+ "totalContributionValue",
1253
+ "uniqueDonorCount",
1254
+ ],
1255
+ description: "Sort field. Defaults to startDate.",
1256
+ },
1257
+ sortDirection: { ...SORT_DIRECTION, description: "Defaults to DESC." },
1258
+ page: PAGE_PROPERTY,
1259
+ limit: LIMIT,
1260
+ raw: RAW_PROPERTY,
1261
+ }),
1262
+ outputSchema: listOutput("campaigns", CAMPAIGN_SCHEMA),
1263
+ handler: (args, ctx) => list({
1264
+ args,
1265
+ path: "/campaigns",
1266
+ key: "campaigns",
1267
+ project: projectCampaign,
1268
+ query: {
1269
+ isSubcampaign: args["isSubcampaign"] === undefined
1270
+ ? args["parentCampaignId"] === undefined
1271
+ ? undefined
1272
+ : "true"
1273
+ : String(args["isSubcampaign"]),
1274
+ parentCampaignId: args["parentCampaignId"],
1275
+ search: args["search"],
1276
+ includeArchived: args["includeArchived"],
1277
+ excludeInactive: args["excludeInactive"],
1278
+ sortBy: args["sortBy"],
1279
+ sortDirection: args["sortDirection"],
1280
+ },
1281
+ }, ctx),
1282
+ },
1283
+ {
1284
+ name: "list_locations",
1285
+ description: "List the nonprofit's locations (campuses) with names and ids for the locationIds filters. The default location is flagged.",
1286
+ annotations: readOnly,
1287
+ inputSchema: namedInput({
1288
+ includeArchived: { type: "boolean", description: "Include archived locations." },
1289
+ sortBy: {
1290
+ type: "string",
1291
+ enum: ["displayOrder", "name", "createdAt"],
1292
+ description: "Sort field. Defaults to displayOrder.",
1293
+ },
1294
+ sortDirection: { ...SORT_DIRECTION, description: "Defaults to ASC." },
1295
+ page: PAGE_PROPERTY,
1296
+ limit: LIMIT,
1297
+ }),
1298
+ outputSchema: listOutput("locations", LOCATION_SCHEMA),
1299
+ handler: (args, ctx) => list({
1300
+ args,
1301
+ path: "/locations",
1302
+ key: "locations",
1303
+ project: projectLocation,
1304
+ query: {
1305
+ includeArchived: args["includeArchived"],
1306
+ sortBy: args["sortBy"],
1307
+ sortDirection: args["sortDirection"],
1308
+ },
1309
+ }, ctx),
1310
+ },
1311
+ {
1312
+ name: "list_tap_events",
1313
+ description: "List Tap-to-give taps with the device, group, and destination each hit. Taps are engagement, not gifts; giving is list_contributions.",
1314
+ annotations: readOnly,
1315
+ inputSchema: namedInput({
1316
+ startDate: DATE_PROPERTY("On or after"),
1317
+ endDate: DATE_PROPERTY("On or before"),
1318
+ groupIds: ID_LIST("a tap event's groupId"),
1319
+ destinationIds: ID_LIST("a tap event's destinationId"),
1320
+ deviceIds: ID_LIST("a tap event's deviceId"),
1321
+ sortDirection: { ...SORT_DIRECTION, description: "Time order. Defaults to DESC." },
1322
+ page: PAGE_PROPERTY,
1323
+ limit: LIMIT,
1324
+ raw: RAW_PROPERTY,
1325
+ }),
1326
+ outputSchema: listOutput("events", TAP_EVENT_SCHEMA),
1327
+ handler: (args, ctx) => list({
1328
+ args,
1329
+ path: "/tap/events",
1330
+ key: "events",
1331
+ project: projectTapEvent,
1332
+ query: {
1333
+ startDate: args["startDate"],
1334
+ endDate: args["endDate"],
1335
+ "groupIds[]": args["groupIds"],
1336
+ "destinationIds[]": args["destinationIds"],
1337
+ "deviceIds[]": args["deviceIds"],
1338
+ sortDirection: args["sortDirection"],
1339
+ },
1340
+ }, ctx),
1341
+ },
1342
+ {
1343
+ name: "list_webhooks",
1344
+ description: "List the nonprofit's webhook subscriptions with destination URL, enabled events, and status. Delivery history is list_webhook_event_logs.",
1345
+ annotations: readOnly,
1346
+ inputSchema: namedInput({
1347
+ status: { type: "string", enum: ["enabled", "disabled"], description: "Subscription status." },
1348
+ sortDirection: { ...SORT_DIRECTION, description: "Created-date order. Defaults to DESC." },
1349
+ page: PAGE_PROPERTY,
1350
+ limit: LIMIT,
1351
+ }),
1352
+ outputSchema: listOutput("webhooks", WEBHOOK_SCHEMA),
1353
+ handler: (args, ctx) => list({
1354
+ args,
1355
+ path: "/webhooks",
1356
+ key: "webhooks",
1357
+ project: projectWebhook,
1358
+ query: { status: args["status"], sortDirection: args["sortDirection"] },
1359
+ }, ctx),
1360
+ },
1361
+ {
1362
+ name: "list_webhook_event_logs",
1363
+ description: "List delivery attempts for one webhook with status, response code, and error. Omits event payloads unless raw. Pages by cursor, not page.",
1364
+ annotations: readOnly,
1365
+ inputSchema: namedInput({
1366
+ webhookId: idProperty("list_webhooks"),
1367
+ status: { type: "string", enum: ["delivered", "failed", "pending"], description: "Delivery status." },
1368
+ webhookEventName: {
1369
+ type: "string",
1370
+ pattern: "^(\\*|[a-z_]+\\.[a-z_]+)$",
1371
+ description: "Event name, e.g. contribution.approved or donor.updated.",
1372
+ },
1373
+ sortDirection: { ...SORT_DIRECTION, description: "Attempt order. Defaults to DESC." },
1374
+ cursor: {
1375
+ type: "string",
1376
+ minLength: 1,
1377
+ description: "This tool pages by cursor: pass nextCursor back unchanged.",
1378
+ },
1379
+ limit: {
1380
+ type: "integer",
1381
+ minimum: 1,
1382
+ maximum: MAX_EVENT_LOG_PAGE_SIZE,
1383
+ description: `Attempts per page, up to Overflow's cap of ${MAX_EVENT_LOG_PAGE_SIZE} for this endpoint. Defaults to ${DEFAULT_EVENT_LOG_PAGE_SIZE}.`,
1384
+ },
1385
+ raw: RAW_PROPERTY,
1386
+ }, ["webhookId"]),
1387
+ outputSchema: {
1388
+ type: "object",
1389
+ properties: {
1390
+ attempts: { type: "array", items: EVENT_LOG_SCHEMA },
1391
+ nextCursor: {
1392
+ type: "string",
1393
+ description: "Present only when more attempts exist; no page object.",
1394
+ },
1395
+ },
1396
+ required: ["attempts"],
1397
+ },
1398
+ handler: async (args, ctx) => {
1399
+ const payload = asRecord(await callOverflow(send, {
1400
+ method: "GET",
1401
+ path: `/webhooks/${segment(args["webhookId"])}/event-logs`,
1402
+ query: {
1403
+ status: args["status"],
1404
+ webhookEventName: args["webhookEventName"],
1405
+ sortDirection: args["sortDirection"],
1406
+ cursor: args["cursor"],
1407
+ limit: args["limit"] ?? DEFAULT_EVENT_LOG_PAGE_SIZE,
1408
+ },
1409
+ }, ctx));
1410
+ const rows = asArray(payload["data"]);
1411
+ const info = asRecord(payload["pageInfo"]);
1412
+ const next = info["nextCursor"];
1413
+ return compact({
1414
+ attempts: args["raw"] === true ? rows : rows.map(projectEventLog),
1415
+ nextCursor: info["hasNextPage"] === true && typeof next === "string" && next !== "" ? next : undefined,
1416
+ });
1417
+ },
1418
+ },
1419
+ ];
1420
+ }
1421
+ // --- Guide ---------------------------------------------------------------------
1422
+ function usageGuide(purpose, environment, instructions) {
1423
+ const accountInstructions = instructions?.trim();
1424
+ const environmentLine = environment === "production"
1425
+ ? "Overflow **production**: live nonprofit giving data, real donors, and real money."
1426
+ : "Overflow **staging**: Overflow's sandbox, isolated from production donors and money.";
1427
+ return `# Overflow usage
1428
+
1429
+ ${environmentLine}
1430
+
1431
+ Nonprofit purpose: ${purpose}${skill.fragments.guide_0}${accountInstructions ? `\n## ${skill.instructionsHeading}\n\n${accountInstructions}\n` : ""}`;
1432
+ }
1433
+ // --- Construction --------------------------------------------------------------
1434
+ /** The closed options overflow() accepts; see `assertKnownOptions`. */
1435
+ const OVERFLOW_OPTIONS = optionsOf()({
1436
+ ...PROVIDER_COMMON,
1437
+ ...keys("environment", "defaultPageSize", "baseUrl"),
1438
+ });
1439
+ /**
1440
+ * A maintained Overflow connection.
1441
+ *
1442
+ * `environment` is required with no default, and the argument runs the other
1443
+ * way from a provider whose environment rides the credential. Overflow
1444
+ * publishes two base URLs and issues separate credentials for each, so a
1445
+ * mismatch fails loudly with a 403 rather than reaching the wrong data — but
1446
+ * that is exactly why a default buys nothing. Defaulting to staging, the
1447
+ * "safe" side, sends every real deployment to a sandbox where its production
1448
+ * key is refused with a generic "Forbidden resource" and no hint that the base
1449
+ * URL is the problem. Defaulting to production makes a test deployment that
1450
+ * forgot the option one pasted key away from charging real donors through the
1451
+ * mutate hatch. The environment is a fact the operator knows and connecta
1452
+ * cannot infer — Overflow's keys carry no recognizable environment marker — so
1453
+ * the operator states it, and the title and the guide's first line repeat it.
1454
+ */
1455
+ export const overflow = asProviderFactory({
1456
+ name: "overflow",
1457
+ title: "Overflow",
1458
+ kind: "api",
1459
+ readme: "Overflow",
1460
+ bundle: {
1461
+ "baselineGzip": 20410,
1462
+ "maxGzip": 80410,
1463
+ "note": "./providers/overflow starts at 20,410 B gzip: a hand-written api() surface over the guarded transport with no hosted-MCP interface, so it pulls in neither remoteMcp nor the Effect core the other providers carry. The cap uses the existing baseline + 60,000 B policy.",
1464
+ },
1465
+ skill,
1466
+ options: OVERFLOW_OPTIONS,
1467
+ create: overflowConnector,
1468
+ });
1469
+ function overflowConnector(id, options) {
1470
+ const purpose = options.purpose?.trim();
1471
+ if (!purpose) {
1472
+ throw new Error("overflow() requires a non-empty nonprofit purpose.");
1473
+ }
1474
+ const environment = options.environment;
1475
+ if (environment !== "production" && environment !== "staging") {
1476
+ throw new Error('overflow() requires environment: "production" or "staging". Overflow issues separate credentials for each, and connecta will not guess which one a deployment means.');
1477
+ }
1478
+ const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
1479
+ if (!Number.isInteger(defaultPageSize) || defaultPageSize < 1 || defaultPageSize > MAX_PAGE_SIZE) {
1480
+ throw new Error(`overflow() defaultPageSize must be a whole number between 1 and ${MAX_PAGE_SIZE}.`);
1481
+ }
1482
+ const send = overflowTransport(options.baseUrl?.trim() || OVERFLOW_API_BASE_URLS[environment]);
1483
+ const label = environment === "production" ? "production" : "staging";
1484
+ return api(id, {
1485
+ ...defined({ authScope: options.authScope, maxResultBytes: options.maxResultBytes }),
1486
+ title: options.title ?? `Overflow (${label})`,
1487
+ description: `Overflow nonprofit giving, ${label}: ${purpose}`,
1488
+ credential: API_KEY_CREDENTIAL,
1489
+ async testCredentials(values, ctx) {
1490
+ // Overflow has no identity endpoint, and /health answers without a
1491
+ // credential, so it proves nothing. One location is the cheapest
1492
+ // authenticated read: every nonprofit has at least its default one, and
1493
+ // its name is the closest thing to "which organization is this?" the
1494
+ // API will say.
1495
+ try {
1496
+ const payload = asRecord(await callOverflow(send, { method: "GET", path: "/locations", query: { limit: 1, sortBy: "displayOrder" } }, {
1497
+ ...ctx,
1498
+ credential: {
1499
+ get: async (field) => (field ? (values[field] ?? null) : null),
1500
+ getAll: async () => values,
1501
+ },
1502
+ }));
1503
+ const first = asRecord(asArray(payload["data"])[0]);
1504
+ const total = payload["totalCount"];
1505
+ const where = typeof first["name"] === "string"
1506
+ ? ` The nonprofit's first location is "${first["name"]}"${typeof total === "number" ? ` of ${total}` : ""}.`
1507
+ : "";
1508
+ return {
1509
+ ok: true,
1510
+ message: `Overflow ${label} accepted the client id and API key.${where}`,
1511
+ };
1512
+ }
1513
+ catch (error) {
1514
+ return {
1515
+ ok: false,
1516
+ message: error instanceof ConnectorCallError
1517
+ ? error.message
1518
+ : `Overflow ${label} rejected the client id and API key.`,
1519
+ };
1520
+ }
1521
+ },
1522
+ callAdmission: options.callAdmission ?? OVERFLOW_ADMISSION,
1523
+ usageGuide: {
1524
+ content: usageGuide(purpose, environment, options.instructions),
1525
+ summary: `Overflow ${label}: money units per field, final statuses, deposits, donor-PII care, and writes via mutate.`,
1526
+ // Required because correct use depends on facts no schema carries:
1527
+ // which statuses are final per asset type, which money unit each field
1528
+ // and write body uses (cents, dollars, or unstated), and
1529
+ // that every write is a mutate-hatch call whose body comes from
1530
+ // Overflow's reference.
1531
+ required: true,
1532
+ },
1533
+ tools: tools(send, defaultPageSize),
1534
+ });
1535
+ }