@blogic-cz/agent-tools 1.5.7 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +21 -8
  2. package/dist/az-tool/service.d.ts.map +1 -1
  3. package/dist/azdo-tool/service.d.ts.map +1 -1
  4. package/dist/credential-guard/index.d.ts +2 -0
  5. package/dist/credential-guard/index.d.ts.map +1 -1
  6. package/dist/db-tool/service.d.ts.map +1 -1
  7. package/dist/gh-tool/gist.d.ts +13 -0
  8. package/dist/gh-tool/gist.d.ts.map +1 -1
  9. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  10. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  11. package/dist/gh-tool/release.d.ts +46 -1
  12. package/dist/gh-tool/release.d.ts.map +1 -1
  13. package/dist/gh-tool/text-input.d.ts +5 -3
  14. package/dist/gh-tool/text-input.d.ts.map +1 -1
  15. package/dist/k8s-tool/service.d.ts.map +1 -1
  16. package/dist/logs-tool/main.d.ts +1 -2
  17. package/dist/logs-tool/main.d.ts.map +1 -1
  18. package/dist/observability-tool/shared.d.ts.map +1 -1
  19. package/dist/session-tool/main.d.ts +1 -2
  20. package/dist/session-tool/main.d.ts.map +1 -1
  21. package/dist/shared/audit.d.ts.map +1 -1
  22. package/dist/shared/content-security.d.ts +6 -0
  23. package/dist/shared/content-security.d.ts.map +1 -0
  24. package/dist/shared/credential-patterns.d.ts +11 -0
  25. package/dist/shared/credential-patterns.d.ts.map +1 -0
  26. package/dist/shared/effect-version.d.ts.map +1 -1
  27. package/dist/shared/error-renderer.d.ts.map +1 -1
  28. package/dist/shared/format.d.ts.map +1 -1
  29. package/dist/shared/index.d.ts +1 -0
  30. package/dist/shared/index.d.ts.map +1 -1
  31. package/dist/shared/output-boundary.d.ts +3 -0
  32. package/dist/shared/output-boundary.d.ts.map +1 -0
  33. package/package.json +1 -1
  34. package/src/audit-tool/main.ts +3 -1
  35. package/src/az-tool/main.ts +3 -1
  36. package/src/az-tool/service.ts +9 -0
  37. package/src/azdo-tool/main.ts +3 -1
  38. package/src/azdo-tool/service.ts +17 -0
  39. package/src/credential-guard/claude-hook.ts +2 -1
  40. package/src/credential-guard/index.ts +936 -87
  41. package/src/db-tool/main.ts +3 -1
  42. package/src/db-tool/service.ts +25 -2
  43. package/src/gh-tool/gist.ts +63 -64
  44. package/src/gh-tool/main.ts +3 -1
  45. package/src/gh-tool/pr/core.ts +10 -3
  46. package/src/gh-tool/pr/review.ts +199 -12
  47. package/src/gh-tool/release.ts +104 -29
  48. package/src/gh-tool/text-input.ts +19 -38
  49. package/src/k8s-tool/main.ts +3 -1
  50. package/src/k8s-tool/service.ts +25 -0
  51. package/src/logs-tool/main.ts +7 -2
  52. package/src/observability-tool/main.ts +5 -7
  53. package/src/observability-tool/shared.ts +41 -1
  54. package/src/session-tool/main.ts +18 -3
  55. package/src/shared/audit.ts +11 -10
  56. package/src/shared/content-security.ts +91 -0
  57. package/src/shared/credential-patterns.ts +62 -0
  58. package/src/shared/effect-version.ts +2 -1
  59. package/src/shared/error-renderer.ts +15 -2
  60. package/src/shared/format.ts +33 -6
  61. package/src/shared/index.ts +5 -0
  62. package/src/shared/output-boundary.ts +29 -0
