@zackbart/connecta 0.15.1 → 0.16.1

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 (335) hide show
  1. package/AGENTS.md +40 -20
  2. package/CHANGELOG.md +653 -0
  3. package/README.md +25 -5
  4. package/bin/connecta.mjs +44 -3
  5. package/dist/access-tokens.d.ts +0 -1
  6. package/dist/access-tokens.js +0 -1
  7. package/dist/activity.d.ts +50 -2
  8. package/dist/activity.js +34 -1
  9. package/dist/apps-shell.d.ts +0 -1
  10. package/dist/apps-shell.js +0 -1
  11. package/dist/auth/bearer.d.ts +0 -1
  12. package/dist/auth/bearer.js +0 -1
  13. package/dist/auth/clerk.d.ts +0 -1
  14. package/dist/auth/clerk.js +45 -9
  15. package/dist/auth/downstream-oauth.d.ts +0 -1
  16. package/dist/auth/downstream-oauth.js +0 -1
  17. package/dist/call-admission.d.ts +0 -1
  18. package/dist/call-admission.js +0 -1
  19. package/dist/catalog-drift.d.ts +99 -0
  20. package/dist/catalog-drift.js +251 -0
  21. package/dist/catalog-fingerprint.d.ts +0 -1
  22. package/dist/catalog-fingerprint.js +0 -1
  23. package/dist/catalog-limits.d.ts +0 -1
  24. package/dist/catalog-limits.js +0 -1
  25. package/dist/catalog-service.d.ts +0 -1
  26. package/dist/catalog-service.js +41 -5
  27. package/dist/catalog.d.ts +11 -1
  28. package/dist/catalog.js +20 -1
  29. package/dist/concurrency.d.ts +0 -1
  30. package/dist/concurrency.js +0 -1
  31. package/dist/connector-scope.d.ts +0 -1
  32. package/dist/connector-scope.js +0 -1
  33. package/dist/connectors/api.d.ts +29 -21
  34. package/dist/connectors/api.js +45 -19
  35. package/dist/connectors/guarded-fetch.d.ts +106 -0
  36. package/dist/connectors/guarded-fetch.js +289 -0
  37. package/dist/connectors/remote-mcp.d.ts +0 -1
  38. package/dist/connectors/remote-mcp.js +0 -1
  39. package/dist/credentials.d.ts +0 -1
  40. package/dist/credentials.js +0 -1
  41. package/dist/errors.d.ts +30 -3
  42. package/dist/errors.js +1 -1
  43. package/dist/execute.d.ts +0 -1
  44. package/dist/execute.js +0 -1
  45. package/dist/executor-admission.d.ts +11 -1
  46. package/dist/executor-admission.js +28 -1
  47. package/dist/executor-result.d.ts +0 -1
  48. package/dist/executor-result.js +0 -1
  49. package/dist/executors/quickjs-child.d.ts +0 -1
  50. package/dist/executors/quickjs-child.js +0 -1
  51. package/dist/executors/quickjs-protocol.d.ts +0 -1
  52. package/dist/executors/quickjs-protocol.js +0 -1
  53. package/dist/executors/quickjs-runtime.d.ts +0 -1
  54. package/dist/executors/quickjs-runtime.js +0 -1
  55. package/dist/executors/quickjs.d.ts +0 -1
  56. package/dist/executors/quickjs.js +2 -1
  57. package/dist/favicon.d.ts +0 -1
  58. package/dist/favicon.js +0 -1
  59. package/dist/index.d.ts +2 -3
  60. package/dist/index.js +24 -7
  61. package/dist/invocation.d.ts +0 -1
  62. package/dist/invocation.js +0 -1
  63. package/dist/json-schema.d.ts +0 -1
  64. package/dist/json-schema.js +0 -1
  65. package/dist/mcp-result.d.ts +0 -1
  66. package/dist/mcp-result.js +0 -1
  67. package/dist/meta-tools.d.ts +0 -1
  68. package/dist/meta-tools.js +0 -1
  69. package/dist/node.d.ts +0 -1
  70. package/dist/node.js +0 -1
  71. package/dist/operator-ui/generated.d.ts +2 -3
  72. package/dist/operator-ui/generated.js +3 -4
  73. package/dist/operator-ui/model.d.ts +10 -1
  74. package/dist/operator-ui/model.js +0 -1
  75. package/dist/operator-ui/view.d.ts +167 -0
  76. package/dist/operator-ui/view.js +270 -0
  77. package/dist/providers/cloudflare.d.ts +0 -1
  78. package/dist/providers/cloudflare.js +261 -299
  79. package/dist/providers/linear.d.ts +26 -5
  80. package/dist/providers/linear.js +32 -61
  81. package/dist/providers/mixpanel.d.ts +35 -4
  82. package/dist/providers/mixpanel.js +53 -79
  83. package/dist/providers/notion.d.ts +0 -1
  84. package/dist/providers/notion.js +87 -42
  85. package/dist/providers/stripe.d.ts +13 -1
  86. package/dist/providers/stripe.js +31 -60
  87. package/dist/registry.d.ts +34 -2
  88. package/dist/registry.js +85 -5
  89. package/dist/retired-toolkits.d.ts +0 -1
  90. package/dist/retired-toolkits.js +0 -1
  91. package/dist/routes/access-tokens.d.ts +0 -1
  92. package/dist/routes/access-tokens.js +0 -1
  93. package/dist/routes/activity.d.ts +0 -1
  94. package/dist/routes/activity.js +0 -1
  95. package/dist/routes/credentials.d.ts +0 -1
  96. package/dist/routes/credentials.js +0 -1
  97. package/dist/routes/mcp.d.ts +0 -1
  98. package/dist/routes/mcp.js +0 -1
  99. package/dist/routes/oauth.d.ts +0 -1
  100. package/dist/routes/oauth.js +0 -1
  101. package/dist/routes/shared.d.ts +2 -1
  102. package/dist/routes/shared.js +0 -1
  103. package/dist/routes/ui.d.ts +0 -1
  104. package/dist/routes/ui.js +0 -1
  105. package/dist/server.d.ts +0 -1
  106. package/dist/server.js +11 -1
  107. package/dist/skills.d.ts +0 -1
  108. package/dist/skills.js +0 -1
  109. package/dist/storage/file.d.ts +0 -1
  110. package/dist/storage/file.js +0 -1
  111. package/dist/storage/memory.d.ts +0 -1
  112. package/dist/storage/memory.js +0 -1
  113. package/dist/timeout.d.ts +0 -1
  114. package/dist/timeout.js +0 -1
  115. package/dist/tool-safety.d.ts +0 -1
  116. package/dist/tool-safety.js +0 -1
  117. package/dist/types.d.ts +45 -1
  118. package/dist/types.js +0 -1
  119. package/dist/ui.d.ts +0 -1
  120. package/dist/ui.js +13 -137
  121. package/dist/validate.d.ts +13 -15
  122. package/dist/validate.js +24 -11
  123. package/dist/version.d.ts +1 -2
  124. package/dist/version.js +1 -2
  125. package/documentation/architecture.md +186 -5
  126. package/documentation/auth.md +11 -0
  127. package/documentation/call-admission.md +142 -6
  128. package/documentation/cloudflare.md +92 -10
  129. package/documentation/code-first-exploration.md +2 -2
  130. package/documentation/code-mode.md +14 -14
  131. package/documentation/connector-guides.md +172 -5
  132. package/documentation/connectors.md +89 -0
  133. package/documentation/linear.md +20 -2
  134. package/documentation/mcp-ui-design.md +2 -2
  135. package/documentation/meta-tools.md +7 -1
  136. package/documentation/mixpanel.md +42 -9
  137. package/documentation/notion.md +23 -2
  138. package/documentation/operations.md +325 -5
  139. package/documentation/operator-ui.md +120 -5
  140. package/documentation/provider-audit.md +160 -0
  141. package/documentation/provider-conventions.md +667 -0
  142. package/documentation/request-admission.md +167 -5
  143. package/documentation/rich-output-design.md +1 -2
  144. package/documentation/stripe.md +15 -0
  145. package/documentation/upgrading.md +454 -0
  146. package/ethos.md +14 -9
  147. package/examples/worker/README.md +102 -12
  148. package/examples/worker/src/index.ts +46 -1
  149. package/examples/worker/wrangler.jsonc +14 -1
  150. package/package.json +10 -6
  151. package/templates/node/.dockerignore +10 -0
  152. package/templates/node/.env.example +40 -3
  153. package/templates/node/AGENTS.md +16 -1
  154. package/templates/node/Dockerfile +49 -0
  155. package/templates/node/README.md +117 -2
  156. package/templates/node/docker-compose.yml +39 -0
  157. package/templates/node/package.json +1 -1
  158. package/templates/node/src/file-activity.ts +182 -0
  159. package/templates/node/src/index.ts +80 -4
  160. package/assets/connecta-clay-hero.png +0 -0
  161. package/dist/access-tokens.d.ts.map +0 -1
  162. package/dist/access-tokens.js.map +0 -1
  163. package/dist/activity.d.ts.map +0 -1
  164. package/dist/activity.js.map +0 -1
  165. package/dist/apps-shell.d.ts.map +0 -1
  166. package/dist/apps-shell.js.map +0 -1
  167. package/dist/auth/bearer.d.ts.map +0 -1
  168. package/dist/auth/bearer.js.map +0 -1
  169. package/dist/auth/clerk.d.ts.map +0 -1
  170. package/dist/auth/clerk.js.map +0 -1
  171. package/dist/auth/downstream-oauth.d.ts.map +0 -1
  172. package/dist/auth/downstream-oauth.js.map +0 -1
  173. package/dist/call-admission.d.ts.map +0 -1
  174. package/dist/call-admission.js.map +0 -1
  175. package/dist/catalog-fingerprint.d.ts.map +0 -1
  176. package/dist/catalog-fingerprint.js.map +0 -1
  177. package/dist/catalog-limits.d.ts.map +0 -1
  178. package/dist/catalog-limits.js.map +0 -1
  179. package/dist/catalog-service.d.ts.map +0 -1
  180. package/dist/catalog-service.js.map +0 -1
  181. package/dist/catalog.d.ts.map +0 -1
  182. package/dist/catalog.js.map +0 -1
  183. package/dist/concurrency.d.ts.map +0 -1
  184. package/dist/concurrency.js.map +0 -1
  185. package/dist/connector-scope.d.ts.map +0 -1
  186. package/dist/connector-scope.js.map +0 -1
  187. package/dist/connectors/api.d.ts.map +0 -1
  188. package/dist/connectors/api.js.map +0 -1
  189. package/dist/connectors/remote-mcp.d.ts.map +0 -1
  190. package/dist/connectors/remote-mcp.js.map +0 -1
  191. package/dist/credentials.d.ts.map +0 -1
  192. package/dist/credentials.js.map +0 -1
  193. package/dist/errors.d.ts.map +0 -1
  194. package/dist/errors.js.map +0 -1
  195. package/dist/execute.d.ts.map +0 -1
  196. package/dist/execute.js.map +0 -1
  197. package/dist/executor-admission.d.ts.map +0 -1
  198. package/dist/executor-admission.js.map +0 -1
  199. package/dist/executor-result.d.ts.map +0 -1
  200. package/dist/executor-result.js.map +0 -1
  201. package/dist/executors/quickjs-child.d.ts.map +0 -1
  202. package/dist/executors/quickjs-child.js.map +0 -1
  203. package/dist/executors/quickjs-protocol.d.ts.map +0 -1
  204. package/dist/executors/quickjs-protocol.js.map +0 -1
  205. package/dist/executors/quickjs-runtime.d.ts.map +0 -1
  206. package/dist/executors/quickjs-runtime.js.map +0 -1
  207. package/dist/executors/quickjs.d.ts.map +0 -1
  208. package/dist/executors/quickjs.js.map +0 -1
  209. package/dist/favicon.d.ts.map +0 -1
  210. package/dist/favicon.js.map +0 -1
  211. package/dist/index.d.ts.map +0 -1
  212. package/dist/index.js.map +0 -1
  213. package/dist/invocation.d.ts.map +0 -1
  214. package/dist/invocation.js.map +0 -1
  215. package/dist/json-schema.d.ts.map +0 -1
  216. package/dist/json-schema.js.map +0 -1
  217. package/dist/mcp-result.d.ts.map +0 -1
  218. package/dist/mcp-result.js.map +0 -1
  219. package/dist/meta-tools.d.ts.map +0 -1
  220. package/dist/meta-tools.js.map +0 -1
  221. package/dist/node.d.ts.map +0 -1
  222. package/dist/node.js.map +0 -1
  223. package/dist/operator-ui/generated.d.ts.map +0 -1
  224. package/dist/operator-ui/generated.js.map +0 -1
  225. package/dist/operator-ui/model.d.ts.map +0 -1
  226. package/dist/operator-ui/model.js.map +0 -1
  227. package/dist/providers/cloudflare.d.ts.map +0 -1
  228. package/dist/providers/cloudflare.js.map +0 -1
  229. package/dist/providers/linear.d.ts.map +0 -1
  230. package/dist/providers/linear.js.map +0 -1
  231. package/dist/providers/mixpanel.d.ts.map +0 -1
  232. package/dist/providers/mixpanel.js.map +0 -1
  233. package/dist/providers/notion.d.ts.map +0 -1
  234. package/dist/providers/notion.js.map +0 -1
  235. package/dist/providers/stripe.d.ts.map +0 -1
  236. package/dist/providers/stripe.js.map +0 -1
  237. package/dist/registry.d.ts.map +0 -1
  238. package/dist/registry.js.map +0 -1
  239. package/dist/retired-toolkits.d.ts.map +0 -1
  240. package/dist/retired-toolkits.js.map +0 -1
  241. package/dist/routes/access-tokens.d.ts.map +0 -1
  242. package/dist/routes/access-tokens.js.map +0 -1
  243. package/dist/routes/activity.d.ts.map +0 -1
  244. package/dist/routes/activity.js.map +0 -1
  245. package/dist/routes/credentials.d.ts.map +0 -1
  246. package/dist/routes/credentials.js.map +0 -1
  247. package/dist/routes/mcp.d.ts.map +0 -1
  248. package/dist/routes/mcp.js.map +0 -1
  249. package/dist/routes/oauth.d.ts.map +0 -1
  250. package/dist/routes/oauth.js.map +0 -1
  251. package/dist/routes/shared.d.ts.map +0 -1
  252. package/dist/routes/shared.js.map +0 -1
  253. package/dist/routes/ui.d.ts.map +0 -1
  254. package/dist/routes/ui.js.map +0 -1
  255. package/dist/server.d.ts.map +0 -1
  256. package/dist/server.js.map +0 -1
  257. package/dist/skills.d.ts.map +0 -1
  258. package/dist/skills.js.map +0 -1
  259. package/dist/storage/file.d.ts.map +0 -1
  260. package/dist/storage/file.js.map +0 -1
  261. package/dist/storage/memory.d.ts.map +0 -1
  262. package/dist/storage/memory.js.map +0 -1
  263. package/dist/timeout.d.ts.map +0 -1
  264. package/dist/timeout.js.map +0 -1
  265. package/dist/tool-safety.d.ts.map +0 -1
  266. package/dist/tool-safety.js.map +0 -1
  267. package/dist/types.d.ts.map +0 -1
  268. package/dist/types.js.map +0 -1
  269. package/dist/ui.d.ts.map +0 -1
  270. package/dist/ui.js.map +0 -1
  271. package/dist/validate.d.ts.map +0 -1
  272. package/dist/validate.js.map +0 -1
  273. package/dist/version.d.ts.map +0 -1
  274. package/dist/version.js.map +0 -1
  275. package/examples/node/README.md +0 -52
  276. package/examples/node/src/index.ts +0 -71
  277. package/src/access-tokens.ts +0 -289
  278. package/src/activity.ts +0 -234
  279. package/src/apps-shell.ts +0 -370
  280. package/src/auth/bearer.ts +0 -61
  281. package/src/auth/clerk.ts +0 -553
  282. package/src/auth/downstream-oauth.ts +0 -575
  283. package/src/call-admission.ts +0 -521
  284. package/src/catalog-fingerprint.ts +0 -52
  285. package/src/catalog-limits.ts +0 -11
  286. package/src/catalog-service.ts +0 -1153
  287. package/src/catalog.ts +0 -790
  288. package/src/concurrency.ts +0 -39
  289. package/src/connector-scope.ts +0 -66
  290. package/src/connectors/api.ts +0 -159
  291. package/src/connectors/remote-mcp.ts +0 -1090
  292. package/src/credentials.ts +0 -428
  293. package/src/errors.ts +0 -364
  294. package/src/execute.ts +0 -1406
  295. package/src/executor-admission.ts +0 -328
  296. package/src/executor-result.ts +0 -107
  297. package/src/executors/quickjs-child.ts +0 -172
  298. package/src/executors/quickjs-protocol.ts +0 -99
  299. package/src/executors/quickjs-runtime.ts +0 -470
  300. package/src/executors/quickjs.ts +0 -776
  301. package/src/favicon.ts +0 -53
  302. package/src/index.ts +0 -666
  303. package/src/invocation.ts +0 -655
  304. package/src/json-schema.ts +0 -11
  305. package/src/mcp-result.ts +0 -43
  306. package/src/meta-tools.ts +0 -1607
  307. package/src/node.ts +0 -206
  308. package/src/operator-ui/browser.css +0 -423
  309. package/src/operator-ui/browser.ts +0 -1281
  310. package/src/operator-ui/generated.ts +0 -4
  311. package/src/operator-ui/model.ts +0 -111
  312. package/src/providers/cloudflare.ts +0 -3999
  313. package/src/providers/linear.ts +0 -302
  314. package/src/providers/mixpanel.ts +0 -229
  315. package/src/providers/notion.ts +0 -1879
  316. package/src/providers/stripe.ts +0 -307
  317. package/src/registry.ts +0 -1085
  318. package/src/retired-toolkits.ts +0 -14
  319. package/src/routes/access-tokens.ts +0 -115
  320. package/src/routes/activity.ts +0 -215
  321. package/src/routes/credentials.ts +0 -254
  322. package/src/routes/mcp.ts +0 -429
  323. package/src/routes/oauth.ts +0 -322
  324. package/src/routes/shared.ts +0 -247
  325. package/src/routes/ui.ts +0 -152
  326. package/src/server.ts +0 -195
  327. package/src/skills.ts +0 -271
  328. package/src/storage/file.ts +0 -123
  329. package/src/storage/memory.ts +0 -44
  330. package/src/timeout.ts +0 -84
  331. package/src/tool-safety.ts +0 -15
  332. package/src/types.ts +0 -523
  333. package/src/ui.ts +0 -744
  334. package/src/validate.ts +0 -387
  335. package/src/version.ts +0 -7
