Skip to content

Tell a first run that there is a browser half - #75

Merged
sotashimozono merged 5 commits into
mainfrom
first-run-names-the-extension
Sep 15, 2026
Merged

sotashimozono merged 5 commits into
mainfrom
first-run-names-the-extension

Conversation

@sotashimozono

Copy link
Copy Markdown
Member

Nothing did. A first run printed a token, the routes, a PAC flag and a loopback URL — and a reader following either of the last two got something that works worse than the product, with no way to learn a better one existed. The routes it lists are names the extension makes resolve; without it they resolve to nothing, which reads as the daemon being broken.

Before:

point the browser at the generated PAC, for example:
  chrome --proxy-pac-url=http://127.0.0.1:7391/proxy.pac

or, without touching proxy settings: http://127.0.0.1:7391/

After:

the browser half is a small extension, and it is what makes
http://<alias>.ssh-browser/ resolve at all. Install it once:
  https://github.com/QAtlasHub/ssh-browser#the-extension
It finds this daemon and takes the token itself — there is nothing to
paste, and nothing to do again after a restart.

Without it, a browser can be pointed at the PAC by hand:
  chrome --proxy-pac-url=http://127.0.0.1:7391/proxy.pac
or read everything on one origin at http://127.0.0.1:7391/

Named by the suffix rather than "those names above", because with no aliases open there is nothing above and the sentence would point at blank space.

Two stale token lines

One said "paste it into the extension once". There has been nothing to paste since the extension started fetching the token itself (background.ts:174), and run.mjs:784 asserts the dashboard has no field for it. An instruction nobody can follow is worse than none.

The other claimed a reused token meant "a browser holding it is still connected" — which is not what a file existing means.

And a check that could not fail

Fixing those shifted the write timing and three e2e checks failed at once:

FAIL  a first https run says the authority is not trusted yet
FAIL  and names the command that fixes it
FAIL  and where the certificate is

They were right to. startDaemon returned as soon as it saw listening on, and the trust advice is printed after that line — so those three were passing on whether one write had landed yet. A check that can go green while the daemon said nothing is not a check.

The harness now waits for the last line of the banner. Nothing between it and listening on can fail, so it is a fact rather than a delay.

228 tests, 63 e2e checks against a real host, fmt/clippy clean.

souta registered the developer account and asked whether CI could take it from
there. Mostly yes, and the line is not where you would guess.

The Chrome Web Store API cannot create a listing. Description, screenshots,
category and the data disclosure are not reachable from it, and the extension ID
does not exist until the Store listing and Privacy tabs have been filled in by a
person. So the first submission is by hand and always will be. Every update
after it is a package upload and a publish, and both are API calls.

`store.yml` does those on a published release. The human gate is not removed, it
moves: merging the release PR is the decision, and what reaches the store is the
package that was built, tested and attached to that release rather than one
dragged into a browser.

A service account rather than a refresh token. A refresh token issued while the
OAuth consent screen is still in "Testing" expires after a week, so the pipeline
would work today and fail next month having changed nothing. v2 of the API takes
service accounts, which do not expire; the JSON key is one secret and the
dashboard takes the account's email under Account.

It refuses before authenticating when a secret is missing, and names which. An
authentication error twenty lines into a log is the same fact told worse.

Two things the docs say and experience will otherwise teach: the API does not set
visibility, so an item whose visibility was changed by hand must be published by
hand once before the API will do it again; and a version cannot be uploaded
twice, which is what the `versions agree` check has been protecting all along.

The endpoints and the service-account flow are from the Chrome for Developers
documentation rather than memory, and the action SHA resolves to the v3.0.0 tag
rather than the moving `v3`.

Signed-off-by: Souta <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
The form asks whether the extension uses remote code and the answer is no, but
it is not an obvious no and the reason was nowhere.

`applyPac` fetches `http://127.0.0.1:<port>/proxy.pac` from the user's own
daemon and hands the text to `chrome.proxy` as `pacScript.data`. Chrome defines
remotely hosted code as "anything that is executed by the browser that is loaded
from someplace other than the extension's own files", and a PAC is JavaScript.
So the case that could contradict the answer exists, and a reviewer reading
seven kilobytes of `background.js` will see it.

It is still no: the PAC runs in the browser's network stack rather than in the
extension, `chrome.proxy` is a first-party API whose documented input is exactly
this, and it arrives from loopback rather than from a server the author
controls -- which is the thing the policy exists to prevent.

