Repository navigation
fix(feeds): keep private subscriptions working across devices - #428
Conversation
… losses Set feed address must keep the synced reference, OPML must reuse a saved private feed's reference and never let a public namesake replace it, the orphan sweep must keep every ID data.json references, and released 2.20.0 and 2.21.1 libraries must load unchanged.
Button registered its click listener on every update, so a press after any prop change dispatched once per update. Register it once when the button is created.
…he first A private feed's synced reference now stays its identity. Settings offers Set feed address on a saved private feed whose URL is not on this device, and OPML import sets a listed private URL the same way, both under the existing reference, so every device keeps resolving its own copy. A public namesake never replaces the feed. The orphan sweep keeps every ID data.json still references, also in records this version drops or fields it does not know. Pasting a feed under a title it no longer has offers Remove for its saved key instead of adding a duplicate. No private feed address reaches data.json: the encoder redacts it in every field, from the first write, keeping signed media and an episode's own link. A URL SecretStorage cannot keep is never saved in its place; startup then leaves data.json as it is and refuses saves. The missing-address notice no longer advises removing the feed. Closes #391
Deploying podnotes with
|
| Latest commit: |
9fd33c3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f3289267.podnotes.pages.dev |
| Branch Preview URL: | https://fix-private-feed-devices.podnotes.pages.dev |
There was a problem hiding this comment.
🟠 High
Line 479 in 9fd33c3
If SecretStorage cannot retain a private feed URL during startup, loadSettings still writes the temporary retainForSession reference to data.json, losing the subscription URL after restart. protectPrivateFeeds reports these failures in feedProtection.failed, but this migration writes unconditionally; abort the write when that list is nonempty so the original file remains intact.
Also found in 1 other location(s)
src/ui/settings/PodNotesSettingsTab.ts:938
Removing the
protection.failedguard lets a raw private-feed settings import report success even when SecretStorage cannot retain its address.protectPrivateFeedscatches that failure and substitutesretainForSession's temporary UUID; strict persistence accepts the placeholder, but the address exists only inFeedUrlRepository.sessionUrls. After restart the imported subscription cannot load, despite the successful import notice. Keep the failure check (or explicitly report a non-durable import) rather than silently accepting temporary references.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/main.ts around line 479:
If SecretStorage cannot retain a private feed URL during startup, `loadSettings` still writes the temporary `retainForSession` reference to `data.json`, losing the subscription URL after restart. `protectPrivateFeeds` reports these failures in `feedProtection.failed`, but this migration writes unconditionally; abort the write when that list is nonempty so the original file remains intact.
Also found in 1 other location(s):
- src/ui/settings/PodNotesSettingsTab.ts:938 -- Removing the `protection.failed` guard lets a raw private-feed settings import report success even when SecretStorage cannot retain its address. `protectPrivateFeeds` catches that failure and substitutes `retainForSession`'s temporary UUID; strict persistence accepts the placeholder, but the address exists only in `FeedUrlRepository.sessionUrls`. After restart the imported subscription cannot load, despite the successful import notice. Keep the failure check (or explicitly report a non-durable import) rather than silently accepting temporary references.
Summary
Fixes #391.
Setting up a synced private feed on another device keeps its existing UUID. Use Set feed address on the saved Settings row, or import a same-title private feed through OPML. A public OPML namesake leaves the private subscription intact. Settings export/import now preserves the valid feed reference too. The orphan sweep retains IDs referenced anywhere in data.json, including unknown fields and object keys.
A publisher title change no longer creates a duplicate subscription: Settings matches the URL across saved feeds and Remove uses the original saved key. The missing-address notice says what is missing on this device. Enter submits Set feed address without reopening the dialog. Public addresses are refused before fetching. The Button change is cleanup only: Obsidian already replaces its callback; the test mock now does the same, and the mock-only regression test was removed.
The persistence guarantee is no private feed address in data.json, including the first write, all episode containers and preserved unknown fields. Settings exports use the same boundary with this device's known private addresses, so an episode description cannot leak the feed address into the exported vault file. Protection uses the current credential-bearing feed URL detector and locally known private addresses. Independent signed
streamUrl,artworkUrl,chaptersUrland episode links remain unchanged, so playback resume keeps working. Path-only detection remains research only; encoded textual representations are outside this change.Verification
Current delta report and replay scripts, snapshots, timings and logs.
The
npm run size:datastress library with one private feed encodes in 5.83 ms, down from 31.65 ms, in Obsidian's renderer (master: 2.18 ms). This is the median of nine alternating rounds. The remaining scan covers arbitrary text and keys. Ranges, the no-private-address case, write-guard cost, and an inclusive file-write probe are in the report.Node v22.23.2, commands run through
fnm exec --using 22 --:npm run lintnpm run format:checknpm run typechecknpm run buildnpm run testnpm run test:toolingnpm run docs:buildnpm run test:e2enpm run sizeSize delta from the prior branch head
2898cc1: +976 raw bytes, +378 gzip bytes, including master's +175 / +25. The fixes alone add +801 / +353. Limits are 330,000 and 100,000 bytes. Masteredb9447was merged without conflicts. Gates, all E2E tests, and live export/storage-failure/restart checks passed on9fd33c35784a659823e9468f87cd06e63740c8c0. Performance codecs are byte-identical across the merge.Real Obsidian 1.14.4 on Linux x64 under Xvfb, two worktree-owned isolated vaults with separate SecretStorage profiles and one process at a time. The synthetic fixture feed is
http://podnotes.test:41627/PRIV2_PATH_SENTINEL/feed.xml?auth=PRIV2_QUERY_SENTINEL; the runner maps its host to loopback.8f0eda38a8767de3be413da3f2f61b34355bfba89290de5dc5629a09851012a9;rg --quiet PRIV2_SUBSCRIBE_SENTINEL after-first-write.jsonexits 1. Every captured write omits the sentinel.Original two-vault verification report · Replay scripts and evidence · Missing-address notice · Title-change before / after
Release impact
No schema or ID migration. Existing UUID references shipped in #295 / 2.20.0 and remain stable. New settings exports carry the non-secret feed UUID; older exports that already discarded it cannot recreate it. SecretStorage remains device-local. If a raw private feed address cannot be stored, PodNotes saves the placeholder and other changes, retains the address for this session, and shows one Notice. The feed needs Set feed address after restart; key-redaction collisions also refuse the write instead of losing entries. Public API and URI formats are unchanged.
Path-secret proposal: recommend verified host patterns plus an explicit privacy action, without implementing either here. The reviewed host docs did not establish that a major host reuses the same durable feed token in enclosure URLs. Signed-resource persistence and re-resolving media on restore remain a separate unit.
Docs at release
sync.md: replace the private-feed one-device limitation with stable-ID setup on each device; explain that the address itself stays local.faq.mdandmobile.md: replace remove/re-add advice with Set feed address and the exact missing-device notice.settings.md: document the new control, UUID preservation through settings transfer, and the session-only fallback when a raw private address cannot be stored.import_export.md: document private OPML revival under the existing ID, public namesake behavior and the limitation of older settings exports that lack the ID.sync.mdandfaq.md: document that a startup sweep can remove this device's secret after a stale sync drops the subscription; removing and re-adding creates a new UUID; Set feed address does not verify show ownership.Note
Fix private feed subscriptions syncing across devices with redacted URLs
data.json.redactPrivateFeedAddressesin privateFeedUrl.ts rewrites matching strings and object keys to placeholders during persistence, settings export, and sync merges.FeedUrlRepositorygainsattachandretainForSession: a second device can populate an existing private-feed reference, and a failed SecretStorage write leaves the address resolvable in memory for the current session only.Set feed addressmodal (FeedAddressModal.ts, PodcastQueryGrid.svelte) lets users recover a synced private feed whose address is unavailable locally; invalid, public, or unloadable URLs leave the saved entry unchanged.internPrivateFeednow throws on SecretStorage failure instead of returning the raw feed; import summaries use new singular/plural wording; the unavailable-address notice is one sentence with no remove/re-add instruction. Risk: privateFeedUrl.ts contains a stray+in theredactPrivateFeedAddressescallback that is a TypeScript syntax error and must be removed before build.Macroscope summarized 9fd33c3.