@clinebot/core 0.0.21 → 0.0.23

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 (260) hide show
  1. package/dist/ClineCore.d.ts +110 -0
  2. package/dist/ClineCore.d.ts.map +1 -0
  3. package/dist/account/cline-account-service.d.ts +2 -1
  4. package/dist/account/cline-account-service.d.ts.map +1 -1
  5. package/dist/account/index.d.ts +1 -1
  6. package/dist/account/index.d.ts.map +1 -1
  7. package/dist/account/rpc.d.ts +3 -1
  8. package/dist/account/rpc.d.ts.map +1 -1
  9. package/dist/account/types.d.ts +3 -0
  10. package/dist/account/types.d.ts.map +1 -1
  11. package/dist/agents/plugin-loader.d.ts.map +1 -1
  12. package/dist/agents/plugin-sandbox-bootstrap.js +17 -17
  13. package/dist/auth/client.d.ts +1 -1
  14. package/dist/auth/client.d.ts.map +1 -1
  15. package/dist/auth/cline.d.ts +1 -1
  16. package/dist/auth/cline.d.ts.map +1 -1
  17. package/dist/auth/codex.d.ts +1 -1
  18. package/dist/auth/codex.d.ts.map +1 -1
  19. package/dist/auth/oca.d.ts +1 -1
  20. package/dist/auth/oca.d.ts.map +1 -1
  21. package/dist/auth/utils.d.ts +2 -2
  22. package/dist/auth/utils.d.ts.map +1 -1
  23. package/dist/index.d.ts +50 -5
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +949 -0
  26. package/dist/providers/local-provider-service.d.ts +4 -4
  27. package/dist/providers/local-provider-service.d.ts.map +1 -1
  28. package/dist/runtime/runtime-builder.d.ts +1 -0
  29. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  30. package/dist/runtime/session-runtime.d.ts +2 -1
  31. package/dist/runtime/session-runtime.d.ts.map +1 -1
  32. package/dist/runtime/team-runtime-registry.d.ts +13 -0
  33. package/dist/runtime/team-runtime-registry.d.ts.map +1 -0
  34. package/dist/session/default-session-manager.d.ts +2 -2
  35. package/dist/session/default-session-manager.d.ts.map +1 -1
  36. package/dist/session/rpc-runtime-ensure.d.ts +53 -0
  37. package/dist/session/rpc-runtime-ensure.d.ts.map +1 -0
  38. package/dist/session/session-config-builder.d.ts +2 -3
  39. package/dist/session/session-config-builder.d.ts.map +1 -1
  40. package/dist/session/session-host.d.ts +8 -18
  41. package/dist/session/session-host.d.ts.map +1 -1
  42. package/dist/session/session-manager.d.ts +1 -1
  43. package/dist/session/session-manager.d.ts.map +1 -1
  44. package/dist/session/session-manifest.d.ts +1 -2
  45. package/dist/session/session-manifest.d.ts.map +1 -1
  46. package/dist/session/unified-session-persistence-service.d.ts +2 -2
  47. package/dist/session/unified-session-persistence-service.d.ts.map +1 -1
  48. package/dist/session/utils/helpers.d.ts +1 -1
  49. package/dist/session/utils/helpers.d.ts.map +1 -1
  50. package/dist/session/utils/types.d.ts +1 -1
  51. package/dist/session/utils/types.d.ts.map +1 -1
  52. package/dist/storage/provider-settings-legacy-migration.d.ts.map +1 -1
  53. package/dist/telemetry/OpenTelemetryProvider.d.ts.map +1 -1
  54. package/dist/telemetry/distinct-id.d.ts +2 -0
  55. package/dist/telemetry/distinct-id.d.ts.map +1 -0
  56. package/dist/telemetry/{opentelemetry.d.ts → index.d.ts} +1 -1
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/index.js +28 -0
  59. package/dist/tools/constants.d.ts +1 -1
  60. package/dist/tools/constants.d.ts.map +1 -1
  61. package/dist/tools/definitions.d.ts +3 -3
  62. package/dist/tools/definitions.d.ts.map +1 -1
  63. package/dist/tools/executors/apply-patch.d.ts +1 -1
  64. package/dist/tools/executors/apply-patch.d.ts.map +1 -1
  65. package/dist/tools/executors/bash.d.ts +1 -1
  66. package/dist/tools/executors/bash.d.ts.map +1 -1
  67. package/dist/tools/executors/editor.d.ts +1 -1
  68. package/dist/tools/executors/editor.d.ts.map +1 -1
  69. package/dist/tools/executors/file-read.d.ts +1 -1
  70. package/dist/tools/executors/file-read.d.ts.map +1 -1
  71. package/dist/tools/executors/index.d.ts +14 -14
  72. package/dist/tools/executors/index.d.ts.map +1 -1
  73. package/dist/tools/executors/search.d.ts +1 -1
  74. package/dist/tools/executors/search.d.ts.map +1 -1
  75. package/dist/tools/executors/web-fetch.d.ts +1 -1
  76. package/dist/tools/executors/web-fetch.d.ts.map +1 -1
  77. package/dist/tools/helpers.d.ts +1 -1
  78. package/dist/tools/helpers.d.ts.map +1 -1
  79. package/dist/tools/index.d.ts +10 -10
  80. package/dist/tools/index.d.ts.map +1 -1
  81. package/dist/tools/model-tool-routing.d.ts +1 -1
  82. package/dist/tools/model-tool-routing.d.ts.map +1 -1
  83. package/dist/tools/presets.d.ts +1 -1
  84. package/dist/tools/presets.d.ts.map +1 -1
  85. package/dist/types/common.d.ts +17 -8
  86. package/dist/types/common.d.ts.map +1 -1
  87. package/dist/types/config.d.ts +4 -3
  88. package/dist/types/config.d.ts.map +1 -1
  89. package/dist/types/provider-settings.d.ts +1 -1
  90. package/dist/types/provider-settings.d.ts.map +1 -1
  91. package/dist/types.d.ts +5 -2
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/version.d.ts +2 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/package.json +44 -38
  96. package/src/ClineCore.ts +137 -0
  97. package/src/account/cline-account-service.test.ts +101 -0
  98. package/src/account/cline-account-service.ts +300 -0
  99. package/src/account/featurebase-token.test.ts +175 -0
  100. package/src/account/index.ts +23 -0
  101. package/src/account/rpc.test.ts +63 -0
  102. package/src/account/rpc.ts +185 -0
  103. package/src/account/types.ts +102 -0
  104. package/src/agents/agent-config-loader.test.ts +236 -0
  105. package/src/agents/agent-config-loader.ts +108 -0
  106. package/src/agents/agent-config-parser.ts +198 -0
  107. package/src/agents/hooks-config-loader.test.ts +20 -0
  108. package/src/agents/hooks-config-loader.ts +118 -0
  109. package/src/agents/index.ts +85 -0
  110. package/src/agents/plugin-config-loader.test.ts +140 -0
  111. package/src/agents/plugin-config-loader.ts +97 -0
  112. package/src/agents/plugin-loader.test.ts +210 -0
  113. package/src/agents/plugin-loader.ts +175 -0
  114. package/src/agents/plugin-sandbox-bootstrap.ts +448 -0
  115. package/src/agents/plugin-sandbox.test.ts +296 -0
  116. package/src/agents/plugin-sandbox.ts +341 -0
  117. package/src/agents/unified-config-file-watcher.test.ts +196 -0
  118. package/src/agents/unified-config-file-watcher.ts +483 -0
  119. package/src/agents/user-instruction-config-loader.test.ts +158 -0
  120. package/src/agents/user-instruction-config-loader.ts +438 -0
  121. package/src/auth/client.test.ts +40 -0
  122. package/src/auth/client.ts +25 -0
  123. package/src/auth/cline.test.ts +130 -0
  124. package/src/auth/cline.ts +420 -0
  125. package/src/auth/codex.test.ts +170 -0
  126. package/src/auth/codex.ts +491 -0
  127. package/src/auth/oca.test.ts +215 -0
  128. package/src/auth/oca.ts +573 -0
  129. package/src/auth/server.ts +216 -0
  130. package/src/auth/types.ts +81 -0
  131. package/src/auth/utils.test.ts +128 -0
  132. package/src/auth/utils.ts +247 -0
  133. package/src/chat/chat-schema.ts +82 -0
  134. package/src/index.ts +479 -0
  135. package/src/input/file-indexer.d.ts +11 -0
  136. package/src/input/file-indexer.test.ts +127 -0
  137. package/src/input/file-indexer.ts +327 -0
  138. package/src/input/index.ts +7 -0
  139. package/src/input/mention-enricher.test.ts +85 -0
  140. package/src/input/mention-enricher.ts +122 -0
  141. package/src/mcp/config-loader.test.ts +238 -0
  142. package/src/mcp/config-loader.ts +219 -0
  143. package/src/mcp/index.ts +26 -0
  144. package/src/mcp/manager.test.ts +106 -0
  145. package/src/mcp/manager.ts +262 -0
  146. package/src/mcp/types.ts +88 -0
  147. package/src/providers/local-provider-registry.ts +232 -0
  148. package/src/providers/local-provider-service.test.ts +783 -0
  149. package/src/providers/local-provider-service.ts +471 -0
  150. package/src/runtime/commands.test.ts +98 -0
  151. package/src/runtime/commands.ts +83 -0
  152. package/src/runtime/hook-file-hooks.test.ts +237 -0
  153. package/src/runtime/hook-file-hooks.ts +859 -0
  154. package/src/runtime/index.ts +37 -0
  155. package/src/runtime/rules.ts +34 -0
  156. package/src/runtime/runtime-builder.team-persistence.test.ts +245 -0
  157. package/src/runtime/runtime-builder.test.ts +371 -0
  158. package/src/runtime/runtime-builder.ts +631 -0
  159. package/src/runtime/runtime-parity.test.ts +143 -0
  160. package/src/runtime/sandbox/subprocess-sandbox.ts +231 -0
  161. package/src/runtime/session-runtime.ts +49 -0
  162. package/src/runtime/skills.ts +44 -0
  163. package/src/runtime/team-runtime-registry.ts +46 -0
  164. package/src/runtime/tool-approval.ts +104 -0
  165. package/src/runtime/workflows.test.ts +119 -0
  166. package/src/runtime/workflows.ts +45 -0
  167. package/src/session/default-session-manager.e2e.test.ts +384 -0
  168. package/src/session/default-session-manager.test.ts +1931 -0
  169. package/src/session/default-session-manager.ts +1422 -0
  170. package/src/session/file-session-service.ts +280 -0
  171. package/src/session/index.ts +45 -0
  172. package/src/session/rpc-runtime-ensure.ts +521 -0
  173. package/src/session/rpc-session-service.ts +107 -0
  174. package/src/session/rpc-spawn-lease.test.ts +49 -0
  175. package/src/session/rpc-spawn-lease.ts +122 -0
  176. package/src/session/runtime-oauth-token-manager.test.ts +137 -0
  177. package/src/session/runtime-oauth-token-manager.ts +272 -0
  178. package/src/session/session-agent-events.ts +248 -0
  179. package/src/session/session-artifacts.ts +106 -0
  180. package/src/session/session-config-builder.ts +113 -0
  181. package/src/session/session-graph.ts +92 -0
  182. package/src/session/session-host.test.ts +89 -0
  183. package/src/session/session-host.ts +205 -0
  184. package/src/session/session-manager.ts +69 -0
  185. package/src/session/session-manifest.ts +29 -0
  186. package/src/session/session-service.team-persistence.test.ts +48 -0
  187. package/src/session/session-service.ts +673 -0
  188. package/src/session/session-team-coordination.ts +229 -0
  189. package/src/session/session-telemetry.ts +100 -0
  190. package/src/session/sqlite-rpc-session-backend.ts +303 -0
  191. package/src/session/unified-session-persistence-service.test.ts +85 -0
  192. package/src/session/unified-session-persistence-service.ts +994 -0
  193. package/src/session/utils/helpers.ts +139 -0
  194. package/src/session/utils/types.ts +57 -0
  195. package/src/session/utils/usage.ts +32 -0
  196. package/src/session/workspace-manager.ts +98 -0
  197. package/src/session/workspace-manifest.ts +100 -0
  198. package/src/storage/artifact-store.ts +1 -0
  199. package/src/storage/file-team-store.ts +257 -0
  200. package/src/storage/index.ts +11 -0
  201. package/src/storage/provider-settings-legacy-migration.test.ts +424 -0
  202. package/src/storage/provider-settings-legacy-migration.ts +826 -0
  203. package/src/storage/provider-settings-manager.test.ts +191 -0
  204. package/src/storage/provider-settings-manager.ts +152 -0
  205. package/src/storage/session-store.ts +1 -0
  206. package/src/storage/sqlite-session-store.ts +275 -0
  207. package/src/storage/sqlite-team-store.ts +454 -0
  208. package/src/storage/team-store.ts +40 -0
  209. package/src/team/index.ts +4 -0
  210. package/src/team/projections.ts +285 -0
  211. package/src/telemetry/ITelemetryAdapter.ts +94 -0
  212. package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
  213. package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
  214. package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
  215. package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
  216. package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
  217. package/src/telemetry/OpenTelemetryProvider.ts +325 -0
  218. package/src/telemetry/TelemetryService.test.ts +134 -0
  219. package/src/telemetry/TelemetryService.ts +141 -0
  220. package/src/telemetry/core-events.ts +400 -0
  221. package/src/telemetry/distinct-id.test.ts +57 -0
  222. package/src/telemetry/distinct-id.ts +58 -0
  223. package/src/telemetry/index.ts +20 -0
  224. package/src/tools/constants.ts +35 -0
  225. package/src/tools/definitions.test.ts +704 -0
  226. package/src/tools/definitions.ts +709 -0
  227. package/src/tools/executors/apply-patch-parser.ts +520 -0
  228. package/src/tools/executors/apply-patch.ts +359 -0
  229. package/src/tools/executors/bash.test.ts +87 -0
  230. package/src/tools/executors/bash.ts +207 -0
  231. package/src/tools/executors/editor.test.ts +35 -0
  232. package/src/tools/executors/editor.ts +219 -0
  233. package/src/tools/executors/file-read.test.ts +49 -0
  234. package/src/tools/executors/file-read.ts +110 -0
  235. package/src/tools/executors/index.ts +87 -0
  236. package/src/tools/executors/search.ts +278 -0
  237. package/src/tools/executors/web-fetch.ts +259 -0
  238. package/src/tools/helpers.ts +130 -0
  239. package/src/tools/index.ts +169 -0
  240. package/src/tools/model-tool-routing.test.ts +86 -0
  241. package/src/tools/model-tool-routing.ts +132 -0
  242. package/src/tools/presets.test.ts +62 -0
  243. package/src/tools/presets.ts +168 -0
  244. package/src/tools/schemas.ts +327 -0
  245. package/src/tools/types.ts +329 -0
  246. package/src/types/common.ts +26 -0
  247. package/src/types/config.ts +86 -0
  248. package/src/types/events.ts +74 -0
  249. package/src/types/index.ts +24 -0
  250. package/src/types/provider-settings.ts +43 -0
  251. package/src/types/sessions.ts +16 -0
  252. package/src/types/storage.ts +64 -0
  253. package/src/types/workspace.ts +7 -0
  254. package/src/types.ts +132 -0
  255. package/src/version.ts +3 -0
  256. package/dist/index.node.d.ts +0 -47
  257. package/dist/index.node.d.ts.map +0 -1
  258. package/dist/index.node.js +0 -948
  259. package/dist/telemetry/opentelemetry.d.ts.map +0 -1
  260. package/dist/telemetry/opentelemetry.js +0 -27
