@lunora/ratelimit 1.0.0-alpha.22 → 1.0.0-alpha.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,37 +59,55 @@ to a procedure's `.use(...)` chain. The procedure builders (`mutation`, …) com
59
59
  from your app's generated `_generated/server` module, not from a package.
60
60
 
61
61
  ```ts
62
- import { RateLimiter, rateLimit } from "@lunora/ratelimit";
62
+ import { dbRateLimit } from "@lunora/ratelimit";
63
63
 
64
64
  import { mutation } from "./_generated/server";
65
65
 
66
- const limiter = new RateLimiter({
67
- config: {
68
- login: { kind: "fixed window", period: 60_000, rate: 5 },
69
- send: { kind: "token bucket", period: 1_000, rate: 10 },
70
- },
71
- });
72
-
66
+ const config = {
67
+ login: { kind: "fixed window", period: 60_000, rate: 5 },
68
+ send: { kind: "token bucket", period: 1_000, rate: 10 },
69
+ };
70
+
71
+ // `dbRateLimit` builds the limiter per call, backed by a Lunora table via
72
+ // `ctx.db`, so the count is durable. A `RateLimiter` built with no explicit
73
+ // `store` falls back to `createMemoryStore` (a per-instance in-memory Map) —
74
+ // fine for a limit that only ever needs to hold within one Durable Object
75
+ // instance, but a limiter like `login` needs a durable store or it stops
76
+ // enforcing the configured rate the moment the DO instance is sharded,
77
+ // replicated, or recreated.
73
78
  // As procedure middleware — throws a structural LunoraError (429/403) on rejection.
74
- export const send = mutation.use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId })).mutation(async ({ ctx }) => {
79
+ export const send = mutation.use(dbRateLimit(config, "send", { key: (ctx) => ctx.auth.userId })).mutation(async ({ ctx }) => {
75
80
  // …
76
81
  });
77
82
  ```
78
83
 
79
- Or call the limiter directly:
84
+ Or call the limiter directly, inside a mutation/action so `ctx.db` is available:
80
85
 
81
86
  ```ts
82
- const status = await limiter.limit("send", { key: userId });
87
+ import { RateLimiter, RateLimitError, createDbStore } from "@lunora/ratelimit";
88
+
89
+ export const login = mutation.mutation(async ({ ctx, args }) => {
90
+ const limiter = new RateLimiter({ config, store: createDbStore({ db: ctx.db }) });
91
+ const status = await limiter.limit("login", { key: args.email });
83
92
 
84
- if (!status.ok) {
85
- // status.retryAfter is milliseconds until the request would succeed.
86
- // status.reason is "rate" or "deny".
87
- }
93
+ if (!status.ok) {
94
+ // Stop here `limit` returns a failing status by default rather than
95
+ // throwing, so falling through authenticates the rejected attempt.
96
+ // status.retryAfter is milliseconds until the request would succeed.
97
+ // status.reason is "rate" or "deny".
98
+ throw new RateLimitError(status);
99
+ }
100
+
101
+ await authenticate(args);
102
+ });
88
103
  ```
89
104
 
90
105
  For durable per-DO state inside a procedure, back the limiter with a Lunora
91
106
  table via `dbRateLimit(config, name, options)`, or supply a `store`
92
- (`createMemoryStore` / `createSqlStore` / `createDbStore`).
107
+ (`createSqlStore` / `createDbStore`). `createMemoryStore` stays the default —
108
+ correct inside a single Durable Object — but constructing a `RateLimiter` with
109
+ no explicit `store` now warns once so that choice is visible instead of
110
+ silent.
93
111
 
94
112
  > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs)**.
