@nuvlore/extension-access-jira 0.1.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.
Files changed (95) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +27 -0
  3. package/package.json +54 -0
  4. package/skills/jira-sso-crud/SKILL.md +30 -0
  5. package/src/jiraSsoCrud.mjs +140 -0
  6. package/tools/jira_sso_comment_create.mjs +34 -0
  7. package/tools/jira_sso_comment_delete.mjs +33 -0
  8. package/tools/jira_sso_comment_update.mjs +35 -0
  9. package/tools/jira_sso_issue.mjs +32 -0
  10. package/tools/jira_sso_issue_create.mjs +36 -0
  11. package/tools/jira_sso_issue_delete.mjs +34 -0
  12. package/tools/jira_sso_issue_transition.mjs +36 -0
  13. package/tools/jira_sso_issue_update.mjs +35 -0
  14. package/tools/jira_sso_myself.mjs +26 -0
  15. package/tools/jira_sso_project.mjs +32 -0
  16. package/tools/jira_sso_project_versions.mjs +32 -0
  17. package/tools/jira_sso_projects.mjs +26 -0
  18. package/tools/jira_sso_read.mjs +34 -0
  19. package/tools/jira_sso_search.mjs +33 -0
  20. package/tools/jira_sso_user_assignable_search.mjs +35 -0
  21. package/tools/jira_sso_verify.mjs +26 -0
  22. package/vendor/extension-sso/LICENSE +202 -0
  23. package/vendor/extension-sso/README.md +35 -0
  24. package/vendor/extension-sso/commands/okta-login.md +10 -0
  25. package/vendor/extension-sso/package.json +52 -0
  26. package/vendor/extension-sso/skills/workday-okta-auth/SKILL.md +20 -0
  27. package/vendor/extension-sso/src/autoAuth.mjs +25 -0
  28. package/vendor/extension-sso/src/browserLogin.mjs +1 -0
  29. package/vendor/extension-sso/src/chromeCookies.mjs +1 -0
  30. package/vendor/extension-sso/src/cookieJar.mjs +1 -0
  31. package/vendor/extension-sso/src/extensionAccess.mjs +173 -0
  32. package/vendor/extension-sso/src/htmlSessionExtract.mjs +1 -0
  33. package/vendor/extension-sso/src/oktaAppAccess.mjs +133 -0
  34. package/vendor/extension-sso/src/oktaAppCatalog.mjs +118 -0
  35. package/vendor/extension-sso/src/oktaAppRead.mjs +119 -0
  36. package/vendor/extension-sso/src/oktaConfig.mjs +12 -0
  37. package/vendor/extension-sso/src/oktaSession.mjs +32 -0
  38. package/vendor/extension-sso/src/serviceOperations.mjs +407 -0
  39. package/vendor/extension-sso/src/serviceReadSearch.mjs +395 -0
  40. package/vendor/extension-sso/src/serviceReads.mjs +109 -0
  41. package/vendor/extension-sso/src/serviceRegistry.mjs +497 -0
  42. package/vendor/extension-sso/src/sessionAuth.mjs +339 -0
  43. package/vendor/extension-sso/src/sessionManager.mjs +212 -0
  44. package/vendor/extension-sso/src/sessionValidate.mjs +74 -0
  45. package/vendor/extension-sso/src/ssoConfig.mjs +31 -0
  46. package/vendor/extension-sso/src/ssoHttpRead.mjs +292 -0
  47. package/vendor/extension-sso/tools/extension-access-tools.shared.mjs +16 -0
  48. package/vendor/extension-sso/tools/okta-app-tools.shared.mjs +5 -0
  49. package/vendor/extension-sso/tools/okta-tools.shared.mjs +18 -0
  50. package/vendor/extension-sso/tools/workday_extension_read.mjs +51 -0
  51. package/vendor/extension-sso/tools/workday_extension_read_access.mjs +65 -0
  52. package/vendor/extension-sso/tools/workday_list_extension_access.mjs +39 -0
  53. package/vendor/extension-sso/tools/workday_list_okta_apps.mjs +18 -0
  54. package/vendor/extension-sso/tools/workday_list_service_operations.mjs +41 -0
  55. package/vendor/extension-sso/tools/workday_list_service_read_search_capabilities.mjs +34 -0
  56. package/vendor/extension-sso/tools/workday_list_services.mjs +31 -0
  57. package/vendor/extension-sso/tools/workday_okta_app_probe_all.mjs +24 -0
  58. package/vendor/extension-sso/tools/workday_okta_app_read.mjs +30 -0
  59. package/vendor/extension-sso/tools/workday_okta_app_read_access.mjs +25 -0
  60. package/vendor/extension-sso/tools/workday_okta_auth_header.mjs +58 -0
  61. package/vendor/extension-sso/tools/workday_okta_login.mjs +65 -0
  62. package/vendor/extension-sso/tools/workday_okta_open_app.mjs +63 -0
  63. package/vendor/extension-sso/tools/workday_okta_open_service.mjs +63 -0
  64. package/vendor/extension-sso/tools/workday_okta_status.mjs +35 -0
  65. package/vendor/extension-sso/tools/workday_service_auth.mjs +65 -0
  66. package/vendor/extension-sso/tools/workday_service_operation_read.mjs +51 -0
  67. package/vendor/extension-sso/tools/workday_service_read.mjs +56 -0
  68. package/vendor/extension-sso/tools/workday_service_search.mjs +62 -0
  69. package/vendor/extension-sso/tools/workday_sso_cookie_header.mjs +59 -0
  70. package/vendor/extension-sso/vendor/extension-browser-sso/LICENSE +202 -0
  71. package/vendor/extension-sso/vendor/extension-browser-sso/README.md +16 -0
  72. package/vendor/extension-sso/vendor/extension-browser-sso/package.json +47 -0
  73. package/vendor/extension-sso/vendor/extension-browser-sso/src/autoAuth.mjs +47 -0
  74. package/vendor/extension-sso/vendor/extension-browser-sso/src/browserHttpRead.mjs +76 -0
  75. package/vendor/extension-sso/vendor/extension-browser-sso/src/browserLogin.mjs +24 -0
  76. package/vendor/extension-sso/vendor/extension-browser-sso/src/chromeCookies.mjs +192 -0
  77. package/vendor/extension-sso/vendor/extension-browser-sso/src/cookieJar.mjs +132 -0
  78. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-data-paths.mjs +222 -0
  79. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/devops-diagnostics.mjs +440 -0
  80. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/enterprise-api-read.mjs +180 -0
  81. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-command-pack.mjs +1 -0
  82. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-plan.mjs +45 -0
  83. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.d.mts +26 -0
  84. package/vendor/extension-sso/vendor/extension-browser-sso/src/extensions/toolkit/read-only-shell-policy.mjs +170 -0
  85. package/vendor/extension-sso/vendor/extension-browser-sso/src/htmlSessionExtract.mjs +64 -0
  86. package/vendor/extension-sso/vendor/extension-read-only-toolkit/LICENSE +202 -0
  87. package/vendor/extension-sso/vendor/extension-read-only-toolkit/README.md +29 -0
  88. package/vendor/extension-sso/vendor/extension-read-only-toolkit/package.json +43 -0
  89. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-data-paths.mjs +222 -0
  90. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/devops-diagnostics.mjs +440 -0
  91. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/enterprise-api-read.mjs +180 -0
  92. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-command-pack.mjs +1 -0
  93. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-plan.mjs +45 -0
  94. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.d.mts +26 -0
  95. package/vendor/extension-sso/vendor/extension-read-only-toolkit/src/read-only-shell-policy.mjs +170 -0
