Skip to content

Releases

StackSight v2.0.1

Choose a tag to compare

@github-actions github-actions released this 06 Aug 08:13

Security

  • A hostile page could suppress the prompt with CSS. Found by testing a live install
    rather than by reading the code. The shadow root is closed, so the page cannot read,
    query or restyle the prompt's contents, and that all held up. But the host element is
    an ordinary node in the page's DOM, and
    #stacksight-prompt-host { display: none !important } hid the entire prompt.

    The host's layout-critical properties are now asserted inline with !important, which
    outranks any author stylesheet including one that uses !important itself, and a
    MutationObserver re-applies them if the style attribute is stripped. transform,
    filter, perspective and contain are locked alongside the obvious ones, because
    any of them on the host makes it the containing block for the fixed-position card
    inside the shadow root, so transform: scale(0) collapsed the prompt without touching
    display.

    Impact was bounded: suppression only, no read, no alteration, no leak. A page can still
    delete the host node outright, which nothing injected into a page's DOM can survive.
    docs/SECURITY.md now says that plainly instead of claiming page CSS cannot reach the
    prompt at all.

Changed

  • npm run verify gained an 18th check, for the host style lock.

Install

Download stacksight-2.0.1.zip below and unzip it somewhere stable, not Downloads or the Desktop: Chrome reads the folder live.

  1. Open chrome://extensions and turn on Developer mode.
  2. Click Load unpacked and select the unzipped stacksight folder.
  3. Pin it from the puzzle-piece menu.

Requires Chrome 111 or newer. Not on the Chrome Web Store, and not planned for it.

Full notes: INSTALL.txt · Architecture · Security model

StackSight v2.0.0

Choose a tag to compare

@github-actions github-actions released this 06 Aug 07:46

A rewrite of the foundations. Everything the extension did, it still does. What changed
is that the numbers it shows are now correct, the claims it makes are now enforced, and
the tests now test the code that ships.

Your 1.x data migrates automatically on first run. The old storage keys are left in
place as a rollback copy rather than deleted.

Fixed

  • Lost updates. Every context did read-modify-write on the whole items map. Two
    tabs finishing a load in the same tick both read the same snapshot and the second
    write erased the first; a popup edit landing during a background visit clobbered it.
    The service worker is now the only writer and applies every mutation through one
    serialised queue. chrome.storage has no compare-and-swap, so this needed a
    structural fix rather than a wrapper around set().
  • Renewal dates on month ends. setMonth() overflowed January 31 to March 3,
    skipping February. Dates now anchor on the original day of month and clamp to the
    target month's length, so a subscription on the 31st goes Jan 31, Feb 28, Mar 31
    instead of drifting or overflowing. Yearly renewals on Feb 29 clamp in common years
    and return to the 29th in leap years.
  • Cost per visit was badly wrong for sites you use a lot. recentVisits held at
    most 30 raw timestamps but was read as "visits in the last 30 days". A site opened
    100 times a month reported 30, overstating its cost per visit by more than three
    times. Replaced with per-day counts, which are unbounded in magnitude and smaller on
    disk.
  • Failed writes were reported as success. A full storage area silently dropped
    data while the UI said it had saved, and dismissSite() returned ok after a failed
    write. Every command now returns a real result and the interface says what happened.
  • Adding a site by hand invented a visit. It set visitCount: 1 and
    lastVisited: now for a site you had not opened, skewing cost per visit from the
    first render.
  • Export and import lost data. Export omitted dismissed sites; import ignored the
    currency that export wrote. The full state now round trips.
  • "Replace everything" did not. It replaced items and stacks only, leaving old
    dismissals in place and still suppressing sites.
  • Import crashed on some valid JSON. {"items": null} passed the shape check and
    then threw inside Object.entries.
  • Dismissed sites grew forever. They now expire on a configurable TTL and are
    capped at 500.
  • A brand new site was immediately flagged stale, since it had no visits yet. New
    sites are exempt until they have existed for a full stale window.
  • The popup squashed its own header and tabs when the list was long, because every
    child of the column flexbox could shrink.

Security

  • connect-src 'none' in an explicit manifest CSP. The no-network claim used to
    rest on a source audit; Chrome now refuses to let the extension open a connection at
    all. The policy also pins script-src, object-src, img-src, style-src,
    frame-src, form-action and base-uri.
  • The prompt is injected on demand rather than declared as a static content script
    on every http(s) page. On a site you already track, or dismissed, or with prompting
    off, no StackSight code touches the page. This reduces runtime exposure, not the
    <all_urls> permission, which is still needed to observe navigations.
  • The prompt renders in a closed shadow root with its own styles. The page it sits
    on can no longer restyle it, read it through element.shadowRoot, or rewrite what it
    is asking.
  • No innerHTML anywhere. The prompt and the popup's dynamic rendering build every
    node with createElement and textContent, so escaping cannot be missed at a call
    site rather than merely being remembered at all of them.
  • The currency symbol is allowlisted. It reached innerHTML unescaped in 1.3.
    Unreachable then, but it would have become live the moment import started reading
    settings, which 2.0 does.
  • Control characters and bidi overrides are stripped from stored text, so a crafted
    page title cannot render back-to-front and impersonate another site.
  • The payload reaches the prompt as an executeScript argument instead of a
    follow-up message, removing the window where the script existed but its listener was
    not registered.

