claude-aws-mfa 1.2.2 → 1.4.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 +14 -0
- package/README.md +33 -1
- package/bun.lock +31 -131
- package/package.json +5 -3
- package/src/config.ts +1 -0
- package/src/dialog-html.ts +5 -2
- package/src/dialog.ts +3 -0
- package/src/flags.ts +48 -0
- package/src/index.ts +40 -9
- package/src/sts.ts +4 -1
- package/src/usage.ts +226 -0
- package/test/dialog-html.test.ts +3 -1
- package/test/index.test.ts +6 -0
- package/test/sts.test.ts +6 -0
- package/test/usage.test.ts +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,20 @@ 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.4.0] - 2026-10-19
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `--help` / `-h` prints a summary of the CLI options.
|
|
29
|
+
- `--usage[=[<role>/]<session>]` prints Bedrock token usage from model invocation logs for a session name (default: the one in the config), optionally under one role, or itemized per role or session with `*` (`--usage='my-role/*'`, `--usage='*'`), with `--from=`/`--to=` for the time range (default: current month) and `--profile=` to query with a different AWS profile. Each model's estimated cost is shown, at current AWS list prices.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Updated `@aws-sdk/client-sts` to 3.1152 (shared with the new Bedrock and CloudWatch Logs clients) and `@types/bun` to 1.4.3.
|
|
33
|
+
|
|
34
|
+
## [v1.3.0] - 2026-10-14
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Configurable STS session name in the credentials dialog (defaults to `claude-aws-mfa`).
|
|
38
|
+
|
|
25
39
|
## [v1.2.2] - 2026-10-09
|
|
26
40
|
|
|
27
41
|
### Changed
|
package/README.md
CHANGED
|
@@ -39,15 +39,19 @@ Add this (plus any addtitional config you prefer) to your Claude Code settings (
|
|
|
39
39
|
| Flag | Description |
|
|
40
40
|
|------|-------------|
|
|
41
41
|
| `--setup` | Launch the guided setup wizard |
|
|
42
|
+
| `-h`, `--help` | Show usage help |
|
|
42
43
|
| `--cache-session` | Cache STS session credentials and reuse them until they expire (default: on) |
|
|
43
44
|
| `--auto-mfa` | Automatically run the configured MFA command and obtain credentials without showing the dialog, requires MFA Command mode (default: on) |
|
|
44
45
|
| `--single-instance-lock` | Prevent multiple concurrent instances from showing overlapping dialogs; subsequent invocations wait for the first to finish and reuse its cached session (default: on) |
|
|
46
|
+
| `--usage[=[<role>/]<session>]` | Print Bedrock token usage for a session name (default: the one in the config), optionally under one role; `*` itemizes instead of credentials (see [Usage report](#usage-report)) |
|
|
47
|
+
| `--profile=<name>` | With `--usage`: query using this AWS profile instead of the tool's own credentials |
|
|
48
|
+
| `--from=<date>` / `--to=<date>` | With `--usage`: time range (default: start of the current month to now; `--to` is exclusive) |
|
|
45
49
|
| `--no-ui` | Never show any GUI dialog. Prints credentials JSON to stdout on success, or an error to stderr on failure — useful for scripting or letting an LLM drive the tool during setup/debugging |
|
|
46
50
|
|
|
47
51
|
## How it works
|
|
48
52
|
|
|
49
53
|
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
|
|
54
|
+
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
55
|
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
56
|
4. Calls `sts:AssumeRole` with the MFA token
|
|
53
57
|
5. If the requested session duration is rejected, automatically retries with shorter durations (12h → 6h → 2h → 1h)
|
|
@@ -91,6 +95,34 @@ Claude Code does not surface a failing command's stderr to the user, so when MFA
|
|
|
91
95
|
|
|
92
96
|
With `--no-ui`, no GUI is ever shown — not even the error dialog above — and the tool never imports the GUI toolkit, so it also works in headless environments without a display. On success, credentials are printed to stdout as usual, keeping stdout cleanly parsable. On failure, an error message is printed to **stderr** instead (e.g. `Missing config: roleArn, mfaCommand` if the config is incomplete, or the underlying STS error if authentication itself failed) and the process exits non-zero. This is intended for scripting and for letting an LLM drive the tool directly while setting up or debugging a configuration — full non-interactive operation requires **MFA Command mode**, since a static "code" mode value can't be re-entered headlessly. The command doesn't need to be a real TOTP generator: for a one-off test or debugging run, `mfaCommand` can simply be `echo <code>` with a fresh, not-yet-used code from your authenticator — it only needs to produce a valid code once.
|
|
93
97
|
|
|
98
|
+
### Usage report
|
|
99
|
+
|
|
100
|
+
`claude-aws-mfa --usage` prints per-model request and token counts (input, cache read/write, output), with an estimated cost, for every Bedrock call made under the configured STS session name.
|
|
101
|
+
|
|
102
|
+
`--usage=` takes `[<role>/]<session>` to choose whose usage to report. Either part can be `*`, which gives one row per role or session (all models merged) instead of one row per model:
|
|
103
|
+
|
|
104
|
+
| Value | Reports |
|
|
105
|
+
|-------|---------|
|
|
106
|
+
| *(none)* | The session name from the config, any role, per model |
|
|
107
|
+
| `claude-aws-mfa` | That session name under any role, per model |
|
|
108
|
+
| `my-role/claude-aws-mfa` | That session of role `my-role`, per model |
|
|
109
|
+
| `my-role/*` | Each session of role `my-role` |
|
|
110
|
+
| `*` | Each role (its sessions merged) |
|
|
111
|
+
| `*/*` | Each role and session pair |
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
claude-aws-mfa --usage # this month, using the tool's own role session
|
|
115
|
+
claude-aws-mfa --usage --from=2026-09-01 --to=2026-10-01 # September
|
|
116
|
+
claude-aws-mfa --usage --profile=billing # query with a different AWS profile
|
|
117
|
+
claude-aws-mfa --usage='my-role/*' # quote * so the shell doesn't expand it
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Only callers that assumed a role are counted; calls made directly with IAM user keys never match.
|
|
121
|
+
|
|
122
|
+
The data comes from Bedrock [model invocation logs](https://docs.aws.amazon.com/bedrock/latest/userguide/model-invocation-logging.html), so invocation logging to CloudWatch Logs must be enabled in the account and region. The querying credentials need `bedrock:GetModelInvocationLoggingConfiguration`, `logs:StartQuery` and `logs:GetQueryResults`. Without `--profile`, credentials are obtained exactly as for Claude Code (cached session, auto-MFA, or the dialog). Errors go to stderr.
|
|
123
|
+
|
|
124
|
+
Costs are estimated from AWS's public [Price List](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/price-changes.html) for the region (no extra permissions needed), at current base on-demand rates — `us.`/`eu.` cross-region profiles at their geo price, `global.` at the global price. They exclude negotiated discounts and the long-context and 1-hour-cache-write surcharges, which the aggregated logs can't distinguish, so treat them as an estimate rather than your bill. If prices can't be fetched, token counts are still printed.
|
|
125
|
+
|
|
94
126
|
## System requirements
|
|
95
127
|
|
|
96
128
|
The GUI dialog uses [webview-bun](https://github.com/tr1ckydev/webview-bun), which requires:
|
package/bun.lock
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "claude-bedrock-mfa",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@aws-sdk/client-
|
|
8
|
+
"@aws-sdk/client-bedrock": "^3.1152.0",
|
|
9
|
+
"@aws-sdk/client-cloudwatch-logs": "^3.1152.0",
|
|
10
|
+
"@aws-sdk/client-sts": "^3.1152.0",
|
|
9
11
|
"webview-bun": "^2.4.0",
|
|
10
12
|
},
|
|
11
13
|
"devDependencies": {
|
|
12
|
-
"@types/bun": "
|
|
14
|
+
"@types/bun": "^1.4.3",
|
|
13
15
|
"node-screenshots": "^0.2.8",
|
|
14
16
|
},
|
|
15
17
|
"peerDependencies": {
|
|
@@ -18,151 +20,61 @@
|
|
|
18
20
|
},
|
|
19
21
|
},
|
|
20
22
|
"packages": {
|
|
21
|
-
"@aws-
|
|
23
|
+
"@aws-sdk/client-bedrock": ["@aws-sdk/client-bedrock@3.1152.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/token-providers": "3.1152.0", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-mZQk6H/Gt34V5LWDJfSf4Ar/FMaUrGwu9TOvstKP7GNo81PJJOkgS+ICjhoV3YX/AZXjZD8mNCp+MYynRitRag=="],
|
|
22
24
|
|
|
23
|
-
"@aws-
|
|
25
|
+
"@aws-sdk/client-cloudwatch-logs": ["@aws-sdk/client-cloudwatch-logs@3.1152.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-626LS/9YffzAUH68bvnyNwXo/WYtShhs7+X6kEH5ARTom1sSk3hHJmR9F+1zc50mHSfJNwYEbUNDmgC5vW4/Xg=="],
|
|
24
26
|
|
|
25
|
-
"@aws-
|
|
27
|
+
"@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1152.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-node": "^3.972.86", "@aws-sdk/signature-v4-multi-region": "^3.996.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-dLvk02UzZPRTeLLh8C86OpJBRzsJZ+fSxGqqdzoWOXACwZOf3xH6d4rQn54eZNPKYWaQNlt4VmfA5mZsfItbgw=="],
|
|
26
28
|
|
|
27
|
-
"@aws-
|
|
29
|
+
"@aws-sdk/core": ["@aws-sdk/core@3.978.3", "", { "dependencies": { "@aws-sdk/types": "^3.974.6", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.35.2", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-nslpzUCTI9b2IQMWSd6mSAXldBexPc8rr0d3dXUutXYFLFaTdsHa2GD7sJdsn3PhyjK0O5K8ypd6zi+RXXPVlg=="],
|
|
28
30
|
|
|
29
|
-
"@aws-sdk/
|
|
31
|
+
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-rsRJcW2iFUhCgclyD6OSbMEaPl8Eu0qSeRZb2uuCK1ViZ66hl5wOe/p6Vr6MO3b+ydyKgFeqza2p6tZThvILlw=="],
|
|
30
32
|
|
|
31
|
-
"@aws-sdk/
|
|
33
|
+
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-pnKdh0Br7OJPj+7qKjzgTfDTFW0RhEMKXNDR2jv9CAM4oHuG/4z3hRh8KtutLfpmItuRtDbKBmwWkDqHwwAWXw=="],
|
|
32
34
|
|
|
33
|
-
"@aws-sdk/credential-provider-
|
|
35
|
+
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.19", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/credential-provider-env": "^3.972.74", "@aws-sdk/credential-provider-http": "^3.972.76", "@aws-sdk/credential-provider-login": "^3.972.81", "@aws-sdk/credential-provider-process": "^3.972.74", "@aws-sdk/credential-provider-sso": "^3.973.18", "@aws-sdk/credential-provider-web-identity": "^3.972.80", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-kcjvAxqQEV8yuwkX58RelNn3ne65o7JgRWf3Z/gVvbi7Xuw1qSDLJlatrLD76+40cmRxmNkLxBoyp/vAQChQ5g=="],
|
|
34
36
|
|
|
35
|
-
"@aws-sdk/credential-provider-
|
|
37
|
+
"@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.81", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-CeQHSbE2CnVdaHIJ/NayRaCecIER+Pm9faUPkmRATO54rCoMTEPSbVieGHzEwvm1dZM2HIs6qQZCznhayNTWJQ=="],
|
|
36
38
|
|
|
37
|
-
"@aws-sdk/credential-provider-
|
|
39
|
+
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.86", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.74", "@aws-sdk/credential-provider-http": "^3.972.76", "@aws-sdk/credential-provider-ini": "^3.973.19", "@aws-sdk/credential-provider-process": "^3.972.74", "@aws-sdk/credential-provider-sso": "^3.973.18", "@aws-sdk/credential-provider-web-identity": "^3.972.80", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WNttSyr6e8innPtBSty//CdnqJcgZN6eAbtw3v/CR2IObz6MYZEo8eusdtuCHJ2ujwFqC2bSPHVyhXGHx8HruQ=="],
|
|
38
40
|
|
|
39
|
-
"@aws-sdk/credential-provider-
|
|
41
|
+
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.74", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-Yj6kZFJQdbR+PygTkqdVLPHHpdDpOS9GCYVcDCuweCRRig9LF24ELQBMfyUw1vspvrZJUfnOGVyMjhaL094ahw=="],
|
|
40
42
|
|
|
41
|
-
"@aws-sdk/credential-provider-
|
|
43
|
+
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.18", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/token-providers": "3.1150.0", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-FnpPhdY4qUGtX/8X6eKxh+uD+9Y19i9FYibgk4SBe0M935s9vocHkHzaxaMNk+YkHmvzgdu8cHA5SCoeU+kkew=="],
|
|
42
44
|
|
|
43
|
-
"@aws-sdk/credential-provider-
|
|
45
|
+
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.80", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-TJF25Ndwy2HxcVj6oxMgp8qVlJAawlExHYBXqhuC6Zt2m+A7WpdkjBD6QglNbiArlteYljZHEY3R7KKmno6ISA=="],
|
|
44
46
|
|
|
45
|
-
"@aws-sdk/
|
|
47
|
+
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.48", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/signature-v4-multi-region": "^3.996.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-B01jdXJtAW/XOiUzGdI2mz67WyEHbGEc9X6Euzw3bjLAa+txTqdZDER4X2FsIEcXzV2ABqudVZ5thali8tCCzQ=="],
|
|
46
48
|
|
|
47
|
-
"@aws-sdk/
|
|
49
|
+
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.48", "", { "dependencies": { "@aws-sdk/types": "^3.974.6", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-cFDXfEbXaGwbnpvR/DyqfxfjxpjXVRVthQLgkwvcfDS5u3+8G9WSj1X1Ag/pTRg/QOeOno1Tq3ycEdrKsCUcOA=="],
|
|
48
50
|
|
|
49
|
-
"@aws-sdk/
|
|
51
|
+
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1152.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-XcQlQFkIDOk4P1dF/ADQAjPK361mBChR/AJghM4my6SgDnsOX77Z7zCUtZfXXbQG4E+vpoWfbySzX/oKXcmV5w=="],
|
|
50
52
|
|
|
51
|
-
"@aws-sdk/
|
|
53
|
+
"@aws-sdk/types": ["@aws-sdk/types@3.974.6", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg=="],
|
|
52
54
|
|
|
53
|
-
"@aws-sdk/
|
|
55
|
+
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="],
|
|
54
56
|
|
|
55
|
-
"@aws
|
|
57
|
+
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
|
|
56
58
|
|
|
57
|
-
"@
|
|
59
|
+
"@smithy/core": ["@smithy/core@3.36.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g=="],
|
|
58
60
|
|
|
59
|
-
"@
|
|
61
|
+
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="],
|
|
60
62
|
|
|
61
|
-
"@
|
|
63
|
+
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="],
|
|
62
64
|
|
|
63
|
-
"@
|
|
65
|
+
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="],
|
|
64
66
|
|
|
65
|
-
"@
|
|
67
|
+
"@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="],
|
|
66
68
|
|
|
67
|
-
"@
|
|
69
|
+
"@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="],
|
|
68
70
|
|
|
69
|
-
"@
|
|
70
|
-
|
|
71
|
-
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.13", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.14", "@aws-sdk/types": "^3.973.3", "@smithy/node-config-provider": "^4.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-PHErmuu+v6iAST48zcsB2cYwDKW45gk6qCp49t1p0NGZ4EaFPr/tA5jl0X/ekDwvWbuT0LTj++fjjdVQAbuh0Q=="],
|
|
72
|
-
|
|
73
|
-
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.7", "", { "dependencies": { "@smithy/types": "^4.13.0", "fast-xml-parser": "5.3.6", "tslib": "^2.6.2" } }, "sha512-9GF86s6mHuc1TYCbuKatMDWl2PyK3KIkpRaI7ul2/gYZPfaLzKZ+ISHhxzVb9KVeakf75tUQe6CXW2gugSCXNw=="],
|
|
74
|
-
|
|
75
|
-
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="],
|
|
76
|
-
|
|
77
|
-
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g=="],
|
|
78
|
-
|
|
79
|
-
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.9", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.10", "@smithy/types": "^4.13.0", "@smithy/util-config-provider": "^4.2.1", "@smithy/util-endpoints": "^3.3.1", "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" } }, "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q=="],
|
|
80
|
-
|
|
81
|
-
"@smithy/core": ["@smithy/core@3.23.6", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.11", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.1", "@smithy/util-body-length-browser": "^4.2.1", "@smithy/util-middleware": "^4.2.10", "@smithy/util-stream": "^4.5.15", "@smithy/util-utf8": "^4.2.1", "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" } }, "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg=="],
|
|
82
|
-
|
|
83
|
-
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.10", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.10", "@smithy/property-provider": "^4.2.10", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.10", "tslib": "^2.6.2" } }, "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ=="],
|
|
84
|
-
|
|
85
|
-
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.10", "@smithy/querystring-builder": "^4.2.10", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g=="],
|
|
86
|
-
|
|
87
|
-
"@smithy/hash-node": ["@smithy/hash-node@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-buffer-from": "^4.2.1", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w=="],
|
|
88
|
-
|
|
89
|
-
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org=="],
|
|
90
|
-
|
|
91
|
-
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q=="],
|
|
92
|
-
|
|
93
|
-
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.10", "", { "dependencies": { "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w=="],
|
|
94
|
-
|
|
95
|
-
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.20", "", { "dependencies": { "@smithy/core": "^3.23.6", "@smithy/middleware-serde": "^4.2.11", "@smithy/node-config-provider": "^4.3.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "@smithy/url-parser": "^4.2.10", "@smithy/util-middleware": "^4.2.10", "tslib": "^2.6.2" } }, "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw=="],
|
|
96
|
-
|
|
97
|
-
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.37", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.10", "@smithy/protocol-http": "^5.3.10", "@smithy/service-error-classification": "^4.2.10", "@smithy/smithy-client": "^4.12.0", "@smithy/types": "^4.13.0", "@smithy/util-middleware": "^4.2.10", "@smithy/util-retry": "^4.2.10", "@smithy/uuid": "^1.1.1", "tslib": "^2.6.2" } }, "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA=="],
|
|
98
|
-
|
|
99
|
-
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.11", "", { "dependencies": { "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg=="],
|
|
100
|
-
|
|
101
|
-
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA=="],
|
|
102
|
-
|
|
103
|
-
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.10", "", { "dependencies": { "@smithy/property-provider": "^4.2.10", "@smithy/shared-ini-file-loader": "^4.4.5", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w=="],
|
|
104
|
-
|
|
105
|
-
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.12", "", { "dependencies": { "@smithy/abort-controller": "^4.2.10", "@smithy/protocol-http": "^5.3.10", "@smithy/querystring-builder": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w=="],
|
|
106
|
-
|
|
107
|
-
"@smithy/property-provider": ["@smithy/property-provider@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw=="],
|
|
108
|
-
|
|
109
|
-
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A=="],
|
|
110
|
-
|
|
111
|
-
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "@smithy/util-uri-escape": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA=="],
|
|
112
|
-
|
|
113
|
-
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q=="],
|
|
114
|
-
|
|
115
|
-
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0" } }, "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg=="],
|
|
116
|
-
|
|
117
|
-
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.5", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug=="],
|
|
118
|
-
|
|
119
|
-
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.10", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.1", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "@smithy/util-hex-encoding": "^4.2.1", "@smithy/util-middleware": "^4.2.10", "@smithy/util-uri-escape": "^4.2.1", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA=="],
|
|
120
|
-
|
|
121
|
-
"@smithy/smithy-client": ["@smithy/smithy-client@4.12.0", "", { "dependencies": { "@smithy/core": "^3.23.6", "@smithy/middleware-endpoint": "^4.4.20", "@smithy/middleware-stack": "^4.2.10", "@smithy/protocol-http": "^5.3.10", "@smithy/types": "^4.13.0", "@smithy/util-stream": "^4.5.15", "tslib": "^2.6.2" } }, "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ=="],
|
|
122
|
-
|
|
123
|
-
"@smithy/types": ["@smithy/types@4.13.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw=="],
|
|
124
|
-
|
|
125
|
-
"@smithy/url-parser": ["@smithy/url-parser@4.2.10", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A=="],
|
|
126
|
-
|
|
127
|
-
"@smithy/util-base64": ["@smithy/util-base64@4.3.1", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.1", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w=="],
|
|
128
|
-
|
|
129
|
-
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g=="],
|
|
130
|
-
|
|
131
|
-
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw=="],
|
|
132
|
-
|
|
133
|
-
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.1", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig=="],
|
|
134
|
-
|
|
135
|
-
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A=="],
|
|
136
|
-
|
|
137
|
-
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.36", "", { "dependencies": { "@smithy/property-provider": "^4.2.10", "@smithy/smithy-client": "^4.12.0", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew=="],
|
|
138
|
-
|
|
139
|
-
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.39", "", { "dependencies": { "@smithy/config-resolver": "^4.4.9", "@smithy/credential-provider-imds": "^4.2.10", "@smithy/node-config-provider": "^4.3.10", "@smithy/property-provider": "^4.2.10", "@smithy/smithy-client": "^4.12.0", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg=="],
|
|
140
|
-
|
|
141
|
-
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.1", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg=="],
|
|
142
|
-
|
|
143
|
-
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA=="],
|
|
144
|
-
|
|
145
|
-
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.10", "", { "dependencies": { "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA=="],
|
|
146
|
-
|
|
147
|
-
"@smithy/util-retry": ["@smithy/util-retry@4.2.10", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.10", "@smithy/types": "^4.13.0", "tslib": "^2.6.2" } }, "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A=="],
|
|
148
|
-
|
|
149
|
-
"@smithy/util-stream": ["@smithy/util-stream@4.5.15", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.11", "@smithy/node-http-handler": "^4.4.12", "@smithy/types": "^4.13.0", "@smithy/util-base64": "^4.3.1", "@smithy/util-buffer-from": "^4.2.1", "@smithy/util-hex-encoding": "^4.2.1", "@smithy/util-utf8": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw=="],
|
|
150
|
-
|
|
151
|
-
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q=="],
|
|
152
|
-
|
|
153
|
-
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.1", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g=="],
|
|
154
|
-
|
|
155
|
-
"@smithy/uuid": ["@smithy/uuid@1.1.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw=="],
|
|
156
|
-
|
|
157
|
-
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
|
71
|
+
"@types/bun": ["@types/bun@1.4.3", "", { "dependencies": { "bun-types": "1.4.3" } }, "sha512-N67Pqy8JXbAY/j0tNraS3V5mubGvc6rrrNDmDCtZLlFzccbUJPDb0tvgUBC5q4m8ybkP93Jcn1QB+RYQ6PvBhw=="],
|
|
158
72
|
|
|
159
73
|
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
|
160
74
|
|
|
161
75
|
"bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
|
|
162
76
|
|
|
163
|
-
"bun-types": ["bun-types@1.3
|
|
164
|
-
|
|
165
|
-
"fast-xml-parser": ["fast-xml-parser@5.3.6", "", { "dependencies": { "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA=="],
|
|
77
|
+
"bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="],
|
|
166
78
|
|
|
167
79
|
"node-screenshots": ["node-screenshots@0.2.8", "", { "optionalDependencies": { "node-screenshots-darwin-arm64": "0.2.8", "node-screenshots-darwin-x64": "0.2.8", "node-screenshots-linux-arm64-gnu": "0.2.8", "node-screenshots-linux-loong64-gnu": "0.2.8", "node-screenshots-linux-x64-gnu": "0.2.8", "node-screenshots-linux-x64-musl": "0.2.8", "node-screenshots-win32-arm64-msvc": "0.2.8", "node-screenshots-win32-ia32-msvc": "0.2.8", "node-screenshots-win32-x64-msvc": "0.2.8" } }, "sha512-7O2ab89hkUgiXkrYj9iYCbJjY1iFJqIyZt0vuVtHFKlnajeFoyW+hur4CPxwwrnCUK0ahr0WLWQ5A6rCzF8rWQ=="],
|
|
168
80
|
|
|
@@ -184,8 +96,6 @@
|
|
|
184
96
|
|
|
185
97
|
"node-screenshots-win32-x64-msvc": ["node-screenshots-win32-x64-msvc@0.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-DdFORB74FghM1L6UtE1TltIR2H0CCCmfQEa8gp2CTSdtBr2b0TKL7ZlbEV6x/wVKRFT59w7Kd6BULdeMmp0X2w=="],
|
|
186
98
|
|
|
187
|
-
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
|
188
|
-
|
|
189
99
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
190
100
|
|
|
191
101
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
@@ -194,16 +104,6 @@
|
|
|
194
104
|
|
|
195
105
|
"webview-bun": ["webview-bun@2.4.0", "", {}, "sha512-0+ugnQlcUHmuW+iLeb+Lzb8rGUJh7WEdXvNsuvaVEXT3EagK380XdD7heVJu0Ek/mNxMY3G2JM142YRQ1hDUGQ=="],
|
|
196
106
|
|
|
197
|
-
"@aws-
|
|
198
|
-
|
|
199
|
-
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
|
200
|
-
|
|
201
|
-
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
|
202
|
-
|
|
203
|
-
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
|
204
|
-
|
|
205
|
-
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
|
206
|
-
|
|
207
|
-
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
|
107
|
+
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1150.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.3", "@aws-sdk/nested-clients": "^3.997.48", "@aws-sdk/types": "^3.974.6", "@smithy/core": "^3.35.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-wDU36ofhabZYu+d2KSriyDbwzxNoaOp+4dG0p3qkabk7HeALR5nIWyQbjoyJZLn3iqWkv/xNdyUfbfevDVDzzg=="],
|
|
208
108
|
}
|
|
209
109
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-aws-mfa",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": "https://github.com/aaronmeriwether/claude-aws-mfa",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-aws-mfa": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@types/bun": "
|
|
10
|
+
"@types/bun": "^1.4.3",
|
|
11
11
|
"node-screenshots": "^0.2.8"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
14
|
"typescript": "^5"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@aws-sdk/client-
|
|
17
|
+
"@aws-sdk/client-bedrock": "^3.1152.0",
|
|
18
|
+
"@aws-sdk/client-cloudwatch-logs": "^3.1152.0",
|
|
19
|
+
"@aws-sdk/client-sts": "^3.1152.0",
|
|
18
20
|
"webview-bun": "^2.4.0"
|
|
19
21
|
}
|
|
20
22
|
}
|
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/flags.ts
CHANGED
|
@@ -1,21 +1,66 @@
|
|
|
1
|
+
export const HELP = `Usage: claude-aws-mfa [options]
|
|
2
|
+
|
|
3
|
+
With no options, prints temporary AWS credentials as JSON for Claude Code's
|
|
4
|
+
awsCredentialExport, prompting for MFA when needed.
|
|
5
|
+
|
|
6
|
+
Options:
|
|
7
|
+
--setup Launch the guided setup wizard
|
|
8
|
+
--[no-]cache-session Reuse cached STS credentials until they expire (default: on)
|
|
9
|
+
--[no-]auto-mfa Run the configured MFA command without showing the dialog (default: on)
|
|
10
|
+
--[no-]single-instance-lock Let only one instance show a dialog at a time (default: on)
|
|
11
|
+
--no-ui Never show a GUI; errors go to stderr
|
|
12
|
+
--usage[=[<role>/]<session>]
|
|
13
|
+
Print Bedrock token usage and estimated cost instead of
|
|
14
|
+
credentials, for a session name (default: the one in the
|
|
15
|
+
config), optionally under one role. Use * to itemize:
|
|
16
|
+
--usage=my-role/* one row per session of role my-role
|
|
17
|
+
--usage=* one row per role
|
|
18
|
+
--profile=<name> With --usage: query using this AWS profile instead of the
|
|
19
|
+
tool's own credentials
|
|
20
|
+
--from=<date> With --usage: start of range (default: start of this month)
|
|
21
|
+
--to=<date> With --usage: end of range, exclusive (default: now)
|
|
22
|
+
-h, --help Show this help`;
|
|
23
|
+
|
|
1
24
|
export interface CliFlags {
|
|
25
|
+
help: boolean;
|
|
2
26
|
setup: boolean;
|
|
3
27
|
cacheSession: boolean | undefined; // undefined = use config default
|
|
4
28
|
autoMfa: boolean | undefined;
|
|
5
29
|
singleInstanceLock: boolean | undefined;
|
|
6
30
|
noUi: boolean;
|
|
31
|
+
usage: boolean;
|
|
32
|
+
usageSpec?: string; // --usage=[<role>/]<session>; undefined = session name from config
|
|
33
|
+
profile?: string;
|
|
34
|
+
from?: string;
|
|
35
|
+
to?: string;
|
|
7
36
|
}
|
|
8
37
|
|
|
9
38
|
export function parseFlags(argv: string[]): CliFlags {
|
|
10
39
|
const flags: CliFlags = {
|
|
40
|
+
help: false,
|
|
11
41
|
setup: false,
|
|
12
42
|
cacheSession: undefined,
|
|
13
43
|
autoMfa: undefined,
|
|
14
44
|
singleInstanceLock: undefined,
|
|
15
45
|
noUi: false,
|
|
46
|
+
usage: false,
|
|
16
47
|
};
|
|
17
48
|
for (const arg of argv.slice(2)) {
|
|
49
|
+
const [key, value] = arg.split(/=(.*)/s);
|
|
50
|
+
if (value !== undefined && (key === "--profile" || key === "--from" || key === "--to")) {
|
|
51
|
+
flags[key.slice(2) as "profile" | "from" | "to"] = value;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (key === "--usage" && value !== undefined) {
|
|
55
|
+
flags.usage = true;
|
|
56
|
+
flags.usageSpec = value || undefined;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
18
59
|
switch (arg) {
|
|
60
|
+
case "-h":
|
|
61
|
+
case "--help":
|
|
62
|
+
flags.help = true;
|
|
63
|
+
break;
|
|
19
64
|
case "setup":
|
|
20
65
|
case "--setup":
|
|
21
66
|
flags.setup = true;
|
|
@@ -41,6 +86,9 @@ export function parseFlags(argv: string[]): CliFlags {
|
|
|
41
86
|
case "--no-ui":
|
|
42
87
|
flags.noUi = true;
|
|
43
88
|
break;
|
|
89
|
+
case "--usage":
|
|
90
|
+
flags.usage = true;
|
|
91
|
+
break;
|
|
44
92
|
}
|
|
45
93
|
}
|
|
46
94
|
return flags;
|
package/src/index.ts
CHANGED
|
@@ -15,10 +15,15 @@ if (process.platform === "linux" && !process.env.WEBKIT_DISABLE_SANDBOX_THIS_IS_
|
|
|
15
15
|
process.exit(proc.exitCode);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
import { parseFlags } from "./flags";
|
|
18
|
+
import { parseFlags, HELP } from "./flags";
|
|
19
19
|
|
|
20
20
|
const flags = parseFlags(process.argv);
|
|
21
21
|
|
|
22
|
+
if (flags.help) {
|
|
23
|
+
console.log(HELP);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
if (flags.setup) {
|
|
23
28
|
const { runSetup } = await import("./setup");
|
|
24
29
|
process.exit(runSetup() ? 0 : 2);
|
|
@@ -44,9 +49,31 @@ function isSessionValid(session: CachedSession | undefined): session is CachedSe
|
|
|
44
49
|
return new Date(session.Expiration).getTime() > Date.now();
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
// --- Helper:
|
|
52
|
+
// --- Helper: query Bedrock usage (--usage); a CLI command, so errors go to stderr ---
|
|
53
|
+
|
|
54
|
+
async function runUsage(opts: Omit<Parameters<typeof import("./usage").printUsage>[0], "spec" | "from" | "to">): Promise<never> {
|
|
55
|
+
const { printUsage } = await import("./usage");
|
|
56
|
+
try {
|
|
57
|
+
await printUsage({ ...opts, spec: flags.usageSpec ?? defaults.sessionName, from: flags.from, to: flags.to });
|
|
58
|
+
process.exit(0);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
process.stderr.write(`Usage query failed: ${err instanceof Error ? err.message : err}\n`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
48
64
|
|
|
49
|
-
|
|
65
|
+
// With --profile, the query uses that AWS profile instead of this tool's own credentials.
|
|
66
|
+
if (flags.usage && flags.profile) await runUsage({ region: defaults.region, profile: flags.profile });
|
|
67
|
+
|
|
68
|
+
// --- Helper: output credentials (or, with --usage, use them for the usage query) ---
|
|
69
|
+
|
|
70
|
+
async function outputCredentials(session: CachedSession): Promise<void> {
|
|
71
|
+
if (flags.usage) {
|
|
72
|
+
await runUsage({
|
|
73
|
+
region: defaults.region,
|
|
74
|
+
credentials: { accessKeyId: session.AccessKeyId, secretAccessKey: session.SecretAccessKey, sessionToken: session.SessionToken },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
50
77
|
console.log(JSON.stringify({
|
|
51
78
|
Credentials: {
|
|
52
79
|
AccessKeyId: session.AccessKeyId,
|
|
@@ -59,7 +86,7 @@ function outputCredentials(session: CachedSession): void {
|
|
|
59
86
|
// --- Helper: report a failure, respecting --no-ui (dialog is never shown in that mode) ---
|
|
60
87
|
|
|
61
88
|
async function reportFailure(message: string): Promise<never> {
|
|
62
|
-
if (flags.noUi) {
|
|
89
|
+
if (flags.noUi || flags.usage) {
|
|
63
90
|
// stdout is reserved for cleanly-parsable credentials JSON on success.
|
|
64
91
|
process.stderr.write(`${message}\n`);
|
|
65
92
|
} else {
|
|
@@ -92,6 +119,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
|
|
|
92
119
|
roleArn: cfg.roleArn,
|
|
93
120
|
mfaCode,
|
|
94
121
|
duration: cfg.duration ?? STANDARD_DURATIONS[0],
|
|
122
|
+
sessionName: cfg.sessionName,
|
|
95
123
|
});
|
|
96
124
|
|
|
97
125
|
const session: CachedSession = {
|
|
@@ -108,6 +136,7 @@ async function tryAutoMfa(cfg: Partial<Config>): Promise<CachedSession | null> {
|
|
|
108
136
|
mfaArn: cfg.mfaArn,
|
|
109
137
|
roleArn: cfg.roleArn,
|
|
110
138
|
duration,
|
|
139
|
+
sessionName: cfg.sessionName,
|
|
111
140
|
mfaMode: cfg.mfaMode,
|
|
112
141
|
mfaCommand: cfg.mfaCommand,
|
|
113
142
|
cacheSession: cfg.cacheSession,
|
|
@@ -146,6 +175,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
|
|
|
146
175
|
roleArn: result.roleArn,
|
|
147
176
|
mfaCode,
|
|
148
177
|
duration: parseInt(result.duration, 10) || STANDARD_DURATIONS[0],
|
|
178
|
+
sessionName: result.sessionName,
|
|
149
179
|
});
|
|
150
180
|
|
|
151
181
|
const session: CachedSession = {
|
|
@@ -162,6 +192,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
|
|
|
162
192
|
mfaArn: result.mfaArn,
|
|
163
193
|
roleArn: result.roleArn,
|
|
164
194
|
duration,
|
|
195
|
+
sessionName: result.sessionName || undefined,
|
|
165
196
|
mfaMode: result.mfaMode,
|
|
166
197
|
mfaCommand: result.mfaCommand || undefined,
|
|
167
198
|
cacheSession: defaults.cacheSession,
|
|
@@ -170,7 +201,7 @@ async function obtainViaDialog(cfg: Partial<Config>): Promise<void> {
|
|
|
170
201
|
...(useCache ? { cachedSession: session } : {}),
|
|
171
202
|
});
|
|
172
203
|
|
|
173
|
-
outputCredentials(session);
|
|
204
|
+
await outputCredentials(session);
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
// --- Main flow ---
|
|
@@ -180,7 +211,7 @@ try {
|
|
|
180
211
|
if (useCache && config?.cachedSession) {
|
|
181
212
|
if (isSessionValid(config.cachedSession)) {
|
|
182
213
|
// Return cached credentials without showing dialog
|
|
183
|
-
outputCredentials(config.cachedSession);
|
|
214
|
+
await outputCredentials(config.cachedSession);
|
|
184
215
|
process.exit(0);
|
|
185
216
|
}
|
|
186
217
|
// Expired — clear the cached session
|
|
@@ -192,7 +223,7 @@ try {
|
|
|
192
223
|
try {
|
|
193
224
|
const session = await tryAutoMfa(defaults);
|
|
194
225
|
if (session) {
|
|
195
|
-
outputCredentials(session);
|
|
226
|
+
await outputCredentials(session);
|
|
196
227
|
process.exit(0);
|
|
197
228
|
}
|
|
198
229
|
} catch (err) {
|
|
@@ -222,7 +253,7 @@ try {
|
|
|
222
253
|
if (useCache) {
|
|
223
254
|
const freshConfig = loadConfig();
|
|
224
255
|
if (freshConfig?.cachedSession && isSessionValid(freshConfig.cachedSession)) {
|
|
225
|
-
outputCredentials(freshConfig.cachedSession);
|
|
256
|
+
await outputCredentials(freshConfig.cachedSession);
|
|
226
257
|
process.exit(0);
|
|
227
258
|
}
|
|
228
259
|
}
|
|
@@ -234,7 +265,7 @@ try {
|
|
|
234
265
|
if (useCache) {
|
|
235
266
|
const freshConfig = loadConfig();
|
|
236
267
|
if (freshConfig?.cachedSession && isSessionValid(freshConfig.cachedSession)) {
|
|
237
|
-
outputCredentials(freshConfig.cachedSession);
|
|
268
|
+
await outputCredentials(freshConfig.cachedSession);
|
|
238
269
|
process.exit(0);
|
|
239
270
|
}
|
|
240
271
|
}
|
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/src/usage.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { BedrockClient, GetModelInvocationLoggingConfigurationCommand } from "@aws-sdk/client-bedrock";
|
|
2
|
+
import { CloudWatchLogsClient, StartQueryCommand, GetQueryResultsCommand } from "@aws-sdk/client-cloudwatch-logs";
|
|
3
|
+
import { DEFAULT_SESSION_NAME } from "./sts";
|
|
4
|
+
|
|
5
|
+
const COLUMNS = ["requests", "inputTokens", "cacheReadTokens", "cacheWriteTokens", "outputTokens"] as const;
|
|
6
|
+
const HEADERS = ["Model", "Requests", "Input", "Cache read", "Cache write", "Output"];
|
|
7
|
+
|
|
8
|
+
/** Start of the current month (local time) through now, unless overridden. */
|
|
9
|
+
export function usageRange(from?: string, to?: string): { start: Date; end: Date } {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const start = from ? new Date(from) : new Date(now.getFullYear(), now.getMonth(), 1);
|
|
12
|
+
const end = to ? new Date(to) : now;
|
|
13
|
+
if (isNaN(start.getTime())) throw new Error(`Invalid --from date: ${from}`);
|
|
14
|
+
if (isNaN(end.getTime())) throw new Error(`Invalid --to date: ${to}`);
|
|
15
|
+
if (start >= end) throw new Error("--from must be before --to");
|
|
16
|
+
return { start, end };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type GroupField = "roleName" | "sessionName";
|
|
20
|
+
const GROUP_HEADERS: Record<GroupField, string> = { roleName: "Role", sessionName: "Session" };
|
|
21
|
+
|
|
22
|
+
/** What `--usage=<spec>` selects; an undefined role/session matches any. */
|
|
23
|
+
export interface UsageSpec {
|
|
24
|
+
role?: string;
|
|
25
|
+
session?: string;
|
|
26
|
+
groupBy: GroupField[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `--usage=[<role>/]<session>`. Either part may be `*`, meaning "itemize by
|
|
31
|
+
* it": `my-role/*` gives one row per session of role my-role. A bare `*` itemizes
|
|
32
|
+
* by role (sessions merged); a bare name matches that session under any role.
|
|
33
|
+
*/
|
|
34
|
+
export function parseUsageSpec(spec: string): UsageSpec {
|
|
35
|
+
if (spec === "*") return { groupBy: ["roleName"] };
|
|
36
|
+
const parts = spec.split("/");
|
|
37
|
+
if (parts.length > 2) throw new Error(`Invalid --usage value "${spec}": expected [<role>/]<session-name>`);
|
|
38
|
+
const [role, session] = parts.length === 2 ? parts : [undefined, parts[0]];
|
|
39
|
+
// IAM's charset for role and session names; also keeps them safe to embed as query literals.
|
|
40
|
+
for (const name of [role, session]) {
|
|
41
|
+
if (name !== undefined && name !== "*" && !/^[\w+=,.@-]+$/.test(name)) {
|
|
42
|
+
throw new Error(`Invalid role or session name "${name}" in --usage value "${spec}"`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
role: role === "*" ? undefined : role,
|
|
47
|
+
session: session === "*" ? undefined : session,
|
|
48
|
+
groupBy: [role === "*" && "roleName", session === "*" && "sessionName"].filter((g): g is GroupField => !!g),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Logs Insights query summing token counts per model (and per group) for the
|
|
54
|
+
* assumed-role callers a spec selects. The caller's ARN is
|
|
55
|
+
* `arn:aws:sts::<acct>:assumed-role/<role>/<session>`; other callers (IAM
|
|
56
|
+
* users, root) never match.
|
|
57
|
+
*/
|
|
58
|
+
export function usageQuery(spec: UsageSpec): string {
|
|
59
|
+
const filters = ["ispresent(sessionName)"];
|
|
60
|
+
if (spec.role) filters.push(`roleName = "${spec.role}"`);
|
|
61
|
+
if (spec.session) filters.push(`sessionName = "${spec.session}"`);
|
|
62
|
+
return [
|
|
63
|
+
"parse identity.arn /:assumed-role\\/(?<roleName>[^\\/]+)\\/(?<sessionName>[^\\/]+)$/",
|
|
64
|
+
`| filter ${filters.join(" and ")}`,
|
|
65
|
+
"| stats count(*) as requests, sum(input.inputTokenCount) as inputTokens,",
|
|
66
|
+
" sum(input.cacheReadInputTokenCount) as cacheReadTokens, sum(input.cacheWriteInputTokenCount) as cacheWriteTokens,",
|
|
67
|
+
` sum(output.outputTokenCount) as outputTokens by ${[...spec.groupBy, "modelId"].join(", ")}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type Column = (typeof COLUMNS)[number];
|
|
72
|
+
/** Per-token USD prices, keyed by `<Price List model name>|<global?>`. */
|
|
73
|
+
export type PriceTable = Record<string, Partial<Record<Column, number>>>;
|
|
74
|
+
|
|
75
|
+
const PRICED_COLUMNS: Record<string, Column> = {
|
|
76
|
+
"Input tokens": "inputTokens",
|
|
77
|
+
"Output tokens": "outputTokens",
|
|
78
|
+
"Prompt cache read input tokens": "cacheReadTokens",
|
|
79
|
+
"Prompt cache write input tokens": "cacheWriteTokens",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Standard on-demand token prices from an AWS Price List offer file
|
|
84
|
+
* (AmazonBedrockFoundationModels). Long-context, 1h-cache-write and
|
|
85
|
+
* latency-optimized variants are skipped: the aggregated logs can't tell those
|
|
86
|
+
* requests apart, so every token is priced at the base rate.
|
|
87
|
+
*/
|
|
88
|
+
export function parsePriceList(offer: any): PriceTable {
|
|
89
|
+
const table: PriceTable = {};
|
|
90
|
+
for (const [sku, product] of Object.entries<any>(offer.products ?? {})) {
|
|
91
|
+
const a = product.attributes ?? {};
|
|
92
|
+
const column = PRICED_COLUMNS[a.inferenceType];
|
|
93
|
+
if (a.feature !== "On-demand Inference" || !column || /long_ctx|1h|LatencyOptimized/i.test(a.usagetype)) continue;
|
|
94
|
+
const dim: any = Object.values<any>(Object.values<any>(offer.terms?.OnDemand?.[sku] ?? {})[0]?.priceDimensions ?? {})[0];
|
|
95
|
+
if (dim?.unit !== "1M tokens") continue;
|
|
96
|
+
(table[`${a.model}|${/global/i.test(a.usagetype)}`] ??= {})[column] = Number(dim.pricePerUnit.USD) / 1e6;
|
|
97
|
+
}
|
|
98
|
+
return table;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** `…/us.anthropic.claude-sonnet-4-5-20250929-v1:0` → `Claude Sonnet 4.5`, the Price List's model name. */
|
|
102
|
+
export function priceListModelName(modelId: string): string {
|
|
103
|
+
const words = modelId.split("/").pop()!.split(".").pop()!.replace(/-\d{8}.*$|-v\d+(:\d+)?$/, "").split("-");
|
|
104
|
+
return words
|
|
105
|
+
.map((w, i) => (i === 0 ? "" : /^\d+$/.test(w) && /^\d+$/.test(words[i - 1]) ? "." : " ") + w[0].toUpperCase() + w.slice(1))
|
|
106
|
+
.join("");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** USD cost of one row, or undefined if any token type it used has no known price. */
|
|
110
|
+
export function rowCost(row: Record<string, string>, prices: PriceTable): number | undefined {
|
|
111
|
+
const isGlobal = row.modelId.split("/").pop()!.startsWith("global.");
|
|
112
|
+
const price = prices[`${priceListModelName(row.modelId)}|${isGlobal}`] ?? {};
|
|
113
|
+
let cost = 0;
|
|
114
|
+
for (const c of COLUMNS.slice(1)) {
|
|
115
|
+
const tokens = Number(row[c] || 0);
|
|
116
|
+
if (!tokens) continue;
|
|
117
|
+
if (price[c] === undefined) return undefined;
|
|
118
|
+
cost += tokens * price[c]!;
|
|
119
|
+
}
|
|
120
|
+
return cost;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const usd = (n: number) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
124
|
+
|
|
125
|
+
interface Tally {
|
|
126
|
+
label: string;
|
|
127
|
+
counts: number[];
|
|
128
|
+
cost: number;
|
|
129
|
+
unpriced: boolean; // some tokens had no list price, so `cost` is a lower bound
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function tally(label: string, rows: Record<string, string>[], prices: PriceTable): Tally {
|
|
133
|
+
const t: Tally = { label, counts: COLUMNS.map(() => 0), cost: 0, unpriced: false };
|
|
134
|
+
for (const r of rows) {
|
|
135
|
+
COLUMNS.forEach((c, i) => (t.counts[i] += Number(r[c] || 0)));
|
|
136
|
+
const cost = rowCost(r, prices);
|
|
137
|
+
if (cost === undefined) t.unpriced = true;
|
|
138
|
+
else t.cost += cost;
|
|
139
|
+
}
|
|
140
|
+
return t;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** One line per model, or per group (role and/or session, models merged) when itemizing. */
|
|
144
|
+
export function formatUsageTable(rows: Record<string, string>[], prices: PriceTable = {}, groupBy: GroupField[] = []): string {
|
|
145
|
+
if (!rows.length) return "No Bedrock invocations found.";
|
|
146
|
+
const labelOf = (r: Record<string, string>) =>
|
|
147
|
+
groupBy.length ? groupBy.map((g) => r[g]).join("/") : (r.modelId ?? "").split("/").pop()!;
|
|
148
|
+
const groups = Map.groupBy(rows, labelOf);
|
|
149
|
+
const tallies = [...groups].map(([label, rs]) => tally(label, rs, prices)).sort((a, b) => b.cost - a.cost);
|
|
150
|
+
const total = tally("TOTAL", rows, prices);
|
|
151
|
+
|
|
152
|
+
const cost = (t: Tally) => (t.unpriced && !t.cost ? "?" : usd(t.cost) + (t.unpriced ? "+" : ""));
|
|
153
|
+
const cells = (t: Tally) => [t.label, ...t.counts.map((n) => n.toLocaleString("en-US")), cost(t)];
|
|
154
|
+
const headers = [groupBy.length ? groupBy.map((g) => GROUP_HEADERS[g]).join("/") : "Model", ...HEADERS.slice(1), "Cost (USD)"];
|
|
155
|
+
const table = [...tallies, total].map(cells);
|
|
156
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...table.map((r) => r[i].length)));
|
|
157
|
+
const line = (cs: string[]) => cs.map((c, i) => (i ? c.padStart(widths[i]) : c.padEnd(widths[i]))).join(" ");
|
|
158
|
+
return [
|
|
159
|
+
line(headers),
|
|
160
|
+
...table.slice(0, -1).map(line),
|
|
161
|
+
widths.map((w) => "-".repeat(w)).join(" "),
|
|
162
|
+
line(table.at(-1)!),
|
|
163
|
+
"",
|
|
164
|
+
"Cost is an estimate at current AWS list prices (base on-demand rates; excludes discounts, long-context and 1h-cache surcharges).",
|
|
165
|
+
...(total.unpriced ? ["? / + = some models have no list price and are excluded from the cost."] : []),
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Bedrock prices for `region` from the public AWS Price List bulk API (no credentials needed). */
|
|
170
|
+
async function fetchPrices(region: string): Promise<PriceTable> {
|
|
171
|
+
const url = `https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonBedrockFoundationModels/current/${region}/index.json`;
|
|
172
|
+
const res = await fetch(url);
|
|
173
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${url}`);
|
|
174
|
+
return parsePriceList(await res.json());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Query Bedrock model invocation logs (CloudWatch Logs destination) for usage
|
|
179
|
+
* by the role/session that `spec` (see parseUsageSpec) selects. Requires invocation logging to be enabled.
|
|
180
|
+
* `credentials` undefined + `profile` set → the SDK resolves that profile.
|
|
181
|
+
*/
|
|
182
|
+
export async function printUsage(opts: {
|
|
183
|
+
region?: string;
|
|
184
|
+
credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string };
|
|
185
|
+
profile?: string;
|
|
186
|
+
spec?: string;
|
|
187
|
+
from?: string;
|
|
188
|
+
to?: string;
|
|
189
|
+
}): Promise<void> {
|
|
190
|
+
const { start, end } = usageRange(opts.from, opts.to);
|
|
191
|
+
const specText = opts.spec || DEFAULT_SESSION_NAME;
|
|
192
|
+
const spec = parseUsageSpec(specText);
|
|
193
|
+
const clientConfig = { region: opts.region || undefined, credentials: opts.credentials, profile: opts.profile };
|
|
194
|
+
|
|
195
|
+
const bedrock = new BedrockClient(clientConfig);
|
|
196
|
+
// Prices are best-effort: token counts are still worth printing without them.
|
|
197
|
+
const prices = bedrock.config.region().then(fetchPrices).catch((err) => {
|
|
198
|
+
process.stderr.write(`Could not fetch Bedrock prices, costs omitted: ${err instanceof Error ? err.message : err}\n`);
|
|
199
|
+
return {};
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const { loggingConfig } = await bedrock.send(new GetModelInvocationLoggingConfigurationCommand({}));
|
|
203
|
+
const logGroupName = loggingConfig?.cloudWatchConfig?.logGroupName;
|
|
204
|
+
if (!logGroupName) {
|
|
205
|
+
throw new Error("Bedrock model invocation logging to CloudWatch Logs is not enabled in this account/region, so there is no per-session usage to query.");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const logs = new CloudWatchLogsClient(clientConfig);
|
|
209
|
+
const { queryId } = await logs.send(new StartQueryCommand({
|
|
210
|
+
logGroupName,
|
|
211
|
+
startTime: Math.floor(start.getTime() / 1000),
|
|
212
|
+
endTime: Math.floor(end.getTime() / 1000),
|
|
213
|
+
queryString: usageQuery(spec),
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
let res;
|
|
217
|
+
do {
|
|
218
|
+
await Bun.sleep(1000);
|
|
219
|
+
res = await logs.send(new GetQueryResultsCommand({ queryId }));
|
|
220
|
+
} while (res.status === "Scheduled" || res.status === "Running");
|
|
221
|
+
if (res.status !== "Complete") throw new Error(`Logs Insights query ${res.status}`);
|
|
222
|
+
|
|
223
|
+
const rows = (res.results ?? []).map((r) => Object.fromEntries(r.map((f) => [f.field!, f.value ?? ""])));
|
|
224
|
+
console.log(`Bedrock usage for ${specText}, ${start.toISOString()} – ${end.toISOString()} (log group ${logGroupName})\n`);
|
|
225
|
+
console.log(formatUsageTable(rows, await prices, spec.groupBy));
|
|
226
|
+
}
|
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/index.test.ts
CHANGED
|
@@ -18,6 +18,12 @@ describe("parseFlags", () => {
|
|
|
18
18
|
expect(parseFlags([...base, "--no-ui"]).noUi).toBe(true);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
test("parses --help and -h", () => {
|
|
22
|
+
expect(parseFlags(base).help).toBe(false);
|
|
23
|
+
expect(parseFlags([...base, "--help"]).help).toBe(true);
|
|
24
|
+
expect(parseFlags([...base, "-h"]).help).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
21
27
|
test("parses --setup", () => {
|
|
22
28
|
expect(parseFlags([...base, "--setup"]).setup).toBe(true);
|
|
23
29
|
expect(parseFlags([...base, "setup"]).setup).toBe(true);
|
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(() => {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { parseUsageSpec, usageQuery, usageRange, formatUsageTable, parsePriceList, priceListModelName, rowCost } from "../src/usage";
|
|
3
|
+
import { parseFlags } from "../src/flags";
|
|
4
|
+
|
|
5
|
+
describe("usage", () => {
|
|
6
|
+
test("parses --usage, --profile=, --from=, --to=", () => {
|
|
7
|
+
const f = parseFlags(["bun", "x", "--usage", "--profile=work", "--from=2026-09-01", "--to=2026-10-01"]);
|
|
8
|
+
expect(f).toMatchObject({ usage: true, profile: "work", from: "2026-09-01", to: "2026-10-01" });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("parses --usage=<spec>", () => {
|
|
12
|
+
expect(parseFlags(["bun", "x", "--usage=my-role/*"])).toMatchObject({ usage: true, usageSpec: "my-role/*" });
|
|
13
|
+
expect(parseFlags(["bun", "x", "--usage"]).usageSpec).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("parses usage specs", () => {
|
|
17
|
+
expect(parseUsageSpec("sess")).toEqual({ role: undefined, session: "sess", groupBy: [] });
|
|
18
|
+
expect(parseUsageSpec("my-role/sess")).toEqual({ role: "my-role", session: "sess", groupBy: [] });
|
|
19
|
+
expect(parseUsageSpec("my-role/*")).toEqual({ role: "my-role", session: undefined, groupBy: ["sessionName"] });
|
|
20
|
+
expect(parseUsageSpec("*")).toEqual({ groupBy: ["roleName"] });
|
|
21
|
+
expect(parseUsageSpec("*/*")).toEqual({ role: undefined, session: undefined, groupBy: ["roleName", "sessionName"] });
|
|
22
|
+
expect(parseUsageSpec("jane.doe@example.com").session).toBe("jane.doe@example.com");
|
|
23
|
+
expect(() => parseUsageSpec("a/b/c")).toThrow("expected");
|
|
24
|
+
expect(() => parseUsageSpec('x" or 1=1')).toThrow("Invalid role or session");
|
|
25
|
+
expect(() => parseUsageSpec("my-role/")).toThrow("Invalid role or session");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("defaults to the current month", () => {
|
|
29
|
+
const { start, end } = usageRange();
|
|
30
|
+
const now = new Date();
|
|
31
|
+
expect(start).toEqual(new Date(now.getFullYear(), now.getMonth(), 1));
|
|
32
|
+
expect(end.getTime()).toBeGreaterThanOrEqual(start.getTime());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("rejects bad ranges", () => {
|
|
36
|
+
expect(() => usageRange("nope")).toThrow("Invalid --from");
|
|
37
|
+
expect(() => usageRange("2026-10-02", "2026-10-01")).toThrow("before");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("query parses role and session from the caller ARN and filters/groups by the spec", () => {
|
|
41
|
+
const q = usageQuery(parseUsageSpec("my-role/*"));
|
|
42
|
+
expect(q).toContain('filter ispresent(sessionName) and roleName = "my-role"\n');
|
|
43
|
+
expect(q).toContain("by sessionName, modelId");
|
|
44
|
+
const re = new RegExp(q.match(/^parse identity\.arn \/(.*)\/$/m)![1]);
|
|
45
|
+
expect("arn:aws:sts::1:assumed-role/my-role/jane.doe@example.com".match(re)?.groups).toEqual({ roleName: "my-role", sessionName: "jane.doe@example.com" });
|
|
46
|
+
expect(re.test("arn:aws:iam::1:user/bob")).toBe(false);
|
|
47
|
+
expect(usageQuery(parseUsageSpec("sess"))).toContain('and sessionName = "sess"\n');
|
|
48
|
+
expect(usageQuery(parseUsageSpec("sess"))).toContain("by modelId");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("formats a table with totals", () => {
|
|
52
|
+
const out = formatUsageTable([
|
|
53
|
+
{ modelId: "m1", requests: "2", inputTokens: "1000", outputTokens: "50" },
|
|
54
|
+
{ modelId: "m2", requests: "1", inputTokens: "10", cacheReadTokens: "5", outputTokens: "1" },
|
|
55
|
+
]);
|
|
56
|
+
expect(out).toContain("TOTAL");
|
|
57
|
+
expect(out.split("\n").find((l) => l.startsWith("TOTAL"))).toMatch(/^TOTAL\s+3\s+1,010\s+5\s+0\s+51\s+\?$/);
|
|
58
|
+
expect(formatUsageTable([])).toBe("No Bedrock invocations found.");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("maps log model IDs to Price List model names", () => {
|
|
62
|
+
expect(priceListModelName("arn:aws:bedrock:us-west-2:1:inference-profile/us.anthropic.claude-opus-5-5")).toBe("Claude Opus 5.5");
|
|
63
|
+
expect(priceListModelName("global.anthropic.claude-sonnet-5")).toBe("Claude Sonnet 5");
|
|
64
|
+
expect(priceListModelName("us.anthropic.claude-sonnet-4-5-20250929-v1:0")).toBe("Claude Sonnet 4.5");
|
|
65
|
+
expect(priceListModelName("anthropic.claude-3-5-haiku-20241022-v1:0")).toBe("Claude 3.5 Haiku");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const product = (model: string, inferenceType: string, usagetype: string) => ({
|
|
69
|
+
attributes: { feature: "On-demand Inference", model, inferenceType, usagetype },
|
|
70
|
+
});
|
|
71
|
+
const term = (usd: string) => ({ t: { priceDimensions: { d: { unit: "1M tokens", pricePerUnit: { USD: usd } } } } });
|
|
72
|
+
const offer = {
|
|
73
|
+
products: {
|
|
74
|
+
a: product("Claude Opus 5.5", "Input tokens", "USW2-MP:USW2_input_tokens_standard-Units"),
|
|
75
|
+
b: product("Claude Opus 5.5", "Input tokens", "USW2-MP:USW2_input_tokens_global_standard-Units"),
|
|
76
|
+
c: product("Claude Opus 5.5", "Input tokens", "USW2-MP:USW2_input_tokens_long_ctx_standard-Units"),
|
|
77
|
+
d: product("Claude Opus 5.5", "Prompt cache write input tokens", "USW2-MP:USW2_cache_write_tokens_1h_standard-Units"),
|
|
78
|
+
e: product("Claude Opus 5.5", "Prompt cache write input tokens", "USW2-MP:USW2_cache_write_tokens_standard-Units"),
|
|
79
|
+
f: product("Claude Opus 5.5", "Output tokens", "USW2-MP:USW2_output_tokens_standard-Units"),
|
|
80
|
+
},
|
|
81
|
+
terms: { OnDemand: { a: term("4.4"), b: term("4"), c: term("99"), d: term("99"), e: term("5.5"), f: term("22") } },
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
test("parses base on-demand prices, skipping long-context and 1h variants", () => {
|
|
85
|
+
expect(parsePriceList(offer)).toEqual({
|
|
86
|
+
"Claude Opus 5.5|false": { inputTokens: 4.4e-6, cacheWriteTokens: 5.5e-6, outputTokens: 22e-6 },
|
|
87
|
+
"Claude Opus 5.5|true": { inputTokens: 4e-6 },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("costs a row, picking global vs geo prices; unknown prices give undefined", () => {
|
|
92
|
+
const prices = parsePriceList(offer);
|
|
93
|
+
expect(rowCost({ modelId: "us.anthropic.claude-opus-5-5", inputTokens: "1000000", outputTokens: "1000000" }, prices)).toBeCloseTo(26.4);
|
|
94
|
+
expect(rowCost({ modelId: "global.anthropic.claude-opus-5-5", inputTokens: "1000000" }, prices)).toBeCloseTo(4);
|
|
95
|
+
expect(rowCost({ modelId: "global.anthropic.claude-opus-5-5", outputTokens: "1" }, prices)).toBeUndefined();
|
|
96
|
+
expect(rowCost({ modelId: "us.anthropic.claude-unknown-9" , inputTokens: "1" }, prices)).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("itemized tables merge models per group and cost each model separately", () => {
|
|
100
|
+
const prices = parsePriceList(offer);
|
|
101
|
+
const out = formatUsageTable(
|
|
102
|
+
[
|
|
103
|
+
{ sessionName: "a", modelId: "us.anthropic.claude-opus-5-5", requests: "1", inputTokens: "1000000" },
|
|
104
|
+
{ sessionName: "a", modelId: "global.anthropic.claude-opus-5-5", requests: "1", inputTokens: "1000000" },
|
|
105
|
+
{ sessionName: "b", modelId: "us.anthropic.claude-unknown-9", requests: "1", inputTokens: "5" },
|
|
106
|
+
],
|
|
107
|
+
prices,
|
|
108
|
+
["sessionName"],
|
|
109
|
+
).split("\n");
|
|
110
|
+
expect(out[0]).toMatch(/^Session\s/);
|
|
111
|
+
expect(out[1]).toMatch(/^a\s+2\s+2,000,000\s.*\$8\.40$/);
|
|
112
|
+
expect(out[2]).toMatch(/^b\s+1\s+5\s.*\?$/);
|
|
113
|
+
expect(out[4]).toMatch(/^TOTAL\s+3\s.*\$8\.40\+$/);
|
|
114
|
+
});
|
|
115
|
+
});
|