Files
logsdu/README.md

244 lines
11 KiB
Markdown
Raw Permalink Normal View History

2026-08-10 14:32:58 -03:00
# logsdu
2026-08-10 15:23:17 -03:00
A Firefox extension for logins that password managers cannot save: the ones
made of a registration number, a date of birth and a document number, instead
of a username and a password.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
Academic portals do this a lot. The form is usually marked `autocomplete="off"`,
none of the fields is a `password` field, and Bitwarden, Firefox and Chrome all
decline to remember it. When the values never change and the institution offers
no other way in, the only option left is copying three values by hand, every
single time -- including the registration number nobody has memorised.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
logsdu stores them once and fills them in. By default it also presses the submit
button, so the normal case is zero clicks.
2026-08-10 14:32:58 -03:00
## Install
2026-08-10 15:23:17 -03:00
Once it is published, from addons.mozilla.org. Until then, build it yourself:
2026-08-10 14:32:58 -03:00
```
corepack pnpm install # first time, or after a dependency change
make xpi # bundle and package dist/logsdu-<version>-firefox.xpi
2026-08-10 14:32:58 -03:00
```
2026-08-10 15:23:17 -03:00
`make xpi` prints the full path of the file and how to install it. The package
it produces is unsigned, and Firefox only accepts unsigned add-ons on the ESR,
Developer Edition and Nightly builds, after setting
`xpinstall.signatures.required` to `false` in `about:config`. On release Firefox
the file has to be signed by Mozilla first -- see "Publishing".
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
Dependencies are managed with **pnpm**, never npm. See "Why pnpm" below.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
## Setting it up
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
Open the extension's options page and fill in four values:
2026-08-10 14:32:58 -03:00
| Field | Example | Notes |
| --- | --- | --- |
| Portal address | `https://portal.example.br/` | Only the origin matters; the path is ignored |
| Registration number | `2000101010` | Digits only |
2026-08-10 15:23:17 -03:00
| Date of birth | `01/01/2000` | Reformatted as you type |
| Document number | `000.000.000-00` | Reformatted as you type |
2026-08-10 14:32:58 -03:00
Paste raw digits if you like -- the options page inserts the separators, because
2026-08-10 15:23:17 -03:00
input masks on these forms expect the values in exactly that shape.
When you press Save, Firefox asks whether logsdu may access the address you
entered. That prompt names one site. Accept it and the extension starts working
there; decline and nothing is stored as usable.
If the portal's markup differs from the common shape, open **Ajustes avançados**
in the options page and adjust the CSS selectors. Invalid selectors are rejected
on save rather than failing silently later.
## What it can access
Nothing, until you say so.
The manifest requests **no host permissions at all**. There is no content script
declared against any site. When you save an address, the extension asks for that
single origin through `permissions.request()`, and a background script then
registers the content script for that one origin and no other.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
That means a fresh install can read no pages, the permission prompt names one
site, and you can revoke it whenever you like in `about:addons` -> Permissions.
Clearing your data in the options page hands the permission back automatically.
The `optional_host_permissions` entry in the manifest is `*://*/*`, because the
address is not known until you type it. It is the set the extension may *ask*
from, not what it holds -- nothing is granted without your click, and what is
granted is one origin.
2026-08-10 14:32:58 -03:00
## How it behaves
- **Fills and submits** on the login page, with no interaction.
2026-08-10 15:23:17 -03:00
- **At most one automatic submit per hour.** After an attempt it drops back to
filling only, so a wrong value cannot resubmit itself on every page load and
lock you out of your account. Correct the values and save; saving clears the
2026-08-10 14:32:58 -03:00
timer, so the next visit tries again immediately.
- **Logging out keeps you logged out.** Clicking the portal's logout control
suppresses the automatic submit for five minutes -- otherwise the logout
redirect lands on the login page and you would be signed straight back in.
- When it fills without submitting, a small note at the bottom of the page says
why. Click it to dismiss.
- The toolbar popup has a **Preencher agora** button that fills without
submitting, for when you want to check the values before sending them.
- Automatic submission can be turned off entirely in the options.
2026-08-10 15:23:17 -03:00
## Where your data goes
Nowhere. It is written to `storage.local`: private to your browser profile,
never synced, never transmitted. The extension makes no network requests of its
own and contains no analytics.
Be clear about the limit, though. This is the same protection a browser-saved
password gets, and it has the same weakness -- anyone with your unlocked
computer can read it. If you need protection at rest, this is the wrong tool.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
The interface is in Portuguese, matching the portals it was written for.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
## Publishing
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
`make xpi` produces the file to upload at
[addons.mozilla.org](https://addons.mozilla.org/developers/addon/submit/).
Two distribution choices:
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
- **Listed** -- public on addons.mozilla.org, searchable, installable by anyone,
and updates are delivered by Mozilla automatically.
- **Unlisted** -- signed but not published. You distribute the signed file
yourself. Updates need a self-hosted update manifest, or resending the file.
Builds are never minified, which is deliberate: AMO requires a separate
source-code submission for any add-on whose uploaded code is machine-generated,
and that obligation would apply to every future release. The whole extension is
about 33 KB, so the saving would not pay for the process, and readable code is
easier for a reviewer -- or anyone auditing what handles their credentials -- to
check.
2026-08-10 14:32:58 -03:00
## Chrome
2026-08-10 15:49:01 -03:00
```
make chrome # unpacked Chrome build in build/chrome/
2026-08-10 15:49:01 -03:00
make crx # package it as dist/logsdu-<version>-chrome.zip
```
Load `build/chrome/` via `chrome://extensions` -> Developer mode -> **Load
unpacked** (select the folder itself), or upload the zip at
2026-08-10 15:49:01 -03:00
[the Web Store dashboard](https://chrome.google.com/webstore/devconsole).
One codebase, two manifests. Chrome MV3 requires a background *service worker*
and rejects Firefox's event-page `background.scripts`; Firefox needs the gecko
block that Chrome has no use for. `esbuild.config.mjs` writes the right manifest
per target, so the port is a build flag rather than a fork. Everything else --
the `chrome.*` namespace, MV3, the permission model -- is shared.
2026-08-10 15:23:17 -03:00
2026-08-10 15:49:01 -03:00
Two things that differ in practice, both handled:
- **Icons must be raster.** Chrome does not accept SVG in `icons`, so the PNGs
in `icons/` are generated from `logsdu.svg` and both browsers use those.
- **Service workers have no `window` or `document`.** A stray reference through
a shared import would break Chrome only, silently, at runtime. `make smoke`
runs the built background bundle in a worker-shaped sandbox to catch that.
What has been verified: Chrome 151 loads the build without errors, and the
background bundle registers exactly one content script for exactly the
configured origin. What has **not** been verified is a real login against a live
portal in Chrome.
A caveat that applies to both browsers: the page-world filler is injected as a
`<script src>` tag, which a site's Content-Security-Policy can refuse. Portals
that send no CSP -- the common case for this kind of form -- are unaffected. A
portal that does would need the filler registered as a `MAIN` world content
script instead.
2026-08-10 14:32:58 -03:00
## Development
```
corepack pnpm install
make # everything: build and package both browsers
make firefox # build Firefox only -> build/firefox/
make chrome # build Chrome only -> build/chrome/
2026-08-10 14:32:58 -03:00
make test # unit tests
2026-08-10 15:49:01 -03:00
make smoke # background bundle under a service worker
corepack pnpm run dev # rebuild Firefox on change
make clean # remove build/, dist/ and stray packages
make distclean # clean, plus node_modules/
2026-08-10 14:32:58 -03:00
```
A bare `make` typechecks once, bundles for both browsers into `build/`, and
packages both into `dist/`. Packaging is only a zip of a directory that was
going to be built anyway, so it costs nothing and keeps `dist/` from drifting
out of step with the sources.
Each browser gets its own directory under `build/`, so the two can coexist and
neither is ever left stale by the other. `dist/` holds the packages meant to be
published, and is kept out of `build/` so that packaging never tries to include
its own output.
2026-08-10 15:49:01 -03:00
2026-08-10 15:23:17 -03:00
While iterating, load the unpacked directory rather than reinstalling an `.xpi`
each time: `about:debugging` -> **This Firefox** -> **Load Temporary Add-on** ->
`build/firefox/manifest.json`, then press **Reload** there after each rebuild. That copy
2026-08-10 15:23:17 -03:00
disappears on restart, which is the point -- it is for development, not daily
use.
2026-08-10 14:32:58 -03:00
2026-08-10 15:23:17 -03:00
`corepack pnpm run dev` watches and rebuilds, static files included, but Firefox
still needs the Reload click to pick anything up.
2026-08-10 14:32:58 -03:00
### Layout
| Path | Role |
| --- | --- |
2026-08-10 15:23:17 -03:00
| `src/manifest.json` | MV3 manifest. Requests no host access |
| `src/background.ts` | Registers the content script for the granted origin |
2026-08-10 14:32:58 -03:00
| `src/content.ts` | Isolated world. Decides whether to act, then delegates |
| `src/injected.ts` | Page world. Does the actual filling and clicking |
2026-08-10 15:23:17 -03:00
| `src/portal.ts` | Default selectors and the shape of a fill request |
2026-08-10 14:32:58 -03:00
| `src/config.ts` | Stored values, the rate limit and the logout cooldown |
| `src/format.ts` | Input normalisers for the three masked fields |
| `src/options.*`, `src/popup.*` | The two bits of UI |
2026-08-10 15:49:01 -03:00
| `tools/sw-smoke.mjs` | Checks the background bundle survives a service worker |
2026-08-10 14:32:58 -03:00
### Why two scripts instead of one
2026-08-10 15:23:17 -03:00
These portals drive their inputs with Inputmask, which replaces each element's
`value` property with its own accessor. A content script assigning `input.value`
from the isolated world writes through Xrays to the *native* setter and skips
that accessor: the field looks right on screen, but the mask's buffer is
unchanged, and the page's submit handler reads the stale buffer back out through
jQuery. So the filling happens inside the page, through the page's own jQuery
and Inputmask, in `injected.ts`.
Submitting is a real click on the button, never `form.submit()`. These portals
intercept the submit event, cancel it, and post by AJAX with a CSRF token taken
from a meta tag. `form.submit()` would bypass that handler and lose the token; a
click reproduces exactly what a person pressing the button does.
### Why the field selectors are configurable
Hardcoding them would tie the extension to one institution while pretending to
be general. They live in `portal.ts` as defaults and can be overridden per
installation, so the same build works for any portal of this shape -- and no
institution is named anywhere in the source, the manifest or the build output.
2026-08-10 14:32:58 -03:00
### Why pnpm, not npm
**Do not run `npm install` or `npm ci` in this repo.** This project is developed
on **ZFS**, and npm's installer renames many directories in parallel while
hoisting and deduping, which ZFS intermittently fails with:
```
npm error code ENOTEMPTY
npm error syscall rename
```
pnpm hard-links packages from a global content-addressable store and does not
perform that rename dance, so it is unaffected. Only `npm install` / `npm ci`
are the problem -- running *scripts* through npm is fine, since that just spawns
tsc and esbuild. The lockfile is `pnpm-lock.yaml`; there is no
`package-lock.json` and none should be created.
## Licence
GPL-3.0-or-later. See `LICENSE`.