The fallback is scoped here too, because the moment to work it out is not the
moment a rejection arrives. The PAC is a function of the suffix and the port and
the extension knows both, so generating it locally settles the question without
losing what the fetch was for: the suffix stays data the daemon reports, so
changing it still needs no extension release.

Signed-off-by: Souta <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
Measured rather than eyeballed: the 128 held 120x120 of artwork with 4 pixels
of padding, where the listing wants 96x96 with 16 on every side. The store
draws its own rounded frame around whatever it is given, so a tile that fills
the canvas has that frame drawn across its corners.

Only the 128, because the rule is the store's. The toolbar icons want the
opposite -- padding at sixteen pixels across is wasted pixels -- so 16, 32 and
48 still fill their square.

Still one drawing. `fill` divides the sampled coordinate, so a smaller value
takes the same shape from further out and lands it smaller in the same canvas;
it is not a second set of numbers to keep in step with the first.

  icon-16   canvas 16x16    artwork 16x16   padding 0
  icon-32   canvas 32x32    artwork 30x30   padding 1
  icon-48   canvas 48x48    artwork 46x46   padding 1
  icon-128  canvas 128x128  artwork 96x96   padding 16

Re-running still produces byte-identical files, which is what keeps a
regenerated icon out of every diff that touches the extension.

Signed-off-by: Souta <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
souta: a bookmark that resolves only after you remember to run `ssh-browser
serve` is not a bookmark. The daemon is not a program anyone wants to run -- it
is what makes a URL work -- so it should already be there.

`ssh-browser autostart` registers it, `--off` takes it back out, and both say
what they touched. No administrator rights on any of the three platforms.

It does the thing rather than printing a command, which is the opposite of
`trust` ten lines above it in `main.rs`. The difference is consent, not effort:
trusting a root changes what the whole machine believes, so that one is a
decision to make with your own hands. Starting a program of your own at login is
what was just asked for, and handing back a command to paste would be the same
failure more politely.

Platforms are a parameter rather than a `cfg!`, for the reason already written
out in `tls::Store`. The plan is data -- files to write, commands to run, paths
to delete -- so all three are checked on one machine, and only `apply` touches
anything.

Windows does not use Task Scheduler, and that is a measurement rather than a
preference. `schtasks /Create /SC ONLOGON` writes to the machine's task store:

  Error: schtasks failed: エラー: アクセスが拒否されました。

Asking somebody to open an elevated prompt in order to start a program of their
own is the thing this command exists to avoid. The Startup folder needs nothing,
is where Windows documents that login programs go, and is undone by deleting a
file you can see. It holds one line of VBScript because Task Scheduler and that
folder both run a console program in a console window, and a window nobody asked
for is a window somebody closes -- which kills the daemon.

Run end to end on the machine it was written for, through the same path a login
takes: no visible window, `http://ssh-browser/` and `http://panza.ssh-browser/`
both 200, `--off` removes it, and installing twice is installing once.

Signed-off-by: Souta <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
Nothing did. A first run printed a control token, the routes, a PAC flag and a
loopback URL — and a reader following either of the last two got something that
works worse than the product with no way to learn there was a better one. The
routes it lists are names the extension makes resolve; without it they resolve
to nothing, which reads as the daemon being broken.

So the banner names the extension first and the two fallbacks after, as
fallbacks. It is named by the suffix rather than as "those names above", because
with no aliases open there is nothing above and the sentence would point at
blank space.

The token lines were stale in a way that mattered. One said "paste it into the
extension once", and there has been nothing to paste since the extension started
fetching the token itself — there is an e2e check asserting the dashboard has no
field for it. An instruction nobody can follow is worse than none. The other
claimed a reused token meant "a browser holding it is still connected", which is
not what a file existing means.

Fixing those shifted the write timing and three e2e checks failed at once. They
were right to: `startDaemon` returned at `listening on`, and the trust advice is
printed after it, so the three checks on the https banner were passing on
whether one write had landed yet. A check that can go green while the daemon
said nothing is not a check. The harness now waits for the last line of the
banner, which nothing between it and `listening on` can fail before.

Signed-off-by: Souta <shimozono-sota631@g.ecc.u-tokyo.ac.jp>
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.56627% with 111 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/ssh-browser/src/main.rs 0.00% 66 Missing ⚠️
crates/ssh-browser/src/autostart/mod.rs 83.08% 45 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sotashimozono
sotashimozono merged commit 8e757f9 into main Sep 15, 2026
20 checks passed
@sotashimozono
sotashimozono deleted the first-run-names-the-extension branch September 15, 2026 03:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant