With access to AES-256, and if the seed is a 256-bit secret about uniformly random value, a simple method that fits is to use that 256-bit secret as AES-256 key, and encipher a 128-bit counter (initially zero, incremented after use) until sufficient output has been produced; that is $n$ (resp. $\lceil 3n/2\rceil$, $2n$) times to produce $n$ AES-128 (resp. AES-192; AES-256) keys.
That's the method used internally by AES-CTR to generate it's keystream, except that here we set the IV at zero. That change introduces the theoretical possibility of a multi-target attack, but it does not matter because a 256-bit key is so large. Further, in the use case, having many client devices with keys derived from the same secret seed does not make multi-target attack easier (having many different seeds would be necessary for that).
Importantly in a JS context, there's no arithmetic on secret data (outside AES, assumed secure). There's some arithmetic for the counter, but it's value is public.
Note: With access to AES-256-CTR, we can even remove the counter arithmetic by using AES-256-CTR decryption of all-zero data, with an all-zero IV.
Perhaps importantly in the context, it's easy to generate the key for one particular user without the cost (time and space) of generating the keys for the other ones; that is, we have an addressable PRNG/DRNG.
If the seeding material was low-entropy, that's another matter. We must use key stretching, preferably memory-hard, and improvising that from AES is not trivial.