@better-auth-ui/core
Version:
Authentication components and data utilities for [Better Auth](https://better-auth.com), available for React and Solid.
136 lines (123 loc) • 4.26 kB
text/typescript
/**
* Human-readable presentation for a single OAuth scope.
*/
export interface OAuthScopeMetadata {
/** Short permission label displayed to the user. */
label: string
/** Optional explanation of the data or access represented by the scope. */
description?: string
}
/**
* A scope entry in list form. Useful when metadata is loaded from a database
* or an API and arrives as an array rather than a keyed object.
*/
export interface OAuthScopeMetadataDefinition extends OAuthScopeMetadata {
/** The raw scope value, e.g. `"calendar.read"`. */
scope: string
}
/**
* Context handed to an {@link OAuthScopeMetadataResolver} so applications can
* vary labels per requesting client or per scope set.
*/
export interface OAuthScopeMetadataContext {
/** The OAuth client ID from the signed authorization request, if known. */
clientId?: string
/** Every scope being displayed alongside this one. */
requestedScopes: readonly string[]
}
/**
* Synchronous lookup for a single scope.
*
* Returning `undefined` means "use the fallback" — the scope stays visible
* with built-in or raw metadata. It never hides a scope.
*/
export type OAuthScopeMetadataResolver = (
scope: string,
context: OAuthScopeMetadataContext
) => OAuthScopeMetadata | undefined
/**
* Keyed scope metadata. Kept as a named type for backwards compatibility with
* the original record-only `scopeMetadata` option.
*/
export type OAuthScopeMetadataMap = Record<string, OAuthScopeMetadata>
/**
* Every shape `scopeMetadata` accepts: a keyed record, a static list, or a
* resolver function.
*
* Resolvers stay synchronous so rendering is deterministic and identical
* across SSR and every UI package. Applications that need remote metadata
* should preload it and pass a record or list.
*/
export type OAuthScopeMetadataSource =
| OAuthScopeMetadataMap
| readonly OAuthScopeMetadataDefinition[]
| OAuthScopeMetadataResolver
/** Built-in metadata for the scopes Better Auth supports out of the box. */
export const oauthProviderScopeMetadata: OAuthScopeMetadataMap = {
openid: {
label: "Verify your identity",
description: "Access your unique account identifier."
},
profile: {
label: "View your profile",
description: "View your name and profile picture."
},
email: {
label: "View your email address",
description: "View your email address and whether it is verified."
},
offline_access: {
label: "Maintain access",
description:
"Access your data when you are not actively using the application."
}
}
/**
* Read a scope key without ever reaching `Object.prototype`.
*
* Scopes come from the OAuth client, so `constructor`, `toString`, and the
* like are attacker-controlled input. A plain index would hand back the
* inherited value — an unlabelled row on the consent screen for a scope the
* user is still about to grant.
*/
function readScopeKey(
map: OAuthScopeMetadataMap,
scope: string
): OAuthScopeMetadata | undefined {
return Object.hasOwn(map, scope) ? map[scope] : undefined
}
function lookup(
source: OAuthScopeMetadataSource,
scope: string,
context: OAuthScopeMetadataContext
): OAuthScopeMetadata | undefined {
if (typeof source === "function") return source(scope, context)
if (Array.isArray(source)) {
return (source as readonly OAuthScopeMetadataDefinition[]).find(
(entry) => entry.scope === scope
)
}
return readScopeKey(source as OAuthScopeMetadataMap, scope)
}
/**
* Resolve the label and description shown for a requested scope.
*
* Resolution order:
* 1. The consumer-provided record, list, or resolver.
* 2. Built-in metadata for the standard OpenID Connect scopes.
* 3. The raw scope value as its own label.
*
* Every requested scope stays visible — there is no way to resolve to
* "nothing", so a user always sees the complete set of permissions.
*/
export function resolveOAuthScopeMetadata(
source: OAuthScopeMetadataSource | undefined,
scope: string,
context: OAuthScopeMetadataContext
): OAuthScopeMetadata {
const resolved = source ? lookup(source, scope, context) : undefined
return (
resolved ??
readScopeKey(oauthProviderScopeMetadata, scope) ?? { label: scope }
)
}