@auxiora/cloud 1.0.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/quotas.js ADDED
@@ -0,0 +1,87 @@
1
+ import { QuotaExceededError } from './types.js';
2
+ import { getPlanQuotas } from './plans.js';
3
+ /**
4
+ * QuotaEnforcer tracks usage against plan limits
5
+ * and throws QuotaExceededError when limits are breached.
6
+ */
7
+ export class QuotaEnforcer {
8
+ usage = new Map();
9
+ tenantManager;
10
+ constructor(options) {
11
+ this.tenantManager = options.tenantManager;
12
+ }
13
+ /**
14
+ * Check whether a metric increment would exceed the quota.
15
+ * Throws QuotaExceededError if the limit would be breached.
16
+ */
17
+ async check(tenantId, metric, increment = 1) {
18
+ const tenant = await this.tenantManager.get(tenantId);
19
+ const quotas = getPlanQuotas(tenant.plan);
20
+ const limit = quotas[metric];
21
+ // -1 means unlimited
22
+ if (limit === -1)
23
+ return;
24
+ const current = this.getUsage(tenantId, metric);
25
+ if (current + increment > limit) {
26
+ throw new QuotaExceededError(metric, limit, current + increment);
27
+ }
28
+ }
29
+ /**
30
+ * Record usage for a metric.
31
+ */
32
+ record(tenantId, metric, value = 1) {
33
+ let tenantUsage = this.usage.get(tenantId);
34
+ if (!tenantUsage) {
35
+ tenantUsage = new Map();
36
+ this.usage.set(tenantId, tenantUsage);
37
+ }
38
+ const current = tenantUsage.get(metric) ?? 0;
39
+ tenantUsage.set(metric, current + value);
40
+ return {
41
+ tenantId,
42
+ metric,
43
+ value: current + value,
44
+ timestamp: new Date().toISOString(),
45
+ };
46
+ }
47
+ /**
48
+ * Get current usage for a specific metric.
49
+ */
50
+ getUsage(tenantId, metric) {
51
+ return this.usage.get(tenantId)?.get(metric) ?? 0;
52
+ }
53
+ /**
54
+ * Get all usage for a tenant.
55
+ */
56
+ getAllUsage(tenantId) {
57
+ const tenantUsage = this.usage.get(tenantId);
58
+ if (!tenantUsage)
59
+ return {};
60
+ const result = {};
61
+ for (const [metric, value] of tenantUsage) {
62
+ result[metric] = value;
63
+ }
64
+ return result;
65
+ }
66
+ /**
67
+ * Reset usage for a tenant (e.g., at billing period start).
68
+ */
69
+ resetUsage(tenantId) {
70
+ this.usage.delete(tenantId);
71
+ }
72
+ /**
73
+ * Get usage as a percentage of the quota limit.
74
+ */
75
+ async getUsagePercentage(tenantId, metric) {
76
+ const tenant = await this.tenantManager.get(tenantId);
77
+ const quotas = getPlanQuotas(tenant.plan);
78
+ const limit = quotas[metric];
79
+ if (limit === -1)
80
+ return 0;
81
+ if (limit === 0)
82
+ return 100;
83
+ const current = this.getUsage(tenantId, metric);
84
+ return Math.round((current / limit) * 100);
85
+ }
86
+ }
87
+ //# sourceMappingURL=quotas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quotas.js","sourceRoot":"","sources":["../src/quotas.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { Tenant, TenantPlan, CloudConfig } from './types.js';
2
+ export interface TenantManagerOptions {
3
+ config: CloudConfig;
4
+ }
5
+ export declare class TenantManager {
6
+ private tenants;
7
+ private config;
8
+ constructor(options: TenantManagerOptions);
9
+ create(email: string, name: string, plan?: TenantPlan): Promise<Tenant>;
10
+ get(id: string): Promise<Tenant>;
11
+ getByEmail(email: string): Promise<Tenant | null>;
12
+ update(id: string, updates: Partial<Pick<Tenant, 'name' | 'email' | 'plan'>>): Promise<Tenant>;
13
+ suspend(id: string, reason: string): Promise<Tenant>;
14
+ reactivate(id: string): Promise<Tenant>;
15
+ delete(id: string): Promise<boolean>;
16
+ list(): Promise<Tenant[]>;
17
+ getQuotas(plan: TenantPlan): import("./types.js").TenantQuotas;
18
+ private persistTenant;
19
+ loadFromDisk(): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIlE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAc;gBAEhB,OAAO,EAAE,oBAAoB;IAInC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,UAAmB,GAAG,OAAO,CAAC,MAAM,CAAC;IAsB/E,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQhC,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IASjD,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAa9F,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYpD,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAWpC,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAI/B,SAAS,CAAC,IAAI,EAAE,UAAU;YAIZ,aAAa;IAMrB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;CAmBpC"}
package/dist/tenant.js ADDED
@@ -0,0 +1,121 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
+ import { TenantNotFoundError } from './types.js';
5
+ import { getPlanQuotas } from './plans.js';
6
+ export class TenantManager {
7
+ tenants = new Map();
8
+ config;
9
+ constructor(options) {
10
+ this.config = options.config;
11
+ }
12
+ async create(email, name, plan = 'free') {
13
+ const id = `tenant-${crypto.randomUUID()}`;
14
+ const dataDir = path.join(this.config.baseDataDir, id);
15
+ await fs.mkdir(dataDir, { recursive: true });
16
+ const tenant = {
17
+ id,
18
+ name,
19
+ email,
20
+ plan,
21
+ status: 'active',
22
+ dataDir,
23
+ createdAt: new Date().toISOString(),
24
+ updatedAt: new Date().toISOString(),
25
+ };
26
+ this.tenants.set(id, tenant);
27
+ await this.persistTenant(tenant);
28
+ return tenant;
29
+ }
30
+ async get(id) {
31
+ const tenant = this.tenants.get(id);
32
+ if (!tenant) {
33
+ throw new TenantNotFoundError(id);
34
+ }
35
+ return tenant;
36
+ }
37
+ async getByEmail(email) {
38
+ for (const tenant of this.tenants.values()) {
39
+ if (tenant.email === email) {
40
+ return tenant;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ async update(id, updates) {
46
+ const tenant = await this.get(id);
47
+ if (updates.name !== undefined)
48
+ tenant.name = updates.name;
49
+ if (updates.email !== undefined)
50
+ tenant.email = updates.email;
51
+ if (updates.plan !== undefined)
52
+ tenant.plan = updates.plan;
53
+ tenant.updatedAt = new Date().toISOString();
54
+ this.tenants.set(id, tenant);
55
+ await this.persistTenant(tenant);
56
+ return tenant;
57
+ }
58
+ async suspend(id, reason) {
59
+ const tenant = await this.get(id);
60
+ tenant.status = 'suspended';
61
+ tenant.suspendedAt = new Date().toISOString();
62
+ tenant.suspendReason = reason;
63
+ tenant.updatedAt = new Date().toISOString();
64
+ this.tenants.set(id, tenant);
65
+ await this.persistTenant(tenant);
66
+ return tenant;
67
+ }
68
+ async reactivate(id) {
69
+ const tenant = await this.get(id);
70
+ tenant.status = 'active';
71
+ tenant.suspendedAt = undefined;
72
+ tenant.suspendReason = undefined;
73
+ tenant.updatedAt = new Date().toISOString();
74
+ this.tenants.set(id, tenant);
75
+ await this.persistTenant(tenant);
76
+ return tenant;
77
+ }
78
+ async delete(id) {
79
+ const tenant = this.tenants.get(id);
80
+ if (!tenant)
81
+ return false;
82
+ tenant.status = 'deleted';
83
+ tenant.updatedAt = new Date().toISOString();
84
+ this.tenants.set(id, tenant);
85
+ await this.persistTenant(tenant);
86
+ return true;
87
+ }
88
+ async list() {
89
+ return Array.from(this.tenants.values()).filter(t => t.status !== 'deleted');
90
+ }
91
+ getQuotas(plan) {
92
+ return getPlanQuotas(plan);
93
+ }
94
+ async persistTenant(tenant) {
95
+ const tenantFile = path.join(tenant.dataDir, 'tenant.json');
96
+ await fs.mkdir(path.dirname(tenantFile), { recursive: true });
97
+ await fs.writeFile(tenantFile, JSON.stringify(tenant, null, 2), 'utf-8');
98
+ }
99
+ async loadFromDisk() {
100
+ try {
101
+ const entries = await fs.readdir(this.config.baseDataDir, { withFileTypes: true });
102
+ for (const entry of entries) {
103
+ if (entry.isDirectory() && entry.name.startsWith('tenant-')) {
104
+ const tenantFile = path.join(this.config.baseDataDir, entry.name, 'tenant.json');
105
+ try {
106
+ const content = await fs.readFile(tenantFile, 'utf-8');
107
+ const tenant = JSON.parse(content);
108
+ this.tenants.set(tenant.id, tenant);
109
+ }
110
+ catch {
111
+ // Skip corrupt tenant files
112
+ }
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ // Base data dir doesn't exist yet
118
+ }
119
+ }
120
+ }
121
+ //# sourceMappingURL=tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /** Tenant plan tiers */
2
+ export type TenantPlan = 'free' | 'pro' | 'team' | 'enterprise';
3
+ /** Tenant status */
4
+ export type TenantStatus = 'active' | 'suspended' | 'pending' | 'deleted';
5
+ /** A cloud tenant */
6
+ export interface Tenant {
7
+ id: string;
8
+ name: string;
9
+ email: string;
10
+ plan: TenantPlan;
11
+ status: TenantStatus;
12
+ stripeCustomerId?: string;
13
+ stripeSubscriptionId?: string;
14
+ dataDir: string;
15
+ createdAt: string;
16
+ updatedAt: string;
17
+ suspendedAt?: string;
18
+ suspendReason?: string;
19
+ }
20
+ /** Quota limits for a given plan */
21
+ export interface TenantQuotas {
22
+ maxMessages: number;
23
+ maxSessions: number;
24
+ maxStorageMb: number;
25
+ maxPlugins: number;
26
+ maxBehaviors: number;
27
+ maxChannels: number;
28
+ maxAgents: number;
29
+ }
30
+ /** A single usage record */
31
+ export interface UsageRecord {
32
+ tenantId: string;
33
+ metric: string;
34
+ value: number;
35
+ timestamp: string;
36
+ }
37
+ /** Billing information */
38
+ export interface BillingInfo {
39
+ tenantId: string;
40
+ plan: TenantPlan;
41
+ stripeCustomerId?: string;
42
+ stripeSubscriptionId?: string;
43
+ currentPeriodStart?: string;
44
+ currentPeriodEnd?: string;
45
+ paymentMethodLast4?: string;
46
+ paymentMethodBrand?: string;
47
+ }
48
+ /** Cloud-specific configuration */
49
+ export interface CloudConfig {
50
+ enabled: boolean;
51
+ baseDataDir: string;
52
+ jwtSecret: string;
53
+ stripeSecretKey?: string;
54
+ stripeWebhookSecret?: string;
55
+ domain: string;
56
+ }
57
+ /** Plan definition with pricing */
58
+ export interface PlanDefinition {
59
+ plan: TenantPlan;
60
+ name: string;
61
+ priceMonthly: number;
62
+ priceYearly: number;
63
+ quotas: TenantQuotas;
64
+ features: string[];
65
+ }
66
+ /** Stripe SDK interface for dependency injection */
67
+ export interface StripeClient {
68
+ createCustomer(email: string, name: string): Promise<{
69
+ id: string;
70
+ }>;
71
+ createSubscription(customerId: string, priceId: string): Promise<{
72
+ id: string;
73
+ status: string;
74
+ }>;
75
+ cancelSubscription(subscriptionId: string): Promise<void>;
76
+ getInvoices(customerId: string): Promise<Array<{
77
+ id: string;
78
+ amount: number;
79
+ status: string;
80
+ created: string;
81
+ }>>;
82
+ createPaymentIntent(customerId: string, amount: number): Promise<{
83
+ clientSecret: string;
84
+ }>;
85
+ }
86
+ /** Quota exceeded error */
87
+ export declare class QuotaExceededError extends Error {
88
+ readonly metric: string;
89
+ readonly limit: number;
90
+ readonly current: number;
91
+ constructor(metric: string, limit: number, current: number);
92
+ }
93
+ /** Tenant not found error */
94
+ export declare class TenantNotFoundError extends Error {
95
+ constructor(tenantId: string);
96
+ }
97
+ /** Tenant suspended error */
98
+ export declare class TenantSuspendedError extends Error {
99
+ constructor(tenantId: string);
100
+ }
101
+ //# 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,28 @@
1
+ /** Quota exceeded error */
2
+ export class QuotaExceededError extends Error {
3
+ metric;
4
+ limit;
5
+ current;
6
+ constructor(metric, limit, current) {
7
+ super(`Quota exceeded for ${metric}: ${current}/${limit}`);
8
+ this.metric = metric;
9
+ this.limit = limit;
10
+ this.current = current;
11
+ this.name = 'QuotaExceededError';
12
+ }
13
+ }
14
+ /** Tenant not found error */
15
+ export class TenantNotFoundError extends Error {
16
+ constructor(tenantId) {
17
+ super(`Tenant not found: ${tenantId}`);
18
+ this.name = 'TenantNotFoundError';
19
+ }
20
+ }
21
+ /** Tenant suspended error */
22
+ export class TenantSuspendedError extends Error {
23
+ constructor(tenantId) {
24
+ super(`Tenant is suspended: ${tenantId}`);
25
+ this.name = 'TenantSuspendedError';
26
+ }
27
+ }
28
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAuFA,2BAA2B;AAC3B,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAEzB;IACA;IACA;IAHlB,YACkB,MAAc,EACd,KAAa,EACb,OAAe;QAE/B,KAAK,CAAC,sBAAsB,MAAM,KAAK,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC;QAJ3C,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAQ;QACb,YAAO,GAAP,OAAO,CAAQ;QAG/B,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,6BAA6B;AAC7B,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,QAAgB;QAC1B,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,6BAA6B;AAC7B,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,QAAgB;QAC1B,KAAK,CAAC,wBAAwB,QAAQ,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@auxiora/cloud",
3
+ "version": "1.0.0",
4
+ "description": "Multi-tenant cloud layer with isolation, billing, and quotas",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@auxiora/core": "1.0.0",
16
+ "@auxiora/logger": "1.0.0",
17
+ "@auxiora/audit": "1.0.0"
18
+ },
19
+ "engines": {
20
+ "node": ">=22.0.0"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "clean": "rm -rf dist",
25
+ "typecheck": "tsc --noEmit"
26
+ }
27
+ }
package/src/billing.ts ADDED
@@ -0,0 +1,142 @@
1
+ import type { BillingInfo, StripeClient, TenantPlan } from './types.js';
2
+ import type { TenantManager } from './tenant.js';
3
+ import { getPlanDefinition } from './plans.js';
4
+
5
+ export interface BillingManagerOptions {
6
+ tenantManager: TenantManager;
7
+ stripe?: StripeClient;
8
+ }
9
+
10
+ /**
11
+ * BillingManager handles Stripe integration for subscription management.
12
+ * Uses dependency injection for the Stripe SDK to enable easy testing.
13
+ */
14
+ export class BillingManager {
15
+ private tenantManager: TenantManager;
16
+ private stripe?: StripeClient;
17
+
18
+ constructor(options: BillingManagerOptions) {
19
+ this.tenantManager = options.tenantManager;
20
+ this.stripe = options.stripe;
21
+ }
22
+
23
+ /**
24
+ * Create a Stripe customer for a tenant.
25
+ */
26
+ async createCustomer(tenantId: string): Promise<string> {
27
+ if (!this.stripe) {
28
+ throw new Error('Stripe not configured');
29
+ }
30
+
31
+ const tenant = await this.tenantManager.get(tenantId);
32
+ const customer = await this.stripe.createCustomer(tenant.email, tenant.name);
33
+
34
+ await this.tenantManager.update(tenantId, {} as any);
35
+ // Store stripeCustomerId directly
36
+ const updated = await this.tenantManager.get(tenantId);
37
+ (updated as any).stripeCustomerId = customer.id;
38
+
39
+ return customer.id;
40
+ }
41
+
42
+ /**
43
+ * Create a subscription for a tenant.
44
+ */
45
+ async createSubscription(tenantId: string, plan: TenantPlan): Promise<{ subscriptionId: string; status: string }> {
46
+ if (!this.stripe) {
47
+ throw new Error('Stripe not configured');
48
+ }
49
+
50
+ const tenant = await this.tenantManager.get(tenantId);
51
+ if (!tenant.stripeCustomerId) {
52
+ throw new Error('No Stripe customer ID. Call createCustomer first.');
53
+ }
54
+
55
+ const planDef = getPlanDefinition(plan);
56
+ const priceId = `price_${plan}_monthly`;
57
+
58
+ const subscription = await this.stripe.createSubscription(tenant.stripeCustomerId, priceId);
59
+
60
+ // Update tenant plan
61
+ await this.tenantManager.update(tenantId, { plan });
62
+
63
+ return {
64
+ subscriptionId: subscription.id,
65
+ status: subscription.status,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Cancel a tenant's subscription.
71
+ */
72
+ async cancelSubscription(tenantId: string): Promise<void> {
73
+ if (!this.stripe) {
74
+ throw new Error('Stripe not configured');
75
+ }
76
+
77
+ const tenant = await this.tenantManager.get(tenantId);
78
+ if (!tenant.stripeSubscriptionId) {
79
+ throw new Error('No active subscription');
80
+ }
81
+
82
+ await this.stripe.cancelSubscription(tenant.stripeSubscriptionId);
83
+ await this.tenantManager.update(tenantId, { plan: 'free' });
84
+ }
85
+
86
+ /**
87
+ * Get billing info for a tenant.
88
+ */
89
+ async getBillingInfo(tenantId: string): Promise<BillingInfo> {
90
+ const tenant = await this.tenantManager.get(tenantId);
91
+
92
+ return {
93
+ tenantId,
94
+ plan: tenant.plan,
95
+ stripeCustomerId: tenant.stripeCustomerId,
96
+ stripeSubscriptionId: tenant.stripeSubscriptionId,
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Get invoices for a tenant.
102
+ */
103
+ async getInvoices(tenantId: string): Promise<Array<{ id: string; amount: number; status: string; created: string }>> {
104
+ if (!this.stripe) {
105
+ return [];
106
+ }
107
+
108
+ const tenant = await this.tenantManager.get(tenantId);
109
+ if (!tenant.stripeCustomerId) {
110
+ return [];
111
+ }
112
+
113
+ return this.stripe.getInvoices(tenant.stripeCustomerId);
114
+ }
115
+
116
+ /**
117
+ * Handle a Stripe webhook event.
118
+ */
119
+ async handleWebhookEvent(eventType: string, data: Record<string, unknown>): Promise<void> {
120
+ switch (eventType) {
121
+ case 'customer.subscription.deleted': {
122
+ const customerId = data.customer as string;
123
+ // Find tenant by customer ID and downgrade
124
+ const tenants = await this.tenantManager.list();
125
+ const tenant = tenants.find(t => t.stripeCustomerId === customerId);
126
+ if (tenant) {
127
+ await this.tenantManager.update(tenant.id, { plan: 'free' });
128
+ }
129
+ break;
130
+ }
131
+ case 'invoice.payment_failed': {
132
+ const customerId = data.customer as string;
133
+ const tenants = await this.tenantManager.list();
134
+ const tenant = tenants.find(t => t.stripeCustomerId === customerId);
135
+ if (tenant) {
136
+ await this.tenantManager.suspend(tenant.id, 'Payment failed');
137
+ }
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export { TenantManager, type TenantManagerOptions } from './tenant.js';
2
+ export { TenantIsolation, type TenantContext, type IsolationOptions } from './isolation.js';
3
+ export { QuotaEnforcer, type QuotaEnforcerOptions } from './quotas.js';
4
+ export { BillingManager, type BillingManagerOptions } from './billing.js';
5
+ export { getPlanDefinition, getPlanQuotas, getAllPlans, isValidPlan } from './plans.js';
6
+ export type {
7
+ Tenant,
8
+ TenantPlan,
9
+ TenantStatus,
10
+ TenantQuotas,
11
+ UsageRecord,
12
+ BillingInfo,
13
+ CloudConfig,
14
+ PlanDefinition,
15
+ StripeClient,
16
+ } from './types.js';
17
+ export {
18
+ QuotaExceededError,
19
+ TenantNotFoundError,
20
+ TenantSuspendedError,
21
+ } from './types.js';
@@ -0,0 +1,69 @@
1
+ import type { Tenant, CloudConfig } from './types.js';
2
+ import { TenantNotFoundError, TenantSuspendedError } from './types.js';
3
+ import type { TenantManager } from './tenant.js';
4
+
5
+ export interface TenantContext {
6
+ tenantId: string;
7
+ tenant: Tenant;
8
+ }
9
+
10
+ export interface IsolationOptions {
11
+ tenantManager: TenantManager;
12
+ config: CloudConfig;
13
+ }
14
+
15
+ /**
16
+ * TenantIsolation provides middleware-like extraction of tenant context
17
+ * from JWT tokens and scoping of all operations to the tenant's data.
18
+ */
19
+ export class TenantIsolation {
20
+ private tenantManager: TenantManager;
21
+
22
+ constructor(options: IsolationOptions) {
23
+ this.tenantManager = options.tenantManager;
24
+ }
25
+
26
+ /**
27
+ * Extract tenant context from a JWT payload.
28
+ * Validates the tenant exists and is active.
29
+ */
30
+ async extractTenant(jwtPayload: { tenantId?: string; sub?: string }): Promise<TenantContext> {
31
+ const tenantId = jwtPayload.tenantId || jwtPayload.sub;
32
+ if (!tenantId) {
33
+ throw new TenantNotFoundError('unknown');
34
+ }
35
+
36
+ const tenant = await this.tenantManager.get(tenantId);
37
+
38
+ if (tenant.status === 'suspended') {
39
+ throw new TenantSuspendedError(tenantId);
40
+ }
41
+
42
+ if (tenant.status === 'deleted') {
43
+ throw new TenantNotFoundError(tenantId);
44
+ }
45
+
46
+ return { tenantId, tenant };
47
+ }
48
+
49
+ /**
50
+ * Scope a file path to the tenant's data directory.
51
+ * Prevents path traversal outside the tenant's sandbox.
52
+ */
53
+ scopePath(tenant: Tenant, relativePath: string): string {
54
+ const resolved = new URL(relativePath, `file://${tenant.dataDir}/`).pathname;
55
+
56
+ if (!resolved.startsWith(tenant.dataDir)) {
57
+ throw new Error('Path traversal attempt detected');
58
+ }
59
+
60
+ return resolved;
61
+ }
62
+
63
+ /**
64
+ * Validate that a tenant can access a given resource.
65
+ */
66
+ validateAccess(context: TenantContext, resourceTenantId: string): boolean {
67
+ return context.tenantId === resourceTenantId;
68
+ }
69
+ }