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,293 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJson } from "../lib/api-client.js";
4
+ import { maskCliToken } from "../lib/cli-token.js";
5
+ import { promptText } from "../lib/prompt.js";
6
+ import { dash, formatTable } from "../lib/table.js";
7
+
8
+ const TOKEN_STATUSES = ["all", "active", "revoked", "expired"];
9
+
10
+ export function createTokensCommand({
11
+ stdin = process.stdin,
12
+ stdout = process.stdout,
13
+ env = process.env
14
+ } = {}) {
15
+ const command = new Command("tokens")
16
+ .description("Manage personal CLI access tokens");
17
+
18
+ command.addCommand(createTokensCreateCommand({ stdout, env }));
19
+ command.addCommand(createTokensListCommand({ stdout, env }));
20
+ command.addCommand(createTokensRevokeCommand({ stdin, stdout, env }));
21
+ command.addCommand(createTokensPurgeCommand({ stdin, stdout, env }));
22
+
23
+ return command;
24
+ }
25
+
26
+ function createTokensCreateCommand({ stdout, env }) {
27
+ return new Command("create")
28
+ .description("Create a personal CLI access token")
29
+ .requiredOption("--name <name>", "CLI token name")
30
+ .option("--expires-in-days <days>", "token lifetime in days, 1 to 365")
31
+ .option("--json", "print machine-readable output")
32
+ .action(async (options) => {
33
+ const name = parseTokenName(options.name);
34
+ const expiresInDays = parseExpiresInDays(options.expiresInDays);
35
+ const body = { name };
36
+ if (expiresInDays !== null) {
37
+ body.expires_in_days = expiresInDays;
38
+ }
39
+
40
+ const created = normalizeCliToken(await requestAuthenticatedJson("/api/v1/cli-tokens", {
41
+ env,
42
+ method: "POST",
43
+ body
44
+ }));
45
+ const output = {
46
+ created: true,
47
+ token: serializeToken(created, { showSecret: true })
48
+ };
49
+
50
+ if (options.json) {
51
+ stdout.write(`${JSON.stringify(output)}\n`);
52
+ return;
53
+ }
54
+
55
+ stdout.write(`Created CLI token: ${created.name || name}\n`);
56
+ stdout.write(`Token ID: ${created.id || "unknown"}\n`);
57
+ stdout.write(`Token: ${created.token || "not returned"}\n`);
58
+ if (created.expires_at) {
59
+ stdout.write(`Expires: ${created.expires_at}\n`);
60
+ }
61
+ });
62
+ }
63
+
64
+ function createTokensListCommand({ stdout, env }) {
65
+ return new Command("list")
66
+ .description("List personal CLI access tokens")
67
+ .option("--status <status>", "status filter: all, active, revoked, or expired", "all")
68
+ .option("--show-secret", "include returned full token values when available")
69
+ .option("--json", "print machine-readable output")
70
+ .action(async (options) => {
71
+ const status = parseChoice(options.status, TOKEN_STATUSES, "--status");
72
+ const tokens = normalizeCliTokens(await requestAuthenticatedJson("/api/v1/cli-tokens", { env }));
73
+ const filtered = applyTokenFilters(tokens, { status });
74
+ const output = {
75
+ total: tokens.length,
76
+ matched: filtered.length,
77
+ filters: {
78
+ status
79
+ },
80
+ tokens: filtered.map((token) => serializeToken(token, { showSecret: Boolean(options.showSecret) }))
81
+ };
82
+
83
+ if (options.json) {
84
+ stdout.write(`${JSON.stringify(output)}\n`);
85
+ return;
86
+ }
87
+
88
+ stdout.write(`CLI tokens: ${output.total} total, ${output.matched} matched\n`);
89
+ if (!filtered.length) {
90
+ stdout.write("No CLI tokens found.\n");
91
+ return;
92
+ }
93
+
94
+ stdout.write("\n");
95
+ stdout.write(`${formatTokensTable(filtered, { showSecret: Boolean(options.showSecret) })}\n`);
96
+ });
97
+ }
98
+
99
+ function createTokensRevokeCommand({ stdin, stdout, env }) {
100
+ return new Command("revoke")
101
+ .description("Revoke one CLI access token")
102
+ .argument("<token_id>", "CLI token id from tokens list")
103
+ .option("-y, --yes", "skip confirmation prompt")
104
+ .option("--json", "print machine-readable output")
105
+ .action(async (tokenId, options) => {
106
+ const target = await resolveTokenById(tokenId, env);
107
+ if (target.revoked) {
108
+ throw new Error("CLI token is already revoked.");
109
+ }
110
+
111
+ if (!options.yes) {
112
+ const answer = await promptText({
113
+ message: `Revoke CLI token ${target.name || tokenId}? Type "yes" to confirm: `,
114
+ stdin,
115
+ stdout,
116
+ optionName: "--yes"
117
+ });
118
+ if (answer.toLowerCase() !== "yes") {
119
+ stdout.write("CLI token revocation cancelled.\n");
120
+ return;
121
+ }
122
+ }
123
+
124
+ await requestAuthenticatedJson(`/api/v1/cli-tokens/${encodeURIComponent(tokenId)}`, {
125
+ env,
126
+ method: "DELETE"
127
+ });
128
+ const output = {
129
+ revoked: true,
130
+ token_id: tokenId,
131
+ name: target.name || null
132
+ };
133
+
134
+ if (options.json) {
135
+ stdout.write(`${JSON.stringify(output)}\n`);
136
+ return;
137
+ }
138
+
139
+ stdout.write(`CLI token revoked: ${target.name || tokenId}\n`);
140
+ });
141
+ }
142
+
143
+ function createTokensPurgeCommand({ stdin, stdout, env }) {
144
+ return new Command("purge")
145
+ .description("Permanently delete one already-revoked CLI access token")
146
+ .argument("<token_id>", "CLI token id from tokens list")
147
+ .option("-y, --yes", "skip confirmation prompt")
148
+ .option("--json", "print machine-readable output")
149
+ .action(async (tokenId, options) => {
150
+ const target = await resolveTokenById(tokenId, env);
151
+ if (!target.revoked) {
152
+ throw new Error("CLI token is still active. Revoke it before purging.");
153
+ }
154
+
155
+ if (!options.yes) {
156
+ const answer = await promptText({
157
+ message: `Permanently delete CLI token ${target.name || tokenId}? Type "yes" to confirm: `,
158
+ stdin,
159
+ stdout,
160
+ optionName: "--yes"
161
+ });
162
+ if (answer.toLowerCase() !== "yes") {
163
+ stdout.write("CLI token purge cancelled.\n");
164
+ return;
165
+ }
166
+ }
167
+
168
+ await requestAuthenticatedJson(`/api/v1/cli-tokens/${encodeURIComponent(tokenId)}?purge=true`, {
169
+ env,
170
+ method: "DELETE"
171
+ });
172
+ const output = {
173
+ purged: true,
174
+ token_id: tokenId,
175
+ name: target.name || null
176
+ };
177
+
178
+ if (options.json) {
179
+ stdout.write(`${JSON.stringify(output)}\n`);
180
+ return;
181
+ }
182
+
183
+ stdout.write(`CLI token purged: ${target.name || tokenId}\n`);
184
+ });
185
+ }
186
+
187
+ async function resolveTokenById(tokenId, env) {
188
+ const tokens = normalizeCliTokens(await requestAuthenticatedJson("/api/v1/cli-tokens", { env }));
189
+ const target = tokens.find((token) => token.id === tokenId);
190
+ if (!target) {
191
+ throw new Error(`CLI token not found: ${tokenId}`);
192
+ }
193
+ return target;
194
+ }
195
+
196
+ function parseTokenName(value) {
197
+ const name = String(value || "").trim();
198
+ if (!name) {
199
+ throw new Error("--name is required.");
200
+ }
201
+ if (name.length > 255) {
202
+ throw new Error("--name must be 255 characters or fewer.");
203
+ }
204
+ return name;
205
+ }
206
+
207
+ function parseExpiresInDays(value) {
208
+ if (value === undefined) {
209
+ return null;
210
+ }
211
+
212
+ const days = Number(value);
213
+ if (!Number.isInteger(days) || days < 1 || days > 365) {
214
+ throw new Error("Invalid --expires-in-days. Allowed values are integers from 1 to 365.");
215
+ }
216
+ return days;
217
+ }
218
+
219
+ function parseChoice(value, allowed, optionName) {
220
+ const normalized = String(value || "").trim().toLowerCase();
221
+ if (!allowed.includes(normalized)) {
222
+ throw new Error(`Invalid ${optionName}. Allowed values: ${allowed.join(", ")}.`);
223
+ }
224
+ return normalized;
225
+ }
226
+
227
+ function normalizeCliTokens(data) {
228
+ const tokens = Array.isArray(data) ? data : (data?.tokens || []);
229
+ return tokens.map(normalizeCliToken);
230
+ }
231
+
232
+ function normalizeCliToken(token = {}) {
233
+ const revoked = token.revoked === true;
234
+ const expiresAt = token.expires_at || "";
235
+ const expired = Boolean(expiresAt && Date.parse(expiresAt) <= Date.now());
236
+ const status = revoked ? "revoked" : expired ? "expired" : "active";
237
+ const fullToken = token.token || "";
238
+ return {
239
+ id: token.id || "",
240
+ name: token.name || "",
241
+ token: fullToken,
242
+ prefix: token.prefix || "",
243
+ prefix_masked: token.prefix_masked || (fullToken ? maskCliToken(fullToken) : formatMaskedPrefix(token.prefix)),
244
+ revoked,
245
+ expired,
246
+ active: status === "active",
247
+ status,
248
+ expires_at: expiresAt,
249
+ last_used_at: token.last_used_at || "",
250
+ created_at: token.created_at || ""
251
+ };
252
+ }
253
+
254
+ function applyTokenFilters(tokens, { status }) {
255
+ if (status === "all") {
256
+ return tokens;
257
+ }
258
+ return tokens.filter((token) => token.status === status);
259
+ }
260
+
261
+ function serializeToken(token, { showSecret = false } = {}) {
262
+ const serialized = {
263
+ id: token.id,
264
+ name: token.name,
265
+ prefix_masked: token.prefix_masked,
266
+ status: token.status,
267
+ revoked: token.revoked,
268
+ expires_at: token.expires_at || null,
269
+ last_used_at: token.last_used_at || null,
270
+ created_at: token.created_at || null
271
+ };
272
+
273
+ if (showSecret && token.token) {
274
+ serialized.token = token.token;
275
+ }
276
+
277
+ return serialized;
278
+ }
279
+
280
+ function formatTokensTable(tokens, { showSecret = false } = {}) {
281
+ return formatTable(tokens, [
282
+ { header: "TOKEN ID", value: (token) => dash(token.id) },
283
+ { header: "NAME", value: (token) => dash(token.name) },
284
+ { header: "TOKEN", value: (token) => showSecret && token.token ? token.token : dash(token.prefix_masked) },
285
+ { header: "STATUS", value: (token) => token.status },
286
+ { header: "EXPIRES", value: (token) => dash(token.expires_at) },
287
+ { header: "LAST USED", value: (token) => dash(token.last_used_at) }
288
+ ]);
289
+ }
290
+
291
+ function formatMaskedPrefix(prefix) {
292
+ return prefix ? `${prefix}***...****` : "";
293
+ }
@@ -0,0 +1,255 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJson } from "../lib/api-client.js";
4
+ import { promptText } from "../lib/prompt.js";
5
+ import { dash, formatTable } from "../lib/table.js";
6
+
7
+ export function createUsersCommand({
8
+ stdin = process.stdin,
9
+ stdout = process.stdout,
10
+ env = process.env
11
+ } = {}) {
12
+ const command = new Command("users")
13
+ .description("Review and manage organization users");
14
+
15
+ command.addCommand(createUsersListCommand({ stdout, env }));
16
+ command.addCommand(createUsersRemoveCommand({ stdin, stdout, env }));
17
+
18
+ return command;
19
+ }
20
+
21
+ function createUsersListCommand({ stdout, env }) {
22
+ return new Command("list")
23
+ .description("List organization users")
24
+ .option("--search <text>", "search user email, role, status, or display name")
25
+ .option("--role <role>", "role filter; validated against dashboard roles")
26
+ .option("--json", "print machine-readable output")
27
+ .action(async (options) => {
28
+ const [usersData, rolesData] = await Promise.all([
29
+ requestAuthenticatedJson("/api/users", { env }),
30
+ requestAuthenticatedJson("/api/users/roles", { env })
31
+ ]);
32
+
33
+ const roles = normalizeRoles(rolesData);
34
+ const role = parseOptionalRole(options.role, roles);
35
+ const users = normalizeUsers(usersData);
36
+ const filtered = applyUserFilters(users, {
37
+ search: options.search,
38
+ role
39
+ });
40
+ const output = buildUsersListOutput({
41
+ users,
42
+ filtered,
43
+ roles,
44
+ filters: {
45
+ search: options.search || null,
46
+ role
47
+ }
48
+ });
49
+
50
+ if (options.json) {
51
+ stdout.write(`${JSON.stringify(output)}\n`);
52
+ return;
53
+ }
54
+
55
+ stdout.write(`Users: ${output.total} total, ${output.matched} matched\n`);
56
+ if (!filtered.length) {
57
+ stdout.write("No users found.\n");
58
+ return;
59
+ }
60
+
61
+ stdout.write("\n");
62
+ stdout.write(`${formatUsersTable(filtered)}\n`);
63
+ });
64
+ }
65
+
66
+ function createUsersRemoveCommand({
67
+ stdin = process.stdin,
68
+ stdout = process.stdout,
69
+ env = process.env
70
+ }) {
71
+ return new Command("remove")
72
+ .description("Remove one organization user")
73
+ .argument("<user>", "user id, user_id, or email from users list")
74
+ .option("-y, --yes", "skip confirmation prompt")
75
+ .option("--json", "print machine-readable output")
76
+ .action(async (userRef, options) => {
77
+ const [usersData, currentUser] = await Promise.all([
78
+ requestAuthenticatedJson("/api/users", { env }),
79
+ requestAuthenticatedJson("/api/user/me", { env })
80
+ ]);
81
+ const users = normalizeUsers(usersData);
82
+ const target = resolveUserReference(userRef, users);
83
+
84
+ validateUserRemoval(target, users, currentUser);
85
+
86
+ if (!options.yes) {
87
+ const answer = await promptText({
88
+ message: `Remove user ${target.email || target.user_id}? Type "yes" to confirm: `,
89
+ stdin,
90
+ stdout,
91
+ optionName: "--yes"
92
+ });
93
+ if (answer.toLowerCase() !== "yes") {
94
+ stdout.write("User removal cancelled.\n");
95
+ return;
96
+ }
97
+ }
98
+
99
+ const data = await requestAuthenticatedJson(`/api/users/${encodeURIComponent(target.user_id)}`, {
100
+ env,
101
+ method: "DELETE"
102
+ });
103
+ const output = {
104
+ removed: true,
105
+ user_id: target.user_id,
106
+ email: target.email || null,
107
+ role: target.role || null,
108
+ message: data?.message || "User deleted successfully"
109
+ };
110
+
111
+ if (options.json) {
112
+ stdout.write(`${JSON.stringify(output)}\n`);
113
+ return;
114
+ }
115
+
116
+ stdout.write(`${output.message}: ${target.email || target.user_id}\n`);
117
+ });
118
+ }
119
+
120
+ function buildUsersListOutput({ users, filtered, roles, filters }) {
121
+ return {
122
+ total: users.length,
123
+ matched: filtered.length,
124
+ filters,
125
+ roles: roles.map((role) => role.value),
126
+ users: filtered
127
+ };
128
+ }
129
+
130
+ function normalizeUsers(data) {
131
+ const users = Array.isArray(data) ? data : (data?.users || []);
132
+ return users.map((user) => {
133
+ const id = user.id || user.user_id || "";
134
+ return {
135
+ id,
136
+ user_id: user.user_id || id,
137
+ email: normalizeEmail(user.email),
138
+ role: user.role || "user",
139
+ status: user.status || "",
140
+ active: user.active !== false,
141
+ display_name: user.display_name || "",
142
+ created_at: user.created_at || ""
143
+ };
144
+ });
145
+ }
146
+
147
+ function normalizeRoles(data) {
148
+ const roles = Array.isArray(data) ? data : (data?.roles || []);
149
+ return roles
150
+ .map((role) => ({
151
+ value: String(role?.value || "").trim(),
152
+ label: role?.label || "",
153
+ description: role?.description || ""
154
+ }))
155
+ .filter((role) => role.value);
156
+ }
157
+
158
+ function parseOptionalRole(value, roles) {
159
+ if (value === undefined || value === null || value === "") {
160
+ return null;
161
+ }
162
+
163
+ const role = String(value).trim().toLowerCase();
164
+ const allowed = roles.map((item) => item.value);
165
+ if (!allowed.includes(role)) {
166
+ throw new Error(`Invalid --role. Allowed values: ${allowed.join(", ")}.`);
167
+ }
168
+ return role;
169
+ }
170
+
171
+ function applyUserFilters(users, { search = "", role = null } = {}) {
172
+ let filtered = users;
173
+
174
+ if (role) {
175
+ filtered = filtered.filter((user) => String(user.role || "").toLowerCase() === role);
176
+ }
177
+
178
+ const query = String(search || "").trim().toLowerCase();
179
+ if (query) {
180
+ filtered = filtered.filter((user) =>
181
+ user.email.includes(query) ||
182
+ String(user.role || "").toLowerCase().includes(query) ||
183
+ String(user.status || "").toLowerCase().includes(query) ||
184
+ String(user.display_name || "").toLowerCase().includes(query)
185
+ );
186
+ }
187
+
188
+ return filtered;
189
+ }
190
+
191
+ function resolveUserReference(userRef, users) {
192
+ const ref = String(userRef || "").trim().toLowerCase();
193
+ const matches = users.filter((user) =>
194
+ String(user.user_id || "").toLowerCase() === ref ||
195
+ String(user.id || "").toLowerCase() === ref ||
196
+ user.email === ref
197
+ );
198
+
199
+ if (matches.length === 0) {
200
+ throw new Error(`User not found: ${userRef}`);
201
+ }
202
+ if (matches.length > 1) {
203
+ throw new Error(`User reference is ambiguous: ${userRef}`);
204
+ }
205
+ return matches[0];
206
+ }
207
+
208
+ function validateUserRemoval(target, users, currentUser) {
209
+ const targetEmail = normalizeEmail(target.email);
210
+ const currentEmail = normalizeEmail(currentUser?.email);
211
+ const currentIds = new Set([
212
+ currentUser?.user_id,
213
+ currentUser?.id,
214
+ currentUser?.subject
215
+ ].filter(Boolean).map((value) => String(value).toLowerCase()));
216
+ const targetIds = [
217
+ target.user_id,
218
+ target.id
219
+ ].filter(Boolean).map((value) => String(value).toLowerCase());
220
+
221
+ if ((targetEmail && currentEmail && targetEmail === currentEmail) ||
222
+ targetIds.some((id) => currentIds.has(id))) {
223
+ throw new Error("Refusing to remove the currently authenticated user.");
224
+ }
225
+
226
+ if (String(target.role || "").toLowerCase() === "admin") {
227
+ const activeAdmins = users.filter((user) =>
228
+ String(user.role || "").toLowerCase() === "admin" &&
229
+ isActiveUser(user)
230
+ );
231
+ if (activeAdmins.length <= 1) {
232
+ throw new Error("Refusing to remove the last active admin.");
233
+ }
234
+ }
235
+ }
236
+
237
+ function isActiveUser(user) {
238
+ const status = String(user.status || "active").toLowerCase();
239
+ return user.active !== false && status !== "revoked" && status !== "deleted";
240
+ }
241
+
242
+ function normalizeEmail(email) {
243
+ return String(email || "").trim().toLowerCase();
244
+ }
245
+
246
+ function formatUsersTable(users) {
247
+ return formatTable(users, [
248
+ { header: "USER ID", value: (user) => dash(user.user_id) },
249
+ { header: "EMAIL", value: (user) => dash(user.email) },
250
+ { header: "ROLE", value: (user) => dash(user.role) },
251
+ { header: "STATUS", value: (user) => dash(user.status) },
252
+ { header: "ACTIVE", value: (user) => user.active ? "yes" : "no" },
253
+ { header: "CREATED", value: (user) => dash(user.created_at) }
254
+ ]);
255
+ }
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+
3
+ import { requestAuthenticatedJsonWithMeta } from "../lib/api-client.js";
4
+ import { normalizeApiUrl } from "../lib/config.js";
5
+
6
+ export function createWhoamiCommand({
7
+ stdout = process.stdout,
8
+ stderr = process.stderr,
9
+ env = process.env
10
+ } = {}) {
11
+ return new Command("whoami")
12
+ .description("Print the currently authenticated ZeusLock user")
13
+ .option("--json", "print machine-readable output")
14
+ .action(async (options) => {
15
+ const user = await loadCurrentUser({ env });
16
+
17
+ if (options.json) {
18
+ stdout.write(`${JSON.stringify(user)}\n`);
19
+ return;
20
+ }
21
+
22
+ stdout.write(`Email: ${user.email || "unknown"}\n`);
23
+ stdout.write(`User ID: ${user.user_id || user.id || user.subject || "unknown"}\n`);
24
+ stdout.write(`Role: ${user.role || "unknown"}\n`);
25
+ stdout.write(`Org ID: ${user.org_id || "unknown"}\n`);
26
+ if (user.status) {
27
+ stdout.write(`Status: ${user.status}\n`);
28
+ }
29
+ stdout.write(`API URL: ${user.apiUrl}\n`);
30
+ });
31
+ }
32
+
33
+ async function loadCurrentUser({ env }) {
34
+ const { data, apiUrl } = await requestAuthenticatedJsonWithMeta("/api/user/me", { env });
35
+ return withApiUrl(data, apiUrl);
36
+ }
37
+
38
+ function withApiUrl(user, apiUrl) {
39
+ return {
40
+ ...user,
41
+ apiUrl: normalizeApiUrl(apiUrl) || apiUrl
42
+ };
43
+ }