95
113
 
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 u}from"./packem_shared/RateLimiter-CitqoGTI.mjs";import{createDbStore as b,createMemoryStore as n,createReadOnlyDbStore as L,createSqlStore as s}from"./packem_shared/createDbStore-CwJBMxDQ.mjs";import{tokenBudget as v}from"./packem_shared/tokenBudget-BRZSEqNB.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-DR8rnuHe.mjs";import{default as i}from"./packem_shared/dbRateLimit--9YF09nZ.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-0mJeyeQu.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 v,evaluate as p}from"./availableAt-DR8rnuHe.mjs";import u from"./RateLimitError-BfvwU1oc.mjs";import{createMemoryStore as N}from"./createDbStore-xtfjDj1h.mjs";const f=(o,e)=>e===void 0?encodeURIComponent(o):`${encodeURIComponent(o)}:${encodeURIComponent(e)}`,g=(o,e)=>{let t=0;for(let r=0;r<o.length;r+=1)t=t*31+o.charCodeAt(r)|0;return Math.abs(t)%e},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.",y=(o,e)=>e>1?{...o,capacity:(o.capacity??o.rate)/e,rate:o.rate/e}:o,L=(o,e,t)=>{const r=f(o,e);return t>1?Array.from({length:t},(i,s)=>`${r}#${String(s)}`):[r]},w=(o,e,t)=>{const r=f(o,e);return t>1?`${r}#${String(g(r,t))}`:r};class z{config;denyList;normalize;now;store;constructor(e){this.config=e.config,this.denyList=new Set(e.denyList),this.normalize=e.normalize??(t=>t),this.now=e.now??Date.now,e.store===void 0&&console.warn(I),this.store=e.store??N();for(const[t,r]of Object.entries(this.config)){if(r.shards!==void 0&&(!Number.isInteger(r.shards)||r.shards<1))throw new n("INTERNAL",`rate limit "${t}": shards must be a positive integer`);if(!Number.isFinite(r.period)||r.period<=0)throw new n("INTERNAL",`rate limit "${t}": period must be a positive number`);if(!Number.isFinite(r.rate)||r.rate<=0)throw new n("INTERNAL",`rate limit "${t}": rate must be a positive number`);if(r.capacity!==void 0&&(!Number.isFinite(r.capacity)||r.capacity<0))throw new n("INTERNAL",`rate limit "${t}": capacity must be a non-negative number`)}}async check(e,t={}){return this.run(e,t,!1)}async getValue(e,t={}){const r=this.resolve(e),i=r.shards??1,s=this.now(),a=this.normalizeKey(t.key),h=w(e,a,i),c=v(y(r,i),await this.store.get(h),s);return{config:r,ts:c.ts,value:c.value}}async limit(e,t={}){return this.run(e,t,!0)}async reset(e,t={}){const r=this.resolve(e).shards??1,i=this.normalizeKey(t.key);await Promise.all(L(e,i,r).map(s=>Promise.resolve(this.store.delete(s))))}normalizeKey(e){return e===void 0?void 0:this.normalize(e)}resolve(e){const t=this.config[e];if(!t)throw new n("INTERNAL",`rate limit "${e}" is not configured`);return t}async run(e,t,r){const i=this.resolve(e),s=this.normalizeKey(t.key);if(s!==void 0&&(this.denyList.has(s)||this.denyList.has(t.key))){const d={ok:!1,reason:"deny",retryAfter:Number.POSITIVE_INFINITY};if(t.throws)throw new u(d);return d}const a=t.count??1;if(!Number.isInteger(a)||a<=0)throw new n("INTERNAL",`rate limit "${e}": count must be a positive integer`);const h=i.shards??1,c=w(e,s,h),b=await this.store.get(c),{status:l,value:m}=p(y(i,h),b,{consume:r,count:a,now:this.now(),reserve:t.reserve??!1});if(m!==void 0&&await this.store.set(c,m),!l.ok&&t.throws)throw new u(l);return l}}export{z 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-0mJeyeQu.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.22",
3
+ "version": "1.0.0-alpha.24",
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.21"
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};
@@ -1 +0,0 @@
1
- const r=(c,t)=>({check:async e=>c.check(t,{key:e}),record:async(e,a)=>{if(!Number.isFinite(a)||a<=0)return c.check(t,{key:e});const{config:i}=await c.getValue(t,{key:e}),n=Math.max(1,Math.floor((i.capacity??i.rate)/(i.shards??1)));return c.limit(t,{count:Math.min(Math.ceil(a),n),key:e,reserve:!0})}});export{r as tokenBudget};