@lunora/ratelimit 1.0.0-alpha.21 → 1.0.0-alpha.23

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
@@ -440,5 +440,50 @@ interface RatelimitApiContext<Context> {
440
440
  * `@lunora/server` a type-only dependency of `@lunora/ratelimit`.
441
441
  */
442
442
  declare const ratelimitPlugin: <Context = unknown>(limiter: LimiterResolver<Context>) => Plugin<Record<never, never>, Context, Context & RatelimitApiContext<Context>>;
443
+ /** A budget bound to one named limit — check before the call, record after it. */
444
+ interface TokenBudget {
445
+ /**
446
+ * Peek at the budget before spending. `ok: false` means it is exhausted:
447
+ * refuse the call, and `retryAfter` says when it refills. Consumes nothing.
448
+ */
449
+ check: (key: string) => Promise<RateLimitStatus>;
450
+ /**
451
+ * Charge the tokens a call actually used. Always call it, including when the
452
+ * call THREW — a failed generation that consumed input tokens still has to be
453
+ * paid for. `tokens` of `0` is a no-op, so a call that spent nothing costs
454
+ * nothing.
455
+ *
456
+ * The charge is a reservation, so it may take the bucket negative: the tokens
457
+ * are already spent, and refusing to record them would let a single oversized
458
+ * call escape the budget entirely.
459
+ */
460
+ record: (key: string, tokens: number) => Promise<RateLimitStatus>;
461
+ }
462
+ /**
463
+ * Bind a {@link TokenBudget} to one of a limiter's named limits.
464
+ *
465
+ * ```ts
466
+ * const budget = tokenBudget(limiter, "tokens");
467
+ * const allowed = await budget.check(userId);
468
+ *
469
+ * if (!allowed.ok) {
470
+ * throw new LunoraError("RATE_LIMITED", `token budget exhausted; retry in ${String(allowed.retryAfter)}ms`);
471
+ * }
472
+ *
473
+ * try {
474
+ * const { text, usage } = await generateText({ model: ctx.ai.model(), prompt });
475
+ *
476
+ * await budget.record(userId, usage?.totalTokens ?? 0);
477
+ *
478
+ * return text;
479
+ * } catch (error) {
480
+ * // The prompt was still sent — charge what is known, then rethrow.
481
+ * await budget.record(userId, estimatedInputTokens);
482
+ *
483
+ * throw error;
484
+ * }
485
+ * ```
486
+ */
487
+ declare const tokenBudget: <Names extends string>(limiter: RateLimiter<Names>, name: Names) => TokenBudget;
443
488
  declare const VERSION = "0.0.0";
444
- 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, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin };
489
+ 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 };
package/dist/index.d.ts CHANGED
@@ -440,5 +440,50 @@ interface RatelimitApiContext<Context> {
440
440
  * `@lunora/server` a type-only dependency of `@lunora/ratelimit`.
441
441
  */
442
442
  declare const ratelimitPlugin: <Context = unknown>(limiter: LimiterResolver<Context>) => Plugin<Record<never, never>, Context, Context & RatelimitApiContext<Context>>;
443
+ /** A budget bound to one named limit — check before the call, record after it. */
444
+ interface TokenBudget {
445
+ /**
446
+ * Peek at the budget before spending. `ok: false` means it is exhausted:
447
+ * refuse the call, and `retryAfter` says when it refills. Consumes nothing.
448
+ */
449
+ check: (key: string) => Promise<RateLimitStatus>;
450
+ /**
451
+ * Charge the tokens a call actually used. Always call it, including when the
452
+ * call THREW — a failed generation that consumed input tokens still has to be
453
+ * paid for. `tokens` of `0` is a no-op, so a call that spent nothing costs
454
+ * nothing.
455
+ *
456
+ * The charge is a reservation, so it may take the bucket negative: the tokens
457
+ * are already spent, and refusing to record them would let a single oversized
458
+ * call escape the budget entirely.
459
+ */
460
+ record: (key: string, tokens: number) => Promise<RateLimitStatus>;
461
+ }
462
+ /**
463
+ * Bind a {@link TokenBudget} to one of a limiter's named limits.
464
+ *
465
+ * ```ts
466
+ * const budget = tokenBudget(limiter, "tokens");
467
+ * const allowed = await budget.check(userId);
468
+ *
469
+ * if (!allowed.ok) {
470
+ * throw new LunoraError("RATE_LIMITED", `token budget exhausted; retry in ${String(allowed.retryAfter)}ms`);
471
+ * }
472
+ *
473
+ * try {
474
+ * const { text, usage } = await generateText({ model: ctx.ai.model(), prompt });
475
+ *
476
+ * await budget.record(userId, usage?.totalTokens ?? 0);
477
+ *
478
+ * return text;
479
+ * } catch (error) {
480
+ * // The prompt was still sent — charge what is known, then rethrow.
481
+ * await budget.record(userId, estimatedInputTokens);
482
+ *
483
+ * throw error;
484
+ * }
485
+ * ```
486
+ */
487
+ declare const tokenBudget: <Names extends string>(limiter: RateLimiter<Names>, name: Names) => TokenBudget;
443
488
  declare const VERSION = "0.0.0";
444
- 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, VERSION, availableAt, createDatabaseStore as createDbStore, createMemoryStore, createReadOnlyDatabaseStore as createReadOnlyDbStore, createSqlStore, databaseRateLimit as dbRateLimit, evaluate, rateLimit, ratelimitPlugin };
489
+ 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 };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-BMlAt2U2.mjs";import{default as i}from"./packem_shared/dbRateLimit-e95-suu-.mjs";import{default as l}from"./packem_shared/RateLimitError-xJ2Iv-i7.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-DLcwK-84.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-D1QHZF9b.mjs";import{RateLimiter as b}from"./packem_shared/RateLimiter-CitqoGTI.mjs";import{createDbStore as u,createMemoryStore as L,createReadOnlyDbStore as n,createSqlStore as s}from"./packem_shared/createDbStore-CwJBMxDQ.mjs";const e="0.0.0";export{l as RateLimitError,b as RateLimiter,e as VERSION,o as availableAt,u as createDbStore,L as createMemoryStore,n as createReadOnlyDbStore,s as createSqlStore,i as dbRateLimit,a as evaluate,x as rateLimit,c as ratelimitPlugin};
1
+ import{availableAt as o,evaluate as a}from"./packem_shared/availableAt-DR8rnuHe.mjs";import{default as i}from"./packem_shared/dbRateLimit-B_VDMbMh.mjs";import{default as l}from"./packem_shared/RateLimitError-BfvwU1oc.mjs";import{rateLimit as x}from"./packem_shared/rateLimit-DXZ0uxTf.mjs";import{ratelimitPlugin as c}from"./packem_shared/ratelimitPlugin-CFnDs9rj.mjs";import{RateLimiter as u}from"./packem_shared/RateLimiter-BuMb1rK0.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-xtfjDj1h.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-DbgcKjcB.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 n}from"@lunora/errors";import{STATUS_BY_REASON as a}from"./rateLimit-DXZ0uxTf.mjs";const s=r=>r.reason==="deny"?"request denied (deny list)":Number.isFinite(r.retryAfter)?`rate limit exceeded; retry after ${String(Math.ceil(r.retryAfter))}ms`:"rate limit exceeded";class c extends n{reason;retryAfter;constructor(e,t){const{code:i,status:o}=a[e.reason??"rate"];super(i,t??s(e),{name:"RateLimitError",status:o}),this.reason=e.reason,this.retryAfter=e.retryAfter}}export{c as default};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"@lunora/errors";import{availableAt as p,evaluate as N}from"./availableAt-DR8rnuHe.mjs";import d from"./RateLimitError-BfvwU1oc.mjs";import{createMemoryStore as b}from"./createDbStore-xtfjDj1h.mjs";const f=(o,t)=>t===void 0?encodeURIComponent(o):`${encodeURIComponent(o)}:${encodeURIComponent(t)}`,g=(o,t)=>{let e=0;for(let r=0;r<o.length;r+=1)e=e*31+o.charCodeAt(r)|0;return Math.abs(e)%t},y=(o,t)=>t>1?{...o,capacity:(o.capacity??o.rate)/t,rate:o.rate/t}:o,I=(o,t,e)=>{const r=f(o,t);return e>1?Array.from({length:e},(s,i)=>`${r}#${String(i)}`):[r]},w=(o,t,e)=>{const r=f(o,t);return e>1?`${r}#${String(g(r,e))}`:r};class R{config;denyList;normalize;now;store;constructor(t){this.config=t.config,this.denyList=new Set(t.denyList),this.normalize=t.normalize??(e=>e),this.now=t.now??Date.now,this.store=t.store??b();for(const[e,r]of Object.entries(this.config)){if(r.shards!==void 0&&(!Number.isInteger(r.shards)||r.shards<1))throw new n("INTERNAL",`rate limit "${e}": shards must be a positive integer`);if(!Number.isFinite(r.period)||r.period<=0)throw new n("INTERNAL",`rate limit "${e}": period must be a positive number`);if(!Number.isFinite(r.rate)||r.rate<=0)throw new n("INTERNAL",`rate limit "${e}": rate must be a positive number`);if(r.capacity!==void 0&&(!Number.isFinite(r.capacity)||r.capacity<0))throw new n("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),s=r.shards??1,i=this.now(),a=this.normalizeKey(e.key),h=w(t,a,s),c=p(y(r,s),await this.store.get(h),i);return{config:r,ts:c.ts,value:c.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){const r=this.resolve(t).shards??1,s=this.normalizeKey(e.key);await Promise.all(I(t,s,r).map(i=>Promise.resolve(this.store.delete(i))))}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new n("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,r){const s=this.resolve(t),i=this.normalizeKey(e.key);if(i!==void 0&&(this.denyList.has(i)||this.denyList.has(e.key))){const l={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new d(l);return l}const a=e.count??1;if(!Number.isInteger(a)||a<=0)throw new n("INTERNAL",`rate limit "${t}": count must be a positive integer`);const h=s.shards??1,c=w(t,i,h),v=await this.store.get(c),{status:m,value:u}=N(y(s,h),v,{consume:r,count:a,now:this.now(),reserve:e.reserve??!1});if(u!==void 0&&await this.store.set(c,u),!m.ok&&e.throws)throw new d(m);return m}}export{R as RateLimiter};
@@ -0,0 +1 @@
1
+ import{LunoraError as A}from"@lunora/errors";const d=e=>e.capacity??e.rate,o=(e,a)=>{throw new A("INTERNAL",`@lunora/ratelimit: requested count ${String(e)} exceeds the limiter capacity ${String(a)}`)},m=(e,a,t)=>{const u=d(e),r=e.rate/e.period,s=a??{ts:t,value:u},c=Math.max(0,t-s.ts);return{available:Math.min(u,s.value+c*r),capacity:u,ratePerMs:r}},k=(e,a,t)=>{const u=e.start??0,r=u+Math.floor((t-u)/e.period)*e.period;if(!a||a.ts<r){let s=0;return a&&(a.value<0||e.capacity!==void 0)&&(s=a.value),{ts:r,value:Math.min(d(e),s+e.rate)}}return{ts:a.ts,value:a.value}},h=(e,a,t)=>{const u=e.start??0,r=u+Math.floor((t-u)/e.period)*e.period,s=t-r,c=(e.period-s)/e.period;let n=0,l=0;return a?.ts===r?(n=a.prev??0,l=a.value):a?.ts===r-e.period&&(n=a.value),{currentCount:l,elapsed:s,previousCount:n,weight:c,windowStart:r}},b=(e,a,t)=>{const{available:u,capacity:r,ratePerMs:s}=m(e,a,t.now);if(u>=t.count){const l={ts:t.now,value:u-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?l:void 0}}const c=t.count-u,n=Math.ceil(c/s);return t.consume&&t.reserve&&t.count<=r?{status:{ok:!0,retryAfter:n},value:{ts:t.now,value:u-t.count}}:(t.count>r&&o(t.count,r),{status:{ok:!1,reason:"rate",retryAfter:n},value:void 0})},C=(e,a,t)=>{const u=d(e),r=k(e,a,t.now);if(r.value>=t.count){const c={ts:r.ts,value:r.value-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?c:void 0}}const s=r.ts+e.period-t.now;return t.consume&&t.reserve&&t.count<=u?{status:{ok:!0,retryAfter:s},value:{ts:r.ts,value:r.value-t.count}}:(t.count>u&&o(t.count,u),{status:{ok:!1,reason:"rate",retryAfter:s},value:void 0})},f=(e,a,t)=>{const u=e.rate,{period:r}=e,{currentCount:s,elapsed:c,previousCount:n,weight:l,windowStart:y}=h(e,a,t.now),w=n*l+s+t.count<=u,i=()=>{const v=u-s-t.count;if(n>0&&v>=0)return Math.ceil(r-c-v*r/n);const M=u-t.count,x=s>0?Math.max(0,r-M*r/s):0;return Math.ceil(r-c+x)};if(w||t.consume&&t.reserve&&t.count<=u){const v={prev:n,ts:y,value:s+t.count};return{status:{ok:!0,retryAfter:w?0:i()},value:t.consume?v:void 0}}return t.count>u&&o(t.count,u),{status:{ok:!1,reason:"rate",retryAfter:i()},value:void 0}},W=(e,a,t)=>{if(e.kind==="token bucket")return{ts:t,value:m(e,a,t).available};if(e.kind==="sliding window"){const{currentCount:u,previousCount:r,weight:s,windowStart:c}=h(e,a,t);return{ts:c,value:Math.max(0,e.rate-(r*s+u))}}return k(e,a,t)},j=(e,a,t)=>e.kind==="token bucket"?b(e,a,t):e.kind==="sliding window"?f(e,a,t):C(e,a,t);export{W as availableAt,j as evaluate};
@@ -0,0 +1 @@
1
+ const w=()=>{const t=new Map;return{delete:e=>{t.delete(e)},get:e=>t.get(e),set:(e,n)=>{t.set(e,n)}}},o=(t,e,...n)=>t.exec.call(t,e,...n).toArray(),b=t=>{const{sql:e}=t,n=t.table??"_lunora_rate_limits";return o(e,`CREATE TABLE IF NOT EXISTS "${n}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:s=>{o(e,`DELETE FROM "${n}" WHERE k = ?`,s)},get:s=>{const c=o(e,`SELECT value, ts, prev FROM "${n}" WHERE k = ?`,s)[0];if(!c)return;const i={ts:c.ts,value:c.value};return c.prev!==null&&(i.prev=c.prev),i},set:(s,l)=>{o(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)}}},E=t=>{const{db:e}=t,n=t.table??"rateLimits",s=t.index??"by_key",l=t.keyField??"key",c=new Map,i=async a=>{const r=await e.query(n).withIndex(s,d=>d.eq(l,a)).first();return c.set(a,r?r._id:void 0),r},v=async a=>(c.has(a)||await i(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 i(a);if(!r)return;const d={ts:r.ts,value:r.value};return r.prev!==null&&r.prev!==void 0&&(d.prev=r.prev),d},set:async(a,r)=>{const d=await v(a),u={[l]:a,ts:r.ts,value:r.value};r.prev!==void 0&&(u.prev=r.prev),d===void 0?c.set(a,await e.insert(n,u)):await e.patch(d,u)}}},p=t=>{const e=s=>{throw new Error(`@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=E({...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{E as createDbStore,w as createMemoryStore,p as createReadOnlyDbStore,b as createSqlStore};
@@ -0,0 +1 @@
1
+ import{rateLimit as o}from"./rateLimit-DXZ0uxTf.mjs";import{RateLimiter as m}from"./RateLimiter-BuMb1rK0.mjs";import{createDbStore as i}from"./createDbStore-xtfjDj1h.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};
@@ -0,0 +1 @@
1
+ import{isLunoraError as u,isInternalCode as l,LunoraError as c}from"@lunora/errors";const f={deny:{code:"FORBIDDEN",status:403},rate:{code:"TOO_MANY_REQUESTS",status:429}},y=(r,a,t)=>a==="deny"?`request denied for "${r}"`:t===void 0?`rate limit "${r}" exceeded`:`rate limit "${r}" exceeded; retry after ${String(t)}ms`,w=(r,a,t={})=>async({ctx:s,next:n})=>{let o;try{o=await(typeof r=="function"?await r(s):r).limit(a,{count:t.count,key:t.key?.(s)})}catch(e){if(u(e)&&l(e.code))throw e;if(console.error(`@lunora/ratelimit: rateLimit("${a}") threw; ${t.failOpen?"failing open":"failing closed"}`,e),t.failOpen)return n();throw new c("SERVICE_UNAVAILABLE",`rate limiter unavailable for "${a}"`,{cause:e,status:503})}if(!o.ok){const e=o.reason??"rate",d=f[e],i=Number.isFinite(o.retryAfter)?Math.ceil(o.retryAfter):void 0;throw new c(d.code,t.message??y(a,e,i),{status:d.status,data:i===void 0?void 0:{retryAfter:i}})}return n()};export{f as STATUS_BY_REASON,w as rateLimit};
@@ -0,0 +1 @@
1
+ const r=t=>({key:"ratelimit",middleware:async({ctx:e,next:i})=>{const n=typeof t=="function"?await t(e):t,a=e.api??{};return i({ctx:{api:{...a,ratelimit:n}}})}});export{r as ratelimitPlugin};
@@ -0,0 +1 @@
1
+ const n=(t,a)=>({check:async c=>t.check(a,{key:c}),record:async(c,r)=>{if(!Number.isFinite(r)||r<=0)return t.check(a,{key:c});const{config:e}=await t.getValue(a,{key:c}),h=Math.max(1,Math.floor((e.capacity??e.rate)/(e.shards??1)));return t.limit(a,{count:Math.min(Math.ceil(r),h),key:c,reserve:!0})}});export{n as tokenBudget};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/ratelimit",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.23",
4
4
  "description": "Rate limiting: token-bucket / fixed-window / sliding-window algorithms, deny list, sharding, pluggable stores, and procedure middleware",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.18"
49
+ "@lunora/errors": "1.0.0-alpha.22"
50
50
  },
51
51
  "engines": {
52
52
  "node": "^22.15.0 || >=24.11.0"
@@ -1 +0,0 @@
1
- import{LunoraError as i}from"@lunora/errors";import{STATUS_BY_REASON as o}from"./rateLimit-DLcwK-84.mjs";const n=e=>e.reason==="deny"?"request denied (deny list)":Number.isFinite(e.retryAfter)?`rate limit exceeded; retry after ${String(Math.ceil(e.retryAfter))}ms`:"rate limit exceeded";class c extends i{reason;retryAfter;constructor(r,t){const{code:s,status:a}=o[r.reason??"rate"];super(s,t??n(r),{name:"RateLimitError",status:a}),this.reason=r.reason,this.retryAfter=r.retryAfter}}export{c as default};
@@ -1 +0,0 @@
1
- import{LunoraError as n}from"@lunora/errors";import{availableAt as p,evaluate as N}from"./availableAt-BMlAt2U2.mjs";import w from"./RateLimitError-xJ2Iv-i7.mjs";import{createMemoryStore as g}from"./createDbStore-CwJBMxDQ.mjs";const y=(r,t)=>t===void 0?encodeURIComponent(r):`${encodeURIComponent(r)}:${encodeURIComponent(t)}`,b=(r,t)=>{let e=0;for(let i=0;i<r.length;i+=1)e=e*31+r.charCodeAt(i)|0;return Math.abs(e)%t},d=(r,t)=>t>1?{...r,capacity:(r.capacity??r.rate)/t,rate:r.rate/t}:r,I=(r,t,e)=>{const i=y(r,t);return e>1?Array.from({length:e},(o,s)=>`${i}#${String(s)}`):[i]},f=(r,t,e)=>{const i=y(r,t);return e>1?`${i}#${String(b(i,e))}`:i};class z{config;denyList;normalize;now;store;constructor(t){this.config=t.config,this.denyList=new Set(t.denyList),this.normalize=t.normalize??(e=>e),this.now=t.now??Date.now,this.store=t.store??g();for(const[e,i]of Object.entries(this.config)){if(i.shards!==void 0&&(!Number.isInteger(i.shards)||i.shards<1))throw new n("INTERNAL",`rate limit "${e}": shards must be a positive integer`);if(!Number.isFinite(i.period)||i.period<=0)throw new n("INTERNAL",`rate limit "${e}": period must be a positive number`);if(!Number.isFinite(i.rate)||i.rate<=0)throw new n("INTERNAL",`rate limit "${e}": rate must be a positive number`);if(i.capacity!==void 0&&(!Number.isFinite(i.capacity)||i.capacity<0))throw new n("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 i=this.resolve(t),o=i.shards??1,s=this.now(),a=this.normalizeKey(e.key),m=f(t,a,o),c=p(d(i,o),await this.store.get(m),s);return{config:i,ts:c.ts,value:c.value}}async limit(t,e={}){return this.run(t,e,!0)}async reset(t,e={}){const i=this.resolve(t).shards??1,o=this.normalizeKey(e.key);await Promise.all(I(t,o,i).map(s=>Promise.resolve(this.store.delete(s))))}normalizeKey(t){return t===void 0?void 0:this.normalize(t)}resolve(t){const e=this.config[t];if(!e)throw new n("INTERNAL",`rate limit "${t}" is not configured`);return e}async run(t,e,i){const o=this.resolve(t),s=this.normalizeKey(e.key);if(s!==void 0&&(this.denyList.has(s)||this.denyList.has(e.key))){const l={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(e.throws)throw new w(l);return l}const a=e.count??1;if(!Number.isInteger(a)||a<=0)throw new n("INTERNAL",`rate limit "${t}": count must be a positive integer`);const m=o.shards??1,c=f(t,s,m),v=await this.store.get(c),{status:h,value:u}=N(d(o,m),v,{consume:i,count:a,now:this.now(),reserve:e.reserve??!1});if(u!==void 0&&await this.store.set(c,u),!h.ok&&e.throws)throw new w(h);return h}}export{z as RateLimiter};
@@ -1 +0,0 @@
1
- import{LunoraError as M}from"@lunora/errors";const l=r=>r.capacity??r.rate,v=(r,e)=>{throw new M("INTERNAL",`@lunora/ratelimit: requested count ${String(r)} exceeds the limiter capacity ${String(e)}`)},f=(r,e,t)=>{const o=l(r),u=r.rate/r.period,n=e??{ts:t,value:o},a=Math.max(0,t-n.ts);return{available:Math.min(o,n.value+a*u),capacity:o,ratePerMs:u}},w=(r,e,t)=>{const o=r.start??0,u=o+Math.floor((t-o)/r.period)*r.period;if(!e||e.ts<u){let n=0;return e&&(e.value<0||r.capacity!==void 0)&&(n=e.value),{ts:u,value:Math.min(l(r),n+r.rate)}}return{ts:e.ts,value:e.value}},k=(r,e,t)=>{const o=r.start??0,u=o+Math.floor((t-o)/r.period)*r.period,n=t-u,a=(r.period-n)/r.period;let s=0,i=0;return e?.ts===u?(s=e.prev??0,i=e.value):e?.ts===u-r.period&&(s=e.value),{currentCount:i,elapsed:n,previousCount:s,weight:a,windowStart:u}},A=(r,e,t)=>{const{available:o,capacity:u,ratePerMs:n}=f(r,e,t.now);if(o>=t.count){const i={ts:t.now,value:o-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?i:void 0}}const a=t.count-o,s=Math.ceil(a/n);return t.consume&&t.reserve&&t.count<=u?{status:{ok:!0,retryAfter:s},value:{ts:t.now,value:o-t.count}}:(t.count>u&&v(t.count,u),{status:{ok:!1,reason:"rate",retryAfter:s},value:void 0})},b=(r,e,t)=>{const o=l(r),u=w(r,e,t.now);if(u.value>=t.count){const a={ts:u.ts,value:u.value-t.count};return{status:{ok:!0,retryAfter:0},value:t.consume?a:void 0}}const n=u.ts+r.period-t.now;return t.consume&&t.reserve&&t.count<=o?{status:{ok:!0,retryAfter:n},value:{ts:u.ts,value:u.value-t.count}}:(t.count>o&&v(t.count,o),{status:{ok:!1,reason:"rate",retryAfter:n},value:void 0})},g=(r,e,t)=>{const o=r.rate,{period:u}=r,{currentCount:n,elapsed:a,previousCount:s,weight:i,windowStart:h}=k(r,e,t.now),d=s*i+n+t.count<=o,p=()=>{const c=o-n-t.count;if(s>0&&c>=0)return Math.ceil(u-a-c*u/s);const m=o-t.count,y=n>0?Math.max(0,u-m*u/n):0;return Math.ceil(u-a+y)};if(d||t.consume&&t.reserve&&t.count<=o){const c={prev:s,ts:h,value:n+t.count};return{status:{ok:!0,retryAfter:d?0:p()},value:t.consume?c:void 0}}return t.count>o&&v(t.count,o),{status:{ok:!1,reason:"rate",retryAfter:p()},value:void 0}},x=(r,e,t)=>{if(r.kind==="token bucket")return{ts:t,value:f(r,e,t).available};if(r.kind==="sliding window"){const{currentCount:o,previousCount:u,weight:n,windowStart:a}=k(r,e,t);return{ts:a,value:Math.max(0,r.rate-(u*n+o))}}return w(r,e,t)},S=(r,e,t)=>r.kind==="token bucket"?A(r,e,t):r.kind==="sliding window"?g(r,e,t):b(r,e,t);export{x as availableAt,S as evaluate};
@@ -1 +0,0 @@
1
- const p=()=>{const t=new Map;return{delete:e=>{t.delete(e)},get:e=>t.get(e),set:(e,s)=>{t.set(e,s)}}},u=(t,e,...s)=>t.exec.call(t,e,...s).toArray(),y=t=>{const{sql:e}=t,s=t.table??"_lunora_rate_limits";return u(e,`CREATE TABLE IF NOT EXISTS "${s}" (k TEXT PRIMARY KEY, value REAL NOT NULL, ts INTEGER NOT NULL, prev REAL)`),{delete:n=>{u(e,`DELETE FROM "${s}" WHERE k = ?`,n)},get:n=>{const l=u(e,`SELECT value, ts, prev FROM "${s}" WHERE k = ?`,n)[0];if(!l)return;const c={ts:l.ts,value:l.value};return l.prev!==null&&(c.prev=l.prev),c},set:(n,l)=>{u(e,`INSERT INTO "${s}" (k, value, ts, prev) VALUES (?, ?, ?, ?) ON CONFLICT(k) DO UPDATE SET value = excluded.value, ts = excluded.ts, prev = excluded.prev`,n,l.value,l.ts,l.prev??null)}}},E=t=>{const{db:e}=t,s=t.table??"rateLimits",n=t.index??"by_key",l=t.keyField??"key",c=new Map,o=async a=>{const r=await e.query(s).withIndex(n,i=>i.eq(l,a)).first();return c.set(a,r?r._id:void 0),r},v=async a=>(c.has(a)||await o(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 o(a);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(a,r)=>{const i=await v(a),d={[l]:a,ts:r.ts,value:r.value};r.prev!==void 0&&(d.prev=r.prev),i===void 0?c.set(a,await e.insert(s,d)):await e.patch(i,d)}}},b=t=>{const e=n=>{throw new Error(`@lunora/ratelimit: \`${n}\` 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\`.`)},s=E({...t,db:{delete:()=>e("delete"),insert:()=>e("insert"),patch:()=>e("patch"),query:t.db.query.bind(t.db)}});return{delete:()=>e("reset"),get:s.get,set:()=>e("limit")}};export{E as createDbStore,p as createMemoryStore,b as createReadOnlyDbStore,y as createSqlStore};
@@ -1 +0,0 @@
1
- import{rateLimit as a}from"./rateLimit-DLcwK-84.mjs";import{RateLimiter as i}from"./RateLimiter-CitqoGTI.mjs";import{createDbStore as m}from"./createDbStore-CwJBMxDQ.mjs";const p=(t,o,r={})=>a(e=>new i({config:t,store:m({db:e.db,...r.store})}),o,r);export{p as default};
@@ -1 +0,0 @@
1
- import{isLunoraError as f,isInternalCode as l,LunoraError as c}from"@lunora/errors";const u={deny:{code:"FORBIDDEN",status:403},rate:{code:"TOO_MANY_REQUESTS",status:429}},m=(r,a,e)=>a==="deny"?`request denied for "${r}"`:e===void 0?`rate limit "${r}" exceeded`:`rate limit "${r}" exceeded; retry after ${String(e)}ms`,A=(r,a,e={})=>async({ctx:n,next:s})=>{let i;try{i=await(typeof r=="function"?await r(n):r).limit(a,{count:e.count,key:e.key?.(n)})}catch(t){if(f(t)&&l(t.code))throw t;if(console.error(`@lunora/ratelimit: rateLimit("${a}") threw; ${e.failOpen?"failing open":"failing closed"}`,t),e.failOpen)return s();throw new c("SERVICE_UNAVAILABLE",`rate limiter unavailable for "${a}"`,{cause:t,status:503})}if(!i.ok){const t=i.reason??"rate",d=u[t],o=Number.isFinite(i.retryAfter)?Math.ceil(i.retryAfter):void 0;throw new c(d.code,e.message??m(a,t,o),{status:d.status,data:o===void 0?void 0:{retryAfter:o}})}return s()};export{u as STATUS_BY_REASON,A as rateLimit};
@@ -1 +0,0 @@
1
- const r=t=>({key:"ratelimit",middleware:async({ctx:i,next:a})=>{const n=typeof t=="function"?await t(i):t,e=i.api??{};return a({ctx:{api:{...e,ratelimit:n}}})}});export{r as ratelimitPlugin};