@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -0,0 +1,335 @@
1
+ // Deterministic inverse RFC 6570 expansion for advertised resource templates.
2
+ // Each expression consumes up to the leftmost following literal. Ambiguous
3
+ // boundaries are refused rather than searched again with another capture.
4
+ const MAX_URI_LENGTH = 8192;
5
+ const MAX_MATCH_WORK = 262_144;
6
+ /** Bound parsing plus templates × URI length for the whole resource read. */
7
+ export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
8
+ let work = 0;
9
+ for (const template of templates) {
10
+ work += uri.length + template.uriTemplate.length;
11
+ if (work > MAX_MATCH_WORK) {
12
+ note("resource_match_budget_exceeded");
13
+ return { matched: false, refusal: "resource_match_budget_exceeded" };
14
+ }
15
+ }
16
+ let matched = false;
17
+ let refusal;
18
+ for (const template of templates) {
19
+ const parsed = parse(template.uriTemplate);
20
+ if (parsed === "resource_template_ambiguous") {
21
+ note(parsed);
22
+ refusal = parsed;
23
+ }
24
+ else if (parsed && matches(uri, parsed))
25
+ matched = true;
26
+ }
27
+ return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
28
+ }
29
+ export function resourceUriMatchesTemplate(uri, template) {
30
+ const parsed = parse(template);
31
+ return typeof parsed === "object" && matches(uri, parsed);
32
+ }
33
+ function parse(template) {
34
+ if (template.length > MAX_URI_LENGTH)
35
+ return;
36
+ const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:/.exec(template)?.[0];
37
+ if (!scheme)
38
+ return;
39
+ const authority = literalAuthority(template.slice(scheme.length));
40
+ if (authority !== undefined && /[{}]/.test(authority))
41
+ return;
42
+ const parts = [];
43
+ let offset = 0;
44
+ let count = 0;
45
+ while (offset < template.length) {
46
+ const start = template.indexOf("{", offset);
47
+ if (start < 0)
48
+ break;
49
+ const literal = template.slice(offset, start);
50
+ const end = template.indexOf("}", start + 1);
51
+ if (literal.includes("}") || end < 0 || ++count > 16)
52
+ return;
53
+ const body = template.slice(start + 1, end);
54
+ const operator = /^[+#./;?&]/.test(body) ? body[0] : "";
55
+ const specs = body.slice(operator.length).split(",");
56
+ if (specs.length > 16)
57
+ return;
58
+ const variables = [];
59
+ for (const spec of specs) {
60
+ const value = /^((?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*)(\*|:[1-9][0-9]{0,3})?$/.exec(spec);
61
+ if (!value)
62
+ return;
63
+ variables.push({
64
+ name: value[1],
65
+ explode: value[2] === "*",
66
+ prefix: value[2]?.startsWith(":") ? Number(value[2].slice(1)) : undefined,
67
+ });
68
+ }
69
+ if (new Set(variables.map((value) => value.name)).size !== variables.length ||
70
+ (variables.length > 1 && variables.some((value) => value.explode)))
71
+ return;
72
+ parts.push(literal, { operator, variables });
73
+ offset = end + 1;
74
+ }
75
+ const tail = template.slice(offset);
76
+ if (/[{}]/.test(tail))
77
+ return;
78
+ parts.push(tail);
79
+ for (let index = 1; index < parts.length; index += 2) {
80
+ const expression = parts[index];
81
+ const literal = parts[index + 1];
82
+ if ((!literal && index + 2 < parts.length) ||
83
+ (literal && Array.from(literal).every((character) => expressionCharacter(expression, character)))) {
84
+ return "resource_template_ambiguous";
85
+ }
86
+ }
87
+ return { parts, scheme, authority };
88
+ }
89
+ function literalAuthority(rest) {
90
+ if (!rest.startsWith("//"))
91
+ return;
92
+ const value = rest.slice(2);
93
+ const end = value.search(/[/?#]|\{[/?#]/);
94
+ return end < 0 ? value : value.slice(0, end);
95
+ }
96
+ function expressionCharacter({ operator, variables }, character) {
97
+ const base = operator === "+" || operator === "#"
98
+ ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.~!$'()*+@-/,%"
99
+ : operator === "."
100
+ ? "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~-%"
101
+ : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.~-%";
102
+ const syntax = operator === ";"
103
+ ? ";=,"
104
+ : operator === "?" || operator === "&"
105
+ ? operator + "&=,"
106
+ : operator === "/" || operator === "."
107
+ ? operator + ","
108
+ : ",";
109
+ return (base.includes(character) || syntax.includes(character) || variables.some((value) => value.name.includes(character)));
110
+ }
111
+ // KMP gives a single forward scan even when a literal has repeated prefixes.
112
+ function findLiteral(uri, literal, offset) {
113
+ const failure = Array.from({ length: literal.length }, () => 0);
114
+ for (let index = 1, prefix = 0; index < literal.length; index++) {
115
+ while (prefix && literal[index] !== literal[prefix])
116
+ prefix = failure[prefix - 1];
117
+ if (literal[index] === literal[prefix])
118
+ prefix++;
119
+ failure[index] = prefix;
120
+ }
121
+ for (let index = offset, prefix = 0; index < uri.length; index++) {
122
+ while (prefix && uri[index] !== literal[prefix])
123
+ prefix = failure[prefix - 1];
124
+ if (uri[index] === literal[prefix])
125
+ prefix++;
126
+ if (prefix === literal.length)
127
+ return index + 1 - prefix;
128
+ }
129
+ return -1;
130
+ }
131
+ function matches(uri, { parts, scheme, authority }) {
132
+ if (uri.length > MAX_URI_LENGTH ||
133
+ !uri.startsWith(scheme) ||
134
+ (authority === undefined && uri.slice(scheme.length).startsWith("//")))
135
+ return false;
136
+ const values = new Map();
137
+ const capture = (variable, raw, path) => {
138
+ const decoded = raw.map((value) => safeValue(value, variable.prefix, path));
139
+ if (decoded.some((value) => value === undefined))
140
+ return false;
141
+ const prior = values.get(variable.name) ?? [];
142
+ prior.push({ parts: decoded, prefix: variable.prefix });
143
+ values.set(variable.name, prior);
144
+ return true;
145
+ };
146
+ let offset = 0;
147
+ for (let index = 0; index < parts.length; index++) {
148
+ const part = parts[index];
149
+ if (typeof part === "string") {
150
+ if (!uri.startsWith(part, offset))
151
+ return false;
152
+ offset += part.length;
153
+ continue;
154
+ }
155
+ const literal = parts[index + 1];
156
+ const end = literal ? findLiteral(uri, literal, offset) : uri.length;
157
+ if (end < 0)
158
+ return false;
159
+ const raw = uri.slice(offset, end);
160
+ offset = end;
161
+ if (!raw)
162
+ continue;
163
+ const { operator, variables } = part;
164
+ const prefix = operator === "+" ? "" : operator;
165
+ if (!raw.startsWith(prefix))
166
+ return false;
167
+ const body = raw.slice(prefix.length);
168
+ if (operator === "?" || operator === "&" || operator === ";") {
169
+ const tokens = body.split(operator === ";" ? ";" : "&");
170
+ let last = -1;
171
+ for (const token of tokens) {
172
+ const equal = token.indexOf("=");
173
+ const name = equal < 0 ? token : token.slice(0, equal);
174
+ const next = variables.findIndex((value) => value.name === name);
175
+ if (next <= last || next < 0 || (equal < 0 && operator !== ";"))
176
+ return false;
177
+ last = next;
178
+ const variable = variables[next];
179
+ const value = equal < 0 ? "" : token.slice(equal + 1);
180
+ if (!capture(variable, variable.prefix === undefined ? value.split(",") : [value], false))
181
+ return false;
182
+ }
183
+ }
184
+ else {
185
+ const separator = operator === "/" || operator === "." ? operator : ",";
186
+ if (operator === "." && body.includes(".."))
187
+ return false;
188
+ const tokens = body.split(separator);
189
+ if (operator === "/" && tokens.length > 1 && tokens.some((value) => !value))
190
+ return false;
191
+ if (variables.length === 1) {
192
+ const variable = variables[0];
193
+ const list = variable.prefix === undefined && (variable.explode || (operator !== "/" && operator !== "."));
194
+ if ((!list && tokens.length > 1) ||
195
+ !capture(variable, list ? tokens : [body], operator === "+" || operator === "#"))
196
+ return false;
197
+ }
198
+ else {
199
+ if (tokens.length > variables.length)
200
+ return false;
201
+ for (const [i, value] of tokens.entries())
202
+ if (!capture(variables[i], [value], operator === "+" || operator === "#"))
203
+ return false;
204
+ }
205
+ }
206
+ }
207
+ if (offset !== uri.length)
208
+ return false;
209
+ const candidates = new Map();
210
+ for (const [name, occurrences] of values) {
211
+ const candidate = occurrences.find((value) => value.prefix === undefined || Array.from(value.parts[0]).length < value.prefix) ??
212
+ occurrences.reduce((a, b) => (a.parts[0].length >= b.parts[0].length ? a : b));
213
+ if (!occurrences.every((value) => value.prefix === undefined
214
+ ? JSON.stringify(value.parts) === JSON.stringify(candidate.parts)
215
+ : value.parts.length === 1 &&
216
+ value.parts[0] === Array.from(candidate.parts[0]).slice(0, value.prefix).join("")))
217
+ return false;
218
+ candidates.set(name, candidate.parts);
219
+ }
220
+ const expanded = parts.map((part) => (typeof part === "string" ? part : expand(part, candidates))).join("");
221
+ const canonical = (text) => text.replace(/%[0-9a-f]{2}/gi, (value) => value.toUpperCase());
222
+ // Empty expressions can join literals into an authority the template did
223
+ // not advertise. Compare the final RFC 3986 authority after re-expansion.
224
+ const rest = uri.slice(scheme.length);
225
+ return (canonical(expanded) === canonical(uri) &&
226
+ literalAuthority(rest) === authority &&
227
+ sameHost(uri, scheme, authority) &&
228
+ safePath(authority === undefined ? rest : rest.slice(authority.length + 2)));
229
+ }
230
+ function sameHost(uri, scheme, authority) {
231
+ // WHATWG special schemes can turn an RFC 3986 path into a host. A literal
232
+ // authority supplies the expected host, including normal port/IDNA handling;
233
+ // without one, WHATWG must also find no host. Parser errors stay local.
234
+ if (authority !== undefined && /[\\\p{Cc}\p{Cf}]/u.test(authority))
235
+ return false;
236
+ try {
237
+ const host = authority === undefined ? "" : new URL(`${scheme}//${authority}/`).host;
238
+ return new URL(uri).host === host;
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }
244
+ function safePath(path) {
245
+ // Inspect the RFC 3986 path before WHATWG removes dot segments. Empty
246
+ // captures can combine with literals to make traversal absent from any
247
+ // individual value. Queries and fragments do not contribute path segments.
248
+ const end = path.search(/[?#]/);
249
+ let value = end < 0 ? path : path.slice(0, end);
250
+ for (let depth = 0; depth < 8; depth++) {
251
+ if (/[\\\p{Cc}\p{Cf}]/u.test(value) || unsafeUnicode(value) || /(?:^|\/)\.{1,2}(?:\/|$)/.test(value))
252
+ return false;
253
+ if (!value.includes("%") || (depth > 0 && !/%[0-9a-f]{2}/i.test(value)))
254
+ return true;
255
+ try {
256
+ value = depth === 0 ? decodeURIComponent(value) : decodeNestedPercentEscapes(value);
257
+ }
258
+ catch {
259
+ return false;
260
+ }
261
+ }
262
+ return false;
263
+ }
264
+ function expand({ operator, variables }, values) {
265
+ const named = operator === ";" || operator === "?" || operator === "&";
266
+ const prefix = operator === "+" ? "" : operator;
267
+ const separator = operator === "/" || operator === "." || operator === ";" ? operator : named ? "&" : ",";
268
+ const encode = (value) => operator === "+" || operator === "#"
269
+ ? value
270
+ .split(/(%[0-9a-f]{2})/gi)
271
+ .map((part) => (/^%[0-9a-f]{2}$/i.test(part) ? part : encodeURI(part)))
272
+ .join("")
273
+ : encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
274
+ const nameValue = (name, value) => named ? name + (value || operator !== ";" ? `=${value}` : "") : value;
275
+ const expansions = [];
276
+ for (const variable of variables) {
277
+ const parts = values.get(variable.name);
278
+ if (!parts)
279
+ continue;
280
+ const encoded = parts.map((value) => encode(variable.prefix === undefined ? value : Array.from(value).slice(0, variable.prefix).join("")));
281
+ expansions.push(variable.explode && parts.length > 1
282
+ ? encoded.map((value) => nameValue(variable.name, value)).join(separator)
283
+ : nameValue(variable.name, encoded.join(",")));
284
+ }
285
+ return expansions.length ? prefix + expansions.join(separator) : "";
286
+ }
287
+ function safeValue(raw, prefix, path) {
288
+ let expanded;
289
+ try {
290
+ expanded = decodeURIComponent(raw);
291
+ }
292
+ catch {
293
+ return undefined;
294
+ }
295
+ let value = expanded;
296
+ // Check each decoding layer. Reserved paths may contain slashes, but cannot
297
+ // introduce URI syntax, a network path, traversal, controls or format marks.
298
+ for (let depth = 0; depth < 8; depth++) {
299
+ if (/[\\:?#&;=\p{Cc}\p{Cf}]/u.test(value) ||
300
+ unsafeUnicode(value) ||
301
+ (!path && value.includes("/")) ||
302
+ value.startsWith("/") ||
303
+ value.split("/").some((segment) => segment === "." || segment === ".."))
304
+ return undefined;
305
+ if (!/%[0-9a-f]{2}/i.test(value))
306
+ return prefix === undefined || Array.from(expanded).length <= prefix ? expanded : undefined;
307
+ try {
308
+ value = decodeNestedPercentEscapes(value);
309
+ }
310
+ catch {
311
+ return undefined;
312
+ }
313
+ }
314
+ return undefined;
315
+ }
316
+ function decodeNestedPercentEscapes(value) {
317
+ // The first decode stays strict. Later layers can contain literal percent
318
+ // signs decoded from %25. Preserve those while decoding every remaining
319
+ // %HH escape, so literal data cannot hide unsafe escapes elsewhere. Invalid
320
+ // UTF-8 still throws and the callers keep the same eight-layer bound.
321
+ return decodeURIComponent(value.replace(/%(?![0-9a-f]{2})/gi, "%25"));
322
+ }
323
+ function unsafeUnicode(value) {
324
+ // Refuse non-ASCII separators and compatibility lookalikes of URI syntax,
325
+ // rather than all non-ASCII. Ordinary percent-encoded UTF-8 path segments
326
+ // and ASCII spaces remain valid. Inspect every decoding layer without
327
+ // normalizing the value used for expansion.
328
+ for (const character of value) {
329
+ if (character <= "\x7f")
330
+ continue;
331
+ if (/\p{Z}/u.test(character) || /[\\/:?#&;=.%@]/.test(character.normalize("NFKC")))
332
+ return true;
333
+ }
334
+ return false;
335
+ }
@@ -27,6 +27,16 @@ export interface CredentialVault {
27
27
  * Without both `seal` and `open`, that state is stored as plaintext.
28
28
  */
29
29
  seal?(connectorId: string, purpose: string, plaintext: string, owner?: string): Promise<string>;
30
+ /** Sign browser OAuth handoffs with a purpose-specific key; nothing is stored. */
31
+ signOAuthHandoff?(payload: string): Promise<string>;
32
+ /** Verify a browser OAuth handoff signature without exposing the key. */
33
+ verifyOAuthHandoff?(payload: string, signature: string): Promise<boolean>;
34
+ /**
35
+ * Host-only key for the MCP request-state codec. Must be a stable,
36
+ * purpose-specific deployment key of at least 32 bytes. The registry never
37
+ * exposes the vault or this key through a connector context or guest API.
38
+ */
39
+ requestStateKey?(): Promise<Uint8Array>;
30
40
  /** Reverse `seal` for the same connector, purpose, and owner; rejects otherwise. */
31
41
  open?(connectorId: string, purpose: string, sealed: string, owner?: string): Promise<string>;
32
42
  }
@@ -25,9 +25,7 @@ export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR = "Stored credential fields
25
25
  export function storedCredentialShape(config, stored) {
26
26
  if (!stored)
27
27
  return { state: "missing" };
28
- const mode = config.fields?.length
29
- ? "multiple"
30
- : "single";
28
+ const mode = config.fields?.length ? "multiple" : "single";
31
29
  const declared = new Set(mode === "multiple" ? config.fields.map((field) => field.name) : ["value"]);
32
30
  const actual = Object.keys(stored);
33
31
  const present = new Set(actual);
@@ -0,0 +1,108 @@
1
+ import { ConnectorCallError } from "./errors.js";
2
+ function refused() {
3
+ throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL cannot be safely checked; refusing authorization.", { retryable: false });
4
+ }
5
+ /** Matching only: never change the endpoint that discovery supplied. */
6
+ export function percentDecoded(value) {
7
+ for (let pass = 0; pass < 8; pass++) {
8
+ const next = value.replace(/(?:%[0-9a-f]{2})+/gi, (escaped) => {
9
+ try {
10
+ return decodeURIComponent(escaped);
11
+ }
12
+ catch {
13
+ // Replacement decoding preserves valid UTF-8 beside malformed bytes.
14
+ const bytes = escaped.match(/%[0-9a-f]{2}/gi);
15
+ return new TextDecoder().decode(Uint8Array.from(bytes, (byte) => parseInt(byte.slice(1), 16)));
16
+ }
17
+ });
18
+ if (next === value)
19
+ return value;
20
+ value = next;
21
+ }
22
+ // Deeper nesting has no stable matching view within the normalization bound.
23
+ if (/%[0-9a-f]{2}/i.test(value))
24
+ refused();
25
+ return value;
26
+ }
27
+ /** RFC 3492 decoding, using no Node-only URL or punycode implementation. URL
28
+ * has already validated the ASCII host. Bound arithmetic and Unicode scalars. */
29
+ function unicodeLabel(label) {
30
+ if (!label.startsWith("xn--"))
31
+ return label;
32
+ const input = label.slice(4);
33
+ const delimiter = input.lastIndexOf("-");
34
+ const output = delimiter < 0 ? [] : Array.from(input.slice(0, delimiter), (char) => char.charCodeAt(0));
35
+ let cursor = delimiter < 0 ? 0 : delimiter + 1;
36
+ let n = 128;
37
+ let i = 0;
38
+ let bias = 72;
39
+ while (cursor < input.length) {
40
+ const previous = i;
41
+ let weight = 1;
42
+ for (let k = 36;; k += 36) {
43
+ const code = input.charCodeAt(cursor++);
44
+ const digit = code >= 97 && code <= 122 ? code - 97 : code >= 48 && code <= 57 ? code - 22 : 36;
45
+ if (digit >= 36)
46
+ refused();
47
+ i += digit * weight;
48
+ if (!Number.isSafeInteger(i))
49
+ refused();
50
+ const threshold = k <= bias ? 1 : k >= bias + 26 ? 26 : k - bias;
51
+ if (digit < threshold)
52
+ break;
53
+ weight *= 36 - threshold;
54
+ if (!Number.isSafeInteger(weight))
55
+ refused();
56
+ }
57
+ const count = output.length + 1;
58
+ let delta = previous === 0 ? Math.floor((i - previous) / 700) : Math.floor((i - previous) / 2);
59
+ delta += Math.floor(delta / count);
60
+ let k = 0;
61
+ while (delta > 455) {
62
+ delta = Math.floor(delta / 35);
63
+ k += 36;
64
+ }
65
+ bias = k + Math.floor((36 * delta) / (delta + 38));
66
+ n += Math.floor(i / count);
67
+ if (n > 0x10ffff || (n >= 0xd800 && n <= 0xdfff))
68
+ refused();
69
+ i %= count;
70
+ output.splice(i++, 0, n);
71
+ }
72
+ return String.fromCodePoint(...output);
73
+ }
74
+ /** Decode each component, and concatenate with and without query names so a
75
+ * credential split across path/query boundaries still matches. Hosts have
76
+ * ASCII and Unicode views and match case-insensitively; paths remain exact. */
77
+ export function credentialUrlViews(value) {
78
+ let url;
79
+ try {
80
+ url = new URL(value);
81
+ }
82
+ catch {
83
+ return refused();
84
+ }
85
+ const host = url.hostname.toLowerCase();
86
+ const hosts = [host, host.split(".").map(unicodeLabel).join(".").toLowerCase()];
87
+ const path = url.pathname.split("/").map(percentDecoded);
88
+ const query = [...url.searchParams].map(([key, item]) => [percentDecoded(key), percentDecoded(item)]);
89
+ const prefix = [url.protocol, percentDecoded(url.username), percentDecoded(url.password)];
90
+ const suffix = [percentDecoded(url.hash.slice(1))];
91
+ const components = [
92
+ value,
93
+ percentDecoded(url.href),
94
+ ...prefix,
95
+ ...hosts,
96
+ url.port,
97
+ percentDecoded(url.pathname),
98
+ percentDecoded(url.search),
99
+ ...path,
100
+ ...query.flat(),
101
+ ...suffix,
102
+ ];
103
+ const joined = hosts.flatMap((hostname) => [
104
+ [...prefix, hostname, url.port, ...path, ...query.flat(), ...suffix].join(""),
105
+ [...prefix, hostname, url.port, ...path, ...query.map((entry) => entry[1]), ...suffix].join(""),
106
+ ]);
107
+ return { hosts, components, joined };
108
+ }
@@ -2,14 +2,19 @@ import type { CredentialMetadata, CredentialVault as Vault } from "./credential-
2
2
  import type { ConnectorCredentialValues, KVStorage } from "./types.js";
3
3
  /**
4
4
  * Encrypted, connector-scoped credential vault over the deployment's existing
5
- * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
5
+ * KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
6
6
  * secret outside the store. The same key seals downstream OAuth state, which
7
7
  * the OAuth provider stores itself.
8
8
  */
9
9
  export declare class CredentialVault implements Vault {
10
10
  private readonly storage;
11
11
  private readonly key;
12
+ private readonly handoffKey;
13
+ private readonly retryKey;
12
14
  constructor(storage: KVStorage, encryptionKey: string);
15
+ signOAuthHandoff(payload: string): Promise<string>;
16
+ requestStateKey(): Promise<Uint8Array>;
17
+ verifyOAuthHandoff(payload: string, signature: string): Promise<boolean>;
13
18
  private additionalData;
14
19
  private read;
15
20
  get(connectorId: string, field?: string, owner?: string): Promise<string | null>;
@@ -1,3 +1,5 @@
1
+ import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
2
+ import { credentialKeys } from "./storage/keys.js";
1
3
  const KEY_BYTES = 32;
2
4
  const IV_BYTES = 12;
3
5
  const MAX_CREDENTIAL_BYTES = 16_384;
@@ -14,9 +16,7 @@ function oauthStateAdditionalData(connectorId, purpose, owner) {
14
16
  return encoder.encode(`connecta:oauth-state:v1|${connectorId}|${owner ?? ""}|${purpose}`);
15
17
  }
16
18
  function storageKey(connectorId, owner) {
17
- return owner
18
- ? `principal:${owner}:conn:${connectorId}:credential:v1`
19
- : `conn:${connectorId}:credential:v1`;
19
+ return credentialKeys.credential(connectorId, owner);
20
20
  }
21
21
  function bytesToBase64(bytes) {
22
22
  let binary = "";
@@ -52,14 +52,11 @@ function parseEnvelope(raw) {
52
52
  function parsePlaintext(raw) {
53
53
  try {
54
54
  const parsed = JSON.parse(raw);
55
- const values = typeof parsed.value === "string"
56
- ? { value: parsed.value }
57
- : parsed.values;
55
+ const values = typeof parsed.value === "string" ? { value: parsed.value } : parsed.values;
58
56
  if (!values ||
59
57
  typeof values !== "object" ||
60
58
  Array.isArray(values) ||
61
- !Object.entries(values).every(([field, value]) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(field) &&
62
- typeof value === "string") ||
59
+ !Object.entries(values).every(([field, value]) => /^[A-Za-z][A-Za-z0-9_-]*$/.test(field) && typeof value === "string") ||
63
60
  typeof parsed.updatedAt !== "string" ||
64
61
  typeof parsed.updatedBy !== "string") {
65
62
  throw new Error("invalid plaintext");
@@ -88,33 +85,48 @@ function validateValues(values) {
88
85
  }
89
86
  normalized[field] = value;
90
87
  }
91
- if (encoder.encode(JSON.stringify(normalized)).byteLength >
92
- MAX_CREDENTIAL_BYTES) {
88
+ if (encoder.encode(JSON.stringify(normalized)).byteLength > MAX_CREDENTIAL_BYTES) {
93
89
  throw new Error(`Credential cannot exceed ${MAX_CREDENTIAL_BYTES} bytes`);
94
90
  }
95
91
  return normalized;
96
92
  }
97
93
  /**
98
94
  * Encrypted, connector-scoped credential vault over the deployment's existing
99
- * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
95
+ * KVStorage. Only ciphertext enters storage; the AES-GCM key remains an environment
100
96
  * secret outside the store. The same key seals downstream OAuth state, which
101
97
  * the OAuth provider stores itself.
102
98
  */
103
99
  export class CredentialVault {
104
100
  storage;
105
101
  key;
102
+ handoffKey;
103
+ retryKey;
106
104
  constructor(storage, encryptionKey) {
107
105
  this.storage = storage;
108
106
  const raw = base64ToBytes(encryptionKey.trim());
109
107
  if (raw.byteLength !== KEY_BYTES) {
110
108
  throw new Error("The credential vault encryption key must be a base64-encoded 32-byte key");
111
109
  }
110
+ this.handoffKey = deriveOAuthHandoffKey(raw);
111
+ this.retryKey = deriveRequestStateKey(raw);
112
112
  this.key = crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
113
113
  }
114
+ async signOAuthHandoff(payload) {
115
+ return bytesToBase64(new Uint8Array(await crypto.subtle.sign("HMAC", await this.handoffKey, encoder.encode(payload))));
116
+ }
117
+ async requestStateKey() {
118
+ return new Uint8Array(await this.retryKey);
119
+ }
120
+ async verifyOAuthHandoff(payload, signature) {
121
+ try {
122
+ return await crypto.subtle.verify("HMAC", await this.handoffKey, base64ToBytes(signature), encoder.encode(payload));
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
114
128
  additionalData(connectorId, owner) {
115
- return encoder.encode(owner
116
- ? `connecta:credential:principal:${owner}:${connectorId}:v1`
117
- : `connecta:credential:${connectorId}:v1`);
129
+ return encoder.encode(owner ? `connecta:credential:principal:${owner}:${connectorId}:v1` : `connecta:credential:${connectorId}:v1`);
118
130
  }
119
131
  async read(connectorId, owner) {
120
132
  const raw = await this.storage.get(storageKey(connectorId, owner));
@@ -207,10 +219,7 @@ export class CredentialVault {
207
219
  /** Decrypt what `seal` produced for this connector, purpose, and owner. */
208
220
  async open(connectorId, purpose, sealed, owner) {
209
221
  const [version, iv, ciphertext, ...rest] = sealed.split(".");
210
- if (version !== SEALED_VERSION ||
211
- iv === undefined ||
212
- ciphertext === undefined ||
213
- rest.length > 0) {
222
+ if (version !== SEALED_VERSION || iv === undefined || ciphertext === undefined || rest.length > 0) {
214
223
  throw new Error("Sealed OAuth state is invalid or corrupted");
215
224
  }
216
225
  try {