promptward

Plan 009: Validate custom-domain input before requesting permissions or registering scripts

Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving to the next step. If anything in the “STOP conditions” section occurs, stop and report — do not improvise. When done, update the status row for this plan in plans/README.md — unless a reviewer dispatched you and told you they maintain the index.

Drift check (run first): git diff --stat a6293e1..HEAD -- src/shared/settings.ts src/sidepanel.ts src/background.ts tests/settings.test.ts Any drift in the excerpts below is a STOP condition.

Status

Why this matters

The side panel’s “Custom domains” form accepts any string. Input like example.com/chat, https://example.com, foo bar, or an empty-after-trim value flows into chrome.permissions.request({ origins: ["*://<input>/*"] }) and later chrome.scripting.registerContentScripts({ matches: [...] }). An invalid match pattern makes those APIs throw; the rejections are unhandled (void-ed async listeners), the UI shows nothing, and — worse — a bad entry persisted to chrome.storage.sync makes registerCustomDomainScripts throw on every subsequent startup, silently breaking content-script registration for ALL custom domains (the whole registration is one call with one matches array). One malformed entry disables protection on every custom site.

Current state

Commands you will need

Purpose Command Expected on success
Typecheck npx tsc -p tsconfig.json --noEmit exit 0
Tests npm test all pass
One file npx vitest run tests/settings.test.ts all pass

Scope

In scope:

Out of scope:

Git workflow

Steps

Step 1: Add the validator

In src/shared/settings.ts, below normalizeHost:

const HOST_PATTERN = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]?$/;

/** Accepts a bare registrable hostname (post-normalizeHost): no scheme, path,
 *  port, spaces, or IP-literal brackets. Rejects single-label hosts ("localhost"). */
export function isValidCustomHost(host: string): boolean {
  return host.length > 0 && host.length <= 253 && HOST_PATTERN.test(host);
}

Verify: npx tsc -p tsconfig.json --noEmit → exit 0.

Step 2: Enforce in the side panel

In src/sidepanel.ts, in the #custom-form submit handler, after normalizeHost:

const host = normalizeHost(input?.value ?? "");
if (!isValidCustomHost(host)) {
  await render("Invalid domain — use a bare hostname like example.com");
  return;
}

(Import isValidCustomHost alongside the existing normalizeHost import.)

Verify: npx tsc -p tsconfig.json --noEmit → exit 0.

Step 3: Make background registration defensive

In src/background.ts, registerCustomDomainScripts:

  1. Filter: const hosts = settings.customDomains.map(normalizeHost).filter(isValidCustomHost); and build matches from hosts; return early when empty (preserving the existing unregister-first behavior).
  2. Wrap the registerContentScripts call so a rejection is logged, not unhandled: .catch((error: unknown) => { console.warn("[PromptWard] custom domain registration failed", error); }) — matching the repo’s existing console.debug("[PromptWard]", ...) tag style.

This guarantees a legacy bad entry already in chrome.storage.sync (written before this fix) can no longer break registration for valid ones… note the filter accomplishes that; the catch is belt-and-braces.

Verify: npx tsc -p tsconfig.json --noEmit → exit 0.

Step 4: Tests

In tests/settings.test.ts, add a describe("isValidCustomHost") block:

Accept: "example.com", "ai.example.co.uk", "my-app.example.io", "chat.example123.com". Reject: "", "localhost" (single label), "example.com/chat", "https://example.com", "foo bar.com", "example..com", "-bad.example.com", "example.com:8080", a 260-char host, "例え.jp" (unicode — see scope note).

Also one integration-shaped case: normalizeHost("*.Example.COM ") feeding isValidCustomHosttrue (the wildcard-strip + validate pipeline used by the side panel).

Verify: npx vitest run tests/settings.test.ts → all pass, ~12 new assertions.

Step 5: Full suite

Verify: npm test → exit 0.

Test plan

Step 4; pattern tests/settings.test.ts. Side-panel and background wiring are thin call sites verified by typecheck (no existing harness imports sidepanel.ts/background.ts; do not build one for this plan).

Done criteria

STOP conditions

Maintenance notes