Repository navigation
Releases
StackSight v2.0.1
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!importantitself, and a
MutationObserverre-applies them if the style attribute is stripped.transform,
filter,perspectiveandcontainare 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, sotransform: 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.mdnow says that plainly instead of claiming page CSS cannot reach the
prompt at all.
Changed
npm run verifygained 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.
- Open
chrome://extensionsand turn on Developer mode. - Click Load unpacked and select the unzipped
stacksightfolder. - 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
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
itemsmap. 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.storagehas no compare-and-swap, so this needed a
structural fix rather than a wrapper aroundset(). - 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.
recentVisitsheld 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, anddismissSite()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: 1and
lastVisited: nowfor 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 insideObject.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 pinsscript-src,object-src,img-src,style-src,
frame-src,form-actionandbase-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 throughelement.shadowRoot, or rewrite what it
is asking. - No
innerHTMLanywhere. The prompt and the popup's dynamic rendering build every
node withcreateElementandtextContent, 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
innerHTMLunescaped 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
executeScriptargument 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
ghfinds 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,confirmandpromptare 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.setguarantees 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, nochrome.*, no I/O and no implicit
clock. Every time-dependent function takesnow, 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 viaexecuteScript, by the pages via<script src>, and by the tests
viarequire. 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.
- Open
chrome://extensionsand turn on Developer mode. - Click Load unpacked and select the unzipped
stacksightfolder. - 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
[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 receivedwarnings when tabs close mid-prompt. Alltabs.sendMessageandruntime.sendMessagecalls wrapped in safe helpers. - HTML5
hiddenattribute being overridden by explicitdisplay: flex/blockon bulk bar and renewal banner. Added global[hidden]override.
Changed
- All storage writes routed through guarded
safeSet/safeStorageSethelpers. - Content script's
onMessagelistener now explicitly returnsfalseto close the response channel cleanly.