@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.
- package/dist/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +4 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +64 -4
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
- package/dist/cc-orchestrator/external-agent-driver.js +202 -38
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +96 -13
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +11 -1
- package/dist/credentials/access.js +77 -1
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/oauth.d.ts +25 -0
- package/dist/credentials/oauth.js +179 -0
- package/dist/credentials/store.d.ts +2 -1
- package/dist/credentials/store.js +19 -8
- package/dist/credentials/types.d.ts +84 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +67 -34
- package/dist/engine/engine.js +448 -247
- package/dist/engine/goal.d.ts +19 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/input-attachments.js +156 -13
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +48 -1
- package/dist/engine/turn-loop.js +307 -37
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +44 -2
- package/dist/hooks/goal-stop-hook.js +775 -52
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/preset/index.js +14 -4
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +291 -73
- package/dist/protocol/types.d.ts +36 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +396 -27
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
- package/dist/tool-system/builtin/background-jobs.js +109 -7
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
- package/dist/tool-system/builtin/drive-claude-code.js +307 -20
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +60 -5
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +35 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +20 -2
- package/dist/tool-system/mcp-manager.js +111 -12
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +38 -7
- 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 {
|
|
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
|
-
*
|
|
81
|
-
*
|
|
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
|
|
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 ? (
|
|
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
|
|
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
|
|
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] =
|
|
102
|
+
file.credentials[idx] = safeCredential;
|
|
98
103
|
else
|
|
99
|
-
file.credentials.push(
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
+
}
|