dsh-mcp 1.3.0 → 1.5.0
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/CHANGELOG.en.md +31 -0
- package/CHANGELOG.md +31 -0
- package/README.en.md +72 -7
- package/README.md +60 -6
- package/lib/client.js +899 -284
- package/lib/index.js +340 -12
- package/lib/oauth.js +170 -50
- package/package.json +1 -1
- package/src/client/GlobalEnvEditor.module.css +204 -0
- package/src/client/GlobalEnvEditor.tsx +254 -0
- package/src/client/McpSettingsSection.module.css +67 -0
- package/src/client/McpSettingsSection.tsx +82 -18
- package/src/client/ServerForm.module.css +0 -47
- package/src/client/ServerForm.tsx +1 -55
- package/src/client/ServersJsonEditor.module.css +90 -0
- package/src/client/ServersJsonEditor.tsx +226 -0
- package/src/client/index.ts +21 -3
- package/src/client/locales.ts +54 -4
- package/src/client/mcp-store.ts +4 -43
- package/src/client/remote-contribution.js +163 -2
- package/src/client/types.ts +2 -0
- package/static/snapshot.en.jpg +0 -0
- package/static/snapshot.en.webp +0 -0
- package/static/snapshot.jpg +0 -0
- package/static/snapshot.webp +0 -0
package/lib/oauth.js
CHANGED
|
@@ -22,6 +22,9 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
22
22
|
/** Credential reference namespace prefix for per-server OAuth tokens. */
|
|
23
23
|
const OAUTH_REF_PREFIX = "DSH_MCP_OAUTH_";
|
|
24
24
|
|
|
25
|
+
/** Credential reference namespace prefix for per-server OAuth client info. */
|
|
26
|
+
const OAUTH_CLIENT_REF_PREFIX = "DSH_MCP_OAUTH_CLIENT_";
|
|
27
|
+
|
|
25
28
|
/** Short stable hash so sanitized server ids cannot collide. */
|
|
26
29
|
function shortHash(value) {
|
|
27
30
|
let hash = 0;
|
|
@@ -32,16 +35,24 @@ function shortHash(value) {
|
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
|
-
* One server's OAuth token credential reference.
|
|
36
|
-
*
|
|
37
38
|
* Credential ref names must match /^[A-Za-z_][A-Za-z0-9_]*$/, but managed
|
|
38
39
|
* server ids may contain hyphens, slashes, spaces, etc.; sanitize the id and
|
|
39
40
|
* append a short stable hash of the original so distinct ids cannot collide
|
|
40
41
|
* (e.g. "a-b" and "a_b").
|
|
41
42
|
*/
|
|
42
|
-
function
|
|
43
|
+
function oauthRef(prefix, serverId) {
|
|
43
44
|
const safe = serverId.replace(/[^A-Za-z0-9_]/g, "_");
|
|
44
|
-
return credentialRef(`${
|
|
45
|
+
return credentialRef(`${prefix}${safe}_${shortHash(serverId)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One server's OAuth token credential reference. */
|
|
49
|
+
function oauthTokenRef(serverId) {
|
|
50
|
+
return oauthRef(OAUTH_REF_PREFIX, serverId);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One server's registered OAuth client credential reference. */
|
|
54
|
+
function oauthClientRef(serverId) {
|
|
55
|
+
return oauthRef(OAUTH_CLIENT_REF_PREFIX, serverId);
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
/** Open the system browser to one URL, best effort. */
|
|
@@ -55,6 +66,54 @@ function openBrowser(url) {
|
|
|
55
66
|
child.unref();
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Extract the `exp` claim from a JWT access token, when it is a JWT.
|
|
71
|
+
* @param accessToken - The access token string.
|
|
72
|
+
* @returns the expiry epoch seconds, or undefined when not a JWT / no exp.
|
|
73
|
+
*/
|
|
74
|
+
function jwtExp(accessToken) {
|
|
75
|
+
try {
|
|
76
|
+
const payload = JSON.parse(
|
|
77
|
+
Buffer.from(String(accessToken).split(".")[1] ?? "", "base64url").toString("utf8")
|
|
78
|
+
);
|
|
79
|
+
return typeof payload.exp === "number" ? payload.exp : undefined;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derive a stable loopback port from the server id. The redirect URI must
|
|
87
|
+
* stay identical across processes because the registered OAuth client's
|
|
88
|
+
* redirect_uris are fixed at registration time; a random port per process
|
|
89
|
+
* would make the authorization server reject the redirect with
|
|
90
|
+
* `redirect_uri_mismatch`.
|
|
91
|
+
* @param serverId - Managed server id.
|
|
92
|
+
* @returns a stable port in [3100, 23099].
|
|
93
|
+
*/
|
|
94
|
+
function stablePort(serverId) {
|
|
95
|
+
let hash = 0;
|
|
96
|
+
for (let i = 0; i < serverId.length; i += 1) {
|
|
97
|
+
hash = (hash * 31 + serverId.charCodeAt(i)) >>> 0;
|
|
98
|
+
}
|
|
99
|
+
return 3100 + (hash % 20000);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Serialize authorization flows per server: the loopback port is stable per
|
|
104
|
+
* server, so two concurrent flows (e.g. mount and a test connection) would
|
|
105
|
+
* both try to listen on the same port and one fails with EADDRINUSE. Queue
|
|
106
|
+
* them so only one browser authorization runs at a time per server.
|
|
107
|
+
*/
|
|
108
|
+
const authLocks = /* @__PURE__ */ new Map();
|
|
109
|
+
|
|
110
|
+
function withAuthLock(serverId, fn) {
|
|
111
|
+
const previous = authLocks.get(serverId) ?? Promise.resolve();
|
|
112
|
+
const run = previous.then(fn, fn);
|
|
113
|
+
authLocks.set(serverId, run.then(() => {}, () => {}));
|
|
114
|
+
return run;
|
|
115
|
+
}
|
|
116
|
+
|
|
58
117
|
/**
|
|
59
118
|
* Build one OAuthClientProvider for a managed server.
|
|
60
119
|
* @param serverId - Managed server id; keys the stored token.
|
|
@@ -64,7 +123,7 @@ function openBrowser(url) {
|
|
|
64
123
|
export function createOAuthProvider(serverId, ctx) {
|
|
65
124
|
let codeVerifierValue = null;
|
|
66
125
|
let savedClientInfo = null;
|
|
67
|
-
const port =
|
|
126
|
+
const port = stablePort(serverId);
|
|
68
127
|
const redirectUrl = `http://127.0.0.1:${port}/callback`;
|
|
69
128
|
const log = ctx?.logger ?? console;
|
|
70
129
|
const tag = `mcp-oauth(${serverId})`;
|
|
@@ -78,19 +137,70 @@ export function createOAuthProvider(serverId, ctx) {
|
|
|
78
137
|
redirect_uris: [redirectUrl],
|
|
79
138
|
},
|
|
80
139
|
async clientInformation() {
|
|
81
|
-
return savedClientInfo;
|
|
140
|
+
if (savedClientInfo !== null) return savedClientInfo;
|
|
141
|
+
const hit = await ctx.credentials.resolve(oauthClientRef(serverId));
|
|
142
|
+
if (hit === undefined) return undefined;
|
|
143
|
+
try {
|
|
144
|
+
const info = JSON.parse(hit.value);
|
|
145
|
+
// The persisted client's registered redirect_uris must cover this
|
|
146
|
+
// loopback URI; otherwise the authorization server rejects the
|
|
147
|
+
// redirect with `redirect_uri_mismatch`. Drop it so the SDK
|
|
148
|
+
// re-registers a client for the current (stable) redirect URI.
|
|
149
|
+
if (!Array.isArray(info.redirect_uris) || !info.redirect_uris.includes(redirectUrl)) {
|
|
150
|
+
log.info(`${tag}: persisted client redirect_uris mismatch, re-registering`);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return info;
|
|
154
|
+
} catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
82
157
|
},
|
|
83
158
|
async saveClientInformation(info) {
|
|
84
159
|
savedClientInfo = info;
|
|
160
|
+
// Persist the registered client (client_id) with the tokens so a later
|
|
161
|
+
// token refresh uses the same client. Without this, every process
|
|
162
|
+
// re-registers a fresh client and the server rejects refresh with
|
|
163
|
+
// "client_id mismatch".
|
|
164
|
+
if (info === undefined || info === null) {
|
|
165
|
+
await ctx.credentials.unset(oauthClientRef(serverId));
|
|
166
|
+
} else {
|
|
167
|
+
await ctx.credentials.set(oauthClientRef(serverId), JSON.stringify(info));
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
async invalidateCredentials(credentialType) {
|
|
171
|
+
// The SDK calls this when a refresh/registration fails with an
|
|
172
|
+
// unrecoverable error (InvalidClient / UnauthorizedClient / InvalidGrant)
|
|
173
|
+
// so the next attempt starts a fresh authorization flow. Clearing the
|
|
174
|
+
// stored tokens is what lets that flow actually open the browser again.
|
|
175
|
+
if (credentialType === undefined || credentialType === "all" || credentialType === "tokens") {
|
|
176
|
+
await ctx.credentials.unset(oauthTokenRef(serverId));
|
|
177
|
+
}
|
|
178
|
+
if (credentialType === undefined || credentialType === "all") {
|
|
179
|
+
savedClientInfo = null;
|
|
180
|
+
await ctx.credentials.unset(oauthClientRef(serverId));
|
|
181
|
+
}
|
|
182
|
+
log.info(`${tag}: credentials invalidated (${String(credentialType)})`);
|
|
85
183
|
},
|
|
86
184
|
async tokens() {
|
|
87
185
|
const hit = await ctx.credentials.resolve(oauthTokenRef(serverId));
|
|
88
186
|
if (hit === undefined) return undefined;
|
|
187
|
+
let tokens;
|
|
89
188
|
try {
|
|
90
|
-
|
|
189
|
+
tokens = JSON.parse(hit.value);
|
|
91
190
|
} catch {
|
|
92
191
|
return undefined;
|
|
93
192
|
}
|
|
193
|
+
// An expired access token makes the SDK try refresh; if the refresh
|
|
194
|
+
// token is also dead (short-lived servers), the SDK throws
|
|
195
|
+
// InvalidTokenError and never re-authorizes. Pre-expire it here so the
|
|
196
|
+
// SDK falls through to a fresh browser authorization flow instead.
|
|
197
|
+
const exp = jwtExp(tokens.access_token);
|
|
198
|
+
if (exp !== undefined && Date.now() / 1000 >= exp) {
|
|
199
|
+
log.info(`${tag}: stored access token expired, clearing for re-authorization`);
|
|
200
|
+
await ctx.credentials.unset(oauthTokenRef(serverId));
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
return tokens;
|
|
94
204
|
},
|
|
95
205
|
async saveTokens(tokens) {
|
|
96
206
|
await ctx.credentials.set(oauthTokenRef(serverId), JSON.stringify(tokens));
|
|
@@ -105,51 +215,61 @@ export function createOAuthProvider(serverId, ctx) {
|
|
|
105
215
|
return codeVerifierValue;
|
|
106
216
|
},
|
|
107
217
|
async redirectToAuthorization(authorizationUrl) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
218
|
+
// Serialize per server: the loopback port is stable, so concurrent
|
|
219
|
+
// flows (mount + test) would collide on the same port. Queue them.
|
|
220
|
+
return withAuthLock(serverId, async () => {
|
|
221
|
+
let resolveResult;
|
|
222
|
+
const resultPromise = new Promise((resolve) => {
|
|
223
|
+
resolveResult = resolve;
|
|
224
|
+
});
|
|
225
|
+
const server = createServer((req, res) => {
|
|
226
|
+
const url = new URL(req.url, redirectUrl);
|
|
227
|
+
const code = url.searchParams.get("code");
|
|
228
|
+
const error = url.searchParams.get("error");
|
|
229
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
230
|
+
if (error) {
|
|
231
|
+
res.end(`授权失败:${error},可关闭此页面并返回 DSH。`);
|
|
232
|
+
log.error(`${tag}: callback rejected: ${error}`);
|
|
233
|
+
resolveResult({ error });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
res.end("授权成功,可关闭此页面并返回 DSH。");
|
|
237
|
+
log.info(`${tag}: authorization callback received (code)`);
|
|
238
|
+
resolveResult({ code });
|
|
239
|
+
});
|
|
240
|
+
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
|
|
241
|
+
log.info(`${tag}: opening browser for authorization: ${authorizationUrl}`);
|
|
242
|
+
openBrowser(authorizationUrl.toString());
|
|
243
|
+
try {
|
|
244
|
+
const result = await resultPromise;
|
|
245
|
+
if (result.error) throw new Error(`OAuth 授权失败:${result.error}`);
|
|
246
|
+
const authServerUrl = new URL(authorizationUrl.origin);
|
|
247
|
+
log.info(`${tag}: discovering authorization server metadata at ${authServerUrl}`);
|
|
248
|
+
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
|
249
|
+
log.info(`${tag}: exchanging authorization code for tokens`);
|
|
250
|
+
// Use the provider accessors, not the closure fields: when the
|
|
251
|
+
// client was loaded from persistence the SDK never calls
|
|
252
|
+
// saveClientInformation, so the closure copy is null and the token
|
|
253
|
+
// request would lack client_id (CAS: "... are required").
|
|
254
|
+
const clientInformation = await this.clientInformation();
|
|
255
|
+
const codeVerifier = await this.codeVerifier();
|
|
256
|
+
const tokens = await exchangeAuthorization(authServerUrl, {
|
|
257
|
+
metadata,
|
|
258
|
+
clientInformation,
|
|
259
|
+
authorizationCode: result.code,
|
|
260
|
+
codeVerifier,
|
|
261
|
+
redirectUri: redirectUrl,
|
|
262
|
+
});
|
|
263
|
+
await this.saveTokens(tokens);
|
|
264
|
+
log.info(`${tag}: tokens saved`);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
log.error(`${tag}: OAuth exchange failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
267
|
+
throw error;
|
|
268
|
+
} finally {
|
|
269
|
+
server.close();
|
|
270
|
+
log.info(`${tag}: callback server closed`);
|
|
122
271
|
}
|
|
123
|
-
res.end("授权成功,可关闭此页面并返回 DSH。");
|
|
124
|
-
log.info(`${tag}: authorization callback received (code)`);
|
|
125
|
-
resolveResult({ code });
|
|
126
272
|
});
|
|
127
|
-
await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
|
|
128
|
-
log.info(`${tag}: opening browser for authorization: ${authorizationUrl}`);
|
|
129
|
-
openBrowser(authorizationUrl.toString());
|
|
130
|
-
try {
|
|
131
|
-
const result = await resultPromise;
|
|
132
|
-
if (result.error) throw new Error(`OAuth 授权失败:${result.error}`);
|
|
133
|
-
const authServerUrl = new URL(authorizationUrl.origin);
|
|
134
|
-
log.info(`${tag}: discovering authorization server metadata at ${authServerUrl}`);
|
|
135
|
-
const metadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
|
136
|
-
log.info(`${tag}: exchanging authorization code for tokens`);
|
|
137
|
-
const tokens = await exchangeAuthorization(authServerUrl, {
|
|
138
|
-
metadata,
|
|
139
|
-
clientInformation: savedClientInfo,
|
|
140
|
-
authorizationCode: result.code,
|
|
141
|
-
codeVerifier: codeVerifierValue,
|
|
142
|
-
redirectUri: redirectUrl,
|
|
143
|
-
});
|
|
144
|
-
await this.saveTokens(tokens);
|
|
145
|
-
log.info(`${tag}: tokens saved`);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
log.error(`${tag}: OAuth exchange failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
-
throw error;
|
|
149
|
-
} finally {
|
|
150
|
-
server.close();
|
|
151
|
-
log.info(`${tag}: callback server closed`);
|
|
152
|
-
}
|
|
153
273
|
},
|
|
154
274
|
};
|
|
155
275
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "MCP 管理界面 + tool search:可视化配置/启停/刷新 MCP 服务器与工具勾选,按需检索(tool search)热注入;工具列表稳定命中缓存、不撑爆上下文。DeepSeek Harness plugin: MCP management UI + tool search — stable tool list, cache-friendly, no context bloat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
.panel {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
6
|
+
border-radius: 10px;
|
|
7
|
+
padding: 10px 14px;
|
|
8
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.hint,
|
|
12
|
+
.muted {
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-size: 12px;
|
|
15
|
+
line-height: 18px;
|
|
16
|
+
color: var(--dsw-alias-label-tertiary);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.row {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.8fr) auto auto;
|
|
22
|
+
align-items: center;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.row input {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
29
|
+
border-radius: 6px;
|
|
30
|
+
padding: 5px 8px;
|
|
31
|
+
outline: none;
|
|
32
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
33
|
+
color: var(--dsw-alias-label-primary);
|
|
34
|
+
font: inherit;
|
|
35
|
+
font-size: 13px;
|
|
36
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.row input:focus-visible {
|
|
40
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
41
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.name {
|
|
45
|
+
width: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.value {
|
|
49
|
+
width: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.secretToggle {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 4px;
|
|
56
|
+
color: var(--dsw-alias-label-secondary);
|
|
57
|
+
font-size: 12px;
|
|
58
|
+
line-height: 18px;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
user-select: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.secretToggle input {
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.secretToggle.secretOn {
|
|
69
|
+
color: var(--dsw-alias-brand-primary);
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.danger {
|
|
74
|
+
border: 1px solid transparent !important;
|
|
75
|
+
background: transparent !important;
|
|
76
|
+
color: var(--dsw-alias-state-error-primary) !important;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.overlay {
|
|
81
|
+
position: fixed;
|
|
82
|
+
inset: 0;
|
|
83
|
+
z-index: 1000;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
background: rgba(0, 0, 0, 0.4);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dialog {
|
|
91
|
+
display: flex;
|
|
92
|
+
flex-direction: column;
|
|
93
|
+
gap: 8px;
|
|
94
|
+
width: min(520px, calc(100vw - 48px));
|
|
95
|
+
max-height: 80vh;
|
|
96
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
97
|
+
border-radius: 12px;
|
|
98
|
+
padding: 16px;
|
|
99
|
+
background: var(--dsw-alias-bg-overlay);
|
|
100
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dialogTitle {
|
|
104
|
+
margin: 0;
|
|
105
|
+
font-size: 15px;
|
|
106
|
+
line-height: 22px;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
color: var(--dsw-alias-label-primary);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.bulkText {
|
|
112
|
+
width: 100%;
|
|
113
|
+
box-sizing: border-box;
|
|
114
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
115
|
+
border-radius: 8px;
|
|
116
|
+
padding: 8px 10px;
|
|
117
|
+
outline: none;
|
|
118
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
119
|
+
color: var(--dsw-alias-label-primary);
|
|
120
|
+
font: inherit;
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
line-height: 18px;
|
|
123
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
124
|
+
resize: vertical;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.bulkText:focus-visible {
|
|
128
|
+
border-color: var(--dsw-alias-brand-primary);
|
|
129
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.retryRow {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 10px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.retryRow button {
|
|
139
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
140
|
+
border-radius: 6px;
|
|
141
|
+
padding: 3px 10px;
|
|
142
|
+
background: transparent;
|
|
143
|
+
color: var(--dsw-alias-label-primary);
|
|
144
|
+
font: inherit;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
line-height: 18px;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.retryRow button:hover {
|
|
151
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.error {
|
|
155
|
+
margin: 0;
|
|
156
|
+
color: var(--dsw-alias-state-error-primary);
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
line-height: 18px;
|
|
159
|
+
word-break: break-all;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.notice {
|
|
163
|
+
margin: 0;
|
|
164
|
+
color: var(--dsw-alias-state-success-primary);
|
|
165
|
+
font-size: 12px;
|
|
166
|
+
line-height: 18px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.actions {
|
|
170
|
+
display: flex;
|
|
171
|
+
justify-content: flex-end;
|
|
172
|
+
gap: 8px;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.actions button {
|
|
176
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
177
|
+
border-radius: 6px;
|
|
178
|
+
padding: 5px 12px;
|
|
179
|
+
background: transparent;
|
|
180
|
+
color: var(--dsw-alias-label-primary);
|
|
181
|
+
font: inherit;
|
|
182
|
+
font-size: 13px;
|
|
183
|
+
line-height: 20px;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.actions button:hover {
|
|
188
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.actions button:disabled {
|
|
192
|
+
cursor: not-allowed;
|
|
193
|
+
opacity: 0.55;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.actions .primary {
|
|
197
|
+
border-color: var(--dsw-alias-button-primary-fill);
|
|
198
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
199
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.actions .primary:hover {
|
|
203
|
+
background: var(--dsw-alias-button-primary-hover);
|
|
204
|
+
}
|