@@ -0,0 +1,222 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+
6
+ export class PathText {
7
+ static normalize(value) {
8
+ return typeof value === "string" ? value.trim() : "";
9
+ }
10
+
11
+ static resolve(path, cwd = process.cwd()) {
12
+ const normalized = PathText.normalize(path);
13
+ if (!normalized) {
14
+ return "";
15
+ }
16
+ return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
17
+ }
18
+ }
19
+
20
+ export class EnvTokenExpander {
21
+ constructor(env = process.env) {
22
+ this.env = env;
23
+ }
24
+
25
+ expand(value) {
26
+ if (typeof value !== "string") {
27
+ return "";
28
+ }
29
+ return value.replace(
30
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g,
31
+ (_match, name, fallback = "") => this.env[name] ?? fallback,
32
+ );
33
+ }
34
+ }
35
+
36
+ export class DevopsDataConfigLoader {
37
+ constructor(env = process.env) {
38
+ this.env = env;
39
+ }
40
+
41
+ configPath() {
42
+ return PathText.resolve(this.env.DEVOPS_DATA_CONFIG ?? "config/devops-data.yaml");
43
+ }
44
+
45
+ load() {
46
+ const path = this.configPath();
47
+ if (!existsSync(path)) {
48
+ return {};
49
+ }
50
+
51
+ const parsed = parseYaml(readFileSync(path, "utf8")) ?? {};
52
+ return parsed.spec?.groups
53
+ ? { dataRoot: parsed.spec.dataRoot, ...parsed.spec.groups }
54
+ : parsed;
55
+ }
56
+ }
57
+
58
+ export class DevopsDataLayout {
59
+ constructor(config = {}, env = process.env) {
60
+ this.config = config;
61
+ this.env = env;
62
+ this.expander = new EnvTokenExpander(env);
63
+ }
64
+
65
+ dataRoot() {
66
+ return PathText.resolve(
67
+ this.env.DEVOPS_DATA_ROOT ??
68
+ (this.env.DEVOPS_HOME ? `${this.env.DEVOPS_HOME}/data` : undefined) ??
69
+ (this.expander.expand(this.config.dataRoot) || undefined) ??
70
+ `${homedir()}/.devops-tool/data`,
71
+ );
72
+ }
73
+
74
+ value(path) {
75
+ return path.split(".").reduce((current, segment) => {
76
+ if (!current || typeof current !== "object") {
77
+ return undefined;
78
+ }
79
+ return current[segment];
80
+ }, this.config);
81
+ }
82
+
83
+ configuredPath(configKey, fallback) {
84
+ const configured = this.expander.expand(this.value(configKey));
85
+ if (!configured) {
86
+ return fallback;
87
+ }
88
+ return isAbsolute(configured) ? configured : resolve(this.dataRoot(), configured);
89
+ }
90
+ }
91
+
92
+ export class DevopsDataPathResolver {
93
+ constructor(env = process.env, config = new DevopsDataConfigLoader(env).load()) {
94
+ this.env = env;
95
+ this.layout = new DevopsDataLayout(config, env);
96
+ }
97
+
98
+ root() {
99
+ return this.layout.dataRoot();
100
+ }
101
+
102
+ dataPath(...segments) {
103
+ return resolve(this.root(), ...segments);
104
+ }
105
+
106
+ cachePath(...segments) {
107
+ return this.dataPath("cache", ...segments);
108
+ }
109
+
110
+ projectPath(...segments) {
111
+ return this.dataPath("project", ...segments);
112
+ }
113
+
114
+ runtimePath(...segments) {
115
+ return this.dataPath("runtime", ...segments);
116
+ }
117
+
118
+ configuredDataPath(envName, configKey, fallback) {
119
+ const envPath = PathText.resolve(this.env[envName]);
120
+ if (envPath) {
121
+ return envPath;
122
+ }
123
+ return this.layout.configuredPath(configKey, fallback);
124
+ }
125
+
126
+ codexSessionRoot() {
127
+ return this.configuredDataPath(
128
+ "DEVOPS_CODEX_SESSION_ROOT",
129
+ "project.sessions",
130
+ this.projectPath("sessions", "codex"),
131
+ );
132
+ }
133
+
134
+ codexMemoryRoot() {
135
+ return this.configuredDataPath(
136
+ "DEVOPS_CODEX_MEMORY_ROOT",
137
+ "project.codexMemory",
138
+ this.projectPath("memory", "codex"),
139
+ );
140
+ }
141
+
142
+ generatedOutputRoot() {
143
+ return this.configuredDataPath(
144
+ "DEVOPS_GENERATED_ROOT",
145
+ "project.generated",
146
+ this.projectPath("generated"),
147
+ );
148
+ }
149
+
150
+ tempDataRoot() {
151
+ return this.configuredDataPath("DEVOPS_TMP_ROOT", "tmp", this.dataPath("tmp"));
152
+ }
153
+
154
+ summary() {
155
+ return {
156
+ dataRoot: this.root(),
157
+ runtime: {
158
+ claude: this.runtimePath("claude"),
159
+ npm: this.runtimePath("npm"),
160
+ cache: this.runtimePath("cache"),
161
+ config: this.runtimePath("config"),
162
+ },
163
+ project: {
164
+ agentMemory: this.projectPath("agent-memory-local"),
165
+ memory: this.projectPath("memory"),
166
+ codexMemory: this.codexMemoryRoot(),
167
+ sessions: this.codexSessionRoot(),
168
+ generated: this.generatedOutputRoot(),
169
+ },
170
+ cache: {},
171
+ tmp: this.tempDataRoot(),
172
+ };
173
+ }
174
+ }
175
+
176
+ function resolver(env = process.env) {
177
+ return new DevopsDataPathResolver(env);
178
+ }
179
+
180
+ export function devopsDataRoot(env = process.env) {
181
+ return resolver(env).root();
182
+ }
183
+
184
+ export function devopsDataPath(...segments) {
185
+ return resolver().dataPath(...segments);
186
+ }
187
+
188
+ export function devopsCachePath(...segments) {
189
+ return resolver().cachePath(...segments);
190
+ }
191
+
192
+ export function devopsProjectDataPath(...segments) {
193
+ return resolver().projectPath(...segments);
194
+ }
195
+
196
+ export function devopsRuntimeDataPath(...segments) {
197
+ return resolver().runtimePath(...segments);
198
+ }
199
+
200
+ export function configuredDataPath(envName, configKey, fallback) {
201
+ return resolver().configuredDataPath(envName, configKey, fallback);
202
+ }
203
+
204
+ export function codexSessionRoot() {
205
+ return resolver().codexSessionRoot();
206
+ }
207
+
208
+ export function codexMemoryRoot() {
209
+ return resolver().codexMemoryRoot();
210
+ }
211
+
212
+ export function generatedOutputRoot() {
213
+ return resolver().generatedOutputRoot();
214
+ }
215
+
216
+ export function tempDataRoot() {
217
+ return resolver().tempDataRoot();
218
+ }
219
+
220
+ export function dataPathSummary() {
221
+ return resolver().summary();
222
+ }
@@ -0,0 +1,440 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream, promises as fs } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { parseAllDocuments } from "yaml";
6
+ import { z } from "zod";
7
+
8
+ export const DEFAULT_SCAN_LIMIT_BYTES = 256 * 1024 * 1024;
9
+ const MAX_SCAN_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
10
+ const OUTPUT_CHAR_LIMIT = 60_000;
11
+ const SENSITIVE_FILE_PATTERN =
12
+ /(^|[/\\])(\.env([./\\]|$)|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$|.*\.(pem|key|p12|pfx|crt)$|kubeconfig$|credentials?$|secrets?(\.|$))/i;
13
+
14
+ export const filePathSchema = z
15
+ .string()
16
+ .min(1)
17
+ .describe("Absolute path, or a path relative to the agent working directory.");
18
+
19
+ export const scanLimitSchema = z
20
+ .number()
21
+ .int()
22
+ .positive()
23
+ .max(MAX_SCAN_LIMIT_BYTES)
24
+ .default(DEFAULT_SCAN_LIMIT_BYTES)
25
+ .describe("Maximum bytes to stream from the file.");
26
+
27
+ export const allowSensitiveSchema = z
28
+ .boolean()
29
+ .default(false)
30
+ .describe("Set true only after explicit user approval to inspect paths that look like secrets or credentials.");
31
+
32
+ export function ok(text) {
33
+ return { content: [{ type: "text", text: truncate(text) }] };
34
+ }
35
+
36
+ export function fail(error) {
37
+ return {
38
+ isError: true,
39
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }]
40
+ };
41
+ }
42
+
43
+ export function defineReadOnlyTool(tool) {
44
+ return {
45
+ ...tool,
46
+ annotations: {
47
+ readOnlyHint: true,
48
+ destructiveHint: false,
49
+ ...(tool.annotations ?? {})
50
+ }
51
+ };
52
+ }
53
+
54
+ export function assertNonHtmlApiResponse(provider, text) {
55
+ const body = String(text || "");
56
+ const head = body.slice(0, 1000).toLowerCase();
57
+ if (/<html\b|<!doctype html\b|<form\b|login|atlassian-token|ajs-/.test(head)) {
58
+ throw new Error(`${provider} API returned an HTML login or browser page instead of API data. Authentication/session refresh must be handled outside this read-only tool with human-in-the-loop approval. HTML body was suppressed.`);
59
+ }
60
+ }
61
+
62
+ function truncate(text) {
63
+ if (text.length <= OUTPUT_CHAR_LIMIT) {
64
+ return text;
65
+ }
66
+ return `${text.slice(0, OUTPUT_CHAR_LIMIT)}\n\n[output truncated at ${OUTPUT_CHAR_LIMIT} characters]`;
67
+ }
68
+
69
+ function resolveFilePath(filePath) {
70
+ return isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
71
+ }
72
+
73
+ async function assertReadableFile(filePath, allowSensitive = false) {
74
+ const resolvedPath = resolveFilePath(filePath);
75
+ if (!allowSensitive && SENSITIVE_FILE_PATTERN.test(resolvedPath)) {
76
+ throw new Error("Refusing to read a path that looks sensitive. Re-run only with explicit user approval and allowSensitive=true.");
77
+ }
78
+
79
+ const stat = await fs.stat(resolvedPath);
80
+ if (!stat.isFile()) {
81
+ throw new Error(`${resolvedPath} is not a regular file.`);
82
+ }
83
+
84
+ return { resolvedPath, stat };
85
+ }
86
+
87
+ async function streamLines(filePath, scanLimitBytes, onLine) {
88
+ const stream = createReadStream(filePath, {
89
+ encoding: "utf8",
90
+ highWaterMark: 1024 * 1024
91
+ });
92
+ const reader = createInterface({ input: stream, crlfDelay: Infinity });
93
+ let bytesScanned = 0;
94
+ let lineNumber = 0;
95
+ let truncatedByScanLimit = false;
96
+
97
+ try {
98
+ for await (const line of reader) {
99
+ bytesScanned += Buffer.byteLength(`${line}\n`);
100
+ if (bytesScanned > scanLimitBytes) {
101
+ truncatedByScanLimit = true;
102
+ reader.close();
103
+ stream.destroy();
104
+ break;
105
+ }
106
+
107
+ lineNumber += 1;
108
+ await onLine(line, lineNumber);
109
+ }
110
+ } finally {
111
+ reader.close();
112
+ stream.destroy();
113
+ }
114
+
115
+ return { bytesScanned, lineNumber, truncatedByScanLimit };
116
+ }
117
+
118
+ function getPathValue(value, path) {
119
+ return path.split(".").reduce((current, part) => {
120
+ if (current === undefined || current === null) {
121
+ return undefined;
122
+ }
123
+ return current[part];
124
+ }, value);
125
+ }
126
+
127
+ function increment(map, key) {
128
+ map.set(key, (map.get(key) ?? 0) + 1);
129
+ }
130
+
131
+ function renderTopMap(title, map, limit = 20) {
132
+ const entries = [...map.entries()].sort((left, right) => right[1] - left[1]).slice(0, limit);
133
+ if (entries.length === 0) {
134
+ return `${title}\n- none`;
135
+ }
136
+ return [title, ...entries.map(([key, count]) => `- ${key}: ${count}`)].join("\n");
137
+ }
138
+
139
+ export async function logPatternSummary(input) {
140
+ const {
141
+ allowSensitive = false,
142
+ filePath,
143
+ maxSamplesPerPattern = 5,
144
+ patterns = [
145
+ "ERROR",
146
+ "WARN",
147
+ "Exception",
148
+ "Traceback",
149
+ "timeout",
150
+ "connection refused",
151
+ "5\\d\\d",
152
+ "OOMKilled",
153
+ "CrashLoopBackOff"
154
+ ],
155
+ scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES
156
+ } = input;
157
+ const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
158
+ const compiled = patterns.map((pattern) => ({ pattern, regex: new RegExp(pattern, "i"), count: 0, samples: [] }));
159
+ let firstTimestamp;
160
+ let lastTimestamp;
161
+ const timestampRegex = /\b\d{4}-\d{2}-\d{2}[T ][0-9:.+-]+Z?\b/;
162
+
163
+ const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
164
+ const timestamp = line.match(timestampRegex)?.[0];
165
+ if (timestamp) {
166
+ firstTimestamp ??= timestamp;
167
+ lastTimestamp = timestamp;
168
+ }
169
+
170
+ for (const item of compiled) {
171
+ if (item.regex.test(line)) {
172
+ item.count += 1;
173
+ if (item.samples.length < maxSamplesPerPattern) {
174
+ item.samples.push(`${lineNumber}: ${line}`);
175
+ }
176
+ }
177
+ }
178
+ });
179
+
180
+ return [
181
+ `path: ${resolvedPath}`,
182
+ `sizeBytes: ${stat.size}`,
183
+ `bytesScanned: ${scan.bytesScanned}`,
184
+ `linesScanned: ${scan.lineNumber}`,
185
+ `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
186
+ `firstTimestampSeen: ${firstTimestamp ?? "unknown"}`,
187
+ `lastTimestampSeen: ${lastTimestamp ?? "unknown"}`,
188
+ "",
189
+ "patternCounts:",
190
+ ...compiled.map((item) => `- /${item.pattern}/i: ${item.count}`),
191
+ "",
192
+ "samples:",
193
+ ...compiled.flatMap((item) => [
194
+ `## /${item.pattern}/i`,
195
+ item.samples.length ? item.samples.join("\n") : "(no samples)"
196
+ ])
197
+ ].join("\n");
198
+ }
199
+
200
+ export async function jsonLogAnalyze(input) {
201
+ const {
202
+ allowSensitive = false,
203
+ filePath,
204
+ groupBy = ["level", "status", "service"],
205
+ maxErrorSamples = 20,
206
+ scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES
207
+ } = input;
208
+ const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
209
+ const fieldCounts = new Map(groupBy.map((field) => [field, new Map()]));
210
+ const errorSamples = [];
211
+ let parsedLines = 0;
212
+ let invalidJsonLines = 0;
213
+
214
+ const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
215
+ if (!line.trim()) {
216
+ return;
217
+ }
218
+
219
+ let parsed;
220
+ try {
221
+ parsed = JSON.parse(line);
222
+ parsedLines += 1;
223
+ } catch {
224
+ invalidJsonLines += 1;
225
+ return;
226
+ }
227
+
228
+ for (const field of groupBy) {
229
+ const value = getPathValue(parsed, field);
230
+ if (value !== undefined) {
231
+ increment(fieldCounts.get(field), String(value));
232
+ }
233
+ }
234
+
235
+ const level = String(getPathValue(parsed, "level") ?? getPathValue(parsed, "severity") ?? "").toLowerCase();
236
+ const status = Number(getPathValue(parsed, "status") ?? getPathValue(parsed, "statusCode") ?? 0);
237
+ const message = String(getPathValue(parsed, "message") ?? getPathValue(parsed, "msg") ?? getPathValue(parsed, "error.message") ?? "");
238
+ if ((level && ["error", "fatal", "warn", "warning"].includes(level)) || status >= 400 || /error|exception|timeout/i.test(message)) {
239
+ if (errorSamples.length < maxErrorSamples) {
240
+ errorSamples.push(`${lineNumber}: ${line}`);
241
+ }
242
+ }
243
+ });
244
+
245
+ return [
246
+ `path: ${resolvedPath}`,
247
+ `sizeBytes: ${stat.size}`,
248
+ `bytesScanned: ${scan.bytesScanned}`,
249
+ `linesScanned: ${scan.lineNumber}`,
250
+ `parsedJsonLines: ${parsedLines}`,
251
+ `invalidJsonLines: ${invalidJsonLines}`,
252
+ `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
253
+ "",
254
+ ...[...fieldCounts.entries()].map(([field, map]) => renderTopMap(`top ${field}:`, map)),
255
+ "",
256
+ "errorSamples:",
257
+ errorSamples.length ? errorSamples.join("\n") : "(none)"
258
+ ].join("\n");
259
+ }
260
+
261
+ async function fetchWithTimeout(url, options = {}, timeoutMs = 15_000) {
262
+ const controller = new AbortController();
263
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
264
+ const startedAt = Date.now();
265
+ try {
266
+ const response = await fetch(url, { ...options, signal: controller.signal });
267
+ const durationMs = Date.now() - startedAt;
268
+ return { response, durationMs };
269
+ } finally {
270
+ clearTimeout(timer);
271
+ }
272
+ }
273
+
274
+ function selectedHeaders(headers, names = []) {
275
+ const selected = {};
276
+ const wanted = names.length ? names : ["content-type", "cache-control", "location", "server", "x-request-id"];
277
+ for (const name of wanted) {
278
+ const value = headers.get(name);
279
+ if (value !== null) {
280
+ selected[name.toLowerCase()] = value;
281
+ }
282
+ }
283
+ return selected;
284
+ }
285
+
286
+ async function responseSummary(url, input = {}) {
287
+ const method = input.method ?? "GET";
288
+ const { response, durationMs } = await fetchWithTimeout(url, { method }, input.timeoutMs ?? 15_000);
289
+ const body = method === "HEAD" ? "" : await response.text();
290
+ const bodyPreviewLimit = input.bodyPreviewChars ?? 2_000;
291
+ const bodyHash = createHash("sha256").update(body).digest("hex");
292
+ let json;
293
+ try {
294
+ json = body ? JSON.parse(body) : undefined;
295
+ } catch {
296
+ json = undefined;
297
+ }
298
+
299
+ return {
300
+ bodyHash,
301
+ bodyPreview: body.slice(0, bodyPreviewLimit),
302
+ durationMs,
303
+ headers: selectedHeaders(response.headers, input.headers),
304
+ json,
305
+ status: response.status,
306
+ statusText: response.statusText,
307
+ url: response.url
308
+ };
309
+ }
310
+
311
+ export async function httpProbe(input) {
312
+ const summary = await responseSummary(input.url, input);
313
+ return JSON.stringify(summary, null, 2);
314
+ }
315
+
316
+ function walkManifests(value, visit, path = []) {
317
+ if (!value || typeof value !== "object") {
318
+ return;
319
+ }
320
+ visit(value, path);
321
+ if (Array.isArray(value)) {
322
+ value.forEach((item, index) => walkManifests(item, visit, [...path, String(index)]));
323
+ } else {
324
+ Object.entries(value).forEach(([key, item]) => walkManifests(item, visit, [...path, key]));
325
+ }
326
+ }
327
+
328
+ export async function kubeManifestLintReadonly(input) {
329
+ const { allowSensitive = false, filePath } = input;
330
+ const { resolvedPath } = await assertReadableFile(filePath, allowSensitive);
331
+ const text = await fs.readFile(resolvedPath, "utf8");
332
+ const docs = parseAllDocuments(text).map((doc) => doc.toJSON()).filter(Boolean);
333
+ const findings = [];
334
+
335
+ for (const doc of docs) {
336
+ const kind = doc.kind ?? "Unknown";
337
+ const name = doc.metadata?.name ?? "unnamed";
338
+ walkManifests(doc, (node, path) => {
339
+ const pathText = path.join(".");
340
+ if (node.image && typeof node.image === "string" && /:latest$|^[^:]+$/.test(node.image)) {
341
+ findings.push(`[image] ${kind}/${name} ${pathText}.image uses an unpinned or latest tag: ${node.image}`);
342
+ }
343
+ if (node.privileged === true) {
344
+ findings.push(`[security] ${kind}/${name} ${pathText}.privileged is true`);
345
+ }
346
+ if (node.hostPath) {
347
+ findings.push(`[security] ${kind}/${name} ${pathText}.hostPath is used`);
348
+ }
349
+ if (node.containers && Array.isArray(node.containers)) {
350
+ for (const container of node.containers) {
351
+ const containerName = container.name ?? "unnamed-container";
352
+ if (!container.resources) {
353
+ findings.push(`[resources] ${kind}/${name} container ${containerName} has no resources block`);
354
+ }
355
+ if (!container.livenessProbe && !container.readinessProbe && kind !== "Job" && kind !== "CronJob") {
356
+ findings.push(`[probes] ${kind}/${name} container ${containerName} has no liveness/readiness probe`);
357
+ }
358
+ if (!container.securityContext && !doc.spec?.template?.spec?.securityContext) {
359
+ findings.push(`[security] ${kind}/${name} container ${containerName} has no securityContext`);
360
+ }
361
+ }
362
+ }
363
+ });
364
+ }
365
+
366
+ return [
367
+ `path: ${resolvedPath}`,
368
+ `documents: ${docs.length}`,
369
+ `findings: ${findings.length}`,
370
+ "",
371
+ findings.length ? findings.join("\n") : "No common manifest hygiene findings."
372
+ ].join("\n");
373
+ }
374
+
375
+ export async function ciLogSummary(input) {
376
+ const { allowSensitive = false, filePath, scanLimitBytes = DEFAULT_SCAN_LIMIT_BYTES } = input;
377
+ const { resolvedPath, stat } = await assertReadableFile(filePath, allowSensitive);
378
+ const errorRegex = /\b(error|failed|failure|exception|traceback|exited with code|npm ERR!|fatal:)\b/i;
379
+ const stepRegex = /(?:^|\s)(?:##\[group\]|##\[section\]|Step \d+\/\d+|Running step|Run )(.{1,160})/i;
380
+ const errors = [];
381
+ const stepCounts = new Map();
382
+ let currentStep = "unknown";
383
+
384
+ const scan = await streamLines(resolvedPath, scanLimitBytes, (line, lineNumber) => {
385
+ const stepMatch = line.match(stepRegex);
386
+ if (stepMatch?.[1]) {
387
+ currentStep = stepMatch[1].trim();
388
+ increment(stepCounts, currentStep);
389
+ }
390
+ if (errorRegex.test(line) && errors.length < 80) {
391
+ errors.push(`${lineNumber} [${currentStep}]: ${line}`);
392
+ }
393
+ });
394
+
395
+ return [
396
+ `path: ${resolvedPath}`,
397
+ `sizeBytes: ${stat.size}`,
398
+ `bytesScanned: ${scan.bytesScanned}`,
399
+ `linesScanned: ${scan.lineNumber}`,
400
+ `truncatedByScanLimit: ${scan.truncatedByScanLimit}`,
401
+ "",
402
+ renderTopMap("topStepsSeen:", stepCounts, 15),
403
+ "",
404
+ "failureLines:",
405
+ errors.length ? errors.join("\n") : "(none)"
406
+ ].join("\n");
407
+ }
408
+
409
+ function diffObject(left, right) {
410
+ const keys = [...new Set([...Object.keys(left ?? {}), ...Object.keys(right ?? {})])].sort();
411
+ return keys
412
+ .filter((key) => left?.[key] !== right?.[key])
413
+ .map((key) => `- ${key}: left=${JSON.stringify(left?.[key])} right=${JSON.stringify(right?.[key])}`);
414
+ }
415
+
416
+ export async function endpointDiff(input) {
417
+ const left = await responseSummary(input.leftUrl, input);
418
+ const right = await responseSummary(input.rightUrl, input);
419
+ const jsonFields = input.jsonFields ?? [];
420
+ const leftJson = Object.fromEntries(jsonFields.map((field) => [field, getPathValue(left.json, field)]));
421
+ const rightJson = Object.fromEntries(jsonFields.map((field) => [field, getPathValue(right.json, field)]));
422
+
423
+ return [
424
+ `leftUrl: ${left.url}`,
425
+ `rightUrl: ${right.url}`,
426
+ `leftStatus: ${left.status} ${left.statusText}`,
427
+ `rightStatus: ${right.status} ${right.statusText}`,
428
+ `leftDurationMs: ${left.durationMs}`,
429
+ `rightDurationMs: ${right.durationMs}`,
430
+ `leftBodySha256: ${left.bodyHash}`,
431
+ `rightBodySha256: ${right.bodyHash}`,
432
+ `bodyHashesEqual: ${left.bodyHash === right.bodyHash}`,
433
+ "",
434
+ "headerDiffs:",
435
+ diffObject(left.headers, right.headers).join("\n") || "(none)",
436
+ "",
437
+ "jsonFieldDiffs:",
438
+ diffObject(leftJson, rightJson).join("\n") || "(none)"
439
+ ].join("\n");
440
+ }