@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.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 (146) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
  19. package/dist/cc-orchestrator/agent-adapter.js +4 -0
  20. package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
  21. package/dist/cc-orchestrator/codex-session-history.js +64 -4
  22. package/dist/cc-orchestrator/external-agent-changes.js +22 -5
  23. package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
  24. package/dist/cc-orchestrator/external-agent-driver.js +202 -38
  25. package/dist/cc-orchestrator/session-history.d.ts +35 -0
  26. package/dist/cc-orchestrator/session-history.js +96 -13
  27. package/dist/cli/agent-server-tcp.js +13 -2
  28. package/dist/context/manager.d.ts +3 -3
  29. package/dist/context/manager.js +6 -6
  30. package/dist/context/token-counter.js +5 -3
  31. package/dist/context/tool-result-storage.d.ts +6 -0
  32. package/dist/context/tool-result-storage.js +25 -4
  33. package/dist/credentials/access.d.ts +11 -1
  34. package/dist/credentials/access.js +77 -1
  35. package/dist/credentials/index.d.ts +3 -1
  36. package/dist/credentials/index.js +2 -0
  37. package/dist/credentials/oauth.d.ts +25 -0
  38. package/dist/credentials/oauth.js +179 -0
  39. package/dist/credentials/store.d.ts +2 -1
  40. package/dist/credentials/store.js +19 -8
  41. package/dist/credentials/types.d.ts +84 -1
  42. package/dist/credentials/types.js +16 -1
  43. package/dist/engine/engine.d.ts +67 -34
  44. package/dist/engine/engine.js +448 -247
  45. package/dist/engine/goal.d.ts +19 -0
  46. package/dist/engine/goal.js +16 -6
  47. package/dist/engine/input-attachments.js +156 -13
  48. package/dist/engine/run-image-input.d.ts +22 -0
  49. package/dist/engine/run-image-input.js +195 -0
  50. package/dist/engine/session-title.d.ts +2 -1
  51. package/dist/engine/session-title.js +4 -1
  52. package/dist/engine/steer-queue.d.ts +3 -1
  53. package/dist/engine/steer-queue.js +10 -2
  54. package/dist/engine/turn-loop.d.ts +48 -1
  55. package/dist/engine/turn-loop.js +307 -37
  56. package/dist/engine/types.d.ts +6 -2
  57. package/dist/git/worktree/crud.d.ts +3 -0
  58. package/dist/git/worktree/crud.js +32 -3
  59. package/dist/git/worktree/git-exec.d.ts +2 -2
  60. package/dist/git/worktree/git-exec.js +47 -11
  61. package/dist/git/worktree/query.d.ts +8 -7
  62. package/dist/git/worktree/query.js +27 -20
  63. package/dist/hooks/events.d.ts +3 -0
  64. package/dist/hooks/events.js +0 -3
  65. package/dist/hooks/goal-stop-hook.d.ts +44 -2
  66. package/dist/hooks/goal-stop-hook.js +775 -52
  67. package/dist/hooks/registry.js +3 -0
  68. package/dist/hooks/shell-runner.d.ts +12 -1
  69. package/dist/hooks/shell-runner.js +160 -9
  70. package/dist/index.d.ts +7 -6
  71. package/dist/index.js +6 -5
  72. package/dist/llm/client-base.js +12 -10
  73. package/dist/llm/types.d.ts +12 -5
  74. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  75. package/dist/plugins/pluginCommandHook.js +111 -13
  76. package/dist/preset/index.js +14 -4
  77. package/dist/protocol/chat-session-manager.d.ts +13 -2
  78. package/dist/protocol/chat-session-manager.js +90 -18
  79. package/dist/protocol/chat-session.d.ts +12 -0
  80. package/dist/protocol/chat-session.js +30 -5
  81. package/dist/protocol/client.d.ts +5 -2
  82. package/dist/protocol/client.js +22 -1
  83. package/dist/protocol/server.d.ts +25 -11
  84. package/dist/protocol/server.js +291 -73
  85. package/dist/protocol/types.d.ts +36 -2
  86. package/dist/protocol/types.js +2 -0
  87. package/dist/services/dream-consolidation.d.ts +3 -0
  88. package/dist/services/dream-consolidation.js +4 -1
  89. package/dist/services/index.d.ts +1 -1
  90. package/dist/services/index.js +1 -1
  91. package/dist/services/oauth.d.ts +34 -10
  92. package/dist/services/oauth.js +233 -98
  93. package/dist/session/session-manager.d.ts +35 -6
  94. package/dist/session/session-manager.js +396 -27
  95. package/dist/session/transcript.d.ts +30 -1
  96. package/dist/session/transcript.js +119 -4
  97. package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
  98. package/dist/tool-system/builtin/agent-notifications.js +19 -7
  99. package/dist/tool-system/builtin/agent.js +5 -1
  100. package/dist/tool-system/builtin/arena.js +1 -0
  101. package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
  102. package/dist/tool-system/builtin/background-jobs.js +109 -7
  103. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  104. package/dist/tool-system/builtin/background-work.js +5 -1
  105. package/dist/tool-system/builtin/bash.d.ts +3 -5
  106. package/dist/tool-system/builtin/bash.js +10 -5
  107. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  108. package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
  109. package/dist/tool-system/builtin/cron-list.definition.js +6 -0
  110. package/dist/tool-system/builtin/cron.d.ts +1 -2
  111. package/dist/tool-system/builtin/cron.js +9 -7
  112. package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
  113. package/dist/tool-system/builtin/drive-claude-code.js +307 -20
  114. package/dist/tool-system/builtin/edit.d.ts +2 -1
  115. package/dist/tool-system/builtin/edit.js +12 -4
  116. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  117. package/dist/tool-system/builtin/generate-video.js +138 -21
  118. package/dist/tool-system/builtin/glob.d.ts +2 -1
  119. package/dist/tool-system/builtin/glob.js +28 -3
  120. package/dist/tool-system/builtin/grep.d.ts +1 -0
  121. package/dist/tool-system/builtin/grep.js +82 -17
  122. package/dist/tool-system/builtin/index.d.ts +25 -11
  123. package/dist/tool-system/builtin/index.js +60 -5
  124. package/dist/tool-system/builtin/sleep.d.ts +1 -2
  125. package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
  126. package/dist/tool-system/builtin/sleep.definition.js +28 -0
  127. package/dist/tool-system/builtin/sleep.js +1 -22
  128. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  129. package/dist/tool-system/builtin/video-providers.js +1 -0
  130. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  131. package/dist/tool-system/builtin/web-fetch.js +44 -3
  132. package/dist/tool-system/builtin/worktree.js +25 -7
  133. package/dist/tool-system/builtin/write.d.ts +2 -1
  134. package/dist/tool-system/builtin/write.js +14 -4
  135. package/dist/tool-system/context.d.ts +35 -5
  136. package/dist/tool-system/executor.js +24 -8
  137. package/dist/tool-system/mcp-manager.d.ts +20 -2
  138. package/dist/tool-system/mcp-manager.js +111 -12
  139. package/dist/tool-system/path-policy.d.ts +19 -0
  140. package/dist/tool-system/path-policy.js +62 -1
  141. package/dist/tool-system/permission.d.ts +43 -3
  142. package/dist/tool-system/permission.js +383 -30
  143. package/dist/tool-system/registry.d.ts +3 -2
  144. package/dist/tool-system/registry.js +52 -34
  145. package/dist/types.d.ts +38 -7
  146. package/package.json +1 -1
