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,308 @@
1
+ import {
2
+ createAuthRecord,
3
+ deleteAuthStore,
4
+ readAuthStore,
5
+ writeAuthStore
6
+ } from "./auth-store.js";
7
+ import { readApiKeyStore } from "./api-key-store.js";
8
+ import { assertCliAccessToken } from "./cli-token.js";
9
+ import { getCommandToken } from "./command-token.js";
10
+ import { DEFAULT_API_URL, resolveApiUrl } from "./config.js";
11
+
12
+ const CLI_TOKEN_401_MESSAGE = "CLI access token expired, revoked, or invalid. Generate a new token on the dashboard CLI page and run `zeuslock auth login --token <token>`.";
13
+
14
+ export class ApiError extends Error {
15
+ constructor(message, { status = null, data = null, responseText = "" } = {}) {
16
+ super(message);
17
+ this.name = "ApiError";
18
+ this.status = status;
19
+ this.data = data;
20
+ this.responseText = responseText;
21
+ }
22
+ }
23
+
24
+ export async function login({ apiUrl, email, password, recaptchaToken = null }) {
25
+ const body = { email, password };
26
+ if (recaptchaToken) {
27
+ body.recaptcha_token = recaptchaToken;
28
+ }
29
+
30
+ return requestJson(apiUrl, "/api/auth/login", {
31
+ method: "POST",
32
+ body
33
+ });
34
+ }
35
+
36
+ export async function verifyMfa({ apiUrl, session, code }) {
37
+ return requestJson(apiUrl, "/api/auth/verify-mfa", {
38
+ method: "POST",
39
+ body: { session, code }
40
+ });
41
+ }
42
+
43
+ export async function refreshAuth({ apiUrl, refreshToken }) {
44
+ return requestJson(apiUrl, "/api/auth/refresh", {
45
+ method: "POST",
46
+ body: { refresh_token: refreshToken }
47
+ });
48
+ }
49
+
50
+ export async function getCurrentUser({ apiUrl, accessToken }) {
51
+ return requestJson(apiUrl, "/api/user/me", {
52
+ headers: {
53
+ Authorization: `Bearer ${accessToken}`
54
+ }
55
+ });
56
+ }
57
+
58
+ export async function getCliTokenIdentity({ apiUrl, accessToken }) {
59
+ return requestJson(apiUrl, "/api/auth/me", {
60
+ headers: {
61
+ Authorization: `Bearer ${accessToken}`
62
+ }
63
+ });
64
+ }
65
+
66
+ export async function resolveApiUrlForRequest(env = process.env) {
67
+ const auth = await readAuthStore(env);
68
+ const apiKey = await readApiKeyStore(env);
69
+ return resolveApiUrl({ env, auth: auth || apiKey }) || DEFAULT_API_URL;
70
+ }
71
+
72
+ export async function requestApiKeyJson(endpoint, {
73
+ method = "GET",
74
+ body,
75
+ headers = {},
76
+ apiKey,
77
+ apiUrl,
78
+ signal,
79
+ env = process.env
80
+ } = {}) {
81
+ const stored = await readApiKeyStore(env);
82
+ const resolvedApiKey = String(apiKey || env.ZEUSLOCK_API_KEY || stored?.apiKey || "").trim();
83
+ if (!resolvedApiKey) {
84
+ throw new Error("API key is required. Provide --api-key, set ZEUSLOCK_API_KEY, or run `zeuslock auth api-key set`.");
85
+ }
86
+
87
+ const resolvedApiUrl = apiUrl || stored?.apiUrl || await resolveApiUrlForRequest(env);
88
+ return requestJson(resolvedApiUrl, endpoint, {
89
+ method,
90
+ body,
91
+ signal,
92
+ headers: {
93
+ ...headers,
94
+ "X-API-Key": resolvedApiKey
95
+ }
96
+ });
97
+ }
98
+
99
+ export async function requestApiKeyFormData(endpoint, formData, {
100
+ headers = {},
101
+ apiKey,
102
+ apiUrl,
103
+ signal,
104
+ env = process.env
105
+ } = {}) {
106
+ const stored = await readApiKeyStore(env);
107
+ const resolvedApiKey = String(apiKey || env.ZEUSLOCK_API_KEY || stored?.apiKey || "").trim();
108
+ if (!resolvedApiKey) {
109
+ throw new Error("API key is required. Provide --api-key, set ZEUSLOCK_API_KEY, or run `zeuslock auth api-key set`.");
110
+ }
111
+
112
+ const resolvedApiUrl = apiUrl || stored?.apiUrl || await resolveApiUrlForRequest(env);
113
+ return requestFormData(resolvedApiUrl, endpoint, formData, {
114
+ signal,
115
+ headers: {
116
+ ...headers,
117
+ "X-API-Key": resolvedApiKey
118
+ }
119
+ });
120
+ }
121
+
122
+ export async function requestAuthenticatedJson(endpoint, {
123
+ method = "GET",
124
+ body,
125
+ headers = {},
126
+ env = process.env
127
+ } = {}) {
128
+ const result = await requestAuthenticatedJsonWithMeta(endpoint, {
129
+ method,
130
+ body,
131
+ headers,
132
+ env
133
+ });
134
+ return result.data;
135
+ }
136
+
137
+ export async function requestAuthenticatedJsonWithMeta(endpoint, {
138
+ method = "GET",
139
+ body,
140
+ headers = {},
141
+ env = process.env
142
+ } = {}) {
143
+ const context = await resolveAuthenticatedContext(env);
144
+ const requestWithToken = (token) => requestJson(context.apiUrl, endpoint, {
145
+ method,
146
+ body,
147
+ headers: {
148
+ ...headers,
149
+ Authorization: `Bearer ${token}`
150
+ }
151
+ });
152
+
153
+ try {
154
+ return {
155
+ data: await requestWithToken(context.accessToken),
156
+ apiUrl: context.apiUrl,
157
+ authMode: context.mode,
158
+ authSource: context.source
159
+ };
160
+ } catch (error) {
161
+ if (context.mode === "token" && error instanceof ApiError && error.status === 401) {
162
+ throw new Error(CLI_TOKEN_401_MESSAGE);
163
+ }
164
+
165
+ if (context.mode === "token" || !(error instanceof ApiError) || error.status !== 401) {
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ if (!context.auth?.refreshToken) {
171
+ await deleteAuthStore(env);
172
+ throw new Error("Session expired. Run `zeuslock auth login` again.");
173
+ }
174
+
175
+ try {
176
+ const tokenResponse = await refreshAuth({ apiUrl: context.apiUrl, refreshToken: context.auth.refreshToken });
177
+ const refreshedAuth = createAuthRecord({
178
+ apiUrl: context.apiUrl,
179
+ email: context.auth.email,
180
+ tokenResponse,
181
+ previous: context.auth
182
+ });
183
+ await writeAuthStore(refreshedAuth, env);
184
+ return {
185
+ data: await requestWithToken(refreshedAuth.accessToken),
186
+ apiUrl: context.apiUrl,
187
+ authMode: refreshedAuth.mode,
188
+ authSource: context.source
189
+ };
190
+ } catch (error) {
191
+ if (error instanceof ApiError && error.status === 401) {
192
+ await deleteAuthStore(env);
193
+ throw new Error("Session expired. Run `zeuslock auth login` again.");
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ async function resolveAuthenticatedContext(env) {
200
+ const auth = await readAuthStore(env);
201
+ const commandToken = getCommandToken(env);
202
+ const envToken = String(env.ZEUSLOCK_API_TOKEN || "").trim();
203
+ const apiUrl = resolveApiUrl({ env, auth }) || DEFAULT_API_URL;
204
+
205
+ if (commandToken) {
206
+ assertCliAccessToken(commandToken, "--token");
207
+ return {
208
+ auth,
209
+ apiUrl,
210
+ accessToken: commandToken,
211
+ mode: "token",
212
+ source: "flag"
213
+ };
214
+ }
215
+
216
+ if (envToken) {
217
+ return {
218
+ auth,
219
+ apiUrl,
220
+ accessToken: envToken,
221
+ mode: "token",
222
+ source: "env"
223
+ };
224
+ }
225
+
226
+ if (auth?.accessToken) {
227
+ return {
228
+ auth,
229
+ apiUrl,
230
+ accessToken: auth.accessToken,
231
+ mode: auth.mode === "token" ? "token" : "session",
232
+ source: "store"
233
+ };
234
+ }
235
+
236
+ throw new Error("Not logged in. Run `zeuslock auth login` first.");
237
+ }
238
+
239
+ export async function requestJson(apiUrl, endpoint, { method = "GET", body, headers = {}, signal } = {}) {
240
+ const url = new URL(endpoint, ensureTrailingSlash(apiUrl));
241
+ const requestHeaders = {
242
+ Accept: "application/json",
243
+ ...headers
244
+ };
245
+
246
+ const request = {
247
+ method,
248
+ headers: requestHeaders,
249
+ signal
250
+ };
251
+
252
+ if (body !== undefined) {
253
+ requestHeaders["Content-Type"] = "application/json";
254
+ request.body = JSON.stringify(body);
255
+ }
256
+
257
+ return fetchJson(url, request);
258
+ }
259
+
260
+ export async function requestFormData(apiUrl, endpoint, formData, { headers = {}, signal } = {}) {
261
+ const url = new URL(endpoint, ensureTrailingSlash(apiUrl));
262
+ const request = {
263
+ method: "POST",
264
+ headers: {
265
+ Accept: "application/json",
266
+ ...headers
267
+ },
268
+ body: formData,
269
+ signal
270
+ };
271
+
272
+ return fetchJson(url, request);
273
+ }
274
+
275
+ async function fetchJson(url, request) {
276
+ let response;
277
+ try {
278
+ response = await fetch(url, request);
279
+ } catch (error) {
280
+ throw new ApiError(error?.message || "Network request failed");
281
+ }
282
+
283
+ const contentType = response.headers.get("content-type") || "";
284
+ let data = null;
285
+ let responseText = "";
286
+
287
+ if (contentType.includes("application/json")) {
288
+ data = await response.json().catch(() => null);
289
+ } else {
290
+ responseText = await response.text().catch(() => "");
291
+ }
292
+
293
+ if (!response.ok) {
294
+ const message =
295
+ data?.error ||
296
+ data?.detail ||
297
+ data?.message ||
298
+ responseText ||
299
+ `Request failed with status ${response.status}`;
300
+ throw new ApiError(message, { status: response.status, data, responseText });
301
+ }
302
+
303
+ return data ?? {};
304
+ }
305
+
306
+ function ensureTrailingSlash(apiUrl) {
307
+ return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
308
+ }
@@ -0,0 +1,84 @@
1
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { getConfigDir } from "./auth-store.js";
5
+ import { normalizeApiUrl } from "./config.js";
6
+
7
+ const API_KEY_FILE_NAME = "api-key.json";
8
+
9
+ export function getApiKeyFilePath(env = process.env) {
10
+ return path.join(getConfigDir(env), API_KEY_FILE_NAME);
11
+ }
12
+
13
+ export async function readApiKeyStore(env = process.env) {
14
+ try {
15
+ const raw = await readFile(getApiKeyFilePath(env), "utf8");
16
+ return normalizeApiKeyRecord(JSON.parse(raw));
17
+ } catch (error) {
18
+ if (error?.code === "ENOENT") {
19
+ return null;
20
+ }
21
+ throw error;
22
+ }
23
+ }
24
+
25
+ export async function writeApiKeyStore(record, env = process.env) {
26
+ const apiKeyRecord = normalizeApiKeyRecord(record);
27
+ if (!apiKeyRecord?.apiKey) {
28
+ throw new Error("Cannot save API key state without an API key");
29
+ }
30
+
31
+ const dir = getConfigDir(env);
32
+ const filePath = getApiKeyFilePath(env);
33
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
34
+
35
+ await mkdir(dir, { recursive: true, mode: 0o700 });
36
+ await chmod(dir, 0o700).catch(() => {});
37
+ await writeFile(tempPath, `${JSON.stringify(apiKeyRecord, null, 2)}\n`, {
38
+ mode: 0o600
39
+ });
40
+ await chmod(tempPath, 0o600).catch(() => {});
41
+ await rename(tempPath, filePath);
42
+ await chmod(filePath, 0o600).catch(() => {});
43
+
44
+ return apiKeyRecord;
45
+ }
46
+
47
+ export async function deleteApiKeyStore(env = process.env) {
48
+ await rm(getApiKeyFilePath(env), { force: true });
49
+ }
50
+
51
+ export function createApiKeyRecord({ apiUrl, apiKey, verified = null }) {
52
+ return normalizeApiKeyRecord({
53
+ apiUrl,
54
+ apiKey,
55
+ apiKeyMasked: maskApiKey(apiKey),
56
+ verified,
57
+ savedAt: new Date().toISOString()
58
+ });
59
+ }
60
+
61
+ export function maskApiKey(apiKey) {
62
+ const value = String(apiKey || "").trim();
63
+ if (!value) {
64
+ return "";
65
+ }
66
+ if (value.length <= 12) {
67
+ return `${value.slice(0, 4)}***`;
68
+ }
69
+ return `${value.slice(0, 8)}***${value.slice(-4)}`;
70
+ }
71
+
72
+ function normalizeApiKeyRecord(record) {
73
+ if (!record || typeof record !== "object") {
74
+ return null;
75
+ }
76
+
77
+ return {
78
+ apiUrl: normalizeApiUrl(record.apiUrl),
79
+ apiKey: typeof record.apiKey === "string" ? record.apiKey : null,
80
+ apiKeyMasked: typeof record.apiKeyMasked === "string" ? record.apiKeyMasked : maskApiKey(record.apiKey),
81
+ verified: record.verified && typeof record.verified === "object" ? record.verified : null,
82
+ savedAt: typeof record.savedAt === "string" ? record.savedAt : null
83
+ };
84
+ }
@@ -0,0 +1,123 @@
1
+ import { mkdir, readFile, rename, rm, chmod, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const AUTH_FILE_NAME = "auth.json";
6
+
7
+ export function getConfigDir(env = process.env, platform = process.platform) {
8
+ if (env.ZEUSLOCK_CONFIG_DIR?.trim()) {
9
+ return path.resolve(env.ZEUSLOCK_CONFIG_DIR.trim());
10
+ }
11
+
12
+ if (platform === "darwin") {
13
+ return path.join(os.homedir(), "Library", "Application Support", "zeuslock");
14
+ }
15
+
16
+ if (platform === "win32") {
17
+ const appData = env.APPDATA?.trim() || path.join(os.homedir(), "AppData", "Roaming");
18
+ return path.join(appData, "ZeusLock");
19
+ }
20
+
21
+ const xdgConfigHome = env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config");
22
+ return path.join(xdgConfigHome, "zeuslock");
23
+ }
24
+
25
+ export function getAuthFilePath(env = process.env) {
26
+ return path.join(getConfigDir(env), AUTH_FILE_NAME);
27
+ }
28
+
29
+ export async function readAuthStore(env = process.env) {
30
+ const filePath = getAuthFilePath(env);
31
+
32
+ try {
33
+ const raw = await readFile(filePath, "utf8");
34
+ const parsed = JSON.parse(raw);
35
+ return normalizeAuthRecord(parsed);
36
+ } catch (error) {
37
+ if (error?.code === "ENOENT") {
38
+ return null;
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ export async function writeAuthStore(record, env = process.env) {
45
+ const authRecord = normalizeAuthRecord(record);
46
+ if (!authRecord?.accessToken) {
47
+ throw new Error("Cannot save auth state without an access token");
48
+ }
49
+
50
+ const dir = getConfigDir(env);
51
+ const filePath = getAuthFilePath(env);
52
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
53
+
54
+ await mkdir(dir, { recursive: true, mode: 0o700 });
55
+ await chmod(dir, 0o700).catch(() => {});
56
+ await writeFile(tempPath, `${JSON.stringify(authRecord, null, 2)}\n`, {
57
+ mode: 0o600
58
+ });
59
+ await chmod(tempPath, 0o600).catch(() => {});
60
+ await rename(tempPath, filePath);
61
+ await chmod(filePath, 0o600).catch(() => {});
62
+
63
+ return authRecord;
64
+ }
65
+
66
+ export async function deleteAuthStore(env = process.env) {
67
+ await rm(getAuthFilePath(env), { force: true });
68
+ }
69
+
70
+ export function createAuthRecord({ apiUrl, email, tokenResponse, previous = null }) {
71
+ const now = new Date();
72
+ const expiresIn = Number(tokenResponse.expires_in);
73
+ const expiresAt = Number.isFinite(expiresIn)
74
+ ? new Date(now.getTime() + expiresIn * 1000).toISOString()
75
+ : (previous?.expiresAt ?? null);
76
+
77
+ return normalizeAuthRecord({
78
+ ...previous,
79
+ mode: "session",
80
+ apiUrl,
81
+ email: email ?? previous?.email ?? null,
82
+ tokenType: tokenResponse.token_type || previous?.tokenType || "Bearer",
83
+ accessToken: tokenResponse.access_token || previous?.accessToken,
84
+ refreshToken: tokenResponse.refresh_token || previous?.refreshToken || null,
85
+ idToken: tokenResponse.id_token || previous?.idToken || null,
86
+ expiresAt,
87
+ savedAt: now.toISOString()
88
+ });
89
+ }
90
+
91
+ export function createCliTokenAuthRecord({ apiUrl, accessToken, email = null }) {
92
+ return normalizeAuthRecord({
93
+ mode: "token",
94
+ apiUrl,
95
+ email,
96
+ tokenType: "Bearer",
97
+ accessToken,
98
+ refreshToken: null,
99
+ idToken: null,
100
+ expiresAt: null,
101
+ savedAt: new Date().toISOString()
102
+ });
103
+ }
104
+
105
+ function normalizeAuthRecord(record) {
106
+ if (!record || typeof record !== "object") {
107
+ return null;
108
+ }
109
+
110
+ const mode = record.mode === "token" ? "token" : "session";
111
+
112
+ return {
113
+ mode,
114
+ apiUrl: typeof record.apiUrl === "string" ? record.apiUrl : null,
115
+ email: typeof record.email === "string" ? record.email : null,
116
+ tokenType: typeof record.tokenType === "string" ? record.tokenType : "Bearer",
117
+ accessToken: typeof record.accessToken === "string" ? record.accessToken : null,
118
+ refreshToken: mode === "token" ? null : typeof record.refreshToken === "string" ? record.refreshToken : null,
119
+ idToken: mode === "token" ? null : typeof record.idToken === "string" ? record.idToken : null,
120
+ expiresAt: mode === "token" ? null : typeof record.expiresAt === "string" ? record.expiresAt : null,
121
+ savedAt: typeof record.savedAt === "string" ? record.savedAt : null
122
+ };
123
+ }
@@ -0,0 +1,22 @@
1
+ const CLI_TOKEN_PATTERN = /^zlu_[A-Za-z0-9_-]{43}$/;
2
+
3
+ export function isCliAccessToken(value) {
4
+ return CLI_TOKEN_PATTERN.test(String(value || "").trim());
5
+ }
6
+
7
+ export function assertCliAccessToken(value, optionName = "--token") {
8
+ if (!isCliAccessToken(value)) {
9
+ throw new Error(`Invalid ${optionName}. Expected zlu_ followed by 43 URL-safe characters.`);
10
+ }
11
+ }
12
+
13
+ export function maskCliToken(token) {
14
+ const value = String(token || "").trim();
15
+ if (!value) {
16
+ return "";
17
+ }
18
+ if (value.length <= 12) {
19
+ return `${value.slice(0, 4)}***`;
20
+ }
21
+ return `${value.slice(0, 8)}***...${value.slice(-4)}`;
22
+ }
@@ -0,0 +1,15 @@
1
+ const commandTokens = new WeakMap();
2
+
3
+ export function setCommandToken(env, token) {
4
+ if (!env || typeof env !== "object") {
5
+ return;
6
+ }
7
+ commandTokens.set(env, String(token || "").trim());
8
+ }
9
+
10
+ export function getCommandToken(env) {
11
+ if (!env || typeof env !== "object") {
12
+ return "";
13
+ }
14
+ return commandTokens.get(env) || "";
15
+ }
@@ -0,0 +1,27 @@
1
+ import { getCommandToken } from "./command-token.js";
2
+
3
+ export const DEFAULT_API_URL = "https://api.zeuslock.ai";
4
+
5
+ export function readEnvConfig(env = process.env) {
6
+ const apiUrl = normalizeApiUrl(env.ZEUSLOCK_API_URL);
7
+ const hasApiToken = Boolean(getCommandToken(env) || env.ZEUSLOCK_API_TOKEN?.trim());
8
+ const hasApiKey = Boolean(env.ZEUSLOCK_API_KEY?.trim());
9
+
10
+ return {
11
+ apiUrl,
12
+ hasApiToken,
13
+ hasApiKey
14
+ };
15
+ }
16
+
17
+ export function resolveApiUrl({ env = process.env, auth = null } = {}) {
18
+ return normalizeApiUrl(env.ZEUSLOCK_API_URL) || normalizeApiUrl(auth?.apiUrl) || DEFAULT_API_URL;
19
+ }
20
+
21
+ export function normalizeApiUrl(value) {
22
+ const candidate = typeof value === "string" ? value.trim() : "";
23
+ if (!candidate) {
24
+ return null;
25
+ }
26
+ return candidate.replace(/\/+$/, "");
27
+ }