toolcraft-openapi 0.0.22 → 0.0.24
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 +2 -3
- package/dist/auth/bearer-token-auth.js +12 -3
- package/dist/auth/types.d.ts +1 -1
- package/dist/bin/generate.d.ts +1 -1
- package/dist/bin/generate.js +46 -21
- package/dist/generate.js +6 -2
- package/dist/http.js +29 -17
- package/dist/interpreter.js +12 -3
- package/dist/mock/fetch.js +22 -5
- package/dist/network-error.js +5 -3
- package/dist/redaction.d.ts +3 -0
- package/dist/redaction.js +38 -0
- package/node_modules/@poe-code/design-system/dist/acp/components.js +3 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/browser.js +6 -1
- package/node_modules/@poe-code/design-system/dist/components/color.js +9 -8
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +3 -2
- package/node_modules/@poe-code/design-system/dist/components/detail-card.d.ts +22 -0
- package/node_modules/@poe-code/design-system/dist/components/detail-card.js +69 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +88 -11
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +1 -1
- package/node_modules/@poe-code/design-system/dist/components/index.js +1 -1
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +82 -5
- package/node_modules/@poe-code/design-system/dist/components/template.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/components/template.js +198 -32
- package/node_modules/@poe-code/design-system/dist/components/text.js +29 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +2 -2
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +77 -32
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +28 -5
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +45 -28
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal-width.js +71 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/events.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +32 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +3 -0
- package/node_modules/@poe-code/design-system/dist/explorer/runtime.js +57 -6
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +12 -15
- package/node_modules/@poe-code/design-system/dist/index.d.ts +3 -1
- package/node_modules/@poe-code/design-system/dist/index.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +2 -1
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +8 -5
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +1 -1
- package/node_modules/@poe-code/design-system/dist/static/menu.js +8 -2
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +10 -4
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +9 -2
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +19 -2
- package/node_modules/@poe-code/design-system/package.json +2 -1
- package/node_modules/auth-store/dist/create-secret-store.js +4 -1
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +7 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +69 -7
- package/node_modules/auth-store/dist/index.d.ts +1 -1
- package/node_modules/auth-store/dist/keychain-store.d.ts +4 -1
- package/node_modules/auth-store/dist/keychain-store.js +18 -16
- package/node_modules/auth-store/dist/provider-store.d.ts +5 -1
- package/node_modules/auth-store/dist/provider-store.js +55 -7
- package/node_modules/auth-store/dist/types.d.ts +3 -1
- package/node_modules/auth-store/package.json +2 -1
- package/package.json +3 -3
- package/dist/lock.d.ts +0 -14
- package/dist/lock.js +0 -152
package/README.md
CHANGED
|
@@ -19,12 +19,11 @@ const auth = bearerTokenAuth({
|
|
|
19
19
|
## Generator CLI
|
|
20
20
|
|
|
21
21
|
`toolcraft-openapi-generate` reads an OpenAPI document from disk or a URL, writes generated
|
|
22
|
-
command files
|
|
22
|
+
command files.
|
|
23
23
|
|
|
24
24
|
- `--input <path-or-url>` — OpenAPI document to read. Defaults to `openapi.json`.
|
|
25
25
|
- `--output <dir>` — directory for generated files. Defaults to `src/generated`.
|
|
26
|
-
- `--
|
|
27
|
-
- `--check` — exits non-zero when generated files or `openapi.lock` would change.
|
|
26
|
+
- `--check` — exits non-zero when generated files would change.
|
|
28
27
|
|
|
29
28
|
### CI drift check
|
|
30
29
|
|
|
@@ -24,22 +24,26 @@ export function bearerTokenAuth(options) {
|
|
|
24
24
|
account: KEYCHAIN_ACCOUNT,
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
|
+
let lastResolvedToken = null;
|
|
27
28
|
async function resolveToken() {
|
|
28
29
|
const envToken = normalizeToken(process.env[options.envVar]);
|
|
29
30
|
if (envToken) {
|
|
30
|
-
|
|
31
|
+
lastResolvedToken = {
|
|
31
32
|
token: envToken,
|
|
32
33
|
tokenSource: `env (${options.envVar})`,
|
|
33
34
|
};
|
|
35
|
+
return lastResolvedToken;
|
|
34
36
|
}
|
|
35
37
|
const storedToken = normalizeToken(await store.get());
|
|
36
38
|
if (!storedToken) {
|
|
39
|
+
lastResolvedToken = null;
|
|
37
40
|
return null;
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
lastResolvedToken = {
|
|
40
43
|
token: storedToken,
|
|
41
44
|
tokenSource: backend,
|
|
42
45
|
};
|
|
46
|
+
return lastResolvedToken;
|
|
43
47
|
}
|
|
44
48
|
const loginCommand = defineCommand({
|
|
45
49
|
name: "login",
|
|
@@ -122,7 +126,12 @@ export function bearerTokenAuth(options) {
|
|
|
122
126
|
}
|
|
123
127
|
throw new UserError(`Run '${commandPrefix} login' first.`);
|
|
124
128
|
},
|
|
125
|
-
async invalidate() {
|
|
129
|
+
async invalidate(token) {
|
|
130
|
+
if (token !== undefined &&
|
|
131
|
+
lastResolvedToken?.token === token &&
|
|
132
|
+
lastResolvedToken.tokenSource.startsWith("env (")) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
126
135
|
await store.delete();
|
|
127
136
|
},
|
|
128
137
|
commands: [defineGroup({
|
package/dist/auth/types.d.ts
CHANGED
package/dist/bin/generate.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ interface GenerateCliFileSystem {
|
|
|
8
8
|
rm(targetPath: string, options?: {
|
|
9
9
|
force?: boolean;
|
|
10
10
|
}): Promise<void>;
|
|
11
|
+
realpath(targetPath: string): Promise<string>;
|
|
11
12
|
stat(targetPath: string): Promise<{
|
|
12
13
|
isDirectory(): boolean;
|
|
13
14
|
}>;
|
|
@@ -26,7 +27,6 @@ interface GenerateCliServices {
|
|
|
26
27
|
interface GenerateCliOptions {
|
|
27
28
|
check: boolean;
|
|
28
29
|
input: string;
|
|
29
|
-
lockPath: string;
|
|
30
30
|
outputDir: string;
|
|
31
31
|
}
|
|
32
32
|
interface SyncGeneratedClientResult {
|
package/dist/bin/generate.js
CHANGED
|
@@ -6,12 +6,10 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { UserError } from "toolcraft";
|
|
8
8
|
import { generate } from "../generate.js";
|
|
9
|
-
import { readOpenApiLock, writeOpenApiLock } from "../lock.js";
|
|
10
9
|
import { parseOpenApiDocument, readOpenApiSourceText } from "../spec-source.js";
|
|
11
10
|
const DEFAULT_OPTIONS = {
|
|
12
11
|
check: false,
|
|
13
12
|
input: "openapi.json",
|
|
14
|
-
lockPath: "openapi.lock",
|
|
15
13
|
outputDir: "src/generated"
|
|
16
14
|
};
|
|
17
15
|
const HELP_TEXT = `Usage: toolcraft-openapi-generate [options]
|
|
@@ -19,8 +17,7 @@ const HELP_TEXT = `Usage: toolcraft-openapi-generate [options]
|
|
|
19
17
|
Options:
|
|
20
18
|
--input <path-or-url> OpenAPI document to read (default: openapi.json)
|
|
21
19
|
--output <dir> Directory for generated command files (default: src/generated)
|
|
22
|
-
--
|
|
23
|
-
--check Exit non-zero if generated output or lock file would change
|
|
20
|
+
--check Exit non-zero if generated output would change
|
|
24
21
|
-h, --help Show this help text
|
|
25
22
|
`;
|
|
26
23
|
export async function runGenerateCli(argv = process.argv, services = {
|
|
@@ -73,8 +70,6 @@ export async function syncGeneratedClient(options, services) {
|
|
|
73
70
|
const document = parseOpenApiDocument(sourceText, options.input);
|
|
74
71
|
const generatedFiles = generate(document, { specSha });
|
|
75
72
|
const outputDir = path.resolve(services.cwd, options.outputDir);
|
|
76
|
-
const lockPath = path.resolve(services.cwd, options.lockPath);
|
|
77
|
-
const currentLock = await readOpenApiLock(services.fs, lockPath);
|
|
78
73
|
const currentFiles = await readGeneratedFiles(services.fs, outputDir);
|
|
79
74
|
const desiredFiles = new Map([
|
|
80
75
|
...generatedFiles.map((file) => [path.resolve(outputDir, file.path), file.contents]),
|
|
@@ -82,11 +77,16 @@ export async function syncGeneratedClient(options, services) {
|
|
|
82
77
|
]);
|
|
83
78
|
const updatedFiles = collectUpdatedFiles(currentFiles, desiredFiles);
|
|
84
79
|
const deletedFiles = collectDeletedFiles(currentFiles, desiredFiles);
|
|
85
|
-
const drifted =
|
|
80
|
+
const drifted = updatedFiles.length > 0 || deletedFiles.length > 0;
|
|
86
81
|
if (!options.check && drifted) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
try {
|
|
83
|
+
await writeGeneratedFiles(services.fs, outputDir, updatedFiles);
|
|
84
|
+
await deleteGeneratedFiles(services.fs, deletedFiles);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await restoreGeneratedFiles(services.fs, outputDir, currentFiles, updatedFiles, deletedFiles);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
return {
|
|
92
92
|
drifted,
|
|
@@ -106,7 +106,7 @@ function parseGenerateCliArgs(argv) {
|
|
|
106
106
|
options.check = true;
|
|
107
107
|
continue;
|
|
108
108
|
}
|
|
109
|
-
if (argument === "--input" || argument === "--output"
|
|
109
|
+
if (argument === "--input" || argument === "--output") {
|
|
110
110
|
const value = argv[index + 1];
|
|
111
111
|
if (value === undefined) {
|
|
112
112
|
throw new UserError(`Missing value for ${JSON.stringify(argument)}.`);
|
|
@@ -123,10 +123,6 @@ function parseGenerateCliArgs(argv) {
|
|
|
123
123
|
assignOptionValue(options, "--output", argument.slice("--output=".length));
|
|
124
124
|
continue;
|
|
125
125
|
}
|
|
126
|
-
if (argument.startsWith("--lock=")) {
|
|
127
|
-
assignOptionValue(options, "--lock", argument.slice("--lock=".length));
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
126
|
throw new UserError(`Unknown argument ${JSON.stringify(argument)}.`);
|
|
131
127
|
}
|
|
132
128
|
return options;
|
|
@@ -139,11 +135,7 @@ function assignOptionValue(options, argument, value) {
|
|
|
139
135
|
options.input = value;
|
|
140
136
|
return;
|
|
141
137
|
}
|
|
142
|
-
|
|
143
|
-
options.outputDir = value;
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
options.lockPath = value;
|
|
138
|
+
options.outputDir = value;
|
|
147
139
|
}
|
|
148
140
|
function createSpecSha(sourceText) {
|
|
149
141
|
return `sha256:${createHash("sha256").update(sourceText).digest("hex")}`;
|
|
@@ -218,17 +210,50 @@ function collectDeletedFiles(currentFiles, desiredFiles) {
|
|
|
218
210
|
}
|
|
219
211
|
return deletedFiles;
|
|
220
212
|
}
|
|
221
|
-
async function writeGeneratedFiles(fs, filesToWrite) {
|
|
213
|
+
async function writeGeneratedFiles(fs, outputDir, filesToWrite) {
|
|
222
214
|
for (const file of filesToWrite) {
|
|
223
215
|
await fs.mkdir(path.dirname(file.path), { recursive: true });
|
|
216
|
+
await assertSafeOutputPath(fs, outputDir, file.path);
|
|
224
217
|
await fs.writeFile(file.path, file.contents, "utf8");
|
|
225
218
|
}
|
|
226
219
|
}
|
|
220
|
+
async function assertSafeOutputPath(fs, outputDir, filePath) {
|
|
221
|
+
const canonicalOutputDir = await fs.realpath(outputDir);
|
|
222
|
+
const canonicalFileParent = await fs.realpath(path.dirname(filePath));
|
|
223
|
+
const relativeParentPath = path.relative(canonicalOutputDir, canonicalFileParent);
|
|
224
|
+
if (relativeParentPath === ".." ||
|
|
225
|
+
relativeParentPath.startsWith(`..${path.sep}`) ||
|
|
226
|
+
path.isAbsolute(relativeParentPath) ||
|
|
227
|
+
canonicalOutputDir !== path.resolve(outputDir)) {
|
|
228
|
+
throw new Error("Generated output must remain inside the output directory.");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
227
231
|
async function deleteGeneratedFiles(fs, filePaths) {
|
|
228
232
|
for (const filePath of filePaths) {
|
|
229
233
|
await fs.rm(filePath, { force: true });
|
|
230
234
|
}
|
|
231
235
|
}
|
|
236
|
+
async function restoreGeneratedFiles(fs, outputDir, currentFiles, updatedFiles, deletedFiles) {
|
|
237
|
+
for (const file of updatedFiles) {
|
|
238
|
+
const previousContents = currentFiles.get(file.path);
|
|
239
|
+
if (previousContents === undefined) {
|
|
240
|
+
await fs.rm(file.path, { force: true });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
await fs.mkdir(path.dirname(file.path), { recursive: true });
|
|
244
|
+
await assertSafeOutputPath(fs, outputDir, file.path);
|
|
245
|
+
await fs.writeFile(file.path, previousContents, "utf8");
|
|
246
|
+
}
|
|
247
|
+
for (const filePath of deletedFiles) {
|
|
248
|
+
const previousContents = currentFiles.get(filePath);
|
|
249
|
+
if (previousContents === undefined) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
253
|
+
await assertSafeOutputPath(fs, outputDir, filePath);
|
|
254
|
+
await fs.writeFile(filePath, previousContents, "utf8");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
232
257
|
function isNotFoundError(error) {
|
|
233
258
|
return (typeof error === "object" &&
|
|
234
259
|
error !== null &&
|
package/dist/generate.js
CHANGED
|
@@ -751,7 +751,7 @@ function isEnumPrimitiveValue(value) {
|
|
|
751
751
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
752
752
|
}
|
|
753
753
|
function isOpenApiScalarType(type) {
|
|
754
|
-
return typeof type === "string" && type
|
|
754
|
+
return typeof type === "string" && Object.prototype.hasOwnProperty.call(SCHEMA_TYPE_TO_KIND, type);
|
|
755
755
|
}
|
|
756
756
|
function expectRequestBody(document, requestBody, operationId, context, refChain = []) {
|
|
757
757
|
if (!isReferenceObject(requestBody)) {
|
|
@@ -931,7 +931,8 @@ function getOperationAuthMode(document, operation, operationId) {
|
|
|
931
931
|
const definedSchemes = document.components?.securitySchemes;
|
|
932
932
|
for (const requirement of security ?? []) {
|
|
933
933
|
for (const schemeName of Object.keys(requirement)) {
|
|
934
|
-
if (definedSchemes !== undefined &&
|
|
934
|
+
if (definedSchemes !== undefined &&
|
|
935
|
+
Object.prototype.hasOwnProperty.call(definedSchemes, schemeName)) {
|
|
935
936
|
continue;
|
|
936
937
|
}
|
|
937
938
|
throw new UserError(`Operation ${JSON.stringify(operationId)} references undefined security scheme ${JSON.stringify(schemeName)} in ${securityScope} security. Expected components.securitySchemes to define it.`);
|
|
@@ -1001,6 +1002,9 @@ function renderConstArray(values) {
|
|
|
1001
1002
|
return `${JSON.stringify(values)} as const`;
|
|
1002
1003
|
}
|
|
1003
1004
|
function renderObjectKey(name) {
|
|
1005
|
+
if (name === "__proto__") {
|
|
1006
|
+
return `[${JSON.stringify(name)}]`;
|
|
1007
|
+
}
|
|
1004
1008
|
if (name === normalizeParamName(name) && isIdentifierName(name)) {
|
|
1005
1009
|
return name;
|
|
1006
1010
|
}
|
package/dist/http.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { text as designText } from "@poe-code/design-system";
|
|
2
2
|
import { UserError } from "toolcraft";
|
|
3
3
|
import { classifyNetworkError } from "./network-error.js";
|
|
4
|
+
import { redactHeaders, redactHeaderValue, redactSensitiveQueryValues } from "./redaction.js";
|
|
4
5
|
const TRANSCRIPT_BODY_BYTE_LIMIT = 4 * 1024;
|
|
5
6
|
export class HttpError extends Error {
|
|
6
7
|
status;
|
|
@@ -29,7 +30,7 @@ export async function requestJson(options) {
|
|
|
29
30
|
const headers = createHeaders(token, hasBody);
|
|
30
31
|
const writeStdout = options.writeStdout ?? process.stdout.write.bind(process.stdout);
|
|
31
32
|
const writeStderr = options.writeStderr ?? process.stderr.write.bind(process.stderr);
|
|
32
|
-
const requestLine = `${method} ${url}`;
|
|
33
|
+
const requestLine = `${method} ${redactSensitiveQueryValues(url)}`;
|
|
33
34
|
if (options.dryRun) {
|
|
34
35
|
writeStdout(formatDryRunOutput(requestLine, headers, options.body));
|
|
35
36
|
return undefined;
|
|
@@ -52,7 +53,7 @@ export async function requestJson(options) {
|
|
|
52
53
|
const text = await response.text();
|
|
53
54
|
const contentType = response.headers.get("content-type");
|
|
54
55
|
const request = createHttpErrorRequest(method, url, headers, options.body);
|
|
55
|
-
const responseHeaders = serializeHeaders(response.headers);
|
|
56
|
+
const responseHeaders = redactHeaders(serializeHeaders(response.headers));
|
|
56
57
|
if (response.ok) {
|
|
57
58
|
if (text.length === 0) {
|
|
58
59
|
if (options.verbose) {
|
|
@@ -75,14 +76,32 @@ export async function requestJson(options) {
|
|
|
75
76
|
message: `Expected a JSON response body but received content-type ${JSON.stringify(contentType ?? "<missing>")}.`
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
let body;
|
|
80
|
+
try {
|
|
81
|
+
body = JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
if (options.verbose) {
|
|
85
|
+
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, text)));
|
|
86
|
+
}
|
|
87
|
+
throw new HttpError({
|
|
88
|
+
request,
|
|
89
|
+
response: {
|
|
90
|
+
status: response.status,
|
|
91
|
+
statusText: response.statusText,
|
|
92
|
+
headers: responseHeaders,
|
|
93
|
+
body: text
|
|
94
|
+
},
|
|
95
|
+
message: "Expected a valid JSON response body but received malformed JSON."
|
|
96
|
+
});
|
|
97
|
+
}
|
|
79
98
|
if (options.verbose) {
|
|
80
99
|
writeStderr(formatTranscriptLines(formatVerboseResponseTranscript(response, responseHeaders, body)));
|
|
81
100
|
}
|
|
82
101
|
return body;
|
|
83
102
|
}
|
|
84
|
-
if (response.status === 401) {
|
|
85
|
-
await options.tokenSource.invalidate?.();
|
|
103
|
+
if (response.status === 401 && options.auth === "required") {
|
|
104
|
+
await options.tokenSource.invalidate?.(token).catch(() => undefined);
|
|
86
105
|
}
|
|
87
106
|
const body = parseResponseBody(text, contentType);
|
|
88
107
|
if (options.verbose) {
|
|
@@ -113,7 +132,9 @@ function buildRequestUrl(options) {
|
|
|
113
132
|
}
|
|
114
133
|
function substitutePathParams(path, pathParams) {
|
|
115
134
|
const resolvedPath = path.replace(/\{([^}]+)\}/g, (_match, key) => {
|
|
116
|
-
const value = pathParams
|
|
135
|
+
const value = pathParams !== undefined && Object.prototype.hasOwnProperty.call(pathParams, key)
|
|
136
|
+
? pathParams[key]
|
|
137
|
+
: undefined;
|
|
117
138
|
if (value === undefined) {
|
|
118
139
|
throw new UserError(`Missing path parameter "${key}".`);
|
|
119
140
|
}
|
|
@@ -142,7 +163,7 @@ function createHeaders(token, hasBody) {
|
|
|
142
163
|
function createHttpErrorRequest(method, url, headers, body) {
|
|
143
164
|
return {
|
|
144
165
|
method,
|
|
145
|
-
url,
|
|
166
|
+
url: redactSensitiveQueryValues(url),
|
|
146
167
|
headers: redactHeaders(headers),
|
|
147
168
|
...(body === undefined ? {} : { body })
|
|
148
169
|
};
|
|
@@ -150,15 +171,6 @@ function createHttpErrorRequest(method, url, headers, body) {
|
|
|
150
171
|
function serializeHeaders(headers) {
|
|
151
172
|
return Object.fromEntries(headers.entries());
|
|
152
173
|
}
|
|
153
|
-
function redactHeaders(headers) {
|
|
154
|
-
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, redactHeaderValue(key, value)]));
|
|
155
|
-
}
|
|
156
|
-
function redactHeaderValue(key, value) {
|
|
157
|
-
if (key.toLowerCase() === "authorization" && value.startsWith("Bearer ")) {
|
|
158
|
-
return "Bearer ****";
|
|
159
|
-
}
|
|
160
|
-
return value;
|
|
161
|
-
}
|
|
162
174
|
function formatDryRunOutput(requestLine, headers, body) {
|
|
163
175
|
const lines = [
|
|
164
176
|
requestLine,
|
|
@@ -175,7 +187,7 @@ function formatDryRunOutput(requestLine, headers, body) {
|
|
|
175
187
|
}
|
|
176
188
|
function formatVerboseRequestTranscript(method, url, headers, body) {
|
|
177
189
|
const lines = [
|
|
178
|
-
`→ ${method} ${url}`,
|
|
190
|
+
`→ ${method} ${redactSensitiveQueryValues(url)}`,
|
|
179
191
|
...Object.entries(headers).map(([key, value]) => {
|
|
180
192
|
const headerValue = redactHeaderValue(key, value);
|
|
181
193
|
return ` ${key}: ${headerValue}`;
|
package/dist/interpreter.js
CHANGED
|
@@ -8,7 +8,7 @@ const QUERY_ARRAY_SERIALIZATION_SEPARATORS = {
|
|
|
8
8
|
const VALUE_REFERENCE_OPERATIONS = {
|
|
9
9
|
param: {
|
|
10
10
|
render: (reference) => renderParamAccess(reference.paramName),
|
|
11
|
-
evaluate: (reference, context) => context.params
|
|
11
|
+
evaluate: (reference, context) => readOwnParam(context.params, reference.paramName)
|
|
12
12
|
},
|
|
13
13
|
resolved: {
|
|
14
14
|
render: (reference) => reference.resolvedName,
|
|
@@ -168,7 +168,7 @@ const REQUEST_SECTION_OPERATIONS = {
|
|
|
168
168
|
if (!optional) {
|
|
169
169
|
return [
|
|
170
170
|
` ${section.key}: {`,
|
|
171
|
-
...sectionFields.map((field) => ` ${
|
|
171
|
+
...sectionFields.map((field) => ` ${renderWireName(field.wireName)}: ${renderValueExpression(field.value)},`),
|
|
172
172
|
" },"
|
|
173
173
|
];
|
|
174
174
|
}
|
|
@@ -177,7 +177,7 @@ const REQUEST_SECTION_OPERATIONS = {
|
|
|
177
177
|
" ? {}",
|
|
178
178
|
" : {",
|
|
179
179
|
` ${section.key}: {`,
|
|
180
|
-
...sectionFields.map((field) => ` ${
|
|
180
|
+
...sectionFields.map((field) => ` ${renderWireName(field.wireName)}: ${renderValueExpression(field.value)},`),
|
|
181
181
|
" },",
|
|
182
182
|
" }),"
|
|
183
183
|
];
|
|
@@ -285,5 +285,14 @@ function renderOmitWhenUndefinedExpression(reference) {
|
|
|
285
285
|
return `${renderValueReference(reference)} === undefined`;
|
|
286
286
|
}
|
|
287
287
|
function renderParamAccess(name) {
|
|
288
|
+
if (Object.prototype.hasOwnProperty.call(Object.prototype, name)) {
|
|
289
|
+
return `(Object.prototype.hasOwnProperty.call(params, ${JSON.stringify(name)}) ? params[${JSON.stringify(name)}] : undefined)`;
|
|
290
|
+
}
|
|
288
291
|
return isIdentifierName(name) ? `params.${name}` : `params[${JSON.stringify(name)}]`;
|
|
289
292
|
}
|
|
293
|
+
function renderWireName(name) {
|
|
294
|
+
return name === "__proto__" ? `[${JSON.stringify(name)}]` : JSON.stringify(name);
|
|
295
|
+
}
|
|
296
|
+
function readOwnParam(params, name) {
|
|
297
|
+
return Object.prototype.hasOwnProperty.call(params, name) ? params[name] : undefined;
|
|
298
|
+
}
|
package/dist/mock/fetch.js
CHANGED
|
@@ -302,17 +302,26 @@ function validateAgainstSchema(value, schema, document, pointer) {
|
|
|
302
302
|
return errors;
|
|
303
303
|
}
|
|
304
304
|
if (types.includes("object") && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
305
|
+
const objectValue = value;
|
|
305
306
|
for (const required of resolved.required ?? []) {
|
|
306
|
-
if (!(required
|
|
307
|
+
if (!Object.prototype.hasOwnProperty.call(objectValue, required)) {
|
|
307
308
|
errors.push(`${pointer}/${required}: required`);
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
if (resolved.properties !== undefined) {
|
|
311
|
-
for (const [key, propValue] of Object.entries(
|
|
312
|
+
for (const [key, propValue] of Object.entries(objectValue)) {
|
|
312
313
|
const propSchema = resolved.properties[key];
|
|
313
314
|
if (propSchema !== undefined) {
|
|
314
315
|
errors.push(...validateAgainstSchema(propValue, propSchema, document, `${pointer}/${key}`));
|
|
315
316
|
}
|
|
317
|
+
else if (resolved.additionalProperties === false) {
|
|
318
|
+
errors.push(`${pointer}/${key}: additional property not allowed`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (resolved.additionalProperties === false && Object.keys(objectValue).length > 0) {
|
|
323
|
+
for (const key of Object.keys(objectValue)) {
|
|
324
|
+
errors.push(`${pointer}/${key}: additional property not allowed`);
|
|
316
325
|
}
|
|
317
326
|
}
|
|
318
327
|
}
|
|
@@ -393,20 +402,28 @@ function appendHeaders(target, source) {
|
|
|
393
402
|
}
|
|
394
403
|
if (source instanceof Headers) {
|
|
395
404
|
source.forEach((value, key) => {
|
|
396
|
-
target
|
|
405
|
+
setHeader(target, key, value);
|
|
397
406
|
});
|
|
398
407
|
return;
|
|
399
408
|
}
|
|
400
409
|
if (Array.isArray(source)) {
|
|
401
410
|
for (const [key, value] of source) {
|
|
402
|
-
target
|
|
411
|
+
setHeader(target, key, value);
|
|
403
412
|
}
|
|
404
413
|
return;
|
|
405
414
|
}
|
|
406
415
|
for (const [key, value] of Object.entries(source)) {
|
|
407
|
-
target
|
|
416
|
+
setHeader(target, key, String(value));
|
|
408
417
|
}
|
|
409
418
|
}
|
|
419
|
+
function setHeader(target, key, value) {
|
|
420
|
+
Object.defineProperty(target, key.toLowerCase(), {
|
|
421
|
+
enumerable: true,
|
|
422
|
+
configurable: true,
|
|
423
|
+
writable: true,
|
|
424
|
+
value
|
|
425
|
+
});
|
|
426
|
+
}
|
|
410
427
|
async function readRequestBody(input, init) {
|
|
411
428
|
if (init?.body !== undefined && init.body !== null) {
|
|
412
429
|
return typeof init.body === "string" ? init.body : await new Response(init.body).text();
|
package/dist/network-error.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { UserError } from "toolcraft";
|
|
2
|
+
import { redactSensitiveQueryValues } from "./redaction.js";
|
|
2
3
|
export function classifyNetworkError(error, url) {
|
|
3
4
|
const networkError = findNetworkError(error);
|
|
4
5
|
const urlParts = new URL(url);
|
|
5
6
|
const host = getHost(networkError, urlParts);
|
|
7
|
+
const redactedUrl = redactSensitiveQueryValues(url);
|
|
6
8
|
switch (networkError?.code) {
|
|
7
9
|
case "ECONNREFUSED":
|
|
8
10
|
return new UserError(`Connection refused: ${host}:${getPort(networkError, urlParts)}. Is the server running?`, { cause: error });
|
|
9
11
|
case "ETIMEDOUT":
|
|
10
|
-
return new UserError(`Request timed out after ${getTimeoutMs(networkError)}ms: ${
|
|
12
|
+
return new UserError(`Request timed out after ${getTimeoutMs(networkError)}ms: ${redactedUrl}.`, {
|
|
11
13
|
cause: error
|
|
12
14
|
});
|
|
13
15
|
case "ENOTFOUND":
|
|
@@ -24,10 +26,10 @@ export function classifyNetworkError(error, url) {
|
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
if (findAbortError(error) !== null) {
|
|
27
|
-
return new UserError(`Request aborted: ${
|
|
29
|
+
return new UserError(`Request aborted: ${redactedUrl}.`, { cause: error });
|
|
28
30
|
}
|
|
29
31
|
if (error instanceof TypeError && error.message === "fetch failed" && !hasCause(error)) {
|
|
30
|
-
return new UserError(`Network request failed: ${
|
|
32
|
+
return new UserError(`Network request failed: ${redactedUrl}.`, { cause: error });
|
|
31
33
|
}
|
|
32
34
|
return null;
|
|
33
35
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const SENSITIVE_QUERY_KEYS = new Set([
|
|
2
|
+
"apikey",
|
|
3
|
+
"accesstoken",
|
|
4
|
+
"authtoken",
|
|
5
|
+
"clientsecret",
|
|
6
|
+
"key",
|
|
7
|
+
"password",
|
|
8
|
+
"secret",
|
|
9
|
+
"sig",
|
|
10
|
+
"signature",
|
|
11
|
+
"token"
|
|
12
|
+
]);
|
|
13
|
+
const SENSITIVE_HEADER_NAMES = new Set(["cookie", "proxy-authorization", "set-cookie"]);
|
|
14
|
+
export function redactHeaders(headers) {
|
|
15
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, redactHeaderValue(key, value)]));
|
|
16
|
+
}
|
|
17
|
+
export function redactHeaderValue(key, value) {
|
|
18
|
+
const normalizedKey = key.toLowerCase();
|
|
19
|
+
if (normalizedKey === "authorization") {
|
|
20
|
+
return value.startsWith("Bearer ") ? "Bearer ****" : "****";
|
|
21
|
+
}
|
|
22
|
+
if (SENSITIVE_HEADER_NAMES.has(normalizedKey)) {
|
|
23
|
+
return "****";
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function redactSensitiveQueryValues(url) {
|
|
28
|
+
const redactedUrl = new URL(url);
|
|
29
|
+
for (const key of redactedUrl.searchParams.keys()) {
|
|
30
|
+
if (SENSITIVE_QUERY_KEYS.has(normalizeQueryKey(key))) {
|
|
31
|
+
redactedUrl.searchParams.set(key, "****");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return redactedUrl.toString();
|
|
35
|
+
}
|
|
36
|
+
function normalizeQueryKey(key) {
|
|
37
|
+
return key.toLowerCase().replaceAll("_", "").replaceAll("-", "");
|
|
38
|
+
}
|
|
@@ -18,7 +18,9 @@ const KIND_COLORS = {
|
|
|
18
18
|
other: (text) => color.dim(text)
|
|
19
19
|
};
|
|
20
20
|
function colorForKind(kind) {
|
|
21
|
-
return KIND_COLORS
|
|
21
|
+
return Object.prototype.hasOwnProperty.call(KIND_COLORS, kind)
|
|
22
|
+
? KIND_COLORS[kind]
|
|
23
|
+
: (text) => color.dim(text);
|
|
22
24
|
}
|
|
23
25
|
function writeLine(line) {
|
|
24
26
|
getAcpWriter()(line);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
interface BrowserProcess {
|
|
2
2
|
once(event: "error", listener: (error: Error) => void): this;
|
|
3
|
-
once(event: "
|
|
3
|
+
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
|
|
4
4
|
unref(): void;
|
|
5
5
|
}
|
|
6
6
|
type SpawnBrowserProcess = (command: string, args: string[], options: {
|
|
@@ -18,7 +18,12 @@ function launchBrowser(command, args, spawnProcess) {
|
|
|
18
18
|
return new Promise((resolve, reject) => {
|
|
19
19
|
const child = spawnProcess(command, args, { detached: true, stdio: "ignore" });
|
|
20
20
|
child.once("error", reject);
|
|
21
|
-
child.once("
|
|
21
|
+
child.once("close", (code, signal) => {
|
|
22
|
+
if (code !== 0) {
|
|
23
|
+
const reason = code === null ? `signal ${signal ?? "unknown"}` : `code ${code}`;
|
|
24
|
+
reject(new Error(`Browser launcher exited with ${reason}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
child.unref();
|
|
23
28
|
resolve();
|
|
24
29
|
});
|
|
@@ -48,6 +48,10 @@ function hexChannel(value, offset) {
|
|
|
48
48
|
}
|
|
49
49
|
function normalizeHex(value) {
|
|
50
50
|
const normalized = value.startsWith("#") ? value.slice(1) : value;
|
|
51
|
+
if ((normalized.length !== 3 && normalized.length !== 6) ||
|
|
52
|
+
Array.from(normalized).some((char) => !"0123456789abcdefABCDEF".includes(char))) {
|
|
53
|
+
throw new Error(`Invalid hexadecimal color: ${value}`);
|
|
54
|
+
}
|
|
51
55
|
if (normalized.length === 3) {
|
|
52
56
|
const red = normalized[0];
|
|
53
57
|
const green = normalized[1];
|
|
@@ -58,14 +62,11 @@ function normalizeHex(value) {
|
|
|
58
62
|
Number.parseInt(`${blue}${blue}`, 16)
|
|
59
63
|
];
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
return [0, 0, 0];
|
|
65
|
+
return [
|
|
66
|
+
hexChannel(normalized, 0),
|
|
67
|
+
hexChannel(normalized, 2),
|
|
68
|
+
hexChannel(normalized, 4)
|
|
69
|
+
];
|
|
69
70
|
}
|
|
70
71
|
function rgbStyle(red, green, blue) {
|
|
71
72
|
return {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { typography } from "../tokens/typography.js";
|
|
2
2
|
import { text } from "./text.js";
|
|
3
3
|
export function formatCommandNotFound(input) {
|
|
4
|
-
const
|
|
5
|
-
|
|
4
|
+
const unknownInput = input.unknownCommand.replaceAll("\r\n", " ").replaceAll("\n", " ").replaceAll("\r", " ");
|
|
5
|
+
const unknown = unknownInput.length > 0
|
|
6
|
+
? unknownInput
|
|
6
7
|
: "<command>";
|
|
7
8
|
return {
|
|
8
9
|
label: `${typography.bold("Unknown command:")} ${text.command(unknown)}`,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ThemePalette } from "../tokens/colors.js";
|
|
2
|
+
export interface DetailCardRow {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DetailCardSection {
|
|
7
|
+
title?: string;
|
|
8
|
+
rows: DetailCardRow[];
|
|
9
|
+
}
|
|
10
|
+
export interface RenderDetailCardOptions {
|
|
11
|
+
theme: ThemePalette;
|
|
12
|
+
title: string;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
badges?: string[];
|
|
15
|
+
prose?: Array<{
|
|
16
|
+
title?: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}>;
|
|
19
|
+
sections?: DetailCardSection[];
|
|
20
|
+
width?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function renderDetailCard(options: RenderDetailCardOptions): string;
|