2026-08-10 15:49:01 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 14:25:30 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 14:25:30 -03:00
2026-08-10 14:25:30 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 15:49:01 -03:00
2026-08-10 14:32:58 -03:00
2026-08-10 15:49:01 -03:00

logsdu

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.

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.

logsdu stores them once and fills them in. By default it also presses the submit button, so the normal case is zero clicks.

Install

Once it is published, from addons.mozilla.org. Until then, build it yourself:

corepack pnpm install     # first time, or after a dependency change
make xpi                  # typecheck, bundle, and package dist/logsdu-<version>.xpi

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".

Dependencies are managed with pnpm, never npm. See "Why pnpm" below.

Setting it up

Open the extension's options page and fill in four values:

Field Example Notes
Portal address https://portal.example.br/ Only the origin matters; the path is ignored
Registration number 2000101010 Digits only
Date of birth 01/01/2000 Reformatted as you type
Document number 000.000.000-00 Reformatted as you type

Paste raw digits if you like -- the options page inserts the separators, because 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.

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.

How it behaves

  • Fills and submits on the login page, with no interaction.
  • 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 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.

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.

The interface is in Portuguese, matching the portals it was written for.

Publishing

make xpi produces the file to upload at addons.mozilla.org. Two distribution choices:

  • 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.

Chrome

make chrome               # unpacked Chrome build in build/
make crx                  # package it as dist/logsdu-<version>-chrome.zip

Load build/ via chrome://extensions -> Developer mode -> Load unpacked, or upload the zip at the Web Store dashboard.

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.

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.

Development

corepack pnpm install
make test                 # unit tests
make smoke                # background bundle under a service worker
make                      # typecheck and bundle into build/
make packages             # both publishable packages into dist/
corepack pnpm run dev     # rebuild on change
make clean

build/ is the unpacked extension and is regenerated from scratch every time. dist/ holds the packages meant to be published, and is kept separate so that packaging never tries to include its own output.

While iterating, load the unpacked directory rather than reinstalling an .xpi each time: about:debugging -> This Firefox -> Load Temporary Add-on -> build/manifest.json, then press Reload there after each rebuild. That copy disappears on restart, which is the point -- it is for development, not daily use.

corepack pnpm run dev watches and rebuilds, static files included, but Firefox still needs the Reload click to pick anything up.

Layout

Path Role
src/manifest.json MV3 manifest. Requests no host access
src/background.ts Registers the content script for the granted origin
src/content.ts Isolated world. Decides whether to act, then delegates
src/injected.ts Page world. Does the actual filling and clicking
src/portal.ts Default selectors and the shape of a fill request
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
tools/sw-smoke.mjs Checks the background bundle survives a service worker

Why two scripts instead of one

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.

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.

Description
Auto login into university portal
Readme GPL-3.0 285 KiB
release 0.3.2 Latest
2026-08-11 11:23:04 -03:00
Languages
TypeScript 68.8%
JavaScript 13.1%
Makefile 10.4%
HTML 5.1%
CSS 2.6%