logsdu v0.1 skeleton

This commit is contained in:
2026-08-10 14:25:30 -03:00
commit 436130ecb9
11 changed files with 1142 additions and 0 deletions

103
src/format.ts Normal file
View File

@@ -0,0 +1,103 @@
// *************************************************************************
// * (C)opyright 2026 by Ruben Carlo Benante *
// * *
// * This program is free software; you can redistribute it and/or modify *
// * it under the terms of the GNU General Public License as published by *
// * the Free Software Foundation, either version 3 of the License, or *
// * (at your option) any later version. *
// * *
// * This program is distributed in the hope that it will be useful, *
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
// * GNU General Public License for more details. *
// * *
// * You should have received a copy of the GNU General Public License *
// * along with this program. If not, see http://www.gnu.org/licenses/. *
// * *
// * Contact author at: *
// * Ruben Carlo Benante *
// * rcb@beco.cc *
// *************************************************************************
// The portal drives its three inputs with Inputmask, so the values we store
// have to already be in the shape the masks expect (the placeholders on the
// page: "2000101010", "01/01/2000", "000.000.000-00"). Normalising here means
// the options page accepts a raw paste of digits and stores something the
// masks will not fight with.
/** Strip everything that is not a digit. */
export function digits(value: string): string {
return value.replace(/\D+/g, '');
}
/** Registration number: digits only, at most 10. */
export function formatRa(value: string): string {
return digits(value).slice(0, 10);
}
/** Birth date as dd/mm/yyyy, formatted progressively while typing. */
export function formatDate(value: string): string {
const d = digits(value).slice(0, 8);
const parts: string[] = [];
if (d.length > 0) parts.push(d.slice(0, 2));
if (d.length > 2) parts.push(d.slice(2, 4));
if (d.length > 4) parts.push(d.slice(4, 8));
return parts.join('/');
}
/** CPF as 000.000.000-00, formatted progressively while typing. */
export function formatCpf(value: string): string {
const d = digits(value).slice(0, 11);
let out = d.slice(0, 3);
if (d.length > 3) out += `.${d.slice(3, 6)}`;
if (d.length > 6) out += `.${d.slice(6, 9)}`;
if (d.length > 9) out += `-${d.slice(9, 11)}`;
return out;
}
export function isCompleteRa(value: string): boolean {
return digits(value).length > 0;
}
/**
* A complete, calendar-valid date. Deliberately checks the round trip rather
* than just the digit count, so 31/02/2000 is rejected before it ever reaches
* the portal.
*/
export function isCompleteDate(value: string): boolean {
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value);
if (!m) return false;
const day = Number(m[1]);
const month = Number(m[2]);
const year = Number(m[3]);
const date = new Date(year, month - 1, day);
return (
date.getFullYear() === year &&
date.getMonth() === month - 1 &&
date.getDate() === day
);
}
export function isCompleteCpf(value: string): boolean {
return digits(value).length === 11;
}
/**
* Origin of a user-typed site address, or null if it cannot be parsed.
* Accepts input without a scheme ("saladigital.example.com") by assuming
* https, which is what someone pasting an address from the URL bar expects.
*/
export function toOrigin(value: string): string | null {
const trimmed = value.trim();
if (!trimmed) return null;
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
? trimmed
: `https://${trimmed}`;
try {
const url = new URL(withScheme);
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
return url.origin;
} catch {
return null;
}
}

43
src/manifest.json Normal file
View File

@@ -0,0 +1,43 @@
{
"manifest_version": 3,
"name": "logsdu",
"version": "0.1.0",
"description": "Fills and submits a three-field academic portal login.",
"icons": {
"48": "icons/logsdu-48.png",
"96": "icons/logsdu-96.png"
},
"browser_specific_settings": {
"gecko": {
"id": "logsdu@beco.cc",
"strict_min_version": "115.0",
"data_collection_permissions": {
"required": ["none"]
}
}
},
"permissions": ["storage"],
"host_permissions": ["*://*/*"],
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content.js"],
"run_at": "document_idle",
"all_frames": false
}
],
"web_accessible_resources": [
{
"resources": ["injected.js"],
"matches": ["*://*/*"]
}
],
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"action": {
"default_popup": "popup.html",
"default_title": "logsdu"
}
}

71
src/portal.ts Normal file
View File

