# logsdu A Firefox extension that logs you into an academic portal whose sign-in form is three fields -- registration number, birth date and national ID -- rather than the usual user and password. That shape defeats password managers. The form is marked `autocomplete="off"`, none of the three inputs is a `password` field, and Bitwarden, Firefox and Chrome all decline to remember it. Since the values never change and the institution does not let you pick a different login method, the only option left is copying three values by hand, every time. logsdu stores them once and fills them in. By default it also presses "Entrar", so the normal case is zero clicks. ## Install Dependencies are managed with **pnpm**, never npm (see "Why pnpm" below). Node 22+ ships `corepack`, so pnpm does not have to be installed globally. ``` corepack pnpm install # first time, or after a dependency change make # typecheck and bundle into build/ ``` Then load it into Firefox: 1. Open `about:debugging` 2. Click **This Firefox** 3. Click **Load Temporary Add-on** 4. Select `build/manifest.json` Open the extension's options page, fill in the four values, and save. A temporary add-on is dropped when Firefox restarts. See "Installing it permanently" for the fix. ## The four values | Field | Example | Notes | | --- | --- | --- | | Portal address | `https://portal.example.br/` | Only the origin matters; the path is ignored | | Registration number | `2000101010` | Digits only | | Birth date | `01/01/2000` | Reformatted as you type | | National ID | `000.000.000-00` | Reformatted as you type | Paste raw digits if you like -- the options page inserts the separators, because the portal's input masks expect the values in exactly that shape. **The portal address is configuration, not code.** No institution is named anywhere in the extension: not in the manifest, not in the source, not in the build output. Be clear about what that does and does not buy you. It stops someone who reads the extension from learning which portal it is for. It does not hide anything from someone who can read the extension's storage -- and that is the same access that would expose your national ID and birth date anyway. ## How it behaves - **Fills and submits** on the login page, with no interaction. - **At most one automatic submit per hour.** After an attempt, the extension drops back to filling only, so a wrong value cannot resubmit itself on every page load and lock you out. 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. ## How the values are stored In `storage.local`: private to this browser profile, never synced, never sent anywhere. That is the same protection a browser-saved password gets, and it has the same limit -- anyone with your unlocked account can read it. If that is not good enough for your threat model, this extension is the wrong tool. ## Installing it permanently Firefox only keeps unsigned add-ons until the next restart. Two ways around it: - **Sign it (recommended).** `make dist` produces `logsdu-.zip`. Upload it to [addons.mozilla.org](https://addons.mozilla.org) as an **unlisted** add-on: signing is automated, nothing is published publicly or reviewed by hand, and you install the signed `.xpi` it hands back. - **Turn signing off.** Firefox Developer Edition, Nightly or ESR will install unsigned add-ons with `xpinstall.signatures.required=false` in `about:config`. Release Firefox ignores that setting. ## Chrome Not yet. The source deliberately avoids anything Firefox-specific: it uses the `chrome.*` namespace, Manifest V3, and no APIs Chrome lacks, so a Chrome build should be a manifest question rather than a rewrite. It has not been tried, so do not assume it works. ## Development ``` corepack pnpm install make test # unit tests make # typecheck and bundle corepack pnpm run dev # rebuild on change make clean ``` After a rebuild, press **Reload** next to the extension in `about:debugging`. `corepack pnpm run dev` watches and rebuilds, including the static files, but Firefox still needs that Reload click to pick anything up. ### Layout | Path | Role | | --- | --- | | `src/manifest.json` | MV3 manifest. Names no site | | `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` | Every selector the extension knows about the form | | `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 | ### Why two scripts instead of one The portal drives its 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. Submitting is a real click on the button, never `form.submit()`. The portal intercepts the submit event, cancels it, and posts 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 "Entrar" does. ### Why the content script matches every URL The address is configured at runtime, so it cannot also be a manifest match pattern -- putting it there is exactly what would name the institution in the shipped code. The script therefore loads everywhere and stops immediately unless the page's origin equals the configured one. Origins are compared whole, so `portal.example.br.evil.tld` does not match. The honest cost: the extension holds read access to every page you visit. The alternative -- registering the content script at runtime with `scripting.registerContentScripts` and an optional host permission -- avoids that at the price of a background script and a permission prompt. ### 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`.