@zackbart/connecta 0.34.1 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/README.md +65 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2044 -1161
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +751 -281
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +29 -7
  53. package/dist/connectors/api-connector.js +145 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/remote-mcp.d.ts +10 -30
  58. package/dist/connectors/remote-mcp.js +955 -563
  59. package/dist/d1.js +10 -243
  60. package/dist/derived-context.js +5 -0
  61. package/dist/describe-config.d.ts +3 -1
  62. package/dist/describe-config.js +2 -0
  63. package/dist/downstream-input.js +347 -260
  64. package/dist/errors.d.ts +87 -6
  65. package/dist/errors.js +114 -4
  66. package/dist/execute.js +87 -50
  67. package/dist/executor-admission.js +3 -0
  68. package/dist/executors/quickjs.js +163 -112
  69. package/dist/guarded-transport.js +50 -0
  70. package/dist/guest-runtime.js +1 -1
  71. package/dist/http/client.js +439 -0
  72. package/dist/http/errors.js +558 -0
  73. package/dist/http/fields.js +49 -0
  74. package/dist/http/hatch-paging.js +318 -0
  75. package/dist/http/hatch.js +1279 -0
  76. package/dist/http/json-schema.js +267 -0
  77. package/dist/http/node-transport.js +241 -0
  78. package/dist/http/oauth.js +162 -0
  79. package/dist/http/paging.js +227 -0
  80. package/dist/http/provider.js +221 -0
  81. package/dist/http/safety.js +82 -0
  82. package/dist/http/services.js +79 -0
  83. package/dist/http/tools.js +379 -0
  84. package/dist/http/vendor-index.js +138 -0
  85. package/dist/http/vendor.js +448 -0
  86. package/dist/http/write-request.js +66 -0
  87. package/dist/http-policy.d.ts +11 -0
  88. package/dist/http-policy.js +45 -0
  89. package/dist/inbound-credential.js +1 -1
  90. package/dist/index.d.ts +2 -2
  91. package/dist/index.js +4 -0
  92. package/dist/invocation-auth.js +30 -19
  93. package/dist/invocation.js +218 -85
  94. package/dist/mcp-result.js +0 -4
  95. package/dist/membership.d.ts +4 -0
  96. package/dist/meta-output.js +1 -1
  97. package/dist/meta-tools.js +531 -406
  98. package/dist/native-recovery.js +52 -0
  99. package/dist/native-tools.js +382 -0
  100. package/dist/node.d.ts +4 -3
  101. package/dist/node.js +45 -8
  102. package/dist/oauth-handoff.js +177 -104
  103. package/dist/oauth-partition.js +6 -20
  104. package/dist/oauth-sealing.js +6 -7
  105. package/dist/operation-vocabulary.js +101 -0
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +17 -4
  108. package/dist/operator-ui/contract.d.ts +8 -3
  109. package/dist/operator-ui/generated.js +4 -4
  110. package/dist/operator-ui/model.d.ts +2 -3
  111. package/dist/program-writes.js +47 -26
  112. package/dist/provider-call-facts.d.ts +115 -0
  113. package/dist/provider-call-facts.js +182 -0
  114. package/dist/provider-call.d.ts +112 -0
  115. package/dist/provider-call.js +271 -0
  116. package/dist/provider.d.ts +65 -31
  117. package/dist/provider.js +116 -39
  118. package/dist/providers/_shared/google/hatch.js +91 -0
  119. package/dist/providers/_shared/google/kit.js +427 -0
  120. package/dist/providers/_shared/google/result-size.js +15 -2
  121. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  122. package/dist/providers/_shared/google/service-account.js +8 -4
  123. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  124. package/dist/providers/_shared/google/workspace.js +6 -425
  125. package/dist/providers/_shared/rest/compact-index.js +86 -0
  126. package/dist/providers/_shared/rest/kit.js +146 -0
  127. package/dist/providers/_shared/rest/operation-index.js +230 -14
  128. package/dist/providers/_shared/rest/response-media.js +11 -0
  129. package/dist/providers/_shared/rest/value-safety.js +353 -195
  130. package/dist/providers/_shared/sensitive-fields.js +27 -0
  131. package/dist/providers/breeze/api.js +584 -0
  132. package/dist/providers/breeze/index.d.ts +1 -3
  133. package/dist/providers/breeze/index.js +67 -1420
  134. package/dist/providers/breeze/openapi.generated.js +60 -0
  135. package/dist/providers/breeze/skill.generated.js +2 -6
  136. package/dist/providers/breeze/tools.js +696 -0
  137. package/dist/providers/ccb/api.js +496 -0
  138. package/dist/providers/ccb/index.d.ts +15 -37
  139. package/dist/providers/ccb/index.js +77 -1702
  140. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  141. package/dist/providers/ccb/lib/constants.js +74 -0
  142. package/dist/providers/ccb/lib/guide.js +9 -0
  143. package/dist/providers/ccb/lib/verdicts.js +387 -0
  144. package/dist/providers/ccb/openapi.generated.js +397 -0
  145. package/dist/providers/ccb/safety.js +295 -0
  146. package/dist/providers/ccb/skill.generated.js +2 -6
  147. package/dist/providers/ccb/tools.js +853 -0
  148. package/dist/providers/cloudflare/api.js +1066 -0
  149. package/dist/providers/cloudflare/index.d.ts +5 -22
  150. package/dist/providers/cloudflare/index.js +79 -197
  151. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  152. package/dist/providers/cloudflare/lib/config.js +2 -0
  153. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  154. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  155. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  156. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  157. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  158. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  159. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  160. package/dist/providers/cloudflare/skill.generated.js +2 -10
  161. package/dist/providers/cloudflare/tools.js +360 -0
  162. package/dist/providers/docs/api.js +51 -0
  163. package/dist/providers/docs/index.d.ts +1 -12
  164. package/dist/providers/docs/index.js +53 -1165
  165. package/dist/providers/docs/lib/constants.d.ts +18 -0
  166. package/dist/providers/docs/lib/constants.js +18 -0
  167. package/dist/providers/docs/lib/discovery.js +26 -0
  168. package/dist/providers/docs/lib/requests.js +48 -0
  169. package/dist/providers/docs/lib/schemas.js +168 -0
  170. package/dist/providers/docs/openapi.generated.js +16 -0
  171. package/dist/providers/docs/safety.js +54 -0
  172. package/dist/providers/docs/skill.generated.js +2 -6
  173. package/dist/providers/docs/tools.js +801 -0
  174. package/dist/providers/drive/api.js +177 -0
  175. package/dist/providers/drive/index.d.ts +1 -12
  176. package/dist/providers/drive/index.js +38 -1448
  177. package/dist/providers/drive/lib/constants.d.ts +6 -0
  178. package/dist/providers/drive/lib/constants.js +6 -0
  179. package/dist/providers/drive/lib/contracts.js +532 -0
  180. package/dist/providers/drive/lib/hatch.js +197 -0
  181. package/dist/providers/drive/openapi.generated.js +77 -0
  182. package/dist/providers/drive/safety.js +90 -0
  183. package/dist/providers/drive/skill.generated.js +2 -6
  184. package/dist/providers/drive/tools.js +1290 -0
  185. package/dist/providers/forms/api.js +71 -0
  186. package/dist/providers/forms/index.d.ts +1 -12
  187. package/dist/providers/forms/index.js +49 -1145
  188. package/dist/providers/forms/lib/constants.d.ts +14 -0
  189. package/dist/providers/forms/lib/constants.js +19 -0
  190. package/dist/providers/forms/lib/hatch.js +72 -0
  191. package/dist/providers/forms/openapi.generated.js +23 -0
  192. package/dist/providers/forms/safety.js +34 -0
  193. package/dist/providers/forms/skill.generated.js +2 -8
  194. package/dist/providers/forms/tools.js +1023 -0
  195. package/dist/providers/github/api.js +150 -0
  196. package/dist/providers/github/index.d.ts +4 -5
  197. package/dist/providers/github/index.js +30 -87
  198. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  199. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  200. package/dist/providers/github/lib/discovery.js +132 -0
  201. package/dist/providers/github/lib/operations.js +58 -0
  202. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  203. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  204. package/dist/providers/github/openapi.generated.js +1216 -0
  205. package/dist/providers/github/safety.js +216 -0
  206. package/dist/providers/github/skill.generated.js +2 -5
  207. package/dist/providers/github/tools.js +506 -0
  208. package/dist/providers/gmail/api.js +114 -0
  209. package/dist/providers/gmail/index.d.ts +1 -12
  210. package/dist/providers/gmail/index.js +73 -1621
  211. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  212. package/dist/providers/gmail/lib/constants.js +16 -0
  213. package/dist/providers/gmail/lib/index.js +173 -0
  214. package/dist/providers/gmail/lib/schemas.js +176 -0
  215. package/dist/providers/gmail/lib/threading.js +120 -0
  216. package/dist/providers/gmail/openapi.generated.js +92 -0
  217. package/dist/providers/gmail/safety.js +21 -0
  218. package/dist/providers/gmail/skill.generated.js +2 -6
  219. package/dist/providers/gmail/tools.js +1435 -0
  220. package/dist/providers/overflow/api.js +1008 -0
  221. package/dist/providers/overflow/index.d.ts +1 -17
  222. package/dist/providers/overflow/index.js +60 -1488
  223. package/dist/providers/overflow/skill.generated.js +2 -5
  224. package/dist/providers/overflow/tools.js +873 -0
  225. package/dist/providers/planning-center/api.js +467 -0
  226. package/dist/providers/planning-center/index.d.ts +4 -35
  227. package/dist/providers/planning-center/index.js +69 -2445
  228. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  229. package/dist/providers/planning-center/lib/constants.js +83 -0
  230. package/dist/providers/planning-center/lib/discovery.js +40 -0
  231. package/dist/providers/planning-center/lib/guide.js +39 -0
  232. package/dist/providers/planning-center/lib/json-api.js +83 -0
  233. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  234. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  235. package/dist/providers/planning-center/safety.js +188 -0
  236. package/dist/providers/planning-center/skill.generated.js +2 -27
  237. package/dist/providers/planning-center/tools.js +1010 -0
  238. package/dist/providers/quickbooks/api.js +299 -0
  239. package/dist/providers/quickbooks/index.d.ts +13 -0
  240. package/dist/providers/quickbooks/index.js +93 -0
  241. package/dist/providers/quickbooks/lib/query.js +118 -0
  242. package/dist/providers/quickbooks/lib/writes.js +168 -0
  243. package/dist/providers/quickbooks/safety.js +222 -0
  244. package/dist/providers/quickbooks/skill.generated.js +5 -0
  245. package/dist/providers/quickbooks/tools.js +117 -0
  246. package/dist/providers/sheets/api.js +133 -0
  247. package/dist/providers/sheets/index.d.ts +4 -16
  248. package/dist/providers/sheets/index.js +61 -1172
  249. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  250. package/dist/providers/sheets/lib/constants.js +14 -0
  251. package/dist/providers/sheets/lib/hatch.js +163 -0
  252. package/dist/providers/sheets/lib/requests.js +105 -0
  253. package/dist/providers/sheets/lib/schemas.js +138 -0
  254. package/dist/providers/sheets/openapi.generated.js +30 -0
  255. package/dist/providers/sheets/safety.js +27 -0
  256. package/dist/providers/sheets/skill.generated.js +2 -7
  257. package/dist/providers/sheets/tools.js +733 -0
  258. package/dist/providers/slides/api.js +147 -0
  259. package/dist/providers/slides/index.d.ts +2 -10
  260. package/dist/providers/slides/index.js +48 -2555
  261. package/dist/providers/slides/lib/constants.d.ts +11 -0
  262. package/dist/providers/slides/lib/constants.js +11 -0
  263. package/dist/providers/slides/lib/requests.js +60 -0
  264. package/dist/providers/slides/lib/schemas.js +565 -0
  265. package/dist/providers/slides/openapi.generated.js +18 -0
  266. package/dist/providers/slides/skill.generated.js +2 -6
  267. package/dist/providers/slides/tools.js +1968 -0
  268. package/dist/providers/stripe/api.js +401 -0
  269. package/dist/providers/stripe/index.d.ts +5 -27
  270. package/dist/providers/stripe/index.js +74 -210
  271. package/dist/providers/stripe/lib/config.d.ts +4 -0
  272. package/dist/providers/stripe/lib/config.js +2 -0
  273. package/dist/providers/stripe/openapi.generated.js +4 -3
  274. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  275. package/dist/providers/stripe/skill.generated.js +2 -11
  276. package/dist/providers/stripe/tools.js +77 -0
  277. package/dist/providers/tithely/api.js +531 -0
  278. package/dist/providers/tithely/index.d.ts +17 -0
  279. package/dist/providers/tithely/index.js +86 -965
  280. package/dist/providers/tithely/skill.generated.js +2 -5
  281. package/dist/providers/tithely/tools.js +426 -0
  282. package/dist/providers/vercel/api.js +551 -0
  283. package/dist/providers/vercel/index.d.ts +9 -25
  284. package/dist/providers/vercel/index.js +65 -260
  285. package/dist/providers/vercel/lib/config.d.ts +2 -0
  286. package/dist/providers/vercel/lib/config.js +2 -0
  287. package/dist/providers/vercel/lib/index.js +117 -0
  288. package/dist/providers/vercel/lib/logs.js +100 -0
  289. package/dist/providers/vercel/lib/pin.js +270 -0
  290. package/dist/providers/vercel/openapi.generated.js +10 -5
  291. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  292. package/dist/providers/vercel/skill.generated.js +2 -7
  293. package/dist/providers/vercel/tools.js +642 -0
  294. package/dist/redaction-identity.js +47 -0
  295. package/dist/redaction-marker.js +125 -0
  296. package/dist/registry.d.ts +21 -15
  297. package/dist/registry.js +409 -376
  298. package/dist/routes/access-tokens.js +1 -1
  299. package/dist/routes/actor-labels.js +1 -1
  300. package/dist/routes/connect.js +1 -1
  301. package/dist/routes/dispatch.js +103 -0
  302. package/dist/routes/mcp.js +170 -79
  303. package/dist/routes/oauth.js +6 -1
  304. package/dist/routes/operator.js +2 -2
  305. package/dist/routes/shared.d.ts +5 -4
  306. package/dist/routes/shared.js +244 -177
  307. package/dist/routes/skills.js +10 -8
  308. package/dist/routes/ui-config.js +27 -3
  309. package/dist/routes/ui-data.js +21 -20
  310. package/dist/routes/ui.js +1 -1
  311. package/dist/runtime/call-admission.js +0 -26
  312. package/dist/runtime/call-entry.js +30 -0
  313. package/dist/runtime/call.js +552 -0
  314. package/dist/runtime/connector-scope.js +7 -0
  315. package/dist/runtime/credential-probe.js +34 -0
  316. package/dist/runtime/executor.js +40 -0
  317. package/dist/runtime/native-storage.js +63 -0
  318. package/dist/runtime/registry.js +27 -0
  319. package/dist/runtime/request-scope.js +72 -0
  320. package/dist/runtime/run.js +15 -7
  321. package/dist/runtime/shared-read.js +34 -5
  322. package/dist/runtime/storage.js +34 -21
  323. package/dist/sent-secrets.d.ts +194 -37
  324. package/dist/sent-secrets.js +1743 -340
  325. package/dist/server.js +5 -56
  326. package/dist/skills.js +322 -223
  327. package/dist/sqlite.js +15 -14
  328. package/dist/storage/d1-copy.js +255 -0
  329. package/dist/storage/d1-driver.js +15 -0
  330. package/dist/storage/keys.js +28 -0
  331. package/dist/storage/sql-driver.js +88 -0
  332. package/dist/storage/sql-native.js +544 -0
  333. package/dist/storage/sql-schema.d.ts +6 -0
  334. package/dist/storage/sql-schema.js +15 -0
  335. package/dist/storage/sql.d.ts +14 -20
  336. package/dist/storage/sql.js +29 -535
  337. package/dist/timeout.js +0 -26
  338. package/dist/tool-safety.js +20 -10
  339. package/dist/types.d.ts +9 -109
  340. package/dist/ui.d.ts +1 -1
  341. package/dist/ui.js +0 -1
  342. package/dist/upward.js +3 -0
  343. package/dist/usage-guide.js +3 -1
  344. package/dist/validate.d.ts +4 -2
  345. package/dist/validate.js +86 -9
  346. package/dist/version.d.ts +1 -1
  347. package/dist/version.js +1 -1
  348. package/dist/worker.js +312 -292
  349. package/dist/workspace-subject.js +5 -1
  350. package/dist/write-outcome.js +454 -34
  351. package/documentation/README.md +28 -7
  352. package/documentation/architecture.md +2409 -255
  353. package/documentation/auth.md +867 -114
  354. package/documentation/code-mode.md +49 -16
  355. package/documentation/deploying.md +164 -20
  356. package/documentation/hosted-mcp-migration.md +460 -0
  357. package/documentation/integrating.md +129 -144
  358. package/documentation/meta-tools.md +409 -32
  359. package/documentation/operating.md +27 -3
  360. package/documentation/operator-tests.md +3 -0
  361. package/documentation/operator-ui.md +84 -48
  362. package/documentation/provider-auth-migration.md +16 -63
  363. package/documentation/provider-migration-0.29.md +10 -5
  364. package/documentation/provider-migration-0.35.md +975 -0
  365. package/documentation/providers.md +1347 -0
  366. package/examples/worker/AGENTS.md +23 -9
  367. package/examples/worker/README.md +254 -135
  368. package/examples/worker/alchemy.run.ts +116 -0
  369. package/examples/worker/gitignore +10 -0
  370. package/examples/worker/src/connecta.config.ts +23 -21
  371. package/examples/worker/wrangler.jsonc +14 -5
  372. package/guides/connecting-agents.md +157 -0
  373. package/guides/how-it-works.md +156 -0
  374. package/guides/safety-and-trust.md +153 -0
  375. package/package.json +30 -50
  376. package/templates/node/.env.example +3 -0
  377. package/templates/node/README.md +22 -2
  378. package/templates/node/docker-compose.yml +1 -0
  379. package/templates/node/package.json +3 -2
  380. package/templates/node/src/connecta.config.ts +8 -0
  381. package/dist/catalog-drift.d.ts +0 -138
  382. package/dist/catalog-drift.js +0 -578
  383. package/dist/operator-ui/agents.d.ts +0 -74
  384. package/dist/operator-ui/agents.js +0 -104
  385. package/dist/operator-ui/config-summary.js +0 -565
  386. package/dist/operator-ui/connection-map.js +0 -376
  387. package/dist/operator-ui/endpoints.js +0 -221
  388. package/dist/operator-ui/fix-prompts.d.ts +0 -18
  389. package/dist/operator-ui/fix-prompts.js +0 -87
  390. package/dist/operator-ui/health.js +0 -52
  391. package/dist/operator-ui/member-page.js +0 -97
  392. package/dist/operator-ui/overview.js +0 -123
  393. package/dist/operator-ui/setup-commands.js +0 -51
  394. package/dist/operator-ui/view.d.ts +0 -401
  395. package/dist/operator-ui/view.js +0 -705
  396. package/dist/providers/_shared/request-token.js +0 -83
  397. package/dist/providers/_shared/rest/dispatch.js +0 -41
  398. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  399. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  400. package/dist/providers/_shared/rest/tools.js +0 -569
  401. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  402. package/dist/providers/basecamp/index.d.ts +0 -45
  403. package/dist/providers/basecamp/index.js +0 -997
  404. package/dist/providers/basecamp/skill.generated.js +0 -9
  405. package/dist/providers/cloudflare/rest.d.ts +0 -37
  406. package/dist/providers/cloudflare/rest.js +0 -1097
  407. package/dist/providers/github/hosted.js +0 -218
  408. package/dist/providers/github/rest.js +0 -321
  409. package/dist/providers/infisical/index.d.ts +0 -7
  410. package/dist/providers/infisical/index.js +0 -713
  411. package/dist/providers/infisical/skill.generated.js +0 -8
  412. package/dist/providers/intercom/index.d.ts +0 -24
  413. package/dist/providers/intercom/index.js +0 -121
  414. package/dist/providers/intercom/skill.generated.js +0 -8
  415. package/dist/providers/linear/index.d.ts +0 -55
  416. package/dist/providers/linear/index.js +0 -537
  417. package/dist/providers/linear/skill.generated.js +0 -8
  418. package/dist/providers/mixpanel/index.d.ts +0 -59
  419. package/dist/providers/mixpanel/index.js +0 -472
  420. package/dist/providers/mixpanel/operations.js +0 -20
  421. package/dist/providers/mixpanel/rest.js +0 -603
  422. package/dist/providers/mixpanel/skill.generated.js +0 -10
  423. package/dist/providers/mixpanel/value-safety.js +0 -16
  424. package/dist/providers/notion/index.d.ts +0 -67
  425. package/dist/providers/notion/index.js +0 -368
  426. package/dist/providers/notion/openapi.generated.js +0 -75
  427. package/dist/providers/notion/rest.d.ts +0 -24
  428. package/dist/providers/notion/rest.js +0 -1226
  429. package/dist/providers/notion/skill.generated.js +0 -11
  430. package/dist/providers/notion/value-safety.js +0 -56
  431. package/dist/providers/revenuecat/index.d.ts +0 -51
  432. package/dist/providers/revenuecat/index.js +0 -697
  433. package/dist/providers/revenuecat/skill.generated.js +0 -11
  434. package/dist/providers/stripe/rest.d.ts +0 -14
  435. package/dist/providers/stripe/rest.js +0 -402
  436. package/dist/providers/vercel/rest.d.ts +0 -20
  437. package/dist/providers/vercel/rest.js +0 -1020
  438. package/dist/providers/wispr-flow/index.d.ts +0 -14
  439. package/dist/providers/wispr-flow/index.js +0 -117
  440. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  441. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,9 +1,14 @@