@@ -0,0 +1,71 @@
// *************************************************************************
// * (C)opyright 2026 by Ruben Carlo Benante *
// * *
// * This program is free software; you can redistribute it and/or modify *
// * it under the terms of the GNU General Public License as published by *
// * the Free Software Foundation, either version 3 of the License, or *
// * (at your option) any later version. *
// * *
// * This program is distributed in the hope that it will be useful, *
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
// * GNU General Public License for more details. *
// * *
// * You should have received a copy of the GNU General Public License *
// * along with this program. If not, see http://www.gnu.org/licenses/. *
// * *
// * Contact author at: *
// * Ruben Carlo Benante *
// * rcb@beco.cc *
// *************************************************************************
// Every selector the extension knows about the target page, in one place.
//
// These describe a form shape, not a site: the address itself is configured
// at runtime and lives only in local storage, so nothing here names the
// institution.
/**
* The login form. Every field lookup is scoped to it on purpose.
*
* The same page carries a "forgot your registration number" modal whose
* inputs are id="CPF" and id="DTNASC". The three login inputs have no id at
* all, so an unscoped lookup for a CPF field finds the modal's copy and
* writes the value into the wrong form.
*/
export const FORM = 'form[name="LoginAP"]';
/** The three credential inputs, relative to FORM. */
export const FIELDS = {
ra: 'input[name="RA"]',
dn: 'input[name="DN"]',
cpf: 'input[name="CPF"]',
} as const;
/** The "Entrar" button, relative to FORM. */
export const SUBMIT = 'button[type="submit"]';
/**
* The logout control, present on the pages behind the login. Clicking it is
* the signal that the user wants to stay logged out, which suppresses the
* auto-submit that would otherwise fire the moment the logout redirect lands
* back on the login page.
*/
export const LOGOUT = '.js_logout';
/** Event names used to talk to the page-world filler. */
export const EVENT_RESULT = 'logsdu:result';
/** Payload handed to the page-world script through its own dataset. */
export interface FillRequest {
ra: string;
dn: string;
cpf: string;
submit: boolean;
}
export interface FillResult {
filled: boolean;
submitted: boolean;
error?: string;
}

67
src/webext.d.ts vendored Normal file
View File

@@ -0,0 +1,67 @@
// *************************************************************************
// * (C)opyright 2026 by Ruben Carlo Benante *
// * *
// * This program is free software; you can redistribute it and/or modify *
// * it under the terms of the GNU General Public License as published by *
// * the Free Software Foundation, either version 3 of the License, or *
// * (at your option) any later version. *
// * *
// * This program is distributed in the hope that it will be useful, *
// * but WITHOUT ANY WARRANTY; without even the implied warranty of *
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
// * GNU General Public License for more details. *
// * *
// * You should have received a copy of the GNU General Public License *
// * along with this program. If not, see http://www.gnu.org/licenses/. *
// * *
// * Contact author at: *
// * Ruben Carlo Benante *
// * rcb@beco.cc *
// *************************************************************************
// Minimal ambient declarations for the slice of the WebExtension API this
// project uses. Hand-written on purpose: pulling in @types/chrome costs a
// dependency and several megabytes for about a dozen call signatures.
//
// The `chrome` namespace is used rather than `browser` because it exists in
// both Chrome and Firefox under MV3, and under MV3 Firefox also returns
// promises from it. That keeps a future Chrome build a manifest concern
// rather than a source concern.
declare namespace chrome {
namespace runtime {
const lastError: { message?: string } | undefined;
function getURL(path: string): string;
function openOptionsPage(): Promise<void>;
const onMessage: {
addListener(
callback: (
message: unknown,
sender: unknown,
sendResponse: (response?: unknown) => void,
) => boolean | undefined | void,
): void;
};
}
namespace storage {
interface StorageArea {
get(keys: string | string[] | null): Promise<Record<string, unknown>>;
set(items: Record<string, unknown>): Promise<void>;
remove(keys: string | string[]): Promise<void>;
}
const local: StorageArea;
}
namespace tabs {
interface Tab {
id?: number;
url?: string;
}
function query(queryInfo: {
active?: boolean;
currentWindow?: boolean;
}): Promise<Tab[]>;
function sendMessage(tabId: number, message: unknown): Promise<unknown>;
}
}