Skip to content

feat(feeds): protect documented private feed addresses - #440

Merged
chhoumann merged 3 commits into
masterfrom
feat/path-secret-hosts
Oct 13, 2026
Merged

chhoumann merged 3 commits into
masterfrom
feat/path-secret-hosts

Conversation

@chhoumann

@chhoumann chhoumann commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

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.

Publisher Recognized host and address rule Evidence Counter-test kind and example
Supercast supercast.com, supercast.tech and their subdomains: /feed/<key>.rss or /feeds/<token>... Official getSubscriber API identifies the feed key as a secret; publisher contribution supplies the legacy form and host family and says feeds are individualized. Public sibling feed on an external host: Mega links feeds.megaphone.fm/mega; premium on mega.supercast.com. Same-host non-feed landing and blog pages also stay public.
Substack substack.com and subdomains: /feed/podcast/<digits>[/s/<digits>]/private/<token>.rss Publisher privacy docs; exact layouts in redacted real examples with a section and without. Same-host public feeds: api.substack.com/feed/podcast/87281.rss and /feed/podcast/87281/s/87957.rss. Other /private/ routes stay public.
Supporting Cast Subdomains of supportingcast.fm: /content/<token>.rss Official API schema, UserFeed.url, explicitly calls this the user-specific URL. Same-host non-private paths: network landing page and a .png content path. No public RSS feed on that host was confirmed.
Transistor Exactly subscribers.transistor.fm, with a nonempty path Official API supplies the unique subscriber RSS URL. Public sibling host: feeds.transistor.fm/the-caffeine-show. Empty paths are also tested; no same-host public feed is claimed.
Pinecast Exactly pinecast.com: /feed/<show>/sub/<subscriber-id>... Publisher private-feed example. Same-host public feed: pinecast.com/feed/example, from the publisher redirect guide.
Captivate Exactly private.captivate.fm, with a nonempty path Publisher-submitted pattern; private podcast FAQ. Public sibling host: feeds.captivate.fm/insider/. Empty paths are also tested; no same-host public feed is claimed.
RedCircle Exactly feeds.redcircle.com: /<id>/pf/<token>...; exactly api.podcache.net: /shows/<id>/pf/<token>... Publisher contribution and its example corpus. Same-host public feeds on both hosts: feeds.redcircle.com/<id> and api.podcache.net/shows/<id>/feed.
Castos Subdomains of castos.com, with a nonempty uuid query value Publisher contribution; publisher explanation says a generic private feed adds uuid to its public URL. Same-host public feed: myshow.castos.com/feed without uuid; feeds.castos.com/abcd1234 also stays public. uuid on 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:e2e passed all 89 tests in 13 files on Obsidian 1.14.4 (303.88 s). PODNOTES_OBSIDIAN_APP=/opt/Obsidian/obsidian npm run test:e2e passed 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

  • Update sync.md, faq.md and import_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.
  • Explain that the first startup after upgrading automatically moves matching saved addresses out of data.json.
  • Tell users to update PodNotes on every device and paste the address once on each additional device through Set feed address.
  • State that unsupported hosts and custom domains are not automatically detected, and existing notes are not rewritten.

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

  • Extends isCredentialBearingUrl with isPublisherPrivateFeed, which classifies path-embedded private feed addresses from Supercast, Substack, Supporting Cast, Transistor, Pinecast, Captivate, RedCircle, Podcache, and Castos. Subdomains are matched via the new isWithin hostname helper; Castos requires a non-empty uuid query value.
  • These addresses are now redacted from persisted settings and nested episode content, migrated into SecretStorage when loading a 2.21.1 library, and treated as session-only when the device has no stored secret.
  • Adds E2E coverage using a Supercast-shaped fixture host and path, verifying the secret stays out of notes, feed cache, and persisted data, and that removing and restoring the secret keeps the same feed reference.
  • The E2E fixture server and instance resolver now support a custom feed host/path (priv3.supercast.com); serializeSettings callers must now pass the private-feed-address map explicitly, since the parameter is no longer optional.
  • Behavioral Change: feed URLs matching the new publisher path patterns are now classified as credential-bearing and removed from persisted data; previously they were stored in the clear. Check serializeSettings call sites for the now-required parameter.

Macroscope summarized 3534f40.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3534f40
Status: ✅  Deploy successful!
Preview URL: https://0e6db3cb.podnotes.pages.dev
Branch Preview URL: https://feat-path-secret-hosts.podnotes.pages.dev

View logs

@chhoumann
chhoumann merged commit 921be0d into master Oct 13, 2026
16 checks passed
@chhoumann
chhoumann deleted the feat/path-secret-hosts branch October 13, 2026 04:28
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