prisma-zod-generator
Version:
Prisma 2+ generator to emit Zod schemas from your Prisma schema
446 lines • 17.8 kB
JavaScript
;
/**
* PZG Pro License Validation
*
* Simple offline-capable license validation with local caching
* Only checks license when Pro features are actually used
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.describePlan = describePlan;
exports.validateLicense = validateLicense;
exports.hasFeature = hasFeature;
exports.requireFeature = requireFeature;
exports.getLicenseStatus = getLicenseStatus;
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const os_1 = require("os");
const path_1 = require("path");
const businessSecurity_1 = require("./utils/businessSecurity");
const errorHandling_1 = require("./utils/errorHandling");
function describePlan(plan) {
switch (plan) {
case 'starter':
return 'Starter';
case 'professional':
return 'Professional';
case 'business':
return 'Business';
case 'enterprise':
return 'Enterprise';
}
}
const CACHE_DIR = (0, path_1.join)((0, os_1.homedir)(), '.cache', 'pzg');
const CACHE_FILE = (0, path_1.join)(CACHE_DIR, 'license.json');
const CACHE_DURATION = 30 * 24 * 60 * 60 * 1000; // 30 days
const LICENSE_KEY_VERSION = 'v2';
const LICENSE_KEY_PREFIX = `pzg_${LICENSE_KEY_VERSION}_`;
const MIN_SIGNATURE_LENGTH = 43;
const MAX_LICENSE_AGE_MS = 365 * 24 * 60 * 60 * 1000; // 1 year max license age for replay protection
// Public key used to verify license signatures (Ed25519)
const DEFAULT_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAwRNEnFQJgBdNnwvnTTAPySp223shjXfioII2qMkqBFQ=
-----END PUBLIC KEY-----`;
function getLicensePublicKey() {
const configured = process.env.PZG_LICENSE_PUBLIC_KEY;
if (configured && configured.trim().length > 0) {
// Allow newline placeholders in env variables
return configured.replace(/\\n/g, '\n');
}
return DEFAULT_PUBLIC_KEY_PEM;
}
function verifyLicenseSignature(encodedData, signature) {
try {
const publicKey = getLicensePublicKey();
const dataBuffer = Buffer.from(encodedData, 'base64url');
const signatureBuffer = Buffer.from(signature, 'base64url');
return (0, crypto_1.verify)(null, dataBuffer, publicKey, signatureBuffer);
}
catch {
return false;
}
}
/**
* Detect if pro features have been tampered with (de-obfuscated)
*
* Checks for signs that obfuscated code has been modified:
* - Readable class names (should be obfuscated to hex)
* - Missing obfuscation markers (pzg_ prefix)
* - Suspicious code patterns
*/
function detectTampering() {
try {
// Skip tampering detection in development mode
if (process.env.NODE_ENV === 'development' || process.env.PZG_DEV_MODE === 'true') {
return false;
}
// Try to resolve pro index module
let proIndexPath;
try {
proIndexPath = require.resolve('./pro/index.js');
}
catch {
// Pro module not found (probably core-only installation)
return false;
}
// Read pro module code
const proCode = (0, fs_1.readFileSync)(proIndexPath, 'utf8');
// Check for obfuscation markers
const obfuscationIndicators = {
// Should have hexadecimal identifiers with pzg_ prefix
hasHexIdentifiers: /pzg_0x[0-9a-f]{4,}/.test(proCode),
// Should NOT have readable class names (sign of de-obfuscation)
hasReadableClasses: /class\s+(PoliciesGenerator|ServerActionsGenerator|DriftGuardGenerator|FormUXGenerator)/.test(proCode),
// Should NOT have readable function names for pro features
hasReadableFunctions: /function\s+(generatePolicies|generateServerActions|generateDriftGuard)/.test(proCode),
// Should have string array indicators
hasStringArray: /_0x[0-9a-f]+\[/.test(proCode),
};
// Tampering detected if:
// 1. No hex identifiers (should be obfuscated)
// 2. Has readable class/function names (de-obfuscated)
const isTampered = !obfuscationIndicators.hasHexIdentifiers ||
obfuscationIndicators.hasReadableClasses ||
obfuscationIndicators.hasReadableFunctions;
if (isTampered) {
// Log tampering indicators for debugging (in non-production)
if (process.env.NODE_ENV !== 'production') {
console.warn('[PZG] Tampering indicators:', obfuscationIndicators);
}
}
return isTampered;
}
catch (error) {
// If we can't check (e.g., file system issues), assume no tampering
// Better to err on the side of functionality
return false;
}
}
/**
* Report tampering violation
*
* Logs the violation and notifies the license server (if configured)
*/
function reportTampering() {
try {
// Log to console
console.error('\n⚠️ PZG Pro Code Tampering Detected');
console.error(' Pro features appear to have been modified or de-obfuscated.');
console.error(' This violates the PZG Pro Commercial License Agreement.');
console.error(' Please reinstall: npm install prisma-zod-generator@latest\n');
// In production, could send telemetry to license server
if (process.env.PZG_REPORT_VIOLATIONS === 'true' && process.env.PZG_LICENSE_KEY) {
// Future: Send anonymous violation report to license server
// This helps track widespread abuse without collecting PII
}
}
catch {
// Silently fail if reporting fails
}
}
/**
* Get license key from environment
*/
function getLicenseKey() {
return process.env.PZG_LICENSE_KEY || null;
}
/**
* Read cached license if valid
*/
function readCachedLicense() {
try {
if (!(0, fs_1.existsSync)(CACHE_FILE))
return null;
const cached = JSON.parse((0, fs_1.readFileSync)(CACHE_FILE, 'utf8'));
const now = Date.now();
// Check if cache is still valid
if (now - cached.cachedAt < cached.validFor) {
return { ...cached.info, cached: true };
}
return null;
}
catch {
return null;
}
}
/**
* Cache license info locally
*/
function cacheLicense(info) {
try {
if (!(0, fs_1.existsSync)(CACHE_DIR)) {
(0, fs_1.mkdirSync)(CACHE_DIR, { recursive: true });
}
const cached = {
info,
cachedAt: Date.now(),
validFor: CACHE_DURATION,
};
(0, fs_1.writeFileSync)(CACHE_FILE, JSON.stringify(cached, null, 2));
}
catch {
// Silently fail cache writes
}
}
/**
* Validate license key with cryptographic verification
* Implements secure license validation with signature checking
*/
function extractSignedLicensePayload(key) {
if (!key.startsWith(LICENSE_KEY_PREFIX)) {
return null;
}
const body = key.slice(LICENSE_KEY_PREFIX.length);
let attemptedSignatureVerification = false;
for (let separatorIndex = body.lastIndexOf('_'); separatorIndex > 0; separatorIndex = body.lastIndexOf('_', separatorIndex - 1)) {
const encodedData = body.slice(0, separatorIndex);
const signature = body.slice(separatorIndex + 1);
if (!encodedData || signature.length < MIN_SIGNATURE_LENGTH) {
continue;
}
attemptedSignatureVerification = true;
if (verifyLicenseSignature(encodedData, signature)) {
return { encodedData, signature };
}
}
if (attemptedSignatureVerification) {
throw new errorHandling_1.LicenseError('Invalid PZG Pro license key. Please check your license key.\n' +
'Get support at: https://github.com/omar-dulaimi/prisma-zod-generator/issues', { reason: 'signature_verification_failed' });
}
return null;
}
async function validateLicenseRemote(key) {
try {
// License format: pzg_v2_<base64url-encoded-license-data>_<base64url-signature>
const payload = extractSignedLicensePayload(key);
if (!payload) {
return null;
}
let licenseData;
try {
// Support both base64 and base64url decoding for backward compatibility
let decodedData;
try {
decodedData = Buffer.from(payload.encodedData, 'base64url').toString('utf8');
}
catch {
decodedData = Buffer.from(payload.encodedData, 'base64').toString('utf8');
}
const parsed = JSON.parse(decodedData);
// Validate structure
if (typeof parsed !== 'object' || parsed === null) {
return null;
}
licenseData = parsed;
}
catch {
return null;
}
// Verify required fields
if (!licenseData.plan || !licenseData.validUntil || !licenseData.maxSeats) {
return null;
}
// Replay attack protection: check license issue age
if (licenseData.issuedAt) {
const issuedDate = new Date(licenseData.issuedAt);
const now = new Date();
const licenseAge = now.getTime() - issuedDate.getTime();
if (licenseAge > MAX_LICENSE_AGE_MS) {
throw new Error('License is too old. Please request a new license.');
}
// Additional validation: license must not be issued in the future
if (issuedDate > now) {
return null; // Invalid license with future timestamp
}
}
// Check expiration
const expirationDate = new Date(licenseData.validUntil);
if (expirationDate < new Date()) {
throw new Error('License has expired. Please renew your PZG Pro subscription.');
}
// Validate plan slug
const allowedPlans = ['starter', 'professional', 'business', 'enterprise'];
if (!allowedPlans.includes(licenseData.plan)) {
return null;
}
const normalizedPlan = licenseData.plan;
// Validate seats
if (typeof licenseData.maxSeats !== 'number' || licenseData.maxSeats < 1) {
return null;
}
const validatedLicense = {
key,
plan: normalizedPlan,
validUntil: licenseData.validUntil,
maxSeats: licenseData.maxSeats,
cached: false,
customerId: licenseData.customerId,
};
return validatedLicense;
}
catch (error) {
if (error instanceof Error && error.message.includes('expired')) {
throw error;
}
return null;
}
}
/**
* Main license validation function
* Returns license info if valid, null if invalid, throws if required but missing
*/
async function validateLicense(required = true) {
var _a, _b;
try {
// Check for code tampering first
if (detectTampering()) {
reportTampering();
throw new errorHandling_1.LicenseError('PZG Pro code tampering detected. Pro features have been modified.\n' +
'This violates the Commercial License Agreement.\n' +
'Please reinstall: npm install prisma-zod-generator@latest\n' +
'Continued violations may result in license termination.', { reason: 'code_tampering_detected' });
}
const key = getLicenseKey();
if (!key) {
if (required) {
throw new errorHandling_1.LicenseError('PZG Pro license required. Set PZG_LICENSE_KEY environment variable.\n' +
'Get your license at: https://omar-dulaimi.github.io/prisma-zod-generator/pricing');
}
return null;
}
// Try cached license first
const cached = await (0, errorHandling_1.safeFileOperation)(() => readCachedLicense(), CACHE_FILE, 'license');
if (cached && cached.key === key) {
// Re-validate cached license with current secret to handle secret changes
try {
const revalidated = await validateLicenseRemote(key);
if (revalidated) {
return cached;
}
}
catch (error) {
// If signature verification fails, propagate the error immediately
if (error instanceof errorHandling_1.LicenseError &&
((_a = error.context) === null || _a === void 0 ? void 0 : _a.reason) === 'signature_verification_failed') {
throw error;
}
// For other validation errors, continue to fresh validation
}
// If revalidation fails, continue to fresh validation
}
// Validate with remote server
try {
const license = await validateLicenseRemote(key);
if (!license) {
if (required) {
throw new errorHandling_1.LicenseError('Invalid PZG Pro license key. Please check your license key.\n' +
'Get support at: https://github.com/omar-dulaimi/prisma-zod-generator/issues', { key: key.substring(0, 8) + '...' });
}
return null;
}
// Cache valid license
await (0, errorHandling_1.safeFileOperation)(() => cacheLicense(license), CACHE_FILE, 'license');
return license;
}
catch (error) {
// Re-throw signature verification errors (security violations) - these should always throw
if (error instanceof errorHandling_1.LicenseError &&
((_b = error.context) === null || _b === void 0 ? void 0 : _b.reason) === 'signature_verification_failed') {
(0, errorHandling_1.logError)(error);
throw error;
}
// For other validation errors, respect the required flag
if (required) {
(0, errorHandling_1.logError)(error instanceof errorHandling_1.LicenseError
? error
: new errorHandling_1.LicenseError(`License validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error }));
throw error;
}
return null;
}
}
catch (error) {
if (error instanceof errorHandling_1.LicenseError) {
(0, errorHandling_1.logError)(error);
throw error;
}
const licenseError = new errorHandling_1.LicenseError(`License validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { originalError: error });
(0, errorHandling_1.logError)(licenseError);
throw licenseError;
}
}
/**
* Check if a specific plan feature is available
*/
function hasFeature(license, feature) {
if (!license)
return false;
const features = {
policies: ['professional', 'business', 'enterprise'],
'server-actions': ['starter', 'professional', 'business', 'enterprise'],
'sdk-publisher': ['professional', 'business', 'enterprise'],
'drift-guard': ['professional', 'business', 'enterprise'],
// Phase 2 - Safety & Governance
'contract-testing-pack': ['business', 'enterprise'],
'postgres-rls-pack': ['professional', 'business', 'enterprise'],
// Phase 3 - Developer Experience & Docs
'form-ux': ['starter', 'professional', 'business', 'enterprise'],
'api-docs-pack': ['business', 'enterprise'],
// Phase 4 - Scale & Compliance
'multi-tenant-kit': ['enterprise'],
'performance-pack': ['professional', 'business', 'enterprise'],
'data-factories': ['business', 'enterprise'],
};
const requiredPlans = features[feature];
return requiredPlans ? requiredPlans.includes(license.plan) : false;
}
/**
* Require a specific feature, throw if not available
*/
async function requireFeature(feature, context) {
// Development bypass for local testing
if (process.env.NODE_ENV === 'development' || process.env.PZG_DEV_MODE === 'true') {
return {
key: 'dev-bypass',
plan: 'enterprise',
validUntil: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
maxSeats: 999,
cached: false,
};
}
const license = await validateLicense(true);
// Use business security for comprehensive validation
const securityContext = {
userId: (context === null || context === void 0 ? void 0 : context.userId) || 'system',
roles: ['authenticated'],
permissions: [],
sessionId: (context === null || context === void 0 ? void 0 : context.sessionId) || 'unknown',
isAdmin: false,
licenseInfo: license !== null && license !== void 0 ? license : undefined,
};
const validation = businessSecurity_1.businessSecurity.validateFeatureAccess(feature, license, securityContext);
if (!validation.allowed) {
throw new errorHandling_1.LicenseError(`Feature access denied: ${validation.reason}\n` +
'Upgrade at: https://omar-dulaimi.github.io/prisma-zod-generator/pricing', { feature, reason: validation.reason });
}
return license;
}
/**
* Get license status for CLI commands
*/
async function getLicenseStatus() {
try {
const license = await validateLicense(false);
if (!license) {
return { valid: false };
}
return {
valid: true,
plan: license.plan,
cached: license.cached,
};
}
catch {
return { valid: false };
}
}
//# sourceMappingURL=license.js.map