You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -35,49 +35,134 @@ Add to your project using your package manager of choice (tip: [`pnpm`](https://
35
35
36
36
pnpm install svelte-api-keys
37
37
38
-
### Create hooks.server handler
38
+
### Create a key store
39
39
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.
41
41
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 =newInMemoryKeyStore()
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:
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
+
awaitredis.connect()
82
+
const storage =awaitRedisKeyStore.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!
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:
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:
`cookie` (default `api-key`) sets the name of a cookie to inspect for an API Key on any incoming request.
59
131
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:
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
+
exportconst handle =api_keys.handle
76
159
```
77
160
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
+
78
163
### Use the API in endpoints
79
164
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.
81
166
82
167
#### Simple Global Limit
83
168
@@ -190,23 +275,21 @@ Add an additional handler to `src/hooks.server.ts`:
190
275
import { sequence } from'@sveltejs/kit/hooks'
191
276
importtype { Handle } from'@sveltejs/kit'
192
277
import { fetchTierForUser } from'$lib/database'
193
-
194
-
// create handle as before, but give it a different name:
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.
237
320
238
321
## TODO
239
322
240
323
Possible enhancements:
241
324
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
0 commit comments