iPad users hit a spurious "network error" on the first tap of
"Send sign-in link", with a second tap succeeding. Cause is iOS
Safari dispatching the POST onto a pooled keep-alive socket the
server/proxy already closed; unlike a GET it isn't transparently
re-sent, so it surfaces as a transport TypeError. The single 500ms
auto-retry was too quick and reused the same dead socket.
Both sign-in entry points (auth.html postWithRetry, index.html
fetchWithRetry) now retry 3x with growing backoff (0 -> +400ms ->
+1.6s) to outlast Safari evicting the socket. Frontend-only.
Ships as 0.2.156.
Refresh AGENTS.md conventions (client IP via req.ip; safeFilename exported),
rewrite Current state to a lean snapshot, move the P2 known-debt detail to
ROADMAP.md.
- 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.
Cross-repo git-hygiene audit remediation: surface ~/Projects/standards/INBOX.md items at session start, and switch .gitignore to the deny-by-default .claude/* block (shared wiring allow-listed) plus the canonical secrets/env lines — per standards/portability.md.
- Replace the Commands-table Lint/Type-check TODOs with the real, verified
commands: `npm run check` (tsc --noEmit over startos/) and `npm run prettier`.
There is no ESLint/linter; server/ JS is untooled.
- Move the client-side relay contract (env vars, /relay/* endpoints, X-Recap-*
headers, file map) out of AGENTS.md into docs/guides/relay-client.md with
paths: frontmatter, lazy-loaded via a .claude/rules symlink; AGENTS.md keeps
a one-line pointer.
- Un-ignore .claude/rules/ so the guide auto-attaches in any clone, while
.claude/ local state (worktrees, plans) stays ignored.
Add the /relay/* endpoints the app actually calls that were omitted (capabilities, policy, tts, jobs/:id, credits/*); fix the Files attribution (add relay-capabilities.js + credits-purchase.js; the /relay/policy proxy lives in index.js only).
- Add Client-side contract with the relay sub-section: env vars
(RECAP_RELAY_BASE_URL, RECAP_RELAY_OPERATOR_KEY ↔ relay_cloud_operator_key),
auth direction the client SENDS, the 12 /relay/* endpoints the consumer
actually calls (verified against providers/relay.js + billing-routes.js +
subscription-reminders.js).
- Drop two relay-internal references now canonical in ../recap-relay/AGENTS.md:
the extendUserTier function name and the Adjacent-repo bullet's
"Private; ships via make install only" sentence.