@formata/limitr 0.5.13
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 +145 -0
- package/dist/gate.d.ts +22 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +79 -0
- package/dist/gate.js.map +1 -0
- package/dist/limitr.d.ts +2 -0
- package/dist/limitr.d.ts.map +1 -0
- package/dist/limitr.js +2 -0
- package/dist/limitr.js.map +1 -0
- package/dist/main.d.ts +296 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +746 -0
- package/dist/main.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Limitr: Open-Source Monetization Policy
|
|
2
|
+
**Limitr is an embedded, open-source policy engine for enforcing plans, limits, and usage in your application.**
|
|
3
|
+
|
|
4
|
+
It is designed for AI products, developer tools, and open-source software where monetization logic must be:
|
|
5
|
+
- inspectable
|
|
6
|
+
- portable
|
|
7
|
+
- deterministic
|
|
8
|
+
- and not hard-coded into application logic
|
|
9
|
+
|
|
10
|
+
> Limitr answers the question:
|
|
11
|
+
> **"What is this customer allowed to consume, right now, and what happens if it exceeds the limit?"**
|
|
12
|
+
|
|
13
|
+
- 👉 Jump to the [Quick Example](#example-seat-based-plan-enforcement-typescript)
|
|
14
|
+
- Additional [info and examples](https://docs.stof.dev/applications/limitr)
|
|
15
|
+
- [Limitr Cloud](https://limitr.dev)
|
|
16
|
+
|
|
17
|
+
## What Limitr Gives You
|
|
18
|
+
- Define plans, entitlements, and limits in a policy document — not application code
|
|
19
|
+
- Enforce limits locally and offline, embedded directly in your app
|
|
20
|
+
- Change limits without redeploying
|
|
21
|
+
- Stripe-agnostic and billing-system-agnostic
|
|
22
|
+
- Inspectable and auditable by developers and customers
|
|
23
|
+
- Event-driven enforcement (usage changes, overages, denials)
|
|
24
|
+
- Policy evolves independently from product code
|
|
25
|
+
- Built on [Stof](https://docs.stof.dev), an open-source data + logic runtime
|
|
26
|
+
|
|
27
|
+
## What Limitr Is Not
|
|
28
|
+
- Not a billing system
|
|
29
|
+
- Not a payment processor
|
|
30
|
+
- Not a hosted SaaS requirement
|
|
31
|
+
- Not a feature-flag system (outside of entitlements)
|
|
32
|
+
|
|
33
|
+
Limitr enforces **truth** about usage and limits.
|
|
34
|
+
Billing and payments can subscribe to Limitr’s events.
|
|
35
|
+
|
|
36
|
+
Limitr is designed to integrate cleanly with existing systems, not replace them.
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
Most applications implement monetization logic in files like `limits.ts`:
|
|
40
|
+
- seat limits
|
|
41
|
+
- usage caps
|
|
42
|
+
- plan checks
|
|
43
|
+
- special cases and overrides
|
|
44
|
+
|
|
45
|
+
Over time, this logic:
|
|
46
|
+
- becomes tightly coupled to the app
|
|
47
|
+
- requires redeploys to change pricing
|
|
48
|
+
- is hard to audit or explain
|
|
49
|
+
- breaks down with usage-based or AI pricing
|
|
50
|
+
|
|
51
|
+
Payments systems (like Stripe) handle money, but not **enforcement**.
|
|
52
|
+
|
|
53
|
+
Limitr separates **monetization policy** from application logic, so limits are:
|
|
54
|
+
- explicit
|
|
55
|
+
- portable
|
|
56
|
+
- testable
|
|
57
|
+
- and easy to evolve
|
|
58
|
+
|
|
59
|
+
## Who Limitr Is For
|
|
60
|
+
Limitr is a good fit if you are:
|
|
61
|
+
- Building an AI or usage-based product
|
|
62
|
+
- Implementing seat-based or credit-based pricing
|
|
63
|
+
- Shipping developer tools or infrastructure
|
|
64
|
+
- Supporting self-hosted or open-source deployments
|
|
65
|
+
- Tired of hardcoding pricing logic in application code
|
|
66
|
+
|
|
67
|
+
## Example: Seat-Based Plan Enforcement (TypeScript [JSR](https://jsr.io/@formata/limitr))
|
|
68
|
+
```typescript
|
|
69
|
+
import { Limitr } from 'jsr:@formata/limitr';
|
|
70
|
+
|
|
71
|
+
// Load a Limitr policy from a DB, string, file, API, etc.
|
|
72
|
+
// Stof is the default format, but can also be yaml, json, etc.
|
|
73
|
+
const policy = await Limitr.new(`
|
|
74
|
+
policy:
|
|
75
|
+
credits:
|
|
76
|
+
seat:
|
|
77
|
+
description: 'A single seat credit that can be tracked per customer.'
|
|
78
|
+
plans:
|
|
79
|
+
free:
|
|
80
|
+
entitlements:
|
|
81
|
+
seats:
|
|
82
|
+
description: 'Each customer (user, org, etc.) with this plan can have up to this many seats.'
|
|
83
|
+
limit:
|
|
84
|
+
credit: 'seat'
|
|
85
|
+
value: 1
|
|
86
|
+
increment: 1
|
|
87
|
+
paid:
|
|
88
|
+
entitlements:
|
|
89
|
+
seats:
|
|
90
|
+
description: 'Each customer (user, org, etc.) with this plan can have up to this many seats.'
|
|
91
|
+
limit:
|
|
92
|
+
credit: 'seat'
|
|
93
|
+
value: 3
|
|
94
|
+
increment: 1
|
|
95
|
+
`, 'yaml');
|
|
96
|
+
|
|
97
|
+
// Entitlements define features + limits (gated behaviors) on plans.
|
|
98
|
+
// Meters are state stored per customer per entitlement.
|
|
99
|
+
|
|
100
|
+
// Create/save/load customers (database, Stripe, etc.)
|
|
101
|
+
await policy.addCustomer('cus_free_customer', 'free');
|
|
102
|
+
await policy.addCustomer('cus_paid_customer', 'paid');
|
|
103
|
+
|
|
104
|
+
// Perform entitlement checks, meter usage, etc.
|
|
105
|
+
await policy.increment('cus_free_customer', 'seats'); // adds one seat
|
|
106
|
+
await policy.increment('cus_paid_customer', 'seats'); // adds one seat
|
|
107
|
+
|
|
108
|
+
// Add callbacks to the document directly or as a library function
|
|
109
|
+
policy.doc.lib('App', 'meter_limit', (json: string) => {
|
|
110
|
+
const record = JSON.parse(json);
|
|
111
|
+
if (record.customer.plan === 'free' && record.entitlement === 'seats') {
|
|
112
|
+
console.log('FREE PLAN SEAT LIMIT HIT, current: ', record.customer.meters.seats.value, ' requested: ', record.invalid_value);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Will return false and emit an meter-limit event in the doc (if App.meter_limit exists, it will also be called)
|
|
117
|
+
if (await policy.increment('cus_free_customer', 'seats')) throw Error("will not get here");
|
|
118
|
+
if (await policy.allow('cus_free_customer', 'seats', 2)) throw Error("cannot request 2 additional seats..");
|
|
119
|
+
if (await policy.increment('cus_paid_customer', 'seats')) {
|
|
120
|
+
// paid customer can add a second seat
|
|
121
|
+
} else {
|
|
122
|
+
throw Error("will not get here");
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
```bash
|
|
126
|
+
> deno run --allow-all typescript/examples/seats.ts
|
|
127
|
+
FREE PLAN SEAT LIMIT HIT, current: 1 requested: 2
|
|
128
|
+
FREE PLAN SEAT LIMIT HIT, current: 1 requested: 3
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### What's happening here?
|
|
132
|
+
- Plans define which entitlements a customer has
|
|
133
|
+
- Entitlements define how a meter is allowed to change with limits
|
|
134
|
+
- Meters are stored on each customer as state
|
|
135
|
+
- Limitr enforces limits and emits events when limits are hit
|
|
136
|
+
- The application decides how to respond (deny, warn, bill, notify)
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
Apache 2.0. See LICENSE for details.
|
|
140
|
+
|
|
141
|
+
## Contributing
|
|
142
|
+
- Open issues or discussions on [GitHub](https://github.com/dev-formata-io/limitr)
|
|
143
|
+
- Chat with us on [Discord](https://discord.gg/Up5kxdeXZt)
|
|
144
|
+
|
|
145
|
+
> Reach out to info@stof.dev to contact us directly
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internally ensure there's no overlap between policy calls.
|
|
3
|
+
* Supports both tail-append and head-prepend execution.
|
|
4
|
+
* WASM-safe promise gating.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LimitrGate {
|
|
7
|
+
private head;
|
|
8
|
+
private tail;
|
|
9
|
+
/**
|
|
10
|
+
* Run at the back of this scheduler (non-priority).
|
|
11
|
+
*/
|
|
12
|
+
run<T>(fn: () => Promise<T> | T): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Run at the front of this scheduler.
|
|
15
|
+
*/
|
|
16
|
+
runFront<T>(fn: () => Promise<T> | T): Promise<T>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Helper function for waiting for the WebSocket to open.
|
|
20
|
+
*/
|
|
21
|
+
export declare function waitOnOpen(ws: WebSocket): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../gate.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,qBAAa,UAAU;IAEnB,OAAO,CAAC,IAAI,CAAuC;IAGnD,OAAO,CAAC,IAAI,CAA+B;IAG3C;;OAEG;IACH,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAO5C;;OAEG;IACH,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAepD;AAGD;;GAEG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvD"}
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 Formata, Inc. All rights reserved.
|
|
3
|
+
//
|
|
4
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
// you may not use this file except in compliance with the License.
|
|
6
|
+
// You may obtain a copy of the License at
|
|
7
|
+
//
|
|
8
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
//
|
|
10
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
// See the License for the specific language governing permissions and
|
|
14
|
+
// limitations under the License.
|
|
15
|
+
//
|
|
16
|
+
/**
|
|
17
|
+
* Internally ensure there's no overlap between policy calls.
|
|
18
|
+
* Supports both tail-append and head-prepend execution.
|
|
19
|
+
* WASM-safe promise gating.
|
|
20
|
+
*/
|
|
21
|
+
export class LimitrGate {
|
|
22
|
+
constructor() {
|
|
23
|
+
// Earliest scheduled work
|
|
24
|
+
this.head = Promise.resolve();
|
|
25
|
+
// Latest scheduled work
|
|
26
|
+
this.tail = this.head;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run at the back of this scheduler (non-priority).
|
|
30
|
+
*/
|
|
31
|
+
run(fn) {
|
|
32
|
+
const next = this.tail.then(fn);
|
|
33
|
+
this.tail = next.catch(() => { });
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run at the front of this scheduler.
|
|
38
|
+
*/
|
|
39
|
+
runFront(fn) {
|
|
40
|
+
let release;
|
|
41
|
+
const blocker = new Promise(resolve => {
|
|
42
|
+
release = resolve;
|
|
43
|
+
});
|
|
44
|
+
const prevTail = this.tail;
|
|
45
|
+
this.tail = blocker;
|
|
46
|
+
const next = Promise.resolve()
|
|
47
|
+
.then(fn)
|
|
48
|
+
.finally(() => {
|
|
49
|
+
this.tail = prevTail;
|
|
50
|
+
release();
|
|
51
|
+
});
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Helper function for waiting for the WebSocket to open.
|
|
57
|
+
*/
|
|
58
|
+
export function waitOnOpen(ws) {
|
|
59
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const onOpen = () => {
|
|
64
|
+
cleanup();
|
|
65
|
+
resolve();
|
|
66
|
+
};
|
|
67
|
+
const onError = (err) => {
|
|
68
|
+
cleanup();
|
|
69
|
+
reject(err);
|
|
70
|
+
};
|
|
71
|
+
const cleanup = () => {
|
|
72
|
+
ws.removeEventListener("open", onOpen);
|
|
73
|
+
ws.removeEventListener("error", onError);
|
|
74
|
+
};
|
|
75
|
+
ws.addEventListener("open", onOpen);
|
|
76
|
+
ws.addEventListener("error", onError);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=gate.js.map
|
package/dist/gate.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../gate.ts"],"names":[],"mappings":"AAAA,EAAE;AACF,oDAAoD;AACpD,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,gDAAgD;AAChD,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AACjC,EAAE;AAGF;;;;GAIG;AACH,MAAM,OAAO,UAAU;IAAvB;QACI,0BAA0B;QAClB,SAAI,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;QAEnD,wBAAwB;QAChB,SAAI,GAAqB,IAAI,CAAC,IAAI,CAAC;IA+B/C,CAAC;IA5BG;;OAEG;IACH,GAAG,CAAI,EAAwB;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACjC,OAAO,IAAI,CAAC;IAChB,CAAC;IAGD;;OAEG;IACH,QAAQ,CAAI,EAAwB;QAChC,IAAI,OAAoB,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;YACxC,OAAO,GAAG,OAAO,CAAC;QACtB,CAAC,CAAC,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC;QACpB,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE;aACzB,IAAI,CAAC,EAAE,CAAC;aACR,OAAO,CAAC,GAAG,EAAE;YACV,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC;YACrB,OAAO,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;QACP,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ;AAGD;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,EAAa;IACpC,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QACnC,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,GAAG,EAAE;YAChB,OAAO,EAAE,CAAC;YACV,OAAO,EAAE,CAAC;QACd,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,CAAC,GAAU,EAAE,EAAE;YAC3B,OAAO,EAAE,CAAC;YACV,MAAM,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,EAAE;YACjB,EAAE,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YACvC,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC7C,CAAC,CAAC;QACF,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACpC,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/limitr.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limitr.d.ts","sourceRoot":"","sources":["../limitr.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,yBAAyppkB,CAAC"}
|