Skip to content

fix: keep private feed secrets and vault files out of settings and OPML transfers - #404

Merged
chhoumann merged 8 commits into
masterfrom
fix/transfer-safety
Oct 12, 2026
Merged

chhoumann merged 8 commits into
masterfrom
fix/transfer-safety

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

Moving PodNotes between vaults through settings export/import, a raw data.json import, or OPML is now safe for private feeds and leaves vault files behind.

  • Private feed secrets stay out of data.json. Importing a data.json whose podcasts include a private feed (a URL with ?auth=... or user:pass@) used to protect the feed itself but keep the secret URL in every queued, favorited and playlist episode, where it stayed for good. Protecting a private feed and cleaning episode snapshots are now one function, protectPrivateFeeds, which the load path, the import, and the writer's read of data.json all call. A private feed's real URL maps to the placeholder that names its saved key, in an episode's url and feedUrl alike, so links keep resolving. A private URL no saved feed matches still becomes its podcast's placeholder. Vaults and backups that an earlier import leaked into are repaired once, a private feed that another device syncs in is never written back raw, and a load that changes nothing writes nothing.
  • An import that cannot protect a secret is refused. When SecretStorage cannot keep an imported private feed URL, the import stops before anything is written and says "Could not finish importing secrets into Obsidian SecretStorage. Existing settings were kept, and retrying is safe." The API-key failure path shares that notice.
  • Re-importing an OPML no longer drops or duplicates a private feed you have. The duplicate check compared the OPML URL with the saved placeholder, so PodNotes fetched the feed again and dropped it as a "duplicate title", or saved a second copy with a second secret when the channel title had changed. It now recognizes the feed by its real URL. In a vault where the feed's secret is missing (it arrived through a settings import), the OPML import gives it the listed URL instead of dropping it, so settings import plus OPML import moves private feeds between vaults.
  • Settings import and export leave this vault's files behind. Transfers no longer carry Local Files, local-file episodes in the queue, favorites and playlists, a playlist's stored current episode, or the episode fields older versions kept in progress records. Playback progress now travels as title, podcast, time, duration and finished: an import adds progress for episodes this vault has not played and keeps this vault's own, even when the player saves a position during the import. The confirm dialog names the queue and favorites when an import replaces them.

Small changes in the same code: the OPML failure notice no longer points at a console that production builds strip, the settings tab drops a try/catch around importOPML that could never fire, both export error handlers show the error itself instead of a reworded copy for a missing folder, and the import trusts the codec's normalization instead of repeating it. These deletions keep the bundle below master's size.

Before (master) After
Import dialog on master Import dialog on this branch
Another vault's local file in this vault's queue fails to play A private feed revived by OPML import lists its episodes

The first row is the import of vault X's settings into vault Y. The second row shows X's local file in Y's queue on master, and on this branch a private feed that arrived through a settings import, revived by an OPML import.

Verification

Obsidian 1.13.7 on headless Linux (Xvfb), in this worktree's isolated vault. Every scenario ran on master 6421261 first, through Settings > PodNotes with the real file picker or by seeding data.json with the plugin off. A local fixture served a private feed at http://127.0.0.1.nip.io:<port>/private/feed.xml?auth=<secret>, and the verify skill's fixtures served PodNotes Fixture and Fixtures/Local Fixture.wav. Counts are plaintext occurrences of the test secret in data.json or in the export file.

