From b1d295ad44f352949517b3c7d09657d17d70bd97 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:32:28 -0700 Subject: [PATCH 01/12] Add Wildcard Host Matching --- AGENTS.md | 47 +++++++++++--- src/entrypoints/auth/App.vue | 23 ++++++- src/entrypoints/background/auth.ts | 21 +++++- src/entrypoints/content/index.ts | 45 +++++++++++-- src/entrypoints/popup/App.vue | 2 + src/utils/hosts.ts | 100 +++++++++++++++++++++++++---- 6 files changed, 209 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c093baa..5bb8931 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,18 +3,45 @@ Auto Auth is a Web Extension to allow automatic HTTP Basic Authentication. - Chrome + Firefox + Firefox Android all using MV3 -- WXT Framework with TypeScript + Vue3 +- WXT (https://wxt.dev/) Framework https://github.com/wxt-dev/wxt +- TypeScript 6 with Vue 3.5 - Bootstrap 5.3 and FontAwesome -## Application Structure +## Project Structure -- The `@/` import resolves to `src/` -- Manifest generated from `wxt.config.ts` -- Entrypoint specific options in `src/entrypoints//index.html` -- Content script specific options in `src/entrypoints/content(.)/index.ts` -- Locales are in `src/locales` and use `@wxt-dev/i18n/module` in `yaml` format +IMPORTANT: Both `useOptions()` and `getOptions()` ALWAYS have DEFAULT values set! -## Style & Conventions +### Files -- Prettier: no semi, single quotes, printWidth 90 (120 for `.vue`). -- Custom logger `debug()` from `utils/logger.ts` use for all log output. +- `wxt.config.ts` - WXT Config and Extension Manifest +- `src/app.config.ts` - WXT Application Runtime Configuration +- `src/entrypoints/background/index.ts` Background Service Worker +- `src/entrypoints/content/index.ts` Content Script Entrypoint +- `src/assets/css/styles.scss` - Main SCSS styles +- `src/main.ts` - Imported by all `src/entrypoints/*/main.ts` files + +### Directories + +- `src` - Source directory for the web extension, WXT Framework. +- `src/entrypoints/**` - WXT entrypoints +- `src/locales` - YAML locale files using `@wxt-dev/i18n` +- `src/components` - Vue components +- `src/directives` - Vue directives + +## Commands + +ALWAYS use the `npm run *` command + +| Command | What it does | +| ------------------ | --------------------------------- | +| `npm run build` | `wxt build` to `.output` | +| `npm run build:ff` | build Firefox only (faster) | +| `npm run clean` | `rm -rf .output` | +| `npm run lint` | `npx eslint src` ESLint | +| `npm run tsc` | `vue-tsc --noEmit` TS Check | +| `npm run prepare` | `wxt prepare` Generate i18n Types | +| `npm run prettier` | ALWAYS RUN AFTER EDITING FILES | + +## Follow Existing Patterns + +Before adding or modifying any feature, search the codebase for the closest existing implementation and follow its full integration chain. Do not stop after writing the core logic — trace every file that touches the feature (imports, registration, configuration, UI binding) and ensure each is accounted for. diff --git a/src/entrypoints/auth/App.vue b/src/entrypoints/auth/App.vue index 62e3c6b..2c46f96 100644 --- a/src/entrypoints/auth/App.vue +++ b/src/entrypoints/auth/App.vue @@ -8,7 +8,7 @@ import { openOptions } from '@/utils/extension.ts' import { getSession, saveKeyValue } from '@/utils/options.ts' import { useOptions } from '@/composables/useOptions.ts' import { showToast } from '@/composables/useToast.ts' -import { Hosts } from '@/utils/hosts.ts' +import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' import ToastAlerts from '@/components/ToastAlerts.vue' import BackToTop from '@/components/BackToTop.vue' import OptionsOffscreen from '@/components/OptionsOffscreen.vue' @@ -135,6 +135,27 @@ onMounted(async () => { await nextTick() usernameEl.value?.select() passRef.value = password + } else { + let bestMatch: string | undefined + let bestSpecificity = Infinity + for (const [pattern, creds] of Object.entries(session)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(hostRef.value, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSpecificity) { + bestSpecificity = specificity + bestMatch = creds + } + } + } + if (bestMatch) { + debug('session wildcard match:', bestMatch) + const [username, password] = parseCreds(bestMatch) + userRef.value = username + await nextTick() + usernameEl.value?.select() + passRef.value = password + } } const link = document.querySelector('link[rel*="icon"]') diff --git a/src/entrypoints/background/auth.ts b/src/entrypoints/background/auth.ts index 56dee17..87e1000 100644 --- a/src/entrypoints/background/auth.ts +++ b/src/entrypoints/background/auth.ts @@ -1,6 +1,6 @@ import { parseCreds } from '@/utils/creds.ts' import { getOptions, getSession } from '@/utils/options.ts' -import { Hosts } from '@/utils/hosts.ts' +import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' // TODO: Logging @@ -97,6 +97,25 @@ async function processRequest( return asyncCallback({ authCredentials }) } + let bestSessionMatch: string | undefined + let bestSessionSpecificity = Infinity + for (const [pattern, creds] of Object.entries(session)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(url.host, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSessionSpecificity) { + bestSessionSpecificity = specificity + bestSessionMatch = creds + } + } + } + if (bestSessionMatch) { + console.log('%cSending Session Creds for:', 'color: SpringGreen', details.requestId) + const [username, password] = parseCreds(bestSessionMatch) + const authCredentials: chrome.webRequest.AuthCredentials = { username, password } + return asyncCallback({ authCredentials }) + } + // New Request Without Credentials console.log('%cNo Credentials for:', 'color: DeepSkyBlue', details.requestId) hijackRequest() diff --git a/src/entrypoints/content/index.ts b/src/entrypoints/content/index.ts index 1732f6d..906b15d 100644 --- a/src/entrypoints/content/index.ts +++ b/src/entrypoints/content/index.ts @@ -1,6 +1,6 @@ import { i18n } from '#imports' import { defineContentScript } from 'wxt/utils/define-content-script' -import { Hosts } from '@/utils/hosts.ts' +import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' // TODO: Logging @@ -27,11 +27,44 @@ export default defineContentScript({ async function onChanged(changes: Record) { // console.debug('content/index.ts - onChanged:', changes) - const items = changes[url.host[0]] // NOTE: Lazy Typing... in changes - if (!items) return - const oldCreds = items.oldValue?.[url.host] - const newCreds = items.newValue?.[url.host] - if (oldCreds !== newCreds) await processCreds(newCreds) + const exactItems = changes[url.host[0]] // NOTE: Lazy Typing... in changes + const wildcardItems = changes['*'] + + if (!exactItems && !wildcardItems) return + + if (exactItems) { + const oldCreds = exactItems.oldValue?.[url.host] + const newCreds = exactItems.newValue?.[url.host] + if (oldCreds !== newCreds) return await processCreds(newCreds) + } + + if (wildcardItems) { + const oldWildcard = findBestWildcardMatch(url.host, wildcardItems.oldValue) + const newWildcard = findBestWildcardMatch(url.host, wildcardItems.newValue) + if (oldWildcard !== newWildcard) await processCreds(newWildcard) + } +} + +function findBestWildcardMatch( + host: string, + patterns: Record | undefined, +): string | undefined { + if (!patterns) return undefined + let bestMatch: string | undefined + let bestSpecificity = Infinity + + for (const [pattern, creds] of Object.entries(patterns)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(host, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSpecificity) { + bestSpecificity = specificity + bestMatch = creds + } + } + } + + return bestMatch } async function processCreds(creds: any) { diff --git a/src/entrypoints/popup/App.vue b/src/entrypoints/popup/App.vue index fc719d2..e1d1204 100644 --- a/src/entrypoints/popup/App.vue +++ b/src/entrypoints/popup/App.vue @@ -72,6 +72,8 @@ onMounted(async () => { const creds = await Hosts.get(url.host) debug('creds:', creds) if (!creds) return debug('No Saved Creds for Host.') + const key = await Hosts.matchKey(url.host) + if (key) hostnameRef.value = key savedCreds.value = creds usernameRef.value = parseCreds(creds)[0] }) diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index 471dc59..661f16a 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -1,7 +1,39 @@ export type HostsRecord = Record +export function matchesWildcard(host: string, pattern: string): boolean { + const hostColon = host.indexOf(':') + const hostName = hostColon === -1 ? host : host.slice(0, hostColon) + const hostPort = hostColon === -1 ? undefined : host.slice(hostColon + 1) + + const patternColon = pattern.indexOf(':') + const patternName = patternColon === -1 ? pattern : pattern.slice(0, patternColon) + const patternPort = patternColon === -1 ? undefined : pattern.slice(patternColon + 1) + + if (patternPort !== undefined && patternPort !== '*') { + if (patternPort !== hostPort) return false + } + + const hostParts = hostName.split('.') + const patternParts = patternName.split('.') + if (patternParts.length !== hostParts.length) return false + + for (let i = 0; i < patternParts.length; i++) { + if (patternParts[i] === '*') { + if (!hostParts[i] || hostParts[i].length === 0) return false + } else if (patternParts[i] !== hostParts[i]) { + return false + } + } + + return true +} + +export function countWildcards(pattern: string): number { + return (pattern.match(/\*/g) || []).length +} + export class Hosts { - static readonly keys: string[] = [...'abcdefghijklmnopqrstuvwxyz0123456789'] + static readonly keys: string[] = [...'*abcdefghijklmnopqrstuvwxyz0123456789'] static async all(): Promise { const sync = await chrome.storage.sync.get(Hosts.keys) @@ -9,8 +41,40 @@ export class Hosts { } static async get(host: string): Promise { + const result = await Hosts.#lookup(host) + return result?.creds + } + + static async matchKey(host: string): Promise { + const result = await Hosts.#lookup(host) + return result?.key + } + + static async #lookup( + host: string, + ): Promise<{ key: string; creds: string } | undefined> { const sync = await Hosts.#getSync(host) - return sync[host] + const exact = sync[host] + if (exact) return { key: host, creds: exact } + + const all = await Hosts.all() + let bestKey: string | undefined + let bestCreds: string | undefined + let bestSpecificity = Infinity + + for (const [pattern, creds] of Object.entries(all)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(host, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSpecificity) { + bestSpecificity = specificity + bestKey = pattern + bestCreds = creds + } + } + } + + return bestKey ? { key: bestKey, creds: bestCreds! } : undefined } static async set(host: string, creds: string): Promise { @@ -49,18 +113,32 @@ export class Hosts { } } -// Above Code is Original - New Code Below - // NOTE: Moved from components/HostModal.vue and exported export function validateHostname(hostname: string): string | undefined { - // console.log('validateHostname:', hostname) + const value = hostname.toLowerCase().trim() + + if (value.includes('*')) { + const colonIndex = value.indexOf(':') + const hostPart = colonIndex === -1 ? value : value.slice(0, colonIndex) + const portPart = colonIndex === -1 ? undefined : value.slice(colonIndex + 1) + + if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) + return undefined + + const segments = hostPart.split('.') + if (segments.length === 0 || segments.some((s) => s === '')) return undefined + for (const segment of segments) { + if (segment === '*') continue + if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(segment)) return undefined + } + + return portPart !== undefined ? `${hostPart}:${portPart}` : hostPart + } + try { - let value = hostname - // console.log('value1:', value) - if (!value.includes('://')) value = `https://${value}` - // console.log('value2:', value) - const url = new URL(value) - // console.log(`url.hostname: "${url.hostname}"`, url) + let urlValue = value + if (!urlValue.includes('://')) urlValue = `https://${urlValue}` + const url = new URL(urlValue) return url.hostname } catch { // invalid hostname From 5ecae41fa3b2111643c66d07452cd34bfe55f907 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 11 Jul 2026 14:35:25 -0700 Subject: [PATCH 02/12] CLeanup duplication processRequest and auth/App.vue and export findBestWildcardMatch --- AGENTS.md | 2 +- src/entrypoints/auth/App.vue | 45 ++++++++++-------------------- src/entrypoints/background/auth.ts | 27 ++++-------------- src/entrypoints/content/index.ts | 24 +--------------- src/utils/hosts.ts | 20 +++++++++++++ 5 files changed, 41 insertions(+), 77 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5bb8931..43f9608 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ IMPORTANT: Both `useOptions()` and `getOptions()` ALWAYS have DEFAULT values set ## Commands -ALWAYS use the `npm run *` command +ALWAYS use the `npm run *` command NEVER pipe output into arbitrary truncation commands. | Command | What it does | | ------------------ | --------------------------------- | diff --git a/src/entrypoints/auth/App.vue b/src/entrypoints/auth/App.vue index 2c46f96..9765c1b 100644 --- a/src/entrypoints/auth/App.vue +++ b/src/entrypoints/auth/App.vue @@ -8,7 +8,7 @@ import { openOptions } from '@/utils/extension.ts' import { getSession, saveKeyValue } from '@/utils/options.ts' import { useOptions } from '@/composables/useOptions.ts' import { showToast } from '@/composables/useToast.ts' -import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' +import { Hosts, findBestWildcardMatch } from '@/utils/hosts.ts' import ToastAlerts from '@/components/ToastAlerts.vue' import BackToTop from '@/components/BackToTop.vue' import OptionsOffscreen from '@/components/OptionsOffscreen.vue' @@ -93,6 +93,14 @@ function saveCredsChange(event?: Event) { sessionStorage.setItem(hostRef.value, saveCreds.value ? '1' : '0') } +async function populateFields(credsStr: string) { + const [username, password] = parseCreds(credsStr) + userRef.value = username + await nextTick() + usernameEl.value?.select() + passRef.value = password +} + onMounted(async () => { // NOTE: Copied from VanillaJS... const searchParams = new URLSearchParams(window.location.search) @@ -117,44 +125,19 @@ onMounted(async () => { debug('session:', session) if (creds) { - debug('if creds:', creds) hasSavedCreds.value = true if (creds !== 'ignored') { - const [username, password] = parseCreds(creds) - userRef.value = username - debug('usernameEl.value:', usernameEl.value) - await nextTick() - usernameEl.value?.select() - passRef.value = password + debug('if creds:', creds) + await populateFields(creds) } } else if (hostRef.value in session) { debug('else hostRef.value in session:', hostRef.value) - const [username, password] = parseCreds(session[hostRef.value]) - userRef.value = username - debug('usernameEl.value:', usernameEl.value) - await nextTick() - usernameEl.value?.select() - passRef.value = password + await populateFields(session[hostRef.value]) } else { - let bestMatch: string | undefined - let bestSpecificity = Infinity - for (const [pattern, creds] of Object.entries(session)) { - if (!pattern.includes('*')) continue - if (matchesWildcard(hostRef.value, pattern)) { - const specificity = countWildcards(pattern) - if (specificity < bestSpecificity) { - bestSpecificity = specificity - bestMatch = creds - } - } - } + const bestMatch = findBestWildcardMatch(hostRef.value, session) if (bestMatch) { debug('session wildcard match:', bestMatch) - const [username, password] = parseCreds(bestMatch) - userRef.value = username - await nextTick() - usernameEl.value?.select() - passRef.value = password + await populateFields(bestMatch) } } diff --git a/src/entrypoints/background/auth.ts b/src/entrypoints/background/auth.ts index 87e1000..2942e5c 100644 --- a/src/entrypoints/background/auth.ts +++ b/src/entrypoints/background/auth.ts @@ -1,6 +1,6 @@ import { parseCreds } from '@/utils/creds.ts' import { getOptions, getSession } from '@/utils/options.ts' -import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' +import { Hosts, findBestWildcardMatch } from '@/utils/hosts.ts' // TODO: Logging @@ -89,33 +89,16 @@ async function processRequest( const session = await getSession() // console.log('session:', session) - if (url.host in session) { + // Find session creds (exact or wildcard match) + const sessionCreds = session[url.host] ?? findBestWildcardMatch(url.host, session) + if (sessionCreds) { console.log('%cSending Session Creds for:', 'color: SpringGreen', details.requestId) - const [username, password] = parseCreds(session[url.host]) + const [username, password] = parseCreds(sessionCreds) const authCredentials: chrome.webRequest.AuthCredentials = { username, password } // console.log('authCredentials:', authCredentials) return asyncCallback({ authCredentials }) } - let bestSessionMatch: string | undefined - let bestSessionSpecificity = Infinity - for (const [pattern, creds] of Object.entries(session)) { - if (!pattern.includes('*')) continue - if (matchesWildcard(url.host, pattern)) { - const specificity = countWildcards(pattern) - if (specificity < bestSessionSpecificity) { - bestSessionSpecificity = specificity - bestSessionMatch = creds - } - } - } - if (bestSessionMatch) { - console.log('%cSending Session Creds for:', 'color: SpringGreen', details.requestId) - const [username, password] = parseCreds(bestSessionMatch) - const authCredentials: chrome.webRequest.AuthCredentials = { username, password } - return asyncCallback({ authCredentials }) - } - // New Request Without Credentials console.log('%cNo Credentials for:', 'color: DeepSkyBlue', details.requestId) hijackRequest() diff --git a/src/entrypoints/content/index.ts b/src/entrypoints/content/index.ts index 906b15d..36392ce 100644 --- a/src/entrypoints/content/index.ts +++ b/src/entrypoints/content/index.ts @@ -1,6 +1,6 @@ import { i18n } from '#imports' import { defineContentScript } from 'wxt/utils/define-content-script' -import { Hosts, matchesWildcard, countWildcards } from '@/utils/hosts.ts' +import { Hosts, findBestWildcardMatch } from '@/utils/hosts.ts' // TODO: Logging @@ -45,28 +45,6 @@ async function onChanged(changes: Record) { } } -function findBestWildcardMatch( - host: string, - patterns: Record | undefined, -): string | undefined { - if (!patterns) return undefined - let bestMatch: string | undefined - let bestSpecificity = Infinity - - for (const [pattern, creds] of Object.entries(patterns)) { - if (!pattern.includes('*')) continue - if (matchesWildcard(host, pattern)) { - const specificity = countWildcards(pattern) - if (specificity < bestSpecificity) { - bestSpecificity = specificity - bestMatch = creds - } - } - } - - return bestMatch -} - async function processCreds(creds: any) { // console.debug('processCreds - tabEnabled:', tabEnabled, '- creds:', creds) if (creds) { diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index 661f16a..b1ff334 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -32,6 +32,26 @@ export function countWildcards(pattern: string): number { return (pattern.match(/\*/g) || []).length } +export function findBestWildcardMatch( + host: string, + patterns: Record | undefined, +): string | undefined { + if (!patterns) return undefined + let bestMatch: string | undefined + let bestSpecificity = Infinity + for (const [pattern, creds] of Object.entries(patterns)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(host, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSpecificity) { + bestSpecificity = specificity + bestMatch = creds + } + } + } + return bestMatch +} + export class Hosts { static readonly keys: string[] = [...'*abcdefghijklmnopqrstuvwxyz0123456789'] From 7fcbfb1c05e619b75840dd870a3c18a821b844d7 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:42:37 -0700 Subject: [PATCH 03/12] Cleanup matchesWildcard Add test-wildcard.ts --- src/utils/hosts.ts | 34 +++++++++++++--------------------- tests/test-wildcard.ts | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 tests/test-wildcard.ts diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index b1ff334..b9b177c 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -1,31 +1,25 @@ export type HostsRecord = Record -export function matchesWildcard(host: string, pattern: string): boolean { - const hostColon = host.indexOf(':') - const hostName = hostColon === -1 ? host : host.slice(0, hostColon) - const hostPort = hostColon === -1 ? undefined : host.slice(hostColon + 1) +function parseHostPort(value: string): [string, string | undefined] { + const colon = value.indexOf(':') + return colon === -1 ? [value, undefined] : [value.slice(0, colon), value.slice(colon + 1)] +} - const patternColon = pattern.indexOf(':') - const patternName = patternColon === -1 ? pattern : pattern.slice(0, patternColon) - const patternPort = patternColon === -1 ? undefined : pattern.slice(patternColon + 1) +export function matchesWildcard(host: string, pattern: string): boolean { + const [hostName, hostPort] = parseHostPort(host) + const [patternName, patternPort] = parseHostPort(pattern) - if (patternPort !== undefined && patternPort !== '*') { - if (patternPort !== hostPort) return false + if (patternPort !== undefined && patternPort !== '*' && patternPort !== hostPort) { + return false } const hostParts = hostName.split('.') const patternParts = patternName.split('.') if (patternParts.length !== hostParts.length) return false - for (let i = 0; i < patternParts.length; i++) { - if (patternParts[i] === '*') { - if (!hostParts[i] || hostParts[i].length === 0) return false - } else if (patternParts[i] !== hostParts[i]) { - return false - } - } - - return true + return patternParts.every( + (part, i) => part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i], + ) } export function countWildcards(pattern: string): number { @@ -138,9 +132,7 @@ export function validateHostname(hostname: string): string | undefined { const value = hostname.toLowerCase().trim() if (value.includes('*')) { - const colonIndex = value.indexOf(':') - const hostPart = colonIndex === -1 ? value : value.slice(0, colonIndex) - const portPart = colonIndex === -1 ? undefined : value.slice(colonIndex + 1) + const [hostPart, portPart] = parseHostPort(value) if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) return undefined diff --git a/tests/test-wildcard.ts b/tests/test-wildcard.ts new file mode 100644 index 0000000..f695763 --- /dev/null +++ b/tests/test-wildcard.ts @@ -0,0 +1,23 @@ +import { matchesWildcard } from '@/utils/hosts.ts' + +const tests: [string, string, boolean][] = [ + ['sub.example.com', '*.example.com', true], + ['a.b.example.com', '*.example.com', false], + ['a.b.example.com', '*.*.com', false], + ['example.com', '*.example.com', false], + ['example.com:8080', 'example.com:*', true], + ['example.com', 'example.com:*', true], + ['example.com:8080', 'example.com:8080', true], + ['example.com:8080', 'example.com:9090', false], + ['sub.example.com', '*.*.com', true], + ['example.com', 'example.com', true], + ['example.com', '*', false], + ['sub.example.com:8080', '*.example.com:*', true], + ['sub.example.com:8080', '*.example.com:9090', false], +] + +for (const [host, pattern, expected] of tests) { + const result = matchesWildcard(host, pattern) + const status = result === expected ? '' : '⛔ FAIL ⛔' + console.log(`${pattern.padEnd(19)} ${expected ? '✅' : '❌'} ${host} ${status}`) +} From 116a074cbae0d5e85e69e35059b97c8db56e8349 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:01:02 -0700 Subject: [PATCH 04/12] Lint --- package.json | 1 + src/utils/hosts.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a18c177..b81a0e4 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "android": "web-ext run -t firefox-android -s .output/firefox-mv3 --firefox-apk org.mozilla.firefox_beta --adb-device", "clean": "rm -rf .output", "tsc": "vue-tsc --noEmit", + "prettier": "npm run prettier:write", "prettier:check": "npx prettier --check .", "prettier:write": "npx prettier --write .", "lint": "npx eslint src", diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index b9b177c..ff994d7 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -2,7 +2,9 @@ export type HostsRecord = Record function parseHostPort(value: string): [string, string | undefined] { const colon = value.indexOf(':') - return colon === -1 ? [value, undefined] : [value.slice(0, colon), value.slice(colon + 1)] + return colon === -1 + ? [value, undefined] + : [value.slice(0, colon), value.slice(colon + 1)] } export function matchesWildcard(host: string, pattern: string): boolean { @@ -17,8 +19,8 @@ export function matchesWildcard(host: string, pattern: string): boolean { const patternParts = patternName.split('.') if (patternParts.length !== hostParts.length) return false - return patternParts.every( - (part, i) => part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i], + return patternParts.every((part, i) => + part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i], ) } @@ -138,7 +140,7 @@ export function validateHostname(hostname: string): string | undefined { return undefined const segments = hostPart.split('.') - if (segments.length === 0 || segments.some((s) => s === '')) return undefined + if (segments.length === 0 || segments.includes('')) return undefined for (const segment of segments) { if (segment === '*') continue if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(segment)) return undefined From ac7ec8b5bd1fab3a72838aa226542cf1baddc245 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:37:50 -0700 Subject: [PATCH 05/12] Cleanup validateHostname --- src/utils/hosts.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index ff994d7..f17bb57 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -136,8 +136,9 @@ export function validateHostname(hostname: string): string | undefined { if (value.includes('*')) { const [hostPart, portPart] = parseHostPort(value) - if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) + if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) { return undefined + } const segments = hostPart.split('.') if (segments.length === 0 || segments.includes('')) return undefined @@ -155,6 +156,6 @@ export function validateHostname(hostname: string): string | undefined { const url = new URL(urlValue) return url.hostname } catch { - // invalid hostname + return undefined } } From 964b045b4c146338bfac68d50228cb206b9d0004 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:21:38 -0700 Subject: [PATCH 06/12] Consolidate loop to findBestWildcard --- src/utils/hosts.ts | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index f17bb57..57ac7d8 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -32,8 +32,16 @@ export function findBestWildcardMatch( host: string, patterns: Record | undefined, ): string | undefined { + return findBestWildcard(host, patterns)?.creds +} + +function findBestWildcard( + host: string, + patterns: Record | undefined, +): { key: string; creds: string } | undefined { if (!patterns) return undefined - let bestMatch: string | undefined + let bestKey: string | undefined + let bestCreds: string | undefined let bestSpecificity = Infinity for (const [pattern, creds] of Object.entries(patterns)) { if (!pattern.includes('*')) continue @@ -41,11 +49,12 @@ export function findBestWildcardMatch( const specificity = countWildcards(pattern) if (specificity < bestSpecificity) { bestSpecificity = specificity - bestMatch = creds + bestKey = pattern + bestCreds = creds } } } - return bestMatch + return bestKey ? { key: bestKey, creds: bestCreds! } : undefined } export class Hosts { @@ -73,24 +82,7 @@ export class Hosts { const exact = sync[host] if (exact) return { key: host, creds: exact } - const all = await Hosts.all() - let bestKey: string | undefined - let bestCreds: string | undefined - let bestSpecificity = Infinity - - for (const [pattern, creds] of Object.entries(all)) { - if (!pattern.includes('*')) continue - if (matchesWildcard(host, pattern)) { - const specificity = countWildcards(pattern) - if (specificity < bestSpecificity) { - bestSpecificity = specificity - bestKey = pattern - bestCreds = creds - } - } - } - - return bestKey ? { key: bestKey, creds: bestCreds! } : undefined + return findBestWildcard(host, await Hosts.all()) } static async set(host: string, creds: string): Promise { From 2bb69d84eb9d711f2ffafd61c4e15d0b997e9810 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:09:37 -0700 Subject: [PATCH 07/12] Fix Popup/validateHostname and HostModal.vue/onSubmit to use Hosts.has --- src/components/HostModal.vue | 6 ++-- src/entrypoints/popup/App.vue | 2 ++ src/utils/hosts.ts | 36 +++++++++------------ tests/test-validate-hostname.ts | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 tests/test-validate-hostname.ts diff --git a/src/components/HostModal.vue b/src/components/HostModal.vue index d75ebb5..c269fbd 100644 --- a/src/components/HostModal.vue +++ b/src/components/HostModal.vue @@ -80,9 +80,9 @@ async function onSubmit() { // existing if (isAdding.value || hostRef.value !== originalHost.value) { - const existing = await Hosts.get(hostRef.value) - debug('existing:', existing) - if (existing) { + const exists = await Hosts.has(hostRef.value) + debug('existing:', exists) + if (exists) { debug('Existing Host:', hostRef.value) hostnameEl.value?.focus() hostnameEl.value?.select() diff --git a/src/entrypoints/popup/App.vue b/src/entrypoints/popup/App.vue index e1d1204..d40e8df 100644 --- a/src/entrypoints/popup/App.vue +++ b/src/entrypoints/popup/App.vue @@ -58,6 +58,7 @@ async function deleteHost(host: string) { async function onSubmit(host: string, user: string, pass: string, original?: string) { debug('popup/App.vue - onSubmit:', host, user, pass, original) await submitHost(host, user, pass, original) + hostnameRef.value = host savedCreds.value = `${user}:${pass}` usernameRef.value = user } @@ -72,6 +73,7 @@ onMounted(async () => { const creds = await Hosts.get(url.host) debug('creds:', creds) if (!creds) return debug('No Saved Creds for Host.') + // TODO: Duplicated Lookup from Hosts.get const key = await Hosts.matchKey(url.host) if (key) hostnameRef.value = key savedCreds.value = creds diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index 57ac7d8..b88d5c5 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -65,6 +65,11 @@ export class Hosts { return Object.assign({}, ...Object.values(sync)) as HostsRecord } + static async has(host: string): Promise { + const sync = await Hosts.#getSync(host) + return host in sync + } + static async get(host: string): Promise { const result = await Hosts.#lookup(host) return result?.creds @@ -125,29 +130,18 @@ export class Hosts { export function validateHostname(hostname: string): string | undefined { const value = hostname.toLowerCase().trim() - if (value.includes('*')) { - const [hostPart, portPart] = parseHostPort(value) - - if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) { - return undefined - } - - const segments = hostPart.split('.') - if (segments.length === 0 || segments.includes('')) return undefined - for (const segment of segments) { - if (segment === '*') continue - if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(segment)) return undefined - } + const [hostPart, portPart] = parseHostPort(value) - return portPart !== undefined ? `${hostPart}:${portPart}` : hostPart + if (portPart !== undefined && portPart !== '*' && !/^\d+$/.test(portPart)) { + return undefined } - try { - let urlValue = value - if (!urlValue.includes('://')) urlValue = `https://${urlValue}` - const url = new URL(urlValue) - return url.hostname - } catch { - return undefined + const segments = hostPart.split('.') + if (segments.length === 0 || segments.includes('')) return undefined + for (const segment of segments) { + if (segment === '*') continue + if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(segment)) return undefined } + + return portPart !== undefined ? `${hostPart}:${portPart}` : hostPart } diff --git a/tests/test-validate-hostname.ts b/tests/test-validate-hostname.ts new file mode 100644 index 0000000..d3c2a51 --- /dev/null +++ b/tests/test-validate-hostname.ts @@ -0,0 +1,56 @@ +import { validateHostname } from '@/utils/hosts.ts' + +const validTests: [string, string][] = [ + ['example.com', 'example.com'], + ['sub.example.com', 'sub.example.com'], + ['a.b.example.com', 'a.b.example.com'], + ['exa-mple.com', 'exa-mple.com'], + ['example.com:8080', 'example.com:8080'], + ['sub.example.com:443', 'sub.example.com:443'], + ['9gag.com', '9gag.com'], + ['localhost', 'localhost'], + ['Example.COM', 'example.com'], + [' Example.COM ', 'example.com'], +] + +const invalidTests: string[] = [ + '.example.com', + '-example.com', + '_example.com', + '!example.com', + '@example.com', + '#example.com', + '$example.com', + '%example.com', + '^example.com', + '&example.com', + '(example.com', + '+example.com', + '=example.com', + '[example.com', + '{example.com', + '|example.com', + ':example.com', + ';example.com', + ',example.com', + '~example.com', + '`example.com', + 'example.com:abc', + 'example..com', + '.', + '-', +] + +console.log('Valid hostnames:') +for (const [input, expected] of validTests) { + const result = validateHostname(input) + const status = result === expected ? '' : `⛔ FAIL (got ${result})` + console.log(` ${input.padEnd(20)} -> ${expected} ${status}`) +} + +console.log('\nInvalid hostnames:') +for (const input of invalidTests) { + const result = validateHostname(input) + const status = result === undefined ? '' : `⛔ FAIL (got ${result})` + console.log(` ${input.padEnd(20)} -> undefined ${status}`) +} From 3f98b2d2db0bca494597925d09beefaa1a876710 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:19:13 -0700 Subject: [PATCH 08/12] Cleanup Hosts and Fix Duplication --- src/entrypoints/popup/App.vue | 14 ++-- src/utils/hosts.ts | 125 ++++++++++++++++------------------ 2 files changed, 65 insertions(+), 74 deletions(-) diff --git a/src/entrypoints/popup/App.vue b/src/entrypoints/popup/App.vue index d40e8df..4fabddf 100644 --- a/src/entrypoints/popup/App.vue +++ b/src/entrypoints/popup/App.vue @@ -70,14 +70,12 @@ onMounted(async () => { const url = new URL(tab.url) debug('url:', url) hostnameRef.value = url.host - const creds = await Hosts.get(url.host) - debug('creds:', creds) - if (!creds) return debug('No Saved Creds for Host.') - // TODO: Duplicated Lookup from Hosts.get - const key = await Hosts.matchKey(url.host) - if (key) hostnameRef.value = key - savedCreds.value = creds - usernameRef.value = parseCreds(creds)[0] + const match = await Hosts.find(url.host) + debug('match:', match) + if (!match) return debug('No Saved Creds for Host.') + hostnameRef.value = match.key + savedCreds.value = match.creds + usernameRef.value = parseCreds(match.creds)[0] }) diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index b88d5c5..cc3185f 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -1,62 +1,5 @@ export type HostsRecord = Record -function parseHostPort(value: string): [string, string | undefined] { - const colon = value.indexOf(':') - return colon === -1 - ? [value, undefined] - : [value.slice(0, colon), value.slice(colon + 1)] -} - -export function matchesWildcard(host: string, pattern: string): boolean { - const [hostName, hostPort] = parseHostPort(host) - const [patternName, patternPort] = parseHostPort(pattern) - - if (patternPort !== undefined && patternPort !== '*' && patternPort !== hostPort) { - return false - } - - const hostParts = hostName.split('.') - const patternParts = patternName.split('.') - if (patternParts.length !== hostParts.length) return false - - return patternParts.every((part, i) => - part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i], - ) -} - -export function countWildcards(pattern: string): number { - return (pattern.match(/\*/g) || []).length -} - -export function findBestWildcardMatch( - host: string, - patterns: Record | undefined, -): string | undefined { - return findBestWildcard(host, patterns)?.creds -} - -function findBestWildcard( - host: string, - patterns: Record | undefined, -): { key: string; creds: string } | undefined { - if (!patterns) return undefined - let bestKey: string | undefined - let bestCreds: string | undefined - let bestSpecificity = Infinity - for (const [pattern, creds] of Object.entries(patterns)) { - if (!pattern.includes('*')) continue - if (matchesWildcard(host, pattern)) { - const specificity = countWildcards(pattern) - if (specificity < bestSpecificity) { - bestSpecificity = specificity - bestKey = pattern - bestCreds = creds - } - } - } - return bestKey ? { key: bestKey, creds: bestCreds! } : undefined -} - export class Hosts { static readonly keys: string[] = [...'*abcdefghijklmnopqrstuvwxyz0123456789'] @@ -71,18 +14,11 @@ export class Hosts { } static async get(host: string): Promise { - const result = await Hosts.#lookup(host) + const result = await Hosts.find(host) return result?.creds } - static async matchKey(host: string): Promise { - const result = await Hosts.#lookup(host) - return result?.key - } - - static async #lookup( - host: string, - ): Promise<{ key: string; creds: string } | undefined> { + static async find(host: string): Promise<{ key: string; creds: string } | undefined> { const sync = await Hosts.#getSync(host) const exact = sync[host] if (exact) return { key: host, creds: exact } @@ -145,3 +81,60 @@ export function validateHostname(hostname: string): string | undefined { return portPart !== undefined ? `${hostPart}:${portPart}` : hostPart } + +export function matchesWildcard(host: string, pattern: string): boolean { + const [hostName, hostPort] = parseHostPort(host) + const [patternName, patternPort] = parseHostPort(pattern) + + if (patternPort !== undefined && patternPort !== '*' && patternPort !== hostPort) { + return false + } + + const hostParts = hostName.split('.') + const patternParts = patternName.split('.') + if (patternParts.length !== hostParts.length) return false + + return patternParts.every((part, i) => + part === '*' ? (hostParts[i]?.length ?? 0) > 0 : part === hostParts[i], + ) +} + +export function findBestWildcardMatch( + host: string, + patterns: Record | undefined, +): string | undefined { + return findBestWildcard(host, patterns)?.creds +} + +function findBestWildcard( + host: string, + patterns: Record | undefined, +): { key: string; creds: string } | undefined { + if (!patterns) return undefined + let bestKey: string | undefined + let bestCreds: string | undefined + let bestSpecificity = Infinity + for (const [pattern, creds] of Object.entries(patterns)) { + if (!pattern.includes('*')) continue + if (matchesWildcard(host, pattern)) { + const specificity = countWildcards(pattern) + if (specificity < bestSpecificity) { + bestSpecificity = specificity + bestKey = pattern + bestCreds = creds + } + } + } + return bestKey ? { key: bestKey, creds: bestCreds! } : undefined +} + +function parseHostPort(value: string): [string, string | undefined] { + const colon = value.indexOf(':') + return colon === -1 + ? [value, undefined] + : [value.slice(0, colon), value.slice(colon + 1)] +} + +function countWildcards(pattern: string): number { + return (pattern.match(/\*/g) || []).length +} From 9072448d3e10cc7366aac6490a060cf53b33bf4b Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:59:08 -0700 Subject: [PATCH 09/12] Fix Bugs --- src/entrypoints/background/auth.ts | 2 +- src/entrypoints/content/index.ts | 9 ++++++++- src/entrypoints/popup/App.vue | 3 ++- src/utils/index.ts | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/entrypoints/background/auth.ts b/src/entrypoints/background/auth.ts index 2942e5c..bd8189b 100644 --- a/src/entrypoints/background/auth.ts +++ b/src/entrypoints/background/auth.ts @@ -65,7 +65,7 @@ async function processRequest( // Check if Request Already Processed if (pendingRequests.includes(details.requestId)) { console.log('%cAlready Processed requestId:', 'color: Orange', details.requestId) - hijackRequest(true) + return hijackRequest(true) } pendingRequests.push(details.requestId) diff --git a/src/entrypoints/content/index.ts b/src/entrypoints/content/index.ts index 36392ce..7fb2508 100644 --- a/src/entrypoints/content/index.ts +++ b/src/entrypoints/content/index.ts @@ -35,7 +35,14 @@ async function onChanged(changes: Record) { if (exactItems) { const oldCreds = exactItems.oldValue?.[url.host] const newCreds = exactItems.newValue?.[url.host] - if (oldCreds !== newCreds) return await processCreds(newCreds) + if (oldCreds !== newCreds) { + // If exact match was removed, check if a wildcard still covers this host + if (!newCreds) { + const wildcard = findBestWildcardMatch(url.host, await Hosts.all()) + return await processCreds(wildcard) + } + return await processCreds(newCreds) + } } if (wildcardItems) { diff --git a/src/entrypoints/popup/App.vue b/src/entrypoints/popup/App.vue index 4fabddf..5160336 100644 --- a/src/entrypoints/popup/App.vue +++ b/src/entrypoints/popup/App.vue @@ -57,7 +57,8 @@ async function deleteHost(host: string) { async function onSubmit(host: string, user: string, pass: string, original?: string) { debug('popup/App.vue - onSubmit:', host, user, pass, original) - await submitHost(host, user, pass, original) + const success = await submitHost(host, user, pass, original) + if (!success) return hostnameRef.value = host savedCreds.value = `${user}:${pass}` usernameRef.value = user diff --git a/src/utils/index.ts b/src/utils/index.ts index 02f6e30..5078356 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -25,7 +25,7 @@ export async function submitHost( user: string, pass: string, original?: string, -) { +): Promise { // console.debug('submitHost:', host, user, pass, original) try { // NOTE: Update Hosts.set to handle this logic... @@ -35,8 +35,10 @@ export async function submitHost( await Hosts.set(host, `${user}:${pass}`) } showToast(`${i18n.t('ui.action.addEdit')}: ${host}`, 'success') + return true } catch (e) { const message = e instanceof Error ? e.message : i18n.t('import.errorUnknown') showToast(`${i18n.t('ui.text.addEditError')}: ${message}`, 'danger') + return false } } From b09b810fd88fb9f62a8befb6cc9066315c35fe58 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:06:38 -0700 Subject: [PATCH 10/12] Fix Minor Bugs/Edge Cases --- src/components/HostModal.vue | 2 +- src/entrypoints/background/auth.ts | 6 ++++- src/entrypoints/content/index.ts | 40 ++++++++++++++++-------------- src/utils/hosts.ts | 2 +- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/components/HostModal.vue b/src/components/HostModal.vue index c269fbd..9cebbb1 100644 --- a/src/components/HostModal.vue +++ b/src/components/HostModal.vue @@ -100,7 +100,7 @@ async function onSubmit() { if (!passRef.value) { debug('No password') passwordEl.value?.focus() - passInvalid.value = `${i18n.t('ui.text.password')} ${i18n.t('ui.text.password')}` + passInvalid.value = `${i18n.t('ui.text.password')} ${i18n.t('ui.text.invalid')}` return } diff --git a/src/entrypoints/background/auth.ts b/src/entrypoints/background/auth.ts index bd8189b..647fcfa 100644 --- a/src/entrypoints/background/auth.ts +++ b/src/entrypoints/background/auth.ts @@ -10,7 +10,11 @@ export function onAuthRequired( details: chrome.webRequest.OnAuthRequiredDetails, asyncCallback?: (response: chrome.webRequest.BlockingResponse) => void, ): chrome.webRequest.BlockingResponse | undefined { - processRequest(details, asyncCallback).catch(console.warn) + // TODO: if (!asyncCallback) throw - should be called here... + processRequest(details, asyncCallback).catch((e) => { + console.warn(e) + if (asyncCallback) asyncCallback({}) + }) return undefined // returned so asyncCallback can be called } diff --git a/src/entrypoints/content/index.ts b/src/entrypoints/content/index.ts index 7fb2508..ae45793 100644 --- a/src/entrypoints/content/index.ts +++ b/src/entrypoints/content/index.ts @@ -54,24 +54,28 @@ async function onChanged(changes: Record) { async function processCreds(creds: any) { // console.debug('processCreds - tabEnabled:', tabEnabled, '- creds:', creds) - if (creds) { - tabEnabled = true - if (creds === 'ignored') { - console.log('%cIgnored - Site is Ignored!', 'color: Gold') - await chrome.runtime.sendMessage({ - badgeText: i18n.t('content.badge.off'), - badgeColor: 'yellow', - }) - } else { - console.log('%cEnabled - Site Credentials Found.', 'color: LimeGreen') - await chrome.runtime.sendMessage({ - badgeText: i18n.t('content.badge.on'), - badgeColor: 'green', - }) + try { + if (creds) { + tabEnabled = true + if (creds === 'ignored') { + console.log('%cIgnored - Site is Ignored!', 'color: Gold') + await chrome.runtime.sendMessage({ + badgeText: i18n.t('content.badge.off'), + badgeColor: 'yellow', + }) + } else { + console.log('%cEnabled - Site Credentials Found.', 'color: LimeGreen') + await chrome.runtime.sendMessage({ + badgeText: i18n.t('content.badge.on'), + badgeColor: 'green', + }) + } + } else if (tabEnabled) { + console.log('%cDisabled - Site Credentials Removed.', 'color: Tomato') + tabEnabled = false + await chrome.runtime.sendMessage({ badgeText: '' }) } - } else if (tabEnabled) { - console.log('%cDisabled - Site Credentials Removed.', 'color: Tomato') - tabEnabled = false - await chrome.runtime.sendMessage({ badgeText: '' }) + } catch (e) { + // extension is reloaded, updated, or the page outlives the background script } } diff --git a/src/utils/hosts.ts b/src/utils/hosts.ts index cc3185f..42ef336 100644 --- a/src/utils/hosts.ts +++ b/src/utils/hosts.ts @@ -39,10 +39,10 @@ export class Hosts { } static async edit(old: string, host: string, creds: string): Promise { + await this.set(host, creds) if (old !== host) { await this.delete(old) } - await this.set(host, creds) } static async update(hosts: HostsRecord): Promise { From 3d8a31b66fda65860e6e8752b5064fb9e4a96499 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:31:26 -0700 Subject: [PATCH 11/12] Lint --- src/entrypoints/content/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entrypoints/content/index.ts b/src/entrypoints/content/index.ts index ae45793..a2b87da 100644 --- a/src/entrypoints/content/index.ts +++ b/src/entrypoints/content/index.ts @@ -75,7 +75,7 @@ async function processCreds(creds: any) { tabEnabled = false await chrome.runtime.sendMessage({ badgeText: '' }) } - } catch (e) { + } catch { // extension is reloaded, updated, or the page outlives the background script } } From b5b96debfa3586f435bae3d055d72f1e83819f4a Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:36:00 -0700 Subject: [PATCH 12/12] Improve Popup Editing --- src/entrypoints/popup/App.vue | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/entrypoints/popup/App.vue b/src/entrypoints/popup/App.vue index 5160336..098535f 100644 --- a/src/entrypoints/popup/App.vue +++ b/src/entrypoints/popup/App.vue @@ -22,7 +22,8 @@ debug('width:', width.value) const options = useOptions() -const hostnameRef = ref('') // tab hostname +const tabHost = ref('') // original tab hostname +const hostnameRef = ref('') // matched hostname const usernameRef = ref('') // saved username const savedCreds = ref('') // has credentials @@ -39,6 +40,12 @@ function deleteClick(host: string) { } } +function setMatch(match: { key: string; creds: string } | undefined) { + hostnameRef.value = match?.key ?? '' + savedCreds.value = match?.creds ?? '' + usernameRef.value = match ? parseCreds(match.creds)[0] : '' +} + // DUPLICATION: HostsTable.vue async function deleteHost(host: string) { debug('popup/App.vue - deleteHost:', host) @@ -46,8 +53,8 @@ async function deleteHost(host: string) { // debug('creds:', creds) try { await Hosts.delete(host) - savedCreds.value = '' // NOTE: These 2 lines are only differences - usernameRef.value = '' // NOTE: These 2 lines are only differences + const match = await Hosts.find(hostnameRef.value) + setMatch(match) showToast(`${i18n.t('ui.text.removed')}: ${host}`, 'success') } catch (e) { const message = e instanceof Error ? e.message : i18n.t('import.errorUnknown') @@ -59,9 +66,8 @@ async function onSubmit(host: string, user: string, pass: string, original?: str debug('popup/App.vue - onSubmit:', host, user, pass, original) const success = await submitHost(host, user, pass, original) if (!success) return - hostnameRef.value = host - savedCreds.value = `${user}:${pass}` - usernameRef.value = user + const match = original !== host ? await Hosts.find(tabHost.value) : { key: host, creds: `${user}:${pass}` } + setMatch(match) } onMounted(async () => { @@ -70,13 +76,10 @@ onMounted(async () => { if (!tab.url) return debug('No URL for Tab - No Access.') const url = new URL(tab.url) debug('url:', url) - hostnameRef.value = url.host + tabHost.value = url.host const match = await Hosts.find(url.host) debug('match:', match) - if (!match) return debug('No Saved Creds for Host.') - hostnameRef.value = match.key - savedCreds.value = match.creds - usernameRef.value = parseCreds(match.creds)[0] + setMatch(match) })