@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,575 +0,0 @@
1
- import type {
2
- OAuthClientInformationContext,
3
- OAuthClientInformationMixed,
4
- OAuthClientMetadata,
5
- OAuthClientProvider,
6
- OAuthTokens,
7
- } from "@modelcontextprotocol/client";
8
- import type { KVStorage } from "../types.js";
9
-
10
- /** A 256-bit random opaque value, hex-encoded — used for the OAuth `state`. */
11
- function randomState(): string {
12
- const bytes = new Uint8Array(32);
13
- crypto.getRandomValues(bytes);
14
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
15
- }
16
-
17
- /** Length-safe, constant-time string compare (no early-exit on first mismatch). */
18
- function timingSafeEqual(a: string, b: string): boolean {
19
- if (a.length !== b.length) return false;
20
- let diff = 0;
21
- for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
22
- return diff === 0;
23
- }
24
-
25
- const LEGACY_GENERATION = "legacy";
26
- const ACTIVE_GENERATION_PREFIX = "v2:";
27
- const RESETTING_GENERATION_PREFIX = "reset:";
28
- const DISCONNECTED_GENERATION_PREFIX = "disconnected:";
29
- const STORED_VALUE_VERSION = 2;
30
- const OAUTH_VALUE_KEYS = [
31
- "oauth:client",
32
- "oauth:tokens",
33
- "oauth:pending",
34
- "oauth:verifier",
35
- "oauth:state",
36
- ] as const;
37
- const MAX_CLEANUP_BACKLOG = 1_000;
38
-
39
- interface StoredOAuthValue<T> {
40
- connectaOAuthVersion: typeof STORED_VALUE_VERSION;
41
- generation: string;
42
- issuer?: string;
43
- value: T;
44
- }
45
-
46
- interface LegacyStoredOAuthValue<T> {
47
- connectaOAuthVersion: 1;
48
- generation: string;
49
- value: T;
50
- }
51
-
52
- function storedOAuthValue<T>(
53
- value: unknown,
54
- ): value is StoredOAuthValue<T> {
55
- if (!value || typeof value !== "object") return false;
56
- const candidate = value as Partial<StoredOAuthValue<T>>;
57
- return (
58
- candidate.connectaOAuthVersion === STORED_VALUE_VERSION &&
59
- typeof candidate.generation === "string" &&
60
- (candidate.issuer === undefined || typeof candidate.issuer === "string") &&
61
- "value" in candidate
62
- );
63
- }
64
-
65
- function legacyStoredOAuthValue<T>(
66
- value: unknown,
67
- ): value is LegacyStoredOAuthValue<T> {
68
- if (!value || typeof value !== "object") return false;
69
- const candidate = value as Partial<LegacyStoredOAuthValue<T>>;
70
- return (
71
- candidate.connectaOAuthVersion === 1 &&
72
- typeof candidate.generation === "string" &&
73
- "value" in candidate
74
- );
75
- }
76
-
77
- function isModernGeneration(generation: string): boolean {
78
- return (
79
- generation.startsWith(ACTIVE_GENERATION_PREFIX) ||
80
- generation.startsWith(RESETTING_GENERATION_PREFIX) ||
81
- generation.startsWith(DISCONNECTED_GENERATION_PREFIX)
82
- );
83
- }
84
-
85
- /**
86
- * Physical key for an OAuth value in one authorization epoch. Legacy values
87
- * keep their historical names so upgrades can read an existing grant. Modern
88
- * values get an epoch-specific namespace: a stale write or delete can then
89
- * affect only its own flow, even if it lands after a replacement flow.
90
- */
91
- export function oauthValueStorageKey(
92
- key: string,
93
- generation: string | null,
94
- ): string {
95
- return generation !== null && isModernGeneration(generation)
96
- ? `${key}:epoch:${generation}`
97
- : key;
98
- }
99
-
100
- function cleanupBacklogKey(generation: string): string {
101
- return `oauth:cleanup:${encodeURIComponent(generation)}`;
102
- }
103
-
104
- /**
105
- * OAuthClientProvider implemented over KVStorage for a single downstream
106
- * connector. Keys live in the connector's namespace as `oauth:<field>`.
107
- *
108
- * Headless twist: redirectToAuthorization() cannot navigate a user agent, so it
109
- * STORES the authorization URL; the registry surfaces it as status
110
- * "auth_required" and the operator opens it. The /oauth/callback/<id> route then
111
- * drives transport.finishAuth(code).
112
- */
113
- export class KvOAuthProvider implements OAuthClientProvider {
114
- /**
115
- * The reset generation this provider's flow started under. Every OAuth value
116
- * it writes carries this epoch, so a late write can land after a reset without
117
- * becoming readable under the new generation.
118
- */
119
- private capturedGeneration: string | null = null;
120
-
121
- constructor(
122
- private readonly connectorId: string,
123
- private readonly storage: KVStorage,
124
- private readonly redirectUri: string,
125
- ) {}
126
-
127
- /**
128
- * Stamp the force-reauth generation the current connect flow started under.
129
- * Called by the connector once per connect, before c.connect(). The callback
130
- * path captures the generation stored beside its verified state instead.
131
- */
132
- captureGeneration(gen: string): void {
133
- this.capturedGeneration = gen;
134
- }
135
-
136
- /**
137
- * The epoch this provider writes under. Direct unit/custom use lazily captures
138
- * the current generation; connector-driven connect and callback paths stamp it
139
- * explicitly before the SDK can write.
140
- */
141
- private async writeGeneration(): Promise<string> {
142
- this.capturedGeneration ??= await this.generation();
143
- return this.capturedGeneration;
144
- }
145
-
146
- /**
147
- * Store a value in the flow's physical epoch namespace. The pre-write check
148
- * avoids needless stale residue. The namespaced key closes the remaining
149
- * check-then-write race: a late old write cannot overwrite a replacement
150
- * flow's value because the two writes have different physical keys.
151
- */
152
- private async writeValue<T>(
153
- key: string,
154
- value: T,
155
- serializeLegacy: (value: T) => string,
156
- issuer?: string,
157
- ): Promise<void> {
158
- const generation = await this.writeGeneration();
159
- if (
160
- generation.startsWith(RESETTING_GENERATION_PREFIX) ||
161
- generation.startsWith(DISCONNECTED_GENERATION_PREFIX) ||
162
- (await this.generation()) !== generation
163
- ) {
164
- return;
165
- }
166
- const stored: StoredOAuthValue<T> = {
167
- connectaOAuthVersion: STORED_VALUE_VERSION,
168
- generation,
169
- ...(issuer !== undefined ? { issuer } : {}),
170
- value,
171
- };
172
- const physicalKey = oauthValueStorageKey(key, generation);
173
- await this.storage.set(
174
- physicalKey,
175
- isModernGeneration(generation) || issuer !== undefined
176
- ? JSON.stringify(stored)
177
- : serializeLegacy(value),
178
- );
179
- // If reset landed after the pre-write check and completed its cleanup
180
- // before this set, remove the now-unreachable residue ourselves. The epoch
181
- // key already provides correctness; this second check is physical hygiene.
182
- const current = await this.generation();
183
- if (current !== generation) {
184
- try {
185
- await this.storage.delete(physicalKey);
186
- } catch {
187
- // Make a transient cleanup failure retryable by the next force reset.
188
- // This is still best-effort if storage cannot accept the backlog write.
189
- try {
190
- await this.rememberRetiredGeneration(current, generation);
191
- } catch {
192
- // The old namespace is already unreadable; storage availability is
193
- // the remaining physical-hygiene boundary.
194
- }
195
- }
196
- }
197
- }
198
-
199
- /**
200
- * Read a value only when it belongs to the active generation. Plain legacy
201
- * values remain readable until the first v2 reset, so upgrades do not discard
202
- * an existing grant; once a modern epoch exists, untagged residue fails
203
- * closed.
204
- */
205
- private async readValue<T>(
206
- key: string,
207
- parseLegacy: (raw: string) => T,
208
- ): Promise<
209
- { value: T; generation: string; issuer?: string } | undefined
210
- > {
211
- const generation = await this.generation();
212
- const raw = await this.storage.get(
213
- oauthValueStorageKey(key, generation),
214
- );
215
- if (raw === null) return undefined;
216
- if (
217
- generation.startsWith(RESETTING_GENERATION_PREFIX) ||
218
- generation.startsWith(DISCONNECTED_GENERATION_PREFIX)
219
- ) {
220
- return undefined;
221
- }
222
-
223
- let parsed: unknown;
224
- try {
225
- parsed = JSON.parse(raw);
226
- } catch {
227
- // Raw string state from a pre-envelope deployment is handled below.
228
- }
229
- if (storedOAuthValue<T>(parsed)) {
230
- return parsed.generation === generation
231
- ? {
232
- value: parsed.value,
233
- generation,
234
- ...(parsed.issuer !== undefined ? { issuer: parsed.issuer } : {}),
235
- }
236
- : undefined;
237
- }
238
- if (legacyStoredOAuthValue<T>(parsed)) {
239
- return parsed.generation === generation
240
- ? { value: parsed.value, generation }
241
- : undefined;
242
- }
243
- if (isModernGeneration(generation)) return undefined;
244
- return { value: parseLegacy(raw), generation };
245
- }
246
-
247
- /**
248
- * Read credentials only for the authorization server that issued them.
249
- *
250
- * Values written before issuer binding are bound on their first read with a
251
- * validated SDK issuer, preserving an existing grant across this upgrade.
252
- * A later issuer change is different: publish a new generation before
253
- * returning no credentials, so every isolate drops the old registration and
254
- * token set and the SDK starts authorization from scratch.
255
- */
256
- private async readIssuerBoundValue<T>(
257
- key: "oauth:client" | "oauth:tokens",
258
- parseLegacy: (raw: string) => T,
259
- serializeLegacy: (value: T) => string,
260
- ctx?: OAuthClientInformationContext,
261
- ): Promise<T | undefined> {
262
- const stored = await this.readValue(key, parseLegacy);
263
- if (!stored || !ctx) return stored?.value;
264
- if (stored.issuer === undefined) {
265
- await this.writeValue(key, stored.value, serializeLegacy, ctx.issuer);
266
- return stored.value;
267
- }
268
- if (stored.issuer === ctx.issuer) return stored.value;
269
-
270
- try {
271
- await this.resetAuthorization();
272
- } finally {
273
- // The issuer-aware flow may continue after the reset. Stamp any later
274
- // writes into the replacement epoch; the connector's generation check
275
- // still discards this connection attempt before caching it.
276
- this.captureGeneration(await this.generation());
277
- }
278
- return undefined;
279
- }
280
-
281
- /** Attempt every key deletion, then report the first backend failure. */
282
- private async deleteAll(keys: readonly string[]): Promise<void> {
283
- let firstError: unknown;
284
- for (const key of keys) {
285
- try {
286
- await this.storage.delete(key);
287
- } catch (error) {
288
- firstError ??= error;
289
- }
290
- }
291
- if (firstError) throw firstError;
292
- }
293
-
294
- private async cleanupBacklog(generation: string): Promise<string[]> {
295
- const raw = await this.storage.get(cleanupBacklogKey(generation));
296
- if (raw === null) return [];
297
- const parsed: unknown = JSON.parse(raw);
298
- if (
299
- !Array.isArray(parsed) ||
300
- parsed.length > MAX_CLEANUP_BACKLOG ||
301
- !parsed.every((value) => typeof value === "string")
302
- ) {
303
- throw new Error(
304
- `Invalid OAuth cleanup backlog for "${this.connectorId}"`,
305
- );
306
- }
307
- return [...new Set(parsed)];
308
- }
309
-
310
- private async rememberRetiredGeneration(
311
- active: string,
312
- retired: string,
313
- ): Promise<void> {
314
- const backlog = await this.cleanupBacklog(active);
315
- if (backlog.includes(retired)) return;
316
- if (backlog.length >= MAX_CLEANUP_BACKLOG) {
317
- throw new Error(
318
- `OAuth cleanup backlog for "${this.connectorId}" is full`,
319
- );
320
- }
321
- await this.storage.set(
322
- cleanupBacklogKey(active),
323
- JSON.stringify([...backlog, retired]),
324
- );
325
- }
326
-
327
- private valueKeysForGeneration(generation: string): string[] {
328
- return OAUTH_VALUE_KEYS.map((key) =>
329
- oauthValueStorageKey(key, generation),
330
- );
331
- }
332
-
333
- get redirectUrl(): string {
334
- return this.redirectUri;
335
- }
336
-
337
- get clientMetadata(): OAuthClientMetadata {
338
- return {
339
- redirect_uris: [this.redirectUri],
340
- client_name: "connecta",
341
- grant_types: ["authorization_code", "refresh_token"],
342
- response_types: ["code"],
343
- token_endpoint_auth_method: "none",
344
- };
345
- }
346
-
347
- async clientInformation(
348
- ctx?: OAuthClientInformationContext,
349
- ): Promise<OAuthClientInformationMixed | undefined> {
350
- return this.readIssuerBoundValue(
351
- "oauth:client",
352
- (raw) => JSON.parse(raw) as OAuthClientInformationMixed,
353
- (value) => JSON.stringify(value),
354
- ctx,
355
- );
356
- }
357
-
358
- async saveClientInformation(
359
- info: OAuthClientInformationMixed,
360
- ctx?: OAuthClientInformationContext,
361
- ): Promise<void> {
362
- await this.writeValue(
363
- "oauth:client",
364
- info,
365
- (value) => JSON.stringify(value),
366
- ctx?.issuer,
367
- );
368
- }
369
-
370
- async tokens(
371
- ctx?: OAuthClientInformationContext,
372
- ): Promise<OAuthTokens | undefined> {
373
- return this.readIssuerBoundValue(
374
- "oauth:tokens",
375
- (raw) => JSON.parse(raw) as OAuthTokens,
376
- (value) => JSON.stringify(value),
377
- ctx,
378
- );
379
- }
380
-
381
- async saveTokens(
382
- tokens: OAuthTokens,
383
- ctx?: OAuthClientInformationContext,
384
- ): Promise<void> {
385
- await this.writeValue(
386
- "oauth:tokens",
387
- tokens,
388
- (value) => JSON.stringify(value),
389
- ctx?.issuer,
390
- );
391
- }
392
-
393
- /**
394
- * OAuth `state`. The SDK calls this (when present) and appends the value to
395
- * the authorization URL. We generate a fresh random value and persist it so
396
- * the public /oauth/callback route can prove the callback belongs to a flow
397
- * WE started. Without it, anyone holding a pending authorization URL could
398
- * complete consent with their own account (login CSRF) — PKCE does not help,
399
- * since the verifier belongs to connecta, not the attacker. Cleared in
400
- * clearPending() once the flow completes.
401
- */
402
- async state(): Promise<string> {
403
- const value = randomState();
404
- await this.writeValue("oauth:state", value, (raw) => raw);
405
- return value;
406
- }
407
-
408
- /**
409
- * Constant-time check of a callback's `state` against the stored one-shot
410
- * value. Absent stored state or absent candidate → false (fail closed).
411
- */
412
- async verifyState(candidate: string | null): Promise<boolean> {
413
- const expected = await this.readValue("oauth:state", (raw) => raw);
414
- if (!expected || candidate === null) return false;
415
- const matches = timingSafeEqual(candidate, expected.value);
416
- if (matches) this.captureGeneration(expected.generation);
417
- return matches;
418
- }
419
-
420
- async saveCodeVerifier(verifier: string): Promise<void> {
421
- await this.writeValue("oauth:verifier", verifier, (raw) => raw);
422
- }
423
-
424
- async codeVerifier(): Promise<string> {
425
- const stored = await this.readValue("oauth:verifier", (raw) => raw);
426
- if (!stored) {
427
- throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
428
- }
429
- return stored.value;
430
- }
431
-
432
- async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
433
- await this.writeValue(
434
- "oauth:pending",
435
- authorizationUrl.toString(),
436
- (raw) => raw,
437
- );
438
- }
439
-
440
- /** The stored authorization URL, if a flow is pending. */
441
- async pendingAuthorizationUrl(): Promise<string | undefined> {
442
- return (
443
- await this.readValue("oauth:pending", (raw) => raw)
444
- )?.value;
445
- }
446
-
447
- /** Clear one-shot flow state after the callback completes. */
448
- async clearPending(): Promise<void> {
449
- const generation = await this.writeGeneration();
450
- await this.deleteAll([
451
- oauthValueStorageKey("oauth:pending", generation),
452
- oauthValueStorageKey("oauth:verifier", generation),
453
- oauthValueStorageKey("oauth:state", generation),
454
- ]);
455
- }
456
-
457
- /**
458
- * Force-reauth epoch shared across isolates through storage. Old numeric
459
- * generations remain valid strings for migration; new resets use unique
460
- * nonces, avoiding the lost-update race of read/increment/write.
461
- */
462
- async generation(): Promise<string> {
463
- return (await this.storage.get("oauth:generation")) ?? LEGACY_GENERATION;
464
- }
465
-
466
- /** True only after an operator disconnect, until an explicit authorization starts. */
467
- async operatorDisconnected(): Promise<boolean> {
468
- return this.isOperatorDisconnectedGeneration(await this.generation());
469
- }
470
-
471
- /** Interpret a generation already read by a connector without another KV lookup. */
472
- isOperatorDisconnectedGeneration(generation: string): boolean {
473
- return generation.startsWith(DISCONNECTED_GENERATION_PREFIX);
474
- }
475
-
476
- /** Publish a unique active epoch without a read/modify/write race. */
477
- async bumpGeneration(): Promise<string> {
478
- const next = `${ACTIVE_GENERATION_PREFIX}${crypto.randomUUID()}`;
479
- await this.storage.set("oauth:generation", next);
480
- return next;
481
- }
482
-
483
- /**
484
- * Fence every flow that could still write, then remove all durable and
485
- * one-shot authorization state. The generation is intentionally retained:
486
- * it is the epoch fence that tells another isolate not to resurrect
487
- * credentials it read before this reset.
488
- *
489
- * Once the fence is durable, attempt every deletion even if one fails. A
490
- * partial backend outage should not leave unrelated secrets behind merely
491
- * because an earlier key happened to be the first failed delete.
492
- */
493
- async resetAuthorization(operatorDisconnected = false): Promise<void> {
494
- const nonce = crypto.randomUUID();
495
- const previous = await this.generation();
496
- const inherited = await this.cleanupBacklog(previous);
497
- const active = `${
498
- operatorDisconnected
499
- ? DISCONNECTED_GENERATION_PREFIX
500
- : ACTIVE_GENERATION_PREFIX
501
- }${nonce}`;
502
- const retired = [...new Set([...inherited, previous])];
503
- if (retired.length > MAX_CLEANUP_BACKLOG) {
504
- throw new Error(
505
- `OAuth cleanup backlog for "${this.connectorId}" is full`,
506
- );
507
- }
508
- // Publish the complete inherited cleanup work under the prospective epoch
509
- // before making that epoch active. A crash or later retry can therefore
510
- // always recover the older namespaces without a storage prefix scan.
511
- await this.storage.set(
512
- cleanupBacklogKey(active),
513
- JSON.stringify(retired),
514
- );
515
- // This is the one authoritative transition. From this point onward every
516
- // old physical namespace is unreadable. There is deliberately no second
517
- // "finalize" write: concurrent resets therefore cannot overwrite a newer
518
- // reset's epoch after their cleanup finishes out of order.
519
- try {
520
- await this.storage.set("oauth:generation", active);
521
- } catch (error) {
522
- try {
523
- await this.storage.delete(cleanupBacklogKey(active));
524
- } catch {
525
- // Best-effort removal of a manifest for an epoch never activated.
526
- }
527
- throw error;
528
- }
529
-
530
- let firstError: unknown;
531
- for (const generation of retired) {
532
- try {
533
- await this.deleteAll(this.valueKeysForGeneration(generation));
534
- await this.storage.delete(cleanupBacklogKey(generation));
535
- } catch (error) {
536
- firstError ??= error;
537
- }
538
- }
539
- // Keep the active manifest immutable for the epoch's whole lifetime, even
540
- // after successful cleanup. A late old-epoch write can land after cleanup;
541
- // if its self-delete fails, the next reset must still inherit the complete
542
- // lineage without racing a manifest shrink/delete. The successor copies
543
- // this manifest before activation and then removes this retired copy.
544
- if (firstError) throw firstError;
545
- }
546
-
547
- async invalidateCredentials(
548
- scope: "all" | "client" | "tokens" | "verifier" | "discovery",
549
- ): Promise<void> {
550
- const generation = await this.writeGeneration();
551
- if (scope === "all") {
552
- await this.deleteAll([
553
- oauthValueStorageKey("oauth:client", generation),
554
- oauthValueStorageKey("oauth:tokens", generation),
555
- oauthValueStorageKey("oauth:verifier", generation),
556
- ]);
557
- return;
558
- }
559
- if (scope === "client") {
560
- await this.storage.delete(
561
- oauthValueStorageKey("oauth:client", generation),
562
- );
563
- }
564
- if (scope === "tokens") {
565
- await this.storage.delete(
566
- oauthValueStorageKey("oauth:tokens", generation),
567
- );
568
- }
569
- if (scope === "verifier") {
570
- await this.storage.delete(
571
- oauthValueStorageKey("oauth:verifier", generation),
572
- );
573
- }
574
- }
575
- }