@odavl/guardian 0.1.0-rc1 → 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/CHANGELOG.md +146 -0
- package/README.md +155 -97
- package/bin/guardian.js +1544 -55
- package/config/README.md +59 -0
- package/config/profiles/landing-demo.yaml +16 -0
- package/package.json +26 -11
- package/policies/landing-demo.json +22 -0
- package/src/enterprise/audit-logger.js +166 -0
- package/src/enterprise/pdf-exporter.js +267 -0
- package/src/enterprise/rbac-gate.js +142 -0
- package/src/enterprise/rbac.js +239 -0
- package/src/enterprise/site-manager.js +180 -0
- package/src/founder/feedback-system.js +156 -0
- package/src/founder/founder-tracker.js +213 -0
- package/src/founder/usage-signals.js +141 -0
- package/src/guardian/alert-ledger.js +121 -0
- package/src/guardian/attempt-engine.js +587 -12
- package/src/guardian/attempt-registry.js +42 -1
- package/src/guardian/attempt-relevance.js +106 -0
- package/src/guardian/attempt.js +85 -39
- package/src/guardian/attempts-filter.js +63 -0
- package/src/guardian/baseline.js +50 -8
- package/src/guardian/breakage-intelligence.js +1 -0
- package/src/guardian/browser-pool.js +131 -0
- package/src/guardian/browser.js +28 -1
- package/src/guardian/ci-cli.js +121 -0
- package/src/guardian/ci-mode.js +15 -0
- package/src/guardian/ci-output.js +38 -0
- package/src/guardian/cli-summary.js +167 -67
- package/src/guardian/config-loader.js +162 -0
- package/src/guardian/data-guardian-detector.js +189 -0
- package/src/guardian/detection-layers.js +271 -0
- package/src/guardian/drift-detector.js +100 -0
- package/src/guardian/enhanced-html-reporter.js +221 -4
- package/src/guardian/env-guard.js +127 -0
- package/src/guardian/failure-intelligence.js +173 -0
- package/src/guardian/first-run-profile.js +89 -0
- package/src/guardian/first-run.js +54 -0
- package/src/guardian/flag-validator.js +111 -0
- package/src/guardian/flow-executor.js +309 -44
- package/src/guardian/html-reporter.js +2 -0
- package/src/guardian/human-reporter.js +431 -0
- package/src/guardian/index.js +22 -19
- package/src/guardian/init-command.js +9 -5
- package/src/guardian/intent-detector.js +146 -0
- package/src/guardian/journey-definitions.js +132 -0
- package/src/guardian/journey-scan-cli.js +145 -0
- package/src/guardian/journey-scanner.js +583 -0
- package/src/guardian/junit-reporter.js +18 -1
- package/src/guardian/language-detection.js +99 -0
- package/src/guardian/live-cli.js +95 -0
- package/src/guardian/live-scheduler-runner.js +137 -0
- package/src/guardian/live-scheduler.js +146 -0
- package/src/guardian/market-reporter.js +357 -82
- package/src/guardian/parallel-executor.js +116 -0
- package/src/guardian/pattern-analyzer.js +348 -0
- package/src/guardian/policy.js +80 -3
- package/src/guardian/prerequisite-checker.js +101 -0
- package/src/guardian/preset-loader.js +27 -18
- package/src/guardian/profile-loader.js +96 -0
- package/src/guardian/reality.js +1612 -115
- package/src/guardian/reporter.js +27 -41
- package/src/guardian/run-artifacts.js +212 -0
- package/src/guardian/run-cleanup.js +207 -0
- package/src/guardian/run-latest.js +90 -0
- package/src/guardian/run-list.js +211 -0
- package/src/guardian/run-summary.js +20 -0
- package/src/guardian/scan-presets.js +100 -11
- package/src/guardian/selector-fallbacks.js +394 -0
- package/src/guardian/semantic-contact-detection.js +255 -0
- package/src/guardian/semantic-contact-finder.js +201 -0
- package/src/guardian/semantic-targets.js +234 -0
- package/src/guardian/site-introspection.js +257 -0
- package/src/guardian/smoke.js +258 -0
- package/src/guardian/snapshot-schema.js +25 -1
- package/src/guardian/snapshot.js +69 -3
- package/src/guardian/stability-scorer.js +169 -0
- package/src/guardian/success-evaluator.js +214 -0
- package/src/guardian/template-command.js +184 -0
- package/src/guardian/text-formatters.js +426 -0
- package/src/guardian/timeout-profiles.js +57 -0
- package/src/guardian/verdict.js +320 -0
- package/src/guardian/verdicts.js +74 -0
- package/src/guardian/wait-for-outcome.js +120 -0
- package/src/guardian/watch-runner.js +181 -0
- package/src/payments/stripe-checkout.js +169 -0
- package/src/plans/plan-definitions.js +148 -0
- package/src/plans/plan-manager.js +211 -0
- package/src/plans/usage-tracker.js +210 -0
- package/src/recipes/recipe-engine.js +188 -0
- package/src/recipes/recipe-failure-analysis.js +159 -0
- package/src/recipes/recipe-registry.js +134 -0
- package/src/recipes/recipe-runtime.js +507 -0
- package/src/recipes/recipe-store.js +410 -0
- package/guardian-contract-v1.md +0 -149
- /package/{guardian.config.json → config/guardian.config.json} +0 -0
- /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
- /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
- /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
- /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
- /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODAVL Guardian Stripe Integration
|
|
3
|
+
* Real Stripe checkout for PRO and BUSINESS plans
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { PLANS } = require('../plans/plan-definitions');
|
|
7
|
+
const { setCurrentPlan } = require('../plans/plan-manager');
|
|
8
|
+
|
|
9
|
+
// Stripe configuration
|
|
10
|
+
const STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY || '';
|
|
11
|
+
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY || '';
|
|
12
|
+
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create Stripe checkout session
|
|
16
|
+
* This is called from the website when user clicks "Upgrade"
|
|
17
|
+
*/
|
|
18
|
+
async function createCheckoutSession(planId, successUrl, cancelUrl) {
|
|
19
|
+
if (!STRIPE_SECRET_KEY) {
|
|
20
|
+
throw new Error('Stripe is not configured. Set STRIPE_SECRET_KEY environment variable.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
24
|
+
const plan = PLANS[planId.toUpperCase()];
|
|
25
|
+
|
|
26
|
+
if (!plan || !plan.stripePriceId) {
|
|
27
|
+
throw new Error(`Invalid plan: ${planId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const session = await stripe.checkout.sessions.create({
|
|
31
|
+
mode: 'subscription',
|
|
32
|
+
payment_method_types: ['card'],
|
|
33
|
+
line_items: [
|
|
34
|
+
{
|
|
35
|
+
price: plan.stripePriceId,
|
|
36
|
+
quantity: 1,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
success_url: successUrl,
|
|
40
|
+
cancel_url: cancelUrl,
|
|
41
|
+
metadata: {
|
|
42
|
+
planId: plan.id,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
sessionId: session.id,
|
|
48
|
+
url: session.url,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Handle Stripe webhook events
|
|
54
|
+
* Called when payment succeeds or subscription changes
|
|
55
|
+
*/
|
|
56
|
+
async function handleWebhook(requestBody, signature) {
|
|
57
|
+
if (!STRIPE_SECRET_KEY || !STRIPE_WEBHOOK_SECRET) {
|
|
58
|
+
throw new Error('Stripe webhooks not configured');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
62
|
+
|
|
63
|
+
let event;
|
|
64
|
+
try {
|
|
65
|
+
event = stripe.webhooks.constructEvent(
|
|
66
|
+
requestBody,
|
|
67
|
+
signature,
|
|
68
|
+
STRIPE_WEBHOOK_SECRET
|
|
69
|
+
);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`Webhook signature verification failed: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle the event
|
|
75
|
+
switch (event.type) {
|
|
76
|
+
case 'checkout.session.completed': {
|
|
77
|
+
const session = event.data.object;
|
|
78
|
+
const planId = session.metadata.planId;
|
|
79
|
+
const customerId = session.customer;
|
|
80
|
+
const subscriptionId = session.subscription;
|
|
81
|
+
|
|
82
|
+
// Activate the plan
|
|
83
|
+
setCurrentPlan(planId, {
|
|
84
|
+
customerId,
|
|
85
|
+
subscriptionId,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
planId,
|
|
91
|
+
customerId,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'customer.subscription.updated':
|
|
96
|
+
case 'customer.subscription.deleted': {
|
|
97
|
+
const subscription = event.data.object;
|
|
98
|
+
const customerId = subscription.customer;
|
|
99
|
+
|
|
100
|
+
if (subscription.status === 'active') {
|
|
101
|
+
// Keep subscription active
|
|
102
|
+
return { success: true, status: 'active' };
|
|
103
|
+
} else if (['canceled', 'unpaid'].includes(subscription.status)) {
|
|
104
|
+
// Downgrade to FREE
|
|
105
|
+
setCurrentPlan('free');
|
|
106
|
+
return { success: true, status: 'downgraded' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { success: true, status: subscription.status };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
default:
|
|
113
|
+
console.log(`Unhandled event type: ${event.type}`);
|
|
114
|
+
return { success: true, ignored: true };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get Stripe public key for client-side
|
|
120
|
+
*/
|
|
121
|
+
function getStripePublicKey() {
|
|
122
|
+
return STRIPE_PUBLIC_KEY;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Create customer portal session
|
|
127
|
+
* Allows users to manage their subscription
|
|
128
|
+
*/
|
|
129
|
+
async function createPortalSession(customerId, returnUrl) {
|
|
130
|
+
if (!STRIPE_SECRET_KEY) {
|
|
131
|
+
throw new Error('Stripe is not configured');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const stripe = require('stripe')(STRIPE_SECRET_KEY);
|
|
135
|
+
|
|
136
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
137
|
+
customer: customerId,
|
|
138
|
+
return_url: returnUrl,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
url: session.url,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Verify Stripe is configured
|
|
148
|
+
*/
|
|
149
|
+
function isStripeConfigured() {
|
|
150
|
+
return !!(STRIPE_SECRET_KEY && STRIPE_PUBLIC_KEY);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get checkout URL for a plan
|
|
155
|
+
* Helper for CLI
|
|
156
|
+
*/
|
|
157
|
+
function getCheckoutUrl(planId) {
|
|
158
|
+
const baseUrl = process.env.GUARDIAN_WEBSITE_URL || 'https://guardian.odavl.com';
|
|
159
|
+
return `${baseUrl}/checkout?plan=${planId}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
createCheckoutSession,
|
|
164
|
+
handleWebhook,
|
|
165
|
+
getStripePublicKey,
|
|
166
|
+
createPortalSession,
|
|
167
|
+
isStripeConfigured,
|
|
168
|
+
getCheckoutUrl,
|
|
169
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODAVL Guardian Plan Definitions
|
|
3
|
+
* Real enforced limits for FREE, PRO, and BUSINESS plans
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const PLANS = {
|
|
7
|
+
FREE: {
|
|
8
|
+
id: 'free',
|
|
9
|
+
name: 'Free',
|
|
10
|
+
price: 0,
|
|
11
|
+
priceMonthly: 0,
|
|
12
|
+
maxScansPerMonth: 10,
|
|
13
|
+
maxSites: 1,
|
|
14
|
+
liveGuardianAllowed: false,
|
|
15
|
+
ciModeAllowed: false,
|
|
16
|
+
alertsAllowed: false,
|
|
17
|
+
features: [
|
|
18
|
+
'Scan public pages',
|
|
19
|
+
'Basic issue detection',
|
|
20
|
+
'CLI usage',
|
|
21
|
+
'Community support',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
PRO: {
|
|
25
|
+
id: 'pro',
|
|
26
|
+
name: 'Pro',
|
|
27
|
+
price: 29,
|
|
28
|
+
priceMonthly: 29,
|
|
29
|
+
stripePriceId: process.env.STRIPE_PRO_PRICE_ID || 'price_pro_placeholder',
|
|
30
|
+
maxScansPerMonth: 200,
|
|
31
|
+
maxSites: 3,
|
|
32
|
+
liveGuardianAllowed: true,
|
|
33
|
+
ciModeAllowed: true,
|
|
34
|
+
alertsAllowed: true,
|
|
35
|
+
features: [
|
|
36
|
+
'Deeper checks & signals',
|
|
37
|
+
'Prioritized findings',
|
|
38
|
+
'Live guardian monitoring',
|
|
39
|
+
'CI/CD integration',
|
|
40
|
+
'Email alerts',
|
|
41
|
+
'Exportable reports',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
BUSINESS: {
|
|
45
|
+
id: 'business',
|
|
46
|
+
name: 'Business',
|
|
47
|
+
price: 99,
|
|
48
|
+
priceMonthly: 99,
|
|
49
|
+
stripePriceId: process.env.STRIPE_BUSINESS_PRICE_ID || 'price_business_placeholder',
|
|
50
|
+
maxScansPerMonth: -1, // unlimited
|
|
51
|
+
maxSites: -1, // unlimited
|
|
52
|
+
liveGuardianAllowed: true,
|
|
53
|
+
ciModeAllowed: true,
|
|
54
|
+
alertsAllowed: true,
|
|
55
|
+
features: [
|
|
56
|
+
'Everything in Pro',
|
|
57
|
+
'Unlimited scans',
|
|
58
|
+
'Unlimited sites',
|
|
59
|
+
'Priority support',
|
|
60
|
+
'Custom integrations',
|
|
61
|
+
'Team collaboration',
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get plan by ID
|
|
68
|
+
*/
|
|
69
|
+
function getPlan(planId) {
|
|
70
|
+
const normalizedId = planId.toUpperCase();
|
|
71
|
+
if (!PLANS[normalizedId]) {
|
|
72
|
+
return PLANS.FREE;
|
|
73
|
+
}
|
|
74
|
+
return PLANS[normalizedId];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a plan allows a specific feature
|
|
79
|
+
*/
|
|
80
|
+
function planAllows(planId, feature) {
|
|
81
|
+
const plan = getPlan(planId);
|
|
82
|
+
switch (feature) {
|
|
83
|
+
case 'liveGuardian':
|
|
84
|
+
return plan.liveGuardianAllowed;
|
|
85
|
+
case 'ciMode':
|
|
86
|
+
return plan.ciModeAllowed;
|
|
87
|
+
case 'alerts':
|
|
88
|
+
return plan.alertsAllowed;
|
|
89
|
+
default:
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if usage is within plan limits
|
|
96
|
+
*/
|
|
97
|
+
function isWithinLimit(planId, currentUsage, limitType) {
|
|
98
|
+
const plan = getPlan(planId);
|
|
99
|
+
|
|
100
|
+
switch (limitType) {
|
|
101
|
+
case 'scans':
|
|
102
|
+
if (plan.maxScansPerMonth === -1) return true; // unlimited
|
|
103
|
+
return currentUsage < plan.maxScansPerMonth;
|
|
104
|
+
case 'sites':
|
|
105
|
+
if (plan.maxSites === -1) return true; // unlimited
|
|
106
|
+
return currentUsage < plan.maxSites;
|
|
107
|
+
default:
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get human-readable error message for limit exceeded
|
|
114
|
+
*/
|
|
115
|
+
function getLimitExceededMessage(planId, limitType) {
|
|
116
|
+
const plan = getPlan(planId);
|
|
117
|
+
|
|
118
|
+
switch (limitType) {
|
|
119
|
+
case 'scans':
|
|
120
|
+
return `You've reached your monthly scan limit for the ${plan.name} plan (${plan.maxScansPerMonth} scans/month). Upgrade to continue scanning.`;
|
|
121
|
+
case 'sites':
|
|
122
|
+
return `You've reached your site limit for the ${plan.name} plan (${plan.maxSites} site${plan.maxSites > 1 ? 's' : ''}). Upgrade to add more sites.`;
|
|
123
|
+
case 'liveGuardian':
|
|
124
|
+
return `Live Guardian monitoring is not available on the ${plan.name} plan. Upgrade to Pro to enable continuous monitoring.`;
|
|
125
|
+
case 'ciMode':
|
|
126
|
+
return `CI/CD mode is not available on the ${plan.name} plan. Upgrade to Pro to enable CI integration.`;
|
|
127
|
+
case 'alerts':
|
|
128
|
+
return `Email alerts are not available on the ${plan.name} plan. Upgrade to Pro to enable notifications.`;
|
|
129
|
+
default:
|
|
130
|
+
return `This feature is not available on your current plan. Upgrade to unlock more features.`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all plans for display
|
|
136
|
+
*/
|
|
137
|
+
function getAllPlans() {
|
|
138
|
+
return [PLANS.FREE, PLANS.PRO, PLANS.BUSINESS];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
PLANS,
|
|
143
|
+
getPlan,
|
|
144
|
+
planAllows,
|
|
145
|
+
isWithinLimit,
|
|
146
|
+
getLimitExceededMessage,
|
|
147
|
+
getAllPlans,
|
|
148
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODAVL Guardian Plan Manager
|
|
3
|
+
* Manages user plans, checks limits, and enforces restrictions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { getPlan, planAllows, isWithinLimit, getLimitExceededMessage } = require('./plan-definitions');
|
|
10
|
+
const { getUsageStats, canPerformScan, canAddSite, recordScan } = require('./usage-tracker');
|
|
11
|
+
|
|
12
|
+
const PLAN_DIR = path.join(os.homedir(), '.odavl-guardian', 'plan');
|
|
13
|
+
const PLAN_FILE = path.join(PLAN_DIR, 'current-plan.json');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure plan directory exists
|
|
17
|
+
*/
|
|
18
|
+
function ensurePlanDir() {
|
|
19
|
+
if (!fs.existsSync(PLAN_DIR)) {
|
|
20
|
+
fs.mkdirSync(PLAN_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get current plan
|
|
26
|
+
*/
|
|
27
|
+
function getCurrentPlan() {
|
|
28
|
+
ensurePlanDir();
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(PLAN_FILE)) {
|
|
31
|
+
// Default to FREE plan
|
|
32
|
+
return {
|
|
33
|
+
planId: 'free',
|
|
34
|
+
activated: new Date().toISOString(),
|
|
35
|
+
stripeCustomerId: null,
|
|
36
|
+
stripeSubscriptionId: null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(fs.readFileSync(PLAN_FILE, 'utf-8'));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error loading plan:', error.message);
|
|
44
|
+
return {
|
|
45
|
+
planId: 'free',
|
|
46
|
+
activated: new Date().toISOString(),
|
|
47
|
+
stripeCustomerId: null,
|
|
48
|
+
stripeSubscriptionId: null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set current plan
|
|
55
|
+
*/
|
|
56
|
+
function setCurrentPlan(planId, stripeData = {}) {
|
|
57
|
+
ensurePlanDir();
|
|
58
|
+
|
|
59
|
+
const planInfo = {
|
|
60
|
+
planId: planId.toLowerCase(),
|
|
61
|
+
activated: new Date().toISOString(),
|
|
62
|
+
stripeCustomerId: stripeData.customerId || null,
|
|
63
|
+
stripeSubscriptionId: stripeData.subscriptionId || null,
|
|
64
|
+
upgraded: new Date().toISOString(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(PLAN_FILE, JSON.stringify(planInfo, null, 2), 'utf-8');
|
|
68
|
+
return planInfo;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a feature is allowed
|
|
73
|
+
*/
|
|
74
|
+
function checkFeatureAllowed(feature) {
|
|
75
|
+
const currentPlan = getCurrentPlan();
|
|
76
|
+
const plan = getPlan(currentPlan.planId);
|
|
77
|
+
const allowed = planAllows(currentPlan.planId, feature);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
allowed,
|
|
81
|
+
plan: plan.name,
|
|
82
|
+
message: allowed ? null : getLimitExceededMessage(currentPlan.planId, feature),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if can perform scan
|
|
88
|
+
*/
|
|
89
|
+
function checkCanScan(url) {
|
|
90
|
+
const currentPlan = getCurrentPlan();
|
|
91
|
+
const plan = getPlan(currentPlan.planId);
|
|
92
|
+
const usage = getUsageStats();
|
|
93
|
+
|
|
94
|
+
// Check site limit first
|
|
95
|
+
if (!canAddSite(url, plan.maxSites)) {
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
reason: 'site_limit',
|
|
99
|
+
message: getLimitExceededMessage(currentPlan.planId, 'sites'),
|
|
100
|
+
plan: plan.name,
|
|
101
|
+
usage: {
|
|
102
|
+
sites: usage.sites.length,
|
|
103
|
+
maxSites: plan.maxSites,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check scan limit
|
|
109
|
+
if (!canPerformScan(plan.maxScansPerMonth)) {
|
|
110
|
+
return {
|
|
111
|
+
allowed: false,
|
|
112
|
+
reason: 'scan_limit',
|
|
113
|
+
message: getLimitExceededMessage(currentPlan.planId, 'scans'),
|
|
114
|
+
plan: plan.name,
|
|
115
|
+
usage: {
|
|
116
|
+
scansThisMonth: usage.scansThisMonth,
|
|
117
|
+
maxScans: plan.maxScansPerMonth,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
allowed: true,
|
|
124
|
+
plan: plan.name,
|
|
125
|
+
usage: {
|
|
126
|
+
scansThisMonth: usage.scansThisMonth,
|
|
127
|
+
maxScans: plan.maxScansPerMonth,
|
|
128
|
+
scansRemaining: plan.maxScansPerMonth === -1 ? 'Unlimited' : plan.maxScansPerMonth - usage.scansThisMonth,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Record scan and enforce limits
|
|
135
|
+
*/
|
|
136
|
+
function performScan(url) {
|
|
137
|
+
const check = checkCanScan(url);
|
|
138
|
+
|
|
139
|
+
if (!check.allowed) {
|
|
140
|
+
throw new Error(check.message);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Record the scan
|
|
144
|
+
recordScan(url);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
plan: check.plan,
|
|
149
|
+
usage: getUsageStats(),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get plan summary for display
|
|
155
|
+
*/
|
|
156
|
+
function getPlanSummary() {
|
|
157
|
+
const currentPlan = getCurrentPlan();
|
|
158
|
+
const plan = getPlan(currentPlan.planId);
|
|
159
|
+
const usage = getUsageStats();
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
plan: {
|
|
163
|
+
id: plan.id,
|
|
164
|
+
name: plan.name,
|
|
165
|
+
price: plan.price,
|
|
166
|
+
},
|
|
167
|
+
limits: {
|
|
168
|
+
scans: {
|
|
169
|
+
max: plan.maxScansPerMonth,
|
|
170
|
+
used: usage.scansThisMonth,
|
|
171
|
+
remaining: plan.maxScansPerMonth === -1 ? 'Unlimited' : Math.max(0, plan.maxScansPerMonth - usage.scansThisMonth),
|
|
172
|
+
},
|
|
173
|
+
sites: {
|
|
174
|
+
max: plan.maxSites,
|
|
175
|
+
used: usage.sites.length,
|
|
176
|
+
remaining: plan.maxSites === -1 ? 'Unlimited' : Math.max(0, plan.maxSites - usage.sites.length),
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
features: {
|
|
180
|
+
liveGuardian: plan.liveGuardianAllowed,
|
|
181
|
+
ciMode: plan.ciModeAllowed,
|
|
182
|
+
alerts: plan.alertsAllowed,
|
|
183
|
+
},
|
|
184
|
+
activated: currentPlan.activated,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Upgrade message helper
|
|
190
|
+
*/
|
|
191
|
+
function getUpgradeMessage() {
|
|
192
|
+
return '\nTo upgrade your plan, visit: https://guardian.odavl.com/pricing\nOr run: guardian upgrade';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get plan file path (for debugging)
|
|
197
|
+
*/
|
|
198
|
+
function getPlanFilePath() {
|
|
199
|
+
return PLAN_FILE;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
getCurrentPlan,
|
|
204
|
+
setCurrentPlan,
|
|
205
|
+
checkFeatureAllowed,
|
|
206
|
+
checkCanScan,
|
|
207
|
+
performScan,
|
|
208
|
+
getPlanSummary,
|
|
209
|
+
getUpgradeMessage,
|
|
210
|
+
getPlanFilePath,
|
|
211
|
+
};
|