fase(17): licensing module with RSA validation

This commit is contained in:
debian
2026-03-08 05:20:54 -04:00
parent 1f1678af17
commit 5a28270dc9
45 changed files with 1789 additions and 48 deletions

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LicenseService = void 0;
const Result_1 = require("../../../shared/domain/Result");
const License_1 = require("../domain/entities/License");
class LicenseService {
constructor(validator) {
this.validator = validator;
this.currentLicense = License_1.License.createFree();
}
getCurrentLicense() {
return this.currentLicense;
}
async activate(licenseKey) {
const result = await this.validator.validate(licenseKey);
if ((0, Result_1.isErr)(result))
return result;
this.currentLicense = result.value;
return result;
}
hasFeature(feature) {
return this.currentLicense.hasFeature(feature);
}
getStatus() {
const license = this.currentLicense;
return {
plan: license.plan.toString(),
organizationName: license.organizationName,
email: license.email,
issuedAt: license.issuedAt.toISOString(),
expiresAt: license.expiresAt?.toISOString() ?? null,
isValid: license.isValid,
features: license.getEntitlements().toArray(),
};
}
}
exports.LicenseService = LicenseService;

View File

@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.License = void 0;
const Entity_1 = require("../../../../shared/domain/Entity");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const LicensePlan_1 = require("../value-objects/LicensePlan");
const FeatureEntitlement_1 = require("../value-objects/FeatureEntitlement");
class License extends Entity_1.Entity {
static createFree() {
return new License({
plan: LicensePlan_1.LicensePlan.free(),
organizationName: 'Community',
email: '',
expiresAt: null,
issuedAt: new Date(),
signature: 'free',
rawKey: 'free',
}, UniqueId_1.UniqueId.create());
}
static reconstitute(props, id) {
return new License(props, id);
}
get plan() { return this.props.plan; }
get organizationName() { return this.props.organizationName; }
get email() { return this.props.email; }
get expiresAt() { return this.props.expiresAt; }
get issuedAt() { return this.props.issuedAt; }
get signature() { return this.props.signature; }
get rawKey() { return this.props.rawKey; }
get isExpired() {
if (!this.props.expiresAt)
return false;
return this.props.expiresAt < new Date();
}
get isValid() {
return !this.isExpired;
}
getEntitlements() {
if (!this.isValid)
return FeatureEntitlement_1.FeatureEntitlement.forFeatures(FeatureEntitlement_1.FREE_FEATURES);
if (this.props.plan.isEnterprise)
return FeatureEntitlement_1.FeatureEntitlement.forFeatures(FeatureEntitlement_1.ENTERPRISE_FEATURES);
if (this.props.plan.isPro)
return FeatureEntitlement_1.FeatureEntitlement.forFeatures(FeatureEntitlement_1.PRO_FEATURES);
return FeatureEntitlement_1.FeatureEntitlement.forFeatures(FeatureEntitlement_1.FREE_FEATURES);
}
hasFeature(feature) {
return this.getEntitlements().has(feature);
}
}
exports.License = License;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FeatureEntitlement = exports.ENTERPRISE_FEATURES = exports.PRO_FEATURES = exports.FREE_FEATURES = void 0;
exports.FREE_FEATURES = [
'exploration:basic',
'findings:basic',
'findings:export',
'reports:basic',
'auth:apikeys',
];
exports.PRO_FEATURES = [
...exports.FREE_FEATURES,
'exploration:scheduled',
'reports:pdf',
'integrations:webhook',
'integrations:slack',
'integrations:github',
'integrations:jira',
];
exports.ENTERPRISE_FEATURES = [
...exports.PRO_FEATURES,
'auth:sso',
'auth:ldap',
'audit:logs',
'branding:whitelabel',
'data:retention',
'infra:postgres',
];
class FeatureEntitlement {
constructor(features) {
this.features = features;
}
static forFeatures(features) {
return new FeatureEntitlement(new Set(features));
}
has(feature) {
return this.features.has(feature);
}
toArray() {
return Array.from(this.features);
}
}
exports.FeatureEntitlement = FeatureEntitlement;

