@pegma/rate-limit 0.0.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/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/durable.d.ts +31 -0
- package/dist/durable.d.ts.map +1 -0
- package/dist/durable.js +213 -0
- package/dist/durable.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +12 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +56 -0
- package/dist/memory.js.map +1 -0
- package/dist/policy.d.ts +34 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +69 -0
- package/dist/policy.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RetireGolden, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# `@pegma/rate-limit`
|
|
2
|
+
|
|
3
|
+
Two explicitly named application rate-limit tiers:
|
|
4
|
+
|
|
5
|
+
- `createMemoryLimiter` is a per-instance sliding window with zero I/O. It is
|
|
6
|
+
approximate under scale-out.
|
|
7
|
+
- `createDurableLimiter` is a shared fixed-window counter over an injected
|
|
8
|
+
`@pegma/storage-core` `Store`. It is for low-rate, expensive or
|
|
9
|
+
abuse-critical operations.
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> This source package is at the one-time `0.0.0` package-name bootstrap stage.
|
|
13
|
+
> It is not an advertised release. A separate `0.1.0` release follows
|
|
14
|
+
> bootstrap and consumer validation.
|
|
15
|
+
|
|
16
|
+
There is no default tier and no automatic upgrade. This package is application
|
|
17
|
+
abuse control, not DDoS protection, HTTP middleware, or billing metering.
|
|
18
|
+
Policy names and limiter keys must be primitive, non-blank strings. Whitespace
|
|
19
|
+
is preserved; it remains part of the host-chosen opaque identity. Inputs must
|
|
20
|
+
also contain well-formed Unicode so UTF-8 hashing cannot alias distinct
|
|
21
|
+
unpaired UTF-16 surrogate code units.
|
|
22
|
+
|
|
23
|
+
## Memory tier
|
|
24
|
+
|
|
25
|
+
The memory-only subpath does not import storage:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import {
|
|
29
|
+
createMemoryLimiter,
|
|
30
|
+
defineRateLimitPolicy,
|
|
31
|
+
} from "@pegma/rate-limit/memory";
|
|
32
|
+
|
|
33
|
+
const policy = defineRateLimitPolicy({
|
|
34
|
+
name: "public-read",
|
|
35
|
+
limit: 60,
|
|
36
|
+
windowMs: 5 * 60_000,
|
|
37
|
+
});
|
|
38
|
+
const limiter = createMemoryLimiter(policy);
|
|
39
|
+
const decision = await limiter.allow(clientAddress);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
With N host instances this can admit approximately N times `limit`. Choosing
|
|
43
|
+
it means accepting that property.
|
|
44
|
+
|
|
45
|
+
## Durable tier
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {
|
|
49
|
+
createDurableLimiter,
|
|
50
|
+
defineRateLimitPolicy,
|
|
51
|
+
} from "@pegma/rate-limit/durable";
|
|
52
|
+
|
|
53
|
+
const limiter = createDurableLimiter(
|
|
54
|
+
defineRateLimitPolicy({
|
|
55
|
+
name: "login",
|
|
56
|
+
limit: 10,
|
|
57
|
+
windowMs: 5 * 60_000,
|
|
58
|
+
}),
|
|
59
|
+
store,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const decision = await limiter.allow(principalOrAddress);
|
|
63
|
+
await limiter.sweep();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Allowed checks perform a decider-mediated storage update. Once the current
|
|
67
|
+
count reaches the limit, checks refuse without writing. Contention exhaustion,
|
|
68
|
+
storage outage, malformed state, and malformed clock values fail closed with
|
|
69
|
+
`retryAfter`, measured in milliseconds.
|
|
70
|
+
|
|
71
|
+
Policy names and limiter keys are domain-separated and SHA-256 hashed for
|
|
72
|
+
backend-safe storage identifiers. Durable counter records store only those
|
|
73
|
+
bounded hashes, never the raw policy name or key. Subject hashes are scoped to
|
|
74
|
+
their policy, but they are deterministic pseudonymous identifiers, not
|
|
75
|
+
anonymization: predictable inputs can still be guessed and activity remains
|
|
76
|
+
linkable within a policy.
|
|
77
|
+
|
|
78
|
+
The durable tier uses fixed windows. A burst across a boundary can briefly
|
|
79
|
+
admit nearly twice the limit. It also puts shared-storage latency on every
|
|
80
|
+
allowed check; do not use it for hot, cheap endpoints.
|
|
81
|
+
|
|
82
|
+
Sweeps list one policy partition and delete structurally valid expired rows
|
|
83
|
+
only if their versions are unchanged. Malformed rows are retained:
|
|
84
|
+
storage-core listings do not expose actual row keys, so pairing a malformed
|
|
85
|
+
row's version with a key reconstructed from its payload could target another
|
|
86
|
+
row. Checks against that malformed row still fail closed.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IsoTimestamp } from "@pegma/spine";
|
|
2
|
+
import { type Store } from "@pegma/storage-core";
|
|
3
|
+
import { type LimiterOptions, type RateLimiter, type RateLimitPolicy } from "./policy.js";
|
|
4
|
+
export interface DurableLimiterOptions extends LimiterOptions {
|
|
5
|
+
/** Total optimistic-concurrency attempts. Defaults to 3. */
|
|
6
|
+
readonly maxAttempts?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SweepResult {
|
|
9
|
+
readonly scanned: number;
|
|
10
|
+
readonly deleted: number;
|
|
11
|
+
}
|
|
12
|
+
export interface DurableRateLimiter extends RateLimiter {
|
|
13
|
+
/**
|
|
14
|
+
* Deletes structurally valid expired records only if their versions are
|
|
15
|
+
* unchanged. Malformed rows are retained because listings do not expose
|
|
16
|
+
* their authoritative storage keys.
|
|
17
|
+
*/
|
|
18
|
+
sweep(now?: IsoTimestamp): Promise<SweepResult>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Creates a shared, fixed-window limiter over an injected Store.
|
|
22
|
+
*
|
|
23
|
+
* This tier pays for one storage update per allowed check. A fixed window can
|
|
24
|
+
* admit nearly twice the limit across a boundary; it is intended for
|
|
25
|
+
* low-rate, expensive or abuse-critical operations, not hot cheap endpoints.
|
|
26
|
+
* Storage failures and exhausted contention retries deny rather than throw.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDurableLimiter(inputPolicy: RateLimitPolicy, store: Store, options?: DurableLimiterOptions): DurableRateLimiter;
|
|
29
|
+
export type { LimiterOptions, RateLimitDecision, RateLimiter, RateLimitPolicy, } from "./policy.js";
|
|
30
|
+
export { defineRateLimitPolicy } from "./policy.js";
|
|
31
|
+
//# sourceMappingURL=durable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable.d.ts","sourceRoot":"","sources":["../src/durable.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAEL,KAAK,KAAK,EAEX,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAML,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AA2CrB,MAAM,WAAW,qBAAsB,SAAQ,cAAc;IAC3D,4DAA4D;IAC5D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD;;;;OAIG;IACH,KAAK,CAAC,GAAG,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACjD;AA8FD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,eAAe,EAC5B,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,qBAA0B,GAClC,kBAAkB,CAyIpB;AAED,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/durable.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { systemClock } from "@pegma/spine";
|
|
2
|
+
import { defineCollection, } from "@pegma/storage-core";
|
|
3
|
+
import { assertRateLimitKey, defineRateLimitPolicy, denyForWindow, retryAfter, timeInMilliseconds, } from "./policy.js";
|
|
4
|
+
const COLLECTION_NAME = "rate-limit-windows";
|
|
5
|
+
const windows = defineCollection({
|
|
6
|
+
name: COLLECTION_NAME,
|
|
7
|
+
key: (value) => ({
|
|
8
|
+
partition: String(value.partition),
|
|
9
|
+
id: String(value.id),
|
|
10
|
+
}),
|
|
11
|
+
codec: {
|
|
12
|
+
encode: (value) => ({ ...value }),
|
|
13
|
+
decode: (record) => ({
|
|
14
|
+
partition: record.partition ?? null,
|
|
15
|
+
id: record.id ?? null,
|
|
16
|
+
policyHash: record.policyHash ?? null,
|
|
17
|
+
subjectHash: record.subjectHash ?? null,
|
|
18
|
+
windowStartMs: record.windowStartMs ?? null,
|
|
19
|
+
count: record.count ?? null,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
async function hashOpaque(value) {
|
|
24
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
25
|
+
return [...new Uint8Array(digest)]
|
|
26
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
27
|
+
.join("");
|
|
28
|
+
}
|
|
29
|
+
async function partitionFor(policy) {
|
|
30
|
+
const policyHash = await hashOpaque(`pegma.rate-limit.policy.v1\u0000${policy.name}`);
|
|
31
|
+
return { partition: `policy-${policyHash}`, policyHash };
|
|
32
|
+
}
|
|
33
|
+
async function subjectHashFor(policyHash, key) {
|
|
34
|
+
return hashOpaque(`pegma.rate-limit.subject.v1\u0000${policyHash}\u0000${key}`);
|
|
35
|
+
}
|
|
36
|
+
function idFor(windowStartMs, subjectHash) {
|
|
37
|
+
return `${windowStartMs}-${subjectHash}`;
|
|
38
|
+
}
|
|
39
|
+
const SHA_256_HEX = /^[0-9a-f]{64}$/u;
|
|
40
|
+
function validWindow(value) {
|
|
41
|
+
return typeof value.partition === "string" &&
|
|
42
|
+
/^policy-[0-9a-f]{64}$/u.test(value.partition) &&
|
|
43
|
+
typeof value.id === "string" &&
|
|
44
|
+
value.id.length > 0 &&
|
|
45
|
+
typeof value.policyHash === "string" &&
|
|
46
|
+
SHA_256_HEX.test(value.policyHash) &&
|
|
47
|
+
typeof value.subjectHash === "string" &&
|
|
48
|
+
SHA_256_HEX.test(value.subjectHash) &&
|
|
49
|
+
typeof value.windowStartMs === "number" &&
|
|
50
|
+
Number.isSafeInteger(value.windowStartMs) &&
|
|
51
|
+
typeof value.count === "number" &&
|
|
52
|
+
Number.isSafeInteger(value.count) &&
|
|
53
|
+
value.count > 0
|
|
54
|
+
? value
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
function sweepIdentity(value, expected) {
|
|
58
|
+
if (typeof value.partition !== "string" ||
|
|
59
|
+
value.partition !== expected.partition ||
|
|
60
|
+
typeof value.id !== "string" ||
|
|
61
|
+
value.id.length === 0 ||
|
|
62
|
+
typeof value.policyHash !== "string" ||
|
|
63
|
+
value.policyHash !== expected.policyHash ||
|
|
64
|
+
typeof value.subjectHash !== "string" ||
|
|
65
|
+
!SHA_256_HEX.test(value.subjectHash) ||
|
|
66
|
+
typeof value.windowStartMs !== "number" ||
|
|
67
|
+
!Number.isSafeInteger(value.windowStartMs) ||
|
|
68
|
+
value.id !== idFor(value.windowStartMs, value.subjectHash)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
partition: value.partition,
|
|
73
|
+
id: value.id,
|
|
74
|
+
policyHash: value.policyHash,
|
|
75
|
+
subjectHash: value.subjectHash,
|
|
76
|
+
windowStartMs: value.windowStartMs,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function assertMaxAttempts(value) {
|
|
80
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
81
|
+
throw new TypeError("maxAttempts must be a positive integer.");
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Creates a shared, fixed-window limiter over an injected Store.
|
|
87
|
+
*
|
|
88
|
+
* This tier pays for one storage update per allowed check. A fixed window can
|
|
89
|
+
* admit nearly twice the limit across a boundary; it is intended for
|
|
90
|
+
* low-rate, expensive or abuse-critical operations, not hot cheap endpoints.
|
|
91
|
+
* Storage failures and exhausted contention retries deny rather than throw.
|
|
92
|
+
*/
|
|
93
|
+
export function createDurableLimiter(inputPolicy, store, options = {}) {
|
|
94
|
+
const policy = defineRateLimitPolicy(inputPolicy);
|
|
95
|
+
const clock = options.clock ?? systemClock;
|
|
96
|
+
const maxAttempts = assertMaxAttempts(options.maxAttempts ?? 3);
|
|
97
|
+
const collection = store.collection(windows);
|
|
98
|
+
let partitionPromise;
|
|
99
|
+
function getPartition() {
|
|
100
|
+
// Start hashing only inside allow/sweep, where fail-closed handling
|
|
101
|
+
// immediately awaits the promise. Eager hashing can leave a rejected
|
|
102
|
+
// digest promise unhandled before the limiter is first used.
|
|
103
|
+
partitionPromise ??= partitionFor(policy);
|
|
104
|
+
return partitionPromise;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
async allow(key, at) {
|
|
108
|
+
assertRateLimitKey(key);
|
|
109
|
+
const now = timeInMilliseconds(clock, at);
|
|
110
|
+
if (now === null) {
|
|
111
|
+
return denyForWindow(policy.windowMs);
|
|
112
|
+
}
|
|
113
|
+
const windowStartMs = Math.floor(now / policy.windowMs) * policy.windowMs;
|
|
114
|
+
const windowEndsAt = windowStartMs + policy.windowMs;
|
|
115
|
+
let deciderAllowed = false;
|
|
116
|
+
try {
|
|
117
|
+
const { partition, policyHash } = await getPartition();
|
|
118
|
+
const subjectHash = await subjectHashFor(policyHash, key);
|
|
119
|
+
const id = idFor(windowStartMs, subjectHash);
|
|
120
|
+
const result = await collection.update({ partition, id }, (current) => {
|
|
121
|
+
if (current === null) {
|
|
122
|
+
deciderAllowed = true;
|
|
123
|
+
return {
|
|
124
|
+
action: "write",
|
|
125
|
+
value: {
|
|
126
|
+
partition,
|
|
127
|
+
id,
|
|
128
|
+
policyHash,
|
|
129
|
+
subjectHash,
|
|
130
|
+
windowStartMs,
|
|
131
|
+
count: 1,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const valid = validWindow(current);
|
|
136
|
+
if (valid === null ||
|
|
137
|
+
valid.partition !== partition ||
|
|
138
|
+
valid.id !== id ||
|
|
139
|
+
valid.policyHash !== policyHash ||
|
|
140
|
+
valid.subjectHash !== subjectHash ||
|
|
141
|
+
valid.windowStartMs !== windowStartMs ||
|
|
142
|
+
valid.count >= policy.limit) {
|
|
143
|
+
deciderAllowed = false;
|
|
144
|
+
return { action: "keep" };
|
|
145
|
+
}
|
|
146
|
+
deciderAllowed = true;
|
|
147
|
+
return {
|
|
148
|
+
action: "write",
|
|
149
|
+
value: { ...valid, count: valid.count + 1 },
|
|
150
|
+
};
|
|
151
|
+
}, { maxAttempts });
|
|
152
|
+
if (result.written && deciderAllowed) {
|
|
153
|
+
return { allowed: true };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Storage outage and contention exhaustion deliberately fail closed.
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
allowed: false,
|
|
161
|
+
retryAfter: retryAfter(windowEndsAt, now),
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
async sweep(at) {
|
|
165
|
+
const now = timeInMilliseconds(clock, at);
|
|
166
|
+
if (now === null) {
|
|
167
|
+
return { scanned: 0, deleted: 0 };
|
|
168
|
+
}
|
|
169
|
+
let expected;
|
|
170
|
+
let rows;
|
|
171
|
+
try {
|
|
172
|
+
expected = await getPartition();
|
|
173
|
+
rows = await collection.listVersioned(expected.partition);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return { scanned: 0, deleted: 0 };
|
|
177
|
+
}
|
|
178
|
+
let deleted = 0;
|
|
179
|
+
for (const row of rows) {
|
|
180
|
+
const identity = sweepIdentity(row.value, expected);
|
|
181
|
+
if (identity === null) {
|
|
182
|
+
// CollectionStore.listVersioned does not expose the actual row key.
|
|
183
|
+
// Reconstructing one from malformed identity fields could pair this
|
|
184
|
+
// row's version with a different row, so retaining it is the only
|
|
185
|
+
// safe choice.
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const countIsMalformed = typeof row.value.count !== "number" ||
|
|
189
|
+
!Number.isSafeInteger(row.value.count) ||
|
|
190
|
+
row.value.count <= 0;
|
|
191
|
+
if (countIsMalformed ||
|
|
192
|
+
identity.windowStartMs + policy.windowMs > now) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const removed = await collection.deleteIfUnchanged({
|
|
197
|
+
partition: identity.partition,
|
|
198
|
+
id: identity.id,
|
|
199
|
+
}, row.version);
|
|
200
|
+
if (removed) {
|
|
201
|
+
deleted += 1;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// A sweep is hygiene; a failed delete leaves the record for later.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { scanned: rows.length, deleted };
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export { defineRateLimitPolicy } from "./policy.js";
|
|
213
|
+
//# sourceMappingURL=durable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable.js","sourceRoot":"","sources":["../src/durable.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { defineRateLimitPolicy, type LimiterOptions, type RateLimitDecision, type RateLimiter, type RateLimitPolicy, } from "./policy.js";
|
|
2
|
+
export { createMemoryLimiter } from "./memory.js";
|
|
3
|
+
export { createDurableLimiter, type DurableLimiterOptions, type DurableRateLimiter, type SweepResult, } from "./durable.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,GAKtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EACL,oBAAoB,GAIrB,MAAM,cAAc,CAAC"}
|
package/dist/memory.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type LimiterOptions, type RateLimiter, type RateLimitPolicy } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a zero-I/O sliding-window limiter.
|
|
4
|
+
*
|
|
5
|
+
* This tier is per-instance. With N application instances it can admit about
|
|
6
|
+
* N times the configured limit; use it only where that approximation is an
|
|
7
|
+
* explicit host decision.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createMemoryLimiter(inputPolicy: RateLimitPolicy, options?: LimiterOptions): RateLimiter;
|
|
10
|
+
export type { LimiterOptions, RateLimitDecision, RateLimiter, RateLimitPolicy, } from "./policy.js";
|
|
11
|
+
export { defineRateLimitPolicy } from "./policy.js";
|
|
12
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,cAAc,EAEnB,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AAIrB;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,eAAe,EAC5B,OAAO,GAAE,cAAmB,GAC3B,WAAW,CA+Cb;AAED,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { systemClock } from "@pegma/spine";
|
|
2
|
+
import { assertRateLimitKey, defineRateLimitPolicy, denyForWindow, retryAfter, timeInMilliseconds, } from "./policy.js";
|
|
3
|
+
const PRUNE_THRESHOLD = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* Creates a zero-I/O sliding-window limiter.
|
|
6
|
+
*
|
|
7
|
+
* This tier is per-instance. With N application instances it can admit about
|
|
8
|
+
* N times the configured limit; use it only where that approximation is an
|
|
9
|
+
* explicit host decision.
|
|
10
|
+
*/
|
|
11
|
+
export function createMemoryLimiter(inputPolicy, options = {}) {
|
|
12
|
+
const policy = defineRateLimitPolicy(inputPolicy);
|
|
13
|
+
const clock = options.clock ?? systemClock;
|
|
14
|
+
const hits = new Map();
|
|
15
|
+
let nextPruneAt = Number.NEGATIVE_INFINITY;
|
|
16
|
+
function liveEntries(entries, cutoff) {
|
|
17
|
+
return entries.filter((at) => at > cutoff);
|
|
18
|
+
}
|
|
19
|
+
function prune(cutoff) {
|
|
20
|
+
for (const [key, entries] of hits) {
|
|
21
|
+
const kept = liveEntries(entries, cutoff);
|
|
22
|
+
if (kept.length === 0) {
|
|
23
|
+
hits.delete(key);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
hits.set(key, kept);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
async allow(key, at) {
|
|
32
|
+
assertRateLimitKey(key);
|
|
33
|
+
const now = timeInMilliseconds(clock, at);
|
|
34
|
+
if (now === null) {
|
|
35
|
+
return denyForWindow(policy.windowMs);
|
|
36
|
+
}
|
|
37
|
+
const cutoff = now - policy.windowMs;
|
|
38
|
+
const entries = liveEntries(hits.get(key) ?? [], cutoff);
|
|
39
|
+
if (entries.length >= policy.limit) {
|
|
40
|
+
return {
|
|
41
|
+
allowed: false,
|
|
42
|
+
retryAfter: retryAfter(entries[0] + policy.windowMs, now),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
entries.push(now);
|
|
46
|
+
hits.set(key, entries);
|
|
47
|
+
if (hits.size > PRUNE_THRESHOLD && now >= nextPruneAt) {
|
|
48
|
+
prune(cutoff);
|
|
49
|
+
nextPruneAt = now + policy.windowMs;
|
|
50
|
+
}
|
|
51
|
+
return { allowed: true };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export { defineRateLimitPolicy } from "./policy.js";
|
|
56
|
+
//# sourceMappingURL=memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,aAAa,EACb,UAAU,EACV,kBAAkB,GAKnB,MAAM,aAAa,CAAC;AAErB,MAAM,eAAe,GAAG,MAAM,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAA4B,EAC5B,OAAO,GAAmB,EAAE;IAE5B,MAAM,MAAM,GAAG,qBAAqB,CAAC,WAAW,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACzC,IAAI,WAAW,GAAG,MAAM,CAAC,iBAAiB,CAAC;IAE3C,SAAS,WAAW,CAAC,OAA0B,EAAE,MAAc;QAC7D,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,SAAS,KAAK,CAAC,MAAc;QAC3B,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,IAAI,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,GAAW,EAAE,EAAiB;YACxC,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACxB,MAAM,GAAG,GAAG,kBAAkB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;YAC1C,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACjB,OAAO,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YACxC,CAAC;YAED,MAAM,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;YACrC,MAAM,OAAO,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC;YACzD,IAAI,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACnC,OAAO;oBACL,OAAO,EAAE,KAAK;oBACd,UAAU,EAAE,UAAU,CAAE,OAAO,CAAC,CAAC,CAAY,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC;iBACtE,CAAC;YACJ,CAAC;YAED,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACvB,IAAI,IAAI,CAAC,IAAI,GAAG,eAAe,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;gBACtD,KAAK,CAAC,MAAM,CAAC,CAAC;gBACd,WAAW,GAAG,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;YACtC,CAAC;YACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC;AAQD,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Clock, IsoTimestamp } from "@pegma/spine";
|
|
2
|
+
/** A named rate limit. Durations and retry-after values are milliseconds. */
|
|
3
|
+
export interface RateLimitPolicy {
|
|
4
|
+
/** Stable configuration name and durable-storage partition. */
|
|
5
|
+
readonly name: string;
|
|
6
|
+
/** Number of allowed checks in one window. */
|
|
7
|
+
readonly limit: number;
|
|
8
|
+
/** Window duration in milliseconds. */
|
|
9
|
+
readonly windowMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RateLimitDecision {
|
|
12
|
+
readonly allowed: boolean;
|
|
13
|
+
/** Milliseconds until this key may be checked again. Present on refusal. */
|
|
14
|
+
readonly retryAfter?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface RateLimiter {
|
|
17
|
+
/**
|
|
18
|
+
* Decides whether `key` may proceed.
|
|
19
|
+
*
|
|
20
|
+
* `now` exists for deterministic replay and tests. Normal hosts inject a
|
|
21
|
+
* Clock at construction and omit it.
|
|
22
|
+
*/
|
|
23
|
+
allow(key: string, now?: IsoTimestamp): Promise<RateLimitDecision>;
|
|
24
|
+
}
|
|
25
|
+
export interface LimiterOptions {
|
|
26
|
+
readonly clock?: Clock;
|
|
27
|
+
}
|
|
28
|
+
/** Validates and returns a named policy. */
|
|
29
|
+
export declare function defineRateLimitPolicy(policy: RateLimitPolicy): RateLimitPolicy;
|
|
30
|
+
export declare function assertRateLimitKey(key: string): void;
|
|
31
|
+
export declare function timeInMilliseconds(clock: Clock, now: IsoTimestamp | undefined): number | null;
|
|
32
|
+
export declare function retryAfter(windowEndsAt: number, now: number): number;
|
|
33
|
+
export declare function denyForWindow(windowMs: number): RateLimitDecision;
|
|
34
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAExD,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAkBD,4CAA4C;AAC5C,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,eAAe,GACtB,eAAe,CA0BjB;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAOpD;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,EACZ,GAAG,EAAE,YAAY,GAAG,SAAS,GAC5B,MAAM,GAAG,IAAI,CAcf;AAED,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAEjE"}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function isWellFormedUnicode(value) {
|
|
2
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3
|
+
const codeUnit = value.charCodeAt(index);
|
|
4
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
5
|
+
const next = value.charCodeAt(index + 1);
|
|
6
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
index += 1;
|
|
10
|
+
}
|
|
11
|
+
else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/** Validates and returns a named policy. */
|
|
18
|
+
export function defineRateLimitPolicy(policy) {
|
|
19
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
20
|
+
throw new TypeError("A rate-limit policy must be an object.");
|
|
21
|
+
}
|
|
22
|
+
const name = Object.getOwnPropertyDescriptor(policy, "name")?.value;
|
|
23
|
+
const limit = Object.getOwnPropertyDescriptor(policy, "limit")?.value;
|
|
24
|
+
const windowMs = Object.getOwnPropertyDescriptor(policy, "windowMs")?.value;
|
|
25
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
26
|
+
throw new TypeError("A rate-limit policy name must be a non-blank string.");
|
|
27
|
+
}
|
|
28
|
+
if (!isWellFormedUnicode(name)) {
|
|
29
|
+
throw new TypeError("A rate-limit policy name must contain well-formed Unicode.");
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
32
|
+
throw new TypeError("A rate-limit policy limit must be a positive integer.");
|
|
33
|
+
}
|
|
34
|
+
if (!Number.isSafeInteger(windowMs) || windowMs <= 0) {
|
|
35
|
+
throw new TypeError("A rate-limit policy windowMs must be a positive integer.");
|
|
36
|
+
}
|
|
37
|
+
return Object.freeze({ name, limit, windowMs });
|
|
38
|
+
}
|
|
39
|
+
export function assertRateLimitKey(key) {
|
|
40
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
41
|
+
throw new TypeError("A rate-limit key must be a non-blank string.");
|
|
42
|
+
}
|
|
43
|
+
if (!isWellFormedUnicode(key)) {
|
|
44
|
+
throw new TypeError("A rate-limit key must contain well-formed Unicode.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function timeInMilliseconds(clock, now) {
|
|
48
|
+
try {
|
|
49
|
+
const timestamp = now ?? clock.now();
|
|
50
|
+
if (typeof timestamp !== "string") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const milliseconds = Date.parse(timestamp);
|
|
54
|
+
return Number.isFinite(milliseconds) &&
|
|
55
|
+
new Date(milliseconds).toISOString() === timestamp
|
|
56
|
+
? milliseconds
|
|
57
|
+
: null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function retryAfter(windowEndsAt, now) {
|
|
64
|
+
return Math.max(1, Math.ceil(windowEndsAt - now));
|
|
65
|
+
}
|
|
66
|
+
export function denyForWindow(windowMs) {
|
|
67
|
+
return { allowed: false, retryAfter: windowMs };
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pegma/rate-limit",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Explicit per-instance sliding-window and shared durable fixed-window rate limiters.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*.d.ts",
|
|
13
|
+
"dist/**/*.d.ts.map",
|
|
14
|
+
"dist/**/*.js",
|
|
15
|
+
"dist/**/*.js.map"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./memory": {
|
|
23
|
+
"types": "./dist/memory.d.ts",
|
|
24
|
+
"import": "./dist/memory.js"
|
|
25
|
+
},
|
|
26
|
+
"./durable": {
|
|
27
|
+
"types": "./dist/durable.d.ts",
|
|
28
|
+
"import": "./dist/durable.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/pegma-dev/rate-limit.git",
|
|
37
|
+
"directory": "packages/rate-limit"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@pegma/spine": "0.1.1",
|
|
41
|
+
"@pegma/storage-core": "0.3.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.json",
|
|
45
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
46
|
+
"prepack": "npm run build"
|
|
47
|
+
}
|
|
48
|
+
}
|