Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,20 @@ missing one fails at runtime only.
- **`src/run-log.ts`** — the per-run step log behind the Activity panel. One
run at a time; `refresh()` guards on `isWorking` because a second run would
overwrite the current-run slot.
- **`src/schedule.ts`** — every delay `setNextTokenRefresh()` may use. Three
rules: a retry delay is never derived from the stored token expiry (after a
- **`src/schedule.ts`** — every delay `setNextTokenRefresh()` may use, plus
`loginRetryAction()`, the decision behind replacing a login nobody finished.
Four rules: a delay is never derived from the stored token expiry (after a
failure it is in the past, which collapses to the floor and reopens the login
page in a loop); a login nobody completed is not retried on a timer; a
failure *after* the token was renewed keeps the expiry schedule rather than
an error retry, which would reopen the login page for an unrelated failure.
No electron imports.
page in a loop, #83); a failure *after* the token was renewed keeps the expiry
schedule rather than an error retry, which would reopen the login page for an
unrelated failure; only the user ending a login themselves
(`cancelledByUser`) stops it being replaced; and a login nobody finished is
replaced with **no backoff at all** — continuous refreshing is the premise, so
a dead device code is replaced by a live one for as long as it takes.
`MIN_LOGIN_CYCLE_MS` is a floor, not a backoff: it never grows, and nothing
waits for it unless a login fails the instant it starts. What keeps that from
piling up login pages is that an attempt with nobody at the machine never
shows anything (see automatic approval). No electron imports.
- **`src/page-script.ts`** — `loadPageScript()` / `injectIntoEveryFrame()`,
used by both injected scripts. Injection follows sub-frames because
`executeJavaScript` on a `WebContents` reaches the top frame only, and a
Expand Down Expand Up @@ -180,6 +187,17 @@ of them says the user is needed (issue #1). Keep these true:
default-browser mode, `shell.openExternal`, destroying the hidden probe only
once the browser is up. A new way for the flow to end without arriving there
is a refresh that hangs invisibly until the device code expires.
- **Unattended, "somewhere" is a hold, not the screen.** When nobody is at the
machine (`powerMonitor.getSystemIdleTime()`, see `AWAY_IDLE_SEC`), the same
`onUserNeeded` *parks* the attempt — nothing is shown, the page keeps being
driven, and the poll loop hands it over the moment somebody is there, as does
a refresh the user asks for (`showParkedLogin`, which is why `refresh()` no
longer just skips a run in progress). A login page opened into an empty room
is dead in ten minutes, which is what made an overnight refresh a morning of
expired credentials. Parking rather than aborting is deliberate twice over: a
slow identity provider can still come through on its own, and the user who
returns gets a page that is already loaded. Presence is re-read at each poll,
never captured once per run.
- **`backgroundThrottling: false` on the window.** Chromium throttles timers in
a window that is not visible, and the driver's scan loop is a timer.
- **Clicking is deliberately narrow.** Only on the device-authorization hosts
Expand Down Expand Up @@ -546,8 +564,12 @@ Worth doing headlessly, since nothing else covers it:
`AWS_IAM_AUTHENTICATOR_PATH` at a stub that prints an `ExecCredential` and
kubectl walks the whole exec path with no AWS account. Cover an existing
config carrying an entry the old loader rejected — a context with no cluster.
- **`src/schedule.ts`**: pure and electron-free, so its delay arithmetic can be
exercised directly.
- **`src/schedule.ts`**: pure and electron-free, so its delay arithmetic and
`loginRetryAction()` can be exercised directly. The behaviour around it —
which refreshes are unattended, what an unattended one is allowed to put on
screen, when a retry fires — needs `dist/aws-sso.js` loaded against stubbed
`electron` (including `powerMonitor.getSystemIdleTime`), a faked SSO OIDC
client and a fake clock, driving a whole night in a second.

Windows behaviour — Squirrel install and update events, the tray icon, toast
notifications, the login item — needs a real Windows machine. CI proves the
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/app-window.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ <h2 id="tray">The tray</h2>
<thead><tr><th>Item</th><th>What it does</th></tr></thead>
<tbody>
<tr><td>Next refresh <em>in 7 hours</em></td><td>Not clickable — when the current token expires, and therefore when Frost will refresh. Updates every 30 seconds.</td></tr>
<tr><td>Sign-in needed — waiting for you</td><td>Not clickable — Frost is holding a sign-in that cannot go further without you. Touch the keyboard, or pick <em>Refresh now</em>, and the login page it is holding appears.</td></tr>
<tr><td>Sign-in needed — trying again</td><td>Not clickable — the last sign-in went unfinished and the next one is starting. Frost replaces them continuously, so this is a glimpse rather than a resting state.</td></tr>
<tr><td>Refresh now</td><td>Starts a refresh immediately. Only shown once Frost is configured.</td></tr>
<tr><td>Settings… / Get Started</td><td>Opens the app window. Reads <em>Get Started</em> until you have saved a start URL.</td></tr>
<tr><td>About Frost</td><td>Opens this website in your browser.</td></tr>
Expand Down
5 changes: 3 additions & 2 deletions docs/docs/credential-refresh.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ <h2 id="notify">Auto-open or notify first</h2>
</p>
<ul>
<li><strong>Auto-open browser</strong> (default) — the login page opens as soon as the token needs replacing. With federated SSO it usually completes without asking you anything.</li>
<li><strong>Notification + hotkey</strong> — Frost posts a system notification and waits. Click the notification or press the refresh hotkey when you are ready, and the login page opens then. If you never do, the run gives up when the device code expires, and Frost waits for you rather than notifying again.</li>
<li><strong>Notification + hotkey</strong> — Frost posts a system notification and waits. Click the notification or press the refresh hotkey when you are ready, and the login page opens then. The notification is only raised while you are at the machine, so one that comes due overnight arrives when you sit down — pointing at a device code issued then, rather than one that expired hours earlier. If you let it expire anyway, Frost offers it again later.</li>
</ul>
<p>See <a href="settings-behavior.html">behavior settings</a> for the trade-off between the two.</p>

Expand All @@ -155,7 +155,8 @@ <h2 id="failures">When a run fails</h2>
kinds of failure want opposite treatment:
</p>
<ul>
<li><strong>Nobody finished the sign-in</strong> — the login window was closed, the notification was never answered, the device code expired unapproved, AWS reported the sign-in denied. Frost stops and waits for you. Trying again would only open another login page for nobody to complete, and an unattended machine would collect one every few minutes all night. The tray menu says <em>Sign-in needed</em>, a single notification tells you so unless you closed the window yourself, and the next refresh is the one <em>you</em> start.</li>
<li><strong>Nobody finished the sign-in</strong> — the notification went unanswered, the device code expired unapproved, the page wanted a password with nobody there to type it. Frost replaces it, <em>immediately and with no backoff</em>: AWS keeps a device code alive for about ten minutes, and when one dies the next sign-in starts at once. That is the point of the whole app — credentials that are current whether or not anyone is at the machine — so there is deliberately no "waiting for you" gap in it. Only you ending the sign-in yourself — closing the login window, refusing it at your identity provider — stops that, because that is you saying "not now"; the next refresh is then the one you start.</li>
<li><strong>Nobody was there at all</strong> — then nothing is ever put on your screen. When the page turns out to need a person, Frost <em>holds</em> that sign-in instead of showing it: no window, no browser tab, no notification. The page keeps being driven in the background, so a slow identity provider or a dropped network still comes through on its own; and the moment you touch the keyboard, the login page Frost has been holding appears — already loaded, within a few seconds. You come back to a live sign-in rather than the wreck of one that expired at 3am.</li>
<li><strong>Something else failed</strong> — no network, an AWS error before any login page opened. Nothing is on your screen to pile up, so Frost retries on its own: a minute later, then two, doubling up to half-hourly until one succeeds.</li>
<li>If the token was renewed and a <em>later</em> step failed — profiles, EKS — the credentials are good, so Frost stays on the normal schedule and tries the whole run again when they expire.</li>
<li>The error is stored and shown on the <strong>Credentials</strong> page, with the failing step marked on the <strong>Activity</strong> page.</li>
Expand Down
28 changes: 18 additions & 10 deletions docs/docs/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,11 @@ <h3>Default browser</h3>
<p>
The trade-off is that Frost has no window to watch: it keeps polling
until you approve or until the device code expires, so abandoning a
sign-in leaves the run waiting rather than failing fast. It gives up
once, quietly, when the code expires — it does not open a second tab.
sign-in leaves the run waiting rather than failing fast. The tab is
opened only when someone is at the machine to use it — a refresh that
comes due while you are away holds the sign-in silently and opens the tab
when you are back, so tabs never collect overnight however many times
Frost tries.
</p>
<p><a href="settings-behavior.html#login-page">Behavior settings →</a></p>

Expand Down Expand Up @@ -199,20 +202,25 @@ <h2 id="timeouts">Timeouts and cancellation</h2>
<tr><td>Close the login window</td><td>Aborts the run at once and records "Login window closed"</td></tr>
<tr><td>Ignore it (in-app window)</td><td>Polls until AWS expires the device code, then records "Login timed out"</td></tr>
<tr><td>Ignore it (default browser)</td><td>The same — Frost has no window to watch, so it polls the code out</td></tr>
<tr><td>Ignore the notification (notify mode)</td><td>Waits for the hotkey or a click until the device code expires, then gives up</td></tr>
<tr><td>Ignore the notification (notify mode)</td><td>Waits for the hotkey or a click until the device code expires, then records "Timed out waiting for user to trigger auth"</td></tr>
<tr><td>Walk away before it comes due</td><td>Runs the refresh anyway, but silently: if the page turns out to need you, Frost holds it — hidden, still being driven — and shows it when you are back</td></tr>
<tr><td>Refuse the sign-in at your identity provider</td><td>Ends the run as soon as AWS reports it, rather than polling on</td></tr>
</tbody>
</table>
</div>
<p>
In every case the failure is recorded on the
<a href="activity.html">Activity</a> page — and then Frost waits for you.
A sign-in nobody completed is not retried on a timer: the retry would
open another login page with nobody there to finish it, and by morning
you would have one for every few minutes you were away. The tray menu
says <em>Sign-in needed</em>, and a notification says so once — unless
you closed the window or refused the sign-in yourself, in which case you
already know. The next attempt is the one you start, from the tray, the
<a href="activity.html">Activity</a> page. What happens next depends on
who ended it. A sign-in <em>nobody</em> completed is replaced straight
away — no pause, no backoff — so there is always a live device code where
the last one died, and a refresh that came due in the night is a login
page waiting for you rather than a dead one. The tray menu says
<em>Sign-in needed — waiting for you</em> while Frost is holding one.
</p>
<p>
Closing the login window or refusing the sign-in at your identity
provider is different: that is you saying "not now", so Frost stops,
quietly, and the next attempt is the one you start from the tray, the
Credentials page or the hotkey.
</p>

Expand Down
5 changes: 4 additions & 1 deletion docs/docs/settings-behavior.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ <h3>Notification + hotkey</h3>
<p>
The cost is that credentials stay expired until you act — commands fail
with <code>ExpiredToken</code> in the meantime — and if you never act, the
run gives up when AWS expires the device code and tries again later.
run ends when AWS expires the device code and Frost posts a fresh
notification for the sign-in that replaces it. A refresh that comes due
while you are away waits for you rather than notifying an empty room, so
the code behind the notification you do get is always a live one.
</p>
<div class="callout">
<span class="callout-icon" aria-hidden="true">💡</span>
Expand Down
41 changes: 33 additions & 8 deletions docs/docs/troubleshooting.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,53 @@ <h2 id="login">Sign-in fails or never completes</h2>
<h4>"Login window closed"</h4>
<p>
The login window was closed before AWS confirmed the approval. Frost
treats that as "not now" and aborts the run. It does not start another
one on its own — trigger a refresh from the tray or the Credentials page
treats that as "not now" and aborts the run. This is the one ending it
does not retry — trigger a refresh from the tray or the Credentials page
when you are ready.
</p>

<h4>"Login timed out"</h4>
<p>
The device code AWS issued expired before the sign-in was approved. Frost
stops there and the tray menu shows <em>Sign-in needed</em>; start
another refresh when you are back. If this happens repeatedly in notify
mode, you may simply not be getting the notification —
The device code AWS issued expired before the sign-in was approved — ten
minutes, typically. Frost starts another sign-in immediately, so a run
like this repeating every ten minutes while you were away is Frost
working as intended, not a fault: each entry is one device code's life.
If it happens repeatedly in notify mode while you <em>are</em> at the
machine, you may simply not be getting the notification —
<a href="#notifications">see below</a>.
</p>

<h4>"Nobody is at the machine, and this login needs the user"</h4>
<p>
Only in <strong>Notification + hotkey</strong> mode, or with automatic
approval turned off: neither can get through without you, so rather than
ask AWS for a device code nobody can redeem, Frost waits and starts the
sign-in when you are back — within half a minute of your first keypress.
Nothing is wrong, and there is one entry for it rather than one every
half minute.
</p>

<h4>A pile of login tabs or windows waiting for me</h4>
<p>
Fixed in the version after 0.1.0. A failed sign-in used to be retried
twice a second from the moment the token expired, and in
<strong>Default browser</strong> mode each attempt opened a tab, so a
machine left alone overnight collected one every few minutes. Frost now
waits for you after a sign-in nobody completed, and only retries by
itself for failures that never put anything on your screen.
opens a login page only when somebody is at the machine to use it: the
sign-ins it runs while you are away are held silently, so trying again —
which it now does continuously — costs you nothing on screen.
</p>

<h4>An overnight refresh left expired credentials and a notification</h4>
<p>
Also fixed in the version after 0.1.0. Frost used to stop trying after a
sign-in nobody completed, so a refresh that came due at 3am was found in
the morning as a notification pointing at a device code that had died ten
minutes after it was issued — with <strong>Auto-open browser</strong> set,
which is precisely the setting that asks Frost not to leave it to you. A
dead code is now replaced immediately, for as long as it takes, so the
credentials come back on their own where they can and the first thing you
see when you sit down is a live login page where they cannot.
</p>

<h4>An AWS error naming the client or the endpoint</h4>
Expand Down
Loading