Skip to content

fix(privacy): reuse the reference of a private address this device holds - #497

Merged
chhoumann merged 7 commits into
masterfrom
fix/reuse-feed-secret
Oct 15, 2026
Merged

chhoumann merged 7 commits into
masterfrom
fix/reuse-feed-secret

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Why

A device still on 2.21.1 can sync a data.json that holds a raw private feed address. Every read that protected that address stored it under a new podnotes-feed-url-<uuid> secret. PodNotes reads one incoming change two or three times, and SecretStorage's API has no delete, so each change left IDs listed for good. Protecting an address this device already holds now reuses its reference.

Fixes #490

What changed

  • FeedUrlRepository.store returns the PodNotes ID that already resolves to the URL on this device, without writing it again. It mints a new UUID only when no such ID exists.
  • The class comment states the rule. A cleared ID is never reused, and neither is an ID that holds another URL here.

Scope

  • PodNotes secret IDs now grow with the number of distinct private addresses, not with the number of reads.
  • IDs that dev builds of 2.22.0 already leaked stay listed. 2.22.0 is unreleased, so there is no cleanup.

Tradeoffs

  • A per-session memo of URL to ID would also stop the growth within a session. It would still add one ID per feed on each restart while a 2.21.1 device keeps syncing, and it needs its own value check. A lookup in SecretStorage holds across restarts.
  • Two saved feeds with the identical address now share one reference. Removing one keeps the other's address, because removePodcast clears a reference only when no other feed holds it, and the startup sweep clears only unreferenced IDs. No path rewrites a private feed's address in place, because attach refuses a slot that holds another URL. If the shared address is missing on a device, setting it for one feed sets it for both, which is right for two feeds with one address.

Blast Radius

Only the code that protects a raw private address calls store: subscribe, OPML import, settings import, and load and sync adoption. Each call now lists SecretStorage once. #489's behavior is unchanged. Each incoming raw change gets one protective save, and nothing raw stays on disk.

Verification

  • On master in isolated Obsidian 1.14.4 on Linux, 10 incoming raw changes of one address grew PodNotes IDs from 0 to 25, and all 25 stayed listed after a plugin reload. Instrumentation showed three store calls per change. Two came from overlapping change notifications and one from the fresh read before the protective save.
  • With the fix, the same probe added 1 ID for 10 changes. The reference stayed the same across the changes and a reload, and a second address got its own ID. Ten changes made ten protective saves, as on master.
  • The new e2e assertions in sync-private-address.test.ts fail on the master bundle, with 9 and 6 new IDs for 3 changes, and pass with the fix. The new unit tests fail on master too.
  • count:writes matches origin/master 407bf43 for a normal session. Both record 0, 0, 1, 2 completed writes for idle time, opening the view, and two settings edits.
  • Passed npm run lint, format:check, typecheck, build, test (2,761 tests), docs:build, and test:tooling (35 tests) under Node 22.23.2.
  • npm run test:e2e passed all 230 tests from a fresh vault on 1.14.4 at this head.
  • npm run size reports 102,860 gzip bytes, up 17 from origin/master 407bf43 (102,843) and 140 under the budget.

Mutation checks

Each wrong alternative fails at least one test.

  • No reuse fails the reuse unit test, the shared-removal test, the adoption test in main.persistence.test.ts, and both e2e triggers.
  • Reuse of any PodNotes ID without the value check fails the two reuse unit tests, a fix(privacy): protect private addresses adopted from sync #489 adoption test, and both e2e triggers.
  • A session memo of URL to ID with no value check recycles a cleared ID and fails "never reuses a cleared reference, one holding another URL here, or a foreign ID".
  • Reuse of another plugin's secret that holds the same URL fails the same test. attach would refuse that ID, so the feed would keep its address for this session only.
  • A raw value read that lets an empty URL match a cleared ID fails the same test.
  • Writing a reused ID again fails "stores a URL this device holds under its reference, and another URL under its own", which checks that reuse writes nothing.
  • Removal that clears a shared reference, with no guard in removePodcast, fails "keeps a private feed's address while another saved podcast uses it" and "keeps the address of a podcast protected with the same private URL as a removed one".

The local fixture address is http://priv3.supercast.com:<port>/feeds/FIX488_ADOPTED_SENTINEL. Logs, probe and mutation scripts, and check output hold the full results.

The synced private feed in the Library after a plugin reload

Docs at release

None.


Note

Medium Risk
Changes how private feed URLs are stored in SecretStorage; every store now scans listSecrets, and shared references affect removal semantics for duplicate addresses.

Overview
Fixes SecretStorage reference sprawl when synced data.json keeps delivering the same raw private feed URL (e.g. from 2.21.1). Each protective read used to mint a new podnotes-feed-url-<uuid>; with no real delete in SecretStorage, those IDs accumulated forever.

FeedUrlRepository.store now lists secrets and returns an existing PodNotes ID that already resolves to the trimmed URL on this device, only allocating a new UUID when none match. Cleared IDs, foreign-plugin keys, and slots holding a different URL are still never reused (documented in the class comment). Identical addresses on two saved feeds can share one reference; removal behavior for that case is covered by new library tests.

Tests and e2e now assert one reference across repeated sync/handlers and plugin reload; persistence tests cover concurrent onExternalSettingsChange calls.

Reviewed by Cursor Bugbot for commit 21c8682. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix FeedUrlRepository.store to reuse existing private-address references

  • Before creating a new secret reference, FeedUrlRepository.store now lists available secrets and returns the first PodNotes-owned reference that resolves to the trimmed URL. A fresh UUID-based reference is still created when no valid match exists.
  • Cleared, foreign-plugin, and mismatched PodNotes references are never reused; cleared IDs stay listed as retired.
  • Adds tests across layers: same-device reuse and cross-device distinct allocation in FeedUrlRepository.test.ts, shared references after podcast removal in library.test.ts, persistence across concurrent updates and restart in main.persistence.test.ts, and end-to-end sync adoption with ID-count assertions in sync-private-address.test.ts.
  • Risk: store now performs a secret listing and lookup on every call. If a stored reference resolves to a different value than expected, a new reference is allocated instead of reusing it — check the matching logic in FeedUrlRepository.store if duplicate secret IDs appear.

Macroscope summarized 21c8682.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 21c8682
Status: ✅  Deploy successful!
Preview URL: https://f7a72457.podnotes.pages.dev
Branch Preview URL: https://fix-reuse-feed-secret.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit 39805c5 into master Oct 15, 2026
18 checks passed
@chhoumann
chhoumann deleted the fix/reuse-feed-secret branch October 15, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Re-protecting a synced private feed address leaves a new secret ID each time

1 participant