Added

  • Insights view. Monthly and yearly spend, how much is going to stale sites, a 30
    day visit sparkline, spend by stack, and most expensive per visit.
  • Settings page. Preferences, a summary of stored data, export and import,
    management of dismissed sites, and the full disclaimer.
  • Configurable thresholds. Stale window, renewal warning window and dismissal TTL
    were constants you had to edit in source. They are settings now.
  • Light, dark and match-system themes, applied to the popup, the settings page and
    the injected prompt.
  • Favicons from Chrome's local icon cache, with deterministic letter avatars when
    there is no cached icon.
  • Fuzzy ranked search, so gh finds GitHub. Matches across name, host, stack and
    note, weighted by field.
  • Pinned sites, which stay above everything including the last launched site.
  • A toolbar badge counting paid sites you have stopped opening.
  • Restore dismissed sites individually or all at once.
  • Erase everything, with confirmation.

Changed

  • alert, confirm and prompt are gone. They blocked the extension process and
    could dismiss the popup mid-action. Replaced by in-popup dialogs and toasts.
  • Modals trap focus, close on Escape, and restore focus to whatever opened them.
    Interactive elements have ARIA roles and labels.
  • Insights replaces the list rather than stacking below it, which pushed the popup
    past the height Chrome allows.
  • Storage is one key. chrome.storage.local.set guarantees nothing across multiple
    keys, so a multi-key write could half-apply. One key means one atomic write.
  • Visit history is per calendar day, in local time. UTC keys would put an 8pm visit
    in UTC-5 on the following day, so the stale flag and "visits this month" would
    disagree with the calendar the user is reading.
  • Import and export moved to the settings page, where there is room for the summary
    and the merge-or-replace choice.

Internal

  • lib/core.js: all pure logic, with no DOM, no chrome.*, no I/O and no implicit
    clock. Every time-dependent function takes now, so tests pin it instead of sleeping.
  • lib/store.js: the serialised command queue, over an injectable storage adapter.
  • Both are classic scripts, loaded by the service worker via importScripts, by the
    content script via executeScript, by the pages via <script src>, and by the tests
    via require. Still no build step and no dependencies.
  • Tests rewritten. The old suite copy-pasted the logic it tested, so it could pass
    while the extension was broken. 57 tests now run against the shipped modules on
    node --test.
  • dev/: a dependency-free harness that runs the real pages in an ordinary tab
    against the real store, used for development and for every screenshot in the README.

Install

Download stacksight-2.0.0.zip below and unzip it somewhere stable, not Downloads or the Desktop: Chrome reads the folder live.

  1. Open chrome://extensions and turn on Developer mode.
  2. Click Load unpacked and select the unzipped stacksight folder.
  3. Pin it from the puzzle-piece menu.

Requires Chrome 111 or newer. Not on the Chrome Web Store, and not planned for it.

Full notes: INSTALL.txt · Architecture · Security model

StackSight 1.3.0

Choose a tag to compare

@Codex-Crusader Codex-Crusader released this 10 May 04:24
6d796c1

[1.3.0] — 2026-05-09

Added

  • JSON Import — file picker with merge-or-replace dialog. Sanitises against malicious URLs (javascript:, data:, file:, etc.) and reports skipped entries. Caps long fields on import.
  • Bulk select — toggle "Select" mode in the items list to multi-select. Bulk Move (with stack picker) and Delete actions.
  • Renewal date tracking — optional date field on paid items, auto-rolls forward when past, banner appears for renewals in the next 7 days, inline indicator for further-out renewals.
  • Custom currency — settings dropdown with $, €, £, ¥, ₹, A$, C$.
  • Welcome page — opens automatically on first install (reason === 'install'). Three-step orientation, four feature cards, privacy callout, full disclaimer in expandable section.
  • First-run launcher coach mark — one-time tooltip showing keyboard shortcuts. Dismissed on click or first keystroke.
  • Settings + About modal — gear icon in header. Currency picker, expandable disclaimer, version, author credit.
  • Footer credit — persistent line below actions bar with author name and version.

Fixed

  • Silenced chrome.runtime.lastError: The message port closed before a response was received warnings when tabs close mid-prompt. All tabs.sendMessage and runtime.sendMessage calls wrapped in safe helpers.
  • HTML5 hidden attribute being overridden by explicit display: flex/block on bulk bar and renewal banner. Added global [hidden] override.

Changed

  • All storage writes routed through guarded safeSet / safeStorageSet helpers.
  • Content script's onMessage listener now explicitly returns false to close the response channel cleanly.