aws-sso: explicit local/remote mode (default local) and ntfy push for approvals

The mode cannot be inferred: Linux/Windows hosts can have a browser and an
always-on Mac can serve headless. /aws-sso local|remote, PI_AWS_SSO_MODE in
.env; a command-set mode wins; old mac/device names map. PI_AWS_SSO_NTFY
pushes the device link (tap to approve), a heads-up when a local login waits
>60 s, failures, and 'renewed'. Topic shown only as host + fingerprint.
Unprompted logins back off 30 min doubling to 4 h; a run that stopped on an
expired session resumes after any later successful login.
This commit is contained in:
pi@mbp-m1-2020
2026-10-11 22:50:31 +02:00
parent fdee3e42d6
commit 0717a5476f
4 changed files with 204 additions and 62 deletions
+35
View File
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
import {
fingerprint, cacheKeyFor, parseIni, resolveSsoSession, parseDeviceOutput,
isSsoExpiredError, reanchor, fmtLeft, probeDue, probeWith, REFRESH_WINDOW_MS,
normalizeMode, resolveMode, autoLoginGap, ntfyRequest, describeTopic,
} from "../extensions/aws-sso.ts";
const START = "https://d-1234567890.awsapps.com/start";
@@ -144,6 +145,40 @@ test("reanchor: a new token BEFORE the end inherits it (silent renewal); AFTER t
assert.deepEqual(reanchor({}, "aaaa1111", T0, T0), { refreshFp: "aaaa1111", since: T0 });
});
test("mode: command beats .env beats default local; old names map; junk ignored", () => {
assert.deepEqual(resolveMode({}, {}), { mode: "local", source: "default" });
assert.deepEqual(resolveMode({}, { PI_AWS_SSO_MODE: "remote" }), { mode: "remote", source: "env" });
assert.deepEqual(resolveMode({ mode: "local" }, { PI_AWS_SSO_MODE: "remote" }), { mode: "local", source: "command" });
assert.deepEqual(resolveMode({ mode: "device" }, {}), { mode: "remote", source: "command" });
assert.deepEqual(resolveMode({ mode: "mac" }, {}), { mode: "local", source: "command" });
assert.deepEqual(resolveMode({ mode: "bogus" }, { PI_AWS_SSO_MODE: " Remote " }), { mode: "remote", source: "env" });
assert.equal(normalizeMode("headless"), null);
});
test("autoLoginGap: 30 min, doubling, capped at 4 h", () => {
assert.deepEqual([0, 1, 2, 3, 4, 9].map((n) => autoLoginGap(n) / 60e3), [30, 60, 120, 240, 240, 240]);
});
test("ntfyRequest: title/click/priority headers, bearer only when given, ASCII-safe; bad URLs refused", () => {
const r = ntfyRequest("https://ntfy.sh/aws-x7Kq2", undefined, { title: "AWS SSO approval needed (mbp)", message: "Code PCDB-WSGH — tap", click: `https://${HOST}/start/#/device?user_code=PCDB-WSGH`, priority: "high", tags: ["key"] });
assert.equal(r.url, "https://ntfy.sh/aws-x7Kq2");
assert.equal(r.init.method, "POST");
assert.deepEqual(r.init.headers, { Title: "AWS SSO approval needed (mbp)", Priority: "high", Click: `https://${HOST}/start/#/device?user_code=PCDB-WSGH`, Tags: "key" });
assert.equal(r.init.body, "Code PCDB-WSGH — tap"); // the body may be UTF-8, headers may not
assert.equal(ntfyRequest("https://n.example/t", "tk_abc", { title: "x", message: "y" }).init.headers.Authorization, "Bearer tk_abc");
assert.equal(ntfyRequest("https://n.example/t", undefined, { title: "Förnyad ✓", message: "y" }).init.headers.Title, "F?rnyad ?");
assert.throws(() => ntfyRequest("https://ntfy.sh/", undefined, { title: "x", message: "y" }), /no topic/);
assert.throws(() => ntfyRequest("file:///etc/passwd", undefined, { title: "x", message: "y" }), /http/);
assert.throws(() => ntfyRequest("not a url", undefined, { title: "x", message: "y" }));
});
test("describeTopic never prints the topic (it is the secret)", () => {
const d = describeTopic("https://ntfy.sh/aws-x7Kq2-very-secret");
assert.match(d, /^ntfy\.sh \(topic [0-9a-f]{8}\)$/);
assert.ok(!d.includes("secret"));
assert.equal(describeTopic(undefined), "not configured");
});
test("fmtLeft", () => {
assert.equal(fmtLeft(-5), "0m");
assert.equal(fmtLeft(25 * 60e3), "25m");