@revstackhq/core 0.0.0-dev-20260215075706
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +109 -0
- package/dist/define.d.ts +88 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +97 -0
- package/dist/engine.d.ts +65 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +163 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +14 -0
- package/dist/types.d.ts +220 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/validator.d.ts +45 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +134 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Revstack Inc.
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
100
|
+
|
|
101
|
+
The above copyright notice and this permission notice shall be included in all
|
|
102
|
+
copies or substantial portions of the Software.
|
|
103
|
+
|
|
104
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
105
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
106
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
107
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
108
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
109
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
110
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @revstackhq/core
|
|
2
|
+
|
|
3
|
+
The shared type system and config authoring toolkit for [Revstack](https://revstack.dev) — Billing as Code for SaaS.
|
|
4
|
+
|
|
5
|
+
This package provides the type-safe helper functions (`defineConfig`, `defineFeature`, `definePlan`, etc.) used to write `revstack.config.ts`, plus the runtime validation engine that powers the CLI and server-side config processing.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @revstackhq/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Writing a Billing Config
|
|
16
|
+
|
|
17
|
+
Use the identity helpers to get full autocompletion and compile-time validation in your `revstack.config.ts`:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { defineConfig, defineFeature, definePlan } from "@revstackhq/core";
|
|
21
|
+
|
|
22
|
+
const features = {
|
|
23
|
+
seats: defineFeature({
|
|
24
|
+
name: "Seats",
|
|
25
|
+
type: "static",
|
|
26
|
+
unit_type: "count",
|
|
27
|
+
}),
|
|
28
|
+
ai_tokens: defineFeature({
|
|
29
|
+
name: "AI Tokens",
|
|
30
|
+
type: "metered",
|
|
31
|
+
unit_type: "count",
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
features,
|
|
37
|
+
plans: {
|
|
38
|
+
default: definePlan<typeof features>({
|
|
39
|
+
name: "Default",
|
|
40
|
+
is_default: true,
|
|
41
|
+
is_public: false,
|
|
42
|
+
type: "free",
|
|
43
|
+
features: {},
|
|
44
|
+
}),
|
|
45
|
+
pro: definePlan<typeof features>({
|
|
46
|
+
name: "Pro",
|
|
47
|
+
is_default: false,
|
|
48
|
+
is_public: true,
|
|
49
|
+
type: "paid",
|
|
50
|
+
prices: [
|
|
51
|
+
{
|
|
52
|
+
amount: 2900,
|
|
53
|
+
currency: "USD",
|
|
54
|
+
billing_interval: "monthly",
|
|
55
|
+
trial_period_days: 14,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
features: {
|
|
59
|
+
seats: { value_limit: 5, is_hard_limit: true },
|
|
60
|
+
ai_tokens: { value_limit: 1000, reset_period: "monthly" },
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Type-Safe Feature Keys
|
|
68
|
+
|
|
69
|
+
When you pass `typeof features` as a generic to `definePlan<typeof features>(...)`, the `features` object is restricted to only the keys defined in your feature dictionary. Typos become compile-time errors:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// ✅ Compiles — "seats" exists in features
|
|
73
|
+
definePlan<typeof features>({ ..., features: { seats: { value_limit: 5 } } });
|
|
74
|
+
|
|
75
|
+
// ❌ Compile error — "seets" is not a valid key
|
|
76
|
+
definePlan<typeof features>({ ..., features: { seets: { value_limit: 5 } } });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API Reference
|
|
80
|
+
|
|
81
|
+
### Config Helpers
|
|
82
|
+
|
|
83
|
+
| Function | Description |
|
|
84
|
+
| ------------------ | ------------------------------------------------------------- |
|
|
85
|
+
| `defineConfig()` | Wraps the root `revstack.config.ts` export for type inference |
|
|
86
|
+
| `defineFeature()` | Define a feature (static, metered, or boolean) |
|
|
87
|
+
| `definePlan()` | Define a plan with optional compile-time feature key checks |
|
|
88
|
+
| `defineAddon()` | Define an add-on (same generic pattern as `definePlan`) |
|
|
89
|
+
| `defineDiscount()` | Define a discount/coupon |
|
|
90
|
+
|
|
91
|
+
### Validation Engine
|
|
92
|
+
|
|
93
|
+
The `validator` module provides runtime validation for billing configs, ensuring structural correctness before they're pushed to Revstack Cloud.
|
|
94
|
+
|
|
95
|
+
### Types
|
|
96
|
+
|
|
97
|
+
All shared TypeScript types are exported from the package root — `FeatureDefInput`, `PlanDefInput`, `PlanFeatureValue`, `RevstackConfig`, and more.
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
`@revstackhq/core` is a **zero-dependency** package designed to be shared across:
|
|
102
|
+
|
|
103
|
+
- **`@revstackhq/cli`** — Uses the define helpers and validator at config-loading time.
|
|
104
|
+
- **`@revstackhq/node`** — Depends on the shared type system for API contracts.
|
|
105
|
+
- **User projects** — Imported directly in `revstack.config.ts` for type-safe authoring.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
FSL-1.1-MIT
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file define.ts
|
|
3
|
+
* @description Identity helpers for "Billing as Code" config authoring.
|
|
4
|
+
*
|
|
5
|
+
* These functions return their input unchanged — their sole purpose is to
|
|
6
|
+
* provide autocompletion, type narrowing, and compile-time validation
|
|
7
|
+
* when developers write their `revstack.config.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { defineConfig, defineFeature, definePlan } from "@revstackhq/core";
|
|
12
|
+
*
|
|
13
|
+
* const features = {
|
|
14
|
+
* seats: defineFeature({ name: "Seats", type: "static", unit_type: "count" }),
|
|
15
|
+
* sso: defineFeature({ name: "SSO", type: "boolean", unit_type: "count" }),
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* export default defineConfig({
|
|
19
|
+
* features,
|
|
20
|
+
* plans: {
|
|
21
|
+
* default: definePlan<typeof features>({
|
|
22
|
+
* name: "Default",
|
|
23
|
+
* is_default: true,
|
|
24
|
+
* is_public: false,
|
|
25
|
+
* type: "free",
|
|
26
|
+
* features: {},
|
|
27
|
+
* }),
|
|
28
|
+
* pro: definePlan<typeof features>({
|
|
29
|
+
* name: "Pro",
|
|
30
|
+
* is_default: false,
|
|
31
|
+
* is_public: true,
|
|
32
|
+
* type: "paid",
|
|
33
|
+
* prices: [{ amount: 2900, currency: "USD", billing_interval: "monthly" }],
|
|
34
|
+
* features: {
|
|
35
|
+
* seats: { value_limit: 5, is_hard_limit: true },
|
|
36
|
+
* sso: { value_bool: true },
|
|
37
|
+
* },
|
|
38
|
+
* }),
|
|
39
|
+
* },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import type { FeatureDefInput, PlanDefInput, PlanFeatureValue, AddonDefInput, DiscountDef, RevstackConfig } from "@/types";
|
|
44
|
+
/**
|
|
45
|
+
* Define a feature with full type inference.
|
|
46
|
+
* Identity function — returns the input as-is.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineFeature<T extends FeatureDefInput>(config: T): T;
|
|
49
|
+
/**
|
|
50
|
+
* Define a Plan with optional compile-time feature key restriction.
|
|
51
|
+
*
|
|
52
|
+
* When called with a generic `F` (your feature dictionary type),
|
|
53
|
+
* the `features` object only accepts keys that exist in `F`.
|
|
54
|
+
*
|
|
55
|
+
* @typeParam F - Feature dictionary type. Pass `typeof yourFeatures` for strict keys.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* // Strict mode — typos cause compile errors:
|
|
60
|
+
* definePlan<typeof features>({ ..., features: { seats: { value_limit: 5 } } });
|
|
61
|
+
*
|
|
62
|
+
* // Loose mode — any string key accepted (backwards compatible):
|
|
63
|
+
* definePlan({ ..., features: { anything: { value_bool: true } } });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function definePlan<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<PlanDefInput, "features"> & {
|
|
67
|
+
features: F extends Record<string, FeatureDefInput> ? Partial<Record<keyof F, PlanFeatureValue>> : Record<string, PlanFeatureValue>;
|
|
68
|
+
}): PlanDefInput;
|
|
69
|
+
/**
|
|
70
|
+
* Define an Add-on with optional compile-time feature key restriction.
|
|
71
|
+
* Same generic pattern as `definePlan`.
|
|
72
|
+
*
|
|
73
|
+
* @typeParam F - Feature dictionary type for key restriction.
|
|
74
|
+
*/
|
|
75
|
+
export declare function defineAddon<F extends Record<string, FeatureDefInput> = Record<string, FeatureDefInput>>(config: Omit<AddonDefInput, "features"> & {
|
|
76
|
+
features: F extends Record<string, FeatureDefInput> ? Partial<Record<keyof F, PlanFeatureValue>> : Record<string, PlanFeatureValue>;
|
|
77
|
+
}): AddonDefInput;
|
|
78
|
+
/**
|
|
79
|
+
* Define a discount/coupon with full type inference.
|
|
80
|
+
* Identity function — returns the input as-is.
|
|
81
|
+
*/
|
|
82
|
+
export declare function defineDiscount<T extends DiscountDef>(config: T): T;
|
|
83
|
+
/**
|
|
84
|
+
* Define the root billing configuration.
|
|
85
|
+
* Wraps the entire `revstack.config.ts` export for type inference.
|
|
86
|
+
*/
|
|
87
|
+
export declare function defineConfig<T extends RevstackConfig>(config: T): T;
|
|
88
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,cAAc,EACf,MAAM,SAAS,CAAC;AAIjB;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAErE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CACxB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAE3E,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG;IACvC,QAAQ,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAC/C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,CAAC,GAC1C,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACtC,GACA,YAAY,CAEd;AAID;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAE3E,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC,GAAG;IACxC,QAAQ,EAAE,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAC/C,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC,CAAC,GAC1C,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACtC,GACA,aAAa,CAEf;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAElE;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,cAAc,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAEnE"}
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file define.ts
|
|
3
|
+
* @description Identity helpers for "Billing as Code" config authoring.
|
|
4
|
+
*
|
|
5
|
+
* These functions return their input unchanged — their sole purpose is to
|
|
6
|
+
* provide autocompletion, type narrowing, and compile-time validation
|
|
7
|
+
* when developers write their `revstack.config.ts`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { defineConfig, defineFeature, definePlan } from "@revstackhq/core";
|
|
12
|
+
*
|
|
13
|
+
* const features = {
|
|
14
|
+
* seats: defineFeature({ name: "Seats", type: "static", unit_type: "count" }),
|
|
15
|
+
* sso: defineFeature({ name: "SSO", type: "boolean", unit_type: "count" }),
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* export default defineConfig({
|
|
19
|
+
* features,
|
|
20
|
+
* plans: {
|
|
21
|
+
* default: definePlan<typeof features>({
|
|
22
|
+
* name: "Default",
|
|
23
|
+
* is_default: true,
|
|
24
|
+
* is_public: false,
|
|
25
|
+
* type: "free",
|
|
26
|
+
* features: {},
|
|
27
|
+
* }),
|
|
28
|
+
* pro: definePlan<typeof features>({
|
|
29
|
+
* name: "Pro",
|
|
30
|
+
* is_default: false,
|
|
31
|
+
* is_public: true,
|
|
32
|
+
* type: "paid",
|
|
33
|
+
* prices: [{ amount: 2900, currency: "USD", billing_interval: "monthly" }],
|
|
34
|
+
* features: {
|
|
35
|
+
* seats: { value_limit: 5, is_hard_limit: true },
|
|
36
|
+
* sso: { value_bool: true },
|
|
37
|
+
* },
|
|
38
|
+
* }),
|
|
39
|
+
* },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
// ─── Feature ─────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Define a feature with full type inference.
|
|
46
|
+
* Identity function — returns the input as-is.
|
|
47
|
+
*/
|
|
48
|
+
export function defineFeature(config) {
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
// ─── Plan (Typed against feature dictionary) ─────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Define a Plan with optional compile-time feature key restriction.
|
|
54
|
+
*
|
|
55
|
+
* When called with a generic `F` (your feature dictionary type),
|
|
56
|
+
* the `features` object only accepts keys that exist in `F`.
|
|
57
|
+
*
|
|
58
|
+
* @typeParam F - Feature dictionary type. Pass `typeof yourFeatures` for strict keys.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Strict mode — typos cause compile errors:
|
|
63
|
+
* definePlan<typeof features>({ ..., features: { seats: { value_limit: 5 } } });
|
|
64
|
+
*
|
|
65
|
+
* // Loose mode — any string key accepted (backwards compatible):
|
|
66
|
+
* definePlan({ ..., features: { anything: { value_bool: true } } });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function definePlan(config) {
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
// ─── Add-on (Typed against feature dictionary) ───────────────
|
|
73
|
+
/**
|
|
74
|
+
* Define an Add-on with optional compile-time feature key restriction.
|
|
75
|
+
* Same generic pattern as `definePlan`.
|
|
76
|
+
*
|
|
77
|
+
* @typeParam F - Feature dictionary type for key restriction.
|
|
78
|
+
*/
|
|
79
|
+
export function defineAddon(config) {
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
// ─── Discount ────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Define a discount/coupon with full type inference.
|
|
85
|
+
* Identity function — returns the input as-is.
|
|
86
|
+
*/
|
|
87
|
+
export function defineDiscount(config) {
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
// ─── Config Root ─────────────────────────────────────────────
|
|
91
|
+
/**
|
|
92
|
+
* Define the root billing configuration.
|
|
93
|
+
* Wraps the entire `revstack.config.ts` export for type inference.
|
|
94
|
+
*/
|
|
95
|
+
export function defineConfig(config) {
|
|
96
|
+
return config;
|
|
97
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file engine.ts
|
|
3
|
+
* @description The Entitlement Engine — the logic core of Revstack.
|
|
4
|
+
*
|
|
5
|
+
* Determines if a user can access a feature based on their active Plan,
|
|
6
|
+
* purchased Add-ons, and subscription payment status. All decisions are
|
|
7
|
+
* pure, stateless computations with no side effects.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { EntitlementEngine } from "@revstackhq/core";
|
|
12
|
+
*
|
|
13
|
+
* const engine = new EntitlementEngine(plan, addons, "active");
|
|
14
|
+
*
|
|
15
|
+
* // Single check
|
|
16
|
+
* const result = engine.check("seats", 4);
|
|
17
|
+
*
|
|
18
|
+
* // Batch check
|
|
19
|
+
* const results = engine.checkBatch({ seats: 4, ai_tokens: 12000 });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
import type { CheckResult, PlanDef, AddonDef, SubscriptionStatus } from "@/types";
|
|
23
|
+
/**
|
|
24
|
+
* The Entitlement Engine — evaluates feature access for a single customer.
|
|
25
|
+
*
|
|
26
|
+
* Instantiate with the customer's active plan, purchased add-ons, and
|
|
27
|
+
* current subscription status. Then call `check()` or `checkBatch()`
|
|
28
|
+
* to evaluate access.
|
|
29
|
+
*
|
|
30
|
+
* **Design decisions:**
|
|
31
|
+
* - Stateless: no mutation, no side effects. Safe to call from any context.
|
|
32
|
+
* - Add-on limits are *summed* with the base plan (e.g., plan gives 5 seats +
|
|
33
|
+
* addon gives 3 = 8 total).
|
|
34
|
+
* - If ANY source sets `is_hard_limit: false`, the entire feature becomes soft-limited.
|
|
35
|
+
*/
|
|
36
|
+
export declare class EntitlementEngine {
|
|
37
|
+
private plan;
|
|
38
|
+
private addons;
|
|
39
|
+
private subscriptionStatus;
|
|
40
|
+
/**
|
|
41
|
+
* @param plan - The customer's active base plan.
|
|
42
|
+
* @param addons - Active add-ons the customer has purchased.
|
|
43
|
+
* @param subscriptionStatus - Current payment/lifecycle state of the subscription.
|
|
44
|
+
*/
|
|
45
|
+
constructor(plan: PlanDef, addons?: AddonDef[], subscriptionStatus?: SubscriptionStatus);
|
|
46
|
+
/**
|
|
47
|
+
* Verify if the customer has access to a specific feature.
|
|
48
|
+
*
|
|
49
|
+
* Aggregates limits from the base plan AND any active add-ons,
|
|
50
|
+
* then evaluates the customer's current usage against those limits.
|
|
51
|
+
*
|
|
52
|
+
* @param featureId - The feature slug to check (e.g., `"seats"`).
|
|
53
|
+
* @param currentUsage - Current consumption count (default: 0).
|
|
54
|
+
* @returns A `CheckResult` with the access decision and metadata.
|
|
55
|
+
*/
|
|
56
|
+
check(featureId: string, currentUsage?: number): CheckResult;
|
|
57
|
+
/**
|
|
58
|
+
* Evaluate multiple features in a single pass.
|
|
59
|
+
*
|
|
60
|
+
* @param usages - Map of `featureSlug → currentUsage` to evaluate.
|
|
61
|
+
* @returns Map of `featureSlug → CheckResult`.
|
|
62
|
+
*/
|
|
63
|
+
checkBatch(usages: Record<string, number>): Record<string, CheckResult>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,OAAO,EAEP,QAAQ,EACR,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAuBjB;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAiB;IAO1B,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,kBAAkB;IAR5B;;;;OAIG;gBAEO,IAAI,EAAE,OAAO,EACb,MAAM,GAAE,QAAQ,EAAO,EACvB,kBAAkB,GAAE,kBAA6B;IAG3D;;;;;;;;;OASG;IACI,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,GAAE,MAAU,GAAG,WAAW;IAsFtE;;;;;OAKG;IACI,UAAU,CACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC;CAS/B"}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file engine.ts
|
|
3
|
+
* @description The Entitlement Engine — the logic core of Revstack.
|
|
4
|
+
*
|
|
5
|
+
* Determines if a user can access a feature based on their active Plan,
|
|
6
|
+
* purchased Add-ons, and subscription payment status. All decisions are
|
|
7
|
+
* pure, stateless computations with no side effects.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { EntitlementEngine } from "@revstackhq/core";
|
|
12
|
+
*
|
|
13
|
+
* const engine = new EntitlementEngine(plan, addons, "active");
|
|
14
|
+
*
|
|
15
|
+
* // Single check
|
|
16
|
+
* const result = engine.check("seats", 4);
|
|
17
|
+
*
|
|
18
|
+
* // Batch check
|
|
19
|
+
* const results = engine.checkBatch({ seats: 4, ai_tokens: 12000 });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Subscription statuses that block all feature access.
|
|
25
|
+
* Customers in these states must resolve their billing before
|
|
26
|
+
* the engine grants any entitlements.
|
|
27
|
+
*/
|
|
28
|
+
const BLOCKED_STATUSES = new Set([
|
|
29
|
+
"past_due",
|
|
30
|
+
"canceled",
|
|
31
|
+
]);
|
|
32
|
+
/** Immutable result returned for all checks when the subscription is blocked. */
|
|
33
|
+
const BLOCKED_RESULT = Object.freeze({
|
|
34
|
+
allowed: false,
|
|
35
|
+
reason: "past_due",
|
|
36
|
+
remaining: 0,
|
|
37
|
+
});
|
|
38
|
+
// ─── Engine ──────────────────────────────────────────────────
|
|
39
|
+
/**
|
|
40
|
+
* The Entitlement Engine — evaluates feature access for a single customer.
|
|
41
|
+
*
|
|
42
|
+
* Instantiate with the customer's active plan, purchased add-ons, and
|
|
43
|
+
* current subscription status. Then call `check()` or `checkBatch()`
|
|
44
|
+
* to evaluate access.
|
|
45
|
+
*
|
|
46
|
+
* **Design decisions:**
|
|
47
|
+
* - Stateless: no mutation, no side effects. Safe to call from any context.
|
|
48
|
+
* - Add-on limits are *summed* with the base plan (e.g., plan gives 5 seats +
|
|
49
|
+
* addon gives 3 = 8 total).
|
|
50
|
+
* - If ANY source sets `is_hard_limit: false`, the entire feature becomes soft-limited.
|
|
51
|
+
*/
|
|
52
|
+
export class EntitlementEngine {
|
|
53
|
+
plan;
|
|
54
|
+
addons;
|
|
55
|
+
subscriptionStatus;
|
|
56
|
+
/**
|
|
57
|
+
* @param plan - The customer's active base plan.
|
|
58
|
+
* @param addons - Active add-ons the customer has purchased.
|
|
59
|
+
* @param subscriptionStatus - Current payment/lifecycle state of the subscription.
|
|
60
|
+
*/
|
|
61
|
+
constructor(plan, addons = [], subscriptionStatus = "active") {
|
|
62
|
+
this.plan = plan;
|
|
63
|
+
this.addons = addons;
|
|
64
|
+
this.subscriptionStatus = subscriptionStatus;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Verify if the customer has access to a specific feature.
|
|
68
|
+
*
|
|
69
|
+
* Aggregates limits from the base plan AND any active add-ons,
|
|
70
|
+
* then evaluates the customer's current usage against those limits.
|
|
71
|
+
*
|
|
72
|
+
* @param featureId - The feature slug to check (e.g., `"seats"`).
|
|
73
|
+
* @param currentUsage - Current consumption count (default: 0).
|
|
74
|
+
* @returns A `CheckResult` with the access decision and metadata.
|
|
75
|
+
*/
|
|
76
|
+
check(featureId, currentUsage = 0) {
|
|
77
|
+
// ── Gate: subscription status ────────────────────────────
|
|
78
|
+
if (BLOCKED_STATUSES.has(this.subscriptionStatus)) {
|
|
79
|
+
return BLOCKED_RESULT;
|
|
80
|
+
}
|
|
81
|
+
// ── 1. Gather entitlements from all sources ──────────────
|
|
82
|
+
const planEntitlement = this.plan.features[featureId];
|
|
83
|
+
const addonEntitlements = this.addons
|
|
84
|
+
.map((addon) => ({ slug: addon.slug, value: addon.features[featureId] }))
|
|
85
|
+
.filter((item) => item.value !== undefined);
|
|
86
|
+
// If neither plan nor add-ons have this feature
|
|
87
|
+
if (planEntitlement === undefined && addonEntitlements.length === 0) {
|
|
88
|
+
return { allowed: false, reason: "feature_missing" };
|
|
89
|
+
}
|
|
90
|
+
// ── 2. Aggregate values across all sources ───────────────
|
|
91
|
+
let totalLimit = 0;
|
|
92
|
+
let isInfinite = false;
|
|
93
|
+
let hasAccess = false;
|
|
94
|
+
let hardLimit = true;
|
|
95
|
+
let granted_by = this.plan.slug;
|
|
96
|
+
const processValue = (val, sourceSlug) => {
|
|
97
|
+
// Boolean feature
|
|
98
|
+
if (val.value_bool === true) {
|
|
99
|
+
hasAccess = true;
|
|
100
|
+
isInfinite = true;
|
|
101
|
+
granted_by = sourceSlug;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Numeric limit feature (static or metered)
|
|
105
|
+
if (val.value_limit !== undefined) {
|
|
106
|
+
hasAccess = true;
|
|
107
|
+
totalLimit += val.value_limit;
|
|
108
|
+
granted_by = sourceSlug;
|
|
109
|
+
}
|
|
110
|
+
// Hard/soft limit flag
|
|
111
|
+
if (val.is_hard_limit === false) {
|
|
112
|
+
hardLimit = false;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// Process base plan first
|
|
116
|
+
if (planEntitlement)
|
|
117
|
+
processValue(planEntitlement, this.plan.slug);
|
|
118
|
+
// Process add-ons (summation logic — limits stack)
|
|
119
|
+
for (const item of addonEntitlements) {
|
|
120
|
+
if (item.value === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
processValue(item.value, item.slug);
|
|
123
|
+
}
|
|
124
|
+
// ── 3. Evaluate access ───────────────────────────────────
|
|
125
|
+
if (!hasAccess) {
|
|
126
|
+
return { allowed: false, reason: "feature_missing" };
|
|
127
|
+
}
|
|
128
|
+
if (isInfinite) {
|
|
129
|
+
return { allowed: true, remaining: Infinity, granted_by };
|
|
130
|
+
}
|
|
131
|
+
// ── 4. Evaluate limits ───────────────────────────────────
|
|
132
|
+
if (currentUsage < totalLimit) {
|
|
133
|
+
return {
|
|
134
|
+
allowed: true,
|
|
135
|
+
reason: "included",
|
|
136
|
+
remaining: totalLimit - currentUsage,
|
|
137
|
+
granted_by,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Limit reached — check if overage is allowed
|
|
141
|
+
if (!hardLimit) {
|
|
142
|
+
return {
|
|
143
|
+
allowed: true,
|
|
144
|
+
reason: "overage_allowed",
|
|
145
|
+
remaining: 0,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return { allowed: false, reason: "limit_reached", remaining: 0 };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Evaluate multiple features in a single pass.
|
|
152
|
+
*
|
|
153
|
+
* @param usages - Map of `featureSlug → currentUsage` to evaluate.
|
|
154
|
+
* @returns Map of `featureSlug → CheckResult`.
|
|
155
|
+
*/
|
|
156
|
+
checkBatch(usages) {
|
|
157
|
+
const results = {};
|
|
158
|
+
for (const [featureId, usage] of Object.entries(usages)) {
|
|
159
|
+
results[featureId] = this.check(featureId, usage);
|
|
160
|
+
}
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC"}
|
package/dist/index.js
ADDED
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":""}
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EntitlementEngine } from "@/engine";
|
|
2
|
+
const planPro = {
|
|
3
|
+
name: "Pro",
|
|
4
|
+
id: "pro",
|
|
5
|
+
price: 2900,
|
|
6
|
+
currency: "USD",
|
|
7
|
+
interval: "month",
|
|
8
|
+
features: {
|
|
9
|
+
sso: true,
|
|
10
|
+
ai_tokens: { limit: 50_000, unitPrice: 0.01, included: true },
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const engine = new EntitlementEngine(planPro);
|
|
14
|
+
console.log(engine.check("ai_tokens", 60_000));
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file types.ts
|
|
3
|
+
* @description Core type definitions for Revstack's Billing Engine.
|
|
4
|
+
* These types map directly to the PostgreSQL database schema and
|
|
5
|
+
* serve as the contract for "Billing as Code".
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The type of a feature/entitlement.
|
|
9
|
+
* - 'boolean': On/Off flag (e.g., SSO Access).
|
|
10
|
+
* - 'static': Fixed numeric limit included in the plan (e.g., 5 Seats).
|
|
11
|
+
* - 'metered': Usage-based, tracked over time (e.g., AI Tokens).
|
|
12
|
+
*/
|
|
13
|
+
export type FeatureType = "boolean" | "static" | "metered";
|
|
14
|
+
/**
|
|
15
|
+
* The unit of measurement for a feature.
|
|
16
|
+
* Used for display, analytics, and billing calculations.
|
|
17
|
+
*/
|
|
18
|
+
export type UnitType = "count" | "bytes" | "seconds" | "tokens" | "requests" | "custom";
|
|
19
|
+
/**
|
|
20
|
+
* How often a feature's usage counter resets.
|
|
21
|
+
*/
|
|
22
|
+
export type ResetPeriod = "monthly" | "yearly" | "never";
|
|
23
|
+
/**
|
|
24
|
+
* Billing interval for a plan's price.
|
|
25
|
+
*/
|
|
26
|
+
export type BillingInterval = "monthly" | "quarterly" | "yearly" | "one_time";
|
|
27
|
+
/**
|
|
28
|
+
* The commercial classification of a plan.
|
|
29
|
+
* - 'free': No payment required (e.g., Default Guest Plan, Starter).
|
|
30
|
+
* - 'paid': Requires active payment method.
|
|
31
|
+
* - 'custom': Enterprise / negotiated pricing.
|
|
32
|
+
*/
|
|
33
|
+
export type PlanType = "paid" | "free" | "custom";
|
|
34
|
+
/**
|
|
35
|
+
* The lifecycle status of a plan.
|
|
36
|
+
* - 'draft': Not yet visible or purchasable.
|
|
37
|
+
* - 'active': Live and available for subscription.
|
|
38
|
+
* - 'archived': No longer available for new subscriptions, existing ones honored.
|
|
39
|
+
*/
|
|
40
|
+
export type PlanStatus = "draft" | "active" | "archived";
|
|
41
|
+
/**
|
|
42
|
+
* The lifecycle state of a customer's subscription.
|
|
43
|
+
* Used by the EntitlementEngine to gate access based on payment status.
|
|
44
|
+
*/
|
|
45
|
+
export type SubscriptionStatus = "active" | "trialing" | "past_due" | "canceled" | "paused";
|
|
46
|
+
/**
|
|
47
|
+
* Definition of a Feature available in the system.
|
|
48
|
+
* Maps to the `entitlements` table in the database.
|
|
49
|
+
*
|
|
50
|
+
* The `slug` field is the primary identifier and matches the dictionary
|
|
51
|
+
* key in `RevstackConfig.features`.
|
|
52
|
+
*/
|
|
53
|
+
export interface FeatureDef {
|
|
54
|
+
/** Unique slug/identifier (matches dictionary key in config). */
|
|
55
|
+
slug: string;
|
|
56
|
+
/** Human-readable display name. */
|
|
57
|
+
name: string;
|
|
58
|
+
/** Optional description for documentation and dashboard. */
|
|
59
|
+
description?: string;
|
|
60
|
+
/** The data type of the feature. */
|
|
61
|
+
type: FeatureType;
|
|
62
|
+
/** The unit of measurement. */
|
|
63
|
+
unit_type: UnitType;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Input type for `defineFeature()`.
|
|
67
|
+
* The `slug` is omitted because it is inferred from the dictionary key.
|
|
68
|
+
*/
|
|
69
|
+
export type FeatureDefInput = Omit<FeatureDef, "slug">;
|
|
70
|
+
/**
|
|
71
|
+
* Configures how a feature behaves inside a specific Plan.
|
|
72
|
+
* Maps to the `plan_entitlements` table in the database.
|
|
73
|
+
*
|
|
74
|
+
* Each field is optional — only set the fields relevant to the feature type:
|
|
75
|
+
* - Boolean features: use `value_bool`.
|
|
76
|
+
* - Static features: use `value_limit` + `is_hard_limit`.
|
|
77
|
+
* - Metered features: use `value_limit` + `reset_period`.
|
|
78
|
+
*/
|
|
79
|
+
export interface PlanFeatureValue {
|
|
80
|
+
/** Numeric limit (e.g., 5 seats, 10000 API calls). */
|
|
81
|
+
value_limit?: number;
|
|
82
|
+
/** Boolean toggle (e.g., SSO enabled/disabled). */
|
|
83
|
+
value_bool?: boolean;
|
|
84
|
+
/** Text value for display or metadata. */
|
|
85
|
+
value_text?: string;
|
|
86
|
+
/** If true, usage is blocked when limit is reached. */
|
|
87
|
+
is_hard_limit?: boolean;
|
|
88
|
+
/** How often usage resets. */
|
|
89
|
+
reset_period?: ResetPeriod;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Defines the pricing for a plan.
|
|
93
|
+
* Maps to the `prices` table in the database.
|
|
94
|
+
*/
|
|
95
|
+
export interface PriceDef {
|
|
96
|
+
/** Price amount in the smallest currency unit (e.g., cents). */
|
|
97
|
+
amount: number;
|
|
98
|
+
/** ISO 4217 currency code (e.g., "USD", "EUR"). */
|
|
99
|
+
currency: string;
|
|
100
|
+
/** How often the customer is billed. */
|
|
101
|
+
billing_interval: BillingInterval;
|
|
102
|
+
/** Number of days for a free trial before billing starts. */
|
|
103
|
+
trial_period_days?: number;
|
|
104
|
+
/** Whether this price is currently active. */
|
|
105
|
+
is_active?: boolean;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Full plan definition. Maps to the `plans` table in the database.
|
|
109
|
+
*
|
|
110
|
+
* The `slug` is the primary identifier and matches the dictionary
|
|
111
|
+
* key in `RevstackConfig.plans`.
|
|
112
|
+
*/
|
|
113
|
+
export interface PlanDef {
|
|
114
|
+
/** Unique slug/identifier (matches dictionary key in config). */
|
|
115
|
+
slug: string;
|
|
116
|
+
/** Human-readable display name. */
|
|
117
|
+
name: string;
|
|
118
|
+
/** Optional description. */
|
|
119
|
+
description?: string;
|
|
120
|
+
/** Whether this is the default guest plan. */
|
|
121
|
+
is_default: boolean;
|
|
122
|
+
/** Whether this plan is visible on the pricing page. */
|
|
123
|
+
is_public: boolean;
|
|
124
|
+
/** Commercial classification. */
|
|
125
|
+
type: PlanType;
|
|
126
|
+
/** Lifecycle status. */
|
|
127
|
+
status: PlanStatus;
|
|
128
|
+
/** Optional pricing tiers (1:N). Free/default plans have no prices. */
|
|
129
|
+
prices?: PriceDef[];
|
|
130
|
+
/** Feature entitlements included in this plan. */
|
|
131
|
+
features: Record<string, PlanFeatureValue>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Input type for `definePlan()`.
|
|
135
|
+
* - `slug` is omitted (inferred from dictionary key).
|
|
136
|
+
* - `status` is optional (defaults to `'active'`).
|
|
137
|
+
*/
|
|
138
|
+
export type PlanDefInput = Omit<PlanDef, "slug" | "status" | "features"> & {
|
|
139
|
+
status?: PlanStatus;
|
|
140
|
+
features: Record<string, PlanFeatureValue>;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* An add-on is a product purchased on top of a subscription.
|
|
144
|
+
* Maps to the `addons` table in the database.
|
|
145
|
+
*/
|
|
146
|
+
export interface AddonDef {
|
|
147
|
+
/** Unique slug/identifier. */
|
|
148
|
+
slug: string;
|
|
149
|
+
/** Human-readable display name. */
|
|
150
|
+
name: string;
|
|
151
|
+
/** Optional description. */
|
|
152
|
+
description?: string;
|
|
153
|
+
/** Billing type. */
|
|
154
|
+
type: "recurring" | "one_time";
|
|
155
|
+
/** Add-on pricing. */
|
|
156
|
+
price: PriceDef;
|
|
157
|
+
/** Feature entitlements this add-on grants. */
|
|
158
|
+
features: Record<string, PlanFeatureValue>;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Input type for `defineAddon()`.
|
|
162
|
+
* The `slug` is omitted (inferred from dictionary key).
|
|
163
|
+
*/
|
|
164
|
+
export type AddonDefInput = Omit<AddonDef, "slug">;
|
|
165
|
+
export type DiscountType = "percent" | "amount";
|
|
166
|
+
export type DiscountDuration = "once" | "forever" | "repeating";
|
|
167
|
+
export interface DiscountDef {
|
|
168
|
+
/** The code the user enters at checkout (e.g., 'BLACKFRIDAY_24'). */
|
|
169
|
+
code: string;
|
|
170
|
+
/** Friendly name for invoices. */
|
|
171
|
+
name?: string;
|
|
172
|
+
/** 'percent' (0–100) or 'amount' (smallest currency unit). */
|
|
173
|
+
type: DiscountType;
|
|
174
|
+
/** The discount value. */
|
|
175
|
+
value: number;
|
|
176
|
+
/** How long the discount lasts. */
|
|
177
|
+
duration: DiscountDuration;
|
|
178
|
+
/** If duration is 'repeating', how many months. */
|
|
179
|
+
duration_in_months?: number;
|
|
180
|
+
/** Restrict to specific plan slugs. Empty = all. */
|
|
181
|
+
applies_to_plans?: string[];
|
|
182
|
+
/** Maximum number of redemptions globally. */
|
|
183
|
+
max_redemptions?: number;
|
|
184
|
+
/** Expiration date (ISO 8601). */
|
|
185
|
+
expires_at?: string;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* The output of the Entitlement Engine.
|
|
189
|
+
* Answers: "Can the user do this?"
|
|
190
|
+
*/
|
|
191
|
+
export interface CheckResult {
|
|
192
|
+
/** Is the action allowed? */
|
|
193
|
+
allowed: boolean;
|
|
194
|
+
/** Why was it allowed or denied? */
|
|
195
|
+
reason?: "feature_missing" | "limit_reached" | "past_due" | "included" | "overage_allowed";
|
|
196
|
+
/** How many units remain before hitting the limit. Infinity if unlimited. */
|
|
197
|
+
remaining?: number;
|
|
198
|
+
/** Estimated overage cost in the smallest currency unit. */
|
|
199
|
+
cost_estimate?: number;
|
|
200
|
+
/** Which source granted access (plan or addon slug). */
|
|
201
|
+
granted_by?: string;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The structure of the `revstack.config.ts` file.
|
|
205
|
+
*
|
|
206
|
+
* Features and plans are dictionaries keyed by slug.
|
|
207
|
+
* The define helpers (`defineFeature`, `definePlan`) return input types
|
|
208
|
+
* without `slug` — it is inferred from the dictionary key.
|
|
209
|
+
*/
|
|
210
|
+
export interface RevstackConfig {
|
|
211
|
+
/** Dictionary of all available features, keyed by slug. */
|
|
212
|
+
features: Record<string, FeatureDefInput>;
|
|
213
|
+
/** Dictionary of all plans, keyed by slug. */
|
|
214
|
+
plans: Record<string, PlanDefInput>;
|
|
215
|
+
/** Dictionary of available add-ons, keyed by slug. */
|
|
216
|
+
addons?: Record<string, AddonDefInput>;
|
|
217
|
+
/** Array of available coupons/discounts. */
|
|
218
|
+
coupons?: DiscountDef[];
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file validator.ts
|
|
3
|
+
* @description Runtime validation for Revstack billing configurations.
|
|
4
|
+
*
|
|
5
|
+
* Validates the business logic invariants of a `RevstackConfig` object
|
|
6
|
+
* before it is synced to the backend. Catches misconfigurations early
|
|
7
|
+
* that TypeScript's type system cannot enforce (e.g., referencing
|
|
8
|
+
* undefined features, negative prices, duplicate slugs).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { validateConfig, defineConfig } from "@revstackhq/core";
|
|
13
|
+
*
|
|
14
|
+
* const config = defineConfig({ features: {}, plans: {} });
|
|
15
|
+
* validateConfig(config); // throws RevstackValidationError if invalid
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { RevstackConfig } from "@/types";
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when `validateConfig()` detects one or more invalid business
|
|
21
|
+
* logic rules in a billing configuration.
|
|
22
|
+
*
|
|
23
|
+
* The `errors` array contains all violations found — the validator
|
|
24
|
+
* collects every issue before throwing, so developers can fix them
|
|
25
|
+
* all at once instead of playing whack-a-mole.
|
|
26
|
+
*/
|
|
27
|
+
export declare class RevstackValidationError extends Error {
|
|
28
|
+
/** All validation violations found in the config. */
|
|
29
|
+
readonly errors: string[];
|
|
30
|
+
constructor(errors: string[]);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validates a complete Revstack billing configuration.
|
|
34
|
+
*
|
|
35
|
+
* Checks the following invariants:
|
|
36
|
+
* 1. **Default plan** — Exactly one plan has `is_default: true`.
|
|
37
|
+
* 2. **Feature references** — Plans/addons only reference features defined in `config.features`.
|
|
38
|
+
* 3. **Non-negative pricing** — All price amounts and limits are ≥ 0.
|
|
39
|
+
* 4. **Discount bounds** — Percentage discounts have values in [0, 100].
|
|
40
|
+
*
|
|
41
|
+
* @param config - The billing configuration to validate.
|
|
42
|
+
* @throws {RevstackValidationError} If any violations are found.
|
|
43
|
+
*/
|
|
44
|
+
export declare function validateConfig(config: RevstackConfig): void;
|
|
45
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAoB,MAAM,SAAS,CAAC;AAIhE;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,qDAAqD;IACrD,SAAgB,MAAM,EAAE,MAAM,EAAE,CAAC;gBAErB,MAAM,EAAE,MAAM,EAAE;CAU7B;AAqGD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAuC3D"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file validator.ts
|
|
3
|
+
* @description Runtime validation for Revstack billing configurations.
|
|
4
|
+
*
|
|
5
|
+
* Validates the business logic invariants of a `RevstackConfig` object
|
|
6
|
+
* before it is synced to the backend. Catches misconfigurations early
|
|
7
|
+
* that TypeScript's type system cannot enforce (e.g., referencing
|
|
8
|
+
* undefined features, negative prices, duplicate slugs).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { validateConfig, defineConfig } from "@revstackhq/core";
|
|
13
|
+
*
|
|
14
|
+
* const config = defineConfig({ features: {}, plans: {} });
|
|
15
|
+
* validateConfig(config); // throws RevstackValidationError if invalid
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
// ─── Error Class ─────────────────────────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Thrown when `validateConfig()` detects one or more invalid business
|
|
21
|
+
* logic rules in a billing configuration.
|
|
22
|
+
*
|
|
23
|
+
* The `errors` array contains all violations found — the validator
|
|
24
|
+
* collects every issue before throwing, so developers can fix them
|
|
25
|
+
* all at once instead of playing whack-a-mole.
|
|
26
|
+
*/
|
|
27
|
+
export class RevstackValidationError extends Error {
|
|
28
|
+
/** All validation violations found in the config. */
|
|
29
|
+
errors;
|
|
30
|
+
constructor(errors) {
|
|
31
|
+
const summary = errors.length === 1
|
|
32
|
+
? `Revstack config validation failed: ${errors[0]}`
|
|
33
|
+
: `Revstack config validation failed with ${errors.length} errors:\n - ${errors.join("\n - ")}`;
|
|
34
|
+
super(summary);
|
|
35
|
+
this.name = "RevstackValidationError";
|
|
36
|
+
this.errors = errors;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// ─── Feature Reference Validation ────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Collects errors for feature keys in a product that don't exist
|
|
42
|
+
* in the root feature dictionary.
|
|
43
|
+
*/
|
|
44
|
+
function validateFeatureReferences(productType, productSlug, features, knownFeatureSlugs, errors) {
|
|
45
|
+
for (const featureSlug of Object.keys(features)) {
|
|
46
|
+
if (!knownFeatureSlugs.has(featureSlug)) {
|
|
47
|
+
errors.push(`${productType} "${productSlug}" references undefined feature "${featureSlug}".`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ─── Pricing Validation ──────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Validates that prices within a plan are non-negative.
|
|
54
|
+
*/
|
|
55
|
+
function validatePlanPricing(planSlug, prices, features, errors) {
|
|
56
|
+
if (prices) {
|
|
57
|
+
for (const price of prices) {
|
|
58
|
+
if (price.amount < 0) {
|
|
59
|
+
errors.push(`Plan "${planSlug}" has a negative price amount (${price.amount}).`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const [featureSlug, value] of Object.entries(features)) {
|
|
64
|
+
if (value.value_limit !== undefined && value.value_limit < 0) {
|
|
65
|
+
errors.push(`Plan "${planSlug}" → feature "${featureSlug}" has a negative value_limit (${value.value_limit}).`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ─── Default Plan Validation ─────────────────────────────────
|
|
70
|
+
/**
|
|
71
|
+
* Ensures exactly one plan has `is_default: true`.
|
|
72
|
+
*/
|
|
73
|
+
function validateDefaultPlan(config, errors) {
|
|
74
|
+
const defaultPlans = Object.entries(config.plans).filter(([, plan]) => plan.is_default);
|
|
75
|
+
if (defaultPlans.length === 0) {
|
|
76
|
+
errors.push("No default plan found. Every project must have exactly one plan with is_default: true.");
|
|
77
|
+
}
|
|
78
|
+
else if (defaultPlans.length > 1) {
|
|
79
|
+
const slugs = defaultPlans.map(([slug]) => slug).join(", ");
|
|
80
|
+
errors.push(`Multiple default plans found (${slugs}). Only one plan can have is_default: true.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ─── Discount Validation ─────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Validates discount-specific business rules.
|
|
86
|
+
*/
|
|
87
|
+
function validateDiscounts(config, errors) {
|
|
88
|
+
if (!config.coupons)
|
|
89
|
+
return;
|
|
90
|
+
for (const coupon of config.coupons) {
|
|
91
|
+
if (coupon.type === "percent" && (coupon.value < 0 || coupon.value > 100)) {
|
|
92
|
+
errors.push(`Discount "${coupon.code}" has an invalid percentage value (${coupon.value}). Must be 0–100.`);
|
|
93
|
+
}
|
|
94
|
+
if (coupon.type === "amount" && coupon.value < 0) {
|
|
95
|
+
errors.push(`Discount "${coupon.code}" has a negative amount value (${coupon.value}).`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ─── Main Validator ──────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Validates a complete Revstack billing configuration.
|
|
102
|
+
*
|
|
103
|
+
* Checks the following invariants:
|
|
104
|
+
* 1. **Default plan** — Exactly one plan has `is_default: true`.
|
|
105
|
+
* 2. **Feature references** — Plans/addons only reference features defined in `config.features`.
|
|
106
|
+
* 3. **Non-negative pricing** — All price amounts and limits are ≥ 0.
|
|
107
|
+
* 4. **Discount bounds** — Percentage discounts have values in [0, 100].
|
|
108
|
+
*
|
|
109
|
+
* @param config - The billing configuration to validate.
|
|
110
|
+
* @throws {RevstackValidationError} If any violations are found.
|
|
111
|
+
*/
|
|
112
|
+
export function validateConfig(config) {
|
|
113
|
+
const errors = [];
|
|
114
|
+
const knownFeatureSlugs = new Set(Object.keys(config.features));
|
|
115
|
+
// ── Default Plan ───────────────────────────────────────────
|
|
116
|
+
validateDefaultPlan(config, errors);
|
|
117
|
+
// ── Plans ──────────────────────────────────────────────────
|
|
118
|
+
for (const [slug, plan] of Object.entries(config.plans)) {
|
|
119
|
+
validateFeatureReferences("Plan", slug, plan.features, knownFeatureSlugs, errors);
|
|
120
|
+
validatePlanPricing(slug, plan.prices, plan.features, errors);
|
|
121
|
+
}
|
|
122
|
+
// ── Add-ons ────────────────────────────────────────────────
|
|
123
|
+
if (config.addons) {
|
|
124
|
+
for (const [slug, addon] of Object.entries(config.addons)) {
|
|
125
|
+
validateFeatureReferences("Addon", slug, addon.features, knownFeatureSlugs, errors);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ── Discounts ──────────────────────────────────────────────
|
|
129
|
+
validateDiscounts(config, errors);
|
|
130
|
+
// ── Throw if any violations were collected ─────────────────
|
|
131
|
+
if (errors.length > 0) {
|
|
132
|
+
throw new RevstackValidationError(errors);
|
|
133
|
+
}
|
|
134
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@revstackhq/core",
|
|
3
|
+
"version": "0.0.0-dev-20260215075706",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "FSL-1.1-MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@eslint/js": "^9.39.1",
|
|
20
|
+
"@types/node": "^20.11.30",
|
|
21
|
+
"eslint": "^9.39.1",
|
|
22
|
+
"eslint-config-prettier": "^10.1.1",
|
|
23
|
+
"eslint-plugin-turbo": "^2.7.1",
|
|
24
|
+
"typescript": "5.9.2",
|
|
25
|
+
"typescript-eslint": "^8.50.0",
|
|
26
|
+
"vitest": "^4.0.18",
|
|
27
|
+
"@revstackhq/eslint-config": "0.0.0"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -p tsconfig.json",
|
|
34
|
+
"check-types": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|