Commit Graph

3 Commits

Author SHA1 Message Date
Keysat d0e98424c1 Fix five P0/P1 security & correctness findings from the full-eval
- Arbitrary file write (P0): validate import keys in /api/library/import via
  a now-exported safeFilename(); a ../../ key is skipped, not written out of
  the scope dir.
- SSRF (P0): guard downloadPodcastAudio — reject non-HTTP(S) schemes, block
  IP-literal and DNS-resolved private/link-local/loopback/reserved/multicast
  and embedded-IPv4 IPv6 targets (closes DNS rebinding), cap + resolve redirects.
- ESM require (P1): top-level import of randomBytes in license-purchase.js
  (the inner require threw on the anon purchase-settle path).
- Concurrency lock (P1): skip the process-global free-tier slot in multi-mode
  so it no longer serializes every cloud tenant onto one job.
- X-Forwarded-For bypass (P1): set Express trust proxy from
  RECAP_TRUSTED_PROXY_HOPS (default 1); getClientIp now reads req.ip instead
  of a client-spoofable XFF entry.

Tests added for safeFilename, the SSRF guard, and getClientIp (119 pass).
Registry blockers deferred (ROADMAP); leaked-key history purge queued.
2026-06-15 13:36:40 -05:00
Keysat 0ae59f3550 Add multi-tenant cloud mode: self-serve purchase, credit metering, core-decoupling
Introduces RECAP_MODE=multi alongside single-mode self-host:
- Tenant auth + accounts (magic-link via System SMTP), per-tenant credit pool,
  anonymous trial minting with per-IP/-64 caps
- Self-serve Pro/Max purchase: inline Lightning (BTCPay) + card (Zaprite),
  prepaid 30-day periods, expiry-reminder emails
- Core-decoupling: relay owns cloud tier/expiry keyed by Recaps user-id
- SQLite (better-sqlite3) schema for multi-mode; filesystem unchanged for single
- StartOS actions/versions through 0.2.155
2026-06-13 14:25:05 -05:00
Keysat 85cb641044 Module split: history storage + meta + 9 routes → server/history.js
• initHistory({ dataDir })          — boot setup; mkdir + path init
  • saveToHistory(...)                — write one summary file
  • loadMeta() / saveMeta(meta)       — _meta.json folder structure
  • setupHistoryRoutes(app, deps)     — registers GET /api/history,
                                        GET/PUT/DELETE /api/history/:id,
                                        PUT /api/history/:id/title,
                                        PUT /api/history/meta,
                                        POST/PUT/DELETE /api/history/folders[/:id],
                                        PUT /api/history/folders/:id/collapsed,
                                        PUT /api/history/move
  • getHistoryDir()                   — exposes the directory for code
                                        that hasn't been extracted yet
                                        (subscriptions / library / process
                                        pipeline)

The DELETE route needs to add the deleted videoId to the skip list so
subscriptions don't re-queue it. That's a cross-module concern, so
setupHistoryRoutes takes addToSkipList as a dependency. For now it's
late-bound to the still-local function in index.js (lambda captures
the scope, not the value); when subscriptions are extracted next, the
import flips cleanly.

server/index.js: 2300 → 2079 lines.

Smoke tested: /api/license-status, /api/health, and /api/history (402
license-gated for unlicensed) all respond as expected.
2026-05-08 17:09:10 -05:00