Skip to content

Add hosted proxy option with informed consent dialog - #131

Merged
patrickunterwegs merged 15 commits into
mainfrom
claude/proxy-trust-disclosure-settings-4v98z4
Sep 3, 2026
Merged

Add hosted proxy option with informed consent dialog#131
patrickunterwegs merged 15 commits into
mainfrom
claude/proxy-trust-disclosure-settings-4v98z4

Conversation

@patrickunterwegs

Copy link
Copy Markdown
Member

Summary

This PR introduces a hosted CORS proxy option for web users who cannot self-host, with an informed-consent flow that ensures users understand the security implications before using it.

Key Changes

  • New Hosted Proxy Option: Added https://spectacled-proxy.fly.dev as an alternative to self-hosted proxies, operated by Techbee
  • Consent Tracking: Added hostedProxyConsentUrl to UserAppPreferencesStore to track which proxy URL the user has explicitly consented to use
  • Trust Dialog: Created new ProxyTrustDialog component that:
    • Clearly explains that credentials pass through the hosted server
    • Requires explicit checkbox acknowledgment before enabling the confirm button
    • Offers self-hosting alternative with direct link to setup instructions
    • Re-displays the disclosure when the hosted proxy is active in settings
  • Redesigned Settings UI: Replaced dropdown menu with radio-button style cards for proxy selection:
    • "My own proxy server" option (recommended) with manual entry and localhost preset
    • "Spectacled proxy (hosted by us)" option with warning badge
    • Conditional disclosure card shown only when hosted proxy is active
  • State Management: Added ownProxyServerDraft to preserve user's custom proxy URL while hosted proxy is selected, allowing seamless switching back
  • Documentation: Updated README and server documentation to explain the hosted proxy, its security model, and when it should be used

Implementation Details

  • The hosted proxy URL is stored as a constant in HttpClientFactory alongside the setup instructions URL
  • Consent is per-URL (not a boolean flag) so that if the hosted proxy URL ever changes, users must re-consent
  • The settings page keeps the security disclosure visible while the hosted proxy is active, ensuring users don't forget what they agreed to
  • All new UI strings are properly localized with resource references

https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX

The web build needs a CORS proxy, and the "spectacled Proxy on Fly.io"
entry in the settings dropdown could be picked with a single tap - even
though that proxy terminates TLS and therefore sees the CalDAV
credentials of everyone who uses it.

Settings -> More now presents the proxy as a choice between two named
options instead of a dropdown of URLs:

- "My own proxy server" (recommended, default), with the URL field and
  the localhost/setup shortcuts underneath it,
- "Spectacled proxy (hosted by us)", badged "Sees your credentials".

Picking the hosted one opens ProxyTrustDialog, which states what the
operator can see, what we do and don't log, that this is unverifiable
from the outside, and the safer alternatives (self-hosting, an
app-specific password). Its confirm button stays disabled until the
acknowledgement checkbox is ticked. Consent is stored as the URL it was
given for, so it never carries over silently to another instance, and
while the hosted proxy is active the settings page keeps the disclosure
on screen with shortcuts to review it or switch back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX
Unit tests have been failing on main since eb8f4d8, first at
:kotlinStoreYarnLock and, once 1020b01 fixed the JS lock, at
:kotlinWasmStoreYarnLock - both with "Lock file was changed". The failure
reproduces on main's own head (8b4d034) and has nothing to do with this
branch, which touches no npm dependency.

kotlin-js-store/wasm/yarn.lock carries the same leftover entry that
1020b01 removed from kotlin-js-store/yarn.lock: an exact-pinned ws@8.18.0
that no dependency in the file requests at any range, left behind when the
pin moved to 8.20.1. This applies that same deletion to the Wasm lock.

Regenerating it properly (./gradlew kotlinWasmUpgradeYarnLock) was not
possible in the environment this was written in, so CI is the check on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX

Copy link
Copy Markdown
Member Author