1
+ import { Effect } from "effect";
1
2
  import { authorizationCredential, authorizationRequest, isMachineCredential } from "../inbound-credential.js";
2
- import { createByteReadingClerkClient } from "./clerk-transport.js";
3
+ import { fromSignal, runEdge } from "../runtime/run.js";
4
+ import { authAll, authCall, authDetached, authFetch, authFlow, authInvoke, authMethod, authProgram, authValue, registerAuth, } from "./native.js";
5
+ // Clerk as the OAuth 2.1 authorization server; connecta is the resource server.
6
+ // Single tenant, no tenant-tag requirement, optional organizationId,
7
+ // allowedDomains, and gate() with ~60s identity caching.
3
8
  import { decodeJwt } from "@clerk/backend/jwt";
4
- import { byteReadResponse } from "../byte-read-response.js";
5
9
  import { failureRecord, logFailure } from "../operator-record.js";
6
10
  import { assertNoRetiredToolkitOptions } from "../retired-toolkits.js";
11
+ import { createByteReadingClerkClient } from "./clerk-transport.js";
7
12
  function logDenial(reason, failure) {
8
13
  logFailure(console, "Clerk request denied", failureRecord({ reason }, failure));
9
14
  }
@@ -53,6 +58,19 @@ function fapiUrl(publishableKey) {
53
58
  }
