claude-aws-mfa 1.2.1 → 1.3.0
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/CHANGELOG.md +11 -1
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/config.ts +1 -0
- package/src/dialog-html.ts +5 -2
- package/src/dialog.ts +3 -0
- package/src/index.ts +4 -0
- package/src/sts.ts +4 -1
- package/test/dialog-html.test.ts +3 -1
- package/test/sts.test.ts +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,16 @@ Releases are automated via CI. To create a new release:
|
|
|
22
22
|
Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
|
|
23
23
|
(e.g. `v1.2.0-rc.1`) publish to npm `@next` and create a GitHub pre-release.
|
|
24
24
|
|
|
25
|
+
## [v1.3.0] - 2026-10-14
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Configurable STS session name in the credentials dialog (defaults to `claude-aws-mfa`).
|
|
29
|
+
|
|
30
|
+
## [v1.2.2] - 2026-10-09
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- Changed GitHub username
|
|
34
|
+
|
|
25
35
|
## [v1.2.1] - 2026-08-10
|
|
26
36
|
|
|
27
37
|
### Fixed
|
|
@@ -32,7 +42,7 @@ Stable versions (e.g. `v1.2.0`) publish to npm `@latest`. Pre-release versions
|
|
|
32
42
|
## [v1.2.0] - 2026-08-10
|
|
33
43
|
|
|
34
44
|
### Added
|
|
35
|
-
- **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/
|
|
45
|
+
- **Claude Code profile awareness**: `--setup` now reads and writes `$CLAUDE_CONFIG_DIR/settings.json` rather than always `~/.claude/settings.json`, so a tool like [claude-as](https://github.com/aaronmeriwether/claude-as) can give one project a Bedrock profile while others stay on OAuth. The saved config and lock file are scoped to the profile as well (`~/.config/claude-aws-mfa<encoded-config-dir>.json`), since separate profiles generally mean separate AWS accounts and a shared cached session would otherwise hand a profile credentials for the wrong account. The default `~/.claude` is unsuffixed, so existing installs keep their current config file with no migration. Since the lock is per-profile, two profiles can prompt at the same time, so the credential, error, and setup dialogs all name their profile in the window title and in a banner under the heading. The default config dir is named too — to a multi-profile user an unlabelled dialog reads equally as "this is the default profile" and "this build does not label profiles".
|
|
36
46
|
- **`--no-ui` mode**: skip the GUI entirely and drive the tool from the command line — useful for scripting and for letting an LLM operate the tool during setup/debugging. Prints credentials JSON to stdout on success, keeping stdout cleanly parsable; on failure, prints the underlying error (or names the specific missing config field(s) when the config is incomplete) to stderr. Never imports the GUI toolkit, so it also works in headless environments.
|
|
37
47
|
- **Error dialog**: authentication failures (STS or MFA command) now show a native dialog box instead of writing to stderr, since Claude Code doesn't surface a failing command's stderr to the user. Suppressed in `--no-ui` mode, where the error is printed to stderr instead.
|
|
38
48
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Add this (plus any addtitional config you prefer) to your Claude Code settings (
|
|
|
47
47
|
## How it works
|
|
48
48
|
|
|
49
49
|
1. On first run, seeds the dialog with credentials from your existing AWS config (`~/.aws/credentials`, environment variables, SSO, etc.)
|
|
50
|
-
2. Shows a native GUI dialog with fields for region, access key, secret key, MFA ARN, role ARN, and session
|
|
50
|
+
2. Shows a native GUI dialog with fields for region, access key, secret key, MFA ARN, role ARN, session duration, and STS session name (defaults to `claude-aws-mfa`)
|
|
51
51
|
3. Collects the MFA token — either type a 6-digit code directly, or switch to **MFA Command** mode and provide a shell command that outputs the code (e.g. `op item get --otp …` for 1Password CLI)
|
|
52
52
|
4. Calls `sts:AssumeRole` with the MFA token
|
|
53
53
|
5. If the requested session duration is rejected, automatically retries with shorter durations (12h → 6h → 2h → 1h)
|
|
@@ -58,7 +58,7 @@ On subsequent runs, all fields are pre-populated from the saved config — just
|
|
|
58
58
|
|
|
59
59
|
### Claude Code profiles
|
|
60
60
|
|
|
61
|
-
Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/
|
|
61
|
+
Claude Code reads its settings from `$CLAUDE_CONFIG_DIR` when that is set, which tools such as [claude-as](https://github.com/aaronmeriwether/claude-as) use to give each project its own credentials. This tool follows suit:
|
|
62
62
|
|
|
63
63
|
- `--setup` reads and writes `$CLAUDE_CONFIG_DIR/settings.json`, so a profile can be Bedrock-only while others stay on OAuth.
|
|
64
64
|
- The config file is scoped per profile — `~/.config/claude-aws-mfa<profile>.json`, where `<profile>` is the config dir path encoded the way Claude encodes project dirs (`/` and `.` both become `-`). Different profiles usually mean different AWS accounts, and a shared cached session would hand a profile credentials for the wrong one.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-aws-mfa",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"repository": "https://github.com/
|
|
5
|
+
"repository": "https://github.com/aaronmeriwether/claude-aws-mfa",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-aws-mfa": "./src/index.ts"
|
|
8
8
|
},
|
package/src/config.ts
CHANGED
package/src/dialog-html.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { STANDARD_DURATIONS } from "./sts";
|
|
1
|
+
import { DEFAULT_SESSION_NAME, STANDARD_DURATIONS } from "./sts";
|
|
2
2
|
import { AUTOSIZE_CSS, AUTOSIZE_SCRIPT } from "./autosize-html";
|
|
3
3
|
import { DEFAULT_PROFILE, type ActiveProfile } from "./config";
|
|
4
4
|
|
|
5
|
-
export const FIELDS = ["region", "accessKeyId", "secretAccessKey", "mfaArn", "roleArn", "duration", "mfaMode", "mfaCode", "mfaCommand"] as const;
|
|
5
|
+
export const FIELDS = ["region", "accessKeyId", "secretAccessKey", "mfaArn", "roleArn", "duration", "sessionName", "mfaMode", "mfaCode", "mfaCommand"] as const;
|
|
6
6
|
|
|
7
7
|
export const FIELD_PATTERNS: Record<string, string> = {
|
|
8
8
|
region: "^[a-z]{2}(-[a-z]+-\\d+)$",
|
|
@@ -11,6 +11,8 @@ export const FIELD_PATTERNS: Record<string, string> = {
|
|
|
11
11
|
mfaArn: "^arn:aws:iam::\\d{12}:mfa/.+$",
|
|
12
12
|
roleArn: "^arn:aws:iam::\\d{12}:role/.+$",
|
|
13
13
|
duration: "^[1-9]\\d*$",
|
|
14
|
+
// AWS RoleSessionName constraint.
|
|
15
|
+
sessionName: "^[\\w+=,.@-]{2,64}$",
|
|
14
16
|
mfaCode: "^\\d{6}$",
|
|
15
17
|
mfaCommand: "^.+$",
|
|
16
18
|
};
|
|
@@ -90,6 +92,7 @@ ${PROFILE_STYLE}
|
|
|
90
92
|
<div class="field"><label>MFA Device ARN</label><input id="mfaArn" placeholder="arn:aws:iam::123456789012:mfa/user"></div>
|
|
91
93
|
<div class="field"><label>Role ARN</label><input id="roleArn" placeholder="arn:aws:iam::123456789012:role/MyRole"></div>
|
|
92
94
|
<div class="field"><label>Session Duration (seconds)</label><input id="duration" placeholder="${STANDARD_DURATIONS[0]}"></div>
|
|
95
|
+
<div class="field"><label>Session Name</label><input id="sessionName" placeholder="${DEFAULT_SESSION_NAME}"></div>
|
|
93
96
|
<hr>
|
|
94
97
|
<div class="radio-field">
|
|
95
98
|
<input type="radio" name="mfaMode" id="mfaModeCode" value="code" checked>
|
package/src/dialog.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { lib } from "webview-bun/src/ffi";
|
|
|
3
3
|
import { activeProfile, type Config } from "./config";
|
|
4
4
|
import { buildHtml, buildErrorHtml } from "./dialog-html";
|
|
5
5
|
import { START_HEIGHT, bindAutosize } from "./autosize";
|
|
6
|
+
import { DEFAULT_SESSION_NAME } from "./sts";
|
|
6
7
|
import { version } from "../package.json";
|
|
7
8
|
|
|
8
9
|
const DIALOG_WIDTH = 440;
|
|
@@ -24,6 +25,7 @@ export interface DialogResult {
|
|
|
24
25
|
mfaArn: string;
|
|
25
26
|
roleArn: string;
|
|
26
27
|
duration: string;
|
|
28
|
+
sessionName: string;
|
|
27
29
|
mfaMode: "code" | "command";
|
|
28
30
|
mfaCode: string;
|
|
29
31
|
mfaCommand: string;
|
|
@@ -81,6 +83,7 @@ export function showDialog(defaults: Partial<Config>): DialogResult | null {
|
|
|
81
83
|
mfaArn: defaults.mfaArn ?? "",
|
|
82
84
|
roleArn: defaults.roleArn ?? "",
|
|
83
85
|
duration: defaults.duration != null ? String(defaults.duration) : "",
|
|
86
|
+
sessionName: defaults.sessionName ?? DEFAULT_SESSION_NAME,
|
|
84
87
|
mfaCommand: defaults.mfaCommand ?? "",
|
|
85
88
|
mfaMode: defaults.mfaMode ?? "code",
|
|
86
89
|
}, activeProfile()));
|
package/src/index.ts
CHANGED
|
@@ -92,6 +92,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
|
|
|
92
92
|
roleArn: cfg.roleArn,
|
|
93
93
|
mfaCode,
|
|
94
94
|
duration: cfg.duration ?? STANDARD_DURATIONS[0],
|
|
95
|
+
sessionName: cfg.sessionName,
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
const session: CachedSession = {
|
|
@@ -108,6 +109,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
|
|
|
108
109
|
mfaArn: cfg.mfaArn,
|
|
109
110
|
roleArn: cfg.roleArn,
|
|
110
111
|
duration,
|
|
112
|
+
sessionName: cfg.sessionName,
|
|
111
113
|
mfaMode: cfg.mfaMode,
|
|
112
114
|
mfaCommand: cfg.mfaCommand,
|
|
113
115
|
cacheSession: cfg.cacheSession,
|
|
@@ -146,6 +148,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
|
|
|
146
148
|
roleArn: result.roleArn,
|
|
147
149
|
mfaCode,
|
|
148
150
|
duration: parseInt(result.duration, 10) || STANDARD_DURATIONS[0],
|
|
151
|
+
sessionName: result.sessionName,
|
|
149
152
|
});
|
|
150
153
|
|
|
151
154
|
const session: CachedSession = {
|
|
@@ -162,6 +165,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
|
|
|
162
165
|
mfaArn: result.mfaArn,
|
|
163
166
|
roleArn: result.roleArn,
|
|
164
167
|
duration,
|
|
168
|
+
sessionName: result.sessionName || undefined,
|
|
165
169
|
mfaMode: result.mfaMode,
|
|
166
170
|
mfaCommand: result.mfaCommand || undefined,
|
|
167
171
|
cacheSession: defaults.cacheSession,
|
package/src/sts.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_SESSION_NAME = "claude-aws-mfa";
|
|
4
|
+
|
|
3
5
|
export const STANDARD_DURATIONS = [129600, 86400, 43200, 28800, 21600, 14400, 7200, 3600];
|
|
4
6
|
|
|
5
7
|
export function isDurationError(err: unknown): boolean {
|
|
@@ -25,6 +27,7 @@ export async function assumeRoleWithMfa(params: {
|
|
|
25
27
|
roleArn: string;
|
|
26
28
|
mfaCode: string;
|
|
27
29
|
duration: number;
|
|
30
|
+
sessionName?: string;
|
|
28
31
|
}) {
|
|
29
32
|
const client = new STSClient({
|
|
30
33
|
region: params.region,
|
|
@@ -37,7 +40,7 @@ export async function assumeRoleWithMfa(params: {
|
|
|
37
40
|
try {
|
|
38
41
|
const { Credentials: c } = await client.send(new AssumeRoleCommand({
|
|
39
42
|
RoleArn: params.roleArn,
|
|
40
|
-
RoleSessionName:
|
|
43
|
+
RoleSessionName: params.sessionName || DEFAULT_SESSION_NAME,
|
|
41
44
|
SerialNumber: params.mfaArn,
|
|
42
45
|
TokenCode: params.mfaCode,
|
|
43
46
|
DurationSeconds: ladder[i],
|
package/test/dialog-html.test.ts
CHANGED
|
@@ -103,7 +103,7 @@ describe("buildHtml", () => {
|
|
|
103
103
|
test("FIELDS list matches expected form fields", () => {
|
|
104
104
|
expect(FIELDS).toEqual([
|
|
105
105
|
"region", "accessKeyId", "secretAccessKey",
|
|
106
|
-
"mfaArn", "roleArn", "duration", "mfaMode", "mfaCode", "mfaCommand",
|
|
106
|
+
"mfaArn", "roleArn", "duration", "sessionName", "mfaMode", "mfaCode", "mfaCommand",
|
|
107
107
|
]);
|
|
108
108
|
});
|
|
109
109
|
|
|
@@ -128,6 +128,7 @@ describe("buildHtml", () => {
|
|
|
128
128
|
mfaArn: "arn:aws:iam::123456789012:mfa/user",
|
|
129
129
|
roleArn: "arn:aws:iam::123456789012:role/TestRole",
|
|
130
130
|
duration: "43200",
|
|
131
|
+
sessionName: "claude-aws-mfa",
|
|
131
132
|
mfaCode: "123456",
|
|
132
133
|
mfaCommand: "op item get --otp myitem",
|
|
133
134
|
};
|
|
@@ -138,6 +139,7 @@ describe("buildHtml", () => {
|
|
|
138
139
|
mfaArn: "not-an-arn",
|
|
139
140
|
roleArn: "not-an-arn",
|
|
140
141
|
duration: "abc",
|
|
142
|
+
sessionName: "has spaces",
|
|
141
143
|
mfaCode: "12",
|
|
142
144
|
mfaCommand: "",
|
|
143
145
|
};
|
package/test/sts.test.ts
CHANGED
|
@@ -117,6 +117,12 @@ describe("assumeRoleWithMfa", () => {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("passes a custom session name to STS", async () => {
|
|
121
|
+
await assumeRoleWithMfa({ ...BASE_PARAMS, sessionName: "alice-laptop" });
|
|
122
|
+
|
|
123
|
+
expect(capturedCommands[0].RoleSessionName).toBe("alice-laptop");
|
|
124
|
+
});
|
|
125
|
+
|
|
120
126
|
test("falls back to shorter duration on duration error", async () => {
|
|
121
127
|
let callCount = 0;
|
|
122
128
|
mockSend = mock(() => {
|