Scope Picker

Reads back what an API key will be able to do in plain words, flags risky scopes and suggests trimming to what was actually used.

StopsAPI key with admin powers leaks

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/scope-picker.json

The files land in your project. They're yours to edit.

Usage

scope-picker-usage.tsx
import { ScopePicker, type ScopeDef } from "@/components/ui/scope-picker"

<ScopePicker
  label="Permissions for the deploy bot key"
  scopes={scopes}
  defaultValue={{ scopes: ["contents:write", "pull_requests:read", "issues:write", "admin:repo", "delete_repo"], expiresInDays: null }}
  used={["contents:read", "pull_requests:read", "issues:write"]}
/>

What it catches

  • Write, delete and admin scopes, called out
  • Scopes granted implicitly through implies chains
  • Scopes unused in the recent window, with a one-tap "Only what you've used"
  • Keys set to never expire, with a suggested expiry

Props

ScopePicker props
PropTypeDefault
label

Visible label.

string"Permissions"
scopes* (required)

Every scope your API offers, with plain-language descriptions.

ScopeDef[]—
value

Selected scopes and expiry (controlled).

ScopeSelection—
defaultValue

Starting selection when uncontrolled.

ScopeSelection{ scopes: [], expiresInDays: 30 }
onValueChange

Called with the selection after every change.

(value: ScopeSelection) => void—
used

Scope ids this key (or a similar one) actually used; enables the "Only what you've used" suggestion.

string[]—
expiryOptions

Expiry choices in days; null means no expiry.

(number | null)[][7, 30, 90, 365, null]
suggestedExpiry

Suggested expiry when "no expiry" is picked.

number90
locale

Locale for expiry dates.

string—
subject

"This key", "This app".

string"This key"
usedWindow

Period the usage data covers, e.g. "90 days".

string"in the last 90 days"
unusedText

Warning for scopes not used in that period.

string"{count} of these weren't used {window}."
onlyUsedLabel

Label of the "Only what you've used" fix.

string"Only what you've used"
noExpiryText

Warning for a key that never expires.

string"A key that never expires keeps working long after it's f…
leastPrivilegeText

Shown when the selection matches usage.

string"Only what it needs."
expiresLabel

Accessible label of the expiry options.

string"Expires"
neverLabel

Label of the never-expires option.

string"Never"
expiryFixLabel

`{days}` is filled in.

string"Expire in {days} days"
expiresOnText

`{date}` or `{days}` is filled in.

string"It expires on {date}."
expiresInText

Expiry readback with a relative time.

string"It expires in {days} days."
neverExpiresText

Readback for a key that never expires.

string"It never expires."
emptyText

Shown when no scope is selected.

string"{subject} can't do anything yet."
includedText

Marks a scope granted by another one.

string"Included by a broader permission"

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Scope checkboxes use <label>s; scopes granted by a parent are marked
  • Expiry options are a radiogroup with an aria-label
  • Readback and warnings render in a polite live region; icons are aria-hidden
  • Highlight animation is skipped under reduced motion

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

This is a UX guard rail. Enforce on the server: Grant only the scopes the server validates, enforce expiry server-side, and check scopes on every API call.

  • Only knows the scope definitions and usage you pass
  • Usage-based trimming can drop a scope needed by a rare job
  • Does not create the key; the server decides what it grants

What it stops

API key with admin powers leaks

Also known as

api key · scopes · permissions · access token · pat · least privilege · developer settings · oauth scopes

Field type

Dependencies

Pairs well with