@zackbart/connecta 0.34.0 → 0.35.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 (428) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/README.md +62 -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 +2051 -1160
  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 +663 -271
  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 +15 -7
  53. package/dist/connectors/api-connector.js +147 -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/option-shapes.js +1 -1
  58. package/dist/connectors/remote-mcp.d.ts +12 -30
  59. package/dist/connectors/remote-mcp.js +960 -564
  60. package/dist/d1.js +10 -243
  61. package/dist/derived-context.js +5 -0
  62. package/dist/describe-config.d.ts +5 -1
  63. package/dist/describe-config.js +3 -0
  64. package/dist/downstream-input.js +347 -260
  65. package/dist/errors.d.ts +87 -6
  66. package/dist/errors.js +114 -4
  67. package/dist/execute.js +87 -50
  68. package/dist/executor-admission.js +3 -0
  69. package/dist/executors/quickjs.js +163 -112
  70. package/dist/guarded-transport.js +50 -0
  71. package/dist/guest-runtime.js +1 -1
  72. package/dist/http/client.js +439 -0
  73. package/dist/http/errors.js +558 -0
  74. package/dist/http/fields.js +49 -0
  75. package/dist/http/hatch-paging.js +318 -0
  76. package/dist/http/hatch.js +1279 -0
  77. package/dist/http/json-schema.js +267 -0
  78. package/dist/http/node-transport.js +241 -0
  79. package/dist/http/oauth.js +162 -0
  80. package/dist/http/paging.js +228 -0
  81. package/dist/http/provider.js +221 -0
  82. package/dist/http/safety.js +82 -0
  83. package/dist/http/services.js +79 -0
  84. package/dist/http/tools.js +383 -0
  85. package/dist/http/vendor-index.js +138 -0
  86. package/dist/http/vendor.js +448 -0
  87. package/dist/http/write-request.js +66 -0
  88. package/dist/http-policy.d.ts +11 -0
  89. package/dist/http-policy.js +45 -0
  90. package/dist/inbound-credential.js +1 -1
  91. package/dist/index.d.ts +2 -2
  92. package/dist/index.js +4 -0
  93. package/dist/invocation-auth.js +30 -19
  94. package/dist/invocation.js +221 -87
  95. package/dist/mcp-result.js +0 -4
  96. package/dist/membership.d.ts +4 -0
  97. package/dist/meta-output.js +1 -1
  98. package/dist/meta-tools.js +534 -408
  99. package/dist/native-recovery.js +52 -0
  100. package/dist/native-tools.js +382 -0
  101. package/dist/node.d.ts +4 -3
  102. package/dist/node.js +45 -8
  103. package/dist/oauth-handoff.js +177 -104
  104. package/dist/oauth-partition.js +6 -20
  105. package/dist/oauth-sealing.js +6 -7
  106. package/dist/operation-vocabulary.js +101 -0
  107. package/dist/operator-record.d.ts +2 -2
  108. package/dist/operator-record.js +17 -4
  109. package/dist/operator-ui/agents.d.ts +5 -4
  110. package/dist/operator-ui/agents.js +17 -7
  111. package/dist/operator-ui/config-summary.js +8 -4
  112. package/dist/operator-ui/connection-map.js +9 -6
  113. package/dist/operator-ui/contract.d.ts +7 -3
  114. package/dist/operator-ui/endpoints.js +5 -14
  115. package/dist/operator-ui/fix-prompts.d.ts +1 -1
  116. package/dist/operator-ui/fix-prompts.js +0 -8
  117. package/dist/operator-ui/generated.js +3 -3
  118. package/dist/operator-ui/health.js +52 -0
  119. package/dist/operator-ui/model.d.ts +2 -3
  120. package/dist/operator-ui/overview.js +11 -19
  121. package/dist/operator-ui/setup-commands.js +0 -4
  122. package/dist/operator-ui/view.d.ts +5 -29
  123. package/dist/operator-ui/view.js +3 -59
  124. package/dist/program-writes.js +47 -26
  125. package/dist/provider-call-facts.d.ts +115 -0
  126. package/dist/provider-call-facts.js +182 -0
  127. package/dist/provider-call.d.ts +112 -0
  128. package/dist/provider-call.js +271 -0
  129. package/dist/provider.d.ts +71 -32
  130. package/dist/provider.js +134 -40
  131. package/dist/providers/_shared/google/hatch.js +91 -0
  132. package/dist/providers/_shared/google/kit.js +537 -0
  133. package/dist/providers/_shared/google/result-size.js +15 -2
  134. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  135. package/dist/providers/_shared/google/service-account.js +8 -4
  136. package/dist/providers/_shared/google/workspace.d.ts +8 -209
  137. package/dist/providers/_shared/google/workspace.js +7 -426
  138. package/dist/providers/_shared/rest/compact-index.js +86 -0
  139. package/dist/providers/_shared/rest/dispatch.js +3 -23
  140. package/dist/providers/_shared/rest/kit.js +546 -0
  141. package/dist/providers/_shared/rest/operation-index.js +230 -14
  142. package/dist/providers/_shared/rest/response-media.js +11 -0
  143. package/dist/providers/_shared/rest/tools.js +101 -78
  144. package/dist/providers/_shared/rest/value-safety.js +353 -195
  145. package/dist/providers/_shared/sensitive-fields.js +27 -0
  146. package/dist/providers/breeze/api.js +584 -0
  147. package/dist/providers/breeze/index.d.ts +3 -3
  148. package/dist/providers/breeze/index.js +67 -1420
  149. package/dist/providers/breeze/openapi.generated.js +60 -0
  150. package/dist/providers/breeze/skill.generated.js +2 -6
  151. package/dist/providers/breeze/tools.js +696 -0
  152. package/dist/providers/ccb/api.js +496 -0
  153. package/dist/providers/ccb/index.d.ts +17 -37
  154. package/dist/providers/ccb/index.js +77 -1702
  155. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  156. package/dist/providers/ccb/lib/constants.js +74 -0
  157. package/dist/providers/ccb/lib/guide.js +9 -0
  158. package/dist/providers/ccb/lib/verdicts.js +387 -0
  159. package/dist/providers/ccb/openapi.generated.js +397 -0
  160. package/dist/providers/ccb/safety.js +295 -0
  161. package/dist/providers/ccb/skill.generated.js +2 -6
  162. package/dist/providers/ccb/tools.js +853 -0
  163. package/dist/providers/cloudflare/api.js +1066 -0
  164. package/dist/providers/cloudflare/index.d.ts +7 -22
  165. package/dist/providers/cloudflare/index.js +80 -194
  166. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  167. package/dist/providers/cloudflare/lib/config.js +2 -0
  168. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  169. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  170. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  171. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  172. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  173. package/dist/providers/cloudflare/openapi.generated.js +3299 -3208
  174. package/dist/providers/cloudflare/{value-safety.js → safety.js} +538 -18
  175. package/dist/providers/cloudflare/skill.generated.js +2 -10
  176. package/dist/providers/cloudflare/tools.js +360 -0
  177. package/dist/providers/docs/api.js +51 -0
  178. package/dist/providers/docs/index.d.ts +1 -12
  179. package/dist/providers/docs/index.js +53 -1165
  180. package/dist/providers/docs/lib/constants.d.ts +18 -0
  181. package/dist/providers/docs/lib/constants.js +18 -0
  182. package/dist/providers/docs/lib/discovery.js +26 -0
  183. package/dist/providers/docs/lib/requests.js +48 -0
  184. package/dist/providers/docs/lib/schemas.js +168 -0
  185. package/dist/providers/docs/openapi.generated.js +16 -0
  186. package/dist/providers/docs/safety.js +54 -0
  187. package/dist/providers/docs/skill.generated.js +2 -6
  188. package/dist/providers/docs/tools.js +802 -0
  189. package/dist/providers/drive/api.js +177 -0
  190. package/dist/providers/drive/index.d.ts +1 -12
  191. package/dist/providers/drive/index.js +38 -1448
  192. package/dist/providers/drive/lib/constants.d.ts +6 -0
  193. package/dist/providers/drive/lib/constants.js +6 -0
  194. package/dist/providers/drive/lib/contracts.js +532 -0
  195. package/dist/providers/drive/lib/hatch.js +197 -0
  196. package/dist/providers/drive/openapi.generated.js +77 -0
  197. package/dist/providers/drive/safety.js +90 -0
  198. package/dist/providers/drive/skill.generated.js +2 -6
  199. package/dist/providers/drive/tools.js +1290 -0
  200. package/dist/providers/forms/api.js +71 -0
  201. package/dist/providers/forms/index.d.ts +1 -12
  202. package/dist/providers/forms/index.js +49 -1145
  203. package/dist/providers/forms/lib/constants.d.ts +14 -0
  204. package/dist/providers/forms/lib/constants.js +19 -0
  205. package/dist/providers/forms/lib/hatch.js +72 -0
  206. package/dist/providers/forms/openapi.generated.js +23 -0
  207. package/dist/providers/forms/safety.js +34 -0
  208. package/dist/providers/forms/skill.generated.js +2 -8
  209. package/dist/providers/forms/tools.js +1024 -0
  210. package/dist/providers/github/api.js +150 -0
  211. package/dist/providers/github/index.d.ts +4 -5
  212. package/dist/providers/github/index.js +30 -87
  213. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  214. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  215. package/dist/providers/github/lib/discovery.js +132 -0
  216. package/dist/providers/github/lib/operations.js +58 -0
  217. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  218. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  219. package/dist/providers/github/openapi.generated.js +1216 -0
  220. package/dist/providers/github/safety.js +216 -0
  221. package/dist/providers/github/skill.generated.js +2 -5
  222. package/dist/providers/github/tools.js +506 -0
  223. package/dist/providers/gmail/api.js +114 -0
  224. package/dist/providers/gmail/index.d.ts +1 -12
  225. package/dist/providers/gmail/index.js +73 -1621
  226. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  227. package/dist/providers/gmail/lib/constants.js +16 -0
  228. package/dist/providers/gmail/lib/index.js +173 -0
  229. package/dist/providers/gmail/lib/schemas.js +176 -0
  230. package/dist/providers/gmail/lib/threading.js +120 -0
  231. package/dist/providers/gmail/openapi.generated.js +92 -0
  232. package/dist/providers/gmail/safety.js +21 -0
  233. package/dist/providers/gmail/skill.generated.js +2 -6
  234. package/dist/providers/gmail/tools.js +1435 -0
  235. package/dist/providers/overflow/api.js +1008 -0
  236. package/dist/providers/overflow/index.d.ts +3 -17
  237. package/dist/providers/overflow/index.js +60 -1488
  238. package/dist/providers/overflow/skill.generated.js +2 -5
  239. package/dist/providers/overflow/tools.js +873 -0
  240. package/dist/providers/planning-center/api.js +467 -0
  241. package/dist/providers/planning-center/index.d.ts +6 -35
  242. package/dist/providers/planning-center/index.js +69 -2445
  243. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  244. package/dist/providers/planning-center/lib/constants.js +83 -0
  245. package/dist/providers/planning-center/lib/discovery.js +40 -0
  246. package/dist/providers/planning-center/lib/guide.js +39 -0
  247. package/dist/providers/planning-center/lib/json-api.js +83 -0
  248. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  249. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  250. package/dist/providers/planning-center/safety.js +188 -0
  251. package/dist/providers/planning-center/skill.generated.js +2 -27
  252. package/dist/providers/planning-center/tools.js +1010 -0
  253. package/dist/providers/sheets/api.js +133 -0
  254. package/dist/providers/sheets/index.d.ts +4 -16
  255. package/dist/providers/sheets/index.js +61 -1172
  256. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  257. package/dist/providers/sheets/lib/constants.js +14 -0
  258. package/dist/providers/sheets/lib/hatch.js +163 -0
  259. package/dist/providers/sheets/lib/requests.js +105 -0
  260. package/dist/providers/sheets/lib/schemas.js +138 -0
  261. package/dist/providers/sheets/openapi.generated.js +30 -0
  262. package/dist/providers/sheets/safety.js +27 -0
  263. package/dist/providers/sheets/skill.generated.js +2 -7
  264. package/dist/providers/sheets/tools.js +734 -0
  265. package/dist/providers/slides/api.js +147 -0
  266. package/dist/providers/slides/index.d.ts +2 -10
  267. package/dist/providers/slides/index.js +48 -2555
  268. package/dist/providers/slides/lib/constants.d.ts +11 -0
  269. package/dist/providers/slides/lib/constants.js +11 -0
  270. package/dist/providers/slides/lib/requests.js +60 -0
  271. package/dist/providers/slides/lib/schemas.js +565 -0
  272. package/dist/providers/slides/openapi.generated.js +18 -0
  273. package/dist/providers/slides/skill.generated.js +2 -6
  274. package/dist/providers/slides/tools.js +1968 -0
  275. package/dist/providers/stripe/api.js +401 -0
  276. package/dist/providers/stripe/index.d.ts +7 -27
  277. package/dist/providers/stripe/index.js +75 -191
  278. package/dist/providers/stripe/lib/config.d.ts +4 -0
  279. package/dist/providers/stripe/lib/config.js +2 -0
  280. package/dist/providers/stripe/openapi.generated.js +4 -3
  281. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  282. package/dist/providers/stripe/skill.generated.js +2 -10
  283. package/dist/providers/stripe/tools.js +77 -0
  284. package/dist/providers/tithely/api.js +531 -0
  285. package/dist/providers/tithely/index.d.ts +19 -0
  286. package/dist/providers/tithely/index.js +86 -965
  287. package/dist/providers/tithely/skill.generated.js +2 -5
  288. package/dist/providers/tithely/tools.js +426 -0
  289. package/dist/providers/vercel/api.js +551 -0
  290. package/dist/providers/vercel/index.d.ts +11 -25
  291. package/dist/providers/vercel/index.js +67 -261
  292. package/dist/providers/vercel/lib/config.d.ts +2 -0
  293. package/dist/providers/vercel/lib/config.js +2 -0
  294. package/dist/providers/vercel/lib/index.js +117 -0
  295. package/dist/providers/vercel/lib/logs.js +100 -0
  296. package/dist/providers/vercel/lib/pin.js +270 -0
  297. package/dist/providers/vercel/openapi.generated.js +398 -376
  298. package/dist/providers/vercel/{value-safety.js → safety.js} +261 -42
  299. package/dist/providers/vercel/skill.generated.js +2 -7
  300. package/dist/providers/vercel/tools.js +642 -0
  301. package/dist/redaction-identity.js +47 -0
  302. package/dist/redaction-marker.js +125 -0
  303. package/dist/registry.d.ts +30 -28
  304. package/dist/registry.js +446 -413
  305. package/dist/routes/access-tokens.js +1 -1
  306. package/dist/routes/actor-labels.js +1 -1
  307. package/dist/routes/connect.js +1 -1
  308. package/dist/routes/dispatch.js +103 -0
  309. package/dist/routes/mcp.js +170 -79
  310. package/dist/routes/oauth.js +3 -1
  311. package/dist/routes/operator.js +2 -2
  312. package/dist/routes/shared.d.ts +5 -4
  313. package/dist/routes/shared.js +244 -177
  314. package/dist/routes/skills.js +10 -8
  315. package/dist/routes/ui-config.js +27 -3
  316. package/dist/routes/ui-data.js +29 -22
  317. package/dist/routes/ui.js +1 -1
  318. package/dist/runtime/call-admission.js +0 -26
  319. package/dist/runtime/call-entry.js +30 -0
  320. package/dist/runtime/call.js +556 -0
  321. package/dist/runtime/connector-scope.js +7 -0
  322. package/dist/runtime/credential-probe.js +34 -0
  323. package/dist/runtime/executor.js +40 -0
  324. package/dist/runtime/native-storage.js +63 -0
  325. package/dist/runtime/registry.js +27 -0
  326. package/dist/runtime/request-scope.js +72 -0
  327. package/dist/runtime/run.js +15 -7
  328. package/dist/runtime/shared-read.js +34 -5
  329. package/dist/runtime/storage.js +34 -21
  330. package/dist/sent-secrets.d.ts +194 -37
  331. package/dist/sent-secrets.js +1743 -340
  332. package/dist/server.js +5 -56
  333. package/dist/skills.js +330 -224
  334. package/dist/sqlite.js +15 -14
  335. package/dist/storage/d1-copy.js +255 -0
  336. package/dist/storage/d1-driver.js +15 -0
  337. package/dist/storage/keys.js +28 -0
  338. package/dist/storage/sql-driver.js +88 -0
  339. package/dist/storage/sql-native.js +544 -0
  340. package/dist/storage/sql-schema.d.ts +6 -0
  341. package/dist/storage/sql-schema.js +15 -0
  342. package/dist/storage/sql.d.ts +14 -20
  343. package/dist/storage/sql.js +29 -535
  344. package/dist/timeout.js +0 -26
  345. package/dist/tool-safety.js +20 -10
  346. package/dist/types.d.ts +11 -99
  347. package/dist/upward.js +3 -0
  348. package/dist/usage-guide.js +3 -1
  349. package/dist/validate.d.ts +4 -2
  350. package/dist/validate.js +86 -9
  351. package/dist/version.d.ts +1 -1
  352. package/dist/version.js +1 -1
  353. package/dist/worker.js +312 -292
  354. package/dist/workspace-subject.js +5 -1
  355. package/dist/write-outcome.js +454 -34
  356. package/documentation/README.md +27 -7
  357. package/documentation/architecture.md +2550 -186
  358. package/documentation/auth.md +821 -98
  359. package/documentation/code-mode.md +56 -21
  360. package/documentation/deploying.md +164 -20
  361. package/documentation/hosted-mcp-migration.md +460 -0
  362. package/documentation/integrating.md +144 -105
  363. package/documentation/meta-tools.md +411 -34
  364. package/documentation/operating.md +28 -4
  365. package/documentation/operator-tests.md +11 -2
  366. package/documentation/operator-ui.md +103 -33
  367. package/documentation/provider-auth-migration.md +38 -63
  368. package/documentation/provider-migration-0.29.md +10 -5
  369. package/documentation/provider-migration-0.35.md +975 -0
  370. package/documentation/providers.md +965 -0
  371. package/examples/worker/AGENTS.md +23 -9
  372. package/examples/worker/README.md +254 -135
  373. package/examples/worker/alchemy.run.ts +116 -0
  374. package/examples/worker/gitignore +10 -0
  375. package/examples/worker/src/connecta.config.ts +23 -21
  376. package/examples/worker/wrangler.jsonc +14 -5
  377. package/guides/connecting-agents.md +157 -0
  378. package/guides/how-it-works.md +156 -0
  379. package/guides/safety-and-trust.md +153 -0
  380. package/package.json +27 -42
  381. package/templates/node/.env.example +3 -0
  382. package/templates/node/README.md +22 -2
  383. package/templates/node/docker-compose.yml +1 -0
  384. package/templates/node/package.json +3 -2
  385. package/templates/node/src/connecta.config.ts +8 -0
  386. package/dist/catalog-drift.d.ts +0 -134
  387. package/dist/catalog-drift.js +0 -493
  388. package/dist/providers/_shared/request-token.js +0 -83
  389. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  390. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  391. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  392. package/dist/providers/basecamp/index.d.ts +0 -43
  393. package/dist/providers/basecamp/index.js +0 -997
  394. package/dist/providers/basecamp/skill.generated.js +0 -9
  395. package/dist/providers/cloudflare/rest.d.ts +0 -37
  396. package/dist/providers/cloudflare/rest.js +0 -1078
  397. package/dist/providers/github/hosted.js +0 -218
  398. package/dist/providers/github/rest.js +0 -321
  399. package/dist/providers/infisical/index.d.ts +0 -7
  400. package/dist/providers/infisical/index.js +0 -713
  401. package/dist/providers/infisical/skill.generated.js +0 -8
  402. package/dist/providers/linear/index.d.ts +0 -55
  403. package/dist/providers/linear/index.js +0 -352
  404. package/dist/providers/linear/skill.generated.js +0 -8
  405. package/dist/providers/mixpanel/index.d.ts +0 -57
  406. package/dist/providers/mixpanel/index.js +0 -468
  407. package/dist/providers/mixpanel/operations.js +0 -20
  408. package/dist/providers/mixpanel/rest.js +0 -603
  409. package/dist/providers/mixpanel/skill.generated.js +0 -10
  410. package/dist/providers/mixpanel/value-safety.js +0 -16
  411. package/dist/providers/notion/index.d.ts +0 -65
  412. package/dist/providers/notion/index.js +0 -368
  413. package/dist/providers/notion/openapi.generated.js +0 -75
  414. package/dist/providers/notion/rest.d.ts +0 -24
  415. package/dist/providers/notion/rest.js +0 -1226
  416. package/dist/providers/notion/skill.generated.js +0 -11
  417. package/dist/providers/notion/value-safety.js +0 -56
  418. package/dist/providers/revenuecat/index.d.ts +0 -49
  419. package/dist/providers/revenuecat/index.js +0 -557
  420. package/dist/providers/revenuecat/skill.generated.js +0 -11
  421. package/dist/providers/stripe/rest.d.ts +0 -14
  422. package/dist/providers/stripe/rest.js +0 -402
  423. package/dist/providers/vercel/rest.d.ts +0 -20
  424. package/dist/providers/vercel/rest.js +0 -1020
  425. package/dist/providers/wispr-flow/index.d.ts +0 -14
  426. package/dist/providers/wispr-flow/index.js +0 -117
  427. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  428. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,1226 +0,0 @@
