@saptools/cf-events 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.
package/dist/index.js ADDED
@@ -0,0 +1,873 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/api.ts
4
+ var MAX_PER_PAGE = 100;
5
+ function parseJson(raw) {
6
+ try {
7
+ return JSON.parse(raw);
8
+ } catch {
9
+ throw new Error("The CF API returned a response that is not valid JSON.");
10
+ }
11
+ }
12
+ function asRecord(value) {
13
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
14
+ return value;
15
+ }
16
+ return {};
17
+ }
18
+ function asArray(value) {
19
+ return Array.isArray(value) ? value : [];
20
+ }
21
+ function asString(value) {
22
+ return typeof value === "string" ? value : "";
23
+ }
24
+ function asNumber(value) {
25
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
26
+ }
27
+ function mapEntityRef(value) {
28
+ const record = asRecord(value);
29
+ return {
30
+ guid: asString(record["guid"]),
31
+ type: asString(record["type"]),
32
+ name: asString(record["name"])
33
+ };
34
+ }
35
+ function optionalGuid(value) {
36
+ const guid = asString(asRecord(value)["guid"]);
37
+ return guid.length > 0 ? guid : void 0;
38
+ }
39
+ function mapAuditEvent(value) {
40
+ const record = asRecord(value);
41
+ return {
42
+ guid: asString(record["guid"]),
43
+ type: asString(record["type"]),
44
+ createdAt: asString(record["created_at"]),
45
+ updatedAt: asString(record["updated_at"]),
46
+ actor: mapEntityRef(record["actor"]),
47
+ target: mapEntityRef(record["target"]),
48
+ data: asRecord(record["data"]),
49
+ spaceGuid: optionalGuid(record["space"]),
50
+ organizationGuid: optionalGuid(record["organization"])
51
+ };
52
+ }
53
+ function mapProcessStat(value) {
54
+ const record = asRecord(value);
55
+ const usage = asRecord(record["usage"]);
56
+ return {
57
+ type: asString(record["type"]),
58
+ index: asNumber(record["index"]) ?? 0,
59
+ state: asString(record["state"]),
60
+ uptimeSeconds: asNumber(record["uptime"]),
61
+ cpu: asNumber(usage["cpu"]),
62
+ memBytes: asNumber(usage["mem"]),
63
+ memQuotaBytes: asNumber(record["mem_quota"]),
64
+ diskBytes: asNumber(usage["disk"]),
65
+ diskQuotaBytes: asNumber(record["disk_quota"])
66
+ };
67
+ }
68
+ function buildAuditEventsPath(input, perPage) {
69
+ const params = new URLSearchParams();
70
+ params.set("target_guids", input.appGuid);
71
+ params.set("order_by", "-created_at");
72
+ params.set("per_page", perPage.toString());
73
+ if (input.types !== void 0 && input.types.length > 0) {
74
+ params.set("types", input.types.join(","));
75
+ }
76
+ if (input.createdAfter !== void 0) {
77
+ params.set("created_ats[gt]", input.createdAfter);
78
+ }
79
+ return `/v3/audit_events?${params.toString()}`;
80
+ }
81
+ function nextPagePath(body) {
82
+ const next = asRecord(asRecord(asRecord(body)["pagination"])["next"]);
83
+ const href = next["href"];
84
+ if (typeof href !== "string" || href.length === 0) {
85
+ return void 0;
86
+ }
87
+ try {
88
+ const url = new URL(href);
89
+ return `${url.pathname}${url.search}`;
90
+ } catch {
91
+ return void 0;
92
+ }
93
+ }
94
+ async function fetchAuditEvents(input, curl) {
95
+ const limit = Math.max(input.limit, 1);
96
+ const perPage = Math.min(limit, MAX_PER_PAGE);
97
+ const events = [];
98
+ let path = buildAuditEventsPath(input, perPage);
99
+ while (path !== void 0 && events.length < limit) {
100
+ const body = parseJson(await curl(path));
101
+ for (const resource of asArray(asRecord(body)["resources"])) {
102
+ events.push(mapAuditEvent(resource));
103
+ if (events.length >= limit) {
104
+ break;
105
+ }
106
+ }
107
+ path = events.length < limit ? nextPagePath(body) : void 0;
108
+ }
109
+ return events;
110
+ }
111
+ async function fetchApp(appGuid, curl) {
112
+ const body = asRecord(parseJson(await curl(`/v3/apps/${appGuid}`)));
113
+ return {
114
+ guid: asString(body["guid"]),
115
+ name: asString(body["name"]),
116
+ state: asString(body["state"])
117
+ };
118
+ }
119
+ async function fetchSshEnabled(appGuid, curl) {
120
+ const body = asRecord(parseJson(await curl(`/v3/apps/${appGuid}/ssh_enabled`)));
121
+ return {
122
+ enabled: body["enabled"] === true,
123
+ reason: asString(body["reason"])
124
+ };
125
+ }
126
+ async function fetchWebProcessStats(appGuid, curl) {
127
+ const body = asRecord(parseJson(await curl(`/v3/apps/${appGuid}/processes/web/stats`)));
128
+ return asArray(body["resources"]).map(mapProcessStat);
129
+ }
130
+
131
+ // src/cf.ts
132
+ import { execFile } from "child_process";
133
+ import { mkdtemp, rm } from "fs/promises";
134
+ import { tmpdir } from "os";
135
+ import { join } from "path";
136
+ var CF_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
137
+ var CF_COMMAND_TIMEOUT_MS = 3e4;
138
+ var CF_RETRY_MAX_ATTEMPTS = 3;
139
+ var CF_RETRY_BASE_DELAY_MS = 120;
140
+ var GUID_PATTERN = /^[0-9a-f-]{16,}$/i;
141
+ async function withCfSession(work) {
142
+ const cfHomeDir = await mkdtemp(join(tmpdir(), "saptools-cf-events-"));
143
+ try {
144
+ return await work({ cfHomeDir });
145
+ } finally {
146
+ await rm(cfHomeDir, { recursive: true, force: true });
147
+ }
148
+ }
149
+ async function prepareCfCliSession(input, ctx) {
150
+ await runCfCommand(["api", input.apiEndpoint], ctx, "Failed to set the CF API endpoint.");
151
+ await runCfCommand(["auth"], ctx, "Failed to authenticate the Cloud Foundry CLI.", {
152
+ CF_USERNAME: input.credentials.email,
153
+ CF_PASSWORD: input.credentials.password
154
+ });
155
+ await runCfCommand(
156
+ ["target", "-o", input.orgName, "-s", input.spaceName],
157
+ ctx,
158
+ "Failed to target the CF org and space."
159
+ );
160
+ }
161
+ async function cfAppGuid(appName, ctx) {
162
+ const stdout = await runCfCommand(
163
+ ["app", appName, "--guid"],
164
+ ctx,
165
+ `Failed to resolve the GUID for app "${appName}".`
166
+ );
167
+ const guid = stdout.trim();
168
+ if (!GUID_PATTERN.test(guid)) {
169
+ throw new Error(
170
+ `Could not resolve the GUID for app "${appName}" - the CF CLI returned an unexpected value.`
171
+ );
172
+ }
173
+ return guid;
174
+ }
175
+ async function cfCurl(path, ctx) {
176
+ return await runCfCommand(["curl", path], ctx, `Failed to call the CF API path "${path}".`);
177
+ }
178
+ async function runCfCommand(args, ctx, failureMessage, envOverrides = {}) {
179
+ const command = resolveCommand();
180
+ const env = buildEnv(ctx.cfHomeDir, envOverrides);
181
+ let lastError;
182
+ for (let attempt = 1; attempt <= CF_RETRY_MAX_ATTEMPTS; attempt += 1) {
183
+ try {
184
+ return await execFileWithResult(command.bin, [...command.argsPrefix, ...args], env);
185
+ } catch (error) {
186
+ lastError = error;
187
+ if (attempt >= CF_RETRY_MAX_ATTEMPTS || !shouldRetryCfCliError(error)) {
188
+ break;
189
+ }
190
+ await sleep(resolveRetryDelayMs(attempt));
191
+ }
192
+ }
193
+ const detail = extractSafeCliDetail(lastError);
194
+ throw new Error(detail.length > 0 ? `${failureMessage} ${detail}` : failureMessage, {
195
+ cause: lastError
196
+ });
197
+ }
198
+ function resolveCommand() {
199
+ const bin = process.env["CF_EVENTS_CF_BIN"] ?? "cf";
200
+ if (/\.(?:c|m)?js$/i.test(bin)) {
201
+ return { bin: process.execPath, argsPrefix: [bin] };
202
+ }
203
+ return { bin, argsPrefix: [] };
204
+ }
205
+ function buildEnv(cfHomeDir, overrides) {
206
+ const env = { ...process.env };
207
+ delete env["SAP_EMAIL"];
208
+ delete env["SAP_PASSWORD"];
209
+ env["CF_HOME"] = cfHomeDir;
210
+ Object.assign(env, overrides);
211
+ return env;
212
+ }
213
+ async function execFileWithResult(command, args, env) {
214
+ return await new Promise((resolvePromise, rejectPromise) => {
215
+ execFile(
216
+ command,
217
+ [...args],
218
+ { env, maxBuffer: CF_MAX_BUFFER_BYTES, timeout: CF_COMMAND_TIMEOUT_MS },
219
+ (error, stdout, stderr) => {
220
+ if (error !== null) {
221
+ rejectPromise(Object.assign(error, { stdout, stderr }));
222
+ return;
223
+ }
224
+ resolvePromise(stdout);
225
+ }
226
+ );
227
+ });
228
+ }
229
+ function shouldRetryCfCliError(error) {
230
+ const haystack = `${readErrorStderr(error)} ${readErrorMessage(error)}`.toLowerCase();
231
+ if (haystack.includes("invalid") || haystack.includes("credentials were rejected") || haystack.includes("authentication failed") || haystack.includes("not authorized") || haystack.includes("forbidden")) {
232
+ return false;
233
+ }
234
+ return haystack.includes("timeout") || haystack.includes("timed out") || haystack.includes("connection reset") || haystack.includes("temporarily unavailable") || haystack.includes("network") || haystack.includes("econnreset") || haystack.includes("econnrefused");
235
+ }
236
+ function readErrorStderr(error) {
237
+ if (typeof error !== "object" || error === null) {
238
+ return "";
239
+ }
240
+ const stderr = error.stderr;
241
+ return typeof stderr === "string" ? stderr : "";
242
+ }
243
+ function readErrorMessage(error) {
244
+ return error instanceof Error ? error.message : "";
245
+ }
246
+ function resolveRetryDelayMs(attempt) {
247
+ const jitter = Math.floor(Math.random() * CF_RETRY_BASE_DELAY_MS);
248
+ return CF_RETRY_BASE_DELAY_MS * attempt + jitter;
249
+ }
250
+ async function sleep(delayMs) {
251
+ await new Promise((resolvePromise) => {
252
+ setTimeout(resolvePromise, delayMs);
253
+ });
254
+ }
255
+ function extractSafeCliDetail(error) {
256
+ const stderr = readErrorStderr(error);
257
+ if (stderr.length === 0) {
258
+ return "";
259
+ }
260
+ const cleaned = stderr.replaceAll(/\s+/g, " ").trim();
261
+ return cleaned.length > 0 ? `(cli: ${cleaned.slice(0, 180)})` : "";
262
+ }
263
+
264
+ // src/types.ts
265
+ var APP_EVENT_TYPES = [
266
+ "audit.app.create",
267
+ "audit.app.update",
268
+ "audit.app.delete-request",
269
+ "audit.app.start",
270
+ "audit.app.stop",
271
+ "audit.app.restage",
272
+ "audit.app.ssh-authorized",
273
+ "audit.app.ssh-unauthorized",
274
+ "audit.app.crash",
275
+ "audit.app.process.create",
276
+ "audit.app.process.scale",
277
+ "audit.app.process.update",
278
+ "audit.app.process.crash",
279
+ "audit.app.process.terminate_instance",
280
+ "audit.app.map-route",
281
+ "audit.app.unmap-route",
282
+ "audit.app.environment_variables.show"
283
+ ];
284
+ var SSH_EVENT_TYPES = ["audit.app.ssh-authorized", "audit.app.ssh-unauthorized"];
285
+ var CRASH_EVENT_TYPES = ["audit.app.crash", "audit.app.process.crash"];
286
+
287
+ // src/events.ts
288
+ var DURATION_PATTERN = /^(\d+)\s*(s|m|h|d)$/;
289
+ var DURATION_UNIT_MS = {
290
+ s: 1e3,
291
+ m: 6e4,
292
+ h: 36e5,
293
+ d: 864e5
294
+ };
295
+ function isCrashEvent(event) {
296
+ return CRASH_EVENT_TYPES.includes(event.type);
297
+ }
298
+ function isSshEvent(event) {
299
+ return SSH_EVENT_TYPES.includes(event.type);
300
+ }
301
+ function filterByTypes(events, types) {
302
+ if (types.length === 0) {
303
+ return events;
304
+ }
305
+ const allowed = new Set(types);
306
+ return events.filter((event) => allowed.has(event.type));
307
+ }
308
+ function sortEventsNewestFirst(events) {
309
+ return [...events].sort((left, right) => right.createdAt.localeCompare(left.createdAt));
310
+ }
311
+ function parseDuration(raw) {
312
+ const match = DURATION_PATTERN.exec(raw.trim().toLowerCase());
313
+ const amountToken = match?.[1];
314
+ const unitToken = match?.[2];
315
+ if (amountToken === void 0 || unitToken === void 0) {
316
+ throw new Error(`Invalid duration "${raw}". Use forms like 30m, 6h, or 7d.`);
317
+ }
318
+ const amount = Number.parseInt(amountToken, 10);
319
+ const unitMs = DURATION_UNIT_MS[unitToken];
320
+ if (unitMs === void 0 || amount <= 0) {
321
+ throw new Error(`Invalid duration "${raw}". Use forms like 30m, 6h, or 7d.`);
322
+ }
323
+ return amount * unitMs;
324
+ }
325
+ function durationToCreatedAfter(raw, now) {
326
+ return new Date(now.getTime() - parseDuration(raw)).toISOString();
327
+ }
328
+ function parseTypeFilter(raw) {
329
+ if (raw === void 0) {
330
+ return [];
331
+ }
332
+ const tokens = raw.split(",").map((token) => token.trim()).filter((token) => token.length > 0);
333
+ const resolved = [];
334
+ for (const token of tokens) {
335
+ if (token === "ssh") {
336
+ resolved.push(...SSH_EVENT_TYPES);
337
+ } else if (token === "crash") {
338
+ resolved.push(...CRASH_EVENT_TYPES);
339
+ } else if (token.startsWith("audit.") || token.startsWith("app.")) {
340
+ resolved.push(token);
341
+ } else {
342
+ throw new Error(
343
+ `Unknown event type "${token}". Use a full CF event type (e.g. audit.app.start) or the shorthand "ssh"/"crash".`
344
+ );
345
+ }
346
+ }
347
+ return [...new Set(resolved)];
348
+ }
349
+ function readString(value) {
350
+ return typeof value === "string" && value.length > 0 ? value : void 0;
351
+ }
352
+ function readNumber(value) {
353
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
354
+ }
355
+ function toCrashRecord(event) {
356
+ return {
357
+ at: event.createdAt,
358
+ index: readNumber(event.data["index"]),
359
+ reason: readString(event.data["reason"]) ?? readString(event.data["exit_description"]),
360
+ exitStatus: readNumber(event.data["exit_status"])
361
+ };
362
+ }
363
+ function summarizeCrashes(appName, events) {
364
+ const crashes = events.filter(isCrashEvent).map(toCrashRecord).sort((left, right) => right.at.localeCompare(left.at));
365
+ const last = crashes[0];
366
+ return {
367
+ appName,
368
+ crashCount: crashes.length,
369
+ lastCrashAt: last?.at,
370
+ lastCrashReason: last?.reason,
371
+ crashes
372
+ };
373
+ }
374
+
375
+ // src/format.ts
376
+ var EVENT_LABELS = {
377
+ "audit.app.create": "App created",
378
+ "audit.app.update": "App updated",
379
+ "audit.app.delete-request": "App delete requested",
380
+ "audit.app.start": "App started",
381
+ "audit.app.stop": "App stopped",
382
+ "audit.app.restage": "App restaged",
383
+ "audit.app.ssh-authorized": "SSH session authorized",
384
+ "audit.app.ssh-unauthorized": "SSH attempt denied",
385
+ "audit.app.crash": "App crashed",
386
+ "audit.app.process.create": "Process created",
387
+ "audit.app.process.scale": "Process scaled",
388
+ "audit.app.process.update": "Process updated",
389
+ "audit.app.process.crash": "Process crashed",
390
+ "audit.app.process.terminate_instance": "Instance terminated",
391
+ "audit.app.map-route": "Route mapped",
392
+ "audit.app.unmap-route": "Route unmapped",
393
+ "audit.app.environment_variables.show": "Env variables viewed"
394
+ };
395
+ function describeEventType(type) {
396
+ return EVENT_LABELS[type] ?? type;
397
+ }
398
+ function formatUptime(seconds) {
399
+ if (seconds === void 0 || seconds <= 0) {
400
+ return "-";
401
+ }
402
+ const totalMinutes = Math.floor(seconds / 60);
403
+ const days = Math.floor(totalMinutes / 1440);
404
+ const hours = Math.floor(totalMinutes % 1440 / 60);
405
+ const minutes = totalMinutes % 60;
406
+ const parts = [];
407
+ if (days > 0) {
408
+ parts.push(`${days.toString()}d`);
409
+ }
410
+ if (hours > 0) {
411
+ parts.push(`${hours.toString()}h`);
412
+ }
413
+ if (minutes > 0 || parts.length === 0) {
414
+ parts.push(`${minutes.toString()}m`);
415
+ }
416
+ return parts.join(" ");
417
+ }
418
+ function formatBytes(bytes) {
419
+ if (bytes === void 0 || bytes < 0) {
420
+ return "-";
421
+ }
422
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
423
+ let value = bytes;
424
+ let unitIndex = 0;
425
+ while (value >= 1024 && unitIndex < units.length - 1) {
426
+ value /= 1024;
427
+ unitIndex += 1;
428
+ }
429
+ return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex] ?? "B"}`;
430
+ }
431
+ function formatCpu(cpu) {
432
+ if (cpu === void 0) {
433
+ return "-";
434
+ }
435
+ return `${(cpu * 100).toFixed(1)}%`;
436
+ }
437
+ function formatRelativeTime(timestamp, now) {
438
+ const at = Date.parse(timestamp);
439
+ if (Number.isNaN(at)) {
440
+ return "unknown";
441
+ }
442
+ const deltaMs = now.getTime() - at;
443
+ if (deltaMs < 6e4) {
444
+ return "just now";
445
+ }
446
+ const minutes = Math.floor(deltaMs / 6e4);
447
+ if (minutes < 60) {
448
+ return `${minutes.toString()}m ago`;
449
+ }
450
+ const hours = Math.floor(minutes / 60);
451
+ if (hours < 24) {
452
+ return `${hours.toString()}h ago`;
453
+ }
454
+ return `${Math.floor(hours / 24).toString()}d ago`;
455
+ }
456
+ function formatEventLine(event, now) {
457
+ const actor = event.actor.name.length > 0 ? event.actor.name : event.actor.type;
458
+ return [
459
+ ` ${event.createdAt.padEnd(26)}`,
460
+ formatRelativeTime(event.createdAt, now).padEnd(11),
461
+ describeEventType(event.type).padEnd(24),
462
+ actor.length > 0 ? actor : "(unknown actor)"
463
+ ].join(" ");
464
+ }
465
+ function formatEventsReport(appName, events, now) {
466
+ if (events.length === 0) {
467
+ return `No audit events found for ${appName}.`;
468
+ }
469
+ return [
470
+ `Audit events for ${appName} (${events.length.toString()}):`,
471
+ ...events.map((event) => formatEventLine(event, now))
472
+ ].join("\n");
473
+ }
474
+ function formatSshStatusReport(status, now) {
475
+ const sessionLines = status.sessions.map((session) => {
476
+ const tag = session.likelyActive ? "[active]" : "[past] ";
477
+ const when = formatRelativeTime(session.authorizedAt, now);
478
+ return ` ${tag} ${session.actor.padEnd(30)} authorized ${session.authorizedAt} (${when})`;
479
+ });
480
+ const deniedLines = status.deniedAttempts.map((attempt) => {
481
+ const actor = attempt.actor.name.length > 0 ? attempt.actor.name : attempt.actor.type;
482
+ return ` ${actor.padEnd(30)} ${attempt.createdAt}`;
483
+ });
484
+ return [
485
+ `SSH status for ${status.appName}`,
486
+ "",
487
+ ` SSH enabled: ${status.sshEnabled ? "yes" : "no"}`,
488
+ ...!status.sshEnabled && status.sshReason.length > 0 ? [` Disabled reason: ${status.sshReason}`] : [],
489
+ ` Recent SSH sessions: ${status.sessions.length.toString()}`,
490
+ ` Likely active sessions: ${status.activeSessionCount.toString()}`,
491
+ ...sessionLines.length > 0 ? ["", " Sessions:", ...sessionLines] : [],
492
+ ...deniedLines.length > 0 ? ["", ` Denied SSH attempts: ${status.deniedAttempts.length.toString()}`, ...deniedLines] : [],
493
+ "",
494
+ ' Note: Cloud Foundry exposes no live-session API; "likely active" sessions',
495
+ " are inferred from recent ssh-authorized audit events."
496
+ ].join("\n");
497
+ }
498
+ function formatCrashReport(summary, now) {
499
+ if (summary.crashCount === 0) {
500
+ return `No crashes found for ${summary.appName}.`;
501
+ }
502
+ const crashLines = summary.crashes.map((crash) => {
503
+ const index = crash.index === void 0 ? "-" : `#${crash.index.toString()}`;
504
+ const reason = crash.reason ?? "unknown";
505
+ const exit = crash.exitStatus === void 0 ? "" : ` exit ${crash.exitStatus.toString()}`;
506
+ return ` ${crash.at} instance ${index} ${reason}${exit}`;
507
+ });
508
+ return [
509
+ `Crash report for ${summary.appName}`,
510
+ "",
511
+ ` Crashes: ${summary.crashCount.toString()}`,
512
+ ...summary.lastCrashAt === void 0 ? [] : [` Last crash: ${summary.lastCrashAt} (${formatRelativeTime(summary.lastCrashAt, now)})`],
513
+ ...summary.lastCrashReason === void 0 ? [] : [` Last reason: ${summary.lastCrashReason}`],
514
+ "",
515
+ " Recent crashes:",
516
+ ...crashLines
517
+ ].join("\n");
518
+ }
519
+ function formatInstanceLine(instance) {
520
+ return [
521
+ ` #${instance.index.toString()}`.padEnd(6),
522
+ instance.state.padEnd(10),
523
+ `up ${formatUptime(instance.uptimeSeconds)}`.padEnd(16),
524
+ `cpu ${formatCpu(instance.cpu)}`.padEnd(13),
525
+ `mem ${formatBytes(instance.memBytes)} / ${formatBytes(instance.memQuotaBytes)}`
526
+ ].join(" ");
527
+ }
528
+ function formatStatusReport(health, now) {
529
+ const lastEventLine = health.lastEvent === void 0 ? " Last event: (none)" : ` Last event: ${describeEventType(health.lastEvent.type)} - ${health.lastEvent.createdAt} (${formatRelativeTime(health.lastEvent.createdAt, now)})`;
530
+ return [
531
+ `App status: ${health.appName}`,
532
+ "",
533
+ ` GUID: ${health.appGuid}`,
534
+ ` Requested state: ${health.requestedState.length > 0 ? health.requestedState : "unknown"}`,
535
+ ` SSH enabled: ${health.sshEnabled ? "yes" : "no"}`,
536
+ ` Instances: ${health.instances.length.toString()}`,
537
+ ...health.instances.length > 0 ? ["", ...health.instances.map((instance) => formatInstanceLine(instance))] : [],
538
+ "",
539
+ lastEventLine
540
+ ].join("\n");
541
+ }
542
+
543
+ // src/runtime.ts
544
+ import { setTimeout as delayPromise } from "timers/promises";
545
+
546
+ // src/selector.ts
547
+ import { readStructure } from "@saptools/cf-sync";
548
+ var SELECTOR_USAGE = 'use "region/org/space/app" or a bare app name';
549
+ function parseSelector(raw) {
550
+ const trimmed = raw.trim();
551
+ if (trimmed.length === 0) {
552
+ throw new Error(`A selector is required: ${SELECTOR_USAGE}.`);
553
+ }
554
+ const parts = trimmed.split("/").map((part) => part.trim());
555
+ if (parts.length === 1) {
556
+ const appName = parts[0] ?? "";
557
+ if (appName.length === 0) {
558
+ throw new Error(`A selector is required: ${SELECTOR_USAGE}.`);
559
+ }
560
+ return { kind: "appName", appName };
561
+ }
562
+ if (parts.length === 4) {
563
+ const [regionKey, orgName, spaceName, appName] = parts;
564
+ if (regionKey === void 0 || orgName === void 0 || spaceName === void 0 || appName === void 0 || regionKey.length === 0 || orgName.length === 0 || spaceName.length === 0 || appName.length === 0) {
565
+ throw new Error(`Invalid selector "${raw}": every region/org/space/app segment must be non-empty.`);
566
+ }
567
+ return { kind: "explicit", regionKey, orgName, spaceName, appName };
568
+ }
569
+ throw new Error(`Invalid selector "${raw}": ${SELECTOR_USAGE}.`);
570
+ }
571
+ function resolveExplicit(raw, parsed, structure) {
572
+ const region = structure.regions.find((entry) => entry.key === parsed.regionKey);
573
+ if (region === void 0) {
574
+ throw new Error(
575
+ `Region "${parsed.regionKey}" is not in the CF topology snapshot. Run \`cf-sync sync\` first (or \`cf-sync regions\` to list valid region keys).`
576
+ );
577
+ }
578
+ const org = region.orgs.find((entry) => entry.name === parsed.orgName);
579
+ if (org === void 0) {
580
+ throw new Error(
581
+ `Org "${parsed.orgName}" was not found in region ${parsed.regionKey}. Run \`cf-sync orgs ${parsed.regionKey}\` to refresh it.`
582
+ );
583
+ }
584
+ const space = org.spaces.find((entry) => entry.name === parsed.spaceName);
585
+ if (space === void 0) {
586
+ throw new Error(
587
+ `Space "${parsed.spaceName}" was not found in ${parsed.regionKey}/${parsed.orgName}. Run \`cf-sync org ${parsed.regionKey} ${parsed.orgName}\` to refresh it.`
588
+ );
589
+ }
590
+ const app = space.apps.find((entry) => entry.name === parsed.appName);
591
+ if (app === void 0) {
592
+ throw new Error(
593
+ `App "${parsed.appName}" was not found in ${parsed.regionKey}/${parsed.orgName}/${parsed.spaceName}. Run \`cf-sync space ${parsed.regionKey} ${parsed.orgName} ${parsed.spaceName}\` to refresh it.`
594
+ );
595
+ }
596
+ return {
597
+ raw,
598
+ regionKey: region.key,
599
+ apiEndpoint: region.apiEndpoint,
600
+ orgName: org.name,
601
+ spaceName: space.name,
602
+ appName: app.name
603
+ };
604
+ }
605
+ function resolveByAppName(raw, appName, structure) {
606
+ const matches = [];
607
+ for (const region of structure.regions) {
608
+ for (const org of region.orgs) {
609
+ for (const space of org.spaces) {
610
+ for (const app of space.apps) {
611
+ if (app.name === appName) {
612
+ matches.push({
613
+ raw,
614
+ regionKey: region.key,
615
+ apiEndpoint: region.apiEndpoint,
616
+ orgName: org.name,
617
+ spaceName: space.name,
618
+ appName: app.name
619
+ });
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+ if (matches.length === 0) {
626
+ throw new Error(
627
+ `App "${appName}" was not found in the CF topology snapshot. Run \`cf-sync sync\` first, or pass a full region/org/space/app selector.`
628
+ );
629
+ }
630
+ if (matches.length > 1) {
631
+ const candidates = matches.map((match) => ` ${match.regionKey}/${match.orgName}/${match.spaceName}/${match.appName}`).join("\n");
632
+ throw new Error(
633
+ `App "${appName}" is ambiguous - it exists in multiple spaces:
634
+ ${candidates}
635
+ Pass a full region/org/space/app selector to disambiguate.`
636
+ );
637
+ }
638
+ const onlyMatch = matches[0];
639
+ if (onlyMatch === void 0) {
640
+ throw new Error(`App "${appName}" could not be resolved.`);
641
+ }
642
+ return onlyMatch;
643
+ }
644
+ async function resolveSelector(raw) {
645
+ const parsed = parseSelector(raw);
646
+ const structure = await readStructure();
647
+ if (structure === void 0) {
648
+ throw new Error(
649
+ "No CF topology snapshot found. Run `cf-sync sync` (or `cf-sync space ...`) first."
650
+ );
651
+ }
652
+ if (parsed.kind === "explicit") {
653
+ return resolveExplicit(raw, parsed, structure);
654
+ }
655
+ return resolveByAppName(raw, parsed.appName, structure);
656
+ }
657
+
658
+ // src/ssh.ts
659
+ var SSH_AUTHORIZED_TYPE = "audit.app.ssh-authorized";
660
+ var SSH_DENIED_TYPE = "audit.app.ssh-unauthorized";
661
+ var LIKELY_ACTIVE_WINDOW_MS = 60 * 60 * 1e3;
662
+ function isWithinActiveWindow(timestamp, now) {
663
+ const at = Date.parse(timestamp);
664
+ if (Number.isNaN(at)) {
665
+ return false;
666
+ }
667
+ const delta = now.getTime() - at;
668
+ return delta >= 0 && delta <= LIKELY_ACTIVE_WINDOW_MS;
669
+ }
670
+ function inferSshSessions(events, now) {
671
+ return events.filter((event) => event.type === SSH_AUTHORIZED_TYPE).map((event) => ({
672
+ actor: event.actor.name.length > 0 ? event.actor.name : event.actor.guid,
673
+ authorizedAt: event.createdAt,
674
+ likelyActive: isWithinActiveWindow(event.createdAt, now)
675
+ })).sort((left, right) => right.authorizedAt.localeCompare(left.authorizedAt));
676
+ }
677
+ function buildSshStatus(input) {
678
+ const sshEvents = input.events.filter(isSshEvent);
679
+ const sessions = inferSshSessions(sshEvents, input.now);
680
+ const deniedAttempts = sshEvents.filter((event) => event.type === SSH_DENIED_TYPE).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
681
+ return {
682
+ appName: input.appName,
683
+ sshEnabled: input.sshEnabled,
684
+ sshReason: input.sshReason,
685
+ sessions,
686
+ deniedAttempts,
687
+ activeSessionCount: sessions.filter((session) => session.likelyActive).length
688
+ };
689
+ }
690
+
691
+ // src/runtime.ts
692
+ var SSH_STATUS_EVENT_LIMIT = 200;
693
+ var WATCH_FETCH_LIMIT = 100;
694
+ function createCfClient(curl) {
695
+ return {
696
+ fetchAuditEvents: (input) => fetchAuditEvents(input, curl),
697
+ fetchApp: (appGuid) => fetchApp(appGuid, curl),
698
+ fetchSshEnabled: (appGuid) => fetchSshEnabled(appGuid, curl),
699
+ fetchWebProcessStats: (appGuid) => fetchWebProcessStats(appGuid, curl)
700
+ };
701
+ }
702
+ async function defaultWithCfApp(selector, credentials, work) {
703
+ return await withCfSession(async (ctx) => {
704
+ await prepareCfCliSession(
705
+ {
706
+ apiEndpoint: selector.apiEndpoint,
707
+ orgName: selector.orgName,
708
+ spaceName: selector.spaceName,
709
+ credentials
710
+ },
711
+ ctx
712
+ );
713
+ const appGuid = await cfAppGuid(selector.appName, ctx);
714
+ const client = createCfClient((path) => cfCurl(path, ctx));
715
+ return await work({ appGuid, client });
716
+ });
717
+ }
718
+ function createDefaultDependencies() {
719
+ return {
720
+ resolveSelector,
721
+ withCfApp: defaultWithCfApp,
722
+ now: () => /* @__PURE__ */ new Date()
723
+ };
724
+ }
725
+ async function delay(ms, signal) {
726
+ try {
727
+ await delayPromise(ms, void 0, { signal });
728
+ } catch {
729
+ }
730
+ }
731
+ var CfEventsRuntime = class {
732
+ deps;
733
+ constructor(deps = createDefaultDependencies()) {
734
+ this.deps = deps;
735
+ }
736
+ async fetchEvents(raw, credentials, options) {
737
+ const selector = await this.deps.resolveSelector(raw);
738
+ return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
739
+ return await client.fetchAuditEvents({
740
+ appGuid,
741
+ types: options.types.length > 0 ? options.types : void 0,
742
+ createdAfter: this.resolveCreatedAfter(options.since),
743
+ limit: options.limit
744
+ });
745
+ });
746
+ }
747
+ async getSshStatus(raw, credentials, since) {
748
+ const selector = await this.deps.resolveSelector(raw);
749
+ return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
750
+ const [sshEnabled, events] = await Promise.all([
751
+ client.fetchSshEnabled(appGuid),
752
+ client.fetchAuditEvents({
753
+ appGuid,
754
+ types: [...SSH_EVENT_TYPES],
755
+ createdAfter: durationToCreatedAfter(since, this.deps.now()),
756
+ limit: SSH_STATUS_EVENT_LIMIT
757
+ })
758
+ ]);
759
+ return buildSshStatus({
760
+ appName: selector.appName,
761
+ sshEnabled: sshEnabled.enabled,
762
+ sshReason: sshEnabled.reason,
763
+ events,
764
+ now: this.deps.now()
765
+ });
766
+ });
767
+ }
768
+ async getCrashes(raw, credentials, options) {
769
+ const selector = await this.deps.resolveSelector(raw);
770
+ return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
771
+ const events = await client.fetchAuditEvents({
772
+ appGuid,
773
+ types: [...CRASH_EVENT_TYPES],
774
+ createdAfter: this.resolveCreatedAfter(options.since),
775
+ limit: options.limit
776
+ });
777
+ return summarizeCrashes(selector.appName, events);
778
+ });
779
+ }
780
+ async getStatus(raw, credentials) {
781
+ const selector = await this.deps.resolveSelector(raw);
782
+ return await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
783
+ const [app, instances, sshEnabled, recent] = await Promise.all([
784
+ client.fetchApp(appGuid),
785
+ client.fetchWebProcessStats(appGuid),
786
+ client.fetchSshEnabled(appGuid),
787
+ client.fetchAuditEvents({ appGuid, types: void 0, createdAfter: void 0, limit: 1 })
788
+ ]);
789
+ return {
790
+ appName: selector.appName,
791
+ appGuid,
792
+ requestedState: app.state,
793
+ sshEnabled: sshEnabled.enabled,
794
+ instances,
795
+ lastEvent: recent[0]
796
+ };
797
+ });
798
+ }
799
+ async watchEvents(raw, credentials, options, onEvent, signal) {
800
+ const selector = await this.deps.resolveSelector(raw);
801
+ await this.deps.withCfApp(selector, credentials, async ({ appGuid, client }) => {
802
+ const seen = /* @__PURE__ */ new Set();
803
+ let cursor = durationToCreatedAfter(options.lookback, this.deps.now());
804
+ while (!signal.aborted) {
805
+ const events = await client.fetchAuditEvents({
806
+ appGuid,
807
+ types: options.types.length > 0 ? options.types : void 0,
808
+ createdAfter: cursor,
809
+ limit: WATCH_FETCH_LIMIT
810
+ });
811
+ const fresh = [];
812
+ for (const event of events) {
813
+ if (!seen.has(event.guid)) {
814
+ seen.add(event.guid);
815
+ fresh.push(event);
816
+ }
817
+ }
818
+ for (const event of [...fresh].reverse()) {
819
+ onEvent(event);
820
+ }
821
+ const newest = events[0];
822
+ if (newest !== void 0 && newest.createdAt.length > 0) {
823
+ cursor = newest.createdAt;
824
+ }
825
+ await delay(options.intervalMs, signal);
826
+ }
827
+ });
828
+ }
829
+ resolveCreatedAfter(since) {
830
+ return since === void 0 ? void 0 : durationToCreatedAfter(since, this.deps.now());
831
+ }
832
+ };
833
+ export {
834
+ APP_EVENT_TYPES,
835
+ CRASH_EVENT_TYPES,
836
+ CfEventsRuntime,
837
+ SSH_EVENT_TYPES,
838
+ buildAuditEventsPath,
839
+ buildSshStatus,
840
+ cfAppGuid,
841
+ cfCurl,
842
+ createDefaultDependencies,
843
+ describeEventType,
844
+ durationToCreatedAfter,
845
+ fetchApp,
846
+ fetchAuditEvents,
847
+ fetchSshEnabled,
848
+ fetchWebProcessStats,
849
+ filterByTypes,
850
+ formatBytes,
851
+ formatCpu,
852
+ formatCrashReport,
853
+ formatEventLine,
854
+ formatEventsReport,
855
+ formatRelativeTime,
856
+ formatSshStatusReport,
857
+ formatStatusReport,
858
+ formatUptime,
859
+ inferSshSessions,
860
+ isCrashEvent,
861
+ isSshEvent,
862
+ parseDuration,
863
+ parseSelector,
864
+ parseTypeFilter,
865
+ prepareCfCliSession,
866
+ resolveSelector,
867
+ runCfCommand,
868
+ sortEventsNewestFirst,
869
+ summarizeCrashes,
870
+ toCrashRecord,
871
+ withCfSession
872
+ };
873
+ //# sourceMappingURL=index.js.map