@@ -0,0 +1,122 @@
1
+ import {
2
+ closeSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { resolveSessionDataDir } from "@clinebot/shared/storage";
12
+
13
+ const DEFAULT_LEASE_TTL_MS = 15_000;
14
+
15
+ interface RpcSpawnLeaseRecord {
16
+ address: string;
17
+ pid: number;
18
+ createdAt: number;
19
+ }
20
+
21
+ export interface RpcSpawnLease {
22
+ path: string;
23
+ release: () => void;
24
+ }
25
+
26
+ function encodeAddress(address: string): string {
27
+ return Buffer.from(address).toString("base64url");
28
+ }
29
+
30
+ function getLeasePath(address: string): string {
31
+ return resolve(
32
+ resolveSessionDataDir(),
33
+ "rpc",
34
+ "spawn-leases",
35
+ `${encodeAddress(address)}.lock`,
36
+ );
37
+ }
38
+
39
+ function isProcessAlive(pid: number): boolean {
40
+ if (!Number.isInteger(pid) || pid <= 0) {
41
+ return false;
42
+ }
43
+ try {
44
+ process.kill(pid, 0);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ function shouldClearLease(path: string, ttlMs: number): boolean {
52
+ try {
53
+ const raw = readFileSync(path, "utf8");
54
+ const parsed = JSON.parse(raw) as Partial<RpcSpawnLeaseRecord>;
55
+ const createdAt = Number(parsed.createdAt ?? 0);
56
+ if (!Number.isFinite(createdAt) || createdAt <= 0) {
57
+ return true;
58
+ }
59
+ if (Date.now() - createdAt > ttlMs) {
60
+ return true;
61
+ }
62
+ return !isProcessAlive(Number(parsed.pid ?? 0));
63
+ } catch {
64
+ return true;
65
+ }
66
+ }
67
+
68
+ export function tryAcquireRpcSpawnLease(
69
+ address: string,
70
+ options?: { ttlMs?: number },
71
+ ): RpcSpawnLease | undefined {
72
+ const ttlMs = Math.max(1_000, options?.ttlMs ?? DEFAULT_LEASE_TTL_MS);
73
+ const path = getLeasePath(address);
74
+ mkdirSync(dirname(path), { recursive: true });
75
+
76
+ if (existsSync(path) && shouldClearLease(path, ttlMs)) {
77
+ rmSync(path, { force: true });
78
+ }
79
+
80
+ let fd: number | undefined;
81
+ try {
82
+ fd = openSync(path, "wx");
83
+ const record: RpcSpawnLeaseRecord = {
84
+ address,
85
+ pid: process.pid,
86
+ createdAt: Date.now(),
87
+ };
88
+ writeFileSync(fd, JSON.stringify(record), "utf8");
89
+ } catch {
90
+ if (typeof fd === "number") {
91
+ try {
92
+ closeSync(fd);
93
+ } catch {
94
+ // Best effort.
95
+ }
96
+ }
97
+ return undefined;
98
+ }
99
+
100
+ let released = false;
101
+ return {
102
+ path,
103
+ release: () => {
104
+ if (released) {
105
+ return;
106
+ }
107
+ released = true;
108
+ try {
109
+ if (typeof fd === "number") {
110
+ closeSync(fd);
111
+ }
112
+ } catch {
113
+ // Best effort.
114
+ }
115
+ try {
116
+ rmSync(path, { force: true });
117
+ } catch {
118
+ // Best effort.
119
+ }
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,137 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ OAuthReauthRequiredError,
4
+ RuntimeOAuthTokenManager,
5
+ } from "./runtime-oauth-token-manager";
6
+
7
+ const {
8
+ getValidOpenAICodexCredentials,
9
+ getValidClineCredentials,
10
+ getValidOcaCredentials,
11
+ } = vi.hoisted(() => ({
12
+ getValidOpenAICodexCredentials: vi.fn(),
13
+ getValidClineCredentials: vi.fn(),
14
+ getValidOcaCredentials: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("../auth/codex", () => ({
18
+ getValidOpenAICodexCredentials,
19
+ }));
20
+
21
+ vi.mock("../auth/cline", () => ({
22
+ getValidClineCredentials,
23
+ }));
24
+
25
+ vi.mock("../auth/oca", () => ({
26
+ getValidOcaCredentials,
27
+ }));
28
+
29
+ describe("RuntimeOAuthTokenManager", () => {
30
+ beforeEach(() => {
31
+ vi.clearAllMocks();
32
+ });
33
+
34
+ it("refreshes and persists OpenAI Codex OAuth credentials", async () => {
35
+ const getProviderSettings = vi.fn().mockReturnValue({
36
+ provider: "openai-codex",
37
+ auth: {
38
+ accessToken: "access-old",
39
+ refreshToken: "refresh-old",
40
+ expiresAt: Date.now() - 1_000,
41
+ accountId: "acct-old",
42
+ },
43
+ });
44
+ const saveProviderSettings = vi.fn();
45
+
46
+ getValidOpenAICodexCredentials.mockResolvedValueOnce({
47
+ access: "access-new",
48
+ refresh: "refresh-new",
49
+ expires: 4_000_000_000_000,
50
+ accountId: "acct-new",
51
+ });
52
+
53
+ const manager = new RuntimeOAuthTokenManager({
54
+ providerSettingsManager: {
55
+ getProviderSettings,
56
+ saveProviderSettings,
57
+ } as never,
58
+ });
59
+
60
+ const result = await manager.resolveProviderApiKey({
61
+ providerId: "openai-codex",
62
+ });
63
+
64
+ expect(result).toMatchObject({
65
+ providerId: "openai-codex",
66
+ apiKey: "access-new",
67
+ accountId: "acct-new",
68
+ refreshed: true,
69
+ });
70
+ expect(saveProviderSettings).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ auth: expect.objectContaining({
73
+ accessToken: "access-new",
74
+ refreshToken: "refresh-new",
75
+ accountId: "acct-new",
76
+ expiresAt: 4_000_000_000_000,
77
+ }),
78
+ }),
79
+ { setLastUsed: false, tokenSource: "oauth" },
80
+ );
81
+ });
82
+
83
+ it("throws re-auth required when refresh returns null", async () => {
84
+ getValidOpenAICodexCredentials.mockResolvedValueOnce(null);
85
+ const manager = new RuntimeOAuthTokenManager({
86
+ providerSettingsManager: {
87
+ getProviderSettings: vi.fn().mockReturnValue({
88
+ provider: "openai-codex",
89
+ auth: {
90
+ accessToken: "access-old",
91
+ refreshToken: "refresh-old",
92
+ expiresAt: Date.now() - 1_000,
93
+ },
94
+ }),
95
+ saveProviderSettings: vi.fn(),
96
+ } as never,
97
+ });
98
+
99
+ await expect(
100
+ manager.resolveProviderApiKey({ providerId: "openai-codex" }),
101
+ ).rejects.toBeInstanceOf(OAuthReauthRequiredError);
102
+ });
103
+
104
+ it("de-duplicates concurrent refresh calls per provider", async () => {
105
+ const refreshBarrier = Promise.resolve().then(() => ({
106
+ access: "access-new",
107
+ refresh: "refresh-new",
108
+ expires: Date.now() + 60_000,
109
+ }));
110
+ getValidOpenAICodexCredentials.mockImplementationOnce(
111
+ async () => refreshBarrier,
112
+ );
113
+
114
+ const manager = new RuntimeOAuthTokenManager({
115
+ providerSettingsManager: {
116
+ getProviderSettings: vi.fn().mockReturnValue({
117
+ provider: "openai-codex",
118
+ auth: {
119
+ accessToken: "access-old",
120
+ refreshToken: "refresh-old",
121
+ expiresAt: Date.now() - 1_000,
122
+ },
123
+ }),
124
+ saveProviderSettings: vi.fn(),
125
+ } as never,
126
+ });
127
+
128
+ const [first, second] = await Promise.all([
129
+ manager.resolveProviderApiKey({ providerId: "openai-codex" }),
130
+ manager.resolveProviderApiKey({ providerId: "openai-codex" }),
131
+ ]);
132
+
133
+ expect(first?.apiKey).toBe("access-new");
134
+ expect(second?.apiKey).toBe("access-new");
135
+ expect(getValidOpenAICodexCredentials).toHaveBeenCalledTimes(1);
136
+ });
137
+ });
@@ -0,0 +1,272 @@
1
+ import type * as LlmsProviders from "@clinebot/llms/providers";
2
+ import {
3
+ type ITelemetryService,
4
+ isOAuthProviderId,
5
+ type OAuthProviderId,
6
+ } from "@clinebot/shared";
7
+ import {
8
+ type ClineOAuthCredentials,
9
+ getValidClineCredentials,
10
+ } from "../auth/cline";
11
+ import { getValidOpenAICodexCredentials } from "../auth/codex";
12
+ import { getValidOcaCredentials } from "../auth/oca";
13
+ import { decodeJwtPayload } from "../auth/utils";
14
+ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
15
+
16
+ const DEFAULT_CLINE_API_BASE_URL = "https://api.cline.bot";
17
+ const WORKOS_TOKEN_PREFIX = "workos:";
18
+
19
+ type ManagedOAuthProviderId = OAuthProviderId;
20
+
21
+ function toStoredAccessToken(
22
+ providerId: ManagedOAuthProviderId,
23
+ accessToken: string,
24
+ ): string {
25
+ if (providerId === "cline") {
26
+ return `${WORKOS_TOKEN_PREFIX}${accessToken}`;
27
+ }
28
+ return accessToken;
29
+ }
30
+
31
+ function fromStoredAccessToken(
32
+ providerId: ManagedOAuthProviderId,
33
+ accessToken: string,
34
+ ): string {
35
+ if (
36
+ providerId === "cline" &&
37
+ accessToken.toLowerCase().startsWith(WORKOS_TOKEN_PREFIX)
38
+ ) {
39
+ return accessToken.slice(WORKOS_TOKEN_PREFIX.length);
40
+ }
41
+ return accessToken;
42
+ }
43
+
44
+ function readExpiryFromToken(accessToken: string): number | null {
45
+ const payload = decodeJwtPayload(accessToken);
46
+ const exp = payload?.exp;
47
+ if (typeof exp === "number" && exp > 0) {
48
+ return exp * 1000;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function deriveCredentialExpiry(
54
+ settings: LlmsProviders.ProviderSettings,
55
+ normalizedAccessToken: string,
56
+ ): number {
57
+ const explicitExpiry = (
58
+ settings.auth as
59
+ | (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
60
+ | undefined
61
+ )?.expiresAt;
62
+ if (
63
+ typeof explicitExpiry === "number" &&
64
+ Number.isFinite(explicitExpiry) &&
65
+ explicitExpiry > 0
66
+ ) {
67
+ return explicitExpiry;
68
+ }
69
+
70
+ const jwtExpiry = readExpiryFromToken(normalizedAccessToken);
71
+ if (jwtExpiry) {
72
+ return jwtExpiry;
73
+ }
74
+
75
+ // Unknown expiry should trigger refresh on next resolution.
76
+ return Date.now() - 1;
77
+ }
78
+
79
+ function toCredentials(
80
+ providerId: ManagedOAuthProviderId,
81
+ settings: LlmsProviders.ProviderSettings,
82
+ ): ClineOAuthCredentials | null {
83
+ const rawAccess = settings.auth?.accessToken?.trim();
84
+ const refreshToken = settings.auth?.refreshToken?.trim();
85
+ if (!rawAccess || !refreshToken) {
86
+ return null;
87
+ }
88
+ const access = fromStoredAccessToken(providerId, rawAccess);
89
+ if (!access) {
90
+ return null;
91
+ }
92
+
93
+ return {
94
+ access,
95
+ refresh: refreshToken,
96
+ expires: deriveCredentialExpiry(settings, access),
97
+ accountId: settings.auth?.accountId,
98
+ };
99
+ }
100
+
101
+ function authSettingsEqual(
102
+ a: LlmsProviders.ProviderSettings["auth"] | undefined,
103
+ b: LlmsProviders.ProviderSettings["auth"] | undefined,
104
+ ): boolean {
105
+ const aExpiry = (
106
+ a as
107
+ | (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
108
+ | undefined
109
+ )?.expiresAt;
110
+ const bExpiry = (
111
+ b as
112
+ | (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
113
+ | undefined
114
+ )?.expiresAt;
115
+ return (
116
+ a?.accessToken === b?.accessToken &&
117
+ a?.refreshToken === b?.refreshToken &&
118
+ a?.accountId === b?.accountId &&
119
+ aExpiry === bExpiry
120
+ );
121
+ }
122
+
123
+ export class OAuthReauthRequiredError extends Error {
124
+ public readonly providerId: ManagedOAuthProviderId;
125
+
126
+ constructor(providerId: ManagedOAuthProviderId) {
127
+ super(
128
+ `OAuth credentials for provider "${providerId}" are no longer valid. Re-run authentication for this provider.`,
129
+ );
130
+ this.name = "OAuthReauthRequiredError";
131
+ this.providerId = providerId;
132
+ }
133
+ }
134
+
135
+ export type RuntimeOAuthResolution = {
136
+ providerId: ManagedOAuthProviderId;
137
+ apiKey: string;
138
+ accountId?: string;
139
+ refreshed: boolean;
140
+ };
141
+
142
+ export class RuntimeOAuthTokenManager {
143
+ private readonly providerSettingsManager: ProviderSettingsManager;
144
+ private readonly telemetry?: ITelemetryService;
145
+ private readonly refreshInFlight = new Map<
146
+ ManagedOAuthProviderId,
147
+ Promise<RuntimeOAuthResolution | null>
148
+ >();
149
+
150
+ constructor(options?: {
151
+ providerSettingsManager?: ProviderSettingsManager;
152
+ telemetry?: ITelemetryService;
153
+ }) {
154
+ this.providerSettingsManager =
155
+ options?.providerSettingsManager ?? new ProviderSettingsManager();
156
+ this.telemetry = options?.telemetry;
157
+ }
158
+
159
+ public async resolveProviderApiKey(input: {
160
+ providerId: string;
161
+ forceRefresh?: boolean;
162
+ }): Promise<RuntimeOAuthResolution | null> {
163
+ if (!isOAuthProviderId(input.providerId)) {
164
+ return null;
165
+ }
166
+ return this.resolveWithSingleFlight(input.providerId, input.forceRefresh);
167
+ }
168
+
169
+ private async resolveWithSingleFlight(
170
+ providerId: ManagedOAuthProviderId,
171
+ forceRefresh = false,
172
+ ): Promise<RuntimeOAuthResolution | null> {
173
+ const currentInFlight = this.refreshInFlight.get(providerId);
174
+ if (currentInFlight) {
175
+ return currentInFlight;
176
+ }
177
+ const pending = this.resolveProviderApiKeyInternal(providerId, forceRefresh)
178
+ .catch((error) => {
179
+ throw error;
180
+ })
181
+ .finally(() => {
182
+ this.refreshInFlight.delete(providerId);
183
+ });
184
+ this.refreshInFlight.set(providerId, pending);
185
+ return pending;
186
+ }
187
+
188
+ private async resolveProviderApiKeyInternal(
189
+ providerId: ManagedOAuthProviderId,
190
+ forceRefresh: boolean,
191
+ ): Promise<RuntimeOAuthResolution | null> {
192
+ const settings =
193
+ this.providerSettingsManager.getProviderSettings(providerId);
194
+ if (!settings) {
195
+ return null;
196
+ }
197
+
198
+ const currentCredentials = toCredentials(providerId, settings);
199
+ if (!currentCredentials) {
200
+ return null;
201
+ }
202
+
203
+ const nextCredentials = await this.resolveCredentials(
204
+ providerId,
205
+ settings,
206
+ currentCredentials,
207
+ forceRefresh,
208
+ );
209
+ if (!nextCredentials) {
210
+ throw new OAuthReauthRequiredError(providerId);
211
+ }
212
+
213
+ const persistedAccessToken = toStoredAccessToken(
214
+ providerId,
215
+ nextCredentials.access,
216
+ );
217
+ const nextAuth = {
218
+ ...(settings.auth ?? {}),
219
+ accessToken: persistedAccessToken,
220
+ refreshToken: nextCredentials.refresh,
221
+ accountId: nextCredentials.accountId,
222
+ } as LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number };
223
+ nextAuth.expiresAt = nextCredentials.expires;
224
+ const nextSettings: LlmsProviders.ProviderSettings = {
225
+ ...settings,
226
+ auth: nextAuth,
227
+ };
228
+ const wasRefreshed = !authSettingsEqual(settings.auth, nextSettings.auth);
229
+ if (wasRefreshed) {
230
+ this.providerSettingsManager.saveProviderSettings(nextSettings, {
231
+ setLastUsed: false,
232
+ tokenSource: "oauth",
233
+ });
234
+ }
235
+
236
+ return {
237
+ providerId,
238
+ apiKey: persistedAccessToken,
239
+ accountId: nextCredentials.accountId,
240
+ refreshed: wasRefreshed,
241
+ };
242
+ }
243
+
244
+ private async resolveCredentials(
245
+ providerId: ManagedOAuthProviderId,
246
+ settings: LlmsProviders.ProviderSettings,
247
+ currentCredentials: ClineOAuthCredentials,
248
+ forceRefresh: boolean,
249
+ ): Promise<ClineOAuthCredentials | null> {
250
+ if (providerId === "cline") {
251
+ return getValidClineCredentials(
252
+ currentCredentials,
253
+ {
254
+ apiBaseUrl: settings.baseUrl?.trim() || DEFAULT_CLINE_API_BASE_URL,
255
+ telemetry: this.telemetry,
256
+ },
257
+ { forceRefresh },
258
+ );
259
+ }
260
+ if (providerId === "oca") {
261
+ return getValidOcaCredentials(
262
+ currentCredentials,
263
+ { forceRefresh, telemetry: this.telemetry },
264
+ { mode: settings.oca?.mode, telemetry: this.telemetry },
265
+ );
266
+ }
267
+ return getValidOpenAICodexCredentials(currentCredentials, {
268
+ forceRefresh,
269
+ telemetry: this.telemetry,
270
+ });
271
+ }
272
+ }