@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 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
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export * from "@/types";
2
+ export * from "@/define";
3
+ export * from "@/engine";
4
+ export * from "@/validator";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -0,0 +1,4 @@
1
+ export * from "@/types";
2
+ export * from "@/define";
3
+ export * from "@/engine";
4
+ export * from "@/validator";
package/dist/test.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=test.d.ts.map
@@ -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));
@@ -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,7 @@
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
+ export {};
@@ -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
+ }