az2aws 1.6.2 → 1.8.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/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.0](https://github.com/kuma0128/az2aws/compare/v1.7.0...v1.8.0) (2026-04-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * show saved credential usage details ([#206](https://github.com/kuma0128/az2aws/issues/206)) ([04855cf](https://github.com/kuma0128/az2aws/commit/04855cf18e42f84ec2d639c825b0b6233730c3a0))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **login:** retry managed profile reset on Windows lock errors ([#199](https://github.com/kuma0128/az2aws/issues/199)) ([a4ad1c7](https://github.com/kuma0128/az2aws/commit/a4ad1c76d0f193cee16f17319e47449c6b5cba44))
14
+ * **login:** skip non-az2aws profiles in --all-profiles ([#196](https://github.com/kuma0128/az2aws/issues/196)) ([0dc6820](https://github.com/kuma0128/az2aws/commit/0dc6820775ebf03637dda78a899e0e2c16c25561))
15
+ * outdated references in README and docker-launch.sh ([#192](https://github.com/kuma0128/az2aws/issues/192)) ([79421ae](https://github.com/kuma0128/az2aws/commit/79421ae4c6c734cb81b37fde6a457e2b9c0752e8))
16
+
17
+
18
+ ### Performance Improvements
19
+
20
+ * switch CI to Corepack and drop manual pnpm store cache ([a27b64c](https://github.com/kuma0128/az2aws/commit/a27b64c4183382a10618a6dfcae0ccf803261894))
21
+
22
+ ## [1.7.0](https://github.com/kuma0128/az2aws/compare/v1.6.2...v1.7.0) (2026-04-13)
23
+
24
+
25
+ ### Features
26
+
27
+ * add credential_process output mode ([#95](https://github.com/kuma0128/az2aws/issues/95)) ([2a3e66f](https://github.com/kuma0128/az2aws/commit/2a3e66fcd19fd461dd2f84fc32caa383fb56990c))
28
+ * add macOS CI coverage and separate E2E job ([#183](https://github.com/kuma0128/az2aws/issues/183)) ([cf617f8](https://github.com/kuma0128/az2aws/commit/cf617f899b5d2ba88650f7ffd26c3195dc32c695))
29
+ * Use Windows app-data conventions for update notifier cache ([#180](https://github.com/kuma0128/az2aws/issues/180)) ([8383ae4](https://github.com/kuma0128/az2aws/commit/8383ae46a468630ce531cc9df0dd7d64f115e49c))
30
+
31
+
32
+ ### Bug Fixes
33
+
34
+ * --no-prompt on account selection screen ([#173](https://github.com/kuma0128/az2aws/issues/173)) ([07c833f](https://github.com/kuma0128/az2aws/commit/07c833ff8a4553b8c32ed7841b3544776c41f907))
35
+ * clear password input before typing to prevent appending ([#171](https://github.com/kuma0128/az2aws/issues/171)) ([dfb0d65](https://github.com/kuma0128/az2aws/commit/dfb0d65c85fdb3a38bbc2d6ccdbd492f18e97c25))
36
+ * enforce whole number validation for session duration hours ([#167](https://github.com/kuma0128/az2aws/issues/167)) ([0bc7fb4](https://github.com/kuma0128/az2aws/commit/0bc7fb4f187dad422f08a42ece57a4b04138c51d))
37
+ * PATH handling when installing Chrome in CI workflow ([#184](https://github.com/kuma0128/az2aws/issues/184)) ([f6fd008](https://github.com/kuma0128/az2aws/commit/f6fd008a65f7c722b78ae65121e1e20f421dc232))
38
+ * support custom AWS config parent directories ([#172](https://github.com/kuma0128/az2aws/issues/172)) ([19753dd](https://github.com/kuma0128/az2aws/commit/19753dd769f31a72b4e0bdb84acf9638c1cd67f7))
39
+
3
40
  ## [1.6.2](https://github.com/kuma0128/az2aws/compare/v1.6.1...v1.6.2) (2026-04-10)
4
41
 
5
42
 
package/CONTRIBUTING.md CHANGED
@@ -90,11 +90,31 @@ pnpm build && node ./lib/index.js
90
90
  | `pnpm build` | Build for production |
91
91
  | `pnpm test` | Run unit tests |
92
92
  | `pnpm test:coverage` | Run unit tests with coverage |
93
+ | `pnpm test:e2e` | Run the live Azure→AWS browser smoke test |
93
94
  | `pnpm lint` | Run ESLint and formatting checks |
94
95
  | `pnpm eslint` | Run ESLint |
95
96
  | `pnpm prettier:check` | Check code formatting |
96
97
  | `pnpm prettier:write` | Auto-fix code formatting |
97
98
 
99
+ ### E2E Smoke Test
100
+
101
+ `pnpm test:e2e` launches a real Puppeteer/Chrome session, signs in through
102
+ Azure AD, and verifies that az2aws can retrieve AWS credentials via
103
+ `credential_process` without persisting them to the shared credentials file.
104
+
105
+ Copy `.env.example` to `.env` and fill in the values. See
106
+ [`vitest.e2e.config.ts`](vitest.e2e.config.ts) for the Vitest settings used by
107
+ this suite.
108
+
109
+ To troubleshoot a failing run, rerun with `AZ2AWS_E2E_MODE=debug` (visible
110
+ browser, auto-fill) or `AZ2AWS_E2E_MODE=gui` (fully manual).
111
+
112
+ > **Note:** Passkey-first Microsoft accounts are not supported — use a dedicated
113
+ > account with standard username/password/MFA.
114
+
115
+ CI only runs this test on `push` to `main` and `workflow_dispatch`; PR
116
+ validation does not depend on repository secrets.
117
+
98
118
  ## Development Workflow
99
119
 
100
120
  1. Create a new branch from `main`:
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  # az2aws
7
7
 
8
- Log in to AWS CLI using [Azure Active Directory](https://azure.microsoft.com) SSO. Supports MFA and places temporary credentials in the proper location for AWS CLI and SDKs.
8
+ Log in to AWS CLI using [Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/) SSO. Supports MFA and places temporary credentials in the proper location for AWS CLI and SDKs.
9
9
 
10
10
  > **💡 Tip:** Let's be honest — typing `az2aws` correctly on the first try is harder than the AWS certification exam. Save your sanity:
11
11
  >
@@ -18,6 +18,25 @@ Log in to AWS CLI using [Azure Active Directory](https://azure.microsoft.com) SS
18
18
  >
19
19
  > Your fingers will thank you. Your keyboard will thank you. Your coworkers will stop hearing you swear.
20
20
 
21
+ ## Contents
22
+
23
+ - [Installation](#installation)
24
+ - [mise (Recommended)](#mise-recommended)
25
+ - [npm](#npm)
26
+ - [Docker](#docker)
27
+ - [Snap](#snap)
28
+ - [Command Options](#command-options)
29
+ - [Usage](#usage)
30
+ - [Configuration](#configuration)
31
+ - [Logging In](#logging-in)
32
+ - [Automation](#automation)
33
+ - [Which profiles `--all-profiles` refreshes](#which-profiles---all-profiles-refreshes)
34
+ - [Getting Your Tenant ID and App ID URI](#getting-your-tenant-id-and-app-id-uri)
35
+ - [How It Works](#how-it-works)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [Support for Other Authentication Providers](#support-for-other-authentication-providers)
38
+ - [Acknowledgements](#acknowledgements)
39
+
21
40
  ## Installation
22
41
 
23
42
  ### mise (Recommended)
@@ -50,11 +69,11 @@ Install [Node.js](https://nodejs.org/) v24 or higher, then install az2aws:
50
69
 
51
70
  #### Linux Notes
52
71
 
53
- You must install [puppeteer dependencies](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-doesnt-launch) first.
72
+ You must install [puppeteer dependencies](https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md) first.
54
73
 
55
74
  **Install for all users:**
56
75
 
57
- sudo npm install -g az2aws --unsafe-perm
76
+ sudo npm install -g az2aws
58
77
  sudo chmod -R go+rx $(npm root -g)
59
78
 
60
79
  **Install for current user only:**
@@ -67,15 +86,19 @@ You must install [puppeteer dependencies](https://github.com/GoogleChrome/puppet
67
86
 
68
87
  #### Windows Notes
69
88
 
70
- If you get a missing Chrome/Chromium error, install the puppeteer dependency manually:
89
+ If you get a missing Chrome/Chromium error, reinstall the Puppeteer browser from the installed `az2aws` package directory:
90
+
91
+ node <npm_global_node_modules>\az2aws\node_modules\puppeteer\install.mjs
71
92
 
72
- node <node_modules_dir>/az2aws/node_modules/puppeteer/install.js
93
+ For an npm global install, replace `<npm_global_node_modules>` with the output of `npm root -g`.
94
+ If you installed az2aws with pnpm or another package manager, locate `puppeteer/install.mjs`
95
+ under the installed `az2aws` package directory and run it with `node`.
73
96
 
74
97
  ### Docker
75
98
 
76
99
  Run az2aws with a volume mounted to your AWS configuration directory:
77
100
 
78
- docker run --rm -it -v ~/.aws:/root/.aws az2aws/az2aws
101
+ docker run --rm -it -v ~/.aws:/root/.aws taiseiito1000/az2aws
79
102
 
80
103
  You can also install the docker-launch.sh script to your PATH:
81
104
 
@@ -108,10 +131,11 @@ https://snapcraft.io/az2aws
108
131
  | `--no-prompt` | Skip prompts, use defaults |
109
132
  | `--enable-chrome-network-service` | Enable Network Service (for 3XX redirects) |
110
133
  | `--no-verify-ssl` | Disable AWS SSL verification |
111
- | `--enable-chrome-seamless-sso` | Enable Azure AD Seamless SSO |
134
+ | `--enable-chrome-seamless-sso` | Enable Microsoft Entra Seamless SSO |
112
135
  | `--no-disable-extensions` | Keep browser extensions enabled |
113
136
  | `--disable-gpu` | Disable GPU acceleration |
114
137
  | `--incognito` | Open the login flow in an incognito browser context |
138
+ | `--credential-process` | Output credentials for AWS CLI credential_process |
115
139
  | `--version (-v)` | Show version number |
116
140
 
117
141
  ## Usage
@@ -148,11 +172,34 @@ Enable "Stay logged in" during configuration to use `--no-prompt` without storin
148
172
  helps avoid reusing an existing browser session, and it overrides any saved
149
173
  "Stay logged in" browser state for that run.
150
174
 
175
+ #### AWS CLI credential_process
176
+
177
+ Configure the profile first so it has the defaults needed for non-interactive
178
+ login, then point AWS CLI at `az2aws`:
179
+
180
+ [profile myprofile]
181
+ credential_process = az2aws --profile myprofile --credential-process
182
+
183
+ `--credential-process` uses the same non-interactive defaults as `--no-prompt`,
184
+ so make sure the profile already has the role and other required values set.
185
+ Standard output is reserved for the AWS CLI JSON payload, while human-readable
186
+ status messages are written to stderr.
187
+
188
+ Example stdout payload:
189
+
190
+ {
191
+ "Version": 1,
192
+ "AccessKeyId": "...",
193
+ "SecretAccessKey": "...",
194
+ "SessionToken": "...",
195
+ "Expiration": "2026-01-01T00:00:00.000Z"
196
+ }
197
+
151
198
  #### Environment Variables
152
199
 
153
200
  You can set defaults via environment variables (use with `--no-prompt`):
154
201
 
155
- - `AZURE_TENANT_ID` / `AZURE_APP_ID_URI` - Azure AD settings
202
+ - `AZURE_TENANT_ID` / `AZURE_APP_ID_URI` - Microsoft Entra ID settings
156
203
  - `AZURE_DEFAULT_USERNAME` / `AZURE_DEFAULT_PASSWORD` - Credentials
157
204
  - `AZURE_DEFAULT_ROLE_ARN` / `AZURE_DEFAULT_DURATION_HOURS` - AWS role settings
158
205
 
@@ -192,10 +239,15 @@ Example:
192
239
  az2aws # Default profile
193
240
  az2aws --profile foo # Named profile
194
241
  az2aws --mode gui # Use browser UI (more reliable)
242
+ az2aws --mode debug # Show the browser while az2aws still drives the flow
195
243
  az2aws --mode gui --incognito # Open a fresh incognito login window
196
244
 
197
245
  You'll be prompted for username, password, and MFA if required. After login, use AWS CLI/SDKs as usual.
198
246
 
247
+ `--mode gui` is fully manual and waits for you to complete the browser flow
248
+ yourself. If you want the browser to stay visible while az2aws still auto-fills
249
+ the login steps, use `--mode debug`.
250
+
199
251
  **Tips:**
200
252
  - Set `AWS_PROFILE` env var instead of using `--profile`
201
253
  - Use `--mode gui --disable-gpu` on VMs or if rendering fails
@@ -217,6 +269,13 @@ If you see device compliance errors (e.g., "Device UnSecured Or Non-Compliant"),
217
269
  Try:
218
270
  `--mode gui` and use your system Chrome via `BROWSER_CHROME_BIN`.
219
271
 
272
+ If your Microsoft account requires a saved passkey prompt before the username
273
+ or password page appears, that flow is unsupported in `az2aws --mode cli`.
274
+ The prompt is rendered by the browser/OS passkey UI instead of the page DOM,
275
+ so az2aws cannot dismiss it automatically. Use `--mode gui` and handle it
276
+ manually, or use an account that can continue with the standard page-based
277
+ username/password/MFA flow.
278
+
220
279
  If you see "Unable to recognize page state!", Azure's login pages may have
221
280
  changed. Try:
222
281
 
@@ -232,9 +291,25 @@ Renew all profiles at once:
232
291
 
233
292
  Credentials are only refreshed if expiring within 11 minutes - safe to run as a cron job.
234
293
 
294
+ ### Which profiles `--all-profiles` refreshes
295
+
296
+ `--all-profiles` iterates every `[default]` / `[profile <name>]` section in
297
+ `~/.aws/config` that has **at least one `azure_*` key** (e.g.
298
+ `azure_tenant_id`, `azure_app_id_uri`, `azure_default_role_arn`). Sections
299
+ without any `azure_*` key — plain AWS profiles, `[sso-session ...]`,
300
+ `[services ...]` — are skipped.
301
+
302
+ Profiles that intentionally keep `azure_tenant_id` / `azure_app_id_uri` in
303
+ environment variables (`AZURE_TENANT_ID`, `AZURE_APP_ID_URI`) instead of
304
+ the config file are still refreshed, as long as they have some other
305
+ `azure_*` key on disk. If required values are missing even after the
306
+ env-var merge, az2aws fails loudly with
307
+ `Profile '<name>' is not configured properly.` rather than skipping
308
+ silently.
309
+
235
310
  ## Getting Your Tenant ID and App ID URI
236
311
 
237
- Ask your Azure AD admin for these values, or extract them from myapps.microsoft.com:
312
+ Ask your Microsoft Entra ID admin for these values, or extract them from myapps.microsoft.com:
238
313
 
239
314
  1. Load the myapps.microsoft.com page.
240
315
  2. Click the app tile for the login you want.
@@ -248,7 +323,7 @@ Ask your Azure AD admin for these values, or extract them from myapps.microsoft.
248
323
 
249
324
  ## How It Works
250
325
 
251
- az2aws uses [Puppeteer](https://github.com/GoogleChrome/puppeteer) to automate a Chromium browser for Azure AD login. It parses the SAML response and calls [AWS STS AssumeRoleWithSAML](http://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html) to get temporary credentials.
326
+ az2aws uses [Puppeteer](https://github.com/puppeteer/puppeteer) to automate a Chromium browser for Microsoft Entra ID login. It parses the SAML response and calls [AWS STS AssumeRoleWithSAML](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithSAML.html) to get temporary credentials.
252
327
 
253
328
  ## Troubleshooting
254
329
 
@@ -260,7 +335,7 @@ If login fails, try these in order:
260
335
 
261
336
  ## Support for Other Authentication Providers
262
337
 
263
- This tool only supports Azure AD. Contributions for other SAML providers are welcome - open an issue on GitHub to discuss.
338
+ This tool only supports Microsoft Entra ID. Contributions for other SAML providers are welcome - open an issue on GitHub to discuss.
264
339
 
265
340
  ## Acknowledgements
266
341
 
package/lib/awsConfig.js CHANGED
@@ -9,9 +9,79 @@ const debug_1 = __importDefault(require("debug"));
9
9
  const paths_1 = require("./paths");
10
10
  const promises_1 = require("node:fs/promises");
11
11
  const fs_1 = __importDefault(require("fs"));
12
+ const node_crypto_1 = __importDefault(require("node:crypto"));
13
+ const path_1 = __importDefault(require("path"));
12
14
  const util_1 = __importDefault(require("util"));
13
15
  const debug = (0, debug_1.default)("az2aws");
14
16
  const writeFile = util_1.default.promisify(fs_1.default.writeFile);
17
+ const awsDirMode = 0o700;
18
+ const awsFileMode = 0o600;
19
+ const ignoredChmodErrorCodes = new Set([
20
+ "EACCES",
21
+ "EINVAL",
22
+ "ENOSYS",
23
+ "ENOTSUP",
24
+ "EPERM",
25
+ "EROFS",
26
+ ]);
27
+ async function hardenPathPermissions(fsPath, mode) {
28
+ if (process.platform === "win32") {
29
+ return;
30
+ }
31
+ try {
32
+ await (0, promises_1.chmod)(fsPath, mode);
33
+ }
34
+ catch (error) {
35
+ const code = error.code;
36
+ if (typeof code === "string" && ignoredChmodErrorCodes.has(code)) {
37
+ debug(`Skipping permission hardening due to ${code}`);
38
+ return;
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+ async function hardenCreatedDirectories(createdDir, targetDir) {
44
+ const resolvedCreatedDir = path_1.default.resolve(createdDir);
45
+ const resolvedTargetDir = path_1.default.resolve(targetDir);
46
+ const relativeTargetDir = path_1.default.relative(resolvedCreatedDir, resolvedTargetDir);
47
+ if (relativeTargetDir === ".." ||
48
+ relativeTargetDir.startsWith(`..${path_1.default.sep}`) ||
49
+ path_1.default.isAbsolute(relativeTargetDir)) {
50
+ debug("Skipping permission hardening because the target directory is not within the created directory.");
51
+ return;
52
+ }
53
+ let currentDir = resolvedCreatedDir;
54
+ await hardenPathPermissions(currentDir, awsDirMode);
55
+ if (!relativeTargetDir) {
56
+ return;
57
+ }
58
+ for (const segment of relativeTargetDir.split(path_1.default.sep)) {
59
+ if (!segment || segment === ".") {
60
+ continue;
61
+ }
62
+ currentDir = path_1.default.join(currentDir, segment);
63
+ await hardenPathPermissions(currentDir, awsDirMode);
64
+ }
65
+ }
66
+ async function atomicWriteTextFile(targetPath, text) {
67
+ const targetDir = path_1.default.dirname(targetPath);
68
+ const tempPath = targetDir === "."
69
+ ? `.${path_1.default.basename(targetPath)}.${node_crypto_1.default.randomUUID()}.tmp`
70
+ : path_1.default.join(targetDir, `.${path_1.default.basename(targetPath)}.${node_crypto_1.default.randomUUID()}.tmp`);
71
+ let shouldCleanupTempPath = false;
72
+ try {
73
+ await writeFile(tempPath, text);
74
+ shouldCleanupTempPath = true;
75
+ await hardenPathPermissions(tempPath, awsFileMode);
76
+ await (0, promises_1.rename)(tempPath, targetPath);
77
+ shouldCleanupTempPath = false;
78
+ }
79
+ finally {
80
+ if (shouldCleanupTempPath) {
81
+ await (0, promises_1.rm)(tempPath, { force: true }).catch(() => undefined);
82
+ }
83
+ }
84
+ }
15
85
  // Autorefresh credential time limit in milliseconds
16
86
  const refreshLimitInMs = 11 * 60 * 1000;
17
87
  exports.awsConfig = {
@@ -70,12 +140,45 @@ exports.awsConfig = {
70
140
  debug(`Received profiles: ${profiles.toString()}`);
71
141
  return profiles;
72
142
  },
143
+ async getAz2awsProfileNames() {
144
+ debug(`Getting az2aws-configured profiles from config.`);
145
+ const config = (await this._loadAsync("config")) || {};
146
+ const profiles = [];
147
+ for (const [sectionName, sectionConfig] of Object.entries(config)) {
148
+ let profileName;
149
+ if (sectionName === "default") {
150
+ profileName = "default";
151
+ }
152
+ else if (sectionName.startsWith("profile ")) {
153
+ profileName = sectionName.substring("profile ".length);
154
+ }
155
+ else {
156
+ debug(`Skipping non-profile section '${sectionName}'`);
157
+ continue;
158
+ }
159
+ // Treat a profile as az2aws-managed if it has at least one azure_* key.
160
+ // Required values (azure_tenant_id / azure_app_id_uri) may still be
161
+ // supplied by environment variables at runtime, so don't hard-require
162
+ // them in the config file.
163
+ const hasAzureKey = sectionConfig &&
164
+ typeof sectionConfig === "object" &&
165
+ Object.keys(sectionConfig).some((key) => key.startsWith("azure_"));
166
+ if (!hasAzureKey) {
167
+ debug(`Skipping profile '${profileName}' because it has no az2aws (azure_*) keys`);
168
+ continue;
169
+ }
170
+ profiles.push(profileName);
171
+ }
172
+ debug(`Received az2aws profiles: ${profiles.toString()}`);
173
+ return profiles;
174
+ },
73
175
  async _loadAsync(type) {
74
- if (!paths_1.paths[type])
176
+ const targetPath = paths_1.paths[type];
177
+ if (!targetPath)
75
178
  throw new Error(`Unknown config type: '${type}'`);
76
179
  return new Promise((resolve, reject) => {
77
- debug(`Loading '${type}' file at '${paths_1.paths[type]}'`);
78
- fs_1.default.readFile(paths_1.paths[type], "utf8", (err, data) => {
180
+ debug(`Loading '${type}' file`);
181
+ fs_1.default.readFile(targetPath, "utf8", (err, data) => {
79
182
  if (err) {
80
183
  if (err.code === "ENOENT") {
81
184
  debug(`File not found. Returning undefined.`);
@@ -86,23 +189,45 @@ exports.awsConfig = {
86
189
  }
87
190
  }
88
191
  debug("Parsing data");
89
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
192
  const parsedIni = ini_1.default.parse(data);
91
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
92
193
  return resolve(parsedIni);
93
194
  });
94
195
  });
95
196
  },
96
197
  async _saveAsync(type, data) {
97
- if (!paths_1.paths[type])
198
+ const targetPath = paths_1.paths[type];
199
+ if (!targetPath)
98
200
  throw new Error(`Unknown config type: '${type}'`);
99
201
  if (!data)
100
202
  throw new Error(`You must provide data for saving.`);
101
203
  debug(`Stringifying ${type} INI data`);
102
204
  const text = ini_1.default.stringify(data);
103
- debug(`Creating AWS config directory '${paths_1.paths.awsDir}' if not exists.`);
104
- await (0, promises_1.mkdir)(paths_1.paths.awsDir, { recursive: true });
105
- debug(`Writing '${type}' INI to file '${paths_1.paths[type]}'`);
106
- await writeFile(paths_1.paths[type], text);
205
+ const targetDir = path_1.default.dirname(targetPath);
206
+ const isDefaultAwsDir = path_1.default.resolve(targetDir) === path_1.default.resolve(paths_1.paths.awsDir);
207
+ if (targetDir !== ".") {
208
+ debug(`Creating target directory for '${type}' if it does not exist.`);
209
+ const createdDir = await (0, promises_1.mkdir)(targetDir, {
210
+ recursive: true,
211
+ mode: awsDirMode,
212
+ });
213
+ if (isDefaultAwsDir) {
214
+ await hardenPathPermissions(targetDir, awsDirMode);
215
+ }
216
+ else if (createdDir) {
217
+ await hardenCreatedDirectories(createdDir, targetDir);
218
+ }
219
+ else {
220
+ debug("Skipping directory permission hardening for existing custom directory.");
221
+ }
222
+ }
223
+ else {
224
+ debug(`Skipping target directory creation for '${type}' because it uses the current working directory.`);
225
+ }
226
+ debug(`Writing '${type}' INI to file atomically`);
227
+ await atomicWriteTextFile(targetPath, text);
228
+ // Defensive: atomicWriteTextFile already sets permissions on the temp file
229
+ // before rename, but we re-apply here in case rename semantics differ across
230
+ // platforms or file-systems.
231
+ await hardenPathPermissions(targetPath, awsFileMode);
107
232
  },
108
233
  };
@@ -5,8 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.configureProfileAsync = configureProfileAsync;
7
7
  const inquirer_1 = __importDefault(require("inquirer"));
8
+ const CLIError_1 = require("./CLIError");
8
9
  const awsConfig_1 = require("./awsConfig");
10
+ const sessionDuration_1 = require("./sessionDuration");
9
11
  async function configureProfileAsync(profileName) {
12
+ var _a;
10
13
  console.log(`Configuring profile '${profileName}'`);
11
14
  const profile = await awsConfig_1.awsConfig.getProfileConfigAsync(profileName);
12
15
  const questions = [
@@ -54,13 +57,8 @@ async function configureProfileAsync(profileName) {
54
57
  type: "input",
55
58
  name: "defaultDurationHours",
56
59
  message: "Default Session Duration Hours (up to 12):",
57
- default: (profile && profile.azure_default_duration_hours) || 1,
58
- validate: (input) => {
59
- const num = Number(input);
60
- if (num > 0 && num <= 12)
61
- return true;
62
- return "Duration hours must be between 1 and 12";
63
- },
60
+ default: (_a = (0, sessionDuration_1.parseSessionDurationHours)(profile === null || profile === void 0 ? void 0 : profile.azure_default_duration_hours)) !== null && _a !== void 0 ? _a : 1,
61
+ validate: sessionDuration_1.validateSessionDurationHours,
64
62
  },
65
63
  {
66
64
  type: "input",
@@ -70,12 +68,16 @@ async function configureProfileAsync(profileName) {
70
68
  },
71
69
  ];
72
70
  const answers = await inquirer_1.default.prompt(questions);
71
+ const defaultDurationHours = (0, sessionDuration_1.parseSessionDurationHours)(answers.defaultDurationHours);
72
+ if (defaultDurationHours === null) {
73
+ throw new CLIError_1.CLIError(sessionDuration_1.sessionDurationHoursValidationMessage);
74
+ }
73
75
  await awsConfig_1.awsConfig.setProfileConfigValuesAsync(profileName, {
74
76
  azure_tenant_id: answers.tenantId,
75
77
  azure_app_id_uri: answers.appIdUri,
76
78
  azure_default_username: answers.username,
77
79
  azure_default_role_arn: answers.defaultRoleArn,
78
- azure_default_duration_hours: answers.defaultDurationHours,
80
+ azure_default_duration_hours: String(defaultDurationHours),
79
81
  azure_default_remember_me: answers.rememberMe === "true",
80
82
  region: answers.region,
81
83
  });
package/lib/index.js CHANGED
@@ -6,7 +6,9 @@ process.on("SIGTERM", () => process.exit(1));
6
6
  const commander_1 = require("commander");
7
7
  const configureProfileAsync_1 = require("./configureProfileAsync");
8
8
  const login_1 = require("./login");
9
+ const sensitiveOutput_1 = require("./sensitiveOutput");
9
10
  const updateNotifier_1 = require("./updateNotifier");
11
+ const validateCliOptions_1 = require("./validateCliOptions");
10
12
  // eslint-disable-next-line @typescript-eslint/no-require-imports
11
13
  const { version } = require("../package.json");
12
14
  const program = new commander_1.Command();
@@ -24,6 +26,7 @@ program
24
26
  .option("--enable-chrome-seamless-sso", "Enable Chromium's pass-through authentication with Azure Active Directory Seamless Single Sign-On")
25
27
  .option("--no-disable-extensions", "Tell Puppeteer not to pass the --disable-extensions flag to Chromium")
26
28
  .option("--disable-gpu", "Tell Puppeteer to pass the --disable-gpu flag to Chromium")
29
+ .option("--credential-process", "Output credentials in JSON format for AWS CLI credential_process")
27
30
  .option("--incognito", "Launch Chromium in incognito mode")
28
31
  .parse(process.argv);
29
32
  const options = program.opts();
@@ -39,6 +42,7 @@ const enableChromeSeamlessSso = !!options.enableChromeSeamlessSso;
39
42
  const forceRefresh = !!options.forceRefresh;
40
43
  const noDisableExtensions = !options.disableExtensions;
41
44
  const disableGpu = !!options.disableGpu;
45
+ const credentialProcess = !!options.credentialProcess;
42
46
  const incognito = !!options.incognito;
43
47
  // Start the update lookup immediately, but only print after the main flow ends.
44
48
  const updateCheckPromise = (0, updateNotifier_1.checkForUpdate)(version, {
@@ -47,6 +51,11 @@ const updateCheckPromise = (0, updateNotifier_1.checkForUpdate)(version, {
47
51
  async function runAsync() {
48
52
  let exitCode = 0;
49
53
  try {
54
+ (0, validateCliOptions_1.validateCliOptions)({
55
+ allProfiles: !!options.allProfiles,
56
+ configure: !!options.configure,
57
+ credentialProcess,
58
+ });
50
59
  if (options.allProfiles) {
51
60
  await login_1.login.loginAll(mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, forceRefresh, noDisableExtensions, disableGpu, incognito);
52
61
  }
@@ -54,16 +63,20 @@ async function runAsync() {
54
63
  await (0, configureProfileAsync_1.configureProfileAsync)(profileName);
55
64
  }
56
65
  else {
57
- await login_1.login.loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito);
66
+ await login_1.login.loginAsync(profileName, mode, disableSandbox, noPrompt, enableChromeNetworkService, awsNoVerifySsl, enableChromeSeamlessSso, noDisableExtensions, disableGpu, incognito, credentialProcess);
58
67
  }
59
68
  }
60
69
  catch (err) {
61
70
  if (err instanceof Error && err.name === "CLIError") {
62
- console.error(err.message);
71
+ console.error((0, sensitiveOutput_1.formatCliErrorMessage)(err.message));
63
72
  exitCode = 2;
64
73
  }
74
+ else if (err instanceof Error) {
75
+ console.error((0, sensitiveOutput_1.formatUnexpectedErrorMessage)(err));
76
+ exitCode = 1;
77
+ }
65
78
  else {
66
- console.error(err);
79
+ console.error((0, sensitiveOutput_1.formatUnexpectedErrorMessage)(err));
67
80
  exitCode = 1;
68
81
  }
69
82
  }