View File

@@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LicensePlan = void 0;
class LicensePlan {
constructor(value) {
this.value = value;
}
static free() { return new LicensePlan('free'); }
static pro() { return new LicensePlan('pro'); }
static enterprise() { return new LicensePlan('enterprise'); }
static fromString(value) {
if (value === 'free' || value === 'pro' || value === 'enterprise') {
return new LicensePlan(value);
}
throw new Error(`Invalid license plan: ${value}`);
}
get isFree() { return this.value === 'free'; }
get isPro() { return this.value === 'pro'; }
get isEnterprise() { return this.value === 'enterprise'; }
toString() { return this.value; }
equals(other) { return this.value === other.value; }
}
exports.LicensePlan = LicensePlan;

20
dist/modules/licensing/index.js vendored Normal file
View File

@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LicensingController = exports.requireFeature = exports.RSALicenseValidator = exports.LicenseService = exports.ENTERPRISE_FEATURES = exports.PRO_FEATURES = exports.FREE_FEATURES = exports.FeatureEntitlement = exports.LicensePlan = exports.License = void 0;
var License_1 = require("./domain/entities/License");
Object.defineProperty(exports, "License", { enumerable: true, get: function () { return License_1.License; } });
var LicensePlan_1 = require("./domain/value-objects/LicensePlan");
Object.defineProperty(exports, "LicensePlan", { enumerable: true, get: function () { return LicensePlan_1.LicensePlan; } });
var FeatureEntitlement_1 = require("./domain/value-objects/FeatureEntitlement");
Object.defineProperty(exports, "FeatureEntitlement", { enumerable: true, get: function () { return FeatureEntitlement_1.FeatureEntitlement; } });
Object.defineProperty(exports, "FREE_FEATURES", { enumerable: true, get: function () { return FeatureEntitlement_1.FREE_FEATURES; } });
Object.defineProperty(exports, "PRO_FEATURES", { enumerable: true, get: function () { return FeatureEntitlement_1.PRO_FEATURES; } });
Object.defineProperty(exports, "ENTERPRISE_FEATURES", { enumerable: true, get: function () { return FeatureEntitlement_1.ENTERPRISE_FEATURES; } });
var LicenseService_1 = require("./application/LicenseService");
Object.defineProperty(exports, "LicenseService", { enumerable: true, get: function () { return LicenseService_1.LicenseService; } });
var RSALicenseValidator_1 = require("./infrastructure/validators/RSALicenseValidator");
Object.defineProperty(exports, "RSALicenseValidator", { enumerable: true, get: function () { return RSALicenseValidator_1.RSALicenseValidator; } });
var FeatureGateMiddleware_1 = require("./infrastructure/middleware/FeatureGateMiddleware");
Object.defineProperty(exports, "requireFeature", { enumerable: true, get: function () { return FeatureGateMiddleware_1.requireFeature; } });
var LicensingController_1 = require("./infrastructure/http/LicensingController");
Object.defineProperty(exports, "LicensingController", { enumerable: true, get: function () { return LicensingController_1.LicensingController; } });

View File

@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LicensingController = void 0;
const express_1 = require("express");
const Result_1 = require("../../../../shared/domain/Result");
class LicensingController {
constructor(licenseService) {
this.licenseService = licenseService;
this.router = (0, express_1.Router)();
this.registerRoutes();
}
registerRoutes() {
this.router.get('/status', this.getStatus.bind(this));
this.router.post('/activate', this.activate.bind(this));
}
getStatus(_req, res) {
res.json(this.licenseService.getStatus());
}
async activate(req, res) {
const { licenseKey } = req.body;
if (!licenseKey || typeof licenseKey !== 'string') {
res.status(400).json({ error: 'licenseKey is required' });
return;
}
const result = await this.licenseService.activate(licenseKey.trim());
if ((0, Result_1.isErr)(result)) {
res.status(422).json({ error: result.error });
return;
}
res.json({
message: 'License activated successfully',
license: this.licenseService.getStatus(),
});
}
}
exports.LicensingController = LicensingController;

