@soulbatical/tetra-core 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/frontend/index.d.ts +0 -2
- package/dist/frontend/index.d.ts.map +1 -1
- package/dist/frontend/index.js +0 -1
- package/dist/frontend/index.js.map +1 -1
- package/dist/shared/billing/SeatBillingService.d.ts +106 -0
- package/dist/shared/billing/SeatBillingService.d.ts.map +1 -0
- package/dist/shared/billing/SeatBillingService.js +292 -0
- package/dist/shared/billing/SeatBillingService.js.map +1 -0
- package/dist/shared/billing/index.d.ts +4 -0
- package/dist/shared/billing/index.d.ts.map +1 -1
- package/dist/shared/billing/index.js +2 -0
- package/dist/shared/billing/index.js.map +1 -1
- package/dist/shared/billing/seat-pricing.d.ts +53 -0
- package/dist/shared/billing/seat-pricing.d.ts.map +1 -0
- package/dist/shared/billing/seat-pricing.js +81 -0
- package/dist/shared/billing/seat-pricing.js.map +1 -0
- package/dist/shared/license/constants.d.ts +12 -10
- package/dist/shared/license/constants.d.ts.map +1 -1
- package/dist/shared/license/constants.js +12 -10
- package/dist/shared/license/constants.js.map +1 -1
- package/dist/shared/license/generator.d.ts +9 -6
- package/dist/shared/license/generator.d.ts.map +1 -1
- package/dist/shared/license/generator.js +15 -13
- package/dist/shared/license/generator.js.map +1 -1
- package/dist/shared/license/index.d.ts +2 -2
- package/dist/shared/license/index.d.ts.map +1 -1
- package/dist/shared/license/index.js +4 -3
- package/dist/shared/license/index.js.map +1 -1
- package/dist/shared/license/validator.d.ts +2 -8
- package/dist/shared/license/validator.d.ts.map +1 -1
- package/dist/shared/license/validator.js +12 -21
- package/dist/shared/license/validator.js.map +1 -1
- package/package.json +1 -1
package/dist/frontend/index.d.ts
CHANGED
|
@@ -16,8 +16,6 @@ export { StorageDropzone } from './storage/StorageDropzone.js';
|
|
|
16
16
|
export type { StorageDropzoneProps } from './storage/StorageDropzone.js';
|
|
17
17
|
export { useFeature } from './hooks/useFeature.js';
|
|
18
18
|
export type { UseFeatureOptions, UseFeatureResult } from './hooks/useFeature.js';
|
|
19
|
-
export { useClickToConfirm } from './hooks/useClickToConfirm.js';
|
|
20
|
-
export type { UseClickToConfirmOptions, UseClickToConfirmResult } from './hooks/useClickToConfirm.js';
|
|
21
19
|
export { FeatureTable } from './components/FeatureTable.js';
|
|
22
20
|
export type { FeatureTableProps, CellRendererMap } from './components/FeatureTable.js';
|
|
23
21
|
export { FeatureFilters } from './components/FeatureFilters.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/frontend/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACnH,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/frontend/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,SAAS,EAAE,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AACnH,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEjF,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAC5D,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEvF,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAE1E,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAExF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/frontend/index.js
CHANGED
|
@@ -12,7 +12,6 @@ export { configureStorageUrls } from './storage/storageUrl.js';
|
|
|
12
12
|
export { useStorageUpload } from './storage/useStorageUpload.js';
|
|
13
13
|
export { StorageDropzone } from './storage/StorageDropzone.js';
|
|
14
14
|
export { useFeature } from './hooks/useFeature.js';
|
|
15
|
-
export { useClickToConfirm } from './hooks/useClickToConfirm.js';
|
|
16
15
|
export { FeatureTable } from './components/FeatureTable.js';
|
|
17
16
|
export { FeatureFilters } from './components/FeatureFilters.js';
|
|
18
17
|
export { FeatureForm } from './components/FeatureForm.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/frontend/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGnD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/frontend/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAEjE,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAG1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeatBillingService — Per-user (seat) billing with volume discounts
|
|
3
|
+
*
|
|
4
|
+
* Extends the base BillingService with metered/seat-based pricing.
|
|
5
|
+
* Each billing period, Tetra counts authenticated users and creates
|
|
6
|
+
* a Stripe invoice for the correct amount based on the tier.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Customer signs up via Stripe Checkout (subscription with $0 base price)
|
|
10
|
+
* 2. Heartbeat reports user count monthly
|
|
11
|
+
* 3. At billing time: count users → calculate tier price → create Stripe invoice item
|
|
12
|
+
* 4. Stripe charges the customer
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { SeatBillingService } from '@soulbatical/tetra-core/billing';
|
|
17
|
+
*
|
|
18
|
+
* const seatBilling = new SeatBillingService({
|
|
19
|
+
* stripe: { secretKey: '...', webhookSecret: '...' },
|
|
20
|
+
* getUserCount: async (orgId) => { ... },
|
|
21
|
+
* ...config,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Report usage (call monthly via cron or heartbeat)
|
|
25
|
+
* await seatBilling.reportUsage(orgId);
|
|
26
|
+
*
|
|
27
|
+
* // Or report for all active subscriptions
|
|
28
|
+
* await seatBilling.reportAllUsage();
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @module @soulbatical/tetra-core/billing
|
|
32
|
+
*/
|
|
33
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
34
|
+
import type { SeatTier } from './seat-pricing.js';
|
|
35
|
+
export interface SeatBillingConfig {
|
|
36
|
+
stripe: {
|
|
37
|
+
secretKey: string;
|
|
38
|
+
webhookSecret: string;
|
|
39
|
+
};
|
|
40
|
+
/** Stripe Price ID for the base subscription (should be $0 or minimal) */
|
|
41
|
+
stripeBasePriceId: string;
|
|
42
|
+
/** Frontend URL for redirect after checkout */
|
|
43
|
+
frontendUrl: string;
|
|
44
|
+
successPath: string;
|
|
45
|
+
cancelPath: string;
|
|
46
|
+
/** Product name (shown on invoices) */
|
|
47
|
+
productName: string;
|
|
48
|
+
/** Function to count authenticated users for an organization */
|
|
49
|
+
getUserCount: (orgId: string) => Promise<number>;
|
|
50
|
+
/** Function to get a system-level DB client */
|
|
51
|
+
getSystemDB: () => SupabaseClient;
|
|
52
|
+
/** Function to get a webhook-level DB client */
|
|
53
|
+
getWebhookDB: (context: string) => SupabaseClient;
|
|
54
|
+
/** Custom seat tiers (defaults to Tetra standard tiers) */
|
|
55
|
+
tiers?: SeatTier[];
|
|
56
|
+
/** Optional callbacks */
|
|
57
|
+
onSubscriptionCreated?: (orgId: string) => Promise<void>;
|
|
58
|
+
onSubscriptionCanceled?: (orgId: string) => Promise<void>;
|
|
59
|
+
onUsageReported?: (orgId: string, users: number, amountCents: number) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
export declare class SeatBillingService {
|
|
62
|
+
private config;
|
|
63
|
+
private stripeClient;
|
|
64
|
+
private tiers;
|
|
65
|
+
constructor(config: SeatBillingConfig);
|
|
66
|
+
private getStripe;
|
|
67
|
+
/**
|
|
68
|
+
* Create a Stripe Checkout session for seat-based billing.
|
|
69
|
+
* The subscription starts with a $0 base price. Actual charges
|
|
70
|
+
* come from usage-based invoice items added each billing period.
|
|
71
|
+
*/
|
|
72
|
+
createCheckout(orgId: string, email?: string): Promise<{
|
|
73
|
+
url: string;
|
|
74
|
+
}>;
|
|
75
|
+
/**
|
|
76
|
+
* Report seat usage for a single organization.
|
|
77
|
+
* Counts current users, calculates the tier price, and creates
|
|
78
|
+
* a Stripe invoice item for the upcoming invoice.
|
|
79
|
+
*/
|
|
80
|
+
reportUsage(orgId: string): Promise<{
|
|
81
|
+
users: number;
|
|
82
|
+
amountCents: number;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Report usage for all active seat-based subscriptions.
|
|
86
|
+
* Call this monthly via a cron job.
|
|
87
|
+
*/
|
|
88
|
+
reportAllUsage(): Promise<{
|
|
89
|
+
reported: number;
|
|
90
|
+
errors: number;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Calculate the price for a given number of users.
|
|
94
|
+
* Useful for showing estimated pricing in the UI.
|
|
95
|
+
*/
|
|
96
|
+
calculatePrice(users: number): import("./seat-pricing.js").SeatPriceResult;
|
|
97
|
+
/**
|
|
98
|
+
* Handle Stripe webhook events for seat billing.
|
|
99
|
+
* Delegates to BillingService for standard subscription events.
|
|
100
|
+
*/
|
|
101
|
+
handleStripeWebhook(rawBody: Buffer, signature: string): Promise<void>;
|
|
102
|
+
private handleCheckoutCompleted;
|
|
103
|
+
private handleSubscriptionDeleted;
|
|
104
|
+
private handlePaymentFailed;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=SeatBillingService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeatBillingService.d.ts","sourceRoot":"","sources":["../../../src/shared/billing/SeatBillingService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAKlD,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,0EAA0E;IAC1E,iBAAiB,EAAE,MAAM,CAAC;IAE1B,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IAEnB,uCAAuC;IACvC,WAAW,EAAE,MAAM,CAAC;IAEpB,gEAAgE;IAChE,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAEjD,+CAA+C;IAC/C,WAAW,EAAE,MAAM,cAAc,CAAC;IAElC,gDAAgD;IAChD,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,cAAc,CAAC;IAElD,2DAA2D;IAC3D,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;IAEnB,yBAAyB;IACzB,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxF;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,KAAK,CAAa;gBAEd,MAAM,EAAE,iBAAiB;YAKvB,SAAS;IAUvB;;;;OAIG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IA4C7E;;;;OAIG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IA4EjF;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAiCrE;;;OAGG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM;IAM5B;;;OAGG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YA2B9D,uBAAuB;YA6BvB,yBAAyB;YAwBzB,mBAAmB;CAkBlC"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SeatBillingService — Per-user (seat) billing with volume discounts
|
|
3
|
+
*
|
|
4
|
+
* Extends the base BillingService with metered/seat-based pricing.
|
|
5
|
+
* Each billing period, Tetra counts authenticated users and creates
|
|
6
|
+
* a Stripe invoice for the correct amount based on the tier.
|
|
7
|
+
*
|
|
8
|
+
* Flow:
|
|
9
|
+
* 1. Customer signs up via Stripe Checkout (subscription with $0 base price)
|
|
10
|
+
* 2. Heartbeat reports user count monthly
|
|
11
|
+
* 3. At billing time: count users → calculate tier price → create Stripe invoice item
|
|
12
|
+
* 4. Stripe charges the customer
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { SeatBillingService } from '@soulbatical/tetra-core/billing';
|
|
17
|
+
*
|
|
18
|
+
* const seatBilling = new SeatBillingService({
|
|
19
|
+
* stripe: { secretKey: '...', webhookSecret: '...' },
|
|
20
|
+
* getUserCount: async (orgId) => { ... },
|
|
21
|
+
* ...config,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Report usage (call monthly via cron or heartbeat)
|
|
25
|
+
* await seatBilling.reportUsage(orgId);
|
|
26
|
+
*
|
|
27
|
+
* // Or report for all active subscriptions
|
|
28
|
+
* await seatBilling.reportAllUsage();
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @module @soulbatical/tetra-core/billing
|
|
32
|
+
*/
|
|
33
|
+
import { calculateSeatPrice, seatPriceToCents, DEFAULT_SEAT_TIERS } from './seat-pricing.js';
|
|
34
|
+
import { createLogger } from '../../utils/logger.js';
|
|
35
|
+
const logger = createLogger('billing:seat');
|
|
36
|
+
export class SeatBillingService {
|
|
37
|
+
config;
|
|
38
|
+
stripeClient = null;
|
|
39
|
+
tiers;
|
|
40
|
+
constructor(config) {
|
|
41
|
+
this.config = config;
|
|
42
|
+
this.tiers = config.tiers ?? DEFAULT_SEAT_TIERS;
|
|
43
|
+
}
|
|
44
|
+
async getStripe() {
|
|
45
|
+
if (!this.stripeClient) {
|
|
46
|
+
const stripe = (await import('stripe')).default;
|
|
47
|
+
this.stripeClient = new stripe(this.config.stripe.secretKey);
|
|
48
|
+
}
|
|
49
|
+
return this.stripeClient;
|
|
50
|
+
}
|
|
51
|
+
// ─── Checkout ─────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Create a Stripe Checkout session for seat-based billing.
|
|
54
|
+
* The subscription starts with a $0 base price. Actual charges
|
|
55
|
+
* come from usage-based invoice items added each billing period.
|
|
56
|
+
*/
|
|
57
|
+
async createCheckout(orgId, email) {
|
|
58
|
+
const stripe = await this.getStripe();
|
|
59
|
+
const db = this.config.getSystemDB();
|
|
60
|
+
// Get or create Stripe customer
|
|
61
|
+
const { data: org } = await db
|
|
62
|
+
.from('organizations')
|
|
63
|
+
.select('id, name, contact_email, stripe_customer_id')
|
|
64
|
+
.eq('id', orgId)
|
|
65
|
+
.single();
|
|
66
|
+
let customerId = org?.stripe_customer_id;
|
|
67
|
+
if (!customerId) {
|
|
68
|
+
const customer = await stripe.customers.create({
|
|
69
|
+
name: org?.name || undefined,
|
|
70
|
+
email: email || org?.contact_email || undefined,
|
|
71
|
+
metadata: { organization_id: orgId },
|
|
72
|
+
});
|
|
73
|
+
customerId = customer.id;
|
|
74
|
+
await db
|
|
75
|
+
.from('organizations')
|
|
76
|
+
.update({ stripe_customer_id: customerId })
|
|
77
|
+
.eq('id', orgId);
|
|
78
|
+
}
|
|
79
|
+
const session = await stripe.checkout.sessions.create({
|
|
80
|
+
customer: customerId,
|
|
81
|
+
mode: 'subscription',
|
|
82
|
+
line_items: [{ price: this.config.stripeBasePriceId, quantity: 1 }],
|
|
83
|
+
success_url: `${this.config.frontendUrl}${this.config.successPath}`,
|
|
84
|
+
cancel_url: `${this.config.frontendUrl}${this.config.cancelPath}`,
|
|
85
|
+
metadata: { org_id: orgId, billing_type: 'seat' },
|
|
86
|
+
subscription_data: {
|
|
87
|
+
metadata: { org_id: orgId, billing_type: 'seat' },
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
return { url: session.url };
|
|
91
|
+
}
|
|
92
|
+
// ─── Usage Reporting ──────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Report seat usage for a single organization.
|
|
95
|
+
* Counts current users, calculates the tier price, and creates
|
|
96
|
+
* a Stripe invoice item for the upcoming invoice.
|
|
97
|
+
*/
|
|
98
|
+
async reportUsage(orgId) {
|
|
99
|
+
const users = await this.config.getUserCount(orgId);
|
|
100
|
+
const price = calculateSeatPrice(users, this.tiers);
|
|
101
|
+
if (price.monthlyPrice === 0) {
|
|
102
|
+
logger.debug({ orgId, users }, 'No charge — within free tier');
|
|
103
|
+
return { users, amountCents: 0 };
|
|
104
|
+
}
|
|
105
|
+
const amountCents = seatPriceToCents(price.monthlyPrice);
|
|
106
|
+
const db = this.config.getSystemDB();
|
|
107
|
+
// Get Stripe subscription
|
|
108
|
+
const { data: sub } = await db
|
|
109
|
+
.from('subscriptions')
|
|
110
|
+
.select('external_subscription_id, external_customer_id')
|
|
111
|
+
.eq('organization_id', orgId)
|
|
112
|
+
.eq('provider', 'stripe')
|
|
113
|
+
.neq('status', 'canceled')
|
|
114
|
+
.order('created_at', { ascending: false })
|
|
115
|
+
.limit(1)
|
|
116
|
+
.single();
|
|
117
|
+
if (!sub?.external_subscription_id) {
|
|
118
|
+
logger.warn({ orgId }, 'No active subscription found — skipping usage report');
|
|
119
|
+
return { users, amountCents: 0 };
|
|
120
|
+
}
|
|
121
|
+
const stripe = await this.getStripe();
|
|
122
|
+
// Get the subscription to find the current invoice
|
|
123
|
+
const subscription = await stripe.subscriptions.retrieve(sub.external_subscription_id);
|
|
124
|
+
// Create an invoice item for the seat usage
|
|
125
|
+
await stripe.invoiceItems.create({
|
|
126
|
+
customer: sub.external_customer_id,
|
|
127
|
+
subscription: sub.external_subscription_id,
|
|
128
|
+
amount: amountCents,
|
|
129
|
+
currency: 'eur',
|
|
130
|
+
description: `${this.config.productName} — ${users} users (${price.tierPricePerUser > 0 ? `€${price.tierPricePerUser}/user` : 'free tier'}${price.minimumApplied ? ', minimum applied' : ''})`,
|
|
131
|
+
metadata: {
|
|
132
|
+
org_id: orgId,
|
|
133
|
+
user_count: String(users),
|
|
134
|
+
tier_price: String(price.tierPricePerUser),
|
|
135
|
+
minimum_applied: String(price.minimumApplied),
|
|
136
|
+
period: subscription.current_period_start
|
|
137
|
+
? new Date(subscription.current_period_start * 1000).toISOString().slice(0, 7)
|
|
138
|
+
: undefined,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
// Log the usage report
|
|
142
|
+
await db.from('subscription_events').insert({
|
|
143
|
+
organization_id: orgId,
|
|
144
|
+
subscription_id: null,
|
|
145
|
+
event_type: 'usage_reported',
|
|
146
|
+
provider: 'stripe',
|
|
147
|
+
data: {
|
|
148
|
+
user_count: users,
|
|
149
|
+
amount_cents: amountCents,
|
|
150
|
+
tier_price_per_user: price.tierPricePerUser,
|
|
151
|
+
minimum_applied: price.minimumApplied,
|
|
152
|
+
effective_price_per_user: price.effectivePricePerUser,
|
|
153
|
+
},
|
|
154
|
+
source: 'system',
|
|
155
|
+
});
|
|
156
|
+
logger.info({ orgId, users, amountCents, tierPrice: price.tierPricePerUser }, 'Seat usage reported to Stripe');
|
|
157
|
+
await this.config.onUsageReported?.(orgId, users, amountCents);
|
|
158
|
+
return { users, amountCents };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Report usage for all active seat-based subscriptions.
|
|
162
|
+
* Call this monthly via a cron job.
|
|
163
|
+
*/
|
|
164
|
+
async reportAllUsage() {
|
|
165
|
+
const db = this.config.getSystemDB();
|
|
166
|
+
const { data: subs } = await db
|
|
167
|
+
.from('subscriptions')
|
|
168
|
+
.select('organization_id')
|
|
169
|
+
.eq('provider', 'stripe')
|
|
170
|
+
.in('status', ['active', 'trialing']);
|
|
171
|
+
if (!subs || subs.length === 0) {
|
|
172
|
+
logger.info('No active subscriptions to report');
|
|
173
|
+
return { reported: 0, errors: 0 };
|
|
174
|
+
}
|
|
175
|
+
let reported = 0;
|
|
176
|
+
let errors = 0;
|
|
177
|
+
for (const sub of subs) {
|
|
178
|
+
try {
|
|
179
|
+
await this.reportUsage(sub.organization_id);
|
|
180
|
+
reported++;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
logger.error({ orgId: sub.organization_id, error: err }, 'Failed to report usage');
|
|
184
|
+
errors++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
logger.info({ reported, errors, total: subs.length }, 'Batch usage reporting complete');
|
|
188
|
+
return { reported, errors };
|
|
189
|
+
}
|
|
190
|
+
// ─── Price calculation (public, for API endpoints) ────────
|
|
191
|
+
/**
|
|
192
|
+
* Calculate the price for a given number of users.
|
|
193
|
+
* Useful for showing estimated pricing in the UI.
|
|
194
|
+
*/
|
|
195
|
+
calculatePrice(users) {
|
|
196
|
+
return calculateSeatPrice(users, this.tiers);
|
|
197
|
+
}
|
|
198
|
+
// ─── Webhook handling ─────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Handle Stripe webhook events for seat billing.
|
|
201
|
+
* Delegates to BillingService for standard subscription events.
|
|
202
|
+
*/
|
|
203
|
+
async handleStripeWebhook(rawBody, signature) {
|
|
204
|
+
const stripe = await this.getStripe();
|
|
205
|
+
let event;
|
|
206
|
+
try {
|
|
207
|
+
event = stripe.webhooks.constructEvent(rawBody, signature, this.config.stripe.webhookSecret);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
logger.error({ error: err.message }, 'Stripe signature verification failed');
|
|
211
|
+
throw new Error('Invalid signature');
|
|
212
|
+
}
|
|
213
|
+
const db = this.config.getWebhookDB('stripe');
|
|
214
|
+
logger.info({ type: event.type, id: event.id }, 'Stripe webhook received');
|
|
215
|
+
switch (event.type) {
|
|
216
|
+
case 'checkout.session.completed':
|
|
217
|
+
await this.handleCheckoutCompleted(event, db);
|
|
218
|
+
break;
|
|
219
|
+
case 'customer.subscription.deleted':
|
|
220
|
+
await this.handleSubscriptionDeleted(event, db);
|
|
221
|
+
break;
|
|
222
|
+
case 'invoice.payment_failed':
|
|
223
|
+
await this.handlePaymentFailed(event, db);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async handleCheckoutCompleted(event, db) {
|
|
228
|
+
const session = event.data.object;
|
|
229
|
+
const orgId = session.metadata?.org_id;
|
|
230
|
+
if (!orgId)
|
|
231
|
+
return;
|
|
232
|
+
const stripeSubId = typeof session.subscription === 'string' ? session.subscription : null;
|
|
233
|
+
const customerId = typeof session.customer === 'string' ? session.customer : '';
|
|
234
|
+
const now = new Date();
|
|
235
|
+
const periodEnd = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
236
|
+
await db.from('subscriptions').insert({
|
|
237
|
+
organization_id: orgId,
|
|
238
|
+
provider: 'stripe',
|
|
239
|
+
external_subscription_id: stripeSubId,
|
|
240
|
+
external_customer_id: customerId,
|
|
241
|
+
plan: 'tetra-seat',
|
|
242
|
+
billing_cycle: 'monthly',
|
|
243
|
+
price_amount_cents: 0, // Base price is 0, charges come via invoice items
|
|
244
|
+
status: 'active',
|
|
245
|
+
current_period_start: now.toISOString(),
|
|
246
|
+
current_period_end: periodEnd.toISOString(),
|
|
247
|
+
});
|
|
248
|
+
await db.from('organizations').update({ plan: 'tetra-seat' }).eq('id', orgId);
|
|
249
|
+
logger.info({ orgId, provider: 'stripe' }, 'Seat subscription created');
|
|
250
|
+
await this.config.onSubscriptionCreated?.(orgId);
|
|
251
|
+
}
|
|
252
|
+
async handleSubscriptionDeleted(event, db) {
|
|
253
|
+
const stripeSub = event.data.object;
|
|
254
|
+
const { data: sub } = await db
|
|
255
|
+
.from('subscriptions')
|
|
256
|
+
.select('id, organization_id')
|
|
257
|
+
.eq('external_subscription_id', stripeSub.id)
|
|
258
|
+
.single();
|
|
259
|
+
if (!sub)
|
|
260
|
+
return;
|
|
261
|
+
await db
|
|
262
|
+
.from('subscriptions')
|
|
263
|
+
.update({
|
|
264
|
+
status: 'canceled',
|
|
265
|
+
canceled_at: new Date().toISOString(),
|
|
266
|
+
ended_at: new Date().toISOString(),
|
|
267
|
+
})
|
|
268
|
+
.eq('id', sub.id);
|
|
269
|
+
await db.from('organizations').update({ plan: 'free' }).eq('id', sub.organization_id);
|
|
270
|
+
logger.info({ orgId: sub.organization_id }, 'Seat subscription canceled');
|
|
271
|
+
await this.config.onSubscriptionCanceled?.(sub.organization_id);
|
|
272
|
+
}
|
|
273
|
+
async handlePaymentFailed(event, db) {
|
|
274
|
+
const invoice = event.data.object;
|
|
275
|
+
const invoiceAny = invoice;
|
|
276
|
+
const subId = typeof invoiceAny.subscription === 'string' ? invoiceAny.subscription : null;
|
|
277
|
+
if (!subId)
|
|
278
|
+
return;
|
|
279
|
+
const { data: sub } = await db
|
|
280
|
+
.from('subscriptions')
|
|
281
|
+
.select('id, organization_id')
|
|
282
|
+
.eq('external_subscription_id', subId)
|
|
283
|
+
.single();
|
|
284
|
+
if (!sub)
|
|
285
|
+
return;
|
|
286
|
+
await db
|
|
287
|
+
.from('subscriptions')
|
|
288
|
+
.update({ status: 'past_due', updated_at: new Date().toISOString() })
|
|
289
|
+
.eq('id', sub.id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=SeatBillingService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SeatBillingService.js","sourceRoot":"","sources":["../../../src/shared/billing/SeatBillingService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAGH,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE7F,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,MAAM,GAAG,YAAY,CAAC,cAAc,CAAC,CAAC;AAqC5C,MAAM,OAAO,kBAAkB;IACrB,MAAM,CAAoB;IAC1B,YAAY,GAAQ,IAAI,CAAC;IACzB,KAAK,CAAa;IAE1B,YAAY,MAAyB;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,kBAAkB,CAAC;IAClD,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;YAChD,IAAI,CAAC,YAAY,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED,6DAA6D;IAE7D;;;;OAIG;IACH,KAAK,CAAC,cAAc,CAAC,KAAa,EAAE,KAAc;QAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAErC,gCAAgC;QAChC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE;aAC3B,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,6CAA6C,CAAC;aACrD,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC;aACf,MAAM,EAAE,CAAC;QAEZ,IAAI,UAAU,GAAG,GAAG,EAAE,kBAAkB,CAAC;QAEzC,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;gBAC7C,IAAI,EAAE,GAAG,EAAE,IAAI,IAAI,SAAS;gBAC5B,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,aAAa,IAAI,SAAS;gBAC/C,QAAQ,EAAE,EAAE,eAAe,EAAE,KAAK,EAAE;aACrC,CAAC,CAAC;YACH,UAAU,GAAG,QAAQ,CAAC,EAAE,CAAC;YAEzB,MAAM,EAAE;iBACL,IAAI,CAAC,eAAe,CAAC;iBACrB,MAAM,CAAC,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC;iBAC1C,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrB,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;YACpD,QAAQ,EAAE,UAAU;YACpB,IAAI,EAAE,cAAc;YACpB,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YACnE,WAAW,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE;YACnE,UAAU,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE;YACjE,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE;YACjD,iBAAiB,EAAE;gBACjB,QAAQ,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE;aAClD;SACF,CAAC,CAAC;QAEH,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;IAC9B,CAAC;IAED,6DAA6D;IAE7D;;;;OAIG;IACH,KAAK,CAAC,WAAW,CAAC,KAAa;QAC7B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QACpD,MAAM,KAAK,GAAG,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAEpD,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,8BAA8B,CAAC,CAAC;YAC/D,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACzD,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAErC,0BAA0B;QAC1B,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE;aAC3B,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,gDAAgD,CAAC;aACxD,EAAE,CAAC,iBAAiB,EAAE,KAAK,CAAC;aAC5B,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;aACxB,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC;aACzB,KAAK,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;aACzC,KAAK,CAAC,CAAC,CAAC;aACR,MAAM,EAAE,CAAC;QAEZ,IAAI,CAAC,GAAG,EAAE,wBAAwB,EAAE,CAAC;YACnC,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,sDAAsD,CAAC,CAAC;YAC/E,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;QACnC,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QAEtC,mDAAmD;QACnD,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QAEvF,4CAA4C;QAC5C,MAAM,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC;YAC/B,QAAQ,EAAE,GAAG,CAAC,oBAAoB;YAClC,YAAY,EAAE,GAAG,CAAC,wBAAwB;YAC1C,MAAM,EAAE,WAAW;YACnB,QAAQ,EAAE,KAAK;YACf,WAAW,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,MAAM,KAAK,WAAW,KAAK,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,gBAAgB,OAAO,CAAC,CAAC,CAAC,WAAW,GAAG,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,GAAG;YAC9L,QAAQ,EAAE;gBACR,MAAM,EAAE,KAAK;gBACb,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC;gBACzB,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC;gBAC1C,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC;gBAC7C,MAAM,EAAE,YAAY,CAAC,oBAAoB;oBACvC,CAAC,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;oBAC9E,CAAC,CAAC,SAAS;aACd;SACF,CAAC,CAAC;QAEH,uBAAuB;QACvB,MAAM,EAAE,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,MAAM,CAAC;YAC1C,eAAe,EAAE,KAAK;YACtB,eAAe,EAAE,IAAI;YACrB,UAAU,EAAE,gBAAgB;YAC5B,QAAQ,EAAE,QAAQ;YAClB,IAAI,EAAE;gBACJ,UAAU,EAAE,KAAK;gBACjB,YAAY,EAAE,WAAW;gBACzB,mBAAmB,EAAE,KAAK,CAAC,gBAAgB;gBAC3C,eAAe,EAAE,KAAK,CAAC,cAAc;gBACrC,wBAAwB,EAAE,KAAK,CAAC,qBAAqB;aACtD;YACD,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CACT,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,CAAC,gBAAgB,EAAE,EAChE,+BAA+B,CAChC,CAAC;QAEF,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;QAC/D,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAChC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc;QAClB,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAErC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE;aAC5B,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,iBAAiB,CAAC;aACzB,EAAE,CAAC,UAAU,EAAE,QAAQ,CAAC;aACxB,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;QAExC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;YACjD,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QACpC,CAAC;QAED,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,IAAI,MAAM,GAAG,CAAC,CAAC;QAEf,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;gBAC5C,QAAQ,EAAE,CAAC;YACb,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,eAAe,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,wBAAwB,CAAC,CAAC;gBACnF,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,gCAAgC,CAAC,CAAC;QACxF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED,6DAA6D;IAE7D;;;OAGG;IACH,cAAc,CAAC,KAAa;QAC1B,OAAO,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/C,CAAC;IAED,6DAA6D;IAE7D;;;OAGG;IACH,KAAK,CAAC,mBAAmB,CAAC,OAAe,EAAE,SAAiB;QAC1D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QAEtC,IAAI,KAAU,CAAC;QACf,IAAI,CAAC;YACH,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC/F,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,sCAAsC,CAAC,CAAC;YAC7E,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACvC,CAAC;QAED,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC9C,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,EAAE,yBAAyB,CAAC,CAAC;QAE3E,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,4BAA4B;gBAC/B,MAAM,IAAI,CAAC,uBAAuB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC9C,MAAM;YACR,KAAK,+BAA+B;gBAClC,MAAM,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAChD,MAAM;YACR,KAAK,wBAAwB;gBAC3B,MAAM,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC1C,MAAM;QACV,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,uBAAuB,CAAC,KAAU,EAAE,EAAkB;QAClE,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAClC,MAAM,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC;QACvC,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,MAAM,WAAW,GAAG,OAAO,OAAO,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3F,MAAM,UAAU,GAAG,OAAO,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAEhF,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAErE,MAAM,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC;YACpC,eAAe,EAAE,KAAK;YACtB,QAAQ,EAAE,QAAQ;YAClB,wBAAwB,EAAE,WAAW;YACrC,oBAAoB,EAAE,UAAU;YAChC,IAAI,EAAE,YAAY;YAClB,aAAa,EAAE,SAAS;YACxB,kBAAkB,EAAE,CAAC,EAAE,kDAAkD;YACzE,MAAM,EAAE,QAAQ;YAChB,oBAAoB,EAAE,GAAG,CAAC,WAAW,EAAE;YACvC,kBAAkB,EAAE,SAAS,CAAC,WAAW,EAAE;SAC5C,CAAC,CAAC;QAEH,MAAM,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC9E,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,2BAA2B,CAAC,CAAC;QACxE,MAAM,IAAI,CAAC,MAAM,CAAC,qBAAqB,EAAE,CAAC,KAAK,CAAC,CAAC;IACnD,CAAC;IAEO,KAAK,CAAC,yBAAyB,CAAC,KAAU,EAAE,EAAkB;QACpE,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAEpC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE;aAC3B,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,qBAAqB,CAAC;aAC7B,EAAE,CAAC,0BAA0B,EAAE,SAAS,CAAC,EAAE,CAAC;aAC5C,MAAM,EAAE,CAAC;QACZ,IAAI,CAAC,GAAG;YAAE,OAAO;QAEjB,MAAM,EAAE;aACL,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC;YACN,MAAM,EAAE,UAAU;YAClB,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACrC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACnC,CAAC;aACD,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;QAEpB,MAAM,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;QACtF,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,eAAe,EAAE,EAAE,4BAA4B,CAAC,CAAC;QAC1E,MAAM,IAAI,CAAC,MAAM,CAAC,sBAAsB,EAAE,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAClE,CAAC;IAEO,KAAK,CAAC,mBAAmB,CAAC,KAAU,EAAE,EAAkB;QAC9D,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC;QAClC,MAAM,UAAU,GAAG,OAAkC,CAAC;QACtD,MAAM,KAAK,GAAG,OAAO,UAAU,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;QAC3F,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE;aAC3B,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,qBAAqB,CAAC;aAC7B,EAAE,CAAC,0BAA0B,EAAE,KAAK,CAAC;aACrC,MAAM,EAAE,CAAC;QACZ,IAAI,CAAC,GAAG;YAAE,OAAO;QAEjB,MAAM,EAAE;aACL,IAAI,CAAC,eAAe,CAAC;aACrB,MAAM,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;aACpE,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACtB,CAAC;CACF"}
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export { BillingService } from './BillingService.js';
|
|
24
|
+
export { SeatBillingService } from './SeatBillingService.js';
|
|
25
|
+
export { calculateSeatPrice, seatPriceToCents, DEFAULT_SEAT_TIERS } from './seat-pricing.js';
|
|
24
26
|
export { addBillingRoutes, addBillingWebhookRoutes } from './routes.js';
|
|
25
27
|
export type { PlanConfig, BillingConfig, BillingCycle, BillingProvider, SubscriptionStatus, SubscriptionRecord, BillingStatusResponse, BillingRouteOptions, } from './types.js';
|
|
28
|
+
export type { SeatBillingConfig } from './SeatBillingService.js';
|
|
29
|
+
export type { SeatTier, SeatPriceResult } from './seat-pricing.js';
|
|
26
30
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EACV,UAAU,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACxE,YAAY,EACV,UAAU,EACV,aAAa,EACb,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACjE,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -21,5 +21,7 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export { BillingService } from './BillingService.js';
|
|
24
|
+
export { SeatBillingService } from './SeatBillingService.js';
|
|
25
|
+
export { calculateSeatPrice, seatPriceToCents, DEFAULT_SEAT_TIERS } from './seat-pricing.js';
|
|
24
26
|
export { addBillingRoutes, addBillingWebhookRoutes } from './routes.js';
|
|
25
27
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/billing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seat-based pricing calculator
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for per-user pricing with volume discounts and tier minimums.
|
|
5
|
+
* Used by: website pricing calculator, SeatBillingService, license validation.
|
|
6
|
+
*
|
|
7
|
+
* @module @soulbatical/tetra-core/billing
|
|
8
|
+
*/
|
|
9
|
+
export interface SeatTier {
|
|
10
|
+
/** Minimum users for this tier (inclusive) */
|
|
11
|
+
min: number;
|
|
12
|
+
/** Maximum users for this tier (inclusive, Infinity for last tier) */
|
|
13
|
+
max: number;
|
|
14
|
+
/** Price per user per month in EUR */
|
|
15
|
+
pricePerUser: number;
|
|
16
|
+
/** Minimum monthly charge for this tier (prevents price dips at tier boundary) */
|
|
17
|
+
minimum: number;
|
|
18
|
+
}
|
|
19
|
+
/** Default Tetra seat pricing tiers */
|
|
20
|
+
export declare const DEFAULT_SEAT_TIERS: SeatTier[];
|
|
21
|
+
export interface SeatPriceResult {
|
|
22
|
+
/** Total monthly price in EUR */
|
|
23
|
+
monthlyPrice: number;
|
|
24
|
+
/** Total yearly price in EUR */
|
|
25
|
+
yearlyPrice: number;
|
|
26
|
+
/** Number of free users */
|
|
27
|
+
freeUsers: number;
|
|
28
|
+
/** Number of paid users */
|
|
29
|
+
paidUsers: number;
|
|
30
|
+
/** Effective price per user (total / users) */
|
|
31
|
+
effectivePricePerUser: number;
|
|
32
|
+
/** Which tier the user count falls in */
|
|
33
|
+
tierIndex: number;
|
|
34
|
+
/** The tier's listed price per user */
|
|
35
|
+
tierPricePerUser: number;
|
|
36
|
+
/** Whether the minimum was applied */
|
|
37
|
+
minimumApplied: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Calculate the monthly price for a given number of seat users.
|
|
41
|
+
*
|
|
42
|
+
* Rules:
|
|
43
|
+
* - First 10 users are free
|
|
44
|
+
* - Price per user decreases with volume
|
|
45
|
+
* - Each tier has a minimum monthly charge (prevents price dips at boundary)
|
|
46
|
+
* - For 1000+ users, the last tier rate applies
|
|
47
|
+
*/
|
|
48
|
+
export declare function calculateSeatPrice(users: number, tiers?: SeatTier[]): SeatPriceResult;
|
|
49
|
+
/**
|
|
50
|
+
* Convert seat price to Stripe-compatible amount in cents.
|
|
51
|
+
*/
|
|
52
|
+
export declare function seatPriceToCents(monthlyPrice: number): number;
|
|
53
|
+
//# sourceMappingURL=seat-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-pricing.d.ts","sourceRoot":"","sources":["../../../src/shared/billing/seat-pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,QAAQ;IACvB,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,sEAAsE;IACtE,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,uCAAuC;AACvC,eAAO,MAAM,kBAAkB,EAAE,QAAQ,EAMxC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,qBAAqB,EAAE,MAAM,CAAC;IAC9B,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,gBAAgB,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,KAAK,GAAE,QAAQ,EAAuB,GACrC,eAAe,CAuDjB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seat-based pricing calculator
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for per-user pricing with volume discounts and tier minimums.
|
|
5
|
+
* Used by: website pricing calculator, SeatBillingService, license validation.
|
|
6
|
+
*
|
|
7
|
+
* @module @soulbatical/tetra-core/billing
|
|
8
|
+
*/
|
|
9
|
+
/** Default Tetra seat pricing tiers */
|
|
10
|
+
export const DEFAULT_SEAT_TIERS = [
|
|
11
|
+
{ min: 1, max: 10, pricePerUser: 0, minimum: 0 },
|
|
12
|
+
{ min: 11, max: 100, pricePerUser: 3.90, minimum: 0 },
|
|
13
|
+
{ min: 101, max: 250, pricePerUser: 2.90, minimum: 351 },
|
|
14
|
+
{ min: 251, max: 500, pricePerUser: 1.90, minimum: 725 },
|
|
15
|
+
{ min: 501, max: 1000, pricePerUser: 0.90, minimum: 950 },
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Calculate the monthly price for a given number of seat users.
|
|
19
|
+
*
|
|
20
|
+
* Rules:
|
|
21
|
+
* - First 10 users are free
|
|
22
|
+
* - Price per user decreases with volume
|
|
23
|
+
* - Each tier has a minimum monthly charge (prevents price dips at boundary)
|
|
24
|
+
* - For 1000+ users, the last tier rate applies
|
|
25
|
+
*/
|
|
26
|
+
export function calculateSeatPrice(users, tiers = DEFAULT_SEAT_TIERS) {
|
|
27
|
+
if (users <= 0) {
|
|
28
|
+
return {
|
|
29
|
+
monthlyPrice: 0,
|
|
30
|
+
yearlyPrice: 0,
|
|
31
|
+
freeUsers: 0,
|
|
32
|
+
paidUsers: 0,
|
|
33
|
+
effectivePricePerUser: 0,
|
|
34
|
+
tierIndex: 0,
|
|
35
|
+
tierPricePerUser: 0,
|
|
36
|
+
minimumApplied: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Find the tier
|
|
40
|
+
let tierIndex = 0;
|
|
41
|
+
let tierPricePerUser = 0;
|
|
42
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
43
|
+
if (users >= tiers[i].min && users <= tiers[i].max) {
|
|
44
|
+
tierIndex = i;
|
|
45
|
+
tierPricePerUser = tiers[i].pricePerUser;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Beyond last tier: use last tier's rate
|
|
50
|
+
if (users > tiers[tiers.length - 1].max) {
|
|
51
|
+
const lastTier = tiers[tiers.length - 1];
|
|
52
|
+
tierIndex = tiers.length - 1;
|
|
53
|
+
tierPricePerUser = lastTier.pricePerUser;
|
|
54
|
+
}
|
|
55
|
+
const tier = tiers[tierIndex];
|
|
56
|
+
const freeUsers = Math.min(users, tiers[0].max);
|
|
57
|
+
const paidUsers = Math.max(0, users - tiers[0].max);
|
|
58
|
+
// Calculate raw price
|
|
59
|
+
const rawPrice = users * tierPricePerUser;
|
|
60
|
+
// Apply minimum
|
|
61
|
+
const minimum = tier?.minimum ?? 0;
|
|
62
|
+
const minimumApplied = rawPrice < minimum && minimum > 0;
|
|
63
|
+
const monthlyPrice = Math.max(rawPrice, minimum);
|
|
64
|
+
return {
|
|
65
|
+
monthlyPrice: Math.round(monthlyPrice * 100) / 100,
|
|
66
|
+
yearlyPrice: Math.round(monthlyPrice * 12 * 100) / 100,
|
|
67
|
+
freeUsers,
|
|
68
|
+
paidUsers,
|
|
69
|
+
effectivePricePerUser: users > 0 ? Math.round((monthlyPrice / users) * 100) / 100 : 0,
|
|
70
|
+
tierIndex,
|
|
71
|
+
tierPricePerUser,
|
|
72
|
+
minimumApplied,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert seat price to Stripe-compatible amount in cents.
|
|
77
|
+
*/
|
|
78
|
+
export function seatPriceToCents(monthlyPrice) {
|
|
79
|
+
return Math.round(monthlyPrice * 100);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=seat-pricing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seat-pricing.js","sourceRoot":"","sources":["../../../src/shared/billing/seat-pricing.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,17 +11,19 @@ export declare const PLAN_HIERARCHY: Record<string, number>;
|
|
|
11
11
|
export declare const EXPIRY_WARNING_DAYS = 30;
|
|
12
12
|
/** Separator between HMAC and payload in the license key */
|
|
13
13
|
export declare const KEY_SEPARATOR = ".";
|
|
14
|
-
/**
|
|
15
|
-
export declare const
|
|
16
|
-
/** Length of the
|
|
17
|
-
export declare const
|
|
14
|
+
/** Hash algorithm for integrity check */
|
|
15
|
+
export declare const HASH_ALGORITHM = "sha256";
|
|
16
|
+
/** Length of the hash hex digest */
|
|
17
|
+
export declare const HASH_LENGTH = 64;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* License integrity model (MUI X style).
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* The hash is simply sha256(payload) — no secret key involved.
|
|
22
|
+
* This is an INTEGRITY check, not a security mechanism.
|
|
23
|
+
* Like MUI X and AG Grid, the license system relies on legal compliance,
|
|
24
|
+
* not technical protection. Anyone can generate a valid key.
|
|
25
|
+
*
|
|
26
|
+
* The value is in the ongoing updates, support, and legal right to use —
|
|
27
|
+
* not in preventing key generation.
|
|
25
28
|
*/
|
|
26
|
-
export declare const SIGNING_SECRET = "tetra-license-v1-soulbatical-bv-2026";
|
|
27
29
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/shared/license/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,oDAAoD;AACpD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAIjD,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,4DAA4D;AAC5D,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/shared/license/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,4FAA4F;AAC5F,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,oDAAoD;AACpD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAIjD,CAAC;AAEF,mDAAmD;AACnD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,4DAA4D;AAC5D,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,yCAAyC;AACzC,eAAO,MAAM,cAAc,WAAW,CAAC;AAEvC,oCAAoC;AACpC,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B;;;;;;;;;;GAUG"}
|
|
@@ -15,17 +15,19 @@ export const PLAN_HIERARCHY = {
|
|
|
15
15
|
export const EXPIRY_WARNING_DAYS = 30;
|
|
16
16
|
/** Separator between HMAC and payload in the license key */
|
|
17
17
|
export const KEY_SEPARATOR = '.';
|
|
18
|
-
/**
|
|
19
|
-
export const
|
|
20
|
-
/** Length of the
|
|
21
|
-
export const
|
|
18
|
+
/** Hash algorithm for integrity check */
|
|
19
|
+
export const HASH_ALGORITHM = 'sha256';
|
|
20
|
+
/** Length of the hash hex digest */
|
|
21
|
+
export const HASH_LENGTH = 64; // sha256 = 32 bytes = 64 hex chars
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* License integrity model (MUI X style).
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* The hash is simply sha256(payload) — no secret key involved.
|
|
26
|
+
* This is an INTEGRITY check, not a security mechanism.
|
|
27
|
+
* Like MUI X and AG Grid, the license system relies on legal compliance,
|
|
28
|
+
* not technical protection. Anyone can generate a valid key.
|
|
29
|
+
*
|
|
30
|
+
* The value is in the ongoing updates, support, and legal right to use —
|
|
31
|
+
* not in preventing key generation.
|
|
29
32
|
*/
|
|
30
|
-
export const SIGNING_SECRET = 'tetra-license-v1-soulbatical-bv-2026';
|
|
31
33
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/shared/license/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B,oDAAoD;AACpD,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,OAAO,EAAE,CAAC;IACV,GAAG,EAAE,CAAC;IACN,UAAU,EAAE,CAAC;CACd,CAAC;AAEF,mDAAmD;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC,
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/shared/license/constants.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAE7B,oDAAoD;AACpD,MAAM,CAAC,MAAM,cAAc,GAA2B;IACpD,OAAO,EAAE,CAAC;IACV,GAAG,EAAE,CAAC;IACN,UAAU,EAAE,CAAC;CACd,CAAC;AAEF,mDAAmD;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC;AAEjC,yCAAyC;AACzC,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC,oCAAoC;AACpC,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC,CAAC,mCAAmC;AAElE;;;;;;;;;;GAUG"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* License Key Generator
|
|
2
|
+
* License Key Generator & Decoder
|
|
3
3
|
*
|
|
4
4
|
* Generates self-validating license keys with embedded metadata.
|
|
5
|
-
* Format: {
|
|
5
|
+
* Format: {SHA256_HEX}{BASE64_PAYLOAD} (no separator, first 64 chars = hash)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Integrity model (MUI X style): hash = sha256(payload), no secret key.
|
|
8
|
+
* This is legal protection, not technical protection.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: generateLicenseKey is exported for internal/admin use only.
|
|
11
|
+
* Consumer projects should never need to generate keys — they only validate.
|
|
9
12
|
*
|
|
10
13
|
* @module @soulbatical/tetra-core/license
|
|
11
14
|
*/
|
|
@@ -33,11 +36,11 @@ export interface GenerateKeyOptions {
|
|
|
33
36
|
/**
|
|
34
37
|
* Generate a license key.
|
|
35
38
|
*
|
|
36
|
-
* @returns The license key string: {
|
|
39
|
+
* @returns The license key string: {SHA256_HASH}{BASE64_PAYLOAD}
|
|
37
40
|
*/
|
|
38
41
|
export declare function generateLicenseKey(options: GenerateKeyOptions): string;
|
|
39
42
|
/**
|
|
40
|
-
* Decode a license key without validating.
|
|
43
|
+
* Decode a license key without validating the hash.
|
|
41
44
|
* Returns null if the key format is invalid.
|
|
42
45
|
*/
|
|
43
46
|
export declare function decodeLicenseKey(key: string): LicensePayload | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../src/shared/license/generator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../src/shared/license/generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAG5F,MAAM,WAAW,kBAAkB;IACjC,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,iDAAiD;IACjD,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;IACvB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,CA0BtE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAiBnE"}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* License Key Generator
|
|
2
|
+
* License Key Generator & Decoder
|
|
3
3
|
*
|
|
4
4
|
* Generates self-validating license keys with embedded metadata.
|
|
5
|
-
* Format: {
|
|
5
|
+
* Format: {SHA256_HEX}{BASE64_PAYLOAD} (no separator, first 64 chars = hash)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Integrity model (MUI X style): hash = sha256(payload), no secret key.
|
|
8
|
+
* This is legal protection, not technical protection.
|
|
9
|
+
*
|
|
10
|
+
* NOTE: generateLicenseKey is exported for internal/admin use only.
|
|
11
|
+
* Consumer projects should never need to generate keys — they only validate.
|
|
9
12
|
*
|
|
10
13
|
* @module @soulbatical/tetra-core/license
|
|
11
14
|
*/
|
|
12
15
|
import crypto from 'node:crypto';
|
|
13
|
-
import { KEY_VERSION,
|
|
16
|
+
import { KEY_VERSION, HASH_ALGORITHM, HASH_LENGTH } from './constants.js';
|
|
14
17
|
/**
|
|
15
18
|
* Generate a license key.
|
|
16
19
|
*
|
|
17
|
-
* @returns The license key string: {
|
|
20
|
+
* @returns The license key string: {SHA256_HASH}{BASE64_PAYLOAD}
|
|
18
21
|
*/
|
|
19
22
|
export function generateLicenseKey(options) {
|
|
20
23
|
const payload = {
|
|
@@ -34,22 +37,21 @@ export function generateLicenseKey(options) {
|
|
|
34
37
|
throw new Error(`Invalid expiry date: ${options.expiryDate}`);
|
|
35
38
|
}
|
|
36
39
|
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
37
|
-
const
|
|
38
|
-
.
|
|
40
|
+
const hash = crypto
|
|
41
|
+
.createHash(HASH_ALGORITHM)
|
|
39
42
|
.update(payloadBase64)
|
|
40
43
|
.digest('hex');
|
|
41
|
-
return `${
|
|
44
|
+
return `${hash}${payloadBase64}`;
|
|
42
45
|
}
|
|
43
46
|
/**
|
|
44
|
-
* Decode a license key without validating.
|
|
47
|
+
* Decode a license key without validating the hash.
|
|
45
48
|
* Returns null if the key format is invalid.
|
|
46
49
|
*/
|
|
47
50
|
export function decodeLicenseKey(key) {
|
|
48
51
|
try {
|
|
49
|
-
|
|
50
|
-
if (separatorIndex === -1)
|
|
52
|
+
if (key.length <= HASH_LENGTH)
|
|
51
53
|
return null;
|
|
52
|
-
const payloadBase64 = key.slice(
|
|
54
|
+
const payloadBase64 = key.slice(HASH_LENGTH);
|
|
53
55
|
const json = Buffer.from(payloadBase64, 'base64url').toString('utf8');
|
|
54
56
|
const payload = JSON.parse(json);
|
|
55
57
|
// Basic shape validation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../src/shared/license/generator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../../src/shared/license/generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAuB1E;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA2B;IAC5D,MAAM,OAAO,GAAmB;QAC9B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC;QACtC,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,WAAW;QACnC,MAAM,EAAE,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;QAC9C,UAAU,EAAE,WAAW;QACvB,GAAG,CAAC,OAAO,CAAC,gBAAgB,IAAI,IAAI,IAAI,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC;QACvF,GAAG,CAAC,OAAO,CAAC,cAAc,IAAI,IAAI,IAAI,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC;QACjF,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,IAAI,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;KAC5E,CAAC;IAEF,yCAAyC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,wBAAwB,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACjF,MAAM,IAAI,GAAG,MAAM;SAChB,UAAU,CAAC,cAAc,CAAC;SAC1B,MAAM,CAAC,aAAa,CAAC;SACrB,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,OAAO,GAAG,IAAI,GAAG,aAAa,EAAE,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,IAAI,WAAW;YAAE,OAAO,IAAI,CAAC;QAE3C,MAAM,aAAa,GAAG,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACtE,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC;QAEnD,yBAAyB;QACzB,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YAChF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* License Module — Barrel Export
|
|
3
3
|
*
|
|
4
4
|
* Offline license key validation for Tetra Pro features.
|
|
5
|
-
*
|
|
6
|
-
* zero network calls.
|
|
5
|
+
* Integrity check via sha256 hash (MUI X model), self-validating keys,
|
|
6
|
+
* zero network calls. Legal compliance, not technical protection.
|
|
7
7
|
*
|
|
8
8
|
* @module @soulbatical/tetra-core/license
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/license/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,WAAW,EACX,aAAa,GACd,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/license/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,YAAY,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,YAAY,EACZ,WAAW,EACX,aAAa,GACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAGzD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG9G,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* License Module — Barrel Export
|
|
3
3
|
*
|
|
4
4
|
* Offline license key validation for Tetra Pro features.
|
|
5
|
-
*
|
|
6
|
-
* zero network calls.
|
|
5
|
+
* Integrity check via sha256 hash (MUI X model), self-validating keys,
|
|
6
|
+
* zero network calls. Legal compliance, not technical protection.
|
|
7
7
|
*
|
|
8
8
|
* @module @soulbatical/tetra-core/license
|
|
9
9
|
*/
|
|
10
|
-
// Generator
|
|
10
|
+
// Generator — exported for internal/admin/CLI use only.
|
|
11
|
+
// Consumer projects should use validateLicense, not generateLicenseKey.
|
|
11
12
|
export { generateLicenseKey, decodeLicenseKey } from './generator.js';
|
|
12
13
|
// Validator (primary API for consumer projects)
|
|
13
14
|
export { validateLicense, clearLicenseCache, isFeatureAvailable, validateLicenseUsage } from './validator.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/license/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/shared/license/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,wDAAwD;AACxD,wEAAwE;AACxE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGtE,gDAAgD;AAChD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE9G,YAAY;AACZ,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* License Key Validator
|
|
3
3
|
*
|
|
4
4
|
* Validates license keys offline — zero network calls.
|
|
5
|
-
*
|
|
5
|
+
* Integrity check via sha256 hash (MUI X model), then decode metadata.
|
|
6
|
+
* No signing secret — this is legal protection, not technical protection.
|
|
6
7
|
*
|
|
7
8
|
* @module @soulbatical/tetra-core/license
|
|
8
9
|
*/
|
|
@@ -24,13 +25,6 @@ export declare function clearLicenseCache(): void;
|
|
|
24
25
|
export declare function isFeatureAvailable(requiredPlan: string, requiredScope: LicenseScope, config?: LicenseConfig): boolean;
|
|
25
26
|
/**
|
|
26
27
|
* Validate license usage against org/user limits.
|
|
27
|
-
*
|
|
28
|
-
* Call this after validateLicense() to check if the current usage
|
|
29
|
-
* is within the license's limits. Returns a validation result with
|
|
30
|
-
* status 'org_limit_exceeded' or 'user_limit_exceeded' if over limit.
|
|
31
|
-
*
|
|
32
|
-
* If the license has no limits (maxOrganizations/maxUsersPerOrg undefined),
|
|
33
|
-
* this always returns valid.
|
|
34
28
|
*/
|
|
35
29
|
export declare function validateLicenseUsage(usage: LicenseUsage, config?: LicenseConfig): LicenseValidationResult;
|
|
36
30
|
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../src/shared/license/validator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../../src/shared/license/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAkB,YAAY,EAAE,uBAAuB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAarH;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,aAAa,GAAG,uBAAuB,CAwB/E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,YAAY,EAC3B,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAWT;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,MAAM,CAAC,EAAE,aAAa,GACrB,uBAAuB,CA2BzB"}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* License Key Validator
|
|
3
3
|
*
|
|
4
4
|
* Validates license keys offline — zero network calls.
|
|
5
|
-
*
|
|
5
|
+
* Integrity check via sha256 hash (MUI X model), then decode metadata.
|
|
6
|
+
* No signing secret — this is legal protection, not technical protection.
|
|
6
7
|
*
|
|
7
8
|
* @module @soulbatical/tetra-core/license
|
|
8
9
|
*/
|
|
9
10
|
import crypto from 'node:crypto';
|
|
10
|
-
import {
|
|
11
|
+
import { HASH_ALGORITHM, HASH_LENGTH, PLAN_HIERARCHY, EXPIRY_WARNING_DAYS, } from './constants.js';
|
|
11
12
|
import { decodeLicenseKey } from './generator.js';
|
|
12
13
|
/** Cached validation result to avoid re-validating on every call */
|
|
13
14
|
let cachedResult = null;
|
|
@@ -62,13 +63,6 @@ export function isFeatureAvailable(requiredPlan, requiredScope, config) {
|
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* Validate license usage against org/user limits.
|
|
65
|
-
*
|
|
66
|
-
* Call this after validateLicense() to check if the current usage
|
|
67
|
-
* is within the license's limits. Returns a validation result with
|
|
68
|
-
* status 'org_limit_exceeded' or 'user_limit_exceeded' if over limit.
|
|
69
|
-
*
|
|
70
|
-
* If the license has no limits (maxOrganizations/maxUsersPerOrg undefined),
|
|
71
|
-
* this always returns valid.
|
|
72
66
|
*/
|
|
73
67
|
export function validateLicenseUsage(usage, config) {
|
|
74
68
|
const baseResult = validateLicense(config);
|
|
@@ -97,23 +91,22 @@ export function validateLicenseUsage(usage, config) {
|
|
|
97
91
|
}
|
|
98
92
|
// ─── Internal ────────────────────────────────────────────────
|
|
99
93
|
function performValidation(key, config) {
|
|
100
|
-
// Step 1:
|
|
101
|
-
|
|
102
|
-
if (separatorIndex === -1) {
|
|
94
|
+
// Step 1: Check minimum length (64 char hash + at least 1 char payload)
|
|
95
|
+
if (key.length <= HASH_LENGTH) {
|
|
103
96
|
return {
|
|
104
97
|
valid: false,
|
|
105
98
|
status: 'invalid',
|
|
106
99
|
message: 'Invalid license key format.',
|
|
107
100
|
};
|
|
108
101
|
}
|
|
109
|
-
const
|
|
110
|
-
const payloadBase64 = key.slice(
|
|
111
|
-
// Step 2: Verify
|
|
112
|
-
const
|
|
113
|
-
.
|
|
102
|
+
const hashProvided = key.slice(0, HASH_LENGTH);
|
|
103
|
+
const payloadBase64 = key.slice(HASH_LENGTH);
|
|
104
|
+
// Step 2: Verify hash integrity (sha256, no secret — MUI X model)
|
|
105
|
+
const hashComputed = crypto
|
|
106
|
+
.createHash(HASH_ALGORITHM)
|
|
114
107
|
.update(payloadBase64)
|
|
115
108
|
.digest('hex');
|
|
116
|
-
if (
|
|
109
|
+
if (hashProvided !== hashComputed) {
|
|
117
110
|
return {
|
|
118
111
|
valid: false,
|
|
119
112
|
status: 'invalid',
|
|
@@ -143,7 +136,6 @@ function performValidation(key, config) {
|
|
|
143
136
|
const now = Date.now();
|
|
144
137
|
const daysRemaining = Math.floor((payload.expiry - now) / (1000 * 60 * 60 * 24));
|
|
145
138
|
if (payload.model === 'perpetual') {
|
|
146
|
-
// Perpetual: key is valid for all releases before expiry date
|
|
147
139
|
const releaseDate = config?.releaseDate
|
|
148
140
|
? new Date(config.releaseDate).getTime()
|
|
149
141
|
: now;
|
|
@@ -158,7 +150,6 @@ function performValidation(key, config) {
|
|
|
158
150
|
}
|
|
159
151
|
}
|
|
160
152
|
else {
|
|
161
|
-
// Subscription: must be currently active
|
|
162
153
|
if (now > payload.expiry) {
|
|
163
154
|
return {
|
|
164
155
|
valid: false,
|
|
@@ -177,7 +168,7 @@ function performValidation(key, config) {
|
|
|
177
168
|
daysRemaining,
|
|
178
169
|
message: `Licensed to ${payload.customer} (${payload.plan}). ${daysRemaining > EXPIRY_WARNING_DAYS ? '' : `Expires in ${daysRemaining} days.`}`.trim(),
|
|
179
170
|
};
|
|
180
|
-
// Log expiry warning
|
|
171
|
+
// Log expiry warning
|
|
181
172
|
if (!config?.silent && daysRemaining <= EXPIRY_WARNING_DAYS && daysRemaining > 0) {
|
|
182
173
|
console.warn(`[Tetra] License expires in ${daysRemaining} days. Renew at https://tetra.soulbatical.com/renew`);
|
|
183
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../../src/shared/license/validator.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../../src/shared/license/validator.ts"],"names":[],"mappings":"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"}
|