Address review: clarify requiredScopes fail-open semantics, simplify scope dedup, make ALL_SCOPES readonly

Co-Authored-By: mantra <mantra@stack-auth.com>
This commit is contained in:
Devin AI 2026-06-01 20:43:16 +00:00
parent 2fd0801e0a
commit f18255b6fa
3 changed files with 12 additions and 4 deletions

View File

@ -443,7 +443,7 @@ export async function createRefreshTokenObj(options: CreateRefreshTokenOptions)
const refreshToken = generateSecureRandomString();
const scopes = options.scopes ? parseScopeString(scopesToString(options.scopes)) : [];
const scopes = options.scopes ? [...new Set(options.scopes)] : [];
const refreshTokenObj = await globalPrismaClient.projectUserRefreshToken.create({
data: {

View File

@ -27,8 +27,16 @@ type ShownEndpointDocumentation = {
description: string,
tags?: string[],
crudOperation?: Capitalize<CrudlOperation>,
// Scopes a client access token must hold to call this endpoint. Enforced centrally in the
// smart route handler (server/admin keys bypass). See `packages/stack-shared/src/scopes.ts`.
// Scopes that a *scoped* client access token must hold to call this endpoint. Enforced centrally
// in the smart route handler. See `packages/stack-shared/src/scopes.ts`.
//
// IMPORTANT — this is an opt-in *down-scoping* restriction, NOT a security boundary. The check is
// fail-open: server/admin secret keys bypass entirely, and a client token that carries no `scope`
// claim (every token minted before scopes existed, and any session created without an explicit
// `scope`) is treated as unrestricted and reaches the handler regardless of this field. Only
// tokens that explicitly declare scopes are constrained to the scopes they list. Do NOT rely on
// `requiredScopes` to keep callers out of an endpoint — use access type / permissions for that.
//
// Omitted / undefined means "no scope required". An empty array means the same, but documents
// that the absence of a requirement was deliberate (useful for the scope-coverage test).
requiredScopes?: Scope[],

View File

@ -30,7 +30,7 @@ export const SCOPES = {
export type Scope = keyof typeof SCOPES;
export const ALL_SCOPES = Object.keys(SCOPES) as Scope[];
export const ALL_SCOPES: readonly Scope[] = Object.keys(SCOPES) as Scope[];
export function isScope(value: string): value is Scope {
return Object.prototype.hasOwnProperty.call(SCOPES, value);