Skip to content

fix(apps): full app UUID in apps list; reject partial app ids at authz boundary - #468

Merged
TennyZhuang merged 2 commits into
mainfrom
rhea/apps-list-full-id
Aug 20, 2026
Merged

fix(apps): full app UUID in apps list; reject partial app ids at authz boundary#468
TennyZhuang merged 2 commits into
mainfrom
rhea/apps-list-full-id

Conversation

@TennyZhuang

@TennyZhuang TennyZhuang commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Problem

hands apps list printed 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 misleading INSUFFICIENT_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 UUID 76304f16-fbf7-488f-8445-e16ffdd6cef8 worked).

Fix (two parts)

  1. CLI (packages/cli/src/commands/apps.ts): apps list prints the full app id — the ID column is what other commands and the API expect. Removes the source of the bad input.
  2. Worker (worker/src/lib/permissions.ts): shape gate at the shared authz boundary — ensureAppRole / ensureAppPermission, before any DB lookup or role check, so every :appId route benefits (not just testflight-upload). An id built solely from UUID characters (hex + dashes) that is not a complete UUID — a truncated/partial id — returns 400 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.

Not oracle-free overall (per @CC-Quiver-Owner review): the surrounding role path already distinguishes existing-vs-absent apps by echoing org_id in the forbidden response (getAppOrgId is a bare existence query). That is pre-existing and deliberately out of scope for this PR to keep the exact clean — tracked as a separate follow-up (backfill org_id on the failure path only when the caller has membership in that org). This PR's gate neither introduces nor fixes that.

Tests

  • New 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.
  • Full worker suite green (506 passed; the 1 failure is the pre-existing sqlite3 ENOENT env-only migration test, unrelated). worker + CLI tsc clean.

🤖 Generated with Claude Code

Rhea Rafferty and others added 2 commits August 19, 2026 18:10
…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
TennyZhuang merged commit 3fb6a04 into main Aug 20, 2026
6 checks passed
@TennyZhuang
TennyZhuang deleted the rhea/apps-list-full-id branch August 20, 2026 04:38
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>
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