Skip to content

feat(dev/tunnel): surface platform rejections + auto-refresh access token#32

Merged
I-am-nothing merged 3 commits into
mainfrom
feat/dev-tunnel-resilience
May 19, 2026
Merged

feat(dev/tunnel): surface platform rejections + auto-refresh access token#32
I-am-nothing merged 3 commits into
mainfrom
feat/dev-tunnel-resilience

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

Summary

Two resilience fixes for `mirrorstack dev --tunnel` caught running the flow end-to-end against the slug-aware platform (api-platform #165):

  1. `await_register_ack` ignored `rpc.err`. The 10s timeout was firing while the platform's rejection (`module_dev_mode_off`, `module_not_yours`) sat unread on the wire. Now correlates `rpc.err` frames by the register id, decodes the ErrorPayload (incl. the new `slug` field from #165), and returns a typed `RegisterError` variant.

  2. Caller maps `RegisterError` → user-actionable output.

    • `ModuleDevModeOff` → warning + deep-link URL (localhost:3001 in dev, `https://apps.` for `api.` in prod, scoped to `/dev/module//dev` when the slug is carried) + open-on-Enter prompt.
    • `ModuleNotYours` → explicit "not owned by you" message with the `mirrorstack module list` cross-check hint.
    • `Rejected` → generic `register rejected (): ` so future error codes surface without a CLI change.
  3. Silent access-token refresh. `tunnel_token`'s 401 path now calls `POST /v1/auth/sessions/refresh` with the stored refresh token (30-day TTL), persists the rotated pair to `credentials.json`, and retries the mint once. Without this, the 15-min access TTL killed long-running `mirrorstack dev` sessions even though the user's session was still valid for weeks.

Pairs with

  • api-platform #165 — must land first (or alongside) for the slug field to flow through.

Test plan

  • `cargo build --release` clean
  • `cargo test` — 64/64 passing
  • Visual: `mirrorstack dev --tunnel` with module's `dev_mode_enabled=false` → warning + browser opens `http://localhost:3001/dev/module//dev`.
  • Visual: idle past 15 min, retry — silent refresh, no "session expired".
  • Visual: register under another user's module (manually-edited Config.ID) → `ModuleNotYours` message.

🤖 Generated with Claude Code

I-am-nothing and others added 3 commits May 20, 2026 03:20
…oken

Two related resilience fixes for `mirrorstack dev --tunnel` after running
the flow end-to-end against the slug-aware platform:

1. await_register_ack ignored rpc.err — the 10s timeout was firing while
   the platform's rejection (module_dev_mode_off, module_not_yours) sat
   unread on the wire. Now correlates rpc.err frames by the register
   id, decodes the ErrorPayload (incl. the new optional slug field from
   api-platform #165), and returns a typed RegisterError variant.

2. Caller in dev/mod.rs maps RegisterError → user-actionable output:
   - ModuleDevModeOff: prints a warning, builds the dev-console URL
     (localhost:3001 for dev hosts, https://apps.<host> for api.<host>
     production, deep-linked to /dev/module/<slug>/dev when the slug
     is carried in the payload, falling back to /dev otherwise), and
     opens it on Enter.
   - ModuleNotYours: explicit "this module isn't owned by you" message
     with the `mirrorstack module list` cross-check hint.
   - Rejected: generic "register rejected (<code>): <message>" so
     future error codes surface without a CLI change.

3. Silent access-token refresh — tunnel_token's 401 path now calls
   POST /v1/auth/sessions/refresh with the stored refresh token (30-day
   TTL), persists the rotated pair to credentials.json, and retries the
   mint once. Without this, the 15-minute access TTL killed long-running
   `mirrorstack dev` sessions even though the user's session was still
   valid for weeks. The refresh layer is scoped to the tunnel-token call
   site for now; can generalize as more long-running calls land.

CLI tests stay 64-passing — these paths are integration-shaped and the
existing test surface doesn't mock the WS frame plane.

Co-Authored-By: Sheng Kun Chang <nothingchang@mirrorstack.ai>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gisterResult alias

Three follow-ons from the simplify review on this PR (api-platform side
was reviewed and came back clean, no fixes needed):

- Add From<serde_json::Error> for RegisterError so the `?` operator on
  serde_json calls converts to RegisterError directly. tunnel.rs::open
  drops eight `.map_err(RegisterError::from)?` adornments — the From
  impl + the existing From<anyhow::Error> impl do the work.
- Drop the double-wrap on serde_json::to_string (was
  `.map_err(|e| RegisterError::from(anyhow!(e)))?`, now just `?`).
- Hoist `type RegisterResult<T> = std::result::Result<T, RegisterError>`
  so the two handshake functions read with the same Result shape as the
  rest of the file.
- await_register_ack's "ignore non-corr_id frames" comment was inside
  the match against frame_type, but the corr_id check happens above it
  — move the comment to the function level where the policy lives.

No behavior change. cargo test stays 64/64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI's `cargo fmt --check` rejected the localhost/loopback match in
dev_console_url because the multi-arm `Some("...") | Some("...") | ...`
form was wrapped onto a long single line under rustfmt's normalize.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 80b97b2 into main May 19, 2026
5 checks 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.

1 participant