claude-aws-mfa 1.0.0-rc.4 → 1.0.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/README.md +4 -10
- package/package.json +1 -1
- package/src/dialog-html.ts +3 -1
- package/src/dialog.ts +2 -1
- package/src/index.ts +2 -2
- package/src/setup-html.ts +21 -40
- package/src/setup.ts +1 -1
- package/src/sts.ts +1 -1
- package/test/sts.test.ts +6 -6
package/README.md
CHANGED
|
@@ -14,25 +14,19 @@ bun install -g claude-aws-mfa@latest
|
|
|
14
14
|
|
|
15
15
|
### Guided Setup
|
|
16
16
|
|
|
17
|
-
Run `claude-aws-mfa --setup`, to access a configuration UI for easy setup of this tool. You will be presented with a checkbox to enable
|
|
17
|
+
Run `claude-aws-mfa --setup`, to access a configuration UI for easy setup of this tool. You will be presented with a checkbox to enable Bedrock via claude-aws-mfa, and fields to customize a few key settings. Specifically, you can pin specific versions of the Anthropic models to use for Opus, Sonnet, and Haiku, in case your Bedrock policy does not allow you access to the latest default models that Claude Code will try to use.
|
|
18
18
|
|
|
19
19
|
Once you have enabled credential-handling and saved the settings, you can launch Claude Code and begin work. You will automatically prompted for your AWS credentials, and Claude will connect to the specified AWS Bedrock account rather than to the Anthropic servers.
|
|
20
20
|
|
|
21
21
|
### Manual Setup
|
|
22
22
|
|
|
23
|
-
Add to your Claude Code settings (`~/.claude/settings.json`):
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"awsCredentialExport": "claude-aws-mfa"
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
If you have an existing `~/.aws/credentials` file, you will likely need to suppress Claude's use of that file so it will correctly invoke the `awsCredentialExport` tool instead.
|
|
23
|
+
Add this (plus any addtitional config you prefer) to your Claude Code settings (`~/.claude/settings.json`):
|
|
32
24
|
|
|
33
25
|
```json
|
|
34
26
|
{
|
|
27
|
+
"awsCredentialExport": "claude-aws-mfa",
|
|
35
28
|
"env": {
|
|
29
|
+
"CLAUDE_CODE_USE_BEDROCK": "1",
|
|
36
30
|
"AWS_SHARED_CREDENTIALS_FILE": "/dev/null"
|
|
37
31
|
}
|
|
38
32
|
}
|
package/package.json
CHANGED
package/src/dialog-html.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { STANDARD_DURATIONS } from "./sts";
|
|
2
|
+
|
|
1
3
|
export const FIELDS = ["region", "accessKeyId", "secretAccessKey", "mfaArn", "roleArn", "duration", "mfaMode", "mfaCode", "mfaCommand"] as const;
|
|
2
4
|
|
|
3
5
|
export const FIELD_PATTERNS: Record<string, string> = {
|
|
@@ -58,7 +60,7 @@ export function buildHtml(config: Record<string, string>) {
|
|
|
58
60
|
<div class="field"><label>Secret Access Key</label><input id="secretAccessKey" type="password"></div>
|
|
59
61
|
<div class="field"><label>MFA Device ARN</label><input id="mfaArn" placeholder="arn:aws:iam::123456789012:mfa/user"></div>
|
|
60
62
|
<div class="field"><label>Role ARN</label><input id="roleArn" placeholder="arn:aws:iam::123456789012:role/MyRole"></div>
|
|
61
|
-
<div class="field"><label>Session Duration (seconds)</label><input id="duration" placeholder="
|
|
63
|
+
<div class="field"><label>Session Duration (seconds)</label><input id="duration" placeholder="${STANDARD_DURATIONS[0]}"></div>
|
|
62
64
|
<hr>
|
|
63
65
|
<div class="radio-field">
|
|
64
66
|
<input type="radio" name="mfaMode" id="mfaModeCode" value="code" checked>
|
package/src/dialog.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Config } from "./config";
|
|
|
4
4
|
import { buildHtml } from "./dialog-html";
|
|
5
5
|
import { version } from "../package.json";
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
export interface DialogResult {
|
|
8
9
|
region: string;
|
|
9
10
|
accessKeyId: string;
|
|
@@ -66,7 +67,7 @@ export function showDialog(defaults: Partial<Config>): DialogResult | null {
|
|
|
66
67
|
secretAccessKey: defaults.secretAccessKey ?? "",
|
|
67
68
|
mfaArn: defaults.mfaArn ?? "",
|
|
68
69
|
roleArn: defaults.roleArn ?? "",
|
|
69
|
-
duration: String(defaults.duration
|
|
70
|
+
duration: defaults.duration != null ? String(defaults.duration) : "",
|
|
70
71
|
mfaCommand: defaults.mfaCommand ?? "",
|
|
71
72
|
mfaMode: defaults.mfaMode ?? "code",
|
|
72
73
|
}));
|
package/src/index.ts
CHANGED
|
@@ -23,7 +23,7 @@ if (arg === "setup" || arg === "--setup") {
|
|
|
23
23
|
|
|
24
24
|
import { loadConfig, saveConfig, seedDefaults } from "./config";
|
|
25
25
|
import { showDialog } from "./dialog";
|
|
26
|
-
import { assumeRoleWithMfa } from "./sts";
|
|
26
|
+
import { assumeRoleWithMfa, STANDARD_DURATIONS } from "./sts";
|
|
27
27
|
|
|
28
28
|
const defaults = loadConfig() ?? await seedDefaults();
|
|
29
29
|
|
|
@@ -51,7 +51,7 @@ try {
|
|
|
51
51
|
mfaArn: result.mfaArn,
|
|
52
52
|
roleArn: result.roleArn,
|
|
53
53
|
mfaCode,
|
|
54
|
-
duration: parseInt(result.duration, 10) ||
|
|
54
|
+
duration: parseInt(result.duration, 10) || STANDARD_DURATIONS[0],
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
saveConfig({
|
package/src/setup-html.ts
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import type { ClaudeSettings } from "./claude-settings";
|
|
2
2
|
|
|
3
|
-
export const ENV_CHECKBOXES = [
|
|
4
|
-
{ key: "CLAUDE_CODE_USE_BEDROCK", value: "1", label: "Enable Bedrock" },
|
|
5
|
-
] as const;
|
|
6
|
-
|
|
7
3
|
export const TEXT_FIELDS = [
|
|
8
4
|
{ key: "ANTHROPIC_DEFAULT_OPUS_MODEL", label: "Opus Model", placeholder: "global.anthropic.claude-opus-4-6" },
|
|
9
5
|
{ key: "ANTHROPIC_DEFAULT_SONNET_MODEL", label: "Sonnet Model", placeholder: "global.anthropic.claude-sonnet-4-6" },
|
|
10
6
|
{ key: "ANTHROPIC_DEFAULT_HAIKU_MODEL", label: "Haiku Model", placeholder: "global.anthropic.claude-haiku-4-5" },
|
|
11
7
|
{ key: "CLAUDE_CODE_SUBAGENT_MODEL", label: "Subagent Model", placeholder: "global.anthropic.claude-sonnet-4-6" },
|
|
12
|
-
{ key: "CLAUDE_CODE_MAX_OUTPUT_TOKENS", label: "Max Output Tokens", placeholder: "32000" },
|
|
13
|
-
{ key: "CLAUDE_CODE_MAX_THINKING_TOKENS", label: "Max Thinking Tokens", placeholder: "31999" },
|
|
14
8
|
] as const;
|
|
15
9
|
|
|
16
10
|
export function buildSetupHtml(env: Record<string, string>, settings: ClaudeSettings, credentialExportCmd: string) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
}).join("\n ");
|
|
24
|
-
|
|
25
|
-
const mfaEnabled = settings.awsCredentialExport !== undefined || env.AWS_SHARED_CREDENTIALS_FILE === "/dev/null";
|
|
26
|
-
const mfaCheckboxHtml = `<div class="checkbox-field">
|
|
27
|
-
<input type="checkbox" id="enableMfa" ${mfaEnabled ? "checked" : ""}>
|
|
28
|
-
<label for="enableMfa">Enable claude-aws-mfa</label>
|
|
11
|
+
const bedrockMfaEnabled = env.CLAUDE_CODE_USE_BEDROCK === "1"
|
|
12
|
+
&& settings.awsCredentialExport !== undefined
|
|
13
|
+
&& env.AWS_SHARED_CREDENTIALS_FILE === "/dev/null";
|
|
14
|
+
const bedrockMfaCheckboxHtml = `<div class="checkbox-field">
|
|
15
|
+
<input type="checkbox" id="enableBedrockMfa" ${bedrockMfaEnabled ? "checked" : ""}>
|
|
16
|
+
<label for="enableBedrockMfa">Enable Bedrock via claude-aws-mfa</label>
|
|
29
17
|
</div>`;
|
|
30
18
|
|
|
31
19
|
const textFieldsHtml = TEXT_FIELDS.map(f => {
|
|
@@ -76,8 +64,7 @@ export function buildSetupHtml(env: Record<string, string>, settings: ClaudeSett
|
|
|
76
64
|
<body>
|
|
77
65
|
<h2>Claude Code Bedrock Setup</h2>
|
|
78
66
|
|
|
79
|
-
${
|
|
80
|
-
${mfaCheckboxHtml}
|
|
67
|
+
${bedrockMfaCheckboxHtml}
|
|
81
68
|
|
|
82
69
|
<hr>
|
|
83
70
|
|
|
@@ -89,7 +76,6 @@ export function buildSetupHtml(env: Record<string, string>, settings: ClaudeSett
|
|
|
89
76
|
</div>
|
|
90
77
|
|
|
91
78
|
<script>
|
|
92
|
-
const ENV_CHECKBOXES = ${JSON.stringify(ENV_CHECKBOXES)};
|
|
93
79
|
const TEXT_FIELDS = ${JSON.stringify(TEXT_FIELDS)};
|
|
94
80
|
const originalEnv = ${JSON.stringify(env)};
|
|
95
81
|
const originalSettings = ${JSON.stringify(settings)};
|
|
@@ -99,26 +85,21 @@ export function buildSetupHtml(env: Record<string, string>, settings: ClaudeSett
|
|
|
99
85
|
const env = {};
|
|
100
86
|
const top = {};
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
env["AWS_SHARED_CREDENTIALS_FILE"] = "/dev/null";
|
|
117
|
-
} else {
|
|
118
|
-
if (originalSettings["awsCredentialExport"] !== undefined) {
|
|
88
|
+
// Combined checkbox controls CLAUDE_CODE_USE_BEDROCK,
|
|
89
|
+
// awsCredentialExport (top-level), and AWS_SHARED_CREDENTIALS_FILE (env).
|
|
90
|
+
// Only save changes when the checkbox state differs from load time.
|
|
91
|
+
const wasEnabled = originalEnv["CLAUDE_CODE_USE_BEDROCK"] === "1"
|
|
92
|
+
&& originalSettings["awsCredentialExport"] !== undefined
|
|
93
|
+
&& originalEnv["AWS_SHARED_CREDENTIALS_FILE"] === "/dev/null";
|
|
94
|
+
const enabled = document.getElementById("enableBedrockMfa").checked;
|
|
95
|
+
if (enabled !== wasEnabled) {
|
|
96
|
+
if (enabled) {
|
|
97
|
+
env["CLAUDE_CODE_USE_BEDROCK"] = "1";
|
|
98
|
+
top["awsCredentialExport"] = credentialExportCmd;
|
|
99
|
+
env["AWS_SHARED_CREDENTIALS_FILE"] = "/dev/null";
|
|
100
|
+
} else {
|
|
101
|
+
env["CLAUDE_CODE_USE_BEDROCK"] = null;
|
|
119
102
|
top["awsCredentialExport"] = null;
|
|
120
|
-
}
|
|
121
|
-
if (originalEnv["AWS_SHARED_CREDENTIALS_FILE"] === "/dev/null") {
|
|
122
103
|
env["AWS_SHARED_CREDENTIALS_FILE"] = null;
|
|
123
104
|
}
|
|
124
105
|
}
|
package/src/setup.ts
CHANGED
package/src/sts.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts";
|
|
2
2
|
|
|
3
|
-
const STANDARD_DURATIONS = [43200, 21600, 7200, 3600];
|
|
3
|
+
export const STANDARD_DURATIONS = [129600, 86400, 43200, 28800, 21600, 14400, 7200, 3600];
|
|
4
4
|
|
|
5
5
|
export function isDurationError(err: unknown): boolean {
|
|
6
6
|
const msg = String(err).toLowerCase();
|
package/test/sts.test.ts
CHANGED
|
@@ -47,7 +47,7 @@ beforeEach(() => {
|
|
|
47
47
|
|
|
48
48
|
describe("durationLadder", () => {
|
|
49
49
|
test("standard duration returns it and all smaller standard values", () => {
|
|
50
|
-
expect(durationLadder(43200)).toEqual([43200, 21600, 7200, 3600]);
|
|
50
|
+
expect(durationLadder(43200)).toEqual([43200, 28800, 21600, 14400, 7200, 3600]);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
test("mid-range standard duration", () => {
|
|
@@ -59,7 +59,7 @@ describe("durationLadder", () => {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
test("non-standard duration is tried first, then smaller standard values", () => {
|
|
62
|
-
expect(durationLadder(50000)).toEqual([50000, 43200, 21600, 7200, 3600]);
|
|
62
|
+
expect(durationLadder(50000)).toEqual([50000, 43200, 28800, 21600, 14400, 7200, 3600]);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
test("non-standard mid-range duration", () => {
|
|
@@ -125,24 +125,24 @@ describe("assumeRoleWithMfa", () => {
|
|
|
125
125
|
|
|
126
126
|
const result = await assumeRoleWithMfa(BASE_PARAMS);
|
|
127
127
|
|
|
128
|
-
expect(result.duration).toBe(
|
|
128
|
+
expect(result.duration).toBe(28800);
|
|
129
129
|
expect(capturedCommands).toHaveLength(2);
|
|
130
130
|
expect(capturedCommands[0].DurationSeconds).toBe(43200);
|
|
131
|
-
expect(capturedCommands[1].DurationSeconds).toBe(
|
|
131
|
+
expect(capturedCommands[1].DurationSeconds).toBe(28800);
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
test("tries all durations in ladder on repeated duration errors", async () => {
|
|
135
135
|
let callCount = 0;
|
|
136
136
|
mockSend = mock(() => {
|
|
137
137
|
callCount++;
|
|
138
|
-
if (callCount <
|
|
138
|
+
if (callCount < 6) return Promise.reject(new Error("DurationSeconds exceeds the maximum"));
|
|
139
139
|
return Promise.resolve(MOCK_CREDENTIALS);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
const result = await assumeRoleWithMfa(BASE_PARAMS);
|
|
143
143
|
|
|
144
144
|
expect(result.duration).toBe(3600);
|
|
145
|
-
expect(capturedCommands).toHaveLength(
|
|
145
|
+
expect(capturedCommands).toHaveLength(6);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
148
|
test("throws non-duration errors immediately without retry", async () => {
|