@@ -55,6 +55,12 @@ export declare function reconstructContentReplacementState(messages: Message[]):
55
55
  * transcript path. Layout: <transcriptDir>/tool-results/
56
56
  */
57
57
  export declare function resolveToolResultsDir(transcriptPath: string): string;
58
+ /**
59
+ * Convert an externally supplied tool_use_id to a bounded internal filename.
60
+ * The original id remains the key in ContentReplacementState/onPersist; it is
61
+ * never used as a path component.
62
+ */
63
+ export declare function safeToolResultFilename(toolUseId: string): string;
58
64
  interface PersistOptions {
59
65
  /** Per-result threshold; default DEFAULT_PERSIST_THRESHOLD. */
60
66
  perResultThreshold?: number;
@@ -22,7 +22,8 @@
22
22
  * parallel Read results), persist the largest ones first until under.
23
23
  */
24
24
  import { mkdirSync, writeFileSync } from "node:fs";
25
- import { dirname, join } from "node:path";
25
+ import { createHash } from "node:crypto";
26
+ import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
26
27
  import { logger } from "../logging/logger.js";
27
28
  // ─── Tunables ───────────────────────────────────────────────────────
28
29
  /** Per-result threshold. Larger than this → persist + replace. */
@@ -77,12 +78,32 @@ function ensureDir(dir) {
77
78
  }
78
79
  }
79
80
  /**
80
- * Write content to <dir>/<toolUseId>.txt. Idempotent: skips if the file
81
- * already exists (tool_use_id is a UUID, content is deterministic per id).
81
+ * Convert an externally supplied tool_use_id to a bounded internal filename.
82
+ * The original id remains the key in ContentReplacementState/onPersist; it is
83
+ * never used as a path component.
82
84
  */
85
+ export function safeToolResultFilename(toolUseId) {
86
+ if (toolUseId.length > 256) {
87
+ throw new RangeError("tool_use_id exceeds the 256 character limit");
88
+ }
89
+ if (isAbsolute(toolUseId) ||
90
+ win32.isAbsolute(toolUseId) ||
91
+ toolUseId.includes("/") ||
92
+ toolUseId.includes("\\")) {
93
+ throw new Error("tool_use_id must not contain a path");
94
+ }
95
+ return `${createHash("sha256").update(toolUseId, "utf8").digest("hex")}.txt`;
96
+ }
97
+ /** Write content to the contained hashed path. Idempotent by original id. */
83
98
  function persistToFile(dir, toolUseId, content) {
99
+ const root = resolve(dir);
100
+ const filename = safeToolResultFilename(toolUseId);
101
+ const filepath = resolve(root, filename);
102
+ const rel = relative(root, filepath);
103
+ if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
104
+ throw new Error("tool_result path escaped its storage root");
105
+ }
84
106
  ensureDir(dir);
