Skip to content

feat(account): require native consent for proxy callers - #641

Open
ODevStudio wants to merge 4 commits into
mainfrom
odev/issue-300-account-proxy-consent
Open

feat(account): require native consent for proxy callers#641
ODevStudio wants to merge 4 commits into
mainfrom
odev/issue-300-account-proxy-consent

Conversation

@ODevStudio

@ODevStudio ODevStudio commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

What changed, and why?

  • Require native approval before a skin, plugin, or named API client can use the
    linked Decent account through the shared proxy service.
  • Remember explicit allow and deny decisions per caller. Custom skins use a
    SHA-256 path key, concurrent requests from one caller share a prompt, and a
    30-second timeout fails closed without storing a denial.
  • Bind each served skin token to its immutable consent key, rotate both token
    and browser origin for every generation, and revoke the token on stop. Port
    3000 remains a no-store entry redirect.
  • Remove each timed-out consent dialog by its exact route so concurrent prompts
    cannot dismiss one another.
  • Add session-only CLI trust flags for headless use, document the new 403
    behavior, and add an account-consent smoke scenario.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore / infra
  • Plugin (DYE2 or bundled skin)

Scope (select all touched areas)

  • BLE transport / device comms
  • REST API / handlers
  • WebSocket API
  • Machine state / shot logic
  • Scale / weight / flow
  • Profiles / beans / grinders / workflows
  • WebUI skins
  • Plugins / JS runtime
  • UI / Flutter widgets
  • Storage / Drift database
  • CI / build / infra
  • Docs / specs

Linked Issues

Fixes #300

Root Cause (if bug fix)

N/A - this adds a missing native consent boundary around linked-account proxy
access.

Regression Test Plan (if bug fix or refactor)

N/A - security coverage spans account consent, REST proxy, plugin callers,
immutable custom-skin identity, token and origin rotation, CLI trust,
concurrent prompts, timeout, and storage failure.

Documentation Obligations (required)

  • API spec updated: assets/api/rest_v1.yml or assets/api/websocket_v1.yml (if REST/WebSocket changed)
  • API docs updated: doc/Api.md (if user-facing endpoint changed)
  • Plugin docs updated: doc/Plugins.md (if events/API changed)
  • Skin docs updated: doc/Skins.md (if skin behavior changed)
  • Profile docs updated: doc/Profiles.md (if profile handling changed)
  • Device docs updated: doc/DeviceManagement.md (if device flows changed)
  • N/A - no docs affected

The Decent app verification skill also documents the consent smoke scenario.

Security Impact (required)

  • New or changed REST endpoints? Yes; account-proxy requests can now return
    403 before any upstream request.
  • New or changed WebSocket topics? No.
  • New or changed network calls? No outbound calls; the local skin server now
    keeps port 3000 as an entry redirect and serves each generation on a fresh
    local port.
  • BLE/USB surface changed? No.
  • File system access changed? No; decisions use the existing credential store.
  • Plugin sandbox boundary changed? Yes; linked-account access now requires a
    trusted native consent decision per caller.
  • Risk and mitigation: Unknown identity, missing UI, timeout, storage failure,
    and skin-server startup failure all fail closed. Skin tokens carry immutable
    caller identities, old tokens and origins are retired, each timeout removes
    its own route, and headless trust is explicit and process-only.

User-Visible Changes

  • The first linked-account request from a skin, plugin, or named API client
    pauses for a native allow/deny dialog.
  • Explicit denial or timeout returns 403 before Decaid sends credentials or
    contacts the upstream service.
  • Existing http://<host>:3000 skin links continue to work through a no-store
    redirect to the active per-generation origin.
  • Headless deployments must opt in with a session-only trust flag.

Verification

Local gates (run before pushing)

  • dart format lib test - no remaining candidate changes
  • flutter analyze - clean
  • flutter test - 3,239 passed, 1 skipped
  • ./scripts/fetch_dye2_plugin.sh - not rerun for this local draft

