@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 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"}
@@ -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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"}
@@ -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,4 @@
1
+ export { defineRateLimitPolicy, } from "./policy.js";
2
+ export { createMemoryLimiter } from "./memory.js";
3
+ export { createDurableLimiter, } from "./durable.js";
4
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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"}
@@ -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
+ }