CI: the yarn-lock failure is main's, not this PR's

The first run here failed at :kotlinStoreYarnLock ("Lock file was changed"). That is not caused by this branch — it touches no npm dependency — and it reproduces on main's own head:

commit failing task
eb8f4d8, 27a0347 (main) :kotlinStoreYarnLock
1020b01 (main, "Update yarn.lock") :kotlinWasmStoreYarnLock
8b4d034 (main head, run 33739867448) :kotlinWasmStoreYarnLock

Two things were wrong with the branch state, both now addressed:

  1. The branch was cut from 27a0347, two commits behind main, so it predated the 1020b01 lock fix — that is why the first run stopped at the JS lock. Merged origin/main in (d8b3293).

  2. kotlin-js-store/wasm/yarn.lock carries the same stale entry that 1020b01 removed from the JS lock: an exact-pinned ws@8.18.0 that nothing in the file requests at any range, left behind when the pin moved to 8.20.1. Applied the same deletion to the Wasm lock (4f53be9). The progression in the table is the evidence: fixing the JS lock moved the failure from the JS check to the Wasm check.

One caveat on the second commit: ./gradlew kotlinWasmUpgradeYarnLock could not be run in the environment this was written in (the sandbox blocks dl.google.com and Maven Central, so Gradle cannot even resolve the Android plugin), so that deletion mirrors the fix already validated on the sibling file rather than being a regenerated lock. If CI disagrees, drop 4f53be9 and regenerate the file locally with that task — and it is worth landing on main either way, since main is red without it.

Worth noting from the first run: compileKotlinJs, compileKotlinJvm and compileKotlinIosSimulatorArm64 all succeeded on this branch's code, so the new settings UI compiles across targets. The previews still want a human eye.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

CI is green on dd58155 (run 33741070902) — closing out the caveat from my comment above.

The hand-applied Wasm lock deletion held: run 33740638015 passed on 4f53be9 before e761a11 arrived through the merge. Since that commit on main makes the identical deletion, 4f53be9 is now a no-op — the branch's net diff against main is just the seven files of the actual change — so nothing needs dropping either way.

No action needed on the lock commits. The change itself is unreviewed as a design: the trust dialog's wording and the persistent disclosure card are the parts worth a human eye, along with the two previews (SettingsMorePage_Preview, ProxyTrustDialog_Preview).


Generated by Claude Code

claude and others added 9 commits September 3, 2026 10:17
The "Use local development proxy" text button spent permanent vertical
space on a dev-only affordance. A trailing-icon menu on the field is the
usual place for presets that fill it, and it has room for more entries if
another preset ever shows up.

Only own-server presets live in it. The hosted proxy stays out: putting it
one tap away in a menu is exactly what this branch replaced, since
selecting it has to go through the consent dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX
On the web build every CalDAV request goes through the proxy, so a user
who reaches the add-account sheet without one configured can pick either
option and only find out at the first request - which fails looking like
an unreachable server rather than a missing setup step.

The selection page now asks that question first on WASM: an explanatory
card carrying the proxy picker, with both option cards disabled until a
proxy is set. Once it is, the card collapses to a one-line confirmation
with a Change affordance, so returning users and every native platform
see the flow exactly as before.

The picker itself moves out of SettingsMorePage into ProxyServerSetup so
both entry points share one implementation. That matters beyond reuse: a
second copy of this UI would be a second chance to get the hosted proxy
selected without its consent dialog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX
e1be6e6 dropped defaultProxyUrl(), so there is no platform default to fall
back to any more - but the comment above proxyUrlProvider still described
one. Say what the code now does, and why that is the point: an
unconfigured web build fails at the browser rather than at a localhost
address nobody is listening on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwACpV4rjwUXSZaBpZGLiX
@patrickunterwegs
patrickunterwegs merged commit 67f72da into main Sep 3, 2026
1 check passed
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.

2 participants