Skip to content

Commit 4aa9f2e

Browse files
committed
feat: add developer command to copy raw JWT to clipboard
New command `artemis.showJwtToken` ("Artemis: Show JWT Token (Developer)") copies the currently active JWT to the clipboard for curl/Postman-based server testing. Scope: - Hidden from the Command Palette unless `artemis.developerMode` is enabled (via `when: config.artemis.developerMode` clause). - Additionally gated at runtime so direct invocation via `vscode.commands.executeCommand` also respects the setting. - Works in both auth modes: Desktop (Cookie) and Theia (Bearer). The `jwt=` cookie prefix is stripped so the clipboard contains the raw JWT value, ready to paste into a `Cookie: jwt=<value>` or `Authorization: Bearer <value>` header. Security: - Full token is NEVER shown in the UI — only a masked preview (first 20 chars) appears in the warning notification. - Full token is NEVER logged — only the masked preview lands in the Artemis Extension output channel. - Notification wording explicitly warns "Do not share, do not commit". Also adds a new public helper `AuthManager.getRawJwt()` that extracts the raw JWT, intended for developer/debug surfaces only.
1 parent 7cc768e commit 4aa9f2e

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

extension/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,18 @@
8282
{
8383
"command": "artemis.clearTrustedDomains",
8484
"title": "Artemis: Clear Trusted Domains"
85+
},
86+
{
87+
"command": "artemis.showJwtToken",
88+
"title": "Artemis: Show JWT Token (Developer)",
89+
"icon": "$(key)"
8590
}
8691
],
8792
"menus": {
8893
"commandPalette": [
8994
{ "command": "artemis.login", "when": "!iris:managedEnvironment" },
90-
{ "command": "artemis.logout", "when": "!iris:managedEnvironment" }
95+
{ "command": "artemis.logout", "when": "!iris:managedEnvironment" },
96+
{ "command": "artemis.showJwtToken", "when": "config.artemis.developerMode" }
9197
]
9298
},
9399
"viewsContainers": {

extension/src/extension/activation/extensionCommands.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { IProviderRegistry } from '../services/ui';
66
import type { TelemetryManager } from '../services/telemetry';
77
import type { ArtemisWebviewProvider, ChatWebviewProvider } from '../provider';
88
import { logger, LogCategory } from '../services/loggingService';
9-
import { processPlantUml, normalizeRelativePath, extractErrorMessage } from '../utils';
9+
import { processPlantUml, normalizeRelativePath, extractErrorMessage, VSCODE_CONFIG } from '../utils';
1010
import { executeReplayCommand } from '../services/telemetry/replay';
1111

1212
// ── Individual command registrations ─────────────────────────────────
@@ -422,6 +422,50 @@ function registerReplaySessionCommand(globalStorageUri: vscode.Uri): vscode.Disp
422422
});
423423
}
424424

425+
/**
426+
* Developer-only command: copy the current raw JWT to the clipboard for use
427+
* in curl/Postman based server testing. Gated on the `artemis.developerMode`
428+
* setting both at the menu level (commandPalette `when` clause) and at runtime
429+
* (defense-in-depth against direct invocation via `vscode.commands.executeCommand`).
430+
*
431+
* The full token is NEVER shown in the UI or written to logs — only a masked
432+
* preview appears in the notification, and the full value lands in the clipboard.
433+
*/
434+
function registerShowJwtTokenCommand(authManager: AuthManager): vscode.Disposable {
435+
return vscode.commands.registerCommand('artemis.showJwtToken', async () => {
436+
const config = vscode.workspace.getConfiguration(VSCODE_CONFIG.ARTEMIS_SECTION);
437+
const developerMode = config.get<boolean>(VSCODE_CONFIG.DEVELOPER_MODE_KEY, false);
438+
if (!developerMode) {
439+
vscode.window.showErrorMessage(
440+
`Enable '${VSCODE_CONFIG.ARTEMIS_SECTION}.${VSCODE_CONFIG.DEVELOPER_MODE_KEY}' in settings to use this command.`
441+
);
442+
return;
443+
}
444+
445+
const hasToken = await authManager.hasAuthToken();
446+
if (!hasToken) {
447+
vscode.window.showInformationMessage('Not logged in to Artemis — no JWT to show.');
448+
return;
449+
}
450+
451+
const rawJwt = await authManager.getRawJwt();
452+
if (!rawJwt) {
453+
vscode.window.showErrorMessage('Failed to retrieve JWT token from secret storage.');
454+
logger.error('getRawJwt returned undefined despite hasAuthToken=true', LogCategory.AUTH);
455+
return;
456+
}
457+
458+
await vscode.env.clipboard.writeText(rawJwt);
459+
460+
const preview = `${rawJwt.substring(0, 20)}...`;
461+
logger.info(`JWT copied to clipboard via developer command (preview: ${preview})`, LogCategory.AUTH);
462+
463+
vscode.window.showWarningMessage(
464+
`JWT copied to clipboard (${preview}). Do not share, do not commit.`
465+
);
466+
});
467+
}
468+
425469
// ── Aggregate registration ───────────────────────────────────────────
426470

427471
export interface CommandDeps {
@@ -449,5 +493,6 @@ export function registerAllCommands(deps: CommandDeps): vscode.Disposable {
449493
registerClearTrustedDomainsCommand(deps.context),
450494
registerStruggleScoreCommand(deps.telemetryManager),
451495
registerReplaySessionCommand(deps.context.globalStorageUri),
496+
registerShowJwtTokenCommand(deps.authManager),
452497
);
453498
}

extension/src/extension/services/auth/authManager.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ export class AuthManager {
5757
return this.getStoredToken();
5858
}
5959

60+
/**
61+
* Returns the raw JWT string (without any "jwt=" cookie prefix), suitable
62+
* for use in a `Cookie: jwt=<value>` or `Authorization: Bearer <value>` header.
63+
*
64+
* Intended for developer/debug commands only — normal code paths should use
65+
* `getAuthHeaders()` instead so auth-mode handling stays centralized.
66+
*
67+
* Returns `undefined` if not authenticated.
68+
*/
69+
public async getRawJwt(): Promise<string | undefined> {
70+
const stored = await this.getStoredToken();
71+
if (!stored) {
72+
return undefined;
73+
}
74+
// Desktop mode stores the token as "jwt=<value>" (cookie string).
75+
// Theia mode stores the raw JWT directly. Strip the prefix if present.
76+
const prefix = `${CONFIG.AUTH_COOKIE_NAME}=`;
77+
return stored.startsWith(prefix) ? stored.substring(prefix.length) : stored;
78+
}
79+
6080
/**
6181
* Returns the stored token string.
6282
* In Desktop mode this is a cookie string ("jwt=<token>"),

0 commit comments

Comments
 (0)