zeuslock-dlp-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJson } from "../lib/api-client.js";
4
+ import { dash, formatTable } from "../lib/table.js";
5
+
6
+ const DAYS = [7, 30, 90];
7
+ const SOURCES = ["all", "endpoint", "browser", "both"];
8
+ const STATUSES = ["all", "sanctioned", "shadow"];
9
+ const CATEGORIES = ["all", "chatbot", "coding_assistant", "image_gen", "agent", "other"];
10
+ const DISABLED_MESSAGE = "Shadow AI is not enabled for this organization.";
11
+
12
+ export function createShadowAiCommand({
13
+ stdout = process.stdout,
14
+ env = process.env
15
+ } = {}) {
16
+ const command = new Command("shadow-ai")
17
+ .description("Review Shadow AI tool usage");
18
+
19
+ command.addCommand(createStatsCommand({ stdout, env }));
20
+ command.addCommand(createToolsCommand({ stdout, env }));
21
+ command.addCommand(createUsersCommand({ stdout, env }));
22
+
23
+ return command;
24
+ }
25
+
26
+ function createStatsCommand({ stdout, env }) {
27
+ return new Command("stats")
28
+ .description("Show Shadow AI detection statistics")
29
+ .option("--days <days>", "history window: 7, 30, or 90", "30")
30
+ .option("--source <source>", "source filter: all, endpoint, browser, or both", "all")
31
+ .option("--status <status>", "policy status filter: all, sanctioned, or shadow", "all")
32
+ .option("--category <category>", "category filter: all, chatbot, coding_assistant, image_gen, agent, or other", "all")
33
+ .option("--json", "print machine-readable output")
34
+ .action(async (options) => {
35
+ const filters = parseSharedFilters(options);
36
+ if (!await isShadowAiEnabled(env)) {
37
+ writeDisabled(stdout, options, filters);
38
+ return;
39
+ }
40
+
41
+ const stats = await requestAuthenticatedJson(`/api/shadow-ai/stats?${buildShadowQuery(filters)}`, { env });
42
+ const output = { enabled: true, filters, stats };
43
+
44
+ if (options.json) {
45
+ stdout.write(`${JSON.stringify(output)}\n`);
46
+ return;
47
+ }
48
+
49
+ stdout.write(formatShadowStats(stats, filters.days));
50
+ });
51
+ }
52
+
53
+ function createToolsCommand({ stdout, env }) {
54
+ return new Command("tools")
55
+ .description("List detected Shadow AI tools")
56
+ .option("--days <days>", "history window: 7, 30, or 90", "30")
57
+ .option("--source <source>", "source filter: all, endpoint, browser, or both", "all")
58
+ .option("--status <status>", "policy status filter: all, sanctioned, or shadow", "all")
59
+ .option("--category <category>", "category filter: all, chatbot, coding_assistant, image_gen, agent, or other", "all")
60
+ .option("--json", "print machine-readable output")
61
+ .action(async (options) => {
62
+ const filters = parseSharedFilters(options);
63
+ if (!await isShadowAiEnabled(env)) {
64
+ writeDisabled(stdout, options, filters);
65
+ return;
66
+ }
67
+
68
+ const data = await requestAuthenticatedJson(`/api/shadow-ai/tools?${buildShadowQuery(filters)}`, { env });
69
+ const tools = normalizeTools(data);
70
+ const output = {
71
+ enabled: true,
72
+ total: typeof data?.total === "number" ? data.total : tools.length,
73
+ period_days: typeof data?.period_days === "number" ? data.period_days : filters.days,
74
+ filters,
75
+ tools
76
+ };
77
+
78
+ if (options.json) {
79
+ stdout.write(`${JSON.stringify(output)}\n`);
80
+ return;
81
+ }
82
+
83
+ stdout.write(`Shadow AI tools: ${output.total} detected (${output.period_days} days)\n`);
84
+ if (!tools.length) {
85
+ stdout.write("No Shadow AI tools found.\n");
86
+ return;
87
+ }
88
+
89
+ stdout.write("\n");
90
+ stdout.write(`${formatTable(tools, [
91
+ { header: "TOOL", value: (tool) => dash(tool.tool_name || tool.tool_key) },
92
+ { header: "CATEGORY", value: (tool) => dash(tool.category) },
93
+ { header: "POLICY", value: (tool) => dash(tool.policy_status) },
94
+ { header: "SOURCE", value: (tool) => dash(tool.detection_source) },
95
+ { header: "USERS", value: (tool) => numberOrZero(tool.unique_users) },
96
+ { header: "CONNECTIONS", value: (tool) => numberOrZero(tool.connections) },
97
+ { header: "RISK", value: (tool) => numberOrZero(tool.risk_score) }
98
+ ])}\n`);
99
+ });
100
+ }
101
+
102
+ function createUsersCommand({ stdout, env }) {
103
+ return new Command("users")
104
+ .description("List users with Shadow AI tool usage")
105
+ .option("--days <days>", "history window: 7, 30, or 90", "30")
106
+ .option("--json", "print machine-readable output")
107
+ .action(async (options) => {
108
+ const days = parseAllowedInt(options.days, DAYS, "--days");
109
+ const filters = { days };
110
+ if (!await isShadowAiEnabled(env)) {
111
+ writeDisabled(stdout, options, filters);
112
+ return;
113
+ }
114
+
115
+ const data = await requestAuthenticatedJson(`/api/shadow-ai/users?days=${days}`, { env });
116
+ const users = normalizeUsers(data);
117
+ const output = {
118
+ enabled: true,
119
+ total: typeof data?.total === "number" ? data.total : users.length,
120
+ period_days: typeof data?.period_days === "number" ? data.period_days : days,
121
+ filters,
122
+ users
123
+ };
124
+
125
+ if (options.json) {
126
+ stdout.write(`${JSON.stringify(output)}\n`);
127
+ return;
128
+ }
129
+
130
+ stdout.write(`Shadow AI users: ${output.total} users (${output.period_days} days)\n`);
131
+ if (!users.length) {
132
+ stdout.write("No Shadow AI users found.\n");
133
+ return;
134
+ }
135
+
136
+ stdout.write("\n");
137
+ stdout.write(`${formatTable(users, [
138
+ { header: "USER", value: (user) => dash(user.username) },
139
+ { header: "DISTINCT TOOLS", value: (user) => numberOrZero(user.distinct_tools) },
140
+ { header: "MOST USED", value: (user) => dash(user.most_used_tool) }
141
+ ])}\n`);
142
+ });
143
+ }
144
+
145
+ async function isShadowAiEnabled(env) {
146
+ const org = await requestAuthenticatedJson("/api/org", { env });
147
+ return Boolean(org?.settings?.shadow_ai_enabled);
148
+ }
149
+
150
+ function writeDisabled(stdout, options, filters) {
151
+ if (options.json) {
152
+ stdout.write(`${JSON.stringify({
153
+ enabled: false,
154
+ message: DISABLED_MESSAGE,
155
+ filters
156
+ })}\n`);
157
+ return;
158
+ }
159
+ stdout.write(`${DISABLED_MESSAGE}\n`);
160
+ }
161
+
162
+ function parseSharedFilters(options) {
163
+ return {
164
+ days: parseAllowedInt(options.days, DAYS, "--days"),
165
+ source: parseOptionalChoice(options.source, SOURCES, "--source") || "all",
166
+ status: parseOptionalChoice(options.status, STATUSES, "--status") || "all",
167
+ category: parseOptionalChoice(options.category, CATEGORIES, "--category") || "all"
168
+ };
169
+ }
170
+
171
+ function buildShadowQuery(filters) {
172
+ const params = new URLSearchParams();
173
+ params.set("days", String(filters.days));
174
+ if (filters.source !== "all") {
175
+ params.set("source", filters.source);
176
+ }
177
+ if (filters.status !== "all") {
178
+ params.set("status", filters.status);
179
+ }
180
+ if (filters.category !== "all") {
181
+ params.set("category", filters.category);
182
+ }
183
+ return params.toString();
184
+ }
185
+
186
+ function parseAllowedInt(value, allowed, optionName) {
187
+ const number = Number(value);
188
+ if (!Number.isInteger(number) || !allowed.includes(number)) {
189
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
190
+ }
191
+ return number;
192
+ }
193
+
194
+ function parseOptionalChoice(value, allowed, optionName) {
195
+ if (value === undefined || value === null || value === "") {
196
+ return null;
197
+ }
198
+ const normalized = String(value).trim().toLowerCase();
199
+ if (!allowed.includes(normalized)) {
200
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
201
+ }
202
+ return normalized;
203
+ }
204
+
205
+ function normalizeTools(data) {
206
+ const tools = Array.isArray(data) ? data : (data?.tools || []);
207
+ return tools.map((tool) => ({
208
+ tool_key: tool.tool_key || "",
209
+ tool_name: tool.tool_name || "",
210
+ category: tool.category || "",
211
+ vendor: tool.vendor || "",
212
+ destination_domain: tool.destination_domain || "",
213
+ policy_status: tool.policy_status || "",
214
+ detection_source: tool.detection_source || "",
215
+ unique_users: numberOrZero(tool.unique_users),
216
+ connections: numberOrZero(tool.connections),
217
+ risk_score: numberOrZero(tool.risk_score)
218
+ }));
219
+ }
220
+
221
+ function normalizeUsers(data) {
222
+ const users = Array.isArray(data) ? data : (data?.users || []);
223
+ return users.map((user) => ({
224
+ username: user.username || "",
225
+ distinct_tools: numberOrZero(user.distinct_tools),
226
+ shadow_tools: numberOrZero(user.shadow_tools),
227
+ most_used_tool: user.most_used_tool || "",
228
+ connections: numberOrZero(user.connections)
229
+ }));
230
+ }
231
+
232
+ function formatShadowStats(stats, days) {
233
+ const evolution = stats?.evolution && typeof stats.evolution === "object" ? stats.evolution : {};
234
+ return [
235
+ `Shadow AI stats (${stats?.period_days || days} days)`,
236
+ `Tools detected: ${numberOrZero(stats?.total_tools_detected)}${formatDelta(evolution.total_tools_detected)}`,
237
+ `Connections: ${numberOrZero(stats?.total_connections)}${formatDelta(evolution.total_connections)}`,
238
+ `Shadow tools: ${numberOrZero(stats?.shadow_tools)}${formatDelta(evolution.shadow_tools)}`,
239
+ `Shadow users: ${numberOrZero(stats?.shadow_users)}${formatDelta(evolution.shadow_users)}`,
240
+ `High-risk tools: ${numberOrZero(stats?.high_risk_tools)}${formatDelta(evolution.high_risk_tools)}`,
241
+ ""
242
+ ].join("\n");
243
+ }
244
+
245
+ function formatDelta(value) {
246
+ const delta = Number(value || 0);
247
+ if (!delta) {
248
+ return "";
249
+ }
250
+ return ` (${delta > 0 ? "+" : ""}${delta})`;
251
+ }
252
+
253
+ function numberOrZero(value) {
254
+ return Number.isFinite(Number(value)) ? Number(value) : 0;
255
+ }
@@ -0,0 +1,241 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { Command, Option } from "commander";
3
+
4
+ import { requestApiKeyJson, resolveApiUrlForRequest } from "../lib/api-client.js";
5
+ import { buildSiemCursorKey, readSiemCursor, writeSiemCursor } from "../lib/siem-cursors.js";
6
+
7
+ const SEVERITIES = ["unknown", "low", "medium", "high", "critical"];
8
+ const OUTPUT_FORMATS = ["jsonl", "json"];
9
+ const MAX_LIMIT = 500;
10
+ const DAY_MS = 24 * 60 * 60 * 1000;
11
+
12
+ export function createSiemCommand({
13
+ stdout = process.stdout,
14
+ env = process.env
15
+ } = {}) {
16
+ const command = new Command("siem")
17
+ .description("Pull SIEM OCSF events");
18
+
19
+ command.addCommand(createSiemPullCommand({ stdout, env }));
20
+ command.addCommand(createSiemTailCommand({ stdout, env }));
21
+
22
+ return command;
23
+ }
24
+
25
+ function createSiemPullCommand({ stdout, env }) {
26
+ return new Command("pull")
27
+ .description("Retrieve one page of SIEM events")
28
+ .option("--api-key <key>", "organization API key; defaults to ZEUSLOCK_API_KEY")
29
+ .option("--since <cursor>", "backend cursor returned as next_cursor")
30
+ .option("--from <time>", "start time as ISO date/time or epoch")
31
+ .option("--to <time>", "end time as ISO date/time or epoch")
32
+ .option("--days <days>", "look back this many days; mutually exclusive with --from")
33
+ .option("--category <category>", "event category filter; current dashboard events use dlp")
34
+ .option("--severity <level>", "minimum severity: unknown, low, medium, high, or critical")
35
+ .option("--limit <count>", `page size: 1-${MAX_LIMIT}; backend default is 100`)
36
+ .option("--format <format>", "output format: jsonl or json", "jsonl")
37
+ .action(async (options) => {
38
+ const apiUrl = await resolveApiUrlForRequest(env);
39
+ const filters = parsePullFilters(options);
40
+ const data = await fetchSiemEvents({
41
+ apiUrl,
42
+ apiKey: options.apiKey,
43
+ env,
44
+ filters
45
+ });
46
+
47
+ writeSiemEvents(stdout, data, {
48
+ format: parseChoice(options.format, OUTPUT_FORMATS, "--format"),
49
+ filters: buildFilterMetadata(filters)
50
+ });
51
+ });
52
+ }
53
+
54
+ function createSiemTailCommand({ stdout, env }) {
55
+ return new Command("tail")
56
+ .description("Continuously poll SIEM events with a persistent cursor")
57
+ .option("--api-key <key>", "organization API key; defaults to ZEUSLOCK_API_KEY")
58
+ .option("--since <cursor>", "start from this backend cursor instead of the saved cursor")
59
+ .option("--category <category>", "event category filter; current dashboard events use dlp")
60
+ .option("--severity <level>", "minimum severity: unknown, low, medium, high, or critical")
61
+ .option("--limit <count>", `page size per poll: 1-${MAX_LIMIT}; backend default is 100`)
62
+ .option("--interval <seconds>", "poll interval in seconds", "5")
63
+ .option("--cursor-name <name>", "local cursor namespace", "default")
64
+ .option("--reset-cursor", "ignore the saved cursor for this run")
65
+ .addOption(new Option("--max-polls <count>", "stop after this many polling cycles").hideHelp())
66
+ .action(async (options) => {
67
+ const apiUrl = await resolveApiUrlForRequest(env);
68
+ const interval = parsePositiveNumber(options.interval, "--interval");
69
+ const maxPolls = options.maxPolls ? parsePositiveInt(options.maxPolls, "--max-polls") : null;
70
+ const baseFilters = parseTailFilters(options);
71
+ const cursorKey = buildSiemCursorKey({
72
+ apiUrl,
73
+ cursorName: options.cursorName || "default",
74
+ category: baseFilters.category,
75
+ severity: baseFilters.severity
76
+ });
77
+ let cursor = options.since || (
78
+ options.resetCursor ? null : await readSiemCursor(cursorKey, env)
79
+ );
80
+ let polls = 0;
81
+
82
+ for (;;) {
83
+ const filters = { ...baseFilters, since: cursor };
84
+ const data = await fetchSiemEvents({
85
+ apiUrl,
86
+ apiKey: options.apiKey,
87
+ env,
88
+ filters
89
+ });
90
+
91
+ writeSiemEvents(stdout, data, {
92
+ format: "jsonl",
93
+ filters: buildFilterMetadata(filters)
94
+ });
95
+
96
+ if (data?.next_cursor) {
97
+ cursor = data.next_cursor;
98
+ await writeSiemCursor(cursorKey, {
99
+ cursor,
100
+ apiUrl,
101
+ cursorName: options.cursorName || "default",
102
+ category: baseFilters.category || null,
103
+ severity: baseFilters.severity || null
104
+ }, env);
105
+ }
106
+
107
+ polls += 1;
108
+ if (maxPolls !== null && polls >= maxPolls) {
109
+ return;
110
+ }
111
+
112
+ await sleep(interval * 1000);
113
+ }
114
+ });
115
+ }
116
+
117
+ async function fetchSiemEvents({ apiUrl, apiKey, env, filters }) {
118
+ const params = new URLSearchParams();
119
+ addParam(params, "since", filters.since);
120
+ addParam(params, "category", filters.category);
121
+ addParam(params, "severity", filters.severity);
122
+ addParam(params, "start_time", filters.startTime);
123
+ addParam(params, "end_time", filters.endTime);
124
+ addParam(params, "limit", filters.limit);
125
+
126
+ const query = params.toString();
127
+ return requestApiKeyJson(`/api/v1/siem/events${query ? `?${query}` : ""}`, {
128
+ apiUrl,
129
+ apiKey,
130
+ env
131
+ });
132
+ }
133
+
134
+ function parsePullFilters(options) {
135
+ if (options.days && options.from) {
136
+ throw new Error("--days and --from are mutually exclusive.");
137
+ }
138
+
139
+ return {
140
+ since: normalizeOptionalString(options.since),
141
+ category: normalizeOptionalString(options.category)?.toLowerCase() || null,
142
+ severity: parseOptionalChoice(options.severity, SEVERITIES, "--severity"),
143
+ startTime: options.days ? daysToStartTime(options.days) : normalizeOptionalString(options.from),
144
+ endTime: normalizeOptionalString(options.to),
145
+ limit: options.limit ? parseLimit(options.limit) : null
146
+ };
147
+ }
148
+
149
+ function parseTailFilters(options) {
150
+ return {
151
+ since: null,
152
+ category: normalizeOptionalString(options.category)?.toLowerCase() || null,
153
+ severity: parseOptionalChoice(options.severity, SEVERITIES, "--severity"),
154
+ startTime: null,
155
+ endTime: null,
156
+ limit: options.limit ? parseLimit(options.limit) : null
157
+ };
158
+ }
159
+
160
+ function writeSiemEvents(stdout, data, { format, filters }) {
161
+ const events = Array.isArray(data?.events) ? data.events : [];
162
+ if (format === "json") {
163
+ stdout.write(`${JSON.stringify({
164
+ events,
165
+ next_cursor: data?.next_cursor || null,
166
+ filters
167
+ })}\n`);
168
+ return;
169
+ }
170
+
171
+ for (const event of events) {
172
+ stdout.write(`${JSON.stringify(event)}\n`);
173
+ }
174
+ }
175
+
176
+ function buildFilterMetadata(filters) {
177
+ return {
178
+ since: filters.since || null,
179
+ category: filters.category || null,
180
+ severity: filters.severity || null,
181
+ start_time: filters.startTime || null,
182
+ end_time: filters.endTime || null,
183
+ limit: filters.limit || null
184
+ };
185
+ }
186
+
187
+ function daysToStartTime(value) {
188
+ const days = parsePositiveInt(value, "--days");
189
+ return new Date(Date.now() - days * DAY_MS).toISOString();
190
+ }
191
+
192
+ function parseLimit(value) {
193
+ const limit = parsePositiveInt(value, "--limit");
194
+ if (limit > MAX_LIMIT) {
195
+ throw new Error(`Invalid --limit. Allowed values: 1-${MAX_LIMIT}.`);
196
+ }
197
+ return limit;
198
+ }
199
+
200
+ function parsePositiveInt(value, optionName) {
201
+ const number = Number(value);
202
+ if (!Number.isInteger(number) || number < 1) {
203
+ throw new Error(`Invalid ${optionName}. Value must be a positive integer.`);
204
+ }
205
+ return number;
206
+ }
207
+
208
+ function parsePositiveNumber(value, optionName) {
209
+ const number = Number(value);
210
+ if (!Number.isFinite(number) || number <= 0) {
211
+ throw new Error(`Invalid ${optionName}. Value must be a positive number.`);
212
+ }
213
+ return number;
214
+ }
215
+
216
+ function parseOptionalChoice(value, allowed, optionName) {
217
+ const normalized = normalizeOptionalString(value)?.toLowerCase();
218
+ if (!normalized) {
219
+ return null;
220
+ }
221
+ return parseChoice(normalized, allowed, optionName);
222
+ }
223
+
224
+ function parseChoice(value, allowed, optionName) {
225
+ const normalized = String(value || "").trim().toLowerCase();
226
+ if (!allowed.includes(normalized)) {
227
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
228
+ }
229
+ return normalized;
230
+ }
231
+
232
+ function normalizeOptionalString(value) {
233
+ const normalized = String(value || "").trim();
234
+ return normalized || null;
235
+ }
236
+
237
+ function addParam(params, key, value) {
238
+ if (value !== null && value !== undefined && value !== "") {
239
+ params.set(key, String(value));
240
+ }
241
+ }
@@ -0,0 +1,170 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestApiKeyJson, requestJson, resolveApiUrlForRequest } from "../lib/api-client.js";
4
+ import { readApiKeyStore } from "../lib/api-key-store.js";
5
+ import { readAuthStore } from "../lib/auth-store.js";
6
+ import { readEnvConfig } from "../lib/config.js";
7
+ import { getPackageInfo } from "../lib/package-info.js";
8
+
9
+ const DEFAULT_STATUS_TIMEOUT_MS = 1500;
10
+
11
+ export function createStatusCommand({ stdout = process.stdout, env = process.env } = {}) {
12
+ return new Command("status")
13
+ .description("Print local CLI configuration status")
14
+ .option("--api-key <key>", "organization API key for license validation; defaults to ZEUSLOCK_API_KEY")
15
+ .option("--require-license", "return a failing exit code unless backend health and license validation pass")
16
+ .option("--json", "print machine-readable output")
17
+ .action(async (options) => {
18
+ const config = readEnvConfig(env);
19
+ const auth = await readAuthStore(env);
20
+ const storedApiKey = await readApiKeyStore(env);
21
+ const packageInfo = getPackageInfo();
22
+ const apiUrl = await resolveApiUrlForRequest(env);
23
+ const apiKey = resolveApiKey(options.apiKey, env, storedApiKey);
24
+ const [health, readiness, license] = await Promise.all([
25
+ checkBackendEndpoint(apiUrl, "/healthz", "ok", env),
26
+ checkBackendEndpoint(apiUrl, "/readyz", "ready", env),
27
+ checkLicense({ apiUrl, apiKey, env, required: Boolean(options.requireLicense) })
28
+ ]);
29
+ const status = {
30
+ name: packageInfo.name,
31
+ version: packageInfo.version,
32
+ node: process.version,
33
+ apiUrl,
34
+ authenticated: config.hasApiToken || Boolean(auth?.accessToken),
35
+ apiKeyConfigured: Boolean(apiKey),
36
+ backend: {
37
+ health,
38
+ readiness
39
+ },
40
+ license
41
+ };
42
+
43
+ if (options.json) {
44
+ stdout.write(`${JSON.stringify(status)}\n`);
45
+ } else {
46
+ stdout.write(`ZeusLock CLI ${status.version}\n`);
47
+ stdout.write(`Node: ${status.node}\n`);
48
+ stdout.write(`API URL: ${status.apiUrl ?? "not configured"}\n`);
49
+ stdout.write(`Auth token: ${status.authenticated ? "configured" : "not configured"}\n`);
50
+ stdout.write(`Org API key: ${status.apiKeyConfigured ? "configured" : "not configured"}\n`);
51
+ stdout.write(`Backend health: ${formatBackendCheck(health)}\n`);
52
+ stdout.write(`Backend readiness: ${formatBackendCheck(readiness)}\n`);
53
+ stdout.write(`License: ${formatLicenseCheck(license)}\n`);
54
+ }
55
+
56
+ if (options.requireLicense && shouldFailRequiredStatus(status)) {
57
+ process.exitCode = 1;
58
+ }
59
+ });
60
+ }
61
+
62
+ async function checkBackendEndpoint(apiUrl, endpoint, expectedStatus, env) {
63
+ try {
64
+ const data = await requestJson(apiUrl, endpoint, {
65
+ signal: buildTimeoutSignal(env)
66
+ });
67
+ const status = data?.status || expectedStatus;
68
+ const ok = status === expectedStatus;
69
+ return {
70
+ ok,
71
+ status,
72
+ error: ok ? null : `Expected status ${expectedStatus}`
73
+ };
74
+ } catch (error) {
75
+ return {
76
+ ok: false,
77
+ status: "error",
78
+ httpStatus: error?.status ?? null,
79
+ error: error?.message || "Backend check failed"
80
+ };
81
+ }
82
+ }
83
+
84
+ async function checkLicense({ apiUrl, apiKey, env, required }) {
85
+ if (!apiKey) {
86
+ return {
87
+ status: required ? "missing_api_key" : "skipped",
88
+ valid: false,
89
+ checked: false,
90
+ error: required ? "API key is required for license validation." : null
91
+ };
92
+ }
93
+
94
+ try {
95
+ const data = await requestApiKeyJson("/api/v1/validate-license", {
96
+ apiUrl,
97
+ apiKey,
98
+ env,
99
+ method: "POST",
100
+ signal: buildTimeoutSignal(env),
101
+ body: {
102
+ source: "desktop_agent"
103
+ }
104
+ });
105
+ const valid = data?.valid === true || data?.ok === true;
106
+ return {
107
+ status: valid ? "valid" : "invalid",
108
+ valid,
109
+ checked: true,
110
+ organization: data?.organization || null,
111
+ features: Array.isArray(data?.features) ? data.features : [],
112
+ limits: data?.limits && typeof data.limits === "object" ? data.limits : {},
113
+ error: data?.error || null
114
+ };
115
+ } catch (error) {
116
+ return {
117
+ status: "error",
118
+ valid: false,
119
+ checked: true,
120
+ error: error?.message || "License validation failed"
121
+ };
122
+ }
123
+ }
124
+
125
+ function buildTimeoutSignal(env) {
126
+ const timeoutMs = Number(env.ZEUSLOCK_STATUS_TIMEOUT_MS) || DEFAULT_STATUS_TIMEOUT_MS;
127
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
128
+ return AbortSignal.timeout(timeoutMs);
129
+ }
130
+
131
+ const controller = new AbortController();
132
+ setTimeout(() => controller.abort(), timeoutMs).unref?.();
133
+ return controller.signal;
134
+ }
135
+
136
+ function resolveApiKey(optionValue, env, storedApiKey) {
137
+ return String(optionValue || env.ZEUSLOCK_API_KEY || storedApiKey?.apiKey || "").trim();
138
+ }
139
+
140
+ function formatBackendCheck(check) {
141
+ if (check.ok) {
142
+ return check.status;
143
+ }
144
+ return `error (${check.error})`;
145
+ }
146
+
147
+ function formatLicenseCheck(license) {
148
+ if (license.status === "skipped") {
149
+ return "skipped (API key not configured)";
150
+ }
151
+ if (license.status === "missing_api_key") {
152
+ return "missing API key";
153
+ }
154
+ if (license.status === "valid") {
155
+ const org = license.organization?.name || license.organization?.org_id;
156
+ return org ? `valid (${org})` : "valid";
157
+ }
158
+ if (license.status === "invalid") {
159
+ return `invalid${license.error ? ` (${license.error})` : ""}`;
160
+ }
161
+ return `error (${license.error || "license validation failed"})`;
162
+ }
163
+
164
+ function shouldFailRequiredStatus(status) {
165
+ return (
166
+ status.backend.health.ok !== true ||
167
+ status.backend.readiness.ok !== true ||
168
+ status.license.valid !== true
169
+ );
170
+ }