@ainecto/mcp 0.1.3 → 0.2.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/README.md +14 -12
- package/dist/bin/ainecto.js +162 -36
- package/dist/bin/ainecto.js.map +1 -1
- package/dist/bin/mcp.js +136 -10
- package/dist/bin/mcp.js.map +1 -1
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @ai-erd/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI-ERD MCP connector and first-party CLI.
|
|
4
4
|
|
|
5
5
|
This package exposes two entry points:
|
|
6
6
|
|
|
7
|
-
- `npx -y @
|
|
7
|
+
- `npx -y @ai-erd/mcp` or `ainecto mcp`: raw stdio-to-HTTP JSON-RPC proxy for AI-ERD MCP.
|
|
8
8
|
- `ainecto <command>`: human CLI for auth and low-level tool calls.
|
|
9
9
|
|
|
10
10
|
Phase 1 keeps the connector as a raw proxy. File, stdin, and inline JSON parsing are only available in first-party CLI commands.
|
|
@@ -41,12 +41,14 @@ Endpoint resolution priority:
|
|
|
41
41
|
1. `--endpoint <url>`
|
|
42
42
|
2. `AINECTO_MCP_ENDPOINT`
|
|
43
43
|
3. `--env dev`
|
|
44
|
-
4. production default, `https://
|
|
44
|
+
4. production default, `https://ai-erd.com/mcp`
|
|
45
|
+
|
|
46
|
+
Endpoint URLs must use `https:`. Plain `http:` is accepted only for localhost loopback targets such as `127.0.0.1`. When `AINECTO_TOKEN` is set, the CLI only sends it to the default prod/dev endpoints unless `AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN=1` is set for an explicitly trusted custom endpoint.
|
|
45
47
|
|
|
46
48
|
## Connector Mode
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
npx -y @
|
|
51
|
+
npx -y @ai-erd/mcp
|
|
50
52
|
ainecto mcp --env dev
|
|
51
53
|
```
|
|
52
54
|
|
|
@@ -61,7 +63,7 @@ Use the direct package configuration until the server is published to the offici
|
|
|
61
63
|
"mcpServers": {
|
|
62
64
|
"ainecto": {
|
|
63
65
|
"command": "npx",
|
|
64
|
-
"args": ["-y", "@
|
|
66
|
+
"args": ["-y", "@ai-erd/mcp"]
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
}
|
|
@@ -74,13 +76,13 @@ For a non-production endpoint, pass CLI flags through the package args:
|
|
|
74
76
|
"mcpServers": {
|
|
75
77
|
"ainecto-dev": {
|
|
76
78
|
"command": "npx",
|
|
77
|
-
"args": ["-y", "@
|
|
79
|
+
"args": ["-y", "@ai-erd/mcp", "--env", "dev"]
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
```
|
|
82
84
|
|
|
83
|
-
The planned official Registry server name is `io.github.codelives/ainecto`, backed by the public npm package `@
|
|
85
|
+
The planned official Registry server name is `io.github.codelives/ainecto`, backed by the public npm package `@ai-erd/mcp`. Registry publication requires the npm package version referenced by `server.json` to include a matching `mcpName` field in `package.json`.
|
|
84
86
|
|
|
85
87
|
## Catalog Sync
|
|
86
88
|
|
|
@@ -92,9 +94,9 @@ AINECTO_CATALOG_SYNC_TOKEN=... npm run sync:tools -- --env prod --check
|
|
|
92
94
|
AINECTO_CATALOG_SYNC_TOKEN=... npm run sync:tools -- --env dev --check
|
|
93
95
|
```
|
|
94
96
|
|
|
95
|
-
The checked-in generated catalogs are deterministic output from `tools/list`. The dev
|
|
97
|
+
The checked-in generated catalogs are deterministic output from `tools/list`. The prod and dev catalogs are live-synced snapshots from `https://ai-erd.com/mcp` and `https://dev.ainecto.com/mcp`. Presentation metadata lives separately in `src/core/catalog/enrichments.ts`.
|
|
96
98
|
|
|
97
|
-
`ainecto tools catalog` prints the local generated catalog.
|
|
99
|
+
`ainecto tools catalog` prints the local generated catalog. Rerun authenticated `sync:tools` when prod or dev `tools/list` changes.
|
|
98
100
|
|
|
99
101
|
## Generated Friendly Commands
|
|
100
102
|
|
|
@@ -126,8 +128,8 @@ The generated `request_upload_token` and `upload_attachments` paths remain raw M
|
|
|
126
128
|
|
|
127
129
|
```bash
|
|
128
130
|
npm pack
|
|
129
|
-
npx -y ./ainecto-mcp-0.1.
|
|
130
|
-
npm exec --package ./ainecto-mcp-0.1.
|
|
131
|
+
npx -y ./ainecto-mcp-0.1.4.tgz --help
|
|
132
|
+
npm exec --package ./ainecto-mcp-0.1.4.tgz -- ainecto --help
|
|
131
133
|
```
|
|
132
134
|
|
|
133
135
|
## Live Dev MCP Smoke
|
package/dist/bin/ainecto.js
CHANGED
|
@@ -26,6 +26,7 @@ var FileTokenStore = class {
|
|
|
26
26
|
}
|
|
27
27
|
async readFile() {
|
|
28
28
|
try {
|
|
29
|
+
await this.hardenExistingPermissions();
|
|
29
30
|
const raw = await readFile(this.filePath, "utf8");
|
|
30
31
|
const parsed = JSON.parse(raw);
|
|
31
32
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -49,6 +50,11 @@ var FileTokenStore = class {
|
|
|
49
50
|
await rename(tmpPath, this.filePath);
|
|
50
51
|
await chmod(this.filePath, 384);
|
|
51
52
|
}
|
|
53
|
+
async hardenExistingPermissions() {
|
|
54
|
+
const dirPath = dirname(this.filePath);
|
|
55
|
+
await chmodIfPermissive(dirPath, 448);
|
|
56
|
+
await chmodIfPermissive(this.filePath, 384);
|
|
57
|
+
}
|
|
52
58
|
};
|
|
53
59
|
function tokenKey(endpoint) {
|
|
54
60
|
return createHash("sha256").update(endpoint).digest("hex");
|
|
@@ -56,6 +62,19 @@ function tokenKey(endpoint) {
|
|
|
56
62
|
function isNodeError(error, code) {
|
|
57
63
|
return error instanceof Error && "code" in error && error.code === code;
|
|
58
64
|
}
|
|
65
|
+
async function chmodIfPermissive(path, mode) {
|
|
66
|
+
try {
|
|
67
|
+
const result = await stat(path);
|
|
68
|
+
if ((result.mode & 63) !== 0) {
|
|
69
|
+
await chmod(path, mode);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (isNodeError(error, "ENOENT")) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
|
|
60
79
|
// src/core/auth/oauth.ts
|
|
61
80
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
@@ -63,8 +82,70 @@ import { createServer } from "http";
|
|
|
63
82
|
import { spawn } from "child_process";
|
|
64
83
|
import { URLSearchParams } from "url";
|
|
65
84
|
|
|
85
|
+
// src/core/config/endpoints.ts
|
|
86
|
+
var DEFAULT_ENDPOINTS = {
|
|
87
|
+
prod: "https://ai-erd.com/mcp",
|
|
88
|
+
dev: "https://dev.ai-erd.com/mcp"
|
|
89
|
+
};
|
|
90
|
+
var LEGACY_DEFAULT_ENDPOINTS = [
|
|
91
|
+
"https://ainecto.com/mcp",
|
|
92
|
+
"https://dev.ainecto.com/mcp"
|
|
93
|
+
];
|
|
94
|
+
function resolveEndpoint(input = {}) {
|
|
95
|
+
const envVars = input.envVars ?? process.env;
|
|
96
|
+
const env = input.env ?? "prod";
|
|
97
|
+
if (input.endpoint) {
|
|
98
|
+
return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
|
|
99
|
+
}
|
|
100
|
+
if (envVars.AINECTO_MCP_ENDPOINT) {
|
|
101
|
+
return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
|
|
102
|
+
}
|
|
103
|
+
return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
|
|
104
|
+
}
|
|
105
|
+
function normalizeEndpoint(endpoint) {
|
|
106
|
+
const url = new URL(endpoint);
|
|
107
|
+
assertTrustedEndpointUrl(url, "MCP endpoint");
|
|
108
|
+
url.hash = "";
|
|
109
|
+
return url.toString().replace(/\/$/, "");
|
|
110
|
+
}
|
|
111
|
+
function isDefaultEndpoint(endpoint) {
|
|
112
|
+
const normalized = normalizeEndpoint(endpoint);
|
|
113
|
+
return Object.values(DEFAULT_ENDPOINTS).includes(normalized) || LEGACY_DEFAULT_ENDPOINTS.includes(normalized);
|
|
114
|
+
}
|
|
115
|
+
function assertTrustedEndpointUrl(value, label = "URL") {
|
|
116
|
+
const url = typeof value === "string" ? new URL(value) : value;
|
|
117
|
+
if (url.protocol === "https:") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);
|
|
124
|
+
}
|
|
125
|
+
function assertLoopbackRedirectUrl(value) {
|
|
126
|
+
const url = new URL(value);
|
|
127
|
+
if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw new Error("OAuth redirect URI must be an http localhost loopback URL.");
|
|
131
|
+
}
|
|
132
|
+
function isLoopbackHostname(hostname) {
|
|
133
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
134
|
+
}
|
|
135
|
+
function assertEnv(value) {
|
|
136
|
+
if (value === void 0) {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
if (value === "prod" || value === "dev") {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
|
|
143
|
+
}
|
|
144
|
+
|
|
66
145
|
// src/core/auth/clientRegistration.ts
|
|
67
146
|
async function registerPublicClient(registrationEndpoint, redirectUri, fetchImpl = fetch) {
|
|
147
|
+
assertTrustedEndpointUrl(registrationEndpoint, "OAuth client registration endpoint");
|
|
148
|
+
assertLoopbackRedirectUrl(redirectUri);
|
|
68
149
|
const response = await fetchImpl(registrationEndpoint, {
|
|
69
150
|
method: "POST",
|
|
70
151
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
@@ -107,6 +188,7 @@ var OAuthClient = class {
|
|
|
107
188
|
async getAccessToken() {
|
|
108
189
|
const envToken = this.envVars.AINECTO_TOKEN;
|
|
109
190
|
if (envToken) {
|
|
191
|
+
assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);
|
|
110
192
|
return envToken;
|
|
111
193
|
}
|
|
112
194
|
const stored = await this.options.tokenStore.load(this.options.endpoint);
|
|
@@ -140,6 +222,7 @@ var OAuthClient = class {
|
|
|
140
222
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
141
223
|
authorizeUrl.searchParams.set("resource", this.options.endpoint);
|
|
142
224
|
authorizeUrl.searchParams.set("state", state);
|
|
225
|
+
assertTrustedEndpointUrl(authorizeUrl, "OAuth authorization URL");
|
|
143
226
|
await this.openBrowserImpl(authorizeUrl.toString());
|
|
144
227
|
const code = await loopback.waitForCode();
|
|
145
228
|
const token = await exchangeToken(metadata.tokenEndpoint, {
|
|
@@ -193,18 +276,30 @@ function shouldRefresh(token, now = Date.now()) {
|
|
|
193
276
|
return token.expiresAt - now < 6e4;
|
|
194
277
|
}
|
|
195
278
|
async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
|
|
279
|
+
assertTrustedEndpointUrl(endpoint, "MCP endpoint");
|
|
196
280
|
const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);
|
|
281
|
+
assertTrustedEndpointUrl(resourceMetadataUrl, "OAuth protected resource metadata URL");
|
|
197
282
|
const resourceMetadata = await fetchJson(resourceMetadataUrl, fetchImpl);
|
|
198
283
|
const authorizationServers = resourceMetadata.authorization_servers;
|
|
199
284
|
if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== "string") {
|
|
200
285
|
throw new Error("OAuth protected resource metadata did not include authorization_servers.");
|
|
201
286
|
}
|
|
202
287
|
const authServer = authorizationServers[0];
|
|
288
|
+
assertTrustedEndpointUrl(authServer, "OAuth authorization server");
|
|
203
289
|
const metadataUrl = authServer.includes("/.well-known/") ? authServer : `${authServer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
|
|
290
|
+
assertTrustedEndpointUrl(metadataUrl, "OAuth authorization server metadata URL");
|
|
204
291
|
const metadata = await fetchJson(metadataUrl, fetchImpl);
|
|
205
292
|
if (typeof metadata.authorization_endpoint !== "string" || typeof metadata.token_endpoint !== "string") {
|
|
206
293
|
throw new Error("OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.");
|
|
207
294
|
}
|
|
295
|
+
assertTrustedEndpointUrl(metadata.authorization_endpoint, "OAuth authorization endpoint");
|
|
296
|
+
assertTrustedEndpointUrl(metadata.token_endpoint, "OAuth token endpoint");
|
|
297
|
+
if (typeof metadata.registration_endpoint === "string") {
|
|
298
|
+
assertTrustedEndpointUrl(metadata.registration_endpoint, "OAuth client registration endpoint");
|
|
299
|
+
}
|
|
300
|
+
if (typeof metadata.issuer === "string") {
|
|
301
|
+
assertTrustedEndpointUrl(metadata.issuer, "OAuth issuer");
|
|
302
|
+
}
|
|
208
303
|
const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);
|
|
209
304
|
if (!codeChallengeMethodsSupported.includes("S256")) {
|
|
210
305
|
throw new Error("OAuth authorization server metadata must support PKCE S256.");
|
|
@@ -219,20 +314,25 @@ async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
|
|
|
219
314
|
}
|
|
220
315
|
async function discoverResourceMetadataUrl(endpoint, fetchImpl) {
|
|
221
316
|
const challengeUrl = new URL(endpoint);
|
|
317
|
+
assertTrustedEndpointUrl(challengeUrl, "MCP endpoint");
|
|
222
318
|
const response = await fetchImpl(challengeUrl, { method: "GET" });
|
|
223
319
|
const challenge = response.headers.get("www-authenticate");
|
|
224
320
|
const fromChallenge = challenge ? parseBearerChallengeParam(challenge, "resource_metadata") : void 0;
|
|
225
321
|
if (fromChallenge) {
|
|
322
|
+
assertTrustedEndpointUrl(fromChallenge, "OAuth protected resource metadata URL");
|
|
226
323
|
return fromChallenge;
|
|
227
324
|
}
|
|
228
325
|
const origin = new URL(endpoint).origin;
|
|
229
|
-
|
|
326
|
+
const fallback = `${origin}/.well-known/oauth-protected-resource`;
|
|
327
|
+
assertTrustedEndpointUrl(fallback, "OAuth protected resource metadata URL");
|
|
328
|
+
return fallback;
|
|
230
329
|
}
|
|
231
330
|
function parseBearerChallengeParam(header, name) {
|
|
232
331
|
const pattern = new RegExp(`${name}="([^"]+)"`);
|
|
233
332
|
return pattern.exec(header)?.[1];
|
|
234
333
|
}
|
|
235
334
|
async function exchangeToken(tokenEndpoint, params, fetchImpl) {
|
|
335
|
+
assertTrustedEndpointUrl(tokenEndpoint, "OAuth token endpoint");
|
|
236
336
|
const response = await fetchImpl(tokenEndpoint, {
|
|
237
337
|
method: "POST",
|
|
238
338
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
@@ -311,48 +411,36 @@ function parseStringArray(value) {
|
|
|
311
411
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
312
412
|
}
|
|
313
413
|
async function fetchJson(url, fetchImpl) {
|
|
414
|
+
assertTrustedEndpointUrl(url, "OAuth metadata URL");
|
|
314
415
|
const response = await fetchImpl(url);
|
|
315
416
|
if (!response.ok) {
|
|
316
417
|
throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);
|
|
317
418
|
}
|
|
318
419
|
return response.json();
|
|
319
420
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
child.unref();
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// src/core/config/endpoints.ts
|
|
328
|
-
var DEFAULT_ENDPOINTS = {
|
|
329
|
-
prod: "https://ainecto.com/mcp",
|
|
330
|
-
dev: "https://dev.ainecto.com/mcp"
|
|
331
|
-
};
|
|
332
|
-
function resolveEndpoint(input = {}) {
|
|
333
|
-
const envVars = input.envVars ?? process.env;
|
|
334
|
-
const env = input.env ?? "prod";
|
|
335
|
-
if (input.endpoint) {
|
|
336
|
-
return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
|
|
421
|
+
function getBrowserOpenCommand(url, platform = process.platform) {
|
|
422
|
+
assertTrustedEndpointUrl(url, "OAuth authorization URL");
|
|
423
|
+
if (platform === "darwin") {
|
|
424
|
+
return { command: "open", args: [url] };
|
|
337
425
|
}
|
|
338
|
-
if (
|
|
339
|
-
return {
|
|
426
|
+
if (platform === "win32") {
|
|
427
|
+
return { command: "explorer.exe", args: [url] };
|
|
340
428
|
}
|
|
341
|
-
return {
|
|
429
|
+
return { command: "xdg-open", args: [url] };
|
|
342
430
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
431
|
+
async function openBrowser(url) {
|
|
432
|
+
const { command, args } = getBrowserOpenCommand(url);
|
|
433
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
434
|
+
child.unref();
|
|
347
435
|
}
|
|
348
|
-
function
|
|
349
|
-
if (
|
|
350
|
-
return
|
|
436
|
+
function assertEnvTokenEndpointAllowed(endpoint, envVars) {
|
|
437
|
+
if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
|
|
438
|
+
return;
|
|
351
439
|
}
|
|
352
|
-
if (
|
|
353
|
-
return
|
|
440
|
+
if (isDefaultEndpoint(endpoint)) {
|
|
441
|
+
return;
|
|
354
442
|
}
|
|
355
|
-
throw new Error(
|
|
443
|
+
throw new Error("AINECTO_TOKEN can only be used with the default prod/dev endpoints. Set AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN=1 only when you intentionally trust the custom endpoint.");
|
|
356
444
|
}
|
|
357
445
|
|
|
358
446
|
// src/core/input/payloadSource.ts
|
|
@@ -478,10 +566,11 @@ var McpRpcClient = class {
|
|
|
478
566
|
}
|
|
479
567
|
const text = await response.text();
|
|
480
568
|
if (!response.ok) {
|
|
481
|
-
throw new McpRpcError(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
569
|
+
throw new McpRpcError(
|
|
570
|
+
`MCP HTTP request failed with HTTP ${response.status}.`,
|
|
571
|
+
"MCP_HTTP_ERROR",
|
|
572
|
+
httpErrorDetails(response.status, text, this.options.envVars ?? process.env)
|
|
573
|
+
);
|
|
485
574
|
}
|
|
486
575
|
if (!text.trim()) {
|
|
487
576
|
return void 0;
|
|
@@ -513,6 +602,13 @@ function isJsonRpcResponse(value) {
|
|
|
513
602
|
function isRecord2(value) {
|
|
514
603
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
515
604
|
}
|
|
605
|
+
function httpErrorDetails(status, body, envVars) {
|
|
606
|
+
const details = { status };
|
|
607
|
+
if (envVars.AINECTO_CLI_DEBUG === "1" || envVars.AINECTO_DEBUG === "1") {
|
|
608
|
+
details.body = safeBodyPreview(body);
|
|
609
|
+
}
|
|
610
|
+
return details;
|
|
611
|
+
}
|
|
516
612
|
function safeBodyPreview(body) {
|
|
517
613
|
return body.length > 500 ? `${body.slice(0, 500)}...` : body;
|
|
518
614
|
}
|
|
@@ -549,12 +645,15 @@ function toErrorShape(error) {
|
|
|
549
645
|
error: {
|
|
550
646
|
code: maybe.code ?? "CLI_ERROR",
|
|
551
647
|
message: error.message,
|
|
552
|
-
details: maybe.details
|
|
648
|
+
details: redactSecrets(maybe.details)
|
|
553
649
|
}
|
|
554
650
|
};
|
|
555
651
|
}
|
|
556
652
|
return { ok: false, error: { code: "CLI_ERROR", message: String(error) } };
|
|
557
653
|
}
|
|
654
|
+
function redactSecrets(value) {
|
|
655
|
+
return redactSecretsInner(value, /* @__PURE__ */ new WeakSet());
|
|
656
|
+
}
|
|
558
657
|
function renderHuman(data) {
|
|
559
658
|
if (Array.isArray(data)) {
|
|
560
659
|
return data.map((item) => renderHuman(item)).join("\n");
|
|
@@ -564,6 +663,33 @@ function renderHuman(data) {
|
|
|
564
663
|
}
|
|
565
664
|
return JSON.stringify(data, null, 2);
|
|
566
665
|
}
|
|
666
|
+
function redactSecretsInner(value, seen) {
|
|
667
|
+
if (typeof value === "string") {
|
|
668
|
+
return redactSecretString(value);
|
|
669
|
+
}
|
|
670
|
+
if (Array.isArray(value)) {
|
|
671
|
+
return value.map((item) => redactSecretsInner(item, seen));
|
|
672
|
+
}
|
|
673
|
+
if (value && typeof value === "object") {
|
|
674
|
+
if (seen.has(value)) {
|
|
675
|
+
return "[circular]";
|
|
676
|
+
}
|
|
677
|
+
seen.add(value);
|
|
678
|
+
const output = {};
|
|
679
|
+
for (const [key, child] of Object.entries(value)) {
|
|
680
|
+
output[key] = isSecretKey(key) ? "[redacted]" : redactSecretsInner(child, seen);
|
|
681
|
+
}
|
|
682
|
+
return output;
|
|
683
|
+
}
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
function isSecretKey(key) {
|
|
687
|
+
const normalized = key.toLowerCase().replace(/-/g, "_");
|
|
688
|
+
return normalized === "authorization" || normalized.includes("access_token") || normalized.includes("refresh_token") || normalized.includes("client_secret") || normalized.includes("token");
|
|
689
|
+
}
|
|
690
|
+
function redactSecretString(value) {
|
|
691
|
+
return value.replace(/("(?:authorization|access_token|refresh_token|client_secret|token)"\s*:\s*")([^"]*)(")/gi, "$1[redacted]$3").replace(/((?:authorization|access_token|refresh_token|client_secret|token)=)([^&\s"']+)/gi, "$1[redacted]").replace(/(authorization:\s*bearer\s+)([^\s,}]+)/gi, "$1[redacted]").replace(/(bearer\s+)(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/gi, "$1[redacted]");
|
|
692
|
+
}
|
|
567
693
|
|
|
568
694
|
// src/core/catalog/generated.prod.ts
|
|
569
695
|
var generatedTools = [
|