> */ private const READ_MAP = array( 'products.search' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'products.list' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'products.read' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'embeds.list' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'embeds.read' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'editor.providers' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'editor.templates' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'diagnostics.read' => array( self::CAP_VIEW_DIAGNOSTICS, self::CAP_FALLBACK_MANAGE ), 'logs.read' => array( self::CAP_VIEW_LOGS, self::CAP_FALLBACK_MANAGE ), 'sync.jobs.read' => array( self::CAP_RUN_SYNC, self::CAP_FALLBACK_MANAGE ), 'audit.read' => array( self::CAP_VIEW_DIAGNOSTICS, self::CAP_FALLBACK_MANAGE ), 'audit.summary' => array( self::CAP_VIEW_DIAGNOSTICS, self::CAP_FALLBACK_MANAGE ), 'settings.read.public' => array( self::CAP_FALLBACK_EDIT ), 'settings.cloud.plans' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), ); /** * §3.2 write-side mapping from route token to OR-list of * capability tokens. * * Held as a private constant so {@see self::requiredForWrite()} * and {@see self::validRoutes()} share the same source of truth. * * @var array> */ private const WRITE_MAP = array( 'products.write' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'embeds.write' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'editor.preview' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'settings.ai.extract_keywords' => array( self::CAP_MANAGE_PRODUCTS, self::CAP_FALLBACK_EDIT ), 'clicks.write' => array(), 'sync.run' => array( self::CAP_RUN_SYNC, self::CAP_FALLBACK_MANAGE ), 'cache.purge' => array( self::CAP_RUN_SYNC, self::CAP_FALLBACK_MANAGE ), 'settings.write' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.apply_credentials' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.checkout_session' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.provision_site' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.connection_email' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.status' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), 'settings.cloud.connection_token' => array( self::CAP_MANAGE_SETTINGS, self::CAP_FALLBACK_MANAGE ), ); /** * Deny-by-default OR-list returned for unknown route tokens. * * The policy is to require the strongest fallback * (`manage_options`) so a future slice that registers a route * without updating this class fails closed rather than open. * * @var list */ private const DENY_BY_DEFAULT = array( self::CAP_FALLBACK_MANAGE ); /** * Return the §3.2 read-side OR-list of capabilities required for * the given route token. * * Unknown route tokens deliberately deny by default to * `manage_options` rather than throw — REST routes registered * by future slices may legitimately introduce new tokens before * this class is updated, and a deny-by-default response keeps * the surface failing closed in the meantime. * * @param string $route Stable route token (case-sensitive). * * @return list */ public static function requiredForRead( string $route ): array { return self::READ_MAP[ $route ] ?? self::DENY_BY_DEFAULT; } /** * Return the §3.2 write-side OR-list of capabilities required for * the given route token. * * See {@see self::requiredForRead()} for the deny-by-default * policy on unknown tokens. * * @param string $route Stable route token (case-sensitive). * * @return list */ public static function requiredForWrite( string $route ): array { return self::WRITE_MAP[ $route ] ?? self::DENY_BY_DEFAULT; } /** * Report whether the supplied {@see RequestContext} satisfies the * given OR-list of required capabilities. * * Returns true when `$required` is empty (no auth is needed for * the route) OR the context carries any one of the required * tokens. * * @param RequestContext $ctx Per-request capability snapshot. * @param list $required OR-list of capability tokens. */ public static function isAuthorized( RequestContext $ctx, array $required ): bool { if ( $required === array() ) { return true; } foreach ( $required as $cap ) { if ( $ctx->hasCapability( $cap ) ) { return true; } } return false; } /** * Return the full list of route tokens this policy currently * recognizes (read-side + write-side, in declaration order). * * Used by tests to assert that every documented route token is * mapped, and by diagnostics surfaces that want to enumerate the * known routes. * * @return list */ public static function validRoutes(): array { $tokens = array_merge( array_keys( self::READ_MAP ), array_keys( self::WRITE_MAP ) ); return array_values( $tokens ); } }