File size: 3,824 Bytes
26abd99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import vscode from 'vscode';

const EXCHANGE_TIMEOUT_MS = 60_000;

/**
 * A promise that can have its underlying resources (like timers or listeners)
 * cleaned up.
 */
interface DisposablePromise<T> extends vscode.Disposable {
  promise: Promise<T>;
}

/**
 * Provides the mechanism to wait for and resolve authorization codes. This
 * class is disposable and will reject any pending operations when disposed.
 */
export class CodeManager implements vscode.Disposable {
  private readonly inFlightPromises = new Map<
    string,
    { resolve: (value: string) => void; reject: (reason: Error) => void }
  >();

  /**
   * Rejects all pending `waitForCode` promises. This should be called
   * when the class instance is no longer needed to prevent resource leaks.
   */
  dispose(): void {
    const error = new Error('Authentication provider has been disposed.');
    for (const promiseHandlers of this.inFlightPromises.values()) {
      promiseHandlers.reject(error);
    }
    this.inFlightPromises.clear();
  }

  /**
   * Waits for an authorization code corresponding to the provided nonce.
   * A nonce can only be used once.
   *
   * @param nonce - A unique string to correlate the request and response.
   * @param token - A cancellation token to cancel the request.
   * @returns A promise that resolves with the authorization code.
   */
  async waitForCode(
    nonce: string,
    token: vscode.CancellationToken,
  ): Promise<string> {
    if (this.inFlightPromises.has(nonce)) {
      throw new Error(`Already waiting for nonce: ${nonce}`);
    }

    const userCancellation = waitForCancellation(token);
    const timeout = waitForTimeout(EXCHANGE_TIMEOUT_MS);

    try {
      const codePromise = new Promise<string>((resolve, reject) => {
        this.inFlightPromises.set(nonce, { resolve, reject });
      });

      // Race the main promise against timeout and user cancellation.
      return await Promise.race([
        codePromise,
        userCancellation.promise,
        timeout.promise,
      ]);
    } finally {
      this.inFlightPromises.delete(nonce);
      userCancellation.dispose();
      timeout.dispose();
    }
  }

  /**
   * Resolves the in-flight promise corresponding to the provided nonce
   * with the provided authorization code.
   *
   * @param nonce - The unique nonce used to correlate the request and response.
   * @param code - The authorization code to resolve for the associated nonce.
   */
  resolveCode(nonce: string, code: string): void {
    const inFlight = this.inFlightPromises.get(nonce);
    if (!inFlight) {
      throw new Error('Unexpected code exchange received');
    }

    inFlight.resolve(code);
  }
}

/**
 * Creates a promise that rejects when the cancellation token is triggered.
 * Returns a disposable to remove the event listener.
 */
function waitForCancellation(
  token: vscode.CancellationToken,
): DisposablePromise<never> {
  let listener: vscode.Disposable;
  const promise = new Promise<never>((_, reject) => {
    listener = token.onCancellationRequested(() => {
      reject(new Error('Authentication was cancelled by the user'));
    });
  });

  return {
    promise,
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    dispose: () => listener.dispose(),
  };
}

/**
 * Creates a promise that rejects after a specified timeout.
 * Returns a disposable to clear the timer.
 */
function waitForTimeout(ms: number): DisposablePromise<never> {
  let timeoutId: NodeJS.Timeout;
  const promise = new Promise<never>((_, reject) => {
    timeoutId = setTimeout(() => {
      reject(new Error('Exchange timeout exceeded'));
    }, ms);
  });

  return {
    promise,
    dispose: () => {
      clearTimeout(timeoutId);
    },
  };
}