| 1 | import * as vscode from "vscode"; |
| 2 | |
| 3 | const SECRET_KEY = "codewhale.runtimeToken"; |
| 4 | const SETTING_KEY = "runtimeToken"; |
| 5 | |
| 6 | let warnedAboutWorkspaceToken = false; |
| 7 | |
| 8 | /** Trim a setting value that arrived as `unknown` from a trust boundary. */ |
| 9 | function nonEmptyString(value: unknown): string | undefined { |
| 10 | if (typeof value !== "string") { |
| 11 | return undefined; |
| 12 | } |
| 13 | const trimmed = value.trim(); |
| 14 | return trimmed.length > 0 ? trimmed : undefined; |
| 15 | } |
| 16 | |
| 17 | /** |
| 18 | * Resolve the runtime bearer token. SecretStorage is authoritative; the legacy |
| 19 | * `codewhale.runtimeToken` setting is a one-way migration source only, which is |
| 20 | * what `package.json`'s deprecation message and `README.md` promise. |
| 21 | * |
| 22 | * Only a *user-level* value is migrated. A workspace- or folder-scoped value is |
| 23 | * attacker-controlled input: `codewhale.runtimeHost` is workspace-settable too, |
| 24 | * so a repo-local `.vscode/settings.json` that supplied both would make merely |
| 25 | * opening the repository ship a bearer token to a host of its choosing. Such a |
| 26 | * value is ignored, never adopted. |
| 27 | */ |
| 28 | export async function resolveToken(context: vscode.ExtensionContext): Promise<string | undefined> { |
| 29 | const stored = nonEmptyString(await context.secrets.get(SECRET_KEY)); |
| 30 | if (stored) { |
| 31 | return stored; |
| 32 | } |
| 33 | |
| 34 | const config = vscode.workspace.getConfiguration("codewhale"); |
| 35 | const inspected = config.inspect<string>(SETTING_KEY); |
| 36 | const userToken = nonEmptyString(inspected?.globalValue); |
| 37 | if (!userToken) { |
| 38 | const workspaceToken = |
| 39 | nonEmptyString(inspected?.workspaceValue) ?? |
| 40 | nonEmptyString(inspected?.workspaceFolderValue); |
| 41 | if (workspaceToken && !warnedAboutWorkspaceToken) { |
| 42 | warnedAboutWorkspaceToken = true; |
| 43 | void vscode.window.showWarningMessage( |
| 44 | "Ignoring codewhale.runtimeToken from workspace settings: a workspace cannot supply the runtime bearer token. Use CodeWhale: Set Runtime Token.", |
| 45 | ); |
| 46 | } |
| 47 | return undefined; |
| 48 | } |
| 49 | |
| 50 | await context.secrets.store(SECRET_KEY, userToken); |
| 51 | // One-way migration: drop the plaintext copy so it stops riding Settings Sync. |
| 52 | try { |
| 53 | await config.update(SETTING_KEY, undefined, vscode.ConfigurationTarget.Global); |
| 54 | } catch { |
| 55 | // A read-only settings.json must not cost the user a working token; the |
| 56 | // secret is already stored, so keep going and leave the plaintext behind. |
| 57 | } |
| 58 | return userToken; |
| 59 | } |
| 60 | |
| 61 | export async function storeToken(context: vscode.ExtensionContext, token: string): Promise<void> { |
| 62 | await context.secrets.store(SECRET_KEY, token.trim()); |
| 63 | } |
| 64 | |
| 65 | export async function promptForToken(context: vscode.ExtensionContext): Promise<string | undefined> { |
| 66 | const entered = await vscode.window.showInputBox({ |
| 67 | prompt: "Codewhale runtime bearer token (stored in VS Code secret storage)", |
| 68 | password: true, |
| 69 | ignoreFocusOut: true, |
| 70 | }); |
| 71 | if (entered === undefined) { |
| 72 | return undefined; |
| 73 | } |
| 74 | const token = entered.trim(); |
| 75 | if (token.length === 0) { |
| 76 | await context.secrets.delete(SECRET_KEY); |
| 77 | return undefined; |
| 78 | } |
| 79 | await storeToken(context, token); |
| 80 | return token; |
| 81 | } |
| 82 |