Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 337fa04

Browse files
Merge pull request #9 from CaptainCodeman/simplify-7
simplify interfaces
2 parents 6680e6b + c1cfb36 commit 337fa04

File tree

14 files changed

+524
-310
lines changed

14 files changed

+524
-310
lines changed

README.md

Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,49 +35,134 @@ Add to your project using your package manager of choice (tip: [`pnpm`](https://
3535

3636
pnpm install svelte-api-keys
3737

38-
### Create hooks.server handler
38+
### Create a key store
3939

40-
Create a key manager instance that provides the interface to generate, store, and validate API keys. The key information can be stored in memory (for development & testing), in Redis, in Firestore, or any database you want by implementing a simple interface.
40+
The key store persists the information associated with an API key which is only ever accessed using the SHA256 hash of the key, for security purposes.
4141

42-
We also create a hooks `handle` function that will hook everything into the SvelteKit processing pipeline:
42+
Provided implementations include an in-memory store, Firestore, and Redis. Other stores such as any popular RDBMS can be created by implementing a simple `KeyStore` interface.
43+
44+
We'll use `src/lib/api_keys.ts` to to store the code in all the following examples:
45+
46+
#### In Memory Key Store
47+
48+
This uses an internal `Map` which is _not_ persisted so is suitable for development, testing and demonstration purposes only!
49+
50+
```ts
51+
import { InMemoryKeyStore } from 'svelte-api-keys'
52+
53+
const storage = new InMemoryKeyStore()
54+
```
55+
56+
#### Firestore Key Store
57+
58+
Firestore is a popular cloud data store from Google. Use the `firebase-admin/firestore` lib to create a Firestore instance and pass it to the `FirestoreKeyStore` constructor. By default, key information is stored in a collection called `api` but this can be overridden in the constructor. To save read costs and improve performance, wrap the store in an `LruCacheKeyStore` instance:
59+
60+
```ts
61+
import { initializeApp } from 'firebase-admin/app'
62+
import { getFirestore } from 'firebase-admin/firestore'
63+
import { FirestoreKeyStore, LruCacheKeyStore } from 'svelte-api-keys'
64+
import { env } from '$env/dynamic/private'
65+
66+
const app = initializeApp({ projectId: env.FIREBASE_PROJECT_ID })
67+
const firestore = getFirestore(app)
68+
const storage = new LruCacheKeyStore(new FirestoreKeyStore(firestore))
69+
```
70+
71+
#### Redis Key Store
72+
73+
Redis is a fast persistable cache and makes for an excellent store. Use the node `redis` package to create a redis client instance and pass it to the `RedisKeyStore` static `create` method, which is used to ensure a search index exists. By default, key information is stored in a hash structure with the prefix `api:` but this can be overridden in the constructor:
74+
75+
```ts
76+
import { createClient } from 'redis'
77+
import { RedisKeyStore } from 'svelte-api-keys'
78+
import { env } from '$env/dynamic/private'
79+
80+
const redis = createClient({ url: env.REDIS_URL })
81+
await redis.connect()
82+
const storage = await RedisKeyStore.create(redis)
83+
```
84+
85+
### Create a Token Bucket store
86+
87+
The token bucket store maintains the state of each token bucket.
88+
89+
Provided implementations include an in-memory store, and Redis. Other stores such as any popular RDBMS can be created by extending a base `TokenBucket` class and implementing a `consume` method.
90+
91+
#### In Memory Token Buckets
92+
93+
This uses an internal `Map` which is _not_ persisted or shared so is suitable for single-server use where potentially allowing excess requests in the event of a process restart would be acceptable, or for development, testing and demonstration purposes only!
94+
95+
```ts
96+
import { InMemoryTokenBucket } from 'svelte-api-keys'
97+
98+
const buckets = new InMemoryTokenBucket()
99+
```
100+
101+
#### Redis Token Buckets
102+
103+
The Redis implementation uses a server-side javascript function to handle the token bucket update logic, so Redis Stack Server is recommended. This function is created automatically when the redis client instance is passed to the `RedisTokenBucket` static `create` method. You can also override the default storage prefix (`bucket:`), module name (`TokenBucket`), and function name (`consume`) if needed.
104+
105+
The key store and token bucket implementations are independent of each other and can be mix-and-matched as required, but it's likely that if you're using redis you'll use the Redis implementations of both so they can be created using the same redis client instance:
43106

44107
```ts
45-
import {
46-
Handler,
47-
KeyExtractor,
48-
InMemoryTokenBucket,
49-
InMemoryKeyStore,
50-
KeyManager,
51-
} from 'svelte-api-keys'
108+
import { createClient } from 'redis'
109+
import { RedisKeyStore, RedisTokenBucket } from 'svelte-api-keys'
110+
import { env } from '$env/dynamic/private'
111+
112+
const redis = createClient({ url: env.REDIS_URL })
113+
await redis.connect()
114+
const storage = await RedisKeyStore.create(redis)
115+
const buckets = await RedisTokenBucket.create(redis)
116+
```
117+
118+
### Create an ApiKeys Manager
119+
120+
The `ApiKeys` manager provides the interface to generate, validate, and manage API keys. It uses the API Key Store internally, and applies SHA256 hashing to keys for security when storing and retrieving them (you can never leak keys if you don't store them!). Normally, you should never access the key store directly - aways use the key manager to do so. When generating keys, it will ensure a key doesn't contain any 'bad words' (which could otherwise be unfortunate and embarrassing!).
121+
122+
The simplest use just requires the key store and token bucket implementations be passed to it:
123+
124+
```ts
125+
export const api_keys = new ApiKeys(storage, buckets)
126+
```
127+
128+
There is an optional parameters object that can also control it's behavior by passing:
52129

53-
// the KeyExtractor allows the handler to select the API key from the request. You can define one or more methods:
54-
// 1. searchParam key, for https://example.com/api?key=myapikey
55-
// 2. name of an HTTP header, such as 'x-api-key'
56-
// 3. name of a cookie to check in the request
57-
// 4. your own custom function which can lookup or transform the key
58-
const extractor = new KeyExtractor({ searchParam: 'key', httpHeader: 'x-api-key' })
130+
`cookie` (default `api-key`) sets the name of a cookie to inspect for an API Key on any incoming request.
59131

60-
// the token-bucket implementation will store the tokens available for each API key / client IP and endpoint group
61-
// an in-memory implementation is suitable for less-critical single server deployments or development & testing, a
62-
// Redis implementation is avaialable for durability and scalability
132+
`httpHeader` (default `x-api-key`) sets the name of an http header to inspect for an API Key on any incoming request. A request containing the http header `x-api-key: my-api-key` would find the key `my-api-key` automatically. Any key found in the http header will override a key found from a cookie.
133+
134+
`searchParam` (default `key`) sets the name of a URL search parameter to inspect for an API Key on any incoming request. A request for `POST /my-endpoint?key=my-api-key` would find the key `my-api-key` automatically. Any key found in the search param will override a key found from an http header or cookie.
135+
136+
`custom` (default undefined) sets a custom key extraction & transform function that allows you to perform your own key lookups, perhaps via an existing session cookie or similar, and also allows you to transform any existing key that has been extracted using the previous settings - you might [prefix keys to indicate their usage as Stripe does](https://docs.stripe.com/docs/api/authentication) for instance. This will override all other methods if specified.
137+
138+
`key_length` (default 32) sets the length, in bytes, of the API key to generate. If you want shorter API keys you could consider setting it to a lower value such as 24 or 16 (but too low risks conflicts when generating new keys). Keys are converted to human-readable format using Base62 for compactness and easy copy-paste.
139+
140+
So as a more complete example your `src/lib/api_keys.ts` may end up looking something like this, but using whatever key store and token bucket implementations make sense for you:
141+
142+
```ts
143+
import { ApiKeys, InMemoryKeyStore, InMemoryTokenBucket } from 'svelte-api-keys'
144+
145+
const storage = new InMemoryKeyStore()
63146
const buckets = new InMemoryTokenBucket()
64147

65-
// the manager is used by the handler, but will also be use when generating and listing API key info to the user
66-
// which is why it's exported. The store is independent to the token bucket storage - the in-memory implementation
67-
// is only suitable for development and testing, persistent implementations are available for Redis or Firestore.
68-
// when using a database key-store, consider wrapping it with an LruCacheKeyStore to improve performance
69-
// optionally pass the key byte length to use (default 32 for 256 bits). Lower values will create shorter key strings
70-
export const manager = new KeyManager(new InMemoryKeyStore(), 16)
148+
export const api_keys = new ApiKeys(storage, buckets, { searchParam: 'api-key', key_length: 16 })
149+
```
150+
151+
### Hook into the SvelteKit Request
71152

72-
// the handle function allows the API system to hook into the SvelteKit request pipeline
73-
// it will extract the API key from the request, validate & retrieve the key info for it
74-
// and provide a fluent API for endpoints to apply limits to
75-
export const handle = new Handler(extractor, manager, buckets).handle
153+
The `ApiKeys` instance we created provides a `.handle` property that can be used to hook it into the SvelteKit request pipeline. Just return this from `hooks.server.ts`:
154+
155+
```ts
156+
import { api_keys } from '$lib/api_keys`
157+
158+
export const handle = api_keys.handle
76159
```
77160

161+
If you already have a `handle` function you can chain them together using the [`sequence`](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks-sequence) helper function.
162+
78163
### Use the API in endpoints
79164

80-
Any request will now have an `api` object available on `locals`. This will have a `key`, and `info` property depending on whether an API Key was sent with the request and whether it was valid. It also provides a fluent API that any endpoint can use to `limit()` the request by passing in the refill rate to apply.
165+
Now our API Key manager is hooked into the SvelteKit pipeline, any request will have an `api` object available on `locals`. This will have a `key`, and `info` property depending on whether an API Key was sent with the request and whether it was valid. It also provides a fluent API that any endpoint can use to `limit()` the request by passing in the refill rate to apply.
81166

82167
#### Simple Global Limit
83168

@@ -190,23 +275,21 @@ Add an additional handler to `src/hooks.server.ts`:
190275
import { sequence } from '@sveltejs/kit/hooks'
191276
import type { Handle } from '@sveltejs/kit'
192277
import { fetchTierForUser } from '$lib/database'
193-
194-
// create handle as before, but give it a different name:
195-
const handleApi = new Handler(extractor, manager, bucket).handle
278+
import { api_keys } from '$lib/api_keys`
196279

197280
// this handle could set the locals.tier based on the api.info.user
198281
const handleTiers: Handle = async ({ event, resolve }) => {
199282
const { locals } = event
200283

201-
// fetchTierForUser is a fictitious API that will return the appropiate tier based on the key info user
202-
// tip: this will benefit from an in-memory LRU + TTL cache to avoid slowing down repeated lookups
284+
// fetchTierForUser is an example API that will return the appropriate tier based on the key info user
285+
// tip: this would benefit from an in-memory LRU + TTL cache to avoid slowing down repeated lookups...
203286
locals.tier = await fetchTierForUser(locals.api.info)
204287

205288
return await resolve(event)
206289
}
207290

208-
// the handle we export is now a sequence of both of them
209-
export const handle = sequence(handleApi, handleTiers)
291+
// the handle we export is now a sequence of our api_keys handler and this one
292+
export const handle = sequence(api_keys.handle, handleTiers)
210293
```
211294

212295
Now our endpoints have access to a `locals.tier` value which can be used to select an appropriate token-bucket refill rate:
@@ -233,11 +316,11 @@ export async function POST({ locals }) {
233316
}
234317
```
235318

236-
Finally, should you need them for whatever reason, the `.limit(rate)` method returns details about the result of the call (which are also set as HTTP Response headers)
319+
Finally, should you need them for whatever reason, the `.limit(rate)` method returns details about the result of the call which are also set as HTTP Response headers - these will allow well-behaved clients to automatically back off when they hit rate limits.
237320

238321
## TODO
239322

240323
Possible enhancements:
241324

242-
* Warn if an endpoint fails to call `.limit(rate)`, at least after any other api methods
243-
* Provide a ready-to-go UI for managing keys
325+
- Warn if an endpoint fails to call `.limit(rate)`, at least after any other api methods
326+
- Provide a ready-to-go UI for managing keys

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "svelte-api-keys",
33
"description": "API Key Generation, Validation, and Rate Limiting for SvelteKit",
4-
"version": "0.0.6",
4+
"version": "0.1.0",
55
"keywords": [
66
"svelte",
77
"sveltekit",

src/hooks.server.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,51 @@
11
import { sequence } from '@sveltejs/kit/hooks'
22
import type { Handle } from '@sveltejs/kit'
3-
import {
4-
Handler,
5-
KeyExtractor,
6-
InMemoryTokenBucket,
7-
KeyManager,
8-
LruCacheKeyStore,
9-
} from 'svelte-api-keys'
10-
11-
// Firestore key store:
3+
import { ApiKeys } from 'svelte-api-keys'
4+
5+
// simple, for development / demo use, is to use in-memory implementations
6+
import { InMemoryKeyStore, InMemoryTokenBucket } from 'svelte-api-keys'
7+
8+
const storage = new InMemoryKeyStore()
9+
const buckets = new InMemoryTokenBucket()
10+
11+
// Firestore for key storage (which will benefit from the LruCache):
1212
/*
1313
import { initializeApp } from 'firebase-admin/app'
1414
import { getFirestore } from 'firebase-admin/firestore'
1515
import { env } from '$env/dynamic/private'
1616
import { dev } from '$app/environment'
17-
import { FirestoreKeyStore } from 'svelte-api-keys'
17+
import { FirestoreKeyStore, LruCacheKeyStore } from 'svelte-api-keys'
18+
1819
if (dev) {
1920
process.env.FIRESTORE_EMULATOR_HOST = '127.0.0.1:8080'
2021
}
22+
2123
const app = initializeApp({ projectId: env.FIREBASE_PROJECT_ID })
2224
const firestore = getFirestore(app)
23-
const keyStore = new FirestoreKeyStore(firestore)
25+
const storage = new LruCacheKeyStore(new FirestoreKeyStore(firestore))
2426
*/
2527

26-
// Redis key store:
28+
// Redis can be used for both key storage and token buckets (but each can be used independently)
2729
/*
2830
import { createClient } from 'redis'
29-
import { RedisKeyStore } from 'svelte-api-keys'
31+
import { RedisKeyStore, RedisTokenBucket } from 'svelte-api-keys'
32+
import { env } from '$env/dynamic/private'
33+
3034
const redis = createClient({ url: env.REDIS_URL })
3135
await redis.connect()
32-
const keyStore = await RedisKeyStore.create(redis)
36+
const storage = await RedisKeyStore.create(redis)
37+
const buckets = await RedisTokenBucket.create(redis)
3338
*/
3439

35-
// In Memory key store:
36-
import { InMemoryKeyStore } from 'svelte-api-keys'
37-
const keyStore = new InMemoryKeyStore()
38-
39-
// caching the in-memory store doesn't make a lot of sense, but
40-
// would when using any database backed store implementation
41-
export const manager = new KeyManager(new LruCacheKeyStore(keyStore))
40+
export const api_keys = new ApiKeys(storage, buckets)
4241

43-
const bucket = new InMemoryTokenBucket()
44-
const extractor = new KeyExtractor({ searchParam: 'key', httpHeader: 'x-api-key' })
42+
// simplest would be:
43+
// export const handle = api_keys.handle
4544

46-
export const handleApi = new Handler(extractor, manager, bucket).handle
45+
// we're giving it a different name to combine it with `sequence` ...
46+
const handleApi = api_keys.handle
4747

48-
export const handleTiers: Handle = async ({ event, resolve }) => {
48+
const handleTiers: Handle = async ({ event, resolve }) => {
4949
const { locals } = event
5050

5151
// simulate usage tiers:

0 commit comments

Comments
 (0)