@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 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).
@@ -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 loginUrl = buildLoginUrl(params.profile, callbackServer.callbackUrl, state);
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 callback = await callbackServer.waitForCallback(CALLBACK_TIMEOUT_MS);
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: callback.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
@@ -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[];
@@ -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",
@@ -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
- resolveCallback?.({ code, state });
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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectorplane/ctrl-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Official VectorPlane CLI.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",