View File

@@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.requireFeature = requireFeature;
function requireFeature(licenseService, feature) {
return (_req, res, next) => {
if (!licenseService.hasFeature(feature)) {
res.status(403).json({
error: 'Feature not available',
feature,
plan: licenseService.getCurrentLicense().plan.toString(),
message: `This feature requires a higher license plan. Current plan: ${licenseService.getCurrentLicense().plan.toString()}`,
});
return;
}
next();
};
}

View File

@@ -0,0 +1,77 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RSALicenseValidator = void 0;
const crypto_1 = __importDefault(require("crypto"));
const Result_1 = require("../../../../shared/domain/Result");
const UniqueId_1 = require("../../../../shared/domain/UniqueId");
const License_1 = require("../../domain/entities/License");
const LicensePlan_1 = require("../../domain/value-objects/LicensePlan");
// Public key used to verify license signatures.
// The corresponding private key is kept secret (used only by generate-license.ts).
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzHPZe5TNJF
EhkFwUEkMvbzXuRSxW98hGxMgrHPKGLJgNw0qFsLQmhDSmVvnrwYE2vCy2Dgm7Qj
7WKFqbZFkVDe8cROZ9K7rQmn0BqckmJbkm2SJnzYL9e9z6b5R8r5w2r5Q2HZFN7
6B3dKCHWHxhyE3N8MCJSN7qBZ7kX8fJqBwBxQL6bZbGP2O5bXrZpFw3xKyGJ5t
vZ9eTuD4JhKJbZbGJ3Q5Q5nNbm3nXY5z9WbBxFbRLYGJbQ7E8mSYnKJZkJzYM
TmOxJbKtJz5mJ9Q7rBxBxLYGJmQtZmKtXZ5t9WbBxFbRLYGJbQ7E8mSYnKJZk
JwIDAQAB
-----END PUBLIC KEY-----`;
class RSALicenseValidator {
constructor(publicKeyPem) {
const pem = publicKeyPem ?? PUBLIC_KEY;
this.publicKey = crypto_1.default.createPublicKey(pem);
}
async validate(licenseKey) {
try {
// License key format: base64(payload_json).base64(signature)
const parts = licenseKey.trim().split('.');
if (parts.length !== 2) {
return (0, Result_1.Err)('Invalid license key format');
}
const [payloadB64, signatureB64] = parts;
let payloadJson;
let rawPayload;
try {
payloadJson = Buffer.from(payloadB64, 'base64').toString('utf-8');
rawPayload = JSON.parse(payloadJson);
}
catch {
return (0, Result_1.Err)('Invalid license key: cannot decode payload');
}
const signature = Buffer.from(signatureB64, 'base64');
const isValid = crypto_1.default.verify('sha256', Buffer.from(payloadJson, 'utf-8'), this.publicKey, signature);
if (!isValid) {
return (0, Result_1.Err)('Invalid license key: signature verification failed');
}
let plan;
try {
plan = LicensePlan_1.LicensePlan.fromString(rawPayload.plan);
}
catch {
return (0, Result_1.Err)(`Invalid plan in license: ${rawPayload.plan}`);
}
const expiresAt = rawPayload.expiresAt ? new Date(rawPayload.expiresAt) : null;
if (expiresAt && expiresAt < new Date()) {
return (0, Result_1.Err)('License has expired');
}
const license = License_1.License.reconstitute({
plan,
organizationName: rawPayload.organizationName,
email: rawPayload.email,
issuedAt: new Date(rawPayload.issuedAt),
expiresAt,
signature: signatureB64,
rawKey: licenseKey,
}, UniqueId_1.UniqueId.create());
return (0, Result_1.Ok)(license);
}
catch (err) {
return (0, Result_1.Err)(`License validation error: ${String(err)}`);
}
}
}
exports.RSALicenseValidator = RSALicenseValidator;