Scenario Master This branch
Import a data.json with a private feed in saved feeds, queue, favorites and a playlist 3 after import, 3 after plugin reload 0 after import, 0 after reload
Upgrade: master imports that file, then this build loads the vault 3 0 after reload, with the notice "Moved private feed URLs out of data.json."; a second reload does not rewrite data.json
Master leaks a vault as above; this build imports that leaked data.json into a fresh vault 3 in the file 0 after import
A queued episode whose url is the private feed URL and that has no feedUrl 0 on load, 1 after import 0 on load, in the default export, and after import
Import while SecretStorage refuses writes "Imported PodNotes settings.", 4 in data.json and 4 in the export Import refused with the notice above; data.json unchanged, 0 in the export
A feed saved as "Saved Alias" with channel title "Private Fixture", then a timestamp link to its episode at 12 s Link plays Link plays from 12 s (the PR's earlier head refused it)
Playlists whose stored current episode is a local file, plus a progress record carrying a local episode; export, then import 9 vault paths in the export and after import 0 and 0; progress keeps its five fields
Another device's private feed and queued episode reach data.json while an import saves 2, written raw 0; the synced feed is saved as a placeholder
Export OPML, import it again "Skipped 1 with duplicate titles", feed fetched again "Skipped 1 existing podcasts", no fetch
Same, after the channel title changed upstream "Saved 1 new podcasts": a second subscription and a second secret "Skipped 1 existing podcasts", one subscription, one secret
Vault A exports settings and OPML with a private feed; vault B imports both "Skipped 1 with duplicate titles", feed has no secret and cannot load Feed gets its secret back, lists its episodes, and plays
Vault X (feed, local file played, feed episode played) exports settings; vault Y with its own queue imports them Export has 4 vault paths; Y's queue and Local Files hold X's local file, which fails with "Could not load this episode's media" Export has 0 vault paths; Y's Local Files stays empty; X's progress arrives and Fixture Episode 1 resumes where X left it

Unit tests: each bug has a test that fails on master (commit b27a826), and each V-404 finding has a test that fails on the earlier head 3d5f9b0 (commit 177c49e). The import tests drive the real confirm dialog, a real data.json writer and the real PlayerController. Every new test was checked by reverting its fix and watching it fail. Three review rounds by Claude Fable 5.1 and GPT 6.0 Astra and the independent V-404 verification shaped the later commits.

npm run lint          exit 0
npm run format:check  exit 0
npm run typecheck     exit 0
npm run build         exit 0
npm run test          93 files, 1575 tests passed; svelte-check clean
npm run docs:build    exit 0
npm run test:e2e      11 files, 68 tests passed
npm run size          gzip 99,965 bytes (master 99,975, gate 100,000); raw 317,450 (master 317,826)

Release impact

No data.json schema change. Private feeds keep SecretStorage and placeholders.

  • On the first load after upgrading, a vault where an earlier data.json import left a private feed URL in episode snapshots is rewritten once without it, with the notice "Moved private feed URLs out of data.json." The load-time notice no longer counts URLs.
  • Settings exports now include playback progress and no longer include Local Files, local-file episodes, or playlists' stored current episodes. Older exports and raw data.json files import as before, minus those entries.
  • An import that SecretStorage cannot complete is refused instead of saving the URL in data.json.
  • Copy changes: the import dialog says "Also replaced: ..." and "Your playback progress, downloads, and local files are kept."; the import and export setting descriptions mention playback progress; the SecretStorage failure notice is "Could not finish importing secrets into Obsidian SecretStorage. Existing settings were kept, and retrying is safe."; the OPML failure notice drops "Check console for details."; export failures show "Unable to create podcast export file:" or "Unable to export settings:" followed by the error.

Docs at release

  • import_export.md, "Settings & templates": playback progress is now included. On import, progress is added for episodes this vault has not played, and this vault's own progress is kept. Local Files, local-file episodes in the queue, favorites and playlists, and download bookkeeping stay in their vault.
  • import_export.md, "Importing settings": the confirmation names the feeds, playlists, queue and favorites it replaces. An import that cannot store a private feed URL or API key in SecretStorage is refused and changes nothing.
  • import_export.md, OPML: importing an OPML that lists a private feed already saved here skips it. In a vault where that feed arrived through a settings import without its private URL, the OPML import restores it.
  • sync.md, private feeds: besides removing and re-adding a private feed whose URL is missing on a device, importing an OPML that lists it now restores it.

Not in this PR

  • A settings import still turns a private feed that already works on the target device into a placeholder until an OPML import revives it, as on master.
  • OPML revival misses a feed whose channel title changed since export (part of the feed-title-as-key problem).
  • Download-only fields (filePath, size) on downloaded feed episodes in the queue still travel, though the target vault ignores them.
  • If SecretStorage cannot protect a private feed that another device synced in, its URL stays in data.json and the next load shows the existing failure notice, as on master.

Note

Keep private feed secrets and vault-local files out of settings and OPML transfers

  • Replaces migratePrivateFeedUrls and scrubMigratedEpisodeUrls with a single protectPrivateFeeds operation in privateFeeds.ts. It moves credential-bearing feed URLs into SecretStorage and scrubs matching raw URLs from all episode snapshots (queue, favorites, playlists, downloads, current episode) in one pass.
  • Settings import in PodNotesSettingsTab.ts now runs protectPrivateFeeds before the first settings write and aborts with a retry-safe notice when a secret cannot be stored, instead of persisting the leak.
  • Settings transfer in settingsTransfer.ts now transfers playedEpisodes progress while excluding vault-local data (localFiles, downloadedEpisodes, podNotes, currentEpisode), and strips local-file episodes, playlist current episodes, and SecretStorage references from queue, favorites, and playlists on both export and import.
  • OPML import in opml.ts skips saved private feeds matched by their resolved URL, fetches unresolved private entries from the listed URL, and re-stores them protected. Export error notices are consolidated.
  • Behavioral Change: playedEpisodes is now included in exports and imports (previously excluded); import confirmation now lists saved feeds, playlists, queue, and favorites that will be replaced; import merges progress additively with current vault values winning conflicts.

Macroscope summarized 5c1b468.

Each test fails on master:
- an imported data.json keeps a private feed URL in queue, favorites and
  playlist episode snapshots on disk
- re-importing an OPML that lists a saved private feed fetches it again and
  saves it a second time
- a settings export carries Local Files and local-file episodes, and an
  import brings another vault's local files into this one
…ML transfers

- Importing a data.json with a private feed now replaces its URL in queue,
  favorites, playlist and history snapshots too, not only in the feed list.
  Interning and scrubbing are one function, protectPrivateFeeds, used by both
  the load-time migration and the import.
- Re-importing an OPML that lists a saved private feed recognizes it by its
  real URL, so it is skipped instead of fetched and saved a second time.
- Settings export and import leave this vault's local files behind: Local
  Files is not exported, and local-file episodes are dropped from the queue,
  favorites and playlists. Playback progress now travels; on import this
  vault's progress wins and the file adds the rest.
- The import dialog names the queue and favorites it replaces.
- The OPML failure notice no longer points to a console that production
  builds strip.
…ML, keep progress imported during playback

- On load, a saved private feed's real URL is replaced in episode snapshots
  too, so data.json files that an earlier import already leaked into are
  repaired once; a load that changes nothing writes nothing.
- OPML import gives a saved private feed whose URL is not on this device the
  listed URL instead of dropping it as a duplicate title, so settings import
  plus OPML import moves private feeds between vaults.
- Progress imported while an episode plays is no longer lost when the player
  saves a position during the import; the live store wins and the file adds
  the rest.
- Import re-hydration trusts the codec's normalization instead of repeating
  it, and stale or narrating comments are removed.
… this device can resolve

An episode snapshot's feedUrl, and its url when it fell back to the feed URL,
becomes its podcast's placeholder whenever it is credential-bearing. This
needs no secret on this device, so importing a backup that an older build
already leaked into no longer writes the URL, and snapshots of unsubscribed
private feeds are cleaned too. The load notice now reads accurately when a
load only repairs snapshots.
# Conflicts:
#	src/ui/settings/PodNotesSettingsTab.ts
A load that cleans episode snapshots while a feed's URL still could not move
into SecretStorage showed both the move notice and the failure notice. It now
shows only the failure notice. A test pins that such a feed keeps its raw URL
while its snapshots get the placeholder.
@cloudflare-workers-and-pages

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

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5c1b468
Status:🚫  Deploy failed.

View logs

Comment thread src/services/privateFeeds.ts Outdated
…nsfers

Each fails on 3d5f9b0:
- an episode without feedUrl keeps a saved private feed's URL
- a feed saved under a key other than its title gets episode placeholders
  named by the title, which links cannot resolve
- an import whose private feed URL SecretStorage cannot keep reports success
  and persists the URL
- a playlist's current episode and progress records carry vault paths out of
  an export and into an import
- a private feed synced to disk while an import saves is written raw
…ransfer

- A private feed's real URL maps to the placeholder that names its saved key,
  and that mapping applies to an episode's url and feedUrl, so an episode
  without feedUrl loses the URL too and links still resolve when the saved key
  differs from the title. A private URL no saved feed matches still becomes
  its podcast's placeholder.
- An import whose private feed URL SecretStorage cannot keep is refused
  before anything is written, with one notice shared with the API-key failure.
- Settings transfers drop a playlist's current episode, which no player reads,
  and keep only title, podcast, time, duration and finished in progress
  records, so neither carries a vault path.
- The writer protects the data.json it reads before merging it, so a private
  feed another device synced is never written back raw.
- Both export error handlers show the error itself instead of a reworded copy
  for a missing folder.
@chhoumann
chhoumann merged commit beda5be into master Oct 12, 2026
10 of 11 checks passed
@chhoumann
chhoumann deleted the fix/transfer-safety branch October 12, 2026 13:02
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.

1 participant