@cantinasecurity/apex-cli 0.1.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.
package/dist/auth.js ADDED
@@ -0,0 +1,143 @@
1
+ import { openInBrowser } from "./browser.js";
2
+ import { saveCredentials } from "./config.js";
3
+ import { ApiError } from "./api-client.js";
4
+ import { APEX_CLI_VERSION } from "./version.js";
5
+ const DEVICE_CLIENT_NAME = "apex";
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ function log(message, quiet = false) {
10
+ if (!quiet) {
11
+ process.stderr.write(`${message}\n`);
12
+ }
13
+ }
14
+ function toCredentials(poll) {
15
+ return {
16
+ accessToken: poll.accessToken,
17
+ refreshToken: poll.refreshToken,
18
+ accessTokenExpiresAt: poll.accessTokenExpiresAt,
19
+ refreshTokenExpiresAt: poll.refreshTokenExpiresAt,
20
+ };
21
+ }
22
+ export async function getMe(client) {
23
+ try {
24
+ return await client.request("/api/cli/v1/me");
25
+ }
26
+ catch (error) {
27
+ if (error instanceof ApiError && error.status === 401) {
28
+ return null;
29
+ }
30
+ throw error;
31
+ }
32
+ }
33
+ export async function startDeviceLogin(client) {
34
+ return client.request("/api/cli/v1/auth/device/start", {
35
+ method: "POST",
36
+ auth: false,
37
+ json: {
38
+ clientName: DEVICE_CLIENT_NAME,
39
+ clientVersion: APEX_CLI_VERSION,
40
+ },
41
+ });
42
+ }
43
+ export async function pollDeviceLogin(client, deviceCode) {
44
+ return client.request("/api/cli/v1/auth/device/poll", {
45
+ method: "POST",
46
+ auth: false,
47
+ json: { deviceCode },
48
+ retryOnUnauthorized: false,
49
+ });
50
+ }
51
+ export async function waitForDeviceLoginApproval(client, options) {
52
+ let intervalSeconds = Math.max(0, options.intervalSeconds ?? 5);
53
+ let expiresAt = options.expiresAt ?? null;
54
+ const timeoutAt = Date.now() + Math.max(0, options.timeoutMs ?? 5 * 60 * 1000);
55
+ function remainingTimeoutMs() {
56
+ return Math.max(0, timeoutAt - Date.now());
57
+ }
58
+ while (remainingTimeoutMs() > 0) {
59
+ const expiresAtTime = expiresAt ? new Date(expiresAt).getTime() : Number.POSITIVE_INFINITY;
60
+ if (Number.isFinite(expiresAtTime) && Date.now() >= expiresAtTime) {
61
+ return {
62
+ status: "expired",
63
+ expiresAt,
64
+ };
65
+ }
66
+ const sleepMs = Math.min(intervalSeconds * 1000, remainingTimeoutMs());
67
+ if (sleepMs > 0) {
68
+ await sleep(sleepMs);
69
+ }
70
+ if (remainingTimeoutMs() === 0) {
71
+ break;
72
+ }
73
+ try {
74
+ const polled = await pollDeviceLogin(client, options.deviceCode);
75
+ if (polled.status === "pending") {
76
+ intervalSeconds = Math.max(0, polled.intervalSeconds);
77
+ expiresAt = polled.expiresAt;
78
+ continue;
79
+ }
80
+ await saveCredentials(toCredentials(polled));
81
+ return {
82
+ status: "approved",
83
+ me: await client.request("/api/cli/v1/me"),
84
+ };
85
+ }
86
+ catch (error) {
87
+ if (error instanceof ApiError && [409, 410].includes(error.status)) {
88
+ return {
89
+ status: "expired",
90
+ expiresAt,
91
+ };
92
+ }
93
+ throw error;
94
+ }
95
+ }
96
+ return {
97
+ status: "pending",
98
+ retryAfterSeconds: intervalSeconds,
99
+ expiresAt,
100
+ };
101
+ }
102
+ export async function login(client, options) {
103
+ const existing = await getMe(client);
104
+ if (existing) {
105
+ return existing;
106
+ }
107
+ const started = await startDeviceLogin(client);
108
+ log(`Login code: ${started.userCode}`, options.quiet);
109
+ log(`Open: ${started.verificationUrl}`, options.quiet);
110
+ if (!options.noOpen) {
111
+ try {
112
+ await openInBrowser(started.verificationUrl);
113
+ }
114
+ catch (error) {
115
+ log(`Failed to open browser automatically: ${String(error)}`, options.quiet);
116
+ }
117
+ }
118
+ const result = await waitForDeviceLoginApproval(client, {
119
+ deviceCode: started.deviceCode,
120
+ intervalSeconds: started.intervalSeconds,
121
+ expiresAt: started.expiresAt,
122
+ timeoutMs: Math.max(0, new Date(started.expiresAt).getTime() - Date.now()),
123
+ });
124
+ if (result.status === "approved") {
125
+ return result.me;
126
+ }
127
+ throw new Error("CLI login timed out before approval completed");
128
+ }
129
+ export async function logout(client) {
130
+ const credentials = await client.getCredentials();
131
+ if (!credentials) {
132
+ return;
133
+ }
134
+ try {
135
+ await client.request("/api/cli/v1/auth/logout", {
136
+ method: "POST",
137
+ auth: true,
138
+ });
139
+ }
140
+ finally {
141
+ await client.clearCredentials();
142
+ }
143
+ }
@@ -0,0 +1,18 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function openInBrowser(url) {
3
+ const platform = process.platform;
4
+ const command = platform === "darwin"
5
+ ? ["open", url]
6
+ : platform === "win32"
7
+ ? ["cmd", "/c", "start", "", url]
8
+ : ["xdg-open", url];
9
+ await new Promise((resolve, reject) => {
10
+ const child = spawn(command[0], command.slice(1), {
11
+ detached: true,
12
+ stdio: "ignore",
13
+ });
14
+ child.on("error", reject);
15
+ child.unref();
16
+ resolve();
17
+ });
18
+ }