Manual verification (if applicable)

  • OS / platform tested: Android 15 on Samsung Galaxy Tab A9+ SM-X210, 1920x1200 landscape; Windows host.
  • Simulated devices? (simulate=1): Yes.
  • Real hardware? (DE1/Bengle/scale): Samsung tablet over USB; no espresso hardware, not applicable.
  • What you personally verified and how: The exact pushed commit 24b76ef7 built and installed on the tablet. A temporary launch harness invoked the production AccountConsentPrompter. The user confirmed the native dialog rendered on the tablet and personally selected Allow. The focused consent suite passed 5/5, covering Allow, Deny, timeout, concurrent route removal, and unavailable UI.
  • Edge cases checked: Unknown caller, missing skin identity, stale skin tabs, server stop, concurrent callers, storage failure, unavailable UI, timeout, allow, deny, and session trust.
  • What you did not verify: A linked-account request end to end, or manual Deny/timeout behavior on Android. Those paths remain covered by automated tests.

Evidence

  • Test output (failing before + passing after)
  • Log snippets
  • Screenshot / recording (UI changes)
  • curl / websocat output (API changes)

Detailed evidence:

  • dart format lib test, followed by
    dart format --output=none --set-exit-if-changed on all review-changed Dart
    files (12 checked, 0 changed)
  • Focused account, proxy, handler, CLI, and plugin tests with the package-matched
    QuickJS DLL on PATH (84 passed)
  • Focused stale-origin, token-revocation, navigation, identity, and concurrent
    timeout regressions (52 passed)
  • flutter analyze (no issues)
  • Full flutter test with the package-matched QuickJS DLL on PATH (3,239
    passed, 1 skipped)
  • Flutter's default Windows path still selected CMake 3.20, but the unchanged
    source built with installed CMake 3.28 and Visual Studio 2022.
  • Samsung Galaxy Tab A9+ SM-X210 (Android 15, 1920x1200 landscape): exact pushed
    commit built and installed; the production consent dialog was invoked through
    a temporary local harness, and the user personally selected Allow.
  • flutter test test/account/account_consent_prompter_test.dart on the review
    commit: 5 passed.
  • Review follow-up commit 09c0a61 updates both consent token extractions to use curl -sL, following port 3000's 307 redirect to the active generation port.
  • No live linked-account smoke ran.

Compatibility & Migration

  • Backward compatible? No; unapproved account-proxy callers now receive
    403 instead of silently using the linked account. Browser clients must also
    follow the port 3000 redirect to the active high-numbered skin port.
  • Config / env changes needed? Yes for headless deployments, which must pass
    --trust-consent=<caller-key> or --trust-all-consent for that process.
  • Database migration needed? No; the existing credential store persists
    account_proxy_consent decisions.
  • Exact steps: Interactive users answer the first-use prompt. Headless owners
    explicitly select the narrow caller trust flag where possible.

Risks & Mitigations

  • Risk: A missing prompt path could lock out legitimate account access.
    • Mitigation: Fail closed with a bounded timeout, expose explicit headless
      trust flags, and cover UI-unavailable and storage failures.
  • Risk: Consent identity could collide across custom skins.
    • Mitigation: Derive the caller key from the skin path with SHA-256 and keep
      decisions per caller.
  • Risk: A stale skin tab could reuse another skin's approval or token.
    • Mitigation: Bind the token to the served skin key, rotate token and origin
      together, revoke on stop, and never reuse an origin within the process.
  • Risk: Two concurrent timeout callbacks could dismiss the wrong dialog.
    • Mitigation: Each timer removes its exact ShadDialogRoute.

Contributor Responsibility

AI-assisted development is allowed. The submitter remains responsible for the submitted work.

  • I have reviewed and understand all changes in this PR and take responsibility for their correctness, security, behavior, licensing, and provenance, including any AI-assisted or AI-generated work.

@tadelv

tadelv commented Aug 20, 2026

Copy link
Copy Markdown
Member

fyi: will need rebase due to #650

Rotate the skin token and browser origin for each served generation so stale pages cannot inherit another skin's consent. Remove timed-out consent dialogs by exact route and keep the port 3000 entry URL as a no-store redirect.
@ODevStudio
ODevStudio force-pushed the odev/issue-300-account-proxy-consent branch from 1903cd5 to 24b76ef Compare August 21, 2026 08:19
@ODevStudio
ODevStudio marked this pull request as ready for review August 22, 2026 14:07
@ODevStudio
ODevStudio requested a review from tadelv August 22, 2026 15:16
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.

Account proxy: client consent prompt over active view

2 participants