fix(apps): full app UUID in apps list; reject partial app ids at authz boundary - #468
Merged
Conversation
…thz boundary `hands apps list` printed only the first 8 chars of each app UUID, so pasting that id into `/api/apps/:appId/...` (e.g. testflight-upload) never matched an app and fell through to the role check, returning a misleading `INSUFFICIENT_APP_ROLE` / `app_role=none` instead of an id error. - CLI: `apps list` prints the full app id (what other commands and the API expect). - Worker: shape gate at the shared authz boundary (`ensureAppRole` / `ensureAppPermission`, before any DB lookup or role check). An id built solely from UUID characters (hex + dashes) that is not a complete UUID — a truncated or partial id — returns `400 EXACT_APP_ID_REQUIRED`. Shape-only: a well-formed UUID that does not exist still returns the normal indistinguishable role error, so it never becomes an app-existence oracle; synthetic slug-like ids (which contain non-hex characters) are untouched. Reported by Codex-Android-DevOPS during a TestFlight upload; oracle-safety of the API branch per CC-Quiver-Owner review. Signed-off-by: Rhea Rafferty <rhea@hands.build> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…path unchanged Review (CC-Quiver-Owner) showed the "no existence oracle" claim over-reached: the shape gate itself adds no existence signal, but the surrounding role path already distinguishes existing vs absent apps via the org_id echoed in the forbidden response. Reword the helper comment and rename the full-UUID test so it claims only what it proves (a well-formed UUID is not shape-gated), and point at the separate pre-existing org_id-leak follow-up. No behavior change. Signed-off-by: Rhea Rafferty <rhea@hands.build> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TennyZhuang
added a commit
that referenced
this pull request
Aug 20, 2026
`hands apps list` full-app-UUID output landed in #468 (already on main) but has not shipped to npm yet. Bump the version so publish-cli can release it. Signed-off-by: Rhea Rafferty <rhea@hands.build> Co-authored-by: Rhea Rafferty <hands-rhea@mail.build> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
hands apps listprinted only the first 8 chars of each app UUID (a.id.slice(0, 8)). Passing that short id to/api/apps/:appId/...(e.g.testflight-upload) never matched an app and fell through to the role check, returning a misleadingINSUFFICIENT_APP_ROLE/app_role=none— looks like a permissions problem, is actually a bad identifier. Reported by @Codex-Android-DevOPS during a TestFlight upload (full UUID76304f16-fbf7-488f-8445-e16ffdd6cef8worked).Fix (two parts)
packages/cli/src/commands/apps.ts):apps listprints the full app id — theIDcolumn is what other commands and the API expect. Removes the source of the bad input.worker/src/lib/permissions.ts): shape gate at the shared authz boundary —ensureAppRole/ensureAppPermission, before any DB lookup or role check, so every:appIdroute benefits (not just testflight-upload). An id built solely from UUID characters (hex + dashes) that is not a complete UUID — a truncated/partial id — returns400 EXACT_APP_ID_REQUIRED.Scope of the shape gate
Shape-only check that adds no existence signal of its own: a well-formed UUID is left to the normal role path exactly as before, and synthetic slug-like ids (which contain non-hex characters) are untouched. It does not touch the DB.
Tests
worker/test/app_id_shape_gate.test.ts(4): 8-char truncation → 400; dashed partial → 400; full UUID → not shape-gated, passes through to the normal role check (403); slug-like ids (app-1,guard-app,legacy-app,other) → normal role check, not shape-rejected.sqlite3 ENOENTenv-only migration test, unrelated). worker + CLItscclean.🤖 Generated with Claude Code