@vectorplane/ctrl-cli 0.1.2 → 0.1.4
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/README.md +6 -0
- package/dist/core/api.d.ts +3 -1
- package/dist/core/api.js +14 -0
- package/dist/core/auth.js +48 -10
- package/dist/core/constants.d.ts +1 -0
- package/dist/core/constants.js +1 -0
- package/dist/core/server.js +14 -1
- package/dist/types/auth.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ vp sync
|
|
|
25
25
|
- abre o navegador para autenticação
|
|
26
26
|
- conclui o login com callback local seguro
|
|
27
27
|
- salva a sessão localmente
|
|
28
|
+
- em ambientes remotos ou isolados, o loopback local pode exigir um fluxo alternativo
|
|
28
29
|
|
|
29
30
|
### `vp sync`
|
|
30
31
|
|
|
@@ -72,3 +73,8 @@ vp sync
|
|
|
72
73
|
- tokens nunca são enviados por query string
|
|
73
74
|
- o callback valida o `state`
|
|
74
75
|
- a sessão fica no diretório do usuário
|
|
76
|
+
|
|
77
|
+
## Notas de fluxo
|
|
78
|
+
|
|
79
|
+
O plano de endurecimento do login local está em [docs/cli-auth-hardening-phases.md](/home/developer/Documentos/Projetos/vectorplane-ctrl-cli/docs/cli-auth-hardening-phases.md).
|
|
80
|
+
O desenho multicanal atual do login está em [docs/cli-login-multichannel.md](/home/developer/Documentos/Projetos/vectorplane-ctrl-cli/docs/cli-login-multichannel.md).
|
package/dist/core/api.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "./logger.js";
|
|
2
|
-
import type { CurrentUserResponse, AuthCodeExchangeRequest, AuthTokenExchangeResponse, RefreshTokenRequest } from "../types/auth.js";
|
|
2
|
+
import type { CurrentUserResponse, AuthCodeExchangeRequest, AuthTokenExchangeResponse, CreateLoginAttemptRequest, CreateLoginAttemptResponse, LoginAttemptStatusResponse, RefreshTokenRequest } from "../types/auth.js";
|
|
3
3
|
import type { AgentCheckoutRequest, AgentHeartbeatRequest, AgentSessionRequest, AgentSessionResponse, EventRequest, ResolveWorkspaceRequest, ResolveWorkspaceResponse, SyncRequest, SyncResponse } from "../types/api.js";
|
|
4
4
|
export declare class VectorPlaneApiClient {
|
|
5
5
|
private readonly apiBaseUrl;
|
|
@@ -7,6 +7,8 @@ export declare class VectorPlaneApiClient {
|
|
|
7
7
|
private readonly logger;
|
|
8
8
|
constructor(apiBaseUrl: string, timeoutMs: number, logger: Logger);
|
|
9
9
|
exchangeToken(payload: AuthCodeExchangeRequest): Promise<AuthTokenExchangeResponse>;
|
|
10
|
+
createLoginAttempt(payload: CreateLoginAttemptRequest): Promise<CreateLoginAttemptResponse>;
|
|
11
|
+
getLoginAttemptStatus(attemptId: string, pollToken: string): Promise<LoginAttemptStatusResponse>;
|
|
10
12
|
refreshToken(payload: RefreshTokenRequest): Promise<AuthTokenExchangeResponse>;
|
|
11
13
|
getCurrentUser(accessToken: string): Promise<CurrentUserResponse>;
|
|
12
14
|
sync(accessToken: string, payload: SyncRequest): Promise<SyncResponse>;
|
package/dist/core/api.js
CHANGED
|
@@ -71,6 +71,20 @@ export class VectorPlaneApiClient {
|
|
|
71
71
|
body: JSON.stringify(payload),
|
|
72
72
|
}, this.timeoutMs, this.logger);
|
|
73
73
|
}
|
|
74
|
+
async createLoginAttempt(payload) {
|
|
75
|
+
return requestJson(`${this.apiBaseUrl}/cli/login/attempts`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
}, this.timeoutMs, this.logger);
|
|
80
|
+
}
|
|
81
|
+
async getLoginAttemptStatus(attemptId, pollToken) {
|
|
82
|
+
const url = new URL(`${this.apiBaseUrl}/cli/login/attempts/${attemptId}`);
|
|
83
|
+
url.searchParams.set("poll_token", pollToken);
|
|
84
|
+
return requestJson(url.toString(), {
|
|
85
|
+
method: "GET",
|
|
86
|
+
}, this.timeoutMs, this.logger);
|
|
87
|
+
}
|
|
74
88
|
async refreshToken(payload) {
|
|
75
89
|
return requestJson(`${this.apiBaseUrl}/cli/token/refresh`, {
|
|
76
90
|
method: "POST",
|
package/dist/core/auth.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { CALLBACK_TIMEOUT_MS, CLI_CLIENT_ID } from "./constants.js";
|
|
3
|
+
import { CALLBACK_TIMEOUT_MS, CLI_CLIENT_ID, LOGIN_ATTEMPT_POLL_INTERVAL_MS } from "./constants.js";
|
|
4
4
|
import { AuthError } from "./errors.js";
|
|
5
5
|
import { createCallbackServer } from "./server.js";
|
|
6
|
-
function buildLoginUrl(profile, callbackUrl, state) {
|
|
7
|
-
const url = new URL("/cli/login", profile.appBaseUrl);
|
|
8
|
-
url.searchParams.set("redirect_uri", callbackUrl);
|
|
9
|
-
url.searchParams.set("state", state);
|
|
10
|
-
return url.toString();
|
|
11
|
-
}
|
|
12
6
|
export function generateLoginState() {
|
|
13
7
|
return randomBytes(24).toString("hex");
|
|
14
8
|
}
|
|
@@ -35,7 +29,12 @@ export async function runLoginFlow(params) {
|
|
|
35
29
|
const state = generateLoginState();
|
|
36
30
|
const callbackServer = await createCallbackServer(params.config.callbackHost, params.config.callbackPort, params.config.callbackPath, state);
|
|
37
31
|
try {
|
|
38
|
-
const
|
|
32
|
+
const loginAttempt = await params.apiClient.createLoginAttempt({
|
|
33
|
+
redirectUri: callbackServer.callbackUrl,
|
|
34
|
+
state,
|
|
35
|
+
client: CLI_CLIENT_ID,
|
|
36
|
+
});
|
|
37
|
+
const loginUrl = loginAttempt.loginUrl;
|
|
39
38
|
params.logger.info("abrindo navegador para autenticação...");
|
|
40
39
|
const opened = await openBrowser(loginUrl);
|
|
41
40
|
if (!opened) {
|
|
@@ -43,9 +42,20 @@ export async function runLoginFlow(params) {
|
|
|
43
42
|
process.stdout.write(`${loginUrl}\n`);
|
|
44
43
|
}
|
|
45
44
|
params.logger.info("aguardando confirmação...");
|
|
46
|
-
const
|
|
45
|
+
const code = await Promise.any([
|
|
46
|
+
callbackServer.waitForCallback(CALLBACK_TIMEOUT_MS).then((callback) => callback.code),
|
|
47
|
+
waitForAuthorizedAttempt({
|
|
48
|
+
apiClient: params.apiClient,
|
|
49
|
+
attemptId: loginAttempt.attemptId,
|
|
50
|
+
pollToken: loginAttempt.pollToken,
|
|
51
|
+
timeoutMs: CALLBACK_TIMEOUT_MS,
|
|
52
|
+
logger: params.logger,
|
|
53
|
+
}),
|
|
54
|
+
]).catch((error) => {
|
|
55
|
+
throw new AuthError("Não foi possível concluir o login do CLI.", error);
|
|
56
|
+
});
|
|
47
57
|
const payload = {
|
|
48
|
-
code
|
|
58
|
+
code,
|
|
49
59
|
client: CLI_CLIENT_ID,
|
|
50
60
|
device: params.machine,
|
|
51
61
|
runtime: params.runtime,
|
|
@@ -68,4 +78,32 @@ export async function runLoginFlow(params) {
|
|
|
68
78
|
await callbackServer.close();
|
|
69
79
|
}
|
|
70
80
|
}
|
|
81
|
+
async function waitForAuthorizedAttempt(params) {
|
|
82
|
+
const startedAt = Date.now();
|
|
83
|
+
let announcedAuthorization = false;
|
|
84
|
+
while (Date.now() - startedAt < params.timeoutMs) {
|
|
85
|
+
const status = await params.apiClient.getLoginAttemptStatus(params.attemptId, params.pollToken);
|
|
86
|
+
const code = pickAuthorizedCode(status);
|
|
87
|
+
if (code) {
|
|
88
|
+
return code;
|
|
89
|
+
}
|
|
90
|
+
if (status.status === "authorized" && !announcedAuthorization) {
|
|
91
|
+
announcedAuthorization = true;
|
|
92
|
+
params.logger.info("autorização recebida pela API. finalizando sessão...");
|
|
93
|
+
}
|
|
94
|
+
await delay(LOGIN_ATTEMPT_POLL_INTERVAL_MS);
|
|
95
|
+
}
|
|
96
|
+
throw new AuthError("Tempo esgotado aguardando a confirmação do login do CLI.");
|
|
97
|
+
}
|
|
98
|
+
function pickAuthorizedCode(status) {
|
|
99
|
+
if ((status.status === "authorized" || status.status === "completed") && typeof status.code === "string" && status.code.length > 0) {
|
|
100
|
+
return status.code;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function delay(ms) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
setTimeout(resolve, ms);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
71
109
|
//# sourceMappingURL=auth.js.map
|
package/dist/core/constants.d.ts
CHANGED
|
@@ -18,5 +18,6 @@ export declare const DEFAULT_STATE: CliState;
|
|
|
18
18
|
export declare const DEFAULT_QUEUE: QueueStore;
|
|
19
19
|
export declare const DEFAULT_WORKSPACE_BINDINGS: WorkspaceBindingStore;
|
|
20
20
|
export declare const CALLBACK_TIMEOUT_MS = 120000;
|
|
21
|
+
export declare const LOGIN_ATTEMPT_POLL_INTERVAL_MS = 1000;
|
|
21
22
|
export declare const SENSITIVE_KEYS: string[];
|
|
22
23
|
export declare const SAFE_ENV_KEYS: string[];
|
package/dist/core/constants.js
CHANGED
|
@@ -56,6 +56,7 @@ export const DEFAULT_WORKSPACE_BINDINGS = {
|
|
|
56
56
|
bindings: {},
|
|
57
57
|
};
|
|
58
58
|
export const CALLBACK_TIMEOUT_MS = 120_000;
|
|
59
|
+
export const LOGIN_ATTEMPT_POLL_INTERVAL_MS = 1_000;
|
|
59
60
|
export const SENSITIVE_KEYS = ["token", "authorization", "refresh", "secret", "password", "cookie"];
|
|
60
61
|
export const SAFE_ENV_KEYS = [
|
|
61
62
|
"SHELL",
|
package/dist/core/server.js
CHANGED
|
@@ -4,6 +4,8 @@ export async function createCallbackServer(host, port, callbackPath, expectedSta
|
|
|
4
4
|
let resolveCallback = null;
|
|
5
5
|
let rejectCallback = null;
|
|
6
6
|
let settled = false;
|
|
7
|
+
let pendingResult = null;
|
|
8
|
+
let pendingError = null;
|
|
7
9
|
const server = http.createServer((request, response) => {
|
|
8
10
|
try {
|
|
9
11
|
const url = new URL(request.url ?? "/", `http://${host}:${port}`);
|
|
@@ -36,7 +38,9 @@ export async function createCallbackServer(host, port, callbackPath, expectedSta
|
|
|
36
38
|
response.end("<html><body><p>VectorPlane: autenticação recebida. Você já pode voltar ao terminal.</p></body></html>");
|
|
37
39
|
if (!settled) {
|
|
38
40
|
settled = true;
|
|
39
|
-
|
|
41
|
+
const result = { code, state };
|
|
42
|
+
pendingResult = result;
|
|
43
|
+
resolveCallback?.(result);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
catch (error) {
|
|
@@ -44,6 +48,7 @@ export async function createCallbackServer(host, port, callbackPath, expectedSta
|
|
|
44
48
|
response.end("VectorPlane callback error.");
|
|
45
49
|
if (!settled) {
|
|
46
50
|
settled = true;
|
|
51
|
+
pendingError = error;
|
|
47
52
|
rejectCallback?.(error);
|
|
48
53
|
}
|
|
49
54
|
}
|
|
@@ -65,6 +70,14 @@ export async function createCallbackServer(host, port, callbackPath, expectedSta
|
|
|
65
70
|
return new Promise((resolve, reject) => {
|
|
66
71
|
resolveCallback = resolve;
|
|
67
72
|
rejectCallback = reject;
|
|
73
|
+
if (pendingError) {
|
|
74
|
+
reject(pendingError);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (pendingResult) {
|
|
78
|
+
resolve(pendingResult);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
68
81
|
const timer = setTimeout(() => {
|
|
69
82
|
if (!settled) {
|
|
70
83
|
settled = true;
|
package/dist/types/auth.d.ts
CHANGED
|
@@ -5,6 +5,26 @@ export interface AuthCodeExchangeRequest {
|
|
|
5
5
|
device: MachineContext;
|
|
6
6
|
runtime: RuntimeContext;
|
|
7
7
|
}
|
|
8
|
+
export interface CreateLoginAttemptRequest {
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
state: string;
|
|
11
|
+
client: "vp-cli";
|
|
12
|
+
}
|
|
13
|
+
export interface CreateLoginAttemptResponse {
|
|
14
|
+
attemptId: string;
|
|
15
|
+
pollToken: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
loginUrl: string;
|
|
18
|
+
}
|
|
19
|
+
export interface LoginAttemptStatusResponse {
|
|
20
|
+
attemptId: string;
|
|
21
|
+
status: "pending" | "authorized" | "completed";
|
|
22
|
+
code: string | null;
|
|
23
|
+
workspace: string | null;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
authorizedAt: string | null;
|
|
26
|
+
completedAt: string | null;
|
|
27
|
+
}
|
|
8
28
|
export interface AuthTokenExchangeResponse {
|
|
9
29
|
access_token: string;
|
|
10
30
|
refresh_token: string;
|