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 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 credential-handling, 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. You can also set the "max output tokens" and "max thinking tokens" values; in a corporate environment these often have recommended values to help manage costs.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-aws-mfa",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "repository": "https://github.com/p120ph37/claude-aws-mfa",
6
6
  "bin": {
@@ -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="43200"></div>
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 ?? 43200),
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) || 43200,
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 envCheckboxesHtml = ENV_CHECKBOXES.map(cb => {
18
- const checked = env[cb.key] === cb.value ? "checked" : "";
19
- return `<div class="checkbox-field">
20
- <input type="checkbox" id="${cb.key}" ${checked}>
21
- <label for="${cb.key}">${cb.label}</label>
22
- </div>`;
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
- ${envCheckboxesHtml}
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
- for (const cb of ENV_CHECKBOXES) {
103
- const el = document.getElementById(cb.key);
104
- if (el.checked) {
105
- env[cb.key] = cb.value;
106
- } else if (originalEnv[cb.key] === cb.value) {
107
- env[cb.key] = null;
108
- }
109
- }
110
-
111
- // Combined "Enable claude-aws-mfa" checkbox controls both
112
- // awsCredentialExport (top-level) and AWS_SHARED_CREDENTIALS_FILE (env)
113
- const mfaChecked = document.getElementById("enableMfa").checked;
114
- if (mfaChecked) {
115
- top["awsCredentialExport"] = credentialExportCmd;
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
@@ -23,7 +23,7 @@ export function runSetup(): boolean {
23
23
 
24
24
  const webview = new Webview(false, {
25
25
  width: 480,
26
- height: 640,
26
+ height: 480,
27
27
  hint: SizeHint.FIXED,
28
28
  });
29
29
  webview.title = `Claude Code Bedrock Setup v${version}`;
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(21600);
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(21600);
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 < 4) return Promise.reject(new Error("DurationSeconds exceeds the maximum"));
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(4);
145
+ expect(capturedCommands).toHaveLength(6);
146
146
  });
147
147
 
148
148
  test("throws non-duration errors immediately without retry", async () => {