Commit Graph
9 Commits
Author SHA1 Message Date
ssmithxandClaude Sonnet 5 db8eb27e5f fix(podcasts): serialize explicit as a real boolean in API responses
SQLite has no boolean type, so raw rows return explicit as 0/1. The
podcast edit form round-trips whatever GET /api/podcasts/:id sends it,
and the update schema requires z.boolean() — so saving any change
without also touching the explicit checkbox failed validation with
"Expected boolean, received number".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 15:42:02 +00:00
ssmithxandClaude Sonnet 5 5ebea55353 feat(episodes): let owners remove episodes from the RSS feed
Adds an "unlisted" flag on episodes rather than reusing the existing
hard-delete route, since a hard delete cascades to purchases/earnings
(ON DELETE CASCADE) and would wipe a producer's sales history and any
unwithdrawn earnings for that episode. Unlisting only affects feed.xml
output — the episode, its purchases, and reseller listings all stay
intact and it can be relisted at any time.

Wires up the missing frontend for it too: the podcast settings page
had no episode list or management controls at all before this, despite
the backend already exposing full episode CRUD.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 21:14:23 +00:00
ssmithxandClaude Sonnet 5 69241a28b1 feat(auth): add an admin-managed login allowlist
Lets the admin restrict which pubkeys may log in, enforced server-side
at /api/auth/login before a session is issued. Disabled by default;
the admin and the bootstrap (no-admin-claimed-yet) case always pass.
Manageable via the existing settings UI/API (npub or hex, one per line).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 18:33:21 +00:00
ssmithxandClaude Sonnet 5 e04d35b131 feat: recorded-episode pricing, Cashu-token payment, podcast editing
- Editable podcast settings: new /podcasts/:id/settings page, reusing
  PodcastForm.vue in an edit mode (PUT instead of POST) since it was
  previously create-only with no way to fix a field (e.g. lightning
  address) after the fact.
- Recorded episodes can now be priced same as uploads: "Publish
  recording" gained an optional price_sats field, wired through the
  existing episode paywall machinery. Live streams themselves stay
  unpaywalled by design — only the resulting recording can be priced.
- Accept Cashu tokens as an alternative to a Lightning invoice:
  POST .../purchase/token redeems a pasted token directly (via the
  mint's swap/receive flow) and finalizes the purchase in one step,
  no quote/confirm round trip. A token worth more than the price is
  treated as a tip (seller gets the full amount); worth less is
  rejected. Added a "pay with a Cashu token instead" option next to
  the existing invoice flow.
- cashu.ts: fixed payout() always requesting an invoice for the full
  held balance with no room for the mint's routing-fee reserve, which
  made a balance that exactly matched one sale's price permanently
  unwithdrawable (needed slightly more than held to cover the fee).
  Now shrinks the request and requotes once if the first quote doesn't
  fit.
- docker-compose.yml / mediamtx.yml: renamed the podsteadr container's
  DNS alias away from the literal string "podsteadr" — on a host whose
  own hostname is "podsteadr", cloud-init's self-hostname /etc/hosts
  entry shadowed the container-network alias, so mediamtx's auth
  webhook callback resolved to the wrong address and rejected every
  RTMP publish attempt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-30 23:18:36 +00:00
ssmithx a050e2e5f9 ui changes 2026-07-30 13:48:45 +00:00
ssmithxandClaude Sonnet 5 e92ef7fef9 feat: Cashu marketplace for paid episodes, resale, and cross-instance discovery
Paid episodes:
- server/services/cashu.ts: self-custodied Cashu wallet against a configured
  mint (NUT-04 mint quote -> bolt11 invoice -> mint/store proofs -> LNURL-pay
  payout on withdraw). Buyers pay a plain Lightning invoice, no Cashu wallet
  needed on their end.
- routes/marketplace.ts: purchase/confirm flow, download-url paywall gate,
  reseller certification/revocation, earnings ledger + withdraw.
- services/marketplace.ts: producer + certified-reseller source resolution,
  with a naive per-seller sales-count reputation signal.

Discovery:
- services/rss.ts: <podsteadr:source> RSS tag on priced episodes (producer +
  resellers, price, sales count, url) so pricing/sources are discoverable
  straight from the feed, not just a separate API call. Locked episodes point
  their <enclosure> at an info page instead of the raw file.
- routes/feeds.ts: /catalog.opml lists every podcast this instance hosts, for
  peer podsteadr servers or any OPML-aware crawler to discover without a
  central directory.

Frontend: episode wizard price/reseller controls, sources display, earnings
dashboard.

Also fixes CORS and a container-image reference:
- app.ts: register @fastify/cors, open on the catalog/feed/sources endpoints
  (already deliberately public/crawlable) and on purchase/confirm (already
  accept stateless NIP-98 header auth for exactly this case). Credentials
  stay off, so the session cookie never crosses origins — cookie-authed
  admin routes stay same-origin-only. Needed so external clients like
  podsteadr-player can browse/buy/play from a different origin.
- docker-compose.yml: fully qualify the mediamtx image reference
  (docker.io/bluenviron/mediamtx:1.19.2) — some Podman hosts have no
  unqualified-search registry configured and fail to resolve short names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 12:43:46 +00:00
ssmithx 2d61abc40c chore: rename project from podpuddle to podsteadr
Renames the repo directory and every podpuddle/PODPUDDLE reference
across code, config, and docs to podsteadr/PODSTEADR (package names,
Docker Compose project/service/volume names, env var names, UI/RSS
strings). Existing Docker volume data (uploaded blobs, mediamtx
recordings, the server's sqlite DB and its nostr identity key) was
migrated to new podsteadr_-prefixed volumes with matching filenames
so it isn't orphaned by the rename.
2026-07-25 01:11:42 +00:00
ssmithxandClaude Fable 5 6dd541b1a4 fix: blossom v4 config schema, container-internal blossom URL, e2e suite
- blossom-server 4.4.1 expects rules nested under storage: — the top-level
  rules list was ignored, leaving an empty ruleset that rejected all uploads
- podpuddle now verifies blobs and uploads recordings via BLOSSOM_URL_INTERNAL
  (http://blossom:3000) while feeds keep the browser-facing URL; unreachable
  blossom now returns 502 instead of a 500
- mediamtx: fix deprecated allow-origin params, disable MoQ
- scripts/e2e.mjs: 14 API checks (login, replay/URL rejection, upload,
  episode, feed, stream keys, mediamtx auth) — all green, plus verified
  live: RTMP publish with key → HLS 200, wrong key refused, status
  planned→live→ended via poller, recording remuxed + published as episode,
  feed XML well-formed with 2 items

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:21:03 +00:00
ssmithxandClaude Fable 5 594a9a8783 feat(server): podpuddle scaffold + Fastify backend (nostr auth, RSS, streams)
- docker-compose stack: podpuddle + MediaMTX (RTMP/WHIP/HLS) + blossom-server
- NIP-98 nostr-only login with session cookies, replay guard, clock-skew window
- podcasts/episodes CRUD; episodes register browser-uploaded blossom blobs
- RSS 2.0 + itunes + podcast namespace feeds with lnaddress value blocks
  (podcast:guid UUIDv5 verified against the spec vector)
- streams API with hashed stream keys; MediaMTX http-auth webhook
  (query/password/bearer forms); API poller flips live/ended status
- NIP-53 kind 30311 live events published with the server's nostr identity
- recordings: ffmpeg remux + server-key blossom upload → podcast episode
- 35 vitest tests green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:01:38 +00:00