v0.2.35
tagged this
30 Jul 02:26
Groundwork for the operator Share/Intake UI, but two of these are live bugs that stand on their own — the UI would only have made them easy to hit. **Portals leaked across users of a tenant.** IntakeModule never recorded an `owner` and listPortals never filtered on one, so a token scoped with `owner_only` listed and could revoke every other user's portals. `/api/fm/intake/list` has been routed all along, so this needed nothing but a direct API call. Share has had both since it shipped; Intake now matches. Migration is fail-closed on purpose: a pre-existing portal has no `owner`, so under `owner_only` it goes invisible and un-revokable while staying live for its visitors until it expires (the 90-day cap self-clears). The remedy is an operator token without `owner_only`. The `owner` is NOT backfilled on read — that would hand ownership to whoever lists the record first. **A revoked portal could come back.** revokePortal used `unset` while receiveUpload rewrites the whole tenant map on every upload, so an interleaved upload restored the record. Revoke now writes a tombstone and putRecord refuses to write over one — the fix Share got last week for the identical race. The read-modify-write race itself is unchanged and still accepted; do not add a lock or a queue for it. **Proxy-mode hosts were promised features they cannot reach.** Both adapters forwarded allow_share/allow_intake unconditionally although none of those endpoints are proxied, so a Laravel/WP host would render controls for routes that 404 — exactly what allow_terminal is standalone-gated to avoid nine lines up in the same file. Laravel now gates them (plus the share/intake link config) on standalone mode; WordPress drops them as dead config. The `edition: pro|agency|enterprise` presets seed those claims directly into the payload, so the proxy path unsets them too — without that the gate looks right and leaks anyway. **intake_base_url** finally gives intake a recipient URL; `POST /api/fm/intake` returned none, so there was nothing to hand a visitor. Mirrors share_base_url, falling back to the request origin — so set it explicitly behind a proxy/CDN. The duplicated fallback is now one `ff_public_link_url()` helper, testable in free core without a paid module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>