@@ -1,428 +0,0 @@
1
- import type {
2
- Connector,
3
- ConnectorCredentialConfig,
4
- ConnectorCredentialValues,
5
- KVStorage,
6
- } from "./types.js";
7
-
8
- const KEY_BYTES = 32;
9
- const IV_BYTES = 12;
10
- const MAX_CREDENTIAL_BYTES = 16_384;
11
- const encoder = new TextEncoder();
12
- const decoder = new TextDecoder();
13
-
14
- interface Envelope {
15
- version: 1;
16
- algorithm: "AES-GCM";
17
- iv: string;
18
- ciphertext: string;
19
- }
20
-
21
- interface CredentialPlaintext {
22
- values: ConnectorCredentialValues;
23
- updatedAt: string;
24
- updatedBy: string;
25
- }
26
-
27
- interface CredentialFieldMetadata {
28
- configured: true;
29
- /** Only emitted when the value is long enough that four chars don't leak much. */
30
- lastFour?: string;
31
- updatedAt: string;
32
- }
33
-
34
- export interface CredentialMetadata {
35
- configured: true;
36
- /** Backward-compatible metadata for the reserved single credential field. */
37
- lastFour?: string;
38
- updatedAt: string;
39
- /** Per-field masked metadata for named multi-value credentials. */
40
- fields?: Record<string, CredentialFieldMetadata>;
41
- }
42
-
43
- /** Which hook a testable credential is checked with. */
44
- type CredentialTestMode = "single" | "multiple";
45
-
46
- /** Operator-safe explanation shared by every surface that detects shape drift. */
47
- export const STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR =
48
- "Stored credential fields do not match this connector's current declaration. Replace the credential before using or testing this connector.";
49
-
50
- export type StoredCredentialShape =
51
- | { state: "missing" }
52
- | {
53
- state: "valid";
54
- mode: CredentialTestMode;
55
- /**
56
- * Stored keys the connector no longer declares, sorted. Harmless — the
57
- * credential works — but worth telling an operator about, since nothing
58
- * else in `/ui` can show a field the declaration has stopped naming.
59
- */
60
- undeclared: string[];
61
- }
62
- | {
63
- state: "mismatch";
64
- mode: CredentialTestMode;
65
- message: typeof STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR;
66
- };
67
-
68
- /**
69
- * Compare a connector's current declaration with the keys in its stored
70
- * credential. Values are deliberately ignored: callers may pass decrypted
71
- * values or `/credentials`' masked field metadata and get the same answer.
72
- *
73
- * The test is CONTAINMENT, not equality: the stored key set is compatible when
74
- * it holds every field currently declared — the reserved `value` key for a
75
- * single-value declaration, every declared name for a named one. Anything the
76
- * declaration asks for and the vault does not have is `mismatch`, which is
77
- * precisely what a renamed field, a newly added field, or a swap between the
78
- * two shapes produces. The swap needs no special case: `value` is never one of
79
- * the declared names, so single→named and named→single each leave the declared
80
- * side unsatisfied. An empty stored map (reachable through a hand-written
81
- * plaintext) satisfies nothing and is a mismatch too.
82
- *
83
- * Extra keys are NOT drift. They are what *dropping* a field leaves behind, and
84
- * every accessor a connector actually uses — `ctx.credential.get("apiKey")`,
85
- * `getAll().apiKey` — keeps returning the right secret across that redeploy.
86
- * Calling it drift would order an operator to re-enter a working secret that
87
- * many providers will not reissue in readable form. The leftovers come back as
88
- * `undeclared` instead, for a surface to mention without blocking anything.
89
- */
90
- export function storedCredentialShape(
91
- config: ConnectorCredentialConfig,
92
- stored: Readonly<Record<string, unknown>> | null,
93
- ): StoredCredentialShape {
94
- if (!stored) return { state: "missing" };
95
- const mode: CredentialTestMode = config.fields?.length
96
- ? "multiple"
97
- : "single";
98
- const declared = new Set(
99
- mode === "multiple" ? config.fields!.map((field) => field.name) : ["value"],
100
- );
101
- const actual = Object.keys(stored);
102
- const present = new Set(actual);
103
- for (const field of declared) {
104
- if (!present.has(field)) {
105
- return {
106
- state: "mismatch",
107
- mode,
108
- message: STORED_CREDENTIAL_SHAPE_MISMATCH_ERROR,
109
- };
110
- }
111
- }
112
- return {
113
- state: "valid",
114
- mode,
115
- undeclared: actual.filter((field) => !declared.has(field)).sort(),
116
- };
117
- }
118
-
119
- /** How many leftover field names an advisory names before it summarizes. */
120
- const UNDECLARED_SAMPLE = 5;
121
-
122
- /**
123
- * The one sentence describing leftover stored fields, so every surface words it
124
- * the same way. Names only — the values stay in the vault, and a field name from
125
- * a previous declaration is not a secret. Deliberately reassuring: nothing is
126
- * broken, and the only thing an operator gains by acting is that a connector
127
- * iterating `getAll()` stops seeing a field its code no longer knows about.
128
- */
129
- export function describeUndeclaredCredentialFields(fields: string[]): string {
130
- const shown = fields.slice(0, UNDECLARED_SAMPLE);
131
- const rest = fields.length - shown.length;
132
- const named = shown.join(", ") + (rest > 0 ? `, and ${rest} more` : "");
133
- return fields.length === 1
134
- ? `Stored credential also holds a field this connector no longer declares (${named}). It keeps working; replace the credential to drop it.`
135
- : `Stored credential also holds fields this connector no longer declares (${named}). It keeps working; replace the credential to drop them.`;
136
- }
137
-
138
- /** A declared credential shape whose only test hook cannot test it. */
139
- export interface CredentialTestMismatch {
140
- /** The shape the connector declared. */
141
- shape: CredentialTestMode;
142
- /** The hook it implements, which that shape cannot use. */
143
- hook: "testCredential" | "testCredentials";
144
- }
145
-
146
- export interface CredentialTestRule {
147
- /** The hook to call, or null when this credential cannot be tested at all. */
148
- mode: CredentialTestMode | null;
149
- /** Set only when the sole implemented hook is the one the shape cannot use. */
150
- mismatch?: CredentialTestMismatch;
151
- }
152
-
153
- /**
154
- * The one rule deciding whether a connector's credential can be tested — read
155
- * by /credentials' `testable` flag, by `POST /ui/credentials/<id>/test` when
156
- * it picks a hook, and by the construction-time mismatch warning, so those
157
- * three cannot drift apart.
158
- *
159
- * The declared credential *shape* selects the hook: named `credential.fields`
160
- * are tested as a set by `testCredentials`, a single-value `credential` by
161
- * `testCredential` on the vault's reserved `value` field. The other hook is
162
- * never substituted — it would be handed a shape the connector never declared —
163
- * so a connector implementing only the mismatched hook is not testable, and
164
- * says so at construction rather than under an operator's click.
165
- */
166
- export function credentialTestRule(
167
- connector: Pick<
168
- Connector,
169
- "credential" | "testCredential" | "testCredentials"
170
- >,
171
- ): CredentialTestRule {
172
- if (!connector.credential) return { mode: null };
173
- if (connector.credential.fields?.length) {
174
- if (connector.testCredentials) return { mode: "multiple" };
175
- return connector.testCredential
176
- ? { mode: null, mismatch: { shape: "multiple", hook: "testCredential" } }
177
- : { mode: null };
178
- }
179
- if (connector.testCredential) return { mode: "single" };
180
- return connector.testCredentials
181
- ? { mode: null, mismatch: { shape: "single", hook: "testCredentials" } }
182
- : { mode: null };
183
- }
184
-
185
- /**
186
- * One clause naming a mismatch, shared by the startup warning and the test
187
- * route's 400 so an operator reads the same explanation in both places.
188
- */
189
- export function describeCredentialTestMismatch(
190
- mismatch: CredentialTestMismatch,
191
- ): string {
192
- return mismatch.shape === "multiple"
193
- ? "it declares named credential fields, which only " +
194
- "`testCredentials(values, ctx)` can test, but implements " +
195
- "`testCredential`"
196
- : "it declares a single-value credential, which only " +
197
- "`testCredential(value, ctx)` can test, but implements " +
198
- "`testCredentials`";
199
- }
200
-
201
- function storageKey(connectorId: string): string {
202
- return `conn:${connectorId}:credential:v1`;
203
- }
204
-
205
- function bytesToBase64(bytes: Uint8Array): string {
206
- let binary = "";
207
- for (const byte of bytes) binary += String.fromCharCode(byte);
208
- return btoa(binary);
209
- }
210
-
211
- function base64ToBytes(value: string): Uint8Array {
212
- let binary: string;
213
- try {
214
- binary = atob(value);
215
- } catch {
216
- throw new Error(
217
- "credentials.encryptionKey must be a base64-encoded 32-byte key",
218
- );
219
- }
220
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
221
- }
222
-
223
- function parseEnvelope(raw: string): Envelope {
224
- try {
225
- const parsed = JSON.parse(raw) as Partial<Envelope>;
226
- if (
227
- parsed.version !== 1 ||
228
- parsed.algorithm !== "AES-GCM" ||
229
- typeof parsed.iv !== "string" ||
230
- typeof parsed.ciphertext !== "string"
231
- ) {
232
- throw new Error("invalid envelope");
233
- }
234
- return parsed as Envelope;
235
- } catch {
236
- throw new Error("Stored credential is invalid or corrupted");
237
- }
238
- }
239
-
240
- function parsePlaintext(raw: string): CredentialPlaintext {
241
- try {
242
- const parsed = JSON.parse(raw) as Partial<CredentialPlaintext> & {
243
- value?: unknown;
244
- };
245
- const values =
246
- typeof parsed.value === "string"
247
- ? { value: parsed.value }
248
- : parsed.values;
249
- if (
250
- !values ||
251
- typeof values !== "object" ||
252
- Array.isArray(values) ||
253
- !Object.entries(values).every(
254
- ([field, value]) =>
255
- /^[A-Za-z][A-Za-z0-9_-]*$/.test(field) &&
256
- typeof value === "string",
257
- ) ||
258
- typeof parsed.updatedAt !== "string" ||
259
- typeof parsed.updatedBy !== "string"
260
- ) {
261
- throw new Error("invalid plaintext");
262
- }
263
- return {
264
- values: { ...values },
265
- updatedAt: parsed.updatedAt,
266
- updatedBy: parsed.updatedBy,
267
- };
268
- } catch {
269
- throw new Error("Stored credential is invalid or corrupted");
270
- }
271
- }
272
-
273
- function validateValues(
274
- values: ConnectorCredentialValues,
275
- ): ConnectorCredentialValues {
276
- const entries = Object.entries(values);
277
- if (entries.length === 0) throw new Error("Credential cannot be empty");
278
- const normalized: ConnectorCredentialValues = {};
279
- for (const [field, value] of entries) {
280
- if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(field)) {
281
- throw new Error(`Invalid credential field "${field}"`);
282
- }
283
- if (typeof value !== "string" || !value.trim()) {
284
- throw new Error(`Credential field "${field}" cannot be empty`);
285
- }
286
- normalized[field] = value;
287
- }
288
- if (
289
- encoder.encode(JSON.stringify(normalized)).byteLength >
290
- MAX_CREDENTIAL_BYTES
291
- ) {
292
- throw new Error(`Credential cannot exceed ${MAX_CREDENTIAL_BYTES} bytes`);
293
- }
294
- return normalized;
295
- }
296
-
297
- /**
298
- * Encrypted, connector-scoped credential vault over the deployment's existing
299
- * KVStorage. Only ciphertext enters KV; the AES-GCM key remains an environment
300
- * secret outside the store.
301
- */
302
- export class CredentialVault {
303
- private readonly key: Promise<CryptoKey>;
304
-
305
- constructor(
306
- private readonly storage: KVStorage,
307
- encryptionKey: string,
308
- ) {
309
- const raw = base64ToBytes(encryptionKey.trim());
310
- if (raw.byteLength !== KEY_BYTES) {
311
- throw new Error(
312
- "credentials.encryptionKey must be a base64-encoded 32-byte key",
313
- );
314
- }
315
- this.key = crypto.subtle.importKey(
316
- "raw",
317
- raw,
318
- { name: "AES-GCM" },
319
- false,
320
- ["encrypt", "decrypt"],
321
- );
322
- }
323
-
324
- private additionalData(connectorId: string): Uint8Array {
325
- return encoder.encode(`connecta:credential:${connectorId}:v1`);
326
- }
327
-
328
- private async read(connectorId: string): Promise<CredentialPlaintext | null> {
329
- const raw = await this.storage.get(storageKey(connectorId));
330
- if (!raw) return null;
331
- const envelope = parseEnvelope(raw);
332
- try {
333
- const plaintext = await crypto.subtle.decrypt(
334
- {
335
- name: "AES-GCM",
336
- iv: base64ToBytes(envelope.iv),
337
- additionalData: this.additionalData(connectorId),
338
- },
339
- await this.key,
340
- base64ToBytes(envelope.ciphertext),
341
- );
342
- return parsePlaintext(decoder.decode(plaintext));
343
- } catch {
344
- throw new Error("Stored credential could not be decrypted");
345
- }
346
- }
347
-
348
- async get(connectorId: string, field = "value"): Promise<string | null> {
349
- return (await this.read(connectorId))?.values[field] ?? null;
350
- }
351
-
352
- async getAll(
353
- connectorId: string,
354
- ): Promise<ConnectorCredentialValues | null> {
355
- const credential = await this.read(connectorId);
356
- return credential ? { ...credential.values } : null;
357
- }
358
-
359
- async metadata(connectorId: string): Promise<CredentialMetadata | null> {
360
- const credential = await this.read(connectorId);
361
- if (!credential) return null;
362
- const fields = Object.fromEntries(
363
- Object.entries(credential.values).map(([field, value]) => [
364
- field,
365
- {
366
- configured: true as const,
367
- // Only reveal the tail on values comfortably longer than four chars;
368
- // for a short secret those four would be half of it.
369
- ...(value.length >= 12 ? { lastFour: value.slice(-4) } : {}),
370
- updatedAt: credential.updatedAt,
371
- },
372
- ]),
373
- );
374
- const single = fields.value;
375
- return {
376
- configured: true,
377
- ...(single?.lastFour ? { lastFour: single.lastFour } : {}),
378
- updatedAt: credential.updatedAt,
379
- fields,
380
- };
381
- }
382
-
383
- async set(
384
- connectorId: string,
385
- value: string,
386
- updatedBy: string,
387
- ): Promise<CredentialMetadata> {
388
- // `await` (not a bare promise return) so a validation throw inside setAll
389
- // never sits handler-less for the thenable-adoption microtask — workerd
390
- // reports that gap as an unhandled rejection.
391
- return await this.setAll(connectorId, { value }, updatedBy);
392
- }
393
-
394
- async setAll(
395
- connectorId: string,
396
- values: ConnectorCredentialValues,
397
- updatedBy: string,
398
- ): Promise<CredentialMetadata> {
399
- const normalized = validateValues(values);
400
- const plaintext: CredentialPlaintext = {
401
- values: normalized,
402
- updatedAt: new Date().toISOString(),
403
- updatedBy,
404
- };
405
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
406
- const ciphertext = await crypto.subtle.encrypt(
407
- {
408
- name: "AES-GCM",
409
- iv,
410
- additionalData: this.additionalData(connectorId),
411
- },
412
- await this.key,
413
- encoder.encode(JSON.stringify(plaintext)),
414
- );
415
- const envelope: Envelope = {
416
- version: 1,
417
- algorithm: "AES-GCM",
418
- iv: bytesToBase64(iv),
419
- ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
420
- };
421
- await this.storage.set(storageKey(connectorId), JSON.stringify(envelope));
422
- return (await this.metadata(connectorId))!;
423
- }
424
-
425
- async delete(connectorId: string): Promise<void> {
426
- await this.storage.delete(storageKey(connectorId));
427
- }
428
- }