Repository navigation
feat(feeds): protect documented private feed addresses - #440
Merged
Merged
Conversation
Deploying podnotes with
|
| Latest commit: |
3534f40
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://0e6db3cb.podnotes.pages.dev |
| Branch Preview URL: | https://feat-path-secret-hosts.podnotes.pages.dev |
This was referenced Oct 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Recognize documented private feed addresses from eight podcast publishers, including both current and legacy Supercast forms. These addresses now use the existing device-local SecretStorage path instead of being written raw to data.json, generated feed links and cache keys.
Settings export now requires the device address map. Regression checks guard a second scrub pass with a newly encountered host, one Notice for multiple failed secret writes, and startup protection of a 2.21.1-shaped library without losing user state or changing its reference on reload.
supercast.com,supercast.techand their subdomains:/feed/<key>.rssor/feeds/<token>...feeds.megaphone.fm/mega; premium onmega.supercast.com. Same-host non-feed landing and blog pages also stay public.substack.comand subdomains:/feed/podcast/<digits>[/s/<digits>]/private/<token>.rssapi.substack.com/feed/podcast/87281.rssand/feed/podcast/87281/s/87957.rss. Other/private/routes stay public.supportingcast.fm:/content/<token>.rssUserFeed.url, explicitly calls this the user-specific URL..pngcontent path. No public RSS feed on that host was confirmed.subscribers.transistor.fm, with a nonempty pathfeeds.transistor.fm/the-caffeine-show. Empty paths are also tested; no same-host public feed is claimed.pinecast.com:/feed/<show>/sub/<subscriber-id>...pinecast.com/feed/example, from the publisher redirect guide.private.captivate.fm, with a nonempty pathfeeds.captivate.fm/insider/. Empty paths are also tested; no same-host public feed is claimed.feeds.redcircle.com:/<id>/pf/<token>...; exactlyapi.podcache.net:/shows/<id>/pf/<token>...feeds.redcircle.com/<id>andapi.podcache.net/shows/<id>/feed.castos.com, with a nonemptyuuidquery valueuuidto its public URL.myshow.castos.com/feedwithoutuuid;feeds.castos.com/abcd1234also stays public.uuidon other hosts stays public.Tokens must be nonempty. Matching uses the parsed hostname with DNS label boundaries and one terminal dot normalized. Paths remain case-sensitive, and encoded separators are not decoded. Existing global auth-query and userinfo rules still apply.
Candidates needing evidence: NPR
uid, Acast Access and Supporting Cast.co/.cc. They are excluded. Unknown hosts and custom domains are not inferred from long or unusual paths.Verification
Node 22.23.2. All required checks ran locally after merging origin/master at e3b90c1:
npm run lint- pass.npm run format:check- pass.npm run typecheck- pass.npm run build- pass.npm run test- pass, 1,971 tests in 106 files; Svelte checks report 0 errors and warnings.npm run test:tooling- pass, 10 tests.npm run docs:build- pass, 18 pages and preserved links.npm run size- pass: 301,276 raw bytes and 96,076 gzip9 bytes. Against the same master build, +711 raw and +260 gzip9. Both remain below 330,000 / 100,000 budgets.npm run test:e2epassed all 89 tests in 13 files on Obsidian 1.14.4 (303.88 s).PODNOTES_OBSIDIAN_APP=/opt/Obsidian/obsidian npm run test:e2epassed the same 89 tests on Obsidian 1.13.7 (313.10 s).Manual checks used real Obsidian 1.14.4 on Linux/Xvfb with isolated vaults. The synthetic feed was
http://priv3.supercast.com:38227/feeds/PRIV3_PATH_SENTINEL/feed.xml, with a 120-second WAV. Settings search and Add, episode playback, Create episode note through the palette, and the direct command all ran. With neutral descriptions, actual saved JSON, the new note and a nonempty v6 cache contained no sentinel. SecretStorage held the address; cache keys and note links used the placeholder.A fresh device A started from a 2.21.1-shaped library with the raw URL. The whole saved document matched the expected address/reference transformation and retained its history, queue, favorite and settings. Reload kept the same reference and disk bytes. A second fresh profile received only A's data.json, showed Set feed address, and made no feed request until the address was pasted. Enter attached it under the same reference. Playback resumed beyond the transferred position with volume 0.73, a 21-second forward skip and the favorite preserved. No runtime errors were captured.
Neutral note | Second device missing the address | Playback after attachment | Evidence archive
The manual captures are from 34950aa before the final transcript merge. The full merged suites cover the combined code. The evidence archive identifies each revision and includes scripts, actual saved JSON, cache text, notes and logs.
Mutation checks failed as intended when the tab omitted its export address map, the scrub reused a host regex across calls, or failed feeds each showed a Notice. Publisher positives also fail against the original classifier. Sixteen classifier mutations were caught.
Independent interrogate pass on the feature diff before the transcript merge: Claude Fable 5.1 and GPT-6 Astra found no critical or warning issues. The merge leaves the production patch unchanged. Accepted nits clarify this evidence, the E2E test title and release documentation targets.
Release impact
No data.json schema change, new migration code, public API change or privacy toggle. Existing startup protection moves newly recognized addresses to SecretStorage and retains the saved reference on reload. A device without that secret asks for Set feed address, which attaches under the existing reference. If SecretStorage fails, feeds work for the session, only placeholders are saved, and one Notice appears; after restart their addresses must be set again.
Known limitation, tracked in #438: an RSS description that quotes the private address still reaches parsed cache content and new notes through
{{description}}. This was reproduced on master and head. The cache is device-local and does not sync with the vault; notes can sync or be shared. This PR protects saved JSON, feed-address fields, generated feed links and cache keys. The neutral fixture does not establish quoted-content protection. Fixing the content flow crosses cache projections and template boundaries, so it is deferred to that issue. Existing notes are not rewritten.The optional scrub-once persistence optimization is deferred. No production conflict was needed when merging the transcript work; both adjacent E2E cache helpers were retained.
Docs at release
sync.md,faq.mdandimport_export.md, which currently describe only userinfo and auth-query detection, with the recognized forms for Supercast, Substack, Supporting Cast, Transistor, Pinecast, Captivate, RedCircle and Castos.No published documentation pages are changed in this PR.
Refs #438.
Note
Add publisher-specific private feed URL detection and protection for path-based secrets
isCredentialBearingUrlwithisPublisherPrivateFeed, which classifies path-embedded private feed addresses from Supercast, Substack, Supporting Cast, Transistor, Pinecast, Captivate, RedCircle, Podcache, and Castos. Subdomains are matched via the newisWithinhostname helper; Castos requires a non-emptyuuidquery value.priv3.supercast.com);serializeSettingscallers must now pass the private-feed-address map explicitly, since the parameter is no longer optional.serializeSettingscall sites for the now-required parameter.Macroscope summarized 3534f40.