54
59
  const GATE_ALLOWED_TTL_MS = 60 * 1000;
55
60
  const GATE_FORBIDDEN_TTL_MS = 30 * 1000;
61
+ const BACKEND_LOOKUP_TIMEOUT_MS = 10_000;
62
+ // Per organization-mode clerkAuth/isolate, shared by membership and the extra
63
+ // opaque verification. Clerk's quota is per instance (100/10s development,
64
+ // 1,000/10s production), not per deployment. These cap bursts, not request
65
+ // rates: ingress rate limiting must protect the shared quota. No queue or
66
+ // request-owned object is retained; only counters and a backoff timestamp.
67
+ const ORGANIZATION_MAX_BACKEND_LOOKUPS = 16;
68
+ const ORGANIZATION_MAX_BACKEND_LOOKUPS_PER_IDENTITY = 2;
69
+ const BACKEND_RETRY_DEFAULT_MS = 10_000;
70
+ const BACKEND_RETRY_MAX_MS = 60_000;
71
+ // A user-filtered membership list holds at most one row, and a token
72
+ // verification describes one token.
73
+ const MAX_BACKEND_RESPONSE_BYTES = 65_536;
56
74
  const ACTIVITY_LABEL_TTL_MS = 5 * 60 * 1000;
57
75
  const ACTIVITY_LABEL_MISS_TTL_MS = 30 * 1000;
58
76
  const ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS = 1_250;
@@ -170,6 +188,168 @@ function normalizeOAuthClientIds(value) {
170
188
  }
171
189
  return new Set(value);
172
190
  }