@@ -0,0 +1,91 @@
1
+ import { findSecretMatches } from "./credential-patterns";
2
+
3
+ const SENSITIVE_ENV_NAME =
4
+ /(?:KEY|TOKEN|SECRET|PASS(?:WORD)?|PWD|CREDENTIAL|AUTH|COOKIE|SESSION|PSK)/i;
5
+ const isSensitiveEnvironmentName = (name: string) =>
6
+ name !== "PWD" &&
7
+ name !== "OLDPWD" &&
8
+ name !== "CODEX_SESSION_ID" &&
9
+ SENSITIVE_ENV_NAME.test(name);
10
+
11
+ const sensitiveEnvironmentValues = (environment: Record<string, string | undefined>) =>
12
+ Object.entries(environment)
13
+ .flatMap(([name, value]) =>
14
+ isSensitiveEnvironmentName(name) && value && value.length >= 8 ? [value] : [],
15
+ )
16
+ .toSorted((left, right) => right.length - left.length);
17
+
18
+ export function unsafeOutboundTextReason(
19
+ text: string,
20
+ environment: Record<string, string | undefined> = process.env,
21
+ ): string | null {
22
+ if (findSecretMatches(text).length > 0) return "a credential pattern";
23
+ if (sensitiveEnvironmentValues(environment).some((value) => text.includes(value))) {
24
+ return "a credential from the process environment";
25
+ }
26
+
27
+ const assignments = text.match(/^[A-Za-z_][A-Za-z0-9_]*=.*$/gm) ?? [];
28
+ if (
29
+ assignments.length >= 5 &&
30
+ assignments.some((line) => isSensitiveEnvironmentName(line.split("=", 1)[0] ?? ""))
31
+ ) {
32
+ return "an environment dump";
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /** Remove recognized secret spans and known sensitive process-environment values. */
38
+ export function redactSensitiveText(
39
+ text: string,
40
+ environment: Record<string, string | undefined> = process.env,
41
+ ): string {
42
+ const spans = findSecretMatches(text).map(({ start, end }) => ({ start, end }));
43
+ for (const value of sensitiveEnvironmentValues(environment)) {
44
+ let start = text.indexOf(value);
45
+ while (start !== -1) {
46
+ spans.push({ start, end: start + value.length });
47
+ start = text.indexOf(value, start + 1);
48
+ }
49
+ }
50
+ if (spans.length === 0) return text;
51
+
52
+ spans.sort((left, right) => left.start - right.start || right.end - left.end);
53
+ let result = "";
54
+ let offset = 0;
55
+ for (const span of spans) {
56
+ if (span.start < offset) {
57
+ offset = Math.max(offset, span.end);
58
+ continue;
59
+ }
60
+ result += text.slice(offset, span.start) + "[REDACTED]";
61
+ offset = span.end;
62
+ }
63
+ return result + text.slice(offset);
64
+ }
65
+
66
+ /** Sanitize values before serialization so JSON and TOON retain their structure. */
67
+ export function redactSensitiveValue<T>(
68
+ value: T,
69
+ environment: Record<string, string | undefined> = process.env,
70
+ ): T {
71
+ if (typeof value === "string") return redactSensitiveText(value, environment) as T;
72
+ if (Array.isArray(value))
73
+ return value.map((item) => redactSensitiveValue(item, environment)) as T;
74
+ if (value === null || typeof value !== "object") return value;
75
+ if (value instanceof Date) return redactSensitiveText(value.toISOString(), environment) as T;
76
+
77
+ return Object.fromEntries(
78
+ Object.entries(value).map(([key, item]) => {
79
+ const serialized = JSON.stringify(item) ?? "undefined";
80
+ const pair = `${JSON.stringify(key)}: ${serialized}`;
81
+ const valueStart = pair.length - serialized.length;
82
+ const contextualSecret =
83
+ typeof item === "string" &&
84
+ findSecretMatches(pair).some((match) => match.start < valueStart && match.end > valueStart);
85
+ return [
86
+ redactSensitiveText(key, environment),
87
+ contextualSecret ? "[REDACTED]" : redactSensitiveValue(item, environment),
88
+ ];
89
+ }),
90
+ ) as T;
91
+ }
@@ -0,0 +1,62 @@
1
+ /** Patterns used by the credential guard and shared output redaction. */
2
+ export const SECRET_PATTERNS = [
3
+ { name: "AWS Access Key", pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/ },
4
+ { name: "GitHub Token", pattern: /gh[ps]_[A-Za-z0-9]{36}/ },
5
+ { name: "GitHub PAT", pattern: /github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}/ },
6
+ { name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{48}/ },
7
+ {
8
+ name: "Generic API Key",
9
+ pattern: /(?:api[_-]?key|apikey)["\s:=]+["']?([A-Za-z0-9_-]{20,})["']?/i,
10
+ },
11
+ {
12
+ name: "Generic Secret",
13
+ pattern:
14
+ /(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
15
+ },
16
+ {
17
+ // eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
18
+ name: "Priv" + "ate Key",
19
+ pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
20
+ },
21
+ {
22
+ name: "JWT Token",
23
+ pattern: /(?:["'=:\s]|^)eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
24
+ },
25
+ { name: "Azure SAS Token", pattern: /[?&]sig=[A-Za-z0-9%+/=]{20,}/ },
26
+ { name: "GCP Service Account Key", pattern: /"type"\s*:\s*"service_account"/ },
27
+ {
28
+ name: "Slack Webhook URL",
29
+ pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
30
+ },
31
+ {
32
+ name: "Discord Webhook URL",
33
+ pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
34
+ },
35
+ {
36
+ name: "Database URL",
37
+ pattern: /(?:postgres(?:ql)?|mysql|mongodb):\/\/(?!\$\{)[^:]+:(?!\$\{)[^@]+@/,
38
+ },
39
+ ];
40
+
41
+ export function findSecretMatches(
42
+ content: string,
43
+ ): Array<{ name: string; start: number; end: number }> {
44
+ const matches: Array<{ name: string; start: number; end: number }> = [];
45
+ for (const { name, pattern } of SECRET_PATTERNS) {
46
+ const scanner = new RegExp(
47
+ pattern.source,
48
+ pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`,
49
+ );
50
+ for (const match of content.matchAll(scanner)) {
51
+ const start = match.index;
52
+ let end = start + match[0].length;
53
+ if (name === "Private Key") {
54
+ const remainder = content.slice(end);
55
+ const closing = /^\r?\n?[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/.exec(remainder);
56
+ end += closing?.[0].length ?? remainder.length;
57
+ }
58
+ matches.push({ name, start, end });
59
+ }
60
+ }
61
+ return matches;
62
+ }
@@ -1,4 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { redactSensitiveText } from "./content-security";
2
3
 
3
4
  // eslint-disable-next-line import/no-relative-parent-imports -- package.json lives at project root, outside src/
4
5
  import pkg from "../../package.json" with { type: "json" };
@@ -27,6 +28,6 @@ export const checkEffectVersion = async (): Promise<boolean> => {
27
28
 
28
29
  const error = effectVersionError(version);
29
30
  if (error === undefined) return true;
30
- console.error(error);
31
+ console.error(redactSensitiveText(error));
31
32
  return false;
32
33
  };
@@ -1,6 +1,7 @@
1
1
  import type { Effect } from "effect";
2
2
 
3
3
  import { Cause, Console } from "effect";
4
+ import { redactSensitiveText } from "./content-security";
4
5
 
5
6
  const formatError = (error: unknown): string => {
6
7
  if (
@@ -42,7 +43,19 @@ const formatError = (error: unknown): string => {
42
43
 
43
44
  const formatCause = (cause: Cause.Cause<unknown>): string => {
44
45
  const firstFailure = cause.reasons.find(Cause.isFailReason);
45
- if (firstFailure !== undefined) return formatError(firstFailure.error);
46
+ if (firstFailure !== undefined) {
47
+ if (
48
+ typeof firstFailure.error === "object" &&
49
+ firstFailure.error !== null &&
50
+ Reflect.get(firstFailure.error, "_tag") === "ShowHelp"
51
+ ) {
52
+ const errors = Reflect.get(firstFailure.error, "errors");
53
+ return Array.isArray(errors) && errors.length > 0
54
+ ? errors.map(formatError).join("; ")
55
+ : "Help requested";
56
+ }
57
+ return formatError(firstFailure.error);
58
+ }
46
59
 
47
60
  const firstDefect = cause.reasons.find(Cause.isDieReason);
48
61
  if (firstDefect !== undefined) {
@@ -56,4 +69,4 @@ const formatCause = (cause: Cause.Cause<unknown>): string => {
56
69
  };
57
70
 
58
71
  export const renderCauseToStderr = (cause: Cause.Cause<unknown>): Effect.Effect<void> =>
59
- Console.error(formatCause(cause));
72
+ Console.error(redactSensitiveText(formatCause(cause)));
@@ -1,7 +1,8 @@
1
1
  import { Flag } from "effect/unstable/cli";
2
- import { encode as encodeToon } from "@toon-format/toon";
2
+ import { decode as decodeToon, encode as encodeToon } from "@toon-format/toon";
3
3
  import { Effect } from "effect";
4
4
 
5
+ import { redactSensitiveText, redactSensitiveValue } from "./content-security";
5
6
  import type { BaseResult, OutputFormat } from "./types";
6
7
 
7
8
  export const formatOption = Flag.Literals("format", ["toon", "json"]).pipe(
@@ -11,18 +12,44 @@ export const formatOption = Flag.Literals("format", ["toon", "json"]).pipe(
11
12
 
12
13
  export function formatOutput<T extends BaseResult>(result: T, format: OutputFormat): string {
13
14
  if (format === "toon") {
14
- return encodeToon(result);
15
+ return encodeToon(redactSensitiveValue(result));
15
16
  }
16
- return JSON.stringify(result, null, 2);
17
+ return JSON.stringify(redactSensitiveValue(result), null, 2);
17
18
  }
18
19
 
19
20
  export function formatAny<T>(data: T, format: OutputFormat): string {
20
21
  if (format === "toon") {
21
- return encodeToon(data);
22
+ return encodeToon(redactSensitiveValue(data));
22
23
  }
23
- return JSON.stringify(data, null, 2);
24
+ return JSON.stringify(redactSensitiveValue(data), null, 2);
24
25
  }
25
26
 
27
+ const redactOutputText = (text: string) => {
28
+ let json: unknown;
29
+ let isJson = false;
30
+ try {
31
+ json = JSON.parse(text);
32
+ isJson = true;
33
+ } catch {
34
+ // Try structured TOON below before treating this as plain text.
35
+ }
36
+ if (isJson) {
37
+ return (
38
+ JSON.stringify(redactSensitiveValue(json), null, text.includes("\n") ? 2 : undefined) ??
39
+ "null"
40
+ );
41
+ }
42
+
43
+ let toon: unknown;
44
+ try {
45
+ toon = decodeToon(text);
46
+ } catch {
47
+ // Raw logs and status strings are not structured output.
48
+ }
49
+ if (toon !== null && typeof toon === "object") return encodeToon(redactSensitiveValue(toon));
50
+ return redactSensitiveText(text);
51
+ };
52
+
26
53
  // `Console.log` drops bytes on a non-blocking pipe: a payload over the pipe buffer arrives
27
54
  // truncated at a page boundary, reaching the caller as invalid JSON. Awaiting the write callback
28
55
  // makes the stream retry the short write. Never replace this with `Console.log`.
@@ -30,7 +57,7 @@ export function formatAny<T>(data: T, format: OutputFormat): string {
30
57
  // instead of a silent exit 0 with partial output.
31
58
  export const logText = (text: string) =>
32
59
  Effect.callback<undefined>((resume) => {
33
- process.stdout.write(`${text}\n`, (error) =>
60
+ process.stdout.write(`${redactOutputText(text)}\n`, (error) =>
34
61
  resume(
35
62
  error && (error as NodeJS.ErrnoException).code !== "EPIPE"
36
63
  ? Effect.die(error)
@@ -14,6 +14,11 @@ export { expandPath, runCommand } from "./bun";
14
14
  export { commonArgOptions, parseCommonArgs } from "./cli";
15
15
 
16
16
  export { renderCauseToStderr } from "./error-renderer";
17
+ export {
18
+ redactSensitiveText,
19
+ redactSensitiveValue,
20
+ unsafeOutboundTextReason,
21
+ } from "./content-security";
17
22
 
18
23
  export { dumpCommandSchema, makeSchemaCommand, type CommandSchema } from "./schema-dump";
19
24
 
@@ -0,0 +1,29 @@
1
+ import { Console, Effect, Terminal } from "effect";
2
+
3
+ import { redactSensitiveText, redactSensitiveValue } from "./content-security";
4
+
5
+ const redactArgument = (value: unknown) =>
6
+ typeof value === "string" ? redactSensitiveText(value) : redactSensitiveValue(value);
7
+
8
+ export const withRedactedOutput = <A, E, R>(program: Effect.Effect<A, E, R>) =>
9
+ Effect.gen(function* () {
10
+ const currentConsole = yield* Console.Console;
11
+ const currentTerminal = yield* Terminal.Terminal;
12
+ const redactedConsole = new Proxy(currentConsole, {
13
+ get(target, key) {
14
+ const member = Reflect.get(target, key);
15
+ return typeof member === "function"
16
+ ? (...args: ReadonlyArray<unknown>) => member.apply(target, args.map(redactArgument))
17
+ : member;
18
+ },
19
+ });
20
+ const redactedTerminal = Object.assign(Object.create(currentTerminal), {
21
+ display: (text: string) => currentTerminal.display(redactSensitiveText(text)),
22
+ });
23
+
24
+ return yield* Effect.provideService(
25
+ Effect.provideService(program, Console.Console, redactedConsole),
26
+ Terminal.Terminal,
27
+ redactedTerminal,
28
+ );
29
+ });