1
- import { retryAfterMs } from "../../connectors/guarded-fetch.js";
2
- import { ConnectorCallError } from "../../errors.js";
3
- import { OperationIndex } from "../_shared/rest/operation-index.js";
4
- import { callRest, restCall, restTools, restTransport, } from "../_shared/rest/tools.js";
5
- import { carriesWithheldLink, valueSafety } from "../_shared/rest/value-safety.js";
6
- import { openapi } from "./openapi.generated.js";
7
- import { NOTION_FILE_HOSTS, NOTION_VALUE_SAFETY, sanitizeLinks } from "./value-safety.js";
8
- /** Notion's REST origin. Every request this connector sends goes to exactly this host. */
9
- export const NOTION_API_BASE_URL = "https://api.notion.com";
10
- /**
11
- * Pinned with no override, and equal to the pinned index's `Notion-Version`
12
- * (a test holds them together, so regenerating the index against a newer
13
- * version fails until this moves with it). Notion's date-named versions keep
14
- * working indefinitely, which makes an override look harmless; it is not.
15
- * `2026-03-11` is where databases split into data sources, `archived` became
16
- * `in_trash`, and block append took a `position` object instead of an `after`
17
- * string. Every projection, write body, and index contract here assumes those
18
- * shapes, so an older version would return quietly wrong results rather than
19
- * fail loudly.
20
- */
21
- export const NOTION_API_VERSION = "2026-03-11";
22
- /** Notion's hard cap on `page_size` for every paginated endpoint. */
23
- export const MAX_PAGE_SIZE = 100;
24
- /** Page retrieval caps `filter_properties` at 100; data-source queries declare no cap. */
25
- const MAX_PAGE_PROPERTY_FILTERS = 100;
26
- /** Notion's cap on `children` per append, and on blocks per page create. */
27
- const MAX_CHILDREN_PER_REQUEST = 100;
28
- /** Notion's cap on one rich-text run's `text.content`, and on runs in one rich-text array. */
29
- const MAX_TEXT_CONTENT = 2_000;
30
- const MAX_RICH_TEXT_RUNS = 100;
31
- /**
32
- * Ceiling on the fetches one `integration_get_page_content` walk may spend. Call admission
33
- * meters tool calls, not the requests inside them, so a deep `depth` would
34
- * otherwise drain the budget invisibly; the walk stops here and reports
35
- * `truncated: true` instead.
36
- */
37
- const MAX_CONTENT_REQUESTS = 20;
38
- /**
39
- * The largest response this connection will read. `page_size` tops out at
40
- * 100 and every payload is JSON, so four mebibytes is a ceiling on absurdity,
41
- * not a budget any real read has to think about.
42
- */
43
- const NOTION_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
44
- const READ_CAP_MS = 60_000;
45
- /**
46
- * POSTs that only read, admitted by `notion_api_read`. Each is a query over
47
- * existing objects that persists nothing. `POST /v1/views/{view_id}/queries`
48
- * is deliberately absent: it creates a stored view query that lives until
49
- * `DELETE /v1/views/{view_id}/queries/{query_id}` removes it.
50
- */
51
- const READ_POSTS = [
52
- ["POST", "/v1/search", "Searches titles shared with the integration; Notion persists nothing."],
53
- [
54
- "POST",
55
- "/v1/data_sources/{data_source_id}/query",
56
- "Lists a data source's rows by filter and sort; persists nothing.",
57
- ],
58
- ["POST", "/v1/blocks/meeting_notes/query", "Lists meeting notes by filter and sort; persists nothing."],
59
- ["POST", "/v1/agents/query", "Lists custom agents by name, filter, and sort; persists nothing."],
60
- ["POST", "/v1/sessions/query", "Lists agent sessions by title, filter, and sort; persists nothing."],
61
- ["POST", "/v1/sessions/{session_id}/events/query", "Lists one session's events; persists nothing."],
62
- ];
63
- function isRecord(value) {
64
- return typeof value === "object" && value !== null && !Array.isArray(value);
65
- }
66
- function invalid(message) {
67
- throw new ConnectorCallError("invalid_args", message);
68
- }
69
- /**
70
- * Notion repeats `Retry-After` in the body as `additional_data.retry_after`,
71
- * a string of whole seconds, so a proxy that drops the header still leaves a
72
- * window to wait out.
73
- */
74
- function bodyRetryAfterMs(additional) {
75
- const raw = isRecord(additional) ? additional["retry_after"] : undefined;
76
- const seconds = typeof raw === "string" || typeof raw === "number" ? Number(raw) : Number.NaN;
77
- return Number.isFinite(seconds) && seconds >= 0 ? Math.trunc(seconds * 1000) : undefined;
78
- }
79
- /** A Notion object id, dashed or not: the only committed-id shape shown when error text is withheld. */
80
- const NOTION_ID = /^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$/i;
81
- /**
82
- * The ids a 503 reports as already committed, so a caller reads them instead
83
- * of repeating the write. For an operation whose error text is withheld, only
84
- * ids shaped as Notion ids are named, since anything else could echo what the
85
- * request sent; the write is still reported as committed.
86
- */
87
- function committedIds(additional, withheld) {
88
- if (!isRecord(additional))
89
- return "";
90
- const resource = additional["committed_resource_id"];
91
- const children = Array.isArray(additional["committed_child_ids"])
92
- ? additional["committed_child_ids"].filter((child) => typeof child === "string")
93
- : [];
94
- if (typeof resource !== "string" || resource === "")
95
- return "";
96
- if (withheld && ![resource, ...children].every((committed) => NOTION_ID.test(committed))) {
97
- return " Notion committed the write before failing; what it committed is withheld because this operation handles credentials. Read the object instead of repeating the write.";
98
- }
99
- return children.length
100
- ? ` Notion committed the write before failing: it appended ${children.length} block(s) (${children.join(", ")}) under ${resource}. Read that block instead of repeating the write.`
101
- : ` Notion committed the write before failing: the object it created or changed is ${resource}. Read it instead of repeating the write.`;
102
- }
103
- /**
104
- * Notion's own next step for a failed write, `additional_data.retry_guidance`.
105
- * An update to an existing object that was saved anyway names no committed
106
- * id, only this, so it is relayed as Notion wrote it.
107
- */
108
- function retryGuidance(additional) {
109
- const guidance = isRecord(additional) ? additional["retry_guidance"] : undefined;
110
- const lines = (Array.isArray(guidance) ? guidance : [guidance]).filter((line) => typeof line === "string" && line.trim() !== "");
111
- return lines.map((line) => line.trim()).join(" ");
112
- }
113
- /**
114
- * Map to what the caller should do next, not to what Notion's `code` says
115
- * happened. The two that are easy to mistranslate:
116
- *
117
- * - **403 is not an authentication failure.** The token is fine; the integration
118
- * lacks a capability, was never shared the object, or the workspace is past
119
- * its block limit, and neither `authorize_connector` nor a new token can fix
120
- * it. Notion's message says which. Non-retryable call failure.
121
- * - **404 does not prove absence.** Notion returns `object_not_found` both for an
122
- * object that does not exist and for one never shared with the integration, and
123
- * will not say which — so not `not_found`, which exists to assert absence
124
- * (H11). The message names both possibilities.
125
- *
126
- * A 503 on a write may still have been saved. When Notion names what it
127
- * committed, or gives its own retry guidance, the failure says so and stops
128
- * being retryable; otherwise the shared path reports a write's outcome as
129
- * unknown (INV-9).
130
- *
131
- * `withheld` replaces every free-text field (message, retry guidance,
132
- * committed ids) for an operation whose error text the review withholds.
133
- * Text that does pass has had its pre-signed file links withheld first
134
- * (`sanitizeLinks`, as on success), since a message can echo the Markdown it
135
- * rejected.
136
- */
137
- function notionFailure(status, headers, payload, withheld = false) {
138
- const body = isRecord(payload) ? payload : undefined;
139
- const code = typeof body?.["code"] === "string" ? body["code"] : undefined;
140
- const additional = body?.["additional_data"];
141
- // For an operation the value-safety review withholds, Notion's message is
142
- // replaced; its code still routes.
143
- const detail = withheld
144
- ? `Notion answered HTTP ${status}; its message is withheld because this operation handles credentials.`
145
- : typeof body?.["message"] === "string" && body["message"].trim()
146
- ? body["message"].trim()
147
- : `Notion returned HTTP ${status}.`;
148
- const retryAfter = retryAfterMs(headers) ?? bodyRetryAfterMs(additional);
149
- const labelled = code ? `Notion ${code}: ${detail}` : detail;
150
- if (status === 429) {
151
- const reason = isRecord(additional) ? additional["rate_limit_reason"] : undefined;
152
- if (reason === "public_api_request_blocked") {
153
- return new ConnectorCallError("connector_call_failed", `${labelled} (limit: ${reason}) Notion has restricted this integration's API access. Retrying will not help; an operator must contact Notion support.`, { retryable: false });
154
- }
155
- const scope = reason === "public_api_space_request_rate_limit"
156
- ? " The workspace's shared budget across all its connections is spent, and its window can exceed a minute."
157
- : " Notion allows each integration 180 requests a minute (600 on Business and Enterprise), and a separate budget shared by the whole workspace.";
158
- return new ConnectorCallError("rate_limited", `${labelled}${typeof reason === "string" ? ` (limit: ${reason})` : ""}${scope} Wait out the retry window.`, { retryAfterMs: retryAfter ?? 1_000 });
159
- }
160
- if (status === 529) {
161
- // Notion documents 529 alongside 429: back off and respect Retry-After.
162
- return new ConnectorCallError("unavailable", `${labelled} Notion is overloaded; retry after the reported window.`, {
163
- retryAfterMs: retryAfter ?? 5_000,
164
- });
165
- }
166
- if (status === 401) {
167
- return new ConnectorCallError("auth_required", `${labelled} The Notion integration token is missing or invalid. Call authorize_connector for recovery options. When available, an operator can set a valid token in this connection in the operator UI.`);
168
- }
169
- if (status === 403) {
170
- return new ConnectorCallError("provider_permission_denied", `${labelled} The token is valid but Notion refused this operation, and Notion's message says why. Usually an operator must enable the matching capability on the Notion integration (comment capabilities are off by default) or share the object with it; a workspace past its plan's block limit also answers 403. Re-authorizing will not help.`, { retryable: false });
171
- }
172
- if (status === 404) {
173
- return new ConnectorCallError("connector_call_failed", `${labelled} Notion returns this both for an object that does not exist and for one that exists but has not been shared with this integration — do not treat it as proof of deletion. Confirm the id, then confirm the page or database is shared with the integration in Notion.`, { retryable: false });
174
- }
175
- if (status === 400) {
176
- // Every documented 400 (validation_error, invalid_json, invalid_request,
177
- // invalid_request_url, missing_version, invalid_beta) is a malformed
178
- // request, which is exactly what invalid_args means to the caller.
179
- return new ConnectorCallError("invalid_args", labelled);
180
- }
181
- if (status === 409) {
182
- return new ConnectorCallError("unavailable", `${labelled} Notion reported a write conflict; this is safe to retry.`, {
183
- retryAfterMs: retryAfter ?? 1_000,
184
- });
185
- }
186
- if (status >= 500) {
187
- const committed = committedIds(additional, withheld);
188
- if (committed) {
189
- return new ConnectorCallError("connector_call_failed", `${labelled}${committed}`, { retryable: false });
190
- }
191
- const guidance = retryGuidance(additional);
192
- if (guidance) {
193
- return new ConnectorCallError("connector_call_failed", withheld
194
- ? `${labelled} Notion sent retry guidance, withheld because this operation handles credentials. Read the object before repeating the write.`
195
- : `${labelled} Notion's guidance: ${guidance}`, { retryable: false });
196
- }
197
- return new ConnectorCallError("unavailable", `${labelled} Notion is failing upstream.`, retryAfter !== undefined ? { retryAfterMs: retryAfter } : {});
198
- }
199
- return new ConnectorCallError("connector_call_failed", labelled, { retryable: false });
200
- }
201
- /**
202
- * Notion lists carry `results` and `has_more`; the cursor, when there is one,
203
- * is `next_cursor` and goes back as `start_cursor`: a query parameter on GET
204
- * lists, a body field on the read-only POST queries. Two shapes differ:
205
- *
206
- * - `POST /v1/views/{view_id}/queries` answers a `view_query` object whose
207
- * first page continues at `GET /v1/views/{view_id}/queries/{query_id}`
208
- * (until `expires_at`), so the page names that path.
209
- * - `POST /v1/blocks/meeting_notes/query` has no cursor at all: `has_more`
210
- * past its `limit` (50 at most) can only be answered by a narrower filter.
211
- * - A data source or view query stops at 10,000 results with `has_more: false`
212
- * and `request_status.type: "incomplete"`. That is a cut-off with no cursor,
213
- * reported like the meeting-note one: `hasMore` without `next`.
214
- *
215
- * Every shape keeps `hasMore`, so a `select` that drops the body's own
216
- * completeness fields cannot hide that a list was cut short.
217
- */
218
- function page(data, call) {
219
- if (!isRecord(data) || typeof data["has_more"] !== "boolean" || !Array.isArray(data["results"]))
220
- return undefined;
221
- if (!data["has_more"] && incomplete(data))
222
- return { hasMore: true };
223
- const hasMore = data["has_more"];
224
- const next = typeof data["next_cursor"] === "string" && data["next_cursor"] !== "" ? data["next_cursor"] : undefined;
225
- if (!hasMore || !next)
226
- return { hasMore };
227
- if (data["object"] === "view_query" && typeof data["id"] === "string" && typeof data["view_id"] === "string") {
228
- return {
229
- hasMore,
230
- next,
231
- param: "start_cursor",
232
- in: "query",
233
- path: `/v1/views/${encodeURIComponent(data["view_id"])}/queries/${encodeURIComponent(data["id"])}`,
234
- };
235
- }
236
- return { hasMore, next, param: "start_cursor", ...(call.method === "GET" ? {} : { in: "body" }) };
237
- }
238
- /** Whether a query stopped at Notion's per-query result cap rather than at its last row. */
239
- function incomplete(data) {
240
- const status = data["request_status"];
241
- return isRecord(status) && status["type"] === "incomplete";
242
- }
243
- /**
244
- * A read copied into a write: a file link whose signature this connector
245
- * withheld would be stored as the placeholder, or fail an exact `old_str`
246
- * match. Refused before dispatch, named tools and generic writes alike.
247
- */
248
- function refuseWithheldLinks(call) {
249
- if (call.method === "GET" || call.method === "HEAD")
250
- return;
251
- if (READ_POSTS.some(([method, path]) => method === call.method && path === call.op.path))
252
- return;
253
- if (!carriesWithheldLink(call.body, NOTION_FILE_HOSTS))
254
- return;
255
- invalid("This write carries a Notion file link whose signature Connecta withheld ([redacted]), so Notion would store the placeholder. A redacted file URL cannot be copied into a write: re-upload the file (POST /v1/file_uploads), reference the existing block instead, and match Markdown edits on the text around the media rather than its URL.");
256
- }
257
- /** The pinned index, shared by every Notion REST connector in the deployment. */
258
- let index;
259
- let transport;
260
- function notionIndex() {
261
- index ??= new OperationIndex(openapi, { vendor: "notion", title: "Notion" });
262
- return index;
263
- }
264
- /** The reviewed value-safety table over the pinned index. */
265
- const SAFETY = valueSafety(NOTION_VALUE_SAFETY, () => notionIndex());
266
- function notionTransport() {
267
- transport ??= restTransport({
268
- provider: "Notion",
269
- baseUrl: NOTION_API_BASE_URL,
270
- headers: { "Notion-Version": NOTION_API_VERSION },
271
- maxResponseBytes: NOTION_MAX_RESPONSE_BYTES,
272
- timeoutMs: READ_CAP_MS,
273
- authenticate: async (ctx) => {
274
- const token = (await ctx.credential?.get())?.trim();
275
- if (!token) {
276
- throw new ConnectorCallError("auth_required", "No Notion integration token is configured for this connector. Call authorize_connector for recovery options. When available, an operator can add the token in this connection in the operator UI.");
277
- }
278
- return { Authorization: `Bearer ${token}` };
279
- },
280
- });
281
- return transport;
282
- }
283
- const VENDOR = {
284
- vendor: "notion",
285
- title: "Notion",
286
- get index() {
287
- return notionIndex();
288
- },
289
- transport(server) {
290
- return server === undefined
291
- ? notionTransport()
292
- : `This operation is served from ${server}, which this connector does not reach.`;
293
- },
294
- failure: (status, headers, payload, call) => notionFailure(status, headers, sanitizeLinks(payload), call ? SAFETY.withholdsErrors(call.op) : false),
295
- readPosts: READ_POSTS,
296
- valueSafety: SAFETY,
297
- // Not an envelope to unwrap: page Markdown carries pre-signed file links
298
- // inside its text, where redaction's URL rule cannot reach. Every success
299
- // response passes here, named tools and paging included.
300
- result: sanitizeLinks,
301
- admit: refuseWithheldLinks,
302
- page,
303
- upload: 'Create the upload with POST /v1/file_uploads and mode "external_url" instead; this connector sends no multipart bodies.',
304
- bodyHint: "Notion's JSON request body as notion_api_details lists it",
305
- };
306
- /**
307
- * Send one named-tool request on the shared path (same transport, framing,
308
- * failure mapping) and insist on an object back. A sent write whose answer
309
- * cannot be read stays unknown: the caller is told not to repeat it.
310
- */
311
- async function notion(ctx, method, path, input = {}) {
312
- const { data } = await callRest(VENDOR, restCall(VENDOR, method, path, input), ctx);
313
- if (!isRecord(data)) {
314
- throw new ConnectorCallError("connector_call_failed", "Notion returned an unreadable or non-object response for a successful status. The request was sent, but its result could not be read; do not repeat a write to recover its result.", { retryable: false });
315
- }
316
- return data;
317
- }
318
- const id = (value) => encodeURIComponent(String(value));
319
- // Projections. Notion wraps every property in a discriminated object, every
320
- // string in an array of annotated rich-text runs, and every user in a nested
321
- // object, so a small query is tens of kilobytes of structure around a few
322
- // hundred bytes of meaning. Every named read collapses that to ids, plain
323
- // text, and flattened values. Nothing is lost for good: the reads that can
324
- // drop something take `raw: true` and return Notion's untouched response.
325
- //
326
- // Unknown types unwrap rather than switch exhaustively, because Notion ships
327
- // additive changes to every pinned version at once — a property type newer than
328
- // this release degrades to its raw value instead of vanishing.
329
- /** Concatenate a rich-text array to its plain text. Safe for every variant. */
330
- function plainText(value) {
331
- if (!Array.isArray(value))
332
- return "";
333
- return value.map((run) => (typeof run?.plain_text === "string" ? run.plain_text : "")).join("");
334
- }
335
- /**
336
- * Wrap a plain string as the rich-text array Notion expects. Notion rejects a
337
- * run over 2,000 characters, so a longer string becomes consecutive runs, cut
338
- * between UTF-16 surrogate pairs, never inside one. Past 100 runs Notion
339
- * rejects the array itself, so that refuses here with the way forward.
340
- */
341
- function richText(value, field) {
342
- const runs = [];
343
- let rest = value;
344
- while (rest.length > MAX_TEXT_CONTENT) {
345
- const high = rest.charCodeAt(MAX_TEXT_CONTENT - 1);
346
- const cut = high >= 0xd800 && high <= 0xdbff ? MAX_TEXT_CONTENT - 1 : MAX_TEXT_CONTENT;
347
- runs.push(rest.slice(0, cut));
348
- rest = rest.slice(cut);
349
- }
350
- runs.push(rest);
351
- if (runs.length > MAX_RICH_TEXT_RUNS) {
352
- invalid(`${field} is ${value.length} characters; Notion accepts at most ${MAX_RICH_TEXT_RUNS} runs of ${MAX_TEXT_CONTENT} characters in one rich-text value. Split it across several blocks.`);
353
- }
354
- return runs.map((content) => ({ type: "text", text: { content } }));
355
- }
356
- function userRef(value) {
357
- if (!value || typeof value !== "object")
358
- return null;
359
- return {
360
- id: typeof value.id === "string" ? value.id : null,
361
- ...(typeof value.name === "string" ? { name: value.name } : {}),
362
- };
363
- }
364
- /**
365
- * Flatten one Notion property value.
366
- *
367
- * The `default` branch is not laziness: Notion adds property types to every
368
- * API version simultaneously, so an exhaustive switch would start returning
369
- * `undefined` for a type that shipped after this release. Unwrapping
370
- * `value[value.type]` degrades an unknown type to its raw payload instead.
371
- */
372
- function projectPropertyValue(value) {
373
- const type = value?.type;
374
- switch (type) {
375
- case "title":
376
- case "rich_text":
377
- return plainText(value[type]);
378
- case "number":
379
- case "checkbox":
380
- case "url":
381
- case "email":
382
- case "phone_number":
383
- case "created_time":
384
- case "last_edited_time":
385
- return value[type] ?? null;
386
- case "select":
387
- case "status":
388
- return value[type]?.name ?? null;
389
- case "multi_select":
390
- return (value.multi_select ?? []).map((option) => option?.name ?? null);
391
- case "date":
392
- return value.date
393
- ? {
394
- start: value.date.start ?? null,
395
- end: value.date.end ?? null,
396
- ...(value.date.time_zone ? { time_zone: value.date.time_zone } : {}),
397
- }
398
- : null;
399
- case "people":
400
- return (value.people ?? []).map(userRef);
401
- case "created_by":
402
- case "last_edited_by":
403
- return userRef(value[type]);
404
- case "files":
405
- return (value.files ?? []).map((file) => ({
406
- name: file?.name ?? null,
407
- // A `file` upload carries a signed URL that expires; an `external` one
408
- // is a plain link. Agents want the link either way.
409
- url: file?.external?.url ?? file?.file?.url ?? null,
410
- }));
411
- case "relation":
412
- return (value.relation ?? [])
413
- .map((related) => related?.id ?? null)
414
- .filter((related) => typeof related === "string");
415
- case "formula":
416
- // Notion answers `unsupported` for a value it cannot compute through the API.
417
- return value.formula?.type === "unsupported" ? null : (value.formula?.[value.formula?.type] ?? null);
418
- case "rollup": {
419
- const rollup = value.rollup;
420
- if (!rollup || rollup.type === "unsupported")
421
- return null;
422
- if (rollup.type === "array") {
423
- return (rollup.array ?? []).map(projectPropertyValue);
424
- }
425
- return rollup[rollup.type] ?? null;
426
- }
427
- case "unique_id":
428
- return value.unique_id?.prefix
429
- ? `${value.unique_id.prefix}-${value.unique_id.number}`
430
- : (value.unique_id?.number ?? null);
431
- case "verification":
432
- return value.verification?.state ?? null;
433
- default:
434
- return type ? (value[type] ?? null) : null;
435
- }
436
- }
437
- function projectProperties(source, select) {
438
- const properties = {};
439
- const truncated = [];
440
- if (!source || typeof source !== "object")
441
- return { properties, truncated };
442
- for (const [name, value] of Object.entries(source)) {
443
- if (select && !select.includes(name))
444
- continue;
445
- properties[name] = projectPropertyValue(value);
446
- if (value?.has_more === true) {
447
- truncated.push({ name, id: typeof value?.id === "string" ? value.id : null });
448
- }
449
- }
450
- return { properties, truncated };
451
- }
452
- /** The title property's name is arbitrary; its `type` is not. */
453
- function pageTitle(source) {
454
- if (!source || typeof source !== "object")
455
- return "";
456
- for (const value of Object.values(source)) {
457
- if (value?.type === "title")
458
- return plainText(value.title);
459
- }
460
- return "";
461
- }
462
- function parentRef(parent) {
463
- if (!parent || typeof parent !== "object")
464
- return null;
465
- const type = parent.type;
466
- if (typeof type !== "string")
467
- return null;
468
- return { type, id: typeof parent[type] === "string" ? parent[type] : null };
469
- }
470
- /**
471
- * An icon as one string: the emoji itself, a native Notion icon's name, a
472
- * workspace custom emoji as `:name:`, or an image URL.
473
- */
474
- function iconRef(icon) {
475
- if (!icon || typeof icon !== "object")
476
- return null;
477
- if (typeof icon.emoji === "string")
478
- return icon.emoji;
479
- if (icon.type === "icon" && typeof icon.icon?.name === "string")
480
- return icon.icon.name;
481
- if (icon.type === "custom_emoji" && typeof icon.custom_emoji?.name === "string") {
482
- return `:${icon.custom_emoji.name}:`;
483
- }
484
- return icon.external?.url ?? icon.file?.url ?? null;
485
- }
486
- function projectPage(page, select) {
487
- const { properties, truncated } = projectProperties(page?.properties, select);
488
- return {
489
- id: page?.id ?? null,
490
- object: "page",
491
- title: pageTitle(page?.properties),
492
- url: page?.url ?? null,
493
- parent: parentRef(page?.parent),
494
- icon: iconRef(page?.icon),
495
- created_time: page?.created_time ?? null,
496
- last_edited_time: page?.last_edited_time ?? null,
497
- created_by: userRef(page?.created_by),
498
- last_edited_by: userRef(page?.last_edited_by),
499
- in_trash: page?.in_trash === true,
500
- is_archived: page?.is_archived === true,
501
- properties,
502
- ...(truncated.length ? { truncated_properties: truncated } : {}),
503
- };
504
- }
505
- /**
506
- * Identity fields only, no properties. A 25-result search across a populated
507
- * database would otherwise drag back hundreds of flattened values for results
508
- * the agent is about to discard; `integration_get_page` fetches them for the one that hit.
509
- */
510
- function projectSearchHit(hit) {
511
- if (hit?.object === "data_source") {
512
- return {
513
- id: hit?.id ?? null,
514
- object: "data_source",
515
- title: plainText(hit?.title) || (hit?.name ?? ""),
516
- database_id: hit?.parent?.database_id ?? null,
517
- url: hit?.url ?? null,
518
- last_edited_time: hit?.last_edited_time ?? null,
519
- };
520
- }
521
- return {
522
- id: hit?.id ?? null,
523
- object: hit?.object ?? "page",
524
- title: hit?.object === "page" ? pageTitle(hit?.properties) : plainText(hit?.title),
525
- url: hit?.url ?? null,
526
- parent: parentRef(hit?.parent),
527
- last_edited_time: hit?.last_edited_time ?? null,
528
- };
529
- }
530
- /** Flatten one block to its text plus the few fields its type actually adds. */
531
- function projectBlock(block, depth) {
532
- const type = block?.type;
533
- const payload = type ? block?.[type] : undefined;
534
- const projected = {
535
- id: block?.id ?? null,
536
- type: type ?? "unsupported",
537
- depth,
538
- text: plainText(payload?.rich_text),
539
- has_children: block?.has_children === true,
540
- };
541
- switch (type) {
542
- case "to_do":
543
- projected["checked"] = payload?.checked === true;
544
- break;
545
- case "code":
546
- projected["language"] = payload?.language ?? null;
547
- break;
548
- case "child_page":
549
- case "child_database":
550
- // Notion gives these a plain string title, not a rich-text array.
551
- projected["text"] = typeof payload?.title === "string" ? payload.title : "";
552
- break;
553
- case "image":
554
- case "video":
555
- case "file":
556
- case "pdf":
557
- projected["url"] = payload?.external?.url ?? payload?.file?.url ?? null;
558
- projected["text"] = plainText(payload?.caption);
559
- break;
560
- case "bookmark":
561
- case "embed":
562
- case "link_preview":
563
- projected["url"] = payload?.url ?? null;
564
- projected["text"] = plainText(payload?.caption);
565
- break;
566
- case "equation":
567
- projected["text"] = payload?.expression ?? "";
568
- break;
569
- case "table_row":
570
- projected["cells"] = (payload?.cells ?? []).map(plainText);
571
- break;
572
- case "callout":
573
- projected["icon"] = iconRef(payload?.icon);
574
- break;
575
- default:
576
- // A block type this projection does not model keeps its payload verbatim
577
- // rather than collapsing to an empty string, so a block type newer than
578
- // this release still carries its content.
579
- if (carriesUnprojectedContent(payload))
580
- projected["raw"] = payload;
581
- break;
582
- }
583
- return projected;
584
- }
585
- function carriesUnprojectedContent(payload) {
586
- if (!payload || typeof payload !== "object")
587
- return false;
588
- if (Array.isArray(payload.rich_text))
589
- return false;
590
- return Object.keys(payload).some((key) => key !== "color");
591
- }
592
- /**
593
- * A data source's schema, reduced to what a caller needs to filter and write.
594
- *
595
- * Select and status options are kept because a filter or a write that invents
596
- * an option name fails; everything else about a property collapses to its type.
597
- */
598
- function projectSchemaProperty(property) {
599
- const type = property?.type;
600
- const projected = { id: property?.id ?? null, type: type ?? null };
601
- const payload = type ? property?.[type] : undefined;
602
- if (type === "select" || type === "multi_select") {
603
- projected["options"] = (payload?.options ?? []).map((option) => option?.name ?? null);
604
- }
605
- else if (type === "status") {
606
- projected["options"] = (payload?.options ?? []).map((option) => option?.name ?? null);
607
- projected["groups"] = (payload?.groups ?? []).map((group) => group?.name ?? null);
608
- }
609
- else if (type === "relation") {
610
- // Requests must send data_source_id; responses carry both. Give the caller
611
- // the one it is allowed to write with.
612
- projected["relation_data_source_id"] = payload?.data_source_id ?? null;
613
- }
614
- else if (type === "formula") {
615
- projected["expression"] = payload?.expression ?? null;
616
- }
617
- else if (type === "rollup") {
618
- projected["rollup"] = {
619
- relation_property_name: payload?.relation_property_name ?? null,
620
- rollup_property_name: payload?.rollup_property_name ?? null,
621
- function: payload?.function ?? null,
622
- };
623
- }
624
- return projected;
625
- }
626
- // Shared schema fragments. The compact renderer inlines every property
627
- // description, so these are paid for once per tool that uses them; the long
628
- // versions live in the usage guide, which is fetched once rather than per tool
629
- // ([#342](https://github.com/zackbart/connecta/issues/342)).
630
- const RAW_PROPERTY = {
631
- type: "boolean",
632
- description: "Return Notion's much larger unprojected response instead of the lean projection.",
633
- };
634
- const PAGE_SIZE_PROPERTY = {
635
- type: "integer",
636
- minimum: 1,
637
- maximum: MAX_PAGE_SIZE,
638
- description: `Results per page (1-${MAX_PAGE_SIZE}). Defaults to the connector's configured page size.`,
639
- };
640
- const START_CURSOR_PROPERTY = {
641
- type: "string",
642
- description: "Opaque next_cursor from the previous response. Pass it back verbatim.",
643
- };
644
- const PROPERTY_SELECT = {
645
- type: "array",
646
- items: { type: "string" },
647
- description: "Return only these property names. Omit for all. The cheapest way to shrink a result.",
648
- };
649
- function listOutputSchema(itemSchema) {
650
- return {
651
- type: "object",
652
- properties: {
653
- results: { type: "array", items: itemSchema },
654
- has_more: { type: "boolean", description: "True when another page exists." },
655
- next_cursor: { type: ["string", "null"], description: "Pass as start_cursor to fetch the next page." },
656
- },
657
- required: ["results", "has_more", "next_cursor"],
658
- };
659
- }
660
- const PAGE_OUTPUT_SCHEMA = {
661
- type: "object",
662
- description: "Projected page. With raw: true this is Notion's full page object instead.",
663
- properties: {
664
- id: { type: "string" },
665
- object: { type: "string" },
666
- title: { type: "string", description: "Plain text of the title property." },
667
- url: { type: ["string", "null"] },
668
- parent: {
669
- type: ["object", "null"],
670
- properties: { type: { type: "string" }, id: { type: ["string", "null"] } },
671
- required: ["type", "id"],
672
- },
673
- icon: { type: ["string", "null"], description: "Emoji, native icon name, :custom_emoji:, or icon URL." },
674
- created_time: { type: ["string", "null"] },
675
- last_edited_time: { type: ["string", "null"] },
676
- created_by: { type: ["object", "null"] },
677
- last_edited_by: { type: ["object", "null"] },
678
- in_trash: { type: "boolean" },
679
- is_archived: { type: "boolean" },
680
- properties: {
681
- type: "object",
682
- description: "Property name to flattened value: text for title/rich_text, name for select/status, array of names for multi_select, array of page ids for relation.",
683
- },
684
- truncated_properties: {
685
- type: "array",
686
- items: {
687
- type: "object",
688
- properties: {
689
- name: { type: "string" },
690
- id: { type: ["string", "null"], description: "The property_id in the property-item path." },
691
- },
692
- required: ["name", "id"],
693
- },
694
- description: "Properties Notion truncated at 25 entries. Read each in full with notion_api_read GET /v1/pages/{page_id}/properties/{property_id}.",
695
- },
696
- },
697
- required: ["id", "title", "properties"],
698
- };
699
- const BLOCK_OUTPUT_SCHEMA = {
700
- type: "object",
701
- properties: {
702
- id: { type: "string" },
703
- type: { type: "string" },
704
- depth: { type: "integer", description: "0 for direct children, 1 for their children, and so on." },
705
- text: { type: "string", description: "Plain text of the block." },
706
- has_children: { type: "boolean" },
707
- checked: { type: "boolean", description: "to_do blocks only." },
708
- language: { type: ["string", "null"], description: "code blocks only." },
709
- url: { type: ["string", "null"], description: "Media and link blocks." },
710
- cells: { type: "array", items: { type: "string" }, description: "table_row blocks only." },
711
- icon: { type: ["string", "null"], description: "callout blocks only." },
712
- raw: {
713
- type: "object",
714
- description: "The block type's untouched payload, present only for types this projection does not model and whose content is not plain rich text.",
715
- },
716
- },
717
- required: ["id", "type", "depth", "text", "has_children"],
718
- };
719
- function resolvePageSize(requested, fallback) {
720
- if (typeof requested === "number" && Number.isFinite(requested)) {
721
- return Math.min(Math.max(Math.trunc(requested), 1), MAX_PAGE_SIZE);
722
- }
723
- return fallback;
724
- }
725
- function listEnvelope(payload, results) {
726
- return { results, has_more: payload?.has_more === true, next_cursor: payload?.next_cursor ?? null };
727
- }
728
- function mappedListEnvelope(payload, project) {
729
- return listEnvelope(payload, (payload?.results ?? []).map(project));
730
- }
731
- function pagination(args, defaultPageSize) {
732
- return {
733
- page_size: resolvePageSize(args.page_size, defaultPageSize),
734
- ...(args.start_cursor ? { start_cursor: args.start_cursor } : {}),
735
- };
736
- }
737
- /** Both page GETs and data-source POST queries filter properties on the URL. */
738
- function propertyQuery(properties, maxFilters) {
739
- if (properties === undefined)
740
- return {};
741
- // The title column's name varies, but its id is always `title`. Fetch it
742
- // even when projection drops the column so the top-level title survives.
743
- const filters = [...new Set([...properties, "title"])];
744
- // Filtering is an optimization. An oversized selection still needs every
745
- // requested column, so retrieve all properties and keep projecting locally.
746
- return maxFilters !== undefined && filters.length > maxFilters ? {} : { filter_properties: filters };
747
- }
748
- /** Exactly-one-of validation, phrased so the agent knows what to send next. */
749
- function requireExactlyOne(provided, hint) {
750
- const present = provided.filter(([, value]) => value !== undefined && value !== null && value !== "");
751
- if (present.length !== 1) {
752
- invalid(`Provide exactly one of ${provided.map(([name]) => name).join(", ")}. ${hint}`);
753
- }
754
- return present[0];
755
- }
756
- /**
757
- * The named tools. Each earns its catalog bytes over the generic set (H14):
758
- * the reads flatten Notion's wrapped properties, rich-text runs, and blocks
759
- * (and walk nesting under a request ceiling); the two writes author Notion's
760
- * rich-text and block shapes from plain text. Thin wrappers the generic tools
761
- * replace one for one (database, users, comments, property items, property
762
- * updates, trash) are gone; the guide names their operations.
763
- */
764
- function namedTools(defaultPageSize) {
765
- return [
766
- {
767
- name: "integration_search",
768
- description: "Find pages and data sources by title across everything shared with this integration. Never searches page content — use integration_query_data_source for rows inside a database. Returns identity fields only.",
769
- annotations: { readOnlyHint: true },
770
- inputSchema: {
771
- type: "object",
772
- required: [],
773
- properties: {
774
- query: {
775
- type: "string",
776
- description: "Title substring to match. Omit to list everything shared with the integration.",
777
- },
778
- object_type: {
779
- type: "string",
780
- enum: ["page", "data_source"],
781
- description: "Restrict results to pages or to data sources. Omit for both.",
782
- },
783
- sort: {
784
- type: "string",
785
- enum: ["last_edited_desc", "last_edited_asc", "relevance"],
786
- description: "Result ordering. Defaults to Notion's relevance order.",
787
- },
788
- page_size: PAGE_SIZE_PROPERTY,
789
- start_cursor: START_CURSOR_PROPERTY,
790
- raw: RAW_PROPERTY,
791
- },
792
- additionalProperties: false,
793
- },
794
- outputSchema: listOutputSchema({
795
- type: "object",
796
- properties: {
797
- id: { type: "string" },
798
- object: { type: "string", description: '"page" or "data_source".' },
799
- title: { type: "string" },
800
- url: { type: ["string", "null"] },
801
- parent: { type: ["object", "null"] },
802
- database_id: { type: ["string", "null"], description: "Data source hits only: the containing database." },
803
- last_edited_time: { type: ["string", "null"] },
804
- },
805
- required: ["id", "object", "title"],
806
- }),
807
- handler: async (args, ctx) => {
808
- const body = pagination(args, defaultPageSize);
809
- if (args.query)
810
- body["query"] = args.query;
811
- if (args.object_type)
812
- body["filter"] = { property: "object", value: args.object_type };
813
- if (args.sort === "relevance") {
814
- body["sort"] = { property: "relevance" };
815
- }
816
- else if (args.sort) {
817
- body["sort"] = {
818
- timestamp: "last_edited_time",
819
- direction: args.sort === "last_edited_asc" ? "ascending" : "descending",
820
- };
821
- }
822
- const payload = await notion(ctx, "POST", "/v1/search", { body });
823
- if (args.raw)
824
- return payload;
825
- return mappedListEnvelope(payload, projectSearchHit);
826
- },
827
- },
828
- {
829
- name: "integration_get_page",
830
- description: "Fetch one page's metadata and flattened property values by id. Returns the page's properties, not its body content — use integration_get_page_content for the blocks.",
831
- annotations: { readOnlyHint: true },
832
- inputSchema: {
833
- type: "object",
834
- properties: {
835
- page_id: { type: "string", description: "Notion page id, with or without dashes." },
836
- properties: PROPERTY_SELECT,
837
- raw: RAW_PROPERTY,
838
- },
839
- required: ["page_id"],
840
- additionalProperties: false,
841
- },
842
- outputSchema: PAGE_OUTPUT_SCHEMA,
843
- handler: async (args, ctx) => {
844
- const payload = await notion(ctx, "GET", `/v1/pages/${id(args.page_id)}`, {
845
- query: propertyQuery(args.properties, MAX_PAGE_PROPERTY_FILTERS),
846
- });
847
- if (args.raw)
848
- return payload;
849
- return projectPage(payload, args.properties);
850
- },
851
- },
852
- {
853
- name: "integration_get_page_content",
854
- // `raw: true` returns the requested level exactly as Notion sent it and
855
- // does not walk nested children, so `depth` is ignored alongside it: a
856
- // raw read of a deep page yields one level.
857
- description: "Read a page's body as a flat list of blocks reduced to plain text. Each block keeps its id, type, and depth so it can be quoted, appended after, or drilled into. Nested content requires depth > 0.",
858
- annotations: { readOnlyHint: true },
859
- inputSchema: {
860
- type: "object",
861
- properties: {
862
- block_id: {
863
- type: "string",
864
- description: "Page id, or any block id to read that block's children. A page id is a valid block id.",
865
- },
866
- depth: {
867
- type: "integer",
868
- minimum: 0,
869
- maximum: 2,
870
- description: "How many levels of nested children to follow. 0 (default) returns direct children only. Each level multiplies downstream requests.",
871
- },
872
- page_size: PAGE_SIZE_PROPERTY,
873
- start_cursor: START_CURSOR_PROPERTY,
874
- raw: {
875
- ...RAW_PROPERTY,
876
- description: `${RAW_PROPERTY["description"]} It returns this one level exactly as Notion sent it and does not walk nested children, so depth is ignored alongside it — read a child block_id directly instead.`,
877
- },
878
- },
879
- required: ["block_id"],
880
- additionalProperties: false,
881
- },
882
- outputSchema: {
883
- ...listOutputSchema(BLOCK_OUTPUT_SCHEMA),
884
- properties: {
885
- ...listOutputSchema(BLOCK_OUTPUT_SCHEMA)["properties"],
886
- truncated: {
887
- type: "boolean",
888
- description: "True when the nested walk stopped at its request ceiling. Some descendants are missing; re-read a specific block_id to continue.",
889
- },
890
- },
891
- required: ["results", "has_more", "next_cursor", "truncated"],
892
- },
893
- handler: async (args, ctx) => {
894
- const top = await notion(ctx, "GET", `/v1/blocks/${id(args.block_id)}/children`, {
895
- query: pagination(args, defaultPageSize),
896
- });
897
- if (args.raw)
898
- return top;
899
- const maxDepth = typeof args.depth === "number" ? args.depth : 0;
900
- let spent = 1;
901
- let truncated = false;
902
- const results = [];
903
- const walk = async (blocks, depth) => {
904
- for (const block of blocks) {
905
- results.push(projectBlock(block, depth));
906
- if (depth >= maxDepth || block?.has_children !== true)
907
- continue;
908
- if (spent >= MAX_CONTENT_REQUESTS) {
909
- truncated = true;
910
- continue;
911
- }
912
- spent += 1;
913
- const child = await notion(ctx, "GET", `/v1/blocks/${id(block.id)}/children`, {
914
- query: { page_size: MAX_PAGE_SIZE },
915
- });
916
- // Nested levels take their first page only; a block with more than
917
- // 100 children is re-read directly rather than paged here.
918
- if (child["has_more"] === true)
919
- truncated = true;
920
- await walk(child["results"] ?? [], depth + 1);
921
- }
922
- };
923
- await walk(top["results"] ?? [], 0);
924
- return { ...listEnvelope(top, results), truncated };
925
- },
926
- },
927
- {
928
- name: "integration_get_data_source_schema",
929
- description: "List a data source's properties with their ids, types, and select/status options. Read this before filtering, sorting, or writing — filters and property names that do not match the schema exactly are rejected.",
930
- annotations: { readOnlyHint: true },
931
- inputSchema: {
932
- type: "object",
933
- properties: {
934
- data_source_id: {
935
- type: "string",
936
- description: "Data source id from integration_search or a database's data_sources, not a database id.",
937
- },
938
- raw: RAW_PROPERTY,
939
- },
940
- required: ["data_source_id"],
941
- additionalProperties: false,
942
- },
943
- outputSchema: {
944
- type: "object",
945
- properties: {
946
- id: { type: "string" },
947
- name: { type: "string" },
948
- database_id: { type: ["string", "null"] },
949
- title_property: {
950
- type: ["string", "null"],
951
- description: "Name of the title-typed property. integration_create_page needs this to title a row.",
952
- },
953
- properties: {
954
- type: "object",
955
- description: "Property name to { id, type, options?, relation_data_source_id? }.",
956
- },
957
- },
958
- required: ["id", "name", "properties"],
959
- },
960
- handler: async (args, ctx) => {
961
- const payload = await notion(ctx, "GET", `/v1/data_sources/${id(args.data_source_id)}`);
962
- if (args.raw)
963
- return payload;
964
- const properties = {};
965
- let titleProperty = null;
966
- for (const [name, property] of Object.entries((payload["properties"] ?? {}))) {
967
- properties[name] = projectSchemaProperty(property);
968
- if (property?.type === "title")
969
- titleProperty = name;
970
- }
971
- return {
972
- id: payload["id"] ?? null,
973
- name: plainText(payload["title"]) || (payload["name"] ?? ""),
974
- database_id: payload["parent"]?.database_id ?? null,
975
- title_property: titleProperty,
976
- properties,
977
- };
978
- },
979
- },
980
- {
981
- name: "integration_query_data_source",
982
- description: "List rows in a data source with optional filtering and sorting, returning each row's properties already flattened. Requires a data_source_id, never a database_id. Narrow with the properties argument to keep results small.",
983
- annotations: { readOnlyHint: true },
984
- inputSchema: {
985
- type: "object",
986
- properties: {
987
- data_source_id: {
988
- type: "string",
989
- description: "Data source id from integration_search or a database's data_sources.",
990
- },
991
- filter: {
992
- type: "object",
993
- description: 'Notion filter object, passed through unchanged. Single condition: {"property":"Status","status":{"equals":"Done"}}. Compound: {"and":[...]} or {"or":[...]}. Property names must match integration_get_data_source_schema exactly.',
994
- },
995
- sorts: {
996
- type: "array",
997
- description: "Sort order, applied in sequence.",
998
- items: {
999
- type: "object",
1000
- properties: {
1001
- property: { type: "string", description: "Property name to sort by." },
1002
- timestamp: {
1003
- type: "string",
1004
- enum: ["created_time", "last_edited_time"],
1005
- description: "Sort by a timestamp instead of a property.",
1006
- },
1007
- direction: {
1008
- type: "string",
1009
- enum: ["ascending", "descending"],
1010
- description: "Direction for this sort.",
1011
- },
1012
- },
1013
- additionalProperties: false,
1014
- },
1015
- },
1016
- properties: PROPERTY_SELECT,
1017
- page_size: PAGE_SIZE_PROPERTY,
1018
- start_cursor: START_CURSOR_PROPERTY,
1019
- raw: RAW_PROPERTY,
1020
- },
1021
- required: ["data_source_id"],
1022
- additionalProperties: false,
1023
- },
1024
- outputSchema: {
1025
- ...listOutputSchema(PAGE_OUTPUT_SCHEMA),
1026
- properties: {
1027
- ...listOutputSchema(PAGE_OUTPUT_SCHEMA)["properties"],
1028
- incomplete: {
1029
- type: "boolean",
1030
- description: "Present when Notion stopped this query at its 10,000-result cap: has_more is false but rows are missing. Narrow the filter.",
1031
- },
1032
- },
1033
- },
1034
- handler: async (args, ctx) => {
1035
- const body = pagination(args, defaultPageSize);
1036
- if (args.filter)
1037
- body["filter"] = args.filter;
1038
- if (args.sorts)
1039
- body["sorts"] = args.sorts;
1040
- const payload = await notion(ctx, "POST", `/v1/data_sources/${id(args.data_source_id)}/query`, {
1041
- query: propertyQuery(args.properties),
1042
- body,
1043
- });
1044
- if (args.raw)
1045
- return payload;
1046
- const rows = mappedListEnvelope(payload, (row) => projectPage(row, args.properties));
1047
- return incomplete(payload) ? { ...rows, incomplete: true } : rows;
1048
- },
1049
- },
1050
- {
1051
- name: "integration_create_page",
1052
- description: "Create a page, either as a child of another page or as a row in a data source. Notion has no idempotency key: a retried create makes a second page, so confirm with search before repeating one.",
1053
- annotations: { readOnlyHint: false },
1054
- inputSchema: {
1055
- // A deliberate subset of the 2026-03-11 create contract, reviewed after
1056
- // the 0.17.0 drift check (#408). Workspace-private pages, templates,
1057
- // page placement, and the expanded icon/cover forms stay out: they
1058
- // change ownership, start asynchronous content work, control ordering,
1059
- // or depend on file surfaces, none of which extend page/row authoring.
1060
- // notion_api_write reaches the full contract.
1061
- required: [],
1062
- type: "object",
1063
- properties: {
1064
- parent_page_id: {
1065
- type: "string",
1066
- description: "Create as a child page of this page. Exactly one parent id, this or parent_data_source_id.",
1067
- },
1068
- parent_data_source_id: {
1069
- type: "string",
1070
- description: "Create as a row in this data source, not a database id. Exactly one parent id, this or parent_page_id.",
1071
- },
1072
- title: { type: "string", description: "Plain-text title." },
1073
- title_property: {
1074
- type: "string",
1075
- description: 'Name of the title-typed property, from integration_get_data_source_schema. Required in practice for a data-source parent, whose title column is rarely called "title". Defaults to "title", which is the only valid key under a page parent.',
1076
- },
1077
- properties: {
1078
- type: "object",
1079
- description: 'Additional Notion property values, keyed by property name and in Notion\'s own wrapped form, e.g. {"Status":{"status":{"name":"Todo"}}}. Read integration_get_data_source_schema first.',
1080
- },
1081
- markdown: {
1082
- type: "string",
1083
- description: "Page body as Notion-flavored Markdown. Mutually exclusive with children.",
1084
- },
1085
- children: {
1086
- type: "array",
1087
- maxItems: MAX_CHILDREN_PER_REQUEST,
1088
- description: "Page body as raw Notion block objects. Mutually exclusive with markdown.",
1089
- items: { type: "object" },
1090
- },
1091
- icon: { type: "string", description: "Emoji to use as the page icon." },
1092
- },
1093
- additionalProperties: false,
1094
- },
1095
- outputSchema: PAGE_OUTPUT_SCHEMA,
1096
- handler: async (args, ctx) => {
1097
- const [parentKey, parentValue] = requireExactlyOne([
1098
- ["parent_page_id", args.parent_page_id],
1099
- ["parent_data_source_id", args.parent_data_source_id],
1100
- ], "A page needs exactly one parent, and a data source is addressed by its data_source_id — never by a database_id.");
1101
- if (args.markdown !== undefined && args.children !== undefined) {
1102
- invalid("Provide either markdown or children for the page body, not both.");
1103
- }
1104
- const properties = { ...args.properties };
1105
- if (args.title !== undefined)
1106
- properties[args.title_property ?? "title"] = { title: richText(args.title, "title") };
1107
- const body = {
1108
- parent: parentKey === "parent_page_id"
1109
- ? { type: "page_id", page_id: parentValue }
1110
- : { type: "data_source_id", data_source_id: parentValue },
1111
- properties,
1112
- };
1113
- if (args.markdown !== undefined)
1114
- body["markdown"] = args.markdown;
1115
- if (args.children !== undefined)
1116
- body["children"] = args.children;
1117
- if (args.icon !== undefined)
1118
- body["icon"] = { type: "emoji", emoji: args.icon };
1119
- return projectPage(await notion(ctx, "POST", "/v1/pages", { body }));
1120
- },
1121
- },
1122
- {
1123
- name: "integration_append_blocks",
1124
- description: "Append content to the end of a page or block, or insert it at a chosen position. Appending only adds: existing blocks are never moved or replaced, and an appended block cannot be relocated later through the API.",
1125
- annotations: { readOnlyHint: false },
1126
- inputSchema: {
1127
- type: "object",
1128
- properties: {
1129
- block_id: { type: "string", description: "Page id or block id to append into." },
1130
- text: {
1131
- type: "array",
1132
- items: { type: "string" },
1133
- maxItems: MAX_CHILDREN_PER_REQUEST,
1134
- description: "Plain-text paragraphs, one block each. Use exactly one of text, checklist, or children.",
1135
- },
1136
- checklist: {
1137
- type: "array",
1138
- items: { type: "string" },
1139
- maxItems: MAX_CHILDREN_PER_REQUEST,
1140
- description: "Plain-text to-do items, one unchecked to_do block each.",
1141
- },
1142
- children: {
1143
- type: "array",
1144
- items: { type: "object" },
1145
- maxItems: MAX_CHILDREN_PER_REQUEST,
1146
- description: "Raw Notion block objects, for anything paragraphs and to-dos cannot express.",
1147
- },
1148
- position: {
1149
- type: "string",
1150
- enum: ["end", "start", "after_block"],
1151
- description: 'Where to insert. Defaults to "end". "after_block" requires after_block_id.',
1152
- },
1153
- after_block_id: {
1154
- type: "string",
1155
- description: 'Insert directly after this block when position is "after_block".',
1156
- },
1157
- },
1158
- required: ["block_id"],
1159
- additionalProperties: false,
1160
- },
1161
- outputSchema: {
1162
- type: "object",
1163
- properties: {
1164
- appended: { type: "integer", description: "How many blocks were created." },
1165
- results: { type: "array", items: BLOCK_OUTPUT_SCHEMA },
1166
- },
1167
- required: ["appended", "results"],
1168
- },
1169
- handler: async (args, ctx) => {
1170
- const [kind, value] = requireExactlyOne([
1171
- ["text", args.text],
1172
- ["checklist", args.checklist],
1173
- ["children", args.children],
1174
- ], "Use text for plain paragraphs, checklist for to-do items, or children for raw Notion blocks.");
1175
- const children = kind === "text"
1176
- ? value.map((line, at) => ({
1177
- object: "block",
1178
- type: "paragraph",
1179
- paragraph: { rich_text: richText(line, `text[${at}]`) },
1180
- }))
1181
- : kind === "checklist"
1182
- ? value.map((line, at) => ({
1183
- object: "block",
1184
- type: "to_do",
1185
- to_do: { rich_text: richText(line, `checklist[${at}]`), checked: false },
1186
- }))
1187
- : value;
1188
- if (children.length === 0)
1189
- invalid("Nothing to append: provide at least one block.");
1190
- const body = { children };
1191
- if (args.position === "after_block") {
1192
- if (!args.after_block_id)
1193
- invalid('position "after_block" requires after_block_id.');
1194
- body["position"] = { type: "after_block", after_block: { id: args.after_block_id } };
1195
- }
1196
- else if (args.position) {
1197
- body["position"] = { type: args.position };
1198
- }
1199
- const payload = await notion(ctx, "PATCH", `/v1/blocks/${id(args.block_id)}/children`, { body });
1200
- const results = (payload["results"] ?? []).map((block) => projectBlock(block, 0));
1201
- return { appended: results.length, results };
1202
- },
1203
- },
1204
- ];
1205
- }
1206
- /** Notion's generic and named REST tools and its credential test. */
1207
- export function notionRest(defaultPageSize) {
1208
- return {
1209
- tools: [...restTools(VENDOR), ...namedTools(defaultPageSize)],
1210
- async testCredential(value, ctx) {
1211
- // Notion has no token-introspection endpoint for internal integrations;
1212
- // identifying the bot is the cheapest call that proves the token is live.
1213
- try {
1214
- const payload = await notion({ ...ctx, credential: { get: async () => value, getAll: async () => ({ value }) } }, "GET", "/v1/users/me");
1215
- const name = payload["bot"]?.workspace_name ?? payload["name"] ?? "Notion";
1216
- return { ok: true, message: `Authenticated as ${name}.` };
1217
- }
1218
- catch (error) {
1219
- return {
1220
- ok: false,
1221
- message: error instanceof ConnectorCallError ? error.message : "Notion rejected the token.",
1222
- };
1223
- }
1224
- },
1225
- };
1226
- }