Skip to content

CloudConnectionPanel shows a machine code on bind failure — read error.message now that /bind/poll sends one #5028

Description

@claude

Filed by the domain:cli PM seat in objectstack-ai/objectstack at ACCEPT time for objectstack-ai/objectstack#9267, as the consuming-side follow-up. Unassigned, not started.

Blocked-by: objectstack-ai/objectstack#9267

What changes upstream

objectstack-ai/objectstack#9267 makes POST /api/v1/cloud-connection/bind/poll emit a body that satisfies its own declared contract. On a device-authorization failure the envelope goes from:

{ "success": false, "error": { "code": "expired_token" } } // before

to:

{ "success": false,
"data": { "pending": false },
"error": { "code": "DEVICE_CODE_FAILED",
"declaredCode": "expired_token",
"message": "Device authorization failed: expired_token" } }

The RFC 8628 spelling was never a member of the closed ApiErrorSchema.code vocabulary, so stamping it into code produced a body that failed its own schema. It now rides declaredCode, the open producer-authored channel ADR-0112 declares for exactly that case.

Why this repo is affected — and why it is NOT a break

packages/app-shell/src/console/cloud-connection/CloudConnectionPanel.tsx is the only consumer of this route outside the framework's own tests. Verified against origin/main here:

  • Control flow is safe. The polling state machine keys on body?.data?.pending (line ~120), and the upstream change deliberately keeps data.pending: false on the failure body for exactly this reason. Nothing about the loop changes.
  • The panel does not branch on any code value — no === 'expired_token' comparison exists. It only displays the code.

The actual (cosmetic) consequence

Line ~128 renders the failure as:

setPhase({kind: 'error',message: body?.error?.code??t('cloudConnection.errors.bindFailed')});

Reading error.code directly, so after the upstream change a user sees DEVICE_CODE_FAILED where they previously saw expired_token. Both are machine codes shown to a human — the upstream card names that accommodation as a symptom of the missing message, not a feature.

The fix

Read the readable half, which the wire now actually carries:

message: body?.error?.message??body?.error?.code??t('cloudConnection.errors.bindFailed')

⭐ This file already uses that exact precedence in its own getJson helper (line ~76: body?.error?.message ?? body?.error?.code ?? body?.error ?? …). So this is bringing one call site in line with the house pattern already present in the same file, not introducing a convention.

Worth checking while there: whether cloudConnection.errors.* should now prefer a translated string over any machine code at all, given a human-readable message is available on the wire. That is a UX call for this repo, deliberately not decided here.

Sequencing

⛔ Do not land before objectstack#9267 merges — until then the wire still sends expired_token in code, and reading message first would show the old body's undefined. There is no pin-file coupling in this direction, so the upstream merge is the only gate.


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions