@profullstack/throttle 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/index.d.ts +7 -1
- package/package.json +1 -1
- package/src/throttle.js +11 -2
package/README.md
CHANGED
|
@@ -110,7 +110,8 @@ selling the same minute twice.
|
|
|
110
110
|
| `gateway` | — | an `@profullstack/x402-gateway`; makes over-limit a 402 |
|
|
111
111
|
| `rules` | `[]` | per-path allowances; most specific match wins |
|
|
112
112
|
| `credential` | `{ limit: 600, ceiling: 1200 }` | budget for callers presenting one |
|
|
113
|
-
| `exempt` | — | never metered, e.g. a
|
|
113
|
+
| `exempt` | — | never metered, e.g. a health check |
|
|
114
|
+
| `credentialFrom` | auth headers | what counts as a credential; read the session cookie too |
|
|
114
115
|
| `openPaths` | — | extra paths never metered |
|
|
115
116
|
| `identify` | the caller's address | what to count against |
|
|
116
117
|
| `store` | per-process | supply one to share a count across a fleet |
|
|
@@ -142,6 +143,18 @@ Any auth scheme counts, not just `Bearer` — matching Bearer alone once dropped
|
|
|
142
143
|
a signed wallet's bulk payout into the anonymous bucket, which it exhausted in
|
|
143
144
|
seconds.
|
|
144
145
|
|
|
146
|
+
A site whose customers arrive in a browser should count its session cookie as a
|
|
147
|
+
credential, so a signed-in merchant watching a dashboard is not metered as an
|
|
148
|
+
anonymous scraper:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
credentialFrom: (request) =>
|
|
152
|
+
sessionIdFrom(request) ?? presentedCredential(request.headers),
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Prefer that to `exempt`. Exempting sessions outright hands an unmetered site to
|
|
156
|
+
anyone willing to sign up first.
|
|
157
|
+
|
|
145
158
|
`credential: false` on a rule keeps it address-bucketed however the request is
|
|
146
159
|
authenticated. **The sign-in routes need this**, or a brute-force attempt bolts
|
|
147
160
|
an `Authorization` header onto every guess and buys itself the integration
|
package/index.d.ts
CHANGED
|
@@ -48,8 +48,14 @@ export interface ThrottleOptions {
|
|
|
48
48
|
store?: QuotaStore;
|
|
49
49
|
/** What to count against. Default: the caller's address, as the edge reported it. */
|
|
50
50
|
identify?: (request: Request) => string | null;
|
|
51
|
-
/** Never metered, e.g. a
|
|
51
|
+
/** Never metered, e.g. a health check. */
|
|
52
52
|
exempt?: (request: Request) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* What counts as a credential for the larger budget. Default: an
|
|
55
|
+
* `Authorization` or `X-API-Key` header. Supply one that also reads a
|
|
56
|
+
* session cookie so signed-in customers are not metered as anonymous.
|
|
57
|
+
*/
|
|
58
|
+
credentialFrom?: (request: Request) => string | null;
|
|
53
59
|
/** Extra paths never metered. The gateway's sales page is added for you. */
|
|
54
60
|
openPaths?: string[];
|
|
55
61
|
/** Per-path allowances. Most specific match wins, whatever order they are in. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profullstack/throttle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An app-wide rate limit whose answer to going over is a price, not a 429. 100 requests a minute free, then x402 Payment Required with an offer, settled by CoinPay. Meters every route, so a scraper wearing a browser user agent is charged like any other crawler.",
|
|
6
6
|
"keywords": [
|
package/src/throttle.js
CHANGED
|
@@ -40,7 +40,8 @@ import { memoryStore } from './store.js';
|
|
|
40
40
|
* @param {object} [options.gateway] a @profullstack/x402-gateway; over-limit is answered 402 with its offer
|
|
41
41
|
* @param {object} [options.store] shared counter; default is per-process
|
|
42
42
|
* @param {(request: Request) => string|null} [options.identify] what to count against; default the caller's address
|
|
43
|
-
* @param {(request: Request) => boolean} [options.exempt] never metered, e.g. a
|
|
43
|
+
* @param {(request: Request) => boolean} [options.exempt] never metered, e.g. a health check
|
|
44
|
+
* @param {(request: Request) => string|null} [options.credentialFrom] what counts as a credential; default an Authorization or X-API-Key header
|
|
44
45
|
* @param {string[]} [options.openPaths] extra paths never metered
|
|
45
46
|
* @param {Array} [options.rules] per-path allowances, most specific wins
|
|
46
47
|
* @param {{limit?: number, ceiling?: number}} [options.credential] budget for callers presenting a credential
|
|
@@ -59,6 +60,14 @@ export function createThrottle(options = {}) {
|
|
|
59
60
|
const rules = compileRules(options.rules, defaults);
|
|
60
61
|
const gateway = options.gateway ?? null;
|
|
61
62
|
const identify = options.identify ?? addressOf;
|
|
63
|
+
/*
|
|
64
|
+
* What counts as a credential. The default reads the request's own auth
|
|
65
|
+
* headers, but a site whose customers arrive in a browser needs its session
|
|
66
|
+
* cookie to count too: a signed-in merchant watching a dashboard is not a
|
|
67
|
+
* scraper, and the alternative -- exempting sessions outright -- hands an
|
|
68
|
+
* unmetered site to anyone willing to sign up first.
|
|
69
|
+
*/
|
|
70
|
+
const credentialFrom = options.credentialFrom ?? ((request) => presentedCredential(request.headers));
|
|
62
71
|
const exempt = options.exempt ?? null;
|
|
63
72
|
const onThrottle = options.onThrottle ?? null;
|
|
64
73
|
|
|
@@ -179,7 +188,7 @@ export function createThrottle(options = {}) {
|
|
|
179
188
|
* budget.
|
|
180
189
|
*/
|
|
181
190
|
const honoursCredential = Boolean(credential) && rule.credential !== false;
|
|
182
|
-
const presented = honoursCredential ?
|
|
191
|
+
const presented = honoursCredential ? credentialFrom(request) : null;
|
|
183
192
|
if (presented) {
|
|
184
193
|
const ceilingRule = { ...rule, limit: rule.credential?.ceiling ?? credential.ceiling };
|
|
185
194
|
const perKeyRule = { ...rule, limit: rule.credential?.limit ?? credential.limit };
|