85
- const filepath = join(dir, `${toolUseId}.txt`);
86
107
  try {
87
108
  // 'wx' = fail if exists. Skipping on collision is correct: same id =
88
109
  // same content. Avoids re-writing the same bytes every turn.
@@ -1,4 +1,4 @@
1
- import type { Credential, CredentialType } from "./types.js";
1
+ import { type Credential, type CredentialType } from "./types.js";
2
2
  import type { SettingsScope } from "../settings/manager.js";
3
3
  import type { Transport } from "../protocol/transport.js";
4
4
  export type CredentialAccessScope = "full" | "project";
@@ -12,6 +12,7 @@ export interface CredentialMetadata {
12
12
  meta?: Credential["meta"];
13
13
  hasSecret: boolean;
14
14
  secretHint?: string;
15
+ oauthStatus?: import("./types.js").OAuthCredentialPublicStatus;
15
16
  }
16
17
  export interface CredentialAccess {
17
18
  listMasked(cwd: string | undefined, scope: CredentialAccessScope): CredentialMetadata[];
@@ -23,6 +24,15 @@ export interface CredentialAccess {
23
24
  scope: CredentialAccessScope;
24
25
  purpose: "use" | "mcp";
25
26
  }): Promise<string>;
27
+ /** Resolve only the bearer material needed by an MCP request. */
28
+ resolveOAuthAccess?(req: {
29
+ id: string;
30
+ scope: "full";
31
+ forceRefresh?: boolean;
32
+ }): Promise<{
33
+ accessToken: string;
34
+ expiresAt?: string;
35
+ }>;
26
36
  materializeCookie?(req: {
27
37
  cwd?: string;
28
38
  id: string;
@@ -2,8 +2,11 @@ import { existsSync, readdirSync, rmSync, statSync, writeFileSync } from "node:f
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
5
6
  import { CredentialStore } from "./store.js";
6
7
  import { formatNetscapeCookies, parseCookieJar } from "./cookie-jar.js";
8
+ import { parseOAuthCredentialSecret, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, } from "./oauth.js";
9
+ import { refreshToken } from "../services/oauth.js";
7
10
  const COOKIE_FILE_PREFIX = "codeshell-cred-cookie-";
8
11
  const COOKIE_FILE_MAX_AGE_MS = 30 * 60 * 1000;
9
12
  let defaultCredentialAccess = null;
@@ -84,6 +87,16 @@ export function createIpcCredentialAccess(transport) {
84
87
  throw new Error(`credential "${req.id}" is unavailable`);
85
88
  return result.value;
86
89
  },
90
+ async resolveOAuthAccess(req) {
91
+ const result = (await request("desktop/oauthAccessResolve", req));
92
+ if (typeof result?.accessToken !== "string") {
93
+ throw new Error(`oauth credential "${req.id}" requires login`);
94
+ }
95
+ return {
96
+ accessToken: result.accessToken,
97
+ expiresAt: typeof result.expiresAt === "string" ? result.expiresAt : undefined,
98
+ };
99
+ },
87
100
  async materializeCookie(req) {
88
101
  const result = (await request("desktop/credentialMaterializeCookie", req));
89
102
  if (typeof result?.cookiesFile !== "string" || typeof result.count !== "number") {
@@ -105,8 +118,10 @@ function toMetadata(cred) {
105
118
  const { secret: _secret, ...rest } = cred;
106
119
  return {
107
120
  ...rest,
121
+ ...(credentialAllowsEnvExposure(cred.type) ? {} : { exposeAsEnv: undefined }),
108
122
  hasSecret: available,
109
- secretHint: available ? (secret.length > 4 ? `****${secret.slice(-4)}` : "****") : undefined,
123
+ secretHint: available ? credentialSecretHint(cred.type, secret) : undefined,
124
+ ...(cred.type === "oauth" ? { oauthStatus: summarizeOAuthCredentialSecret(secret) } : {}),
110
125
  };
111
126
  }
112
127
  function cloneMetadata(list) {
@@ -127,6 +142,8 @@ export const localCredentialAccess = {
127
142
  const out = {};
128
143
  const creds = storeFor(cwd).list(scope);
129
144
  for (const cred of creds) {
145
+ if (!credentialAllowsEnvExposure(cred.type))
146
+ continue;
130
147
  const name = cred.exposeAsEnv?.trim();
131
148
  if (name && isCredentialSecretAvailable(cred.secret))
132
149
  out[name] = cred.secret;
@@ -140,6 +157,9 @@ export const localCredentialAccess = {
140
157
  }
141
158
  return cred.secret;
142
159
  },
160
+ async resolveOAuthAccess(req) {
161
+ return resolveLocalOAuthAccess(req.id, Boolean(req.forceRefresh));
162
+ },
143
163
  async materializeCookie(req) {
144
164
  const cred = storeFor(req.cwd).resolve(req.id, req.scope);
145
165
  if (!cred || cred.type !== "cookie" || !isCredentialSecretAvailable(cred.secret)) {
@@ -148,6 +168,62 @@ export const localCredentialAccess = {
148
168
  return materializeCookieSecret(cred.id, cred.secret);
149
169
  },
150
170
  };
171
+ const localOAuthRefreshes = new Map();
172
+ async function resolveLocalOAuthAccess(id, forceRefresh) {
173
+ const store = new CredentialStore(undefined);
174
+ const cred = store.resolve(id, "full");
175
+ if (!cred || cred.type !== "oauth" || !isCredentialSecretAvailable(cred.secret)) {
176
+ throw new Error(`oauth credential "${id}" requires login`);
177
+ }
178
+ const parsed = parseOAuthCredentialSecret(cred.secret);
179
+ const decision = shouldRefreshOAuthCredential(parsed);
180
+ if (!forceRefresh && decision === "no") {
181
+ return { accessToken: parsed.accessToken, expiresAt: parsed.expiresAt };
182
+ }
183
+ if (!parsed.refreshToken || !parsed.tokenEndpoint || !parsed.clientId) {
184
+ throw new Error(`oauth credential "${id}" requires login`);
185
+ }
186
+ const inflight = localOAuthRefreshes.get(id);
187
+ if (inflight)
188
+ return inflight;
189
+ const pending = (async () => {
190
+ const latestCred = store.resolve(id, "full");
191
+ if (!latestCred || latestCred.type !== "oauth" || !latestCred.secret) {
192
+ throw new Error(`oauth credential "${id}" requires login`);
193
+ }
194
+ const latest = parseOAuthCredentialSecret(latestCred.secret);
195
+ if (!forceRefresh && shouldRefreshOAuthCredential(latest) === "no") {
196
+ return { accessToken: latest.accessToken, expiresAt: latest.expiresAt };
197
+ }
198
+ if (!latest.refreshToken || !latest.tokenEndpoint || !latest.clientId) {
199
+ throw new Error(`oauth credential "${id}" requires login`);
200
+ }
201
+ const tokens = await refreshToken({
202
+ clientId: latest.clientId,
203
+ clientSecret: latest.clientSecret,
204
+ tokenEndpoint: latest.tokenEndpoint,
205
+ authorizationEndpoint: "",
206
+ scopes: latest.scopes,
207
+ resource: latest.resource,
208
+ }, latest.refreshToken);
209
+ const next = {
210
+ ...latest,
211
+ accessToken: tokens.accessToken,
212
+ refreshToken: tokens.refreshToken ?? latest.refreshToken,
213
+ expiresAt: tokens.expiresAt ? new Date(tokens.expiresAt).toISOString() : undefined,
214
+ tokenType: tokens.tokenType,
215
+ scope: tokens.scope ?? latest.scope,
216
+ };
217
+ store.save("user", {
218
+ ...latestCred,
219
+ secret: JSON.stringify(next),
220
+ meta: { ...latestCred.meta, lastRefreshAt: new Date().toISOString() },
221
+ });
222
+ return { accessToken: next.accessToken, expiresAt: next.expiresAt };
223
+ })().finally(() => localOAuthRefreshes.delete(id));
224
+ localOAuthRefreshes.set(id, pending);
225
+ return pending;
226
+ }
151
227
  export function materializeCookieSecret(credentialId, secret) {
152
228
  const jar = parseCookieJar(secret);
153
229
  if (jar.length === 0)
@@ -2,7 +2,9 @@ export { CredentialStore } from "./store.js";
2
2
  export type { CredentialScope, MaskedCredential } from "./store.js";
3
3
  export { type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./cipher.js";
4
4
  export { getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, type CredentialAccess, type CredentialAccessScope, type CredentialMetadata, type CredentialSnapshot, type CredentialSnapshotEntry, } from "./access.js";
5
- export type { Credential, CredentialType, CredentialStoreFile } from "./types.js";
5
+ export type { Credential, CredentialType, CredentialStoreFile, OAuthCredentialPublicStatus, OAuthCredentialSecret, OAuthTokenResponse, } from "./types.js";
6
+ export { credentialAllowsEnvExposure, credentialSecretHint } from "./types.js";
7
+ export { buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, type OAuthClockOptions, type OAuthRefreshHandler, type OAuthRefreshRequest, } from "./oauth.js";
6
8
  export { formatNetscapeCookies, parseCookieJar, type CookieLike } from "./cookie-jar.js";
7
9
  export { useCredentialToolDef, useCredentialToolDefFor, useCredentialTool, sweepStaleCredentialCookies, } from "./use-credential-tool.js";
8
10
  export { credentialUseGate } from "./use-gate.js";
@@ -1,6 +1,8 @@
1
1
  export { CredentialStore } from "./store.js";
2
2
  export { PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./cipher.js";
3
3
  export { getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, } from "./access.js";
4
+ export { credentialAllowsEnvExposure, credentialSecretHint } from "./types.js";
5
+ export { buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, } from "./oauth.js";
4
6
  export { formatNetscapeCookies, parseCookieJar } from "./cookie-jar.js";
5
7
  export { useCredentialToolDef, useCredentialToolDefFor, useCredentialTool, sweepStaleCredentialCookies, } from "./use-credential-tool.js";
6
8
  export { credentialUseGate } from "./use-gate.js";
@@ -0,0 +1,25 @@
1
+ import type { OAuthCredentialPublicStatus, OAuthCredentialSecret, OAuthTokenResponse } from "./types.js";
2
+ export interface OAuthClockOptions {
3
+ now?: number | (() => number);
4
+ skewMs?: number;
5
+ }
6
+ export interface OAuthRefreshRequest {
7
+ credentialId: string;
8
+ tokenEndpoint: string;
9
+ refreshToken: string;
10
+ clientId?: string;
11
+ clientSecret?: string;
12
+ scope?: string;
13
+ scopes?: string[];
14
+ }
15
+ export type OAuthRefreshHandler = (req: OAuthRefreshRequest) => Promise<OAuthCredentialSecret>;
16
+ export declare function parseOAuthCredentialSecret(secret: string): OAuthCredentialSecret;
17
+ /** Merge and validate an OAuth token endpoint response without losing rotation state. */
18
+ export declare function mergeOAuthTokenResponse(previous: OAuthCredentialSecret | undefined, response: OAuthTokenResponse, opts?: {
19
+ now?: number;
20
+ }): OAuthCredentialSecret;
21
+ export declare function shouldRefreshOAuthCredential(secret: OAuthCredentialSecret, opts?: OAuthClockOptions): "no" | "refresh" | "login_required";
22
+ export declare function isOAuthAccessTokenExpired(secret: Pick<OAuthCredentialSecret, "accessToken" | "expiresAt">, opts?: OAuthClockOptions): boolean;
23
+ export declare function oauthCredentialStatus(secret: Pick<OAuthCredentialSecret, "accessToken" | "expiresAt">, opts?: OAuthClockOptions): Pick<OAuthCredentialPublicStatus, "state" | "expiresAt" | "expiresInMs">;
24
+ export declare function buildOAuthRefreshRequest(credentialId: string, secret: OAuthCredentialSecret): OAuthRefreshRequest | undefined;
25
+ export declare function summarizeOAuthCredentialSecret(secret: string | undefined, opts?: OAuthClockOptions): OAuthCredentialPublicStatus;
@@ -0,0 +1,179 @@
1
+ const DEFAULT_OAUTH_REFRESH_SKEW_MS = 60_000;
2
+ function nowMs(opts = {}) {
3
+ if (typeof opts.now === "function")
4
+ return opts.now();
5
+ if (typeof opts.now === "number")
6
+ return opts.now;
7
+ return Date.now();
8
+ }
9
+ function optionalString(value) {
10
+ return typeof value === "string" && value.length > 0 ? value : undefined;
11
+ }
12
+ function optionalStringArray(value) {
13
+ if (!Array.isArray(value))
14
+ return undefined;
15
+ const out = value.filter((x) => typeof x === "string" && x.length > 0);
16
+ return out.length > 0 ? out : undefined;
17
+ }
18
+ function optionalFiniteNumber(value) {
19
+ if (value === undefined)
20
+ return undefined;
21
+ if (typeof value !== "number" || !Number.isFinite(value)) {
22
+ throw new Error("OAuth token response expires_in must be a finite non-negative number");
23
+ }
24
+ if (value < 0) {
25
+ throw new Error("OAuth token response expires_in must be a finite non-negative number");
26
+ }
27
+ return value;
28
+ }
29
+ function parseExpiresAt(value) {
30
+ if (!value)
31
+ return undefined;
32
+ const ms = Date.parse(value);
33
+ if (!Number.isFinite(ms)) {
34
+ throw new Error(`OAuth credential secret has invalid expiresAt: ${value}`);
35
+ }
36
+ return ms;
37
+ }
38
+ export function parseOAuthCredentialSecret(secret) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(secret);
42
+ }
43
+ catch {
44
+ throw new Error("OAuth credential secret must be a JSON object");
45
+ }
46
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
47
+ throw new Error("OAuth credential secret must be a JSON object");
48
+ }
49
+ const raw = parsed;
50
+ if (raw.version !== undefined && raw.version !== 1) {
51
+ throw new Error("OAuth credential secret version must be 1");
52
+ }
53
+ const accessToken = optionalString(raw.accessToken);
54
+ if (!accessToken) {
55
+ throw new Error("OAuth credential secret must include accessToken");
56
+ }
57
+ const expiresAt = optionalString(raw.expiresAt);
58
+ parseExpiresAt(expiresAt);
59
+ const tokenType = optionalString(raw.tokenType);
60
+ if (tokenType && tokenType.toLowerCase() !== "bearer") {
61
+ throw new Error("OAuth credential tokenType must be Bearer");
62
+ }
63
+ return {
64
+ version: raw.version === 1 ? 1 : undefined,
65
+ accessToken,
66
+ refreshToken: optionalString(raw.refreshToken),
67
+ expiresAt,
68
+ tokenType,
69
+ scope: optionalString(raw.scope),
70
+ scopes: optionalStringArray(raw.scopes),
71
+ tokenEndpoint: optionalString(raw.tokenEndpoint),
72
+ clientId: optionalString(raw.clientId),
73
+ clientSecret: optionalString(raw.clientSecret),
74
+ issuer: optionalString(raw.issuer),
75
+ resource: optionalString(raw.resource),
76
+ revocationEndpoint: optionalString(raw.revocationEndpoint),
77
+ clientRegistration: raw.clientRegistration &&
78
+ typeof raw.clientRegistration === "object" &&
79
+ !Array.isArray(raw.clientRegistration) &&
80
+ optionalString(raw.clientRegistration.clientId)
81
+ ? {
82
+ clientId: optionalString(raw.clientRegistration.clientId),
83
+ clientSecret: optionalString(raw.clientRegistration.clientSecret),
84
+ clientIdIssuedAt: typeof raw.clientRegistration.clientIdIssuedAt ===
85
+ "number"
86
+ ? raw.clientRegistration.clientIdIssuedAt
87
+ : undefined,
88
+ clientSecretExpiresAt: typeof raw.clientRegistration.clientSecretExpiresAt ===
89
+ "number"
90
+ ? raw.clientRegistration
91
+ .clientSecretExpiresAt
92
+ : undefined,
93
+ }
94
+ : undefined,
95
+ };
96
+ }
97
+ /** Merge and validate an OAuth token endpoint response without losing rotation state. */
98
+ export function mergeOAuthTokenResponse(previous, response, opts = {}) {
99
+ const accessToken = optionalString(response.access_token);
100
+ if (!accessToken)
101
+ throw new Error("OAuth token response must include access_token");
102
+ const tokenType = optionalString(response.token_type) ?? "Bearer";
103
+ if (tokenType.toLowerCase() !== "bearer") {
104
+ throw new Error(`OAuth token response token_type must be Bearer`);
105
+ }
106
+ const expiresIn = optionalFiniteNumber(response.expires_in);
107
+ const now = opts.now ?? Date.now();
108
+ const responseScope = optionalString(response.scope);
109
+ const refreshToken = optionalString(response.refresh_token) ?? previous?.refreshToken;
110
+ return {
111
+ ...previous,
112
+ version: 1,
113
+ accessToken,
114
+ refreshToken,
115
+ expiresAt: expiresIn === undefined ? undefined : new Date(now + expiresIn * 1000).toISOString(),
116
+ tokenType: "Bearer",
117
+ scope: responseScope ?? previous?.scope,
118
+ scopes: responseScope ? responseScope.split(/\s+/).filter(Boolean) : previous?.scopes,
119
+ };
120
+ }
121
+ export function shouldRefreshOAuthCredential(secret, opts = {}) {
122
+ if (!isOAuthAccessTokenExpired(secret, opts))
123
+ return "no";
124
+ return secret.refreshToken && secret.tokenEndpoint ? "refresh" : "login_required";
125
+ }
126
+ export function isOAuthAccessTokenExpired(secret, opts = {}) {
127
+ const expiresAtMs = parseExpiresAt(secret.expiresAt);
128
+ if (expiresAtMs === undefined)
129
+ return false;
130
+ const skewMs = opts.skewMs ?? DEFAULT_OAUTH_REFRESH_SKEW_MS;
131
+ return expiresAtMs <= nowMs(opts) + skewMs;
132
+ }
133
+ export function oauthCredentialStatus(secret, opts = {}) {
134
+ const expiresAtMs = parseExpiresAt(secret.expiresAt);
135
+ if (expiresAtMs === undefined) {
136
+ return { state: "valid", expiresAt: undefined, expiresInMs: undefined };
137
+ }
138
+ const expiresInMs = expiresAtMs - nowMs(opts);
139
+ return {
140
+ state: isOAuthAccessTokenExpired(secret, opts) ? "expired" : "valid",
141
+ expiresAt: secret.expiresAt,
142
+ expiresInMs,
143
+ };
144
+ }
145
+ export function buildOAuthRefreshRequest(credentialId, secret) {
146
+ if (!secret.refreshToken || !secret.tokenEndpoint)
147
+ return undefined;
148
+ return {
149
+ credentialId,
150
+ tokenEndpoint: secret.tokenEndpoint,
151
+ refreshToken: secret.refreshToken,
152
+ clientId: secret.clientId,
153
+ clientSecret: secret.clientSecret,
154
+ scope: secret.scope,
155
+ scopes: secret.scopes,
156
+ };
157
+ }
158
+ export function summarizeOAuthCredentialSecret(secret, opts = {}) {
159
+ if (!secret)
160
+ return { state: "missing" };
161
+ try {
162
+ const parsed = parseOAuthCredentialSecret(secret);
163
+ const status = oauthCredentialStatus(parsed, opts);
164
+ return {
165
+ ...status,
166
+ hasRefreshToken: Boolean(parsed.refreshToken),
167
+ tokenEndpoint: parsed.tokenEndpoint,
168
+ clientId: parsed.clientId,
169
+ scope: parsed.scope,
170
+ scopes: parsed.scopes,
171
+ };
172
+ }
173
+ catch (err) {
174
+ return {
175
+ state: "invalid",
176
+ error: err instanceof Error ? err.message : String(err),
177
+ };
178
+ }
179
+ }
@@ -1,10 +1,11 @@
1
- import type { Credential } from "./types.js";
1
+ import { type Credential } from "./types.js";
2
2
  import { type EncryptionCipher } from "./cipher.js";
3
3
  export type CredentialScope = "user" | "project";
4
4
  export interface MaskedCredential extends Omit<Credential, "secret"> {
5
5
  hasSecret: boolean;
6
6
  /** 形如 `****abcd`,绝不含完整明文。 */
7
7
  secretHint?: string;
8
+ oauthStatus?: import("./types.js").OAuthCredentialPublicStatus;
8
9
  }
9
10
  /**
10
11
  * 两层凭证库,镜像 SettingsManager 的 user(~/.code-shell)/ project(<cwd>/.code-shell)
@@ -1,8 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { homedir } from "node:os";
4
+ import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
4
5
  import { getDefaultCredentialCipher } from "./cipher.js";
5
6
  import { logger } from "../logging/logger.js";
7
+ import { summarizeOAuthCredentialSecret } from "./oauth.js";
6
8
  /** 测试可经 process.env.HOME 覆盖(镜像 settings/manager.ts userHome)。 */
7
9
  function userHome() {
8
10
  return process.env.HOME ?? homedir();
@@ -92,11 +94,14 @@ export class CredentialStore {
92
94
  /** Upsert by id within a scope. */
93
95
  save(scope, cred) {
94
96
  const file = this.read(scope);
97
+ const safeCredential = credentialAllowsEnvExposure(cred.type)
98
+ ? cred
99
+ : { ...cred, exposeAsEnv: undefined };
95
100
  const idx = file.credentials.findIndex((c) => c.id === cred.id);
96
101
  if (idx >= 0)
97
- file.credentials[idx] = cred;
102
+ file.credentials[idx] = safeCredential;
98
103
  else
99
- file.credentials.push(cred);
104
+ file.credentials.push(safeCredential);
100
105
  this.write(scope, file);
101
106
  }
102
107
  /**
@@ -109,7 +114,12 @@ export class CredentialStore {
109
114
  const idx = file.credentials.findIndex((c) => c.id === id);
110
115
  if (idx < 0)
111
116
  return;
112
- file.credentials[idx] = { ...file.credentials[idx], ...fields };
117
+ const current = file.credentials[idx];
118
+ file.credentials[idx] = {
119
+ ...current,
120
+ ...fields,
121
+ ...(credentialAllowsEnvExposure(current.type) ? {} : { exposeAsEnv: undefined }),
122
+ };
113
123
  this.write(scope, file);
114
124
  }
115
125
  remove(scope, id) {
@@ -160,6 +170,8 @@ export class CredentialStore {
160
170
  : [this.read("user").credentials, this.read("project").credentials];
161
171
  for (const layer of layers) {
162
172
  for (const c of layer) {
173
+ if (!credentialAllowsEnvExposure(c.type))
174
+ continue;
163
175
  const name = c.exposeAsEnv?.trim();
164
176
  if (name && typeof c.secret === "string" && c.secret.length > 0) {
165
177
  out[name] = c.secret;
@@ -173,11 +185,10 @@ export class CredentialStore {
173
185
  const { secret, ...rest } = c;
174
186
  return {
175
187
  ...rest,
188
+ ...(credentialAllowsEnvExposure(c.type) ? {} : { exposeAsEnv: undefined }),
176
189
  hasSecret: typeof secret === "string" && secret.length > 0,
177
- // Reveal at most the last 4 chars — and ONLY when the secret is longer
178
- // than 4, so those chars aren't the whole secret. `"ab".slice(-4)` is
179
- // "ab", so a short secret would otherwise leak in full through the hint.
180
- secretHint: secret ? (secret.length > 4 ? `****${secret.slice(-4)}` : "****") : undefined,
190
+ secretHint: credentialSecretHint(c.type, secret),
191
+ ...(c.type === "oauth" ? { oauthStatus: summarizeOAuthCredentialSecret(secret) } : {}),
181
192
  };
182
193
  });
183
194
  }
@@ -3,7 +3,68 @@
3
3
  * - token/link 第一期已有;cookie 第二期改为「具名 cookie 凭证」进库
4
4
  * (用户主动按域拓取存,支持同域多账号),见 credential-use-gate 设计稿。
5
5
  */
6
- export type CredentialType = "token" | "link" | "cookie";
6
+ export type CredentialType = "token" | "link" | "cookie" | "oauth";
7
+ /**
8
+ * Only established single-value credentials may be copied into a process
9
+ * environment. OAuth records and cookie jars are structured multi-secret
10
+ * values and must remain behind the credential resolver boundary.
11
+ */
12
+ export declare function credentialAllowsEnvExposure(type: CredentialType): boolean;
13
+ /** Build a renderer-safe hint without deriving bytes from structured JSON. */
14
+ export declare function credentialSecretHint(type: CredentialType, secret: string | undefined): string | undefined;
15
+ export interface OAuthCredentialSecret {
16
+ /** Schema version for the JSON stored in Credential.secret. */
17
+ version?: 1;
18
+ /** Current access token. MCP HTTP auth sends this as Authorization: Bearer. */
19
+ accessToken: string;
20
+ /** Optional refresh token. Stored encrypted with the credential secret. */
21
+ refreshToken?: string;
22
+ /** ISO-8601 expiry time for accessToken. Missing means "unknown", not expired. */
23
+ expiresAt?: string;
24
+ /** Upstream token type, usually Bearer. Stored for round-trip/debugging. */
25
+ tokenType?: string;
26
+ /** Space-delimited OAuth scope string returned by many token endpoints. */
27
+ scope?: string;
28
+ /** Structured scopes used by first-party UI/catalog metadata. */
29
+ scopes?: string[];
30
+ /** Token endpoint to use when a future refresh flow is wired. */
31
+ tokenEndpoint?: string;
32
+ /** OAuth client id associated with this token. */
33
+ clientId?: string;
34
+ /** Optional confidential client secret, if the integration requires it. */
35
+ clientSecret?: string;
36
+ /** OAuth issuer discovered for the MCP protected resource. */
37
+ issuer?: string;
38
+ /** RFC 8707 resource indicator used for authorization/token requests. */
39
+ resource?: string;
40
+ /** Optional endpoint used to revoke the refresh/access token on logout. */
41
+ revocationEndpoint?: string;
42
+ /** Dynamic registration result, retained with the encrypted secret. */
43
+ clientRegistration?: {
44
+ clientId: string;
45
+ clientSecret?: string;
46
+ clientIdIssuedAt?: number;
47
+ clientSecretExpiresAt?: number;
48
+ };
49
+ }
50
+ export interface OAuthTokenResponse {
51
+ access_token: unknown;
52
+ refresh_token?: unknown;
53
+ expires_in?: unknown;
54
+ token_type?: unknown;
55
+ scope?: unknown;
56
+ }
57
+ export interface OAuthCredentialPublicStatus {
58
+ state: "valid" | "expired" | "missing" | "invalid";
59
+ expiresAt?: string;
60
+ expiresInMs?: number;
61
+ hasRefreshToken?: boolean;
62
+ tokenEndpoint?: string;
63
+ clientId?: string;
64
+ scope?: string;
65
+ scopes?: string[];
66
+ error?: string;
67
+ }
7
68
  export interface Credential {
8
69
  /**
9
70
  * 引用键,kebab-case,全局/项目两层内唯一。
@@ -19,6 +80,7 @@ export interface Credential {
19
80
  * 密文(UI 只显示掩码):
20
81
  * - token: token 值;
21
82
  * - link: client id/secret 等的 JSON 字符串;
83
+ * - oauth: OAuthCredentialSecret JSON 字符串(access/refresh/expiry/client info);
22
84
  * - cookie: 序列化的 cookie jar(JSON.stringify 的 ElectronCookieLike[])。
23
85
  */
24
86
  secret?: string;
@@ -48,6 +110,27 @@ export interface Credential {
48
110
  domain?: string;
49
111
  scope?: "domain" | "all";
50
112
  switchMode?: "clear" | "merge";
113
+ /** OAuth provider/integration id, e.g. "figma". */
114
+ oauthProvider?: string;
115
+ /** MCP server identity associated with an advanced OAuth login. */
116
+ mcpServerName?: string;
117
+ mcpServerUrl?: string;
118
+ issuer?: string;
119
+ resource?: string;
120
+ /** OAuth authorization endpoint, used by UI/login launchers. */
121
+ authUrl?: string;
122
+ /** OAuth token endpoint, duplicated from secret for non-secret UI display. */
123
+ tokenEndpoint?: string;
124
+ /** OAuth client id, duplicated from secret for non-secret UI display. */
125
+ clientId?: string;
126
+ /** OAuth scopes requested for this credential. */
127
+ scopes?: string[];
128
+ revocationEndpoint?: string;
129
+ /** Last successful refresh time. */
130
+ lastRefreshAt?: string;
131
+ /** Normalized refresh diagnostics; never contains endpoint response bodies. */
132
+ lastRefreshFailedAt?: string;
133
+ lastRefreshErrorCode?: "invalid_grant" | "network" | "server_error" | "invalid_response";
51
134
  };
52
135
  }
53
136
  export interface CredentialStoreFile {
@@ -1 +1,16 @@
1
- export {};
1
+ /**
2
+ * Only established single-value credentials may be copied into a process
3
+ * environment. OAuth records and cookie jars are structured multi-secret
4
+ * values and must remain behind the credential resolver boundary.
5
+ */
6
+ export function credentialAllowsEnvExposure(type) {
7
+ return type === "token" || type === "link";
8
+ }
9
+ /** Build a renderer-safe hint without deriving bytes from structured JSON. */
10
+ export function credentialSecretHint(type, secret) {
11
+ if (!secret)
12
+ return undefined;
13
+ if (type === "oauth" || type === "cookie")
14
+ return "****";
15
+ return secret.length > 4 ? `****${secret.slice(-4)}` : "****";
16
+ }