191
+ /**
192
+ * Checked at construction because the ID becomes a Backend API path segment,
193
+ * and a typo would otherwise refuse every person at request time.
194
+ */
195
+ function normalizeOrganizationId(value, allowedOAuthClientIds) {
196
+ if (value === undefined)
197
+ return undefined;
198
+ if (typeof value !== "string" || !/^org_[A-Za-z0-9_]{27}$/.test(value)) {
199
+ throw new Error("clerkAuth: `organizationId` is not a Clerk organization ID. Copy it from " +
200
+ "the Clerk dashboard: `org_` followed by 27 letters and digits.");
201
+ }
202
+ // On a shared instance every deployment's OAuth clients and tokens come
203
+ // from one issuer, so the resource-bound audience is what keeps a token for
204
+ // one deployment out of another. An unbound-token fallback removes it.
205
+ if (allowedOAuthClientIds.size > 0) {
206
+ throw new Error("clerkAuth: `organizationId` requires resource-bound OAuth tokens. Remove " +
207
+ "`allowedOAuthClientIds` and enable Clerk's resource audiences.");
208
+ }
209
+ return value;
210
+ }
211
+ /**
212
+ * Is this user a member of the organization, according to Clerk now? The
213
+ * given signal and a ten-second deadline bound the call. Any response that is
214
+ * not a clear answer throws, so the caller refuses.
215
+ */
216
+ function organizationMember(secretKey, organizationId, userId, signal) {
217
+ return runEdge(authProgram(organizationMemberNative, [secretKey, organizationId, userId, signal]));
218
+ }
219
+ const organizationMemberNative = /* @__PURE__ */ registerAuth(organizationMember, function (secretKey, organizationId, userId, signal) {
220
+ return authFlow(function* () {
221
+ const url = new URL(`https://api.clerk.com/v1/organizations/${encodeURIComponent(organizationId)}/memberships`);
222
+ url.searchParams.set("user_id", userId);
223
+ const list = (yield authCall(backendJson, [
224
+ url,
225
+ { headers: { Authorization: `Bearer ${secretKey}`, "Clerk-API-Version": "2026-05-12" } },
226
+ signal,
227
+ ]));
228
+ if (!isRecord(list) || !Array.isArray(list.data)) {
229
+ throw new Error("Organization membership response invalid");
230
+ }
231
+ // The filter narrows the list; membership is still read exactly from each
232
+ // row, so an ignored or widened filter cannot admit someone else's row.
233
+ return list.data.some((membership) => isRecord(membership) &&
234
+ membership.object === "organization_membership" &&
235
+ isRecord(membership.organization) &&
236
+ membership.organization.id === organizationId &&
237
+ isRecord(membership.public_user_data) &&
238
+ membership.public_user_data.user_id === userId);
239
+ });
240
+ });
241
+ /**
242
+ * One Backend API request, bounded by the signal, a ten-second deadline, and
243
+ * `MAX_BACKEND_RESPONSE_BYTES`. A redirect, a non-2xx status, or an unreadable
244
+ * body throws. Every exit aborts the request and cancels its body, so no
245
+ * upstream outlives the caller's decision (INV-7).
246
+ */
247
+ function backendJson(url, init, signal) {
248
+ return runEdge(authProgram(backendJsonNative, [url, init, signal]));
249
+ }
250
+ const backendJsonNative = /* @__PURE__ */ registerAuth(backendJson, function (url, init, signal) {
251
+ return authFlow(function* () {
252
+ const settled = new AbortController();
253
+ const request = AbortSignal.any([signal, settled.signal, AbortSignal.timeout(BACKEND_LOOKUP_TIMEOUT_MS)]);
254
+ let response;
255
+ try {
256
+ // Workers reject `redirect: "error"`; `manual` returns a redirect
257
+ // unfollowed, and a redirect is not an answer. Workers and Node return
258
+ // its 3xx; an `opaqueredirect` has status 0, which `ok` refuses.
259
+ response = (yield authFetch(url, { ...init, redirect: "manual", signal: request }));
260
+ if (response.status === 429)
261
+ throw new BackendRateLimited(backendRetryMs(response.headers.get("Retry-After")));
262
+ if (response.status >= 300 && response.status < 400)
263
+ throw new Error("Clerk Backend API request redirected");
264
+ if (!response.ok)
265
+ throw new Error("Clerk Backend API request failed");
266
+ return JSON.parse((yield authCall(boundedText, [response, request])));
267
+ }
268
+ finally {
269
+ settled.abort();
270
+ void response?.body?.cancel().catch(() => { });
271
+ }
272
+ });
273
+ });
274
+ /** Only locally parsed, bounded timing facts leave the response's request. */
275
+ class BackendRateLimited extends Error {
276
+ retryMs;
277
+ constructor(retryMs) {
278
+ super("Clerk Backend API rate limited");
279
+ this.retryMs = retryMs;
280
+ }
281
+ }
282
+ class OrganizationLookupBusy extends Error {
283
+ retrySeconds;
284
+ constructor(retrySeconds) {
285
+ super("Clerk organization lookup busy");
286
+ this.retrySeconds = retrySeconds;
287
+ }
288
+ }
289
+ /** Clerk sends seconds; accept an HTTP date too. Never trust an unbounded delay. */
290
+ function backendRetryMs(value) {
291
+ let delay = BACKEND_RETRY_DEFAULT_MS;
292
+ if (value !== null) {
293
+ const parsed = /^\d+$/.test(value)
294
+ ? Number(value) * 1_000
295
+ : /^[A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d{2}:\d{2}:\d{2} GMT$/.test(value)
296
+ ? Date.parse(value) - Date.now()
297
+ : NaN;
298
+ if (!Number.isNaN(parsed))
299
+ delay = parsed;
300
+ }
301
+ return Math.max(1_000, Math.min(BACKEND_RETRY_MAX_MS, delay));
302
+ }
303
+ /**
304
+ * The body as text, read as bytes (see `byteReadResponse`) and refused past
305
+ * `MAX_BACKEND_RESPONSE_BYTES`. An abort cancels a read the upstream has
306
+ * stopped feeding, so the caller's deadline holds while a body stays open.
307
+ */
308
+ function boundedText(response, signal) {
309
+ return runEdge(authProgram(boundedTextNative, [response, signal]));
310
+ }
311
+ const boundedTextNative = /* @__PURE__ */ registerAuth(boundedText, function (response, signal) {
312
+ return authFlow(function* () {
313
+ const reader = response.body?.getReader();
314
+ if (!reader)
315
+ return "";
316
+ void reader.closed.catch(() => { });
317
+ const cancel = () => void reader.cancel().catch(() => { });
318
+ signal.addEventListener("abort", cancel, { once: true });
319
+ const decoder = new TextDecoder();
320
+ let size = 0;
321
+ let text = "";
322
+ try {
323
+ while (true) {
324
+ signal.throwIfAborted();
325
+ const { done, value } = (yield authInvoke(reader, "read", []));
326
+ signal.throwIfAborted();
327
+ if (done)
328
+ return text + decoder.decode();
329
+ size += value.byteLength;
330
+ if (size > MAX_BACKEND_RESPONSE_BYTES)
331
+ throw new Error("Clerk Backend API response too large");
332
+ text += decoder.decode(value, { stream: true });
333
+ }
334
+ }
335
+ finally {
336
+ signal.removeEventListener("abort", cancel);
337
+ reader.releaseLock();
338
+ }
339
+ });
340
+ });
341
+ /**
342
+ * Valid only while the clock reads from the lookup's start to start + ttl.
343
+ * A step back before the start expires the entry, and a step forward can
344
+ * only expire it early.
345
+ */
346
+ function withinTtl(started, ttl) {
347
+ const age = Date.now() - started;
348
+ return age >= 0 && age < ttl;
349
+ }
350
+ function isRecord(value) {
351
+ return typeof value === "object" && value !== null && !Array.isArray(value);
352
+ }
173
353
  /** Only verified claims reach this comparison; resource URLs match exactly. */
174
354
  function oauthBindingRejection(claims, resource, clientId, allowedClientIds) {
175
355
  const bindings = ["aud", "resource"].filter((key) => Object.prototype.hasOwnProperty.call(claims, key));
@@ -190,38 +370,42 @@ function oauthBindingRejection(claims, resource, clientId, allowedClientIds) {
190
370
  * endpoint directly after SDK authentication, without a shared response cache
191
371
  * or SDK patch. Its subject/client and active verdict must still agree.
192
372
  */
193
- async function opaqueOAuthClaims(token, secretKey, userId, clientId, signal) {
194
- const response = await fetch("https://api.clerk.com/v1/oauth_applications/access_tokens/verify", {
195
- method: "POST",
196
- headers: {
197
- Authorization: `Bearer ${secretKey}`,
198
- "Content-Type": "application/json",
199
- "Clerk-API-Version": "2026-05-12",
200
- },
201
- body: JSON.stringify({ access_token: token }),
202
- redirect: "error",
203
- signal: AbortSignal.any([signal, AbortSignal.timeout(10_000)]),
204
- });
205
- if (!response.ok)
206
- throw new Error("Opaque OAuth verification failed");
207
- const claims = await byteReadResponse(response).json();
208
- if (!claims ||
209
- typeof claims !== "object" ||
210
- Array.isArray(claims) ||
211
- !("object" in claims) ||
212
- claims.object !== "clerk_idp_oauth_access_token" ||
213
- !("subject" in claims) ||
214
- claims.subject !== userId ||
215
- !("client_id" in claims) ||
216
- claims.client_id !== clientId ||
217
- !("revoked" in claims) ||
218
- claims.revoked !== false ||
219
- !("expired" in claims) ||
220
- claims.expired !== false) {
221
- throw new Error("Opaque OAuth verification response invalid");
222
- }
223
- return claims;
373
+ function opaqueOAuthClaims(token, secretKey, userId, clientId, signal) {
374
+ return runEdge(authProgram(opaqueOAuthClaimsNative, [token, secretKey, userId, clientId, signal]));
224
375
  }
376
+ const opaqueOAuthClaimsNative = /* @__PURE__ */ registerAuth(opaqueOAuthClaims, function (token, secretKey, userId, clientId, signal) {
377
+ return authFlow(function* () {
378
+ const claims = (yield authCall(backendJson, [
379
+ "https://api.clerk.com/v1/oauth_applications/access_tokens/verify",
380
+ {
381
+ method: "POST",
382
+ headers: {
383
+ Authorization: `Bearer ${secretKey}`,
384
+ "Content-Type": "application/json",
385
+ "Clerk-API-Version": "2026-05-12",
386
+ },
387
+ body: JSON.stringify({ access_token: token }),
388
+ },
389
+ signal,
390
+ ]));
391
+ if (!claims ||
392
+ typeof claims !== "object" ||
393
+ Array.isArray(claims) ||
394
+ !("object" in claims) ||
395
+ claims.object !== "clerk_idp_oauth_access_token" ||
396
+ !("subject" in claims) ||
397
+ claims.subject !== userId ||
398
+ !("client_id" in claims) ||
399
+ claims.client_id !== clientId ||
400
+ !("revoked" in claims) ||
401
+ claims.revoked !== false ||
402
+ !("expired" in claims) ||
403
+ claims.expired !== false) {
404
+ throw new Error("Opaque OAuth verification response invalid");
405
+ }
406
+ return claims;
407
+ });
408
+ });
225
409
  /**
226
410
  * The domain of an email address, lowercased for comparison, or null when the
227
411
  * address does not have exactly one well-formed domain to read.
@@ -244,7 +428,8 @@ function emailDomain(email) {
244
428
  /**
245
429
  * Clerk inbound auth.
246
430
  *
247
- * `allowedDomains` and `gate` decide who is admitted; both must pass.
431
+ * `organizationId`, `allowedDomains`, and `gate` decide who is admitted; each
432
+ * configured check must pass.
248
433
  */
249
434
  /** `"/<pool>"` for a pool endpoint path, null for `/mcp` and anything else. */
250
435
  function mcpPoolSuffix(pathname) {
@@ -256,12 +441,9 @@ export function clerkAuth(opts) {
256
441
  // Before the Clerk client, so a malformed key fails as a connecta
257
442
  // configuration error rather than however the SDK happens to treat it.
258
443
  const frontendApiUrl = fapiUrl(opts.publishableKey);
259
- const clerk = createByteReadingClerkClient({
260
- secretKey: opts.secretKey,
261
- publishableKey: opts.publishableKey,
262
- });
263
444
  const allowedDomains = normalizeAllowedDomains(opts.allowedDomains);
264
445
  const allowedOAuthClientIds = normalizeOAuthClientIds(opts.allowedOAuthClientIds);
446
+ const organizationId = normalizeOrganizationId(opts.organizationId, allowedOAuthClientIds);
265
447
  const requestedScopes = opts.scopes ?? ["openid", "profile", "email"];
266
448
  if (!Array.isArray(requestedScopes) ||
267
449
  requestedScopes.some((scope) => typeof scope !== "string" || !/^[\x21\x23-\x5b\x5d-\x7e]+$/.test(scope))) {
@@ -269,8 +451,64 @@ export function clerkAuth(opts) {
269
451
  }
270
452
  const scopes = [...requestedScopes];
271
453
  const gateCache = new Map();
454
+ const membershipCache = new Map();
455
+ // Membership lookups started, in order, and the latest start among the
456
+ // entries the cache has evicted.
457
+ let membershipLookupsStarted = 0;
458
+ let evictedMembershipSequence = 0;
459
+ let activeOrganizationLookups = 0;
460
+ const organizationLookupsByIdentity = new Map();
461
+ // Wall-clock end of the current 429 backoff. Like the membership TTLs it
462
+ // assumes the clock does not step back: a rollback lengthens the remaining
463
+ // window by the size of the step.
464
+ let organizationRetryUntil = 0;
465
+ const organizationLookup = authMethod((userId, signal, lookup) => {
466
+ return authFlow(function* () {
467
+ const now = Date.now();
468
+ if (now < organizationRetryUntil) {
469
+ throw new OrganizationLookupBusy(Math.ceil((organizationRetryUntil - now) / 1000));
470
+ }
471
+ const activeForIdentity = organizationLookupsByIdentity.get(userId) ?? 0;
472
+ if (activeOrganizationLookups >= ORGANIZATION_MAX_BACKEND_LOOKUPS ||
473
+ activeForIdentity >= ORGANIZATION_MAX_BACKEND_LOOKUPS_PER_IDENTITY) {
474
+ throw new OrganizationLookupBusy(1);
475
+ }
476
+ signal.throwIfAborted();
477
+ activeOrganizationLookups++;
478
+ organizationLookupsByIdentity.set(userId, activeForIdentity + 1);
479
+ let released = false;
480
+ const release = () => {
481
+ if (released)
482
+ return;
483
+ released = true;
484
+ activeOrganizationLookups--;
485
+ const remaining = organizationLookupsByIdentity.get(userId) - 1;
486
+ if (remaining === 0)
487
+ organizationLookupsByIdentity.delete(userId);
488
+ else
489
+ organizationLookupsByIdentity.set(userId, remaining);
490
+ };
491
+ // workerd may discard the awaited continuation when its caller leaves.
492
+ // Release in that request's own abort handler too; finally is idempotent.
493
+ // The same caller signal cancels backendJson's request-owned upstream.
494
+ signal.addEventListener("abort", release, { once: true });
495
+ try {
496
+ return (yield authCall(lookup, []));
497
+ }
498
+ catch (error) {
499
+ if (error instanceof BackendRateLimited) {
500
+ organizationRetryUntil = Math.max(organizationRetryUntil, Date.now() + error.retryMs);
501
+ throw new OrganizationLookupBusy(Math.ceil((organizationRetryUntil - Date.now()) / 1000));
502
+ }
503
+ throw error;
504
+ }
505
+ finally {
506
+ signal.removeEventListener("abort", release);
507
+ release();
508
+ }
509
+ });
510
+ });
272
511
  const activityLabelCache = new Map();
273
- const pendingActivityLabels = new Map();
274
512
  const inFlightActivityLabelIds = new Set();
275
513
  let activeActivityLabelLookups = 0;
276
514
  const resolveBase = (baseUrl) => opts.publicUrl ?? baseUrl;
@@ -280,58 +518,57 @@ export function clerkAuth(opts) {
280
518
  exp: Date.now() + (label ? ACTIVITY_LABEL_TTL_MS : ACTIVITY_LABEL_MISS_TTL_MS),
281
519
  });
282
520
  };
283
- const resolveActivityLabel = async (userId) => {
284
- const cached = readIdentityCache(activityLabelCache, userId);
285
- if (cached)
286
- return cached.label;
287
- const existing = pendingActivityLabels.get(userId);
288
- if (existing)
289
- return existing;
290
- // The Clerk SDK's getUser call has no AbortSignal. Keep the real upstream
291
- // concurrency bounded even when requests hang forever: do not queue more
292
- // identities in memory, and do not start a duplicate for an id whose raw
293
- // lookup outlived its caller-facing deadline.
294
- if (inFlightActivityLabelIds.has(userId) || activeActivityLabelLookups >= ACTIVITY_LABEL_MAX_IN_FLIGHT) {
295
- cacheActivityLabel(userId, undefined);
296
- return undefined;
297
- }
298
- activeActivityLabelLookups++;
299
- inFlightActivityLabelIds.add(userId);
300
- const upstream = clerk.users
301
- .getUser(userId)
302
- .then((user) => {
303
- const label = activityLabelForUser(user);
304
- cacheActivityLabel(userId, label);
305
- return label;
306
- })
307
- .catch(() => {
308
- cacheActivityLabel(userId, undefined);
309
- return undefined;
310
- })
311
- .finally(() => {
312
- activeActivityLabelLookups--;
313
- inFlightActivityLabelIds.delete(userId);
314
- });
315
- const lookup = new Promise((resolve) => {
316
- let settled = false;
317
- const finish = (label) => {
318
- if (settled)
319
- return;
320
- settled = true;
521
+ const resolveActivityLabel = authMethod((userId, owner) => {
522
+ return authFlow(function* () {
523
+ if (owner?.aborted)
524
+ return undefined;
525
+ const cached = readIdentityCache(activityLabelCache, userId);
526
+ if (cached)
527
+ return cached.label;
528
+ // Each reader owns its lookup and transport. Keep raw concurrency bounded
529
+ // even if a custom transport fails to settle after cancellation.
530
+ if (inFlightActivityLabelIds.has(userId) || activeActivityLabelLookups >= ACTIVITY_LABEL_MAX_IN_FLIGHT) {
531
+ return undefined;
532
+ }
533
+ const ended = new AbortController();
534
+ const signal = owner ? AbortSignal.any([owner, ended.signal]) : ended.signal;
535
+ activeActivityLabelLookups++;
536
+ inFlightActivityLabelIds.add(userId);
537
+ const lookup = authFlow(function* () {
538
+ try {
539
+ signal.throwIfAborted();
540
+ const user = (yield authInvoke(createByteReadingClerkClient(opts, signal).users, "getUser", [
541
+ userId,
542
+ ]));
543
+ if (signal.aborted)
544
+ return undefined;
545
+ const label = activityLabelForUser(user);
546
+ cacheActivityLabel(userId, label);
547
+ return label;
548
+ }
549
+ catch {
550
+ if (!signal.aborted)
551
+ cacheActivityLabel(userId, undefined);
552
+ return undefined;
553
+ }
554
+ finally {
555
+ activeActivityLabelLookups--;
556
+ inFlightActivityLabelIds.delete(userId);
557
+ }
558
+ });
559
+ const timer = setTimeout(() => ended.abort(), ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS);
560
+ try {
561
+ return (yield Effect.raceFirst(authDetached(lookup), fromSignal(signal)));
562
+ }
563
+ catch {
564
+ return undefined;
565
+ }
566
+ finally {
321
567
  clearTimeout(timer);
322
- resolve(label);
323
- };
324
- const timer = setTimeout(() => {
325
- cacheActivityLabel(userId, undefined);
326
- finish(undefined);
327
- }, ACTIVITY_LABEL_LOOKUP_TIMEOUT_MS);
328
- void upstream.then(finish);
329
- }).finally(() => {
330
- pendingActivityLabels.delete(userId);
568
+ ended.abort();
569
+ }
331
570
  });
332
- pendingActivityLabels.set(userId, lookup);
333
- return lookup;
334
- };
571
+ });
335
572
  const challenge = (request, baseUrl) => {
336
573
  const tokenPresent = authorizationCredential(request).kind !== "absent";
337
574
  const error = tokenPresent ? `error="invalid_token", ` : "";
@@ -356,67 +593,181 @@ export function clerkAuth(opts) {
356
593
  status: 403,
357
594
  headers: { "Content-Type": "application/json" },
358
595
  });
596
+ const organizationBusy = (error) => {
597
+ logDenial("organization_lookup_busy");
598
+ return Response.json({
599
+ error: {
600
+ code: "organization_lookup_busy",
601
+ message: "Organization admission is temporarily busy. Retry later.",
602
+ retryable: true,
603
+ },
604
+ }, { status: 503, headers: { "Retry-After": String(error.retrySeconds) } });
605
+ };
359
606
  /**
360
607
  * The domain half of admission. Fails CLOSED on every uncertainty — no
361
608
  * primary email, an unverified one, a malformed address, or the lookup
362
609
  * itself failing — because "we could not tell" and "they belong here" must
363
- * not be the same answer for a membership rule.
610
+ * not be the same answer for a membership rule. A failed or cancelled
611
+ * lookup is `undefined`, not a verdict, so it refuses without being cached.
364
612
  */
365
- const checkDomain = async (userId) => {
366
- if (!allowedDomains)
367
- return true;
368
- let email;
369
- try {
370
- const user = await clerk.users.getUser(userId);
371
- const primary = user.emailAddresses?.find((address) => address.id === user.primaryEmailAddressId);
372
- if (primary?.verification?.status === "verified") {
373
- email = primary.emailAddress;
613
+ const checkDomain = authMethod((userId, clerk) => {
614
+ return authFlow(function* () {
615
+ if (!allowedDomains)
616
+ return true;
617
+ let email;
618
+ try {
619
+ const user = (yield authInvoke(clerk.users, "getUser", [userId]));
620
+ const primary = user.emailAddresses?.find((address) => address.id === user.primaryEmailAddressId);
621
+ if (primary?.verification?.status === "verified") {
622
+ email = primary.emailAddress;
623
+ }
374
624
  }
625
+ catch (error) {
626
+ logDenial("email_lookup_failed", error);
627
+ return undefined;
628
+ }
629
+ const domain = email ? emailDomain(email) : null;
630
+ if (!domain) {
631
+ logDenial("verified_email_invalid");
632
+ return false;
633
+ }
634
+ if (!allowedDomains.has(domain)) {
635
+ logDenial("email_domain_denied");
636
+ return false;
637
+ }
638
+ return true;
639
+ });
640
+ });
641
+ /** A cached answer still within its TTL, counted from its lookup's start. */
642
+ const cachedMembership = (key) => {
643
+ const verdict = membershipCache.get(key);
644
+ if (verdict?.member === undefined)
645
+ return undefined;
646
+ if (!withinTtl(verdict.started, verdict.member ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS)) {
647
+ membershipCache.set(key, { started: verdict.started, sequence: verdict.sequence });
648
+ return undefined;
375
649
  }
376
- catch (error) {
377
- logDenial("email_lookup_failed", error);
378
- return false;
379
- }
380
- const domain = email ? emailDomain(email) : null;
381
- if (!domain) {
382
- logDenial("verified_email_invalid");
383
- return false;
384
- }
385
- if (!allowedDomains.has(domain)) {
386
- logDenial("email_domain_denied");
387
- return false;
650
+ membershipCache.delete(key);
651
+ membershipCache.set(key, verdict);
652
+ return verdict.member;
653
+ };
654
+ /**
655
+ * Cache a lookup's answer unless a lookup that started later has already
656
+ * written this key. Evicting an entry raises a floor under every key,
657
+ * because the evicted entry no longer guards its own: a lookup that started
658
+ * before it never writes. Either way an older answer never replaces a
659
+ * newer verdict. A floor-suppressed answer invalidates an older entry for
660
+ * its own key rather than leaving that older verdict authoritative.
661
+ */
662
+ const cacheMembership = (key, verdict) => {
663
+ const current = membershipCache.get(key);
664
+ if (current && current.sequence > verdict.sequence)
665
+ return;
666
+ if (verdict.sequence <= evictedMembershipSequence) {
667
+ // The floor may have advanced past both lookups while an older verdict
668
+ // for this key remained cached. Suppressing the newer answer must not
669
+ // preserve that older admission. Forget it; the floor still prevents
670
+ // either lookup (or an evicted, newer one) from being overwritten.
671
+ if (current && current.sequence < verdict.sequence)
672
+ membershipCache.delete(key);
673
+ return;
388
674
  }
389
- return true;
675
+ membershipCache.delete(key);
676
+ membershipCache.set(key, verdict);
677
+ if (membershipCache.size <= GATE_CACHE_MAX_IDENTITIES)
678
+ return;
679
+ const [oldestKey, oldest] = membershipCache.entries().next().value;
680
+ membershipCache.delete(oldestKey);
681
+ evictedMembershipSequence = Math.max(evictedMembershipSequence, oldest.sequence);
390
682
  };
683
+ /**
684
+ * The organization half of admission, checked before the domain allowlist
685
+ * and `gate`, for every token type and session. No claim answers it or is
686
+ * read: an OAuth JWT verifies offline and can outlive a removed membership
687
+ * by a day, and a member's token is audience-bound to this deployment
688
+ * whichever organization they chose at consent. Membership always comes
689
+ * from the Backend API, cached per organization and user for 60 seconds
690
+ * from the start of the lookup that answered (30 for a non-member), since
691
+ * Clerk answered no earlier. With a clock that does not step back unseen
692
+ * within the TTL window, every request reaching this check 60 seconds or
693
+ * more after a member's removal is refused. An unseen rollback within that
694
+ * window can extend admission beyond 60 seconds of elapsed time.
695
+ *
696
+ * Each request that misses the cache runs and owns its own lookup. On
697
+ * Workers one request may not touch another's fetch, signal, or body, so
698
+ * concurrent misses within the plain-counter bounds each ask Clerk, and a caller that leaves cancels only
699
+ * its own lookup. A failed or abandoned lookup is not an answer, so it
700
+ * refuses without being cached.
701
+ */
702
+ const checkOrganization = authMethod((userId, signal) => {
703
+ return authFlow(function* () {
704
+ if (!organizationId)
705
+ return true;
706
+ const key = `${organizationId}/${userId}`;
707
+ const cached = cachedMembership(key);
708
+ if (cached !== undefined)
709
+ return cached;
710
+ const started = Date.now();
711
+ const sequence = ++membershipLookupsStarted;
712
+ let member;
713
+ try {
714
+ member = (yield authCall(organizationLookup, [
715
+ userId,
716
+ signal,
717
+ authMethod(() => {
718
+ return authCall(organizationMember, [opts.secretKey, organizationId, userId, signal]);
719
+ }),
720
+ ]));
721
+ }
722
+ catch (error) {
723
+ if (error instanceof OrganizationLookupBusy)
724
+ return organizationBusy(error);
725
+ logDenial("organization_lookup_failed");
726
+ return false;
727
+ }
728
+ cacheMembership(key, { member, started, sequence });
729
+ if (!member)
730
+ logDenial("organization_membership_denied");
731
+ return member;
732
+ });
733
+ });
391
734
  /**
392
735
  * Is this authenticated user admitted? The domain allowlist and `gate` both
393
736
  * have to say yes, and the allowlist runs first so an outsider never reaches
394
737
  * operator gate code. One cached verdict covers both, so composing them costs
395
738
  * no more Clerk calls than `gate` alone did.
396
739
  */
397
- const checkGate = async (userId) => {
398
- if (!opts.gate && !allowedDomains)
399
- return true;
400
- const hit = readIdentityCache(gateCache, userId);
401
- if (hit)
402
- return hit.allowed;
403
- let allowed = false;
404
- try {
405
- const domainAllowed = await checkDomain(userId);
406
- allowed = domainAllowed && (opts.gate ? await opts.gate(userId, clerk) : true);
407
- if (domainAllowed && !allowed)
408
- logDenial("gate_denied");
409
- }
410
- catch {
411
- logDenial("gate_failed");
412
- allowed = false;
413
- }
414
- writeIdentityCache(gateCache, userId, {
415
- allowed,
416
- exp: Date.now() + (allowed ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS),
740
+ const checkGate = authMethod((userId, clerk, signal) => {
741
+ return authFlow(function* () {
742
+ if (!opts.gate && !allowedDomains)
743
+ return true;
744
+ const hit = readIdentityCache(gateCache, userId);
745
+ if (hit)
746
+ return hit.allowed;
747
+ let allowed;
748
+ try {
749
+ const domainAllowed = (yield authCall(checkDomain, [userId, clerk]));
750
+ if (domainAllowed === undefined)
751
+ return false;
752
+ signal.throwIfAborted();
753
+ allowed = domainAllowed && (opts.gate ? (yield authInvoke(opts, "gate", [userId, clerk])) : true);
754
+ // `clerk` is bound to this signal; a gate may turn its cancelled
755
+ // lookup into `false`. Only a completed, uncancelled verdict is cached.
756
+ signal.throwIfAborted();
757
+ if (domainAllowed && !allowed)
758
+ logDenial("gate_denied");
759
+ }
760
+ catch {
761
+ logDenial("gate_failed");
762
+ return false;
763
+ }
764
+ writeIdentityCache(gateCache, userId, {
765
+ allowed,
766
+ exp: Date.now() + (allowed ? GATE_ALLOWED_TTL_MS : GATE_FORBIDDEN_TTL_MS),
767
+ });
768
+ return allowed;
417
769
  });
418
- return allowed;
419
- };
770
+ });
420
771
  return {
421
772
  kind: "clerk",
422
773
  interactiveOperator: true,
@@ -436,126 +787,182 @@ export function clerkAuth(opts) {
436
787
  ...(opts.signInUrl ? { signInUrl: opts.signInUrl } : {}),
437
788
  ...(opts.signUpUrl ? { signUpUrl: opts.signUpUrl } : {}),
438
789
  },
439
- async handleMetadata(request, baseUrl) {
440
- const { pathname } = new URL(request.url);
441
- const protectedResource = pathname === "/.well-known/oauth-protected-resource" ||
442
- pathname === "/.well-known/oauth-protected-resource/mcp" ||
443
- /^\/\.well-known\/oauth-protected-resource\/mcp\/[a-z0-9_-]+$/.test(pathname);
444
- if (!protectedResource)
445
- return null;
446
- if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS") {
447
- return new Response(null, { status: 405, headers: { ...CORS_HEADERS, Allow: "GET, HEAD, OPTIONS" } });
448
- }
449
- if (request.method === "OPTIONS") {
450
- return new Response(null, { status: 204, headers: CORS_HEADERS });
451
- }
452
- const base = resolveBase(baseUrl);
453
- const pool = pathname.startsWith("/.well-known/oauth-protected-resource/mcp/")
454
- ? mcpPoolSuffix(pathname.slice("/.well-known/oauth-protected-resource".length))
455
- : null;
456
- if (pathname === "/.well-known/oauth-protected-resource" ||
457
- pathname === "/.well-known/oauth-protected-resource/mcp" ||
458
- pool) {
459
- return Response.json({
460
- resource: `${base}/mcp${pool ?? ""}`,
461
- authorization_servers: [frontendApiUrl],
462
- bearer_methods_supported: ["header"],
463
- scopes_supported: scopes,
464
- }, { headers: CORS_HEADERS });
465
- }
466
- return null;
467
- },
468
- async authorize(request, baseUrl) {
469
- const credential = authorizationCredential(request);
470
- const tokenPresent = credential.kind !== "absent";
471
- if (credential.kind === "invalid" || isMachineCredential(request)) {
472
- logDenial("authorization_header_invalid");
473
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
474
- }
475
- const browserOAuthRoute = /^\/(?:connect|oauth\/callback)\//.test(new URL(request.url).pathname);
476
- let userId;
477
- let sessionCookies;
478
- try {
479
- const pathname = new URL(request.url).pathname;
480
- const isMcp = pathname === "/mcp" || pathname.startsWith("/mcp/");
481
- const state = await clerk.authenticateRequest(authorizationRequest(request), {
482
- acceptsToken: isMcp ? "oauth_token" : "session_token",
483
- });
484
- const browserOAuth = !tokenPresent && browserOAuthRoute;
485
- // Consent can outlast Clerk's session JWT. Preserve the SDK's browser
486
- // handshake, which returns here with a refreshed, verified session.
487
- if (browserOAuth && state.status === "handshake" && state.headers.has("location")) {
488
- return {
489
- ok: false,
490
- response: new Response(null, {
491
- status: 307,
492
- headers: state.headers,
493
- }),
494
- };
790
+ handleMetadata: authMethod(function (request, baseUrl) {
791
+ return authValue(() => {
792
+ const { pathname } = new URL(request.url);
793
+ const protectedResource = pathname === "/.well-known/oauth-protected-resource" ||
794
+ pathname === "/.well-known/oauth-protected-resource/mcp" ||
795
+ /^\/\.well-known\/oauth-protected-resource\/mcp\/[a-z0-9_-]+$/.test(pathname);
796
+ if (!protectedResource)
797
+ return null;
798
+ if (request.method !== "GET" && request.method !== "HEAD" && request.method !== "OPTIONS") {
799
+ return new Response(null, { status: 405, headers: { ...CORS_HEADERS, Allow: "GET, HEAD, OPTIONS" } });
495
800
  }
496
- if (browserOAuth)
497
- sessionCookies = state.headers?.getSetCookie();
498
- const auth = state.toAuth();
499
- if (!auth?.isAuthenticated) {
500
- logDenial("authentication_failed");
501
- return {
502
- ok: false,
503
- response: unauthorized(baseUrl, tokenPresent, request),
504
- };
801
+ if (request.method === "OPTIONS") {
802
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
505
803
  }
506
- if (isMcp) {
507
- const resource = `${resolveBase(baseUrl)}/mcp${mcpPoolSuffix(pathname) ?? ""}`;
508
- if (auth.tokenType !== "oauth_token") {
509
- logDenial("token_type_mismatch");
510
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
511
- }
512
- const token = await auth.getToken();
513
- let claims;
514
- if (token.startsWith("oat_")) {
515
- try {
516
- claims = await opaqueOAuthClaims(token, opts.secretKey, auth.userId, auth.clientId, request.signal);
804
+ const base = resolveBase(baseUrl);
805
+ const pool = pathname.startsWith("/.well-known/oauth-protected-resource/mcp/")
806
+ ? mcpPoolSuffix(pathname.slice("/.well-known/oauth-protected-resource".length))
807
+ : null;
808
+ if (pathname === "/.well-known/oauth-protected-resource" ||
809
+ pathname === "/.well-known/oauth-protected-resource/mcp" ||
810
+ pool) {
811
+ return Response.json({
812
+ resource: `${base}/mcp${pool ?? ""}`,
813
+ authorization_servers: [frontendApiUrl],
814
+ bearer_methods_supported: ["header"],
815
+ scopes_supported: scopes,
816
+ }, { headers: CORS_HEADERS });
817
+ }
818
+ return null;
819
+ });
820
+ }),
821
+ authorize: authMethod(function (request, baseUrl) {
822
+ return authFlow(function* () {
823
+ const credential = authorizationCredential(request);
824
+ const tokenPresent = credential.kind !== "absent";
825
+ if (credential.kind === "invalid" || isMachineCredential(request)) {
826
+ logDenial("authorization_header_invalid");
827
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
828
+ }
829
+ const finished = new AbortController();
830
+ const signal = AbortSignal.any([request.signal, finished.signal]);
831
+ const timer = setTimeout(() => finished.abort(new Error("Clerk authentication timed out")), BACKEND_LOOKUP_TIMEOUT_MS);
832
+ const pending = new Set();
833
+ // SDK-created verification clients, JWKS, handshakes, domain lookups and
834
+ // gate calls all close over this authorization's signal, never another
835
+ // request's client. The long-lived client above serves display labels only.
836
+ const clerk = createByteReadingClerkClient(opts, signal, pending);
837
+ try {
838
+ signal.throwIfAborted();
839
+ const browserOAuthRoute = /^\/(?:connect|oauth\/callback)\//.test(new URL(request.url).pathname);
840
+ let userId;
841
+ let sessionCookies;
842
+ try {
843
+ const pathname = new URL(request.url).pathname;
844
+ const isMcp = pathname === "/mcp" || pathname.startsWith("/mcp/");
845
+ const state = (yield authInvoke(clerk, "authenticateRequest", [
846
+ authorizationRequest(request),
847
+ {
848
+ acceptsToken: isMcp ? "oauth_token" : "session_token",
849
+ },
850
+ ]));
851
+ const browserOAuth = !tokenPresent && browserOAuthRoute;
852
+ // Consent can outlast Clerk's session JWT. Preserve the SDK's browser
853
+ // handshake, which returns here with a refreshed, verified session.
854
+ if (browserOAuth && state.status === "handshake" && state.headers.has("location")) {
855
+ return {
856
+ ok: false,
857
+ response: new Response(null, {
858
+ status: 307,
859
+ headers: state.headers,
860
+ }),
861
+ };
517
862
  }
518
- catch {
519
- logDenial("oauth_verification_failed");
520
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
863
+ if (browserOAuth)
864
+ sessionCookies = state.headers?.getSetCookie();
865
+ const auth = state.toAuth();
866
+ if (!auth?.isAuthenticated) {
867
+ logDenial("authentication_failed");
868
+ return {
869
+ ok: false,
870
+ response: unauthorized(baseUrl, tokenPresent, request),
871
+ };
521
872
  }
873
+ if (isMcp) {
874
+ const resource = `${resolveBase(baseUrl)}/mcp${mcpPoolSuffix(pathname) ?? ""}`;
875
+ if (auth.tokenType !== "oauth_token") {
876
+ logDenial("token_type_mismatch");
877
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
878
+ }
879
+ const token = (yield authInvoke(auth, "getToken", []));
880
+ let claims;
881
+ if (token.startsWith("oat_")) {
882
+ try {
883
+ const verify = authMethod(() => {
884
+ return authCall(opaqueOAuthClaims, [
885
+ token,
886
+ opts.secretKey,
887
+ auth.userId,
888
+ auth.clientId,
889
+ signal,
890
+ ]);
891
+ });
892
+ claims = (yield organizationId
893
+ ? authCall(organizationLookup, [auth.userId ?? "", signal, verify])
894
+ : authCall(verify, []));
895
+ }
896
+ catch (error) {
897
+ if (error instanceof OrganizationLookupBusy)
898
+ return { ok: false, response: organizationBusy(error) };
899
+ logDenial("oauth_verification_failed");
900
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
901
+ }
902
+ }
903
+ else {
904
+ claims = decodeJwt(token).payload;
905
+ }
906
+ const rejection = oauthBindingRejection(claims, resource, auth.clientId, allowedOAuthClientIds);
907
+ if (rejection) {
908
+ logDenial(rejection);
909
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
910
+ }
911
+ // The membership lookup is keyed by subject, so on a shared
912
+ // instance the issuer and subject must be this instance's and the
913
+ // SDK's. An opaque token's claims come from this instance's Backend
914
+ // API, which has already matched the subject and carries no issuer.
915
+ if (organizationId &&
916
+ !token.startsWith("oat_") &&
917
+ (claims.iss !== frontendApiUrl || claims.sub !== auth.userId)) {
918
+ logDenial("token_identity_mismatch");
919
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
920
+ }
921
+ }
922
+ else {
923
+ if (auth.tokenType !== "session_token") {
924
+ logDenial("token_type_mismatch");
925
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
926
+ }
927
+ // Browser session origins retain their existing deployment pin.
928
+ const azp = auth.sessionClaims?.azp;
929
+ const origin = new URL(resolveBase(baseUrl)).origin;
930
+ if (azp && azp !== origin) {
931
+ logDenial("session_origin_mismatch");
932
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
933
+ }
934
+ // The session's active organization is not read; membership is.
935
+ if (organizationId &&
936
+ (auth.sessionClaims.iss !== frontendApiUrl || auth.sessionClaims.sub !== auth.userId)) {
937
+ logDenial("token_identity_mismatch");
938
+ return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
939
+ }
940
+ }
941
+ userId = auth.userId ?? undefined;
522
942
  }
523
- else {
524
- claims = decodeJwt(token).payload;
525
- }
526
- const rejection = oauthBindingRejection(claims, resource, auth.clientId, allowedOAuthClientIds);
527
- if (rejection) {
528
- logDenial(rejection);
529
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
943
+ catch {
944
+ logDenial("authentication_failed");
945
+ return { ok: false, response: unauthorized(baseUrl, true, request) };
530
946
  }
531
- }
532
- else {
533
- if (auth.tokenType !== "session_token") {
534
- logDenial("token_type_mismatch");
535
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
947
+ if (!userId) {
948
+ logDenial("user_missing");
949
+ return { ok: false, response: unauthorized(baseUrl, true, request) };
536
950
  }
537
- // Browser session origins retain their existing deployment pin.
538
- const azp = auth.sessionClaims?.azp;
539
- const origin = new URL(resolveBase(baseUrl)).origin;
540
- if (azp && azp !== origin) {
541
- logDenial("session_origin_mismatch");
542
- return { ok: false, response: unauthorized(baseUrl, tokenPresent, request) };
951
+ const membership = (yield authCall(checkOrganization, [userId, signal]));
952
+ if (membership instanceof Response)
953
+ return { ok: false, response: membership };
954
+ if (!membership || !(yield authCall(checkGate, [userId, clerk, signal]))) {
955
+ return { ok: false, response: forbidden() };
543
956
  }
957
+ signal.throwIfAborted();
958
+ return { ok: true, userId, ...(sessionCookies?.length ? { sessionCookies } : {}) };
544
959
  }
545
- userId = auth.userId ?? undefined;
546
- }
547
- catch {
548
- logDenial("authentication_failed");
549
- return { ok: false, response: unauthorized(baseUrl, true, request) };
550
- }
551
- if (!userId) {
552
- logDenial("user_missing");
553
- return { ok: false, response: unauthorized(baseUrl, true, request) };
554
- }
555
- if (!(await checkGate(userId))) {
556
- return { ok: false, response: forbidden() };
557
- }
558
- return { ok: true, userId, ...(sessionCookies?.length ? { sessionCookies } : {}) };
559
- },
960
+ finally {
961
+ clearTimeout(timer);
962
+ finished.abort();
963
+ (yield authAll(pending, true));
964
+ }
965
+ });
966
+ }),
560
967
  };
561
968
  }