File size: 2,219 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Server-side gate for the remote login helper's "paste credentials" flow.
 *
 * Google's `firstparty/nativeapp` consent for embedded desktop clients only
 * releases the authorization code when the loopback redirect is reachable, which
 * never happens on a remote VPS install. The remote login helper runs the OAuth
 * locally and emits a credential blob (see ./credentialBlob.ts); the dashboard
 * POSTs that blob to /api/oauth/<provider>/paste-credentials, which decodes it
 * and persists the connection via the same finalize path as `device-complete`.
 *
 * This module holds the pure, security-relevant gate: which providers may use
 * the paste path, and the requirement that the blob's embedded provider matches
 * the route provider (so a blob minted for one provider cannot be replayed
 * against another).
 */

import { decodeCredentialBlob, type CredentialBlob } from "./credentialBlob";

/**
 * Providers eligible for the paste-credentials flow: Google native-loopback
 * clients whose consent cannot complete on a headless/remote host. `agy` is the
 * Antigravity alias. Codex is intentionally excluded — it has its own browser
 * device flow (`device-complete`) that works remotely.
 */
export const PASTE_CREDENTIAL_PROVIDERS = new Set(["antigravity", "agy", "gemini-cli"]);

/**
 * Validate + decode a pasted credential blob for a given route provider.
 * Throws a descriptive error if the provider is not allowlisted, if the blob's
 * embedded provider does not match, or if the blob itself is malformed.
 */
export function parsePastedCredentials(routeProvider: string, blob: string): CredentialBlob {
  if (!PASTE_CREDENTIAL_PROVIDERS.has(routeProvider)) {
    throw new Error(
      `paste-credentials not supported for provider: ${routeProvider}. ` +
        `Supported: ${[...PASTE_CREDENTIAL_PROVIDERS].join(", ")}`
    );
  }

  // decodeCredentialBlob validates prefix/version/JSON shape + access_token.
  const decoded = decodeCredentialBlob(blob);

  if (decoded.provider !== routeProvider) {
    throw new Error(
      `Pasted credential provider mismatch: blob is for "${decoded.provider}" ` +
        `but the route provider is "${routeProvider}"`
    );
  }

  return decoded;
}