claude-aws-mfa 1.1.0 → 1.2.1
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/.github/workflows/release.yml +95 -40
- package/.github/workflows/test.yml +3 -0
- package/CHANGELOG.md +23 -0
- package/README.md +30 -6
- package/package.json +1 -1
- package/src/autosize-html.ts +31 -0
- package/src/autosize.ts +39 -0
- package/src/claude-settings.ts +13 -5
- package/src/config.ts +84 -3
- package/src/credential-export.ts +32 -0
- package/src/dialog-html.ts +76 -3
- package/src/dialog.ts +36 -6
- package/src/flags.ts +5 -0
- package/src/index.ts +83 -55
- package/src/setup-html.ts +16 -3
- package/src/setup.ts +12 -15
- package/test/autosize.test.ts +56 -0
- package/test/config.test.ts +82 -1
- package/test/dialog-html.test.ts +53 -1
- package/test/gui-screenshot.test.ts +28 -11
- package/test/index.test.ts +5 -0
- package/test/setup.test.ts +59 -0
package/test/dialog-html.test.ts
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { buildHtml, FIELDS, FIELD_PATTERNS } from "../src/dialog-html";
|
|
2
|
+
import { buildHtml, buildErrorHtml, FIELDS, FIELD_PATTERNS } from "../src/dialog-html";
|
|
3
|
+
|
|
4
|
+
describe("profile banner", () => {
|
|
5
|
+
const profile = { name: "work", path: "~/.claude-profiles/work" };
|
|
6
|
+
|
|
7
|
+
test("is shown for the default profile too, never blank", () => {
|
|
8
|
+
for (const html of [buildHtml({}), buildErrorHtml("boom")]) {
|
|
9
|
+
expect(html).toContain('class="profile"');
|
|
10
|
+
expect(html).toContain("<b>default</b>");
|
|
11
|
+
expect(html).toContain("~/.claude");
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("names the profile in both the credential and error dialogs", () => {
|
|
16
|
+
for (const html of [buildHtml({}, profile), buildErrorHtml("boom", profile)]) {
|
|
17
|
+
expect(html).toContain('class="profile"');
|
|
18
|
+
expect(html).toContain("<b>work</b>");
|
|
19
|
+
expect(html).toContain("~/.claude-profiles/work");
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("escapes the profile name and path", () => {
|
|
24
|
+
const html = buildHtml({}, { name: '<img src=x onerror="alert(1)">', path: "/a&b" });
|
|
25
|
+
expect(html).not.toContain("<img src=x");
|
|
26
|
+
expect(html).toContain("<img");
|
|
27
|
+
expect(html).toContain("/a&b");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
3
30
|
|
|
4
31
|
describe("buildHtml", () => {
|
|
5
32
|
const config = {
|
|
@@ -161,3 +188,28 @@ describe("buildHtml", () => {
|
|
|
161
188
|
expect(html).toContain("currentMode()");
|
|
162
189
|
});
|
|
163
190
|
});
|
|
191
|
+
|
|
192
|
+
describe("buildErrorHtml", () => {
|
|
193
|
+
test("produces valid HTML with doctype", () => {
|
|
194
|
+
const html = buildErrorHtml("Something went wrong");
|
|
195
|
+
expect(html).toStartWith("<!DOCTYPE html>");
|
|
196
|
+
expect(html).toContain("</html>");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("includes the message text", () => {
|
|
200
|
+
const html = buildErrorHtml("STS AssumeRole failed: AccessDenied");
|
|
201
|
+
expect(html).toContain("STS AssumeRole failed: AccessDenied");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("has an OK button", () => {
|
|
205
|
+
const html = buildErrorHtml("error");
|
|
206
|
+
expect(html).toContain("_ok()");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("escapes HTML special characters in the message", () => {
|
|
210
|
+
const html = buildErrorHtml('<script>alert(1)</script> & "quoted"');
|
|
211
|
+
expect(html).not.toContain("<script>alert(1)</script>");
|
|
212
|
+
expect(html).toContain("<script>");
|
|
213
|
+
expect(html).toContain("&");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -41,7 +41,10 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
41
41
|
typeof import("node-screenshots").Window
|
|
42
42
|
> | null = null;
|
|
43
43
|
|
|
44
|
+
let titles: string[] = [];
|
|
45
|
+
|
|
44
46
|
beforeAll(async () => {
|
|
47
|
+
const start = Date.now();
|
|
45
48
|
windowModule = await import("node-screenshots");
|
|
46
49
|
|
|
47
50
|
mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
@@ -52,28 +55,42 @@ describe.skipIf(!process.env.GUI_TEST)("GUI screenshot tests", () => {
|
|
|
52
55
|
stderr: "inherit",
|
|
53
56
|
});
|
|
54
57
|
|
|
55
|
-
// Poll for the window to appear
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
58
|
+
// Poll for the window to appear. The budget is 90s rather than a snug
|
|
59
|
+
// multiple of the ~8s a warm runner takes: on Windows the first WebView2
|
|
60
|
+
// launch on a fresh image bootstraps its runtime and user-data folder
|
|
61
|
+
// before any window exists, which is what made this test flaky in CI.
|
|
62
|
+
// Waiting longer costs nothing when the window shows up on schedule.
|
|
63
|
+
//
|
|
64
|
+
// Window.all() may throw if the window manager hasn't registered EWMH
|
|
65
|
+
// atoms yet (e.g. _NET_CLIENT_LIST_STACKING on Linux), so we catch and
|
|
66
|
+
// retry.
|
|
67
|
+
const deadline = Date.now() + 90_000;
|
|
60
68
|
while (Date.now() < deadline) {
|
|
61
69
|
try {
|
|
62
70
|
const windows = windowModule.Window.all();
|
|
63
|
-
|
|
64
|
-
try { return w.title()
|
|
65
|
-
catch { return false; }
|
|
71
|
+
titles = windows.map((w) => {
|
|
72
|
+
try { return w.title() ?? ""; } catch { return ""; }
|
|
66
73
|
});
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const i = titles.findIndex((t) => t.includes("Claude AWS MFA"));
|
|
75
|
+
if (i >= 0) {
|
|
76
|
+
foundWindow = windows[i]!;
|
|
69
77
|
break;
|
|
70
78
|
}
|
|
71
79
|
} catch {
|
|
72
80
|
// Window.all() not yet available — WM still initializing
|
|
73
81
|
}
|
|
82
|
+
// A dead launcher will never produce a window; stop and let the
|
|
83
|
+
// "app launches without error" assertion report the exit code.
|
|
84
|
+
if (proc.exitCode !== null) break;
|
|
74
85
|
await Bun.sleep(500);
|
|
75
86
|
}
|
|
76
|
-
|
|
87
|
+
if (!foundWindow) {
|
|
88
|
+
// Without this a failure is a bare `null` — no way to tell "window never
|
|
89
|
+
// opened" from "opened under a title we didn't match".
|
|
90
|
+
console.log(`No matching window after ${Math.round((Date.now() - start) / 1000)}s.`);
|
|
91
|
+
console.log(`Launcher exitCode: ${proc.exitCode}. Visible titles: ${JSON.stringify(titles)}`);
|
|
92
|
+
}
|
|
93
|
+
}, 120_000);
|
|
77
94
|
|
|
78
95
|
afterAll(() => {
|
|
79
96
|
proc?.kill();
|
package/test/index.test.ts
CHANGED
|
@@ -11,6 +11,11 @@ describe("parseFlags", () => {
|
|
|
11
11
|
expect(flags.cacheSession).toBeUndefined();
|
|
12
12
|
expect(flags.autoMfa).toBeUndefined();
|
|
13
13
|
expect(flags.singleInstanceLock).toBeUndefined();
|
|
14
|
+
expect(flags.noUi).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("parses --no-ui", () => {
|
|
18
|
+
expect(parseFlags([...base, "--no-ui"]).noUi).toBe(true);
|
|
14
19
|
});
|
|
15
20
|
|
|
16
21
|
test("parses --setup", () => {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { getCredentialExportCommand } from "../src/credential-export";
|
|
6
|
+
import { bin } from "../package.json";
|
|
7
|
+
|
|
8
|
+
const BIN_NAME = Object.keys(bin)[0];
|
|
9
|
+
|
|
10
|
+
describe("getCredentialExportCommand", () => {
|
|
11
|
+
const origArgv = Bun.argv;
|
|
12
|
+
const origWhich = Bun.which;
|
|
13
|
+
let tmpDir: string;
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
Bun.argv = origArgv;
|
|
17
|
+
Bun.which = origWhich;
|
|
18
|
+
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("uses the bare bin name when it resolves via PATH to this same script (bin invoked directly)", () => {
|
|
22
|
+
tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
|
|
23
|
+
const scriptTarget = join(tmpDir, "index.ts");
|
|
24
|
+
writeFileSync(scriptTarget, "");
|
|
25
|
+
// Mirrors real installed-bin behavior: the shim on PATH is a symlink whose
|
|
26
|
+
// target Bun resolves into Bun.argv[1].
|
|
27
|
+
const shim = join(tmpDir, BIN_NAME);
|
|
28
|
+
symlinkSync(scriptTarget, shim);
|
|
29
|
+
|
|
30
|
+
Bun.argv = [process.execPath, scriptTarget];
|
|
31
|
+
Bun.which = ((cmd: string) => (cmd === BIN_NAME ? shim : null)) as typeof Bun.which;
|
|
32
|
+
|
|
33
|
+
expect(getCredentialExportCommand()).toBe(BIN_NAME);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("falls back to an explicit bun+script invocation when not resolvable via PATH", () => {
|
|
37
|
+
tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
|
|
38
|
+
const scriptTarget = join(tmpDir, "index.ts");
|
|
39
|
+
writeFileSync(scriptTarget, "");
|
|
40
|
+
|
|
41
|
+
Bun.argv = [process.execPath, scriptTarget];
|
|
42
|
+
Bun.which = (() => null) as typeof Bun.which;
|
|
43
|
+
|
|
44
|
+
expect(getCredentialExportCommand()).toBe(`${process.execPath} ${scriptTarget}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("falls back when PATH resolves the bin name to a different install", () => {
|
|
48
|
+
tmpDir = mkdtempSync(join(tmpdir(), "claude-aws-mfa-setup-test-"));
|
|
49
|
+
const scriptTarget = join(tmpDir, "index.ts");
|
|
50
|
+
const otherInstall = join(tmpDir, "other-index.ts");
|
|
51
|
+
writeFileSync(scriptTarget, "");
|
|
52
|
+
writeFileSync(otherInstall, "");
|
|
53
|
+
|
|
54
|
+
Bun.argv = [process.execPath, scriptTarget];
|
|
55
|
+
Bun.which = ((cmd: string) => (cmd === BIN_NAME ? otherInstall : null)) as typeof Bun.which;
|
|
56
|
+
|
|
57
|
+
expect(getCredentialExportCommand()).toBe(`${process.execPath} ${scriptTarget}`);
|
|
58
|
+
});
|
|
59
|
+
});
|