@lunora/ratelimit 1.0.0-alpha.123 → 1.0.0-alpha.125

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/dist/index.d.mts CHANGED
@@ -80,6 +80,18 @@ interface RateLimitStore {
80
80
  delete: (storageKey: string) => Promise<void> | void;
81
81
  get: (storageKey: string) => Promise<RateLimitValue | undefined> | RateLimitValue | undefined;
82
82
  set: (storageKey: string, value: RateLimitValue) => Promise<void> | void;
83
+ /**
84
+ * Optional conditional write: persist `value` only while the stored value is
85
+ * still `prior` (what `get` returned for this consume, `undefined` for no
86
+ * row), and resolve `false` when it no longer is. When present the limiter
87
+ * writes through it instead of `set`, and a `false` makes it read and decide
88
+ * again rather than admit off a stale read.
89
+ *
90
+ * A store whose `get` and write cannot be interleaved by another consume
91
+ * (memory, SQL: synchronous) needs none. One whose calls can be
92
+ * (`createDbStore`: every call is awaited I/O) must have it.
93
+ */
94
+ setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
83
95
  }
84
96
  /** Inputs to {@link evaluate}. */
85
97
  interface EvaluateOptions {
@@ -173,6 +185,16 @@ declare class RateLimiter<Names extends string = string> {
173
185
  reset(name: Names, args?: {
174
186
  key?: string;
175
187
  }): Promise<void>;
188
+ /**
189
+ * Read, evaluate and write one consume. A store that reports the write lost
190
+ * (`setIf` resolved `false`: another consume changed the value since this one
191
+ * read it) gets a fresh read and a fresh decision, so the admission always
192
+ * rests on the value the write replaced. Bounded: each lost round means some
193
+ * other consume was admitted (a refusal writes nothing), and past
194
+ * {@link MAX_DECIDE_ATTEMPTS} the call is refused with `retryAfter: 0` rather
195
+ * than admitted.
196
+ */
197
+ private decide;
176
198
  private normalizeKey;
177
199
  private resolve;
178
200
  private run;
@@ -249,34 +271,14 @@ interface SqlStoreOptions {
249
271
  * SQLite-backed store for durable, per-DO rate-limit state. Persists each
250
272
  * `(name, key)` pair as one row so limits survive hibernation and eviction.
251
273
  *
252
- * **Atomicity:** the store does not wrap individual operations in an explicit
253
- * SQL transaction. Inside a Durable Object the DO's input gate serializes
254
- * every RPC call against the storage, so the limiter's read-modify-write
255
- * sequence runs to completion without interleaving — this is the same
256
- * guarantee the surrounding `evaluate()` step depends on. **Outside a DO**
257
- * (e.g. driving `createSqlStore` from a long-lived `node:sqlite` connection in
258
- * tests or a custom host) the caller is responsible for serialization; the
259
- * SQL surface used here (`exec`) is not a substitute for transactional
260
- * isolation across concurrent invocations.
274
+ * **Atomicity:** `exec` is synchronous and the limiter does not yield between a
275
+ * synchronous store's read and its write, so one consume's read, decision and
276
+ * write run as a single JavaScript step. No other call on the same isolate can
277
+ * interleave, which is what makes concurrent consumes admit exactly the
278
+ * configured rate. Two isolates sharing one SQLite file (not a Durable Object
279
+ * case) are not covered; serialize those yourself.
261
280
  */
262
281
  declare const createSqlStore: (options: SqlStoreOptions) => RateLimitStore;
263
- /**
264
- * The slice of an index-range builder the store uses. Mirrors `@lunora/server`'s
265
- * `IndexRangeBuilder` field-for-field so the real `ctx.db` query builder is
266
- * assignable; only `eq` is exercised.
267
- */
268
- interface RateLimitDatabaseIndexRange {
269
- eq: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
270
- gt: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
271
- gte: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
272
- lt: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
273
- lte: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
274
- }
275
- /** The slice of a `ctx.db` table query the store relies on. */
276
- interface RateLimitDatabaseQuery {
277
- first: () => Promise<Record<string, unknown> | null>;
278
- withIndex: (indexName: string, range: (q: RateLimitDatabaseIndexRange) => RateLimitDatabaseIndexRange) => RateLimitDatabaseQuery;
279
- }
280
282
  /**
281
283
  * The READ slice — everything the store needs to answer `RateLimiter.getValue`
282
284
  * and `check`, which the docs describe as projecting the stored value forward to
@@ -287,9 +289,16 @@ interface RateLimitDatabaseQuery {
287
289
  * at all — every remaining-quota display had to cast, and a cast that appears
288
290
  * often enough stops carrying information. The distinction was already in the
289
291
  * methods: `getValue`/`check` read, `limit`/`reset` write.
292
+ *
293
+ * `findFirst` rather than `query(table).withIndex(...)`: a `.global()` table's
294
+ * reader has no `withIndex`, so the indexed builder failed every read there.
295
+ * `findFirst` routes to whichever backend owns the table, and an equality on the
296
+ * key column still uses its index.
290
297
  */
291
298
  interface RateLimitDatabaseReader {
292
- query: (table: string) => RateLimitDatabaseQuery;
299
+ findFirst: (table: string, args?: {
300
+ where?: Record<string, unknown>;
301
+ }) => Promise<Record<string, unknown> | null>;
293
302
  }
294
303
  /**
295
304
  * The slice of the Lunora ORM writer (`ctx.db` on a mutation/action) the store
@@ -299,14 +308,20 @@ interface RateLimitDatabaseReader {
299
308
  */
300
309
  interface RateLimitDatabase extends RateLimitDatabaseReader {
301
310
  delete: <T extends string>(id: Id<T>) => Promise<void>;
302
- insert: <T extends string>(table: T, document: Record<string, unknown>) => Promise<Id<T>>;
303
- patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
311
+ insert: <T extends string>(table: T, document: Record<string, unknown>, options?: {
312
+ clientId?: string;
313
+ }) => Promise<Id<T>>;
314
+ patchIf: <T extends string>(table: T, id: Id<T>, args: {
315
+ patch: Record<string, unknown>;
316
+ where: Record<string, unknown>;
317
+ }) => Promise<{
318
+ applied: boolean;
319
+ doc: Record<string, unknown> | null;
320
+ }>;
304
321
  }
305
- /** The table/column/index knobs shared by the read-only and read-write stores. */
322
+ /** The table/column knobs shared by the read-only and read-write stores. */
306
323
  interface DatabaseStoreLocation {
307
- /** Index that resolves a row by its key column. Defaults to `by_key`. */
308
- index?: string;
309
- /** Column storing the opaque key. Defaults to `key`. */
324
+ /** Column storing the opaque key. Defaults to `key`. Index it: every consume reads by it. */
310
325
  keyField?: string;
311
326
  /** Table holding one row per `(name, key)` pair. Defaults to `rateLimits`. */
312
327
  table?: string;
@@ -320,9 +335,10 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
320
335
  db: RateLimitDatabaseReader;
321
336
  }
322
337
  /**
323
- * Store backed by a Lunora table through `ctx.db`, for durable per-DO limits
324
- * inside a procedure (the procedure context exposes no raw SQL). Declare a
325
- * table with the key column and its index, e.g.
338
+ * Store backed by a Lunora table through `ctx.db`, for durable limits inside a
339
+ * procedure (the procedure context exposes no raw SQL). Works on a shard-local
340
+ * table and on a `.global()` one. Declare a table with the key column and its
341
+ * index, e.g.
326
342
  *
327
343
  * ```ts
328
344
  * rateLimits: defineTable({
@@ -333,15 +349,22 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
333
349
  * }).index("by_key", ["key"])
334
350
  * ```
335
351
  *
336
- * Each operation is a read-then-write; inside a mutation/action that pair runs
337
- * under the DO's input gate, so it is atomic against concurrent calls.
352
+ * **Atomic against concurrent consumes.** `setIf` writes only if the row still
353
+ * holds the value `get` returned: an existing row through `ctx.db.patchIf` with
354
+ * that value as its `where`, a new row through an `insert` under an id derived
355
+ * from the key, which a concurrent creator's insert collides with. A consume
356
+ * that loses either race gets `false` back and the limiter reads and decides
357
+ * again, so a limit of 1 admits one caller however many arrive together — on a
358
+ * `.global()` table too, where D1 calls do not hold the Durable Object's input
359
+ * gate.
338
360
  *
339
361
  * **Consumption commits with the procedure.** A mutation's `ctx.db` writes ride
340
362
  * its storage transaction, so a handler that throws after `limit()` rolls the
341
363
  * consumed unit back with everything else — inside a mutation this store counts
342
364
  * successful calls, not attempts. To charge every attempt (a login limiter),
343
365
  * consume from an action, where each write commits on its own, or return a
344
- * failure value from the mutation instead of throwing.
366
+ * failure value from the mutation instead of throwing. A `.global()` table's
367
+ * row lives in D1, outside the DO transaction, so there the unit stays spent.
345
368
  */
346
369
  declare const createDatabaseStore: (options: DatabaseStoreOptions) => RateLimitStore;
347
370
  /**
@@ -370,8 +393,8 @@ declare const createReadOnlyDatabaseStore: (options: ReadOnlyDatabaseStoreOption
370
393
  * on the DO the procedure runs on). Every query/mutation/action ctx exposes a
371
394
  * compatible `db`, so it slots straight into a `.use(...)` chain.
372
395
  *
373
- * Pass `options.store` to point at a non-default backing table/index/key column
374
- * (defaults: table `rateLimits`, index `by_key`, key column `key`); the rest of
396
+ * Pass `options.store` to point at a non-default backing table/key column
397
+ * (defaults: table `rateLimits`, key column `key`); the rest of
375
398
  * `options` (`key`, `count`, `failOpen`, `message`) is forwarded to
376
399
  * {@link rateLimit} unchanged. When `config` is precisely typed, `name`
377
400
  * autocompletes to its declared limit names.
@@ -496,4 +519,4 @@ interface TokenBudget {
496
519
  */
497
520
  declare const tokenBudget: <Names extends string>(limiter: RateLimiter<Names>, name: Names) => TokenBudget;
498
521
  declare const VERSION = "0.0.0";
499
- export { type DatabaseStoreOptions as DbStoreOptions, type EvaluateOptions, type EvaluateResult, type LimiterResolver, type RateLimitArgs, type RateLimitConfig, type RateLimitConfigMap, type RateLimitDatabase as RateLimitDb, type RateLimitDatabaseIndexRange as RateLimitDbIndexRange, type RateLimitDatabaseQuery as RateLimitDbQuery, type RateLimitDatabaseReader as RateLimitDbReader, RateLimitError, type RateLimitKind, type RateLimitMiddlewareOptions, type RateLimitReason, type RateLimitStatus, type RateLimitStore, type RateLimitValue, RateLimiter, type RateLimiterOptions, type RatelimitApiContext, type ReadOnlyDatabaseStoreOptions as ReadOnlyDbStoreOptions, type SqlLike, type SqlStoreOptions, type TokenBudget, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin, tokenBudget };
522
+ export { type DatabaseStoreOptions as DbStoreOptions, type EvaluateOptions, type EvaluateResult, type LimiterResolver, type RateLimitArgs, type RateLimitConfig, type RateLimitConfigMap, type RateLimitDatabase as RateLimitDb, type RateLimitDatabaseReader as RateLimitDbReader, RateLimitError, type RateLimitKind, type RateLimitMiddlewareOptions, type RateLimitReason, type RateLimitStatus, type RateLimitStore, type RateLimitValue, RateLimiter, type RateLimiterOptions, type RatelimitApiContext, type ReadOnlyDatabaseStoreOptions as ReadOnlyDbStoreOptions, type SqlLike, type SqlStoreOptions, type TokenBudget, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin, tokenBudget };
package/dist/index.d.ts CHANGED
@@ -80,6 +80,18 @@ interface RateLimitStore {
80
80
  delete: (storageKey: string) => Promise<void> | void;
81
81
  get: (storageKey: string) => Promise<RateLimitValue | undefined> | RateLimitValue | undefined;
82
82
  set: (storageKey: string, value: RateLimitValue) => Promise<void> | void;
83
+ /**
84
+ * Optional conditional write: persist `value` only while the stored value is
85
+ * still `prior` (what `get` returned for this consume, `undefined` for no
86
+ * row), and resolve `false` when it no longer is. When present the limiter
87
+ * writes through it instead of `set`, and a `false` makes it read and decide
88
+ * again rather than admit off a stale read.
89
+ *
90
+ * A store whose `get` and write cannot be interleaved by another consume
91
+ * (memory, SQL: synchronous) needs none. One whose calls can be
92
+ * (`createDbStore`: every call is awaited I/O) must have it.
93
+ */
94
+ setIf?: (storageKey: string, value: RateLimitValue, prior: RateLimitValue | undefined) => Promise<boolean>;
83
95
  }
84
96
  /** Inputs to {@link evaluate}. */
85
97
  interface EvaluateOptions {
@@ -173,6 +185,16 @@ declare class RateLimiter<Names extends string = string> {
173
185
  reset(name: Names, args?: {
174
186
  key?: string;
175
187
  }): Promise<void>;
188
+ /**
189
+ * Read, evaluate and write one consume. A store that reports the write lost
190
+ * (`setIf` resolved `false`: another consume changed the value since this one
191
+ * read it) gets a fresh read and a fresh decision, so the admission always
192
+ * rests on the value the write replaced. Bounded: each lost round means some
193
+ * other consume was admitted (a refusal writes nothing), and past
194
+ * {@link MAX_DECIDE_ATTEMPTS} the call is refused with `retryAfter: 0` rather
195
+ * than admitted.
196
+ */
197
+ private decide;
176
198
  private normalizeKey;
177
199
  private resolve;
178
200
  private run;
@@ -249,34 +271,14 @@ interface SqlStoreOptions {
249
271
  * SQLite-backed store for durable, per-DO rate-limit state. Persists each
250
272
  * `(name, key)` pair as one row so limits survive hibernation and eviction.
251
273
  *
252
- * **Atomicity:** the store does not wrap individual operations in an explicit
253
- * SQL transaction. Inside a Durable Object the DO's input gate serializes
254
- * every RPC call against the storage, so the limiter's read-modify-write
255
- * sequence runs to completion without interleaving — this is the same
256
- * guarantee the surrounding `evaluate()` step depends on. **Outside a DO**
257
- * (e.g. driving `createSqlStore` from a long-lived `node:sqlite` connection in
258
- * tests or a custom host) the caller is responsible for serialization; the
259
- * SQL surface used here (`exec`) is not a substitute for transactional
260
- * isolation across concurrent invocations.
274
+ * **Atomicity:** `exec` is synchronous and the limiter does not yield between a
275
+ * synchronous store's read and its write, so one consume's read, decision and
276
+ * write run as a single JavaScript step. No other call on the same isolate can
277
+ * interleave, which is what makes concurrent consumes admit exactly the
278
+ * configured rate. Two isolates sharing one SQLite file (not a Durable Object
279
+ * case) are not covered; serialize those yourself.
261
280
  */
262
281
  declare const createSqlStore: (options: SqlStoreOptions) => RateLimitStore;
263
- /**
264
- * The slice of an index-range builder the store uses. Mirrors `@lunora/server`'s
265
- * `IndexRangeBuilder` field-for-field so the real `ctx.db` query builder is
266
- * assignable; only `eq` is exercised.
267
- */
268
- interface RateLimitDatabaseIndexRange {
269
- eq: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
270
- gt: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
271
- gte: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
272
- lt: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
273
- lte: (field: string, value: unknown) => RateLimitDatabaseIndexRange;
274
- }
275
- /** The slice of a `ctx.db` table query the store relies on. */
276
- interface RateLimitDatabaseQuery {
277
- first: () => Promise<Record<string, unknown> | null>;
278
- withIndex: (indexName: string, range: (q: RateLimitDatabaseIndexRange) => RateLimitDatabaseIndexRange) => RateLimitDatabaseQuery;
279
- }
280
282
  /**
281
283
  * The READ slice — everything the store needs to answer `RateLimiter.getValue`
282
284
  * and `check`, which the docs describe as projecting the stored value forward to
@@ -287,9 +289,16 @@ interface RateLimitDatabaseQuery {
287
289
  * at all — every remaining-quota display had to cast, and a cast that appears
288
290
  * often enough stops carrying information. The distinction was already in the
289
291
  * methods: `getValue`/`check` read, `limit`/`reset` write.
292
+ *
293
+ * `findFirst` rather than `query(table).withIndex(...)`: a `.global()` table's
294
+ * reader has no `withIndex`, so the indexed builder failed every read there.
295
+ * `findFirst` routes to whichever backend owns the table, and an equality on the
296
+ * key column still uses its index.
290
297
  */
291
298
  interface RateLimitDatabaseReader {
292
- query: (table: string) => RateLimitDatabaseQuery;
299
+ findFirst: (table: string, args?: {
300
+ where?: Record<string, unknown>;
301
+ }) => Promise<Record<string, unknown> | null>;
293
302
  }
294
303
  /**
295
304
  * The slice of the Lunora ORM writer (`ctx.db` on a mutation/action) the store
@@ -299,14 +308,20 @@ interface RateLimitDatabaseReader {
299
308
  */
300
309
  interface RateLimitDatabase extends RateLimitDatabaseReader {
301
310
  delete: <T extends string>(id: Id<T>) => Promise<void>;
302
- insert: <T extends string>(table: T, document: Record<string, unknown>) => Promise<Id<T>>;
303
- patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
311
+ insert: <T extends string>(table: T, document: Record<string, unknown>, options?: {
312
+ clientId?: string;
313
+ }) => Promise<Id<T>>;
314
+ patchIf: <T extends string>(table: T, id: Id<T>, args: {
315
+ patch: Record<string, unknown>;
316
+ where: Record<string, unknown>;
317
+ }) => Promise<{
318
+ applied: boolean;
319
+ doc: Record<string, unknown> | null;
320
+ }>;
304
321
  }
305
- /** The table/column/index knobs shared by the read-only and read-write stores. */
322
+ /** The table/column knobs shared by the read-only and read-write stores. */
306
323
  interface DatabaseStoreLocation {
307
- /** Index that resolves a row by its key column. Defaults to `by_key`. */
308
- index?: string;
309
- /** Column storing the opaque key. Defaults to `key`. */
324
+ /** Column storing the opaque key. Defaults to `key`. Index it: every consume reads by it. */
310
325
  keyField?: string;
311
326
  /** Table holding one row per `(name, key)` pair. Defaults to `rateLimits`. */
312
327
  table?: string;
@@ -320,9 +335,10 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
320
335
  db: RateLimitDatabaseReader;
321
336
  }
322
337
  /**
323
- * Store backed by a Lunora table through `ctx.db`, for durable per-DO limits
324
- * inside a procedure (the procedure context exposes no raw SQL). Declare a
325
- * table with the key column and its index, e.g.
338
+ * Store backed by a Lunora table through `ctx.db`, for durable limits inside a
339
+ * procedure (the procedure context exposes no raw SQL). Works on a shard-local
340
+ * table and on a `.global()` one. Declare a table with the key column and its
341
+ * index, e.g.
326
342
  *
327
343
  * ```ts
328
344
  * rateLimits: defineTable({
@@ -333,15 +349,22 @@ interface ReadOnlyDatabaseStoreOptions extends DatabaseStoreLocation {
333
349
  * }).index("by_key", ["key"])
334
350
  * ```
335
351
  *
336
- * Each operation is a read-then-write; inside a mutation/action that pair runs
337
- * under the DO's input gate, so it is atomic against concurrent calls.
352
+ * **Atomic against concurrent consumes.** `setIf` writes only if the row still
353
+ * holds the value `get` returned: an existing row through `ctx.db.patchIf` with
354
+ * that value as its `where`, a new row through an `insert` under an id derived
355
+ * from the key, which a concurrent creator's insert collides with. A consume
356
+ * that loses either race gets `false` back and the limiter reads and decides
357
+ * again, so a limit of 1 admits one caller however many arrive together — on a
358
+ * `.global()` table too, where D1 calls do not hold the Durable Object's input
359
+ * gate.
338
360
  *
339
361
  * **Consumption commits with the procedure.** A mutation's `ctx.db` writes ride
340
362
  * its storage transaction, so a handler that throws after `limit()` rolls the
341
363
  * consumed unit back with everything else — inside a mutation this store counts
342
364
  * successful calls, not attempts. To charge every attempt (a login limiter),
343
365
  * consume from an action, where each write commits on its own, or return a
344
- * failure value from the mutation instead of throwing.
366
+ * failure value from the mutation instead of throwing. A `.global()` table's
367
+ * row lives in D1, outside the DO transaction, so there the unit stays spent.
345
368
  */
346
369
  declare const createDatabaseStore: (options: DatabaseStoreOptions) => RateLimitStore;
347
370
  /**
@@ -370,8 +393,8 @@ declare const createReadOnlyDatabaseStore: (options: ReadOnlyDatabaseStoreOption
370
393
  * on the DO the procedure runs on). Every query/mutation/action ctx exposes a
371
394
  * compatible `db`, so it slots straight into a `.use(...)` chain.
372
395
  *
373
- * Pass `options.store` to point at a non-default backing table/index/key column
374
- * (defaults: table `rateLimits`, index `by_key`, key column `key`); the rest of
396
+ * Pass `options.store` to point at a non-default backing table/key column
397
+ * (defaults: table `rateLimits`, key column `key`); the rest of
375
398
  * `options` (`key`, `count`, `failOpen`, `message`) is forwarded to
376
399
  * {@link rateLimit} unchanged. When `config` is precisely typed, `name`
377
400
  * autocompletes to its declared limit names.
@@ -496,4 +519,4 @@ interface TokenBudget {
496
519
  */
497
520
  declare const tokenBudget: <Names extends string>(limiter: RateLimiter<Names>, name: Names) => TokenBudget;
498
521
  declare const VERSION = "0.0.0";
499
- export { type DatabaseStoreOptions as DbStoreOptions, type EvaluateOptions, type EvaluateResult, type LimiterResolver, type RateLimitArgs, type RateLimitConfig, type RateLimitConfigMap, type RateLimitDatabase as RateLimitDb, type RateLimitDatabaseIndexRange as RateLimitDbIndexRange, type RateLimitDatabaseQuery as RateLimitDbQuery, type RateLimitDatabaseReader as RateLimitDbReader, RateLimitError, type RateLimitKind, type RateLimitMiddlewareOptions, type RateLimitReason, type RateLimitStatus, type RateLimitStore, type RateLimitValue, RateLimiter, type RateLimiterOptions, type RatelimitApiContext, type ReadOnlyDatabaseStoreOptions as ReadOnlyDbStoreOptions, type SqlLike, type SqlStoreOptions, type TokenBudget, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin, tokenBudget };
522
+ export { type DatabaseStoreOptions as DbStoreOptions, type EvaluateOptions, type EvaluateResult, type LimiterResolver, type RateLimitArgs, type RateLimitConfig, type RateLimitConfigMap, type RateLimitDatabase as RateLimitDb, type RateLimitDatabaseReader as RateLimitDbReader, RateLimitError, type RateLimitKind, type RateLimitMiddlewareOptions, type RateLimitReason, type RateLimitStatus, type RateLimitStore, type RateLimitValue, RateLimiter, type RateLimiterOptions, type RatelimitApiContext, type ReadOnlyDatabaseStoreOptions as ReadOnlyDbStoreOptions, type SqlLike, type SqlStoreOptions, type TokenBudget, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin, tokenBudget };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BSKYrouo.mjs";import{default as i}from"./packem_shared/dbRateLimit-BuvEF1x0.mjs";import{default as l}from"./packem_shared/RateLimitError-9Y5OLxpl.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-CWZur6XE.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-CFnDs9rj.mjs";import{RateLimiter as u}from"./packem_shared/RateLimiter-Cbc67DNk.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-ve7GsO2i.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-BRlWn1im.mjs";const e="0.0.0";export{l as RateLimitError,u as RateLimiter,e as VERSION,o as availableAt,b as createDbStore,n as createMemoryStore,L as createReadOnlyDbStore,s as createSqlStore,i as dbRateLimit,a as evaluate,x as rateLimit,c as ratelimitPlugin,v as tokenBudget};
1
+ import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BSKYrouo.mjs";import{default as i}from"./packem_shared/dbRateLimit-B1Jf_kBi.mjs";import{default as l}from"./packem_shared/RateLimitError-9Y5OLxpl.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-CWZur6XE.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-CFnDs9rj.mjs";import{RateLimiter as u}from"./packem_shared/RateLimiter-Cr80iRjQ.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-k6ol27eT.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-BRlWn1im.mjs";const e="0.0.0";export{l as RateLimitError,u as RateLimiter,e as VERSION,o as availableAt,b as createDbStore,n as createMemoryStore,L as createReadOnlyDbStore,s as createSqlStore,i as dbRateLimit,a as evaluate,x as rateLimit,c as ratelimitPlugin,v as tokenBudget};
@@ -0,0 +1 @@
1
+ import{LunoraError as h}from"@lunora/errors";import{availableAt as w,evaluate as p}from"./availableAt-BSKYrouo.mjs";import m from"./RateLimitError-9Y5OLxpl.mjs";import{createMemoryStore as y}from"./createDbStore-k6ol27eT.mjs";const v=/^[\d.:a-f]+$/iu,d=i=>{const t=i.indexOf("%"),e=t===-1?i:i.slice(0,t);if(!e.includes(":")||!v.test(e))return i;let r;try{r=new URL(`http://[${e}]/`).hostname.slice(1,-1)}catch{return i}const[c="",o]=r.split("::"),s=c===""?[]:c.split(":"),l=o===void 0||o===""?[]:o.split(":"),n=[...s,...Array.from({length:8-s.length-l.length},()=>"0"),...l].map(a=>Number.parseInt(a,16));if(n.slice(0,5).every(a=>a===0)&&n[5]===65535){const[a=0,f=0]=n.slice(6);return[a>>8,a&255,f>>8,f&255].join(".")}return`${n.slice(0,4).map(a=>a.toString(16)).join(":")}::/64`},u=(i,t)=>t===void 0?encodeURIComponent(i):`${encodeURIComponent(i)}:${encodeURIComponent(t)}`,b=16,N=i=>typeof i?.then=="function",g=i=>i?t=>d(i(t)):d,I="@lunora/ratelimit: new RateLimiter() was built with no explicit `store`, so it falls back to `createMemoryStore()` — an in-process Map. That Map is durable and correctly shared only for calls that land on the same Durable Object instance; it is NOT shared across `.shardBy(...)` shards or `.global()` replicas, and it resets when the DO instance is evicted/restarted. For a limit that must hold across any of those, pass a durable `store` (`createDbStore` or `createSqlStore`) instead. Pass `store: createMemoryStore()` explicitly once you've confirmed the in-memory default is correct here, to silence this warning.";class E{config;denyList;normalize;now;store;constructor(t){this.config=t.config,this.normalize=g(t.normalize),this.denyList=new Set([...t.denyList??[]].flatMap(e=>[e,this.normalize(e)])),this.now=t.now??Date.now,t.store===void 0&&console.warn(I),this.store=t.store??y();for(const[e,r]of Object.entries(this.config)){if(Object.hasOwn(r,"shards"))throw new h("INTERNAL",`rate limit "${e}": \`shards\` is no longer supported — remove it; the limit now enforces its full \`rate\` (lower \`rate\` if you raised it to offset the split)`);if(!Number.isFinite(r.period)||r.period<=0)throw new h("INTERNAL",`rate limit "${e}": period must be a positive number`);if(!Number.isFinite(r.rate)||r.rate<=0)throw new h("INTERNAL",`rate limit "${e}": rate must be a positive number`);if(r.capacity!==void 0&&(!Number.isFinite(r.capacity)||r.capacity<0))throw new h("INTERNAL",`rate limit "${e}": capacity must be a non-negative number`)}}async check(t,e={}){return this.run(t,e,!1)}async getValue(t,e={}){const r=this.resolve(t),c=this.now(),o=u(t,this.normalizeKey(e.key)),s=w(r,await this.store.get(o),c);return{config:r,ts:s.ts,value:s.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){this.resolve(t),await Promise.resolve(this.store.delete(u(t,this.normalizeKey(e.key))))}async decide(t,e,r){for(let c=0;c<b;c+=1){const o=this.store.get(e),s=N(o)?await o:o,{status:l,value:n}=p(t,s,{...r,now:this.now()});if(n===void 0)return l;if(this.store.setIf===void 0)return await this.store.set(e,n),l;if(await this.store.setIf(e,n,s))return l}return{ok:!1,reason:"rate",retryAfter:0}}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new h("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,r){const c=this.resolve(t),o=this.normalizeKey(e.key);if(o!==void 0&&(this.denyList.has(o)||this.denyList.has(e.key))){const a={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new m(a);return a}const s=e.count??1;if(!Number.isInteger(s)||s<=0)throw new h("INTERNAL",`rate limit "${t}": count must be a positive integer`);const l=u(t,o),n=await this.decide(c,l,{consume:r,count:s,reserve:e.reserve??!1});if(!n.ok&&e.throws)throw new m(n);return n}}export{E as RateLimiter};
@@ -0,0 +1 @@
1
+ import{LunoraError as E}from"@lunora/errors";const m=()=>{const n=new Map;return{delete:t=>{n.delete(t)},get:t=>n.get(t),set:(t,c)=>{n.set(t,c)}}},u=(n,t,...c)=>n.exec.call(n,t,...c).toArray(),L=n=>{const{sql:t}=n,c=n.table??"_lunora_rate_limits";return u(t,`CREATE TABLE IF NOT EXISTS "${c}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:s=>{u(t,`DELETE FROM "${c}" WHERE k = ?`,s)},get:s=>{const l=u(t,`SELECT value, ts, prev FROM "${c}" WHERE k = ?`,s)[0];if(!l)return;const d={ts:l.ts,value:l.value};return l.prev!==null&&(d.prev=l.prev),d},set:(s,a)=>{u(t,`INSERT INTO "${c}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,s,a.value,a.ts,a.prev??null)}}},b=n=>n.toString(16).padStart(2,"0"),S=async(n,t)=>{const s=[...new Uint8Array(await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`${n}\0${t}`))).subarray(0,16)];s[6]=128+s[6]%16,s[8]=128+s[8]%64;const a=s.map(l=>b(l)).join("");return`${a.slice(0,8)}-${a.slice(8,12)}-${a.slice(12,16)}-${a.slice(16,20)}-${a.slice(20)}`},T=n=>n?.code==="CONFLICT",h=n=>{const{db:t}=n,c=n.table??"rateLimits",s=n.keyField??"key",a=new Map,l=async e=>{const r=await t.findFirst(c,{where:{[s]:e}});return a.set(e,r?r._id:void 0),r},d=async e=>(a.has(e)||await l(e),a.get(e)),v=(e,r)=>{const i={[s]:e,ts:r.ts,value:r.value};return r.prev!==void 0&&(i.prev=r.prev),i},w=async(e,r)=>{try{return a.set(e,await t.insert(c,r,{clientId:await S(c,e)})),!0}catch(i){if(T(i))return a.delete(e),!1;throw i}};return{delete:async e=>{const r=await d(e);r!==void 0&&await t.delete(r),a.delete(e)},get:async e=>{const r=await l(e);if(!r)return;const i={ts:r.ts,value:r.value};return r.prev!==null&&r.prev!==void 0&&(i.prev=r.prev),i},set:async(e,r)=>{const i=v(e,r);let o=await d(e);if(o===void 0){if(await w(e,i))return;await l(e),o=a.get(e)}o!==void 0&&await t.patchIf(c,o,{patch:i,where:{}})},setIf:async(e,r,i)=>{const o=v(e,r);if(i===void 0)return w(e,o);const f=await d(e);if(f===void 0)return!1;const{applied:p}=await t.patchIf(c,f,{patch:o,where:{prev:i.prev??null,ts:i.ts,value:i.value}});return p||a.delete(e),p}}},R=n=>{const t=s=>{throw new E("INTERNAL",`@lunora/ratelimit: \`${s}\` needs a writable \`ctx.db\`, but this store was created with \`createReadOnlyDbStore\` (a query context). Use \`createDbStore\` from a mutation or action; a query can only call \`getValue\`/\`check\`.`)},c=h({...n,db:{delete:()=>t("delete"),findFirst:n.db.findFirst.bind(n.db),insert:()=>t("insert"),patchIf:()=>t("patchIf")}});return{delete:()=>t("reset"),get:c.get,set:()=>t("limit"),setIf:()=>t("limit")}};export{h as createDbStore,m as createMemoryStore,R as createReadOnlyDbStore,L as createSqlStore};
@@ -1 +1 @@
1
- import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-Cbc67DNk.mjs";import{createDbStore as i}from"./createDbStore-ve7GsO2i.mjs";const f=(e,r,t={})=>o(a=>new m({config:e,store:i({db:a.db,...t.store})}),r,t);export{f as default};
1
+ import{rateLimit as o}from"./rateLimit-CWZur6XE.mjs";import{RateLimiter as m}from"./RateLimiter-Cr80iRjQ.mjs";import{createDbStore as i}from"./createDbStore-k6ol27eT.mjs";const f=(e,r,t={})=>o(a=>new m({config:e,store:i({db:a.db,...t.store})}),r,t);export{f as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/ratelimit",
3
- "version": "1.0.0-alpha.123",
3
+ "version": "1.0.0-alpha.125",
4
4
  "description": "Rate limiting: token-bucket / fixed-window / sliding-window algorithms, deny list, pluggable stores, and procedure middleware",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- import{LunoraError as l}from"@lunora/errors";import{availableAt as p,evaluate as y}from"./availableAt-BSKYrouo.mjs";import f from"./RateLimitError-9Y5OLxpl.mjs";import{createMemoryStore as v}from"./createDbStore-ve7GsO2i.mjs";const b=/^[\d.:a-f]+$/iu,w=i=>{const t=i.indexOf("%"),e=t===-1?i:i.slice(0,t);if(!e.includes(":")||!b.test(e))return i;let r;try{r=new URL(`http://[${e}]/`).hostname.slice(1,-1)}catch{return i}const[a="",s]=r.split("::"),n=a===""?[]:a.split(":"),h=s===void 0||s===""?[]:s.split(":"),c=[...n,...Array.from({length:8-n.length-h.length},()=>"0"),...h].map(o=>Number.parseInt(o,16));if(c.slice(0,5).every(o=>o===0)&&c[5]===65535){const[o=0,m=0]=c.slice(6);return[o>>8,o&255,m>>8,m&255].join(".")}return`${c.slice(0,4).map(o=>o.toString(16)).join(":")}::/64`},u=(i,t)=>t===void 0?encodeURIComponent(i):`${encodeURIComponent(i)}:${encodeURIComponent(t)}`,g=i=>i?t=>w(i(t)):w,N="@lunora/ratelimit: new RateLimiter() was built with no explicit `store`, so it falls back to `createMemoryStore()` — an in-process Map. That Map is durable and correctly shared only for calls that land on the same Durable Object instance; it is NOT shared across `.shardBy(...)` shards or `.global()` replicas, and it resets when the DO instance is evicted/restarted. For a limit that must hold across any of those, pass a durable `store` (`createDbStore` or `createSqlStore`) instead. Pass `store: createMemoryStore()` explicitly once you've confirmed the in-memory default is correct here, to silence this warning.";class S{config;denyList;normalize;now;store;constructor(t){this.config=t.config,this.normalize=g(t.normalize),this.denyList=new Set([...t.denyList??[]].flatMap(e=>[e,this.normalize(e)])),this.now=t.now??Date.now,t.store===void 0&&console.warn(N),this.store=t.store??v();for(const[e,r]of Object.entries(this.config)){if(Object.hasOwn(r,"shards"))throw new l("INTERNAL",`rate limit "${e}": \`shards\` is no longer supported — remove it; the limit now enforces its full \`rate\` (lower \`rate\` if you raised it to offset the split)`);if(!Number.isFinite(r.period)||r.period<=0)throw new l("INTERNAL",`rate limit "${e}": period must be a positive number`);if(!Number.isFinite(r.rate)||r.rate<=0)throw new l("INTERNAL",`rate limit "${e}": rate must be a positive number`);if(r.capacity!==void 0&&(!Number.isFinite(r.capacity)||r.capacity<0))throw new l("INTERNAL",`rate limit "${e}": capacity must be a non-negative number`)}}async check(t,e={}){return this.run(t,e,!1)}async getValue(t,e={}){const r=this.resolve(t),a=this.now(),s=u(t,this.normalizeKey(e.key)),n=p(r,await this.store.get(s),a);return{config:r,ts:n.ts,value:n.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){this.resolve(t),await Promise.resolve(this.store.delete(u(t,this.normalizeKey(e.key))))}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new l("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,r){const a=this.resolve(t),s=this.normalizeKey(e.key);if(s!==void 0&&(this.denyList.has(s)||this.denyList.has(e.key))){const d={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new f(d);return d}const n=e.count??1;if(!Number.isInteger(n)||n<=0)throw new l("INTERNAL",`rate limit "${t}": count must be a positive integer`);const h=u(t,s),c=await this.store.get(h),{status:o,value:m}=y(a,c,{consume:r,count:n,now:this.now(),reserve:e.reserve??!1});if(m!==void 0&&await this.store.set(h,m),!o.ok&&e.throws)throw new f(o);return o}}export{S as RateLimiter};
@@ -1 +0,0 @@
1
- import{LunoraError as E}from"@lunora/errors";const p=()=>{const t=new Map;return{delete:e=>{t.delete(e)},get:e=>t.get(e),set:(e,n)=>{t.set(e,n)}}},i=(t,e,...n)=>t.exec.call(t,e,...n).toArray(),T=t=>{const{sql:e}=t,n=t.table??"_lunora_rate_limits";return i(e,`CREATE TABLE IF NOT EXISTS "${n}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:s=>{i(e,`DELETE FROM "${n}" WHERE k = ?`,s)},get:s=>{const c=i(e,`SELECT value, ts, prev FROM "${n}" WHERE k = ?`,s)[0];if(!c)return;const d={ts:c.ts,value:c.value};return c.prev!==null&&(d.prev=c.prev),d},set:(s,l)=>{i(e,`INSERT INTO "${n}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,s,l.value,l.ts,l.prev??null)}}},w=t=>{const{db:e}=t,n=t.table??"rateLimits",s=t.index??"by_key",l=t.keyField??"key",c=new Map,d=async a=>{const r=await e.query(n).withIndex(s,o=>o.eq(l,a)).first();return c.set(a,r?r._id:void 0),r},v=async a=>(c.has(a)||await d(a),c.get(a));return{delete:async a=>{const r=await v(a);r!==void 0&&await e.delete(r),c.delete(a)},get:async a=>{const r=await d(a);if(!r)return;const o={ts:r.ts,value:r.value};return r.prev!==null&&r.prev!==void 0&&(o.prev=r.prev),o},set:async(a,r)=>{const o=await v(a),u={[l]:a,ts:r.ts,value:r.value};r.prev!==void 0&&(u.prev=r.prev),o===void 0?c.set(a,await e.insert(n,u)):await e.patch(o,u)}}},S=t=>{const e=s=>{throw new E("INTERNAL",`@lunora/ratelimit: \`${s}\` needs a writable \`ctx.db\`, but this store was created with \`createReadOnlyDbStore\` (a query context). Use \`createDbStore\` from a mutation or action; a query can only call \`getValue\`/\`check\`.`)},n=w({...t,db:{delete:()=>e("delete"),insert:()=>e("insert"),patch:()=>e("patch"),query:t.db.query.bind(t.db)}});return{delete:()=>e("reset"),get:n.get,set:()=>e("limit")}};export{w as createDbStore,p as createMemoryStore,S as createReadOnlyDbStore,T as createSqlStore};