@odavl/guardian 0.2.0 → 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.
Files changed (84) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/README.md +155 -97
  3. package/bin/guardian.js +1345 -60
  4. package/config/README.md +59 -0
  5. package/config/profiles/landing-demo.yaml +16 -0
  6. package/package.json +21 -11
  7. package/policies/landing-demo.json +22 -0
  8. package/src/enterprise/audit-logger.js +166 -0
  9. package/src/enterprise/pdf-exporter.js +267 -0
  10. package/src/enterprise/rbac-gate.js +142 -0
  11. package/src/enterprise/rbac.js +239 -0
  12. package/src/enterprise/site-manager.js +180 -0
  13. package/src/founder/feedback-system.js +156 -0
  14. package/src/founder/founder-tracker.js +213 -0
  15. package/src/founder/usage-signals.js +141 -0
  16. package/src/guardian/alert-ledger.js +121 -0
  17. package/src/guardian/attempt-engine.js +568 -7
  18. package/src/guardian/attempt-registry.js +42 -1
  19. package/src/guardian/attempt-relevance.js +106 -0
  20. package/src/guardian/attempt.js +24 -0
  21. package/src/guardian/baseline.js +12 -4
  22. package/src/guardian/breakage-intelligence.js +1 -0
  23. package/src/guardian/ci-cli.js +121 -0
  24. package/src/guardian/ci-output.js +4 -3
  25. package/src/guardian/cli-summary.js +79 -92
  26. package/src/guardian/config-loader.js +162 -0
  27. package/src/guardian/drift-detector.js +100 -0
  28. package/src/guardian/enhanced-html-reporter.js +221 -4
  29. package/src/guardian/env-guard.js +127 -0
  30. package/src/guardian/failure-intelligence.js +173 -0
  31. package/src/guardian/first-run-profile.js +89 -0
  32. package/src/guardian/first-run.js +6 -1
  33. package/src/guardian/flag-validator.js +17 -3
  34. package/src/guardian/html-reporter.js +2 -0
  35. package/src/guardian/human-reporter.js +431 -0
  36. package/src/guardian/index.js +22 -19
  37. package/src/guardian/init-command.js +9 -5
  38. package/src/guardian/intent-detector.js +146 -0
  39. package/src/guardian/journey-definitions.js +132 -0
  40. package/src/guardian/journey-scan-cli.js +145 -0
  41. package/src/guardian/journey-scanner.js +583 -0
  42. package/src/guardian/junit-reporter.js +18 -1
  43. package/src/guardian/live-cli.js +95 -0
  44. package/src/guardian/live-scheduler-runner.js +137 -0
  45. package/src/guardian/live-scheduler.js +146 -0
  46. package/src/guardian/market-reporter.js +341 -81
  47. package/src/guardian/pattern-analyzer.js +348 -0
  48. package/src/guardian/policy.js +80 -3
  49. package/src/guardian/preset-loader.js +9 -6
  50. package/src/guardian/reality.js +1278 -117
  51. package/src/guardian/reporter.js +27 -41
  52. package/src/guardian/run-artifacts.js +212 -0
  53. package/src/guardian/run-cleanup.js +207 -0
  54. package/src/guardian/run-latest.js +90 -0
  55. package/src/guardian/run-list.js +211 -0
  56. package/src/guardian/scan-presets.js +100 -11
  57. package/src/guardian/selector-fallbacks.js +394 -0
  58. package/src/guardian/semantic-contact-finder.js +2 -1
  59. package/src/guardian/site-introspection.js +257 -0
  60. package/src/guardian/smoke.js +2 -2
  61. package/src/guardian/snapshot-schema.js +25 -1
  62. package/src/guardian/snapshot.js +46 -2
  63. package/src/guardian/stability-scorer.js +169 -0
  64. package/src/guardian/template-command.js +184 -0
  65. package/src/guardian/text-formatters.js +426 -0
  66. package/src/guardian/verdict.js +320 -0
  67. package/src/guardian/verdicts.js +74 -0
  68. package/src/guardian/watch-runner.js +3 -7
  69. package/src/payments/stripe-checkout.js +169 -0
  70. package/src/plans/plan-definitions.js +148 -0
  71. package/src/plans/plan-manager.js +211 -0
  72. package/src/plans/usage-tracker.js +210 -0
  73. package/src/recipes/recipe-engine.js +188 -0
  74. package/src/recipes/recipe-failure-analysis.js +159 -0
  75. package/src/recipes/recipe-registry.js +134 -0
  76. package/src/recipes/recipe-runtime.js +507 -0
  77. package/src/recipes/recipe-store.js +410 -0
  78. package/guardian-contract-v1.md +0 -149
  79. /package/{guardian.config.json → config/guardian.config.json} +0 -0
  80. /package/{guardian.policy.json → config/guardian.policy.json} +0 -0
  81. /package/{guardian.profile.docs.yaml → config/profiles/docs.yaml} +0 -0
  82. /package/{guardian.profile.ecommerce.yaml → config/profiles/ecommerce.yaml} +0 -0
  83. /package/{guardian.profile.marketing.yaml → config/profiles/marketing.yaml} +0 -0
  84. /package/{guardian.profile.saas.yaml → config/profiles/saas.yaml} +0 -0
@@ -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
+ };
@@ -0,0 +1,210 @@
1
+ /**
2
+ * ODAVL Guardian Usage Tracker
3
+ * Tracks scans per month and enforces plan limits
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const os = require('os');
9
+
10
+ const USAGE_DIR = path.join(os.homedir(), '.odavl-guardian', 'usage');
11
+ const USAGE_FILE = path.join(USAGE_DIR, 'usage.json');
12
+
13
+ /**
14
+ * Ensure usage directory exists
15
+ */
16
+ function ensureUsageDir() {
17
+ if (!fs.existsSync(USAGE_DIR)) {
18
+ fs.mkdirSync(USAGE_DIR, { recursive: true });
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Get current month key (YYYY-MM)
24
+ */
25
+ function getCurrentMonthKey() {
26
+ const now = new Date();
27
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
28
+ }
29
+
30
+ /**
31
+ * Load usage data
32
+ */
33
+ function loadUsage() {
34
+ ensureUsageDir();
35
+
36
+ if (!fs.existsSync(USAGE_FILE)) {
37
+ return {
38
+ currentMonth: getCurrentMonthKey(),
39
+ scansThisMonth: 0,
40
+ sites: [],
41
+ totalScans: 0,
42
+ history: {},
43
+ };
44
+ }
45
+
46
+ try {
47
+ const data = JSON.parse(fs.readFileSync(USAGE_FILE, 'utf-8'));
48
+
49
+ // Reset if new month
50
+ const currentMonth = getCurrentMonthKey();
51
+ if (data.currentMonth !== currentMonth) {
52
+ // Archive old month
53
+ data.history[data.currentMonth] = {
54
+ scans: data.scansThisMonth,
55
+ sites: data.sites.length,
56
+ };
57
+
58
+ // Reset for new month
59
+ data.currentMonth = currentMonth;
60
+ data.scansThisMonth = 0;
61
+ }
62
+
63
+ return data;
64
+ } catch (error) {
65
+ console.error('Error loading usage data:', error.message);
66
+ return {
67
+ currentMonth: getCurrentMonthKey(),
68
+ scansThisMonth: 0,
69
+ sites: [],
70
+ totalScans: 0,
71
+ history: {},
72
+ };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save usage data
78
+ */
79
+ function saveUsage(usage) {
80
+ ensureUsageDir();
81
+ fs.writeFileSync(USAGE_FILE, JSON.stringify(usage, null, 2), 'utf-8');
82
+ }
83
+
84
+ /**
85
+ * Record a scan
86
+ */
87
+ function recordScan(url) {
88
+ const usage = loadUsage();
89
+
90
+ // Extract domain from URL
91
+ let domain;
92
+ try {
93
+ const urlObj = new URL(url);
94
+ domain = urlObj.hostname;
95
+ } catch {
96
+ domain = url;
97
+ }
98
+
99
+ // Increment scan count
100
+ usage.scansThisMonth += 1;
101
+ usage.totalScans += 1;
102
+
103
+ // Track unique site
104
+ if (!usage.sites.includes(domain)) {
105
+ usage.sites.push(domain);
106
+ }
107
+
108
+ // Record timestamp
109
+ if (!usage.scanHistory) {
110
+ usage.scanHistory = [];
111
+ }
112
+ usage.scanHistory.push({
113
+ url,
114
+ domain,
115
+ timestamp: new Date().toISOString(),
116
+ });
117
+
118
+ // Keep only last 100 scans in history
119
+ if (usage.scanHistory.length > 100) {
120
+ usage.scanHistory = usage.scanHistory.slice(-100);
121
+ }
122
+
123
+ saveUsage(usage);
124
+ return usage;
125
+ }
126
+
127
+ /**
128
+ * Get current usage stats
129
+ */
130
+ function getUsageStats() {
131
+ return loadUsage();
132
+ }
133
+
134
+ /**
135
+ * Get scans remaining for current plan
136
+ */
137
+ function getScansRemaining(maxScans) {
138
+ const usage = loadUsage();
139
+ if (maxScans === -1) return 'Unlimited';
140
+ return Math.max(0, maxScans - usage.scansThisMonth);
141
+ }
142
+
143
+ /**
144
+ * Check if can perform scan
145
+ */
146
+ function canPerformScan(maxScans) {
147
+ if (maxScans === -1) return true; // unlimited
148
+ const usage = loadUsage();
149
+ return usage.scansThisMonth < maxScans;
150
+ }
151
+
152
+ /**
153
+ * Check if can add site
154
+ */
155
+ function canAddSite(url, maxSites) {
156
+ if (maxSites === -1) return true; // unlimited
157
+
158
+ const usage = loadUsage();
159
+
160
+ // Extract domain
161
+ let domain;
162
+ try {
163
+ const urlObj = new URL(url);
164
+ domain = urlObj.hostname;
165
+ } catch {
166
+ domain = url;
167
+ }
168
+
169
+ // If site already tracked, it's OK
170
+ if (usage.sites.includes(domain)) {
171
+ return true;
172
+ }
173
+
174
+ // Check if under limit
175
+ return usage.sites.length < maxSites;
176
+ }
177
+
178
+ /**
179
+ * Reset usage (for testing or manual reset)
180
+ */
181
+ function resetUsage() {
182
+ ensureUsageDir();
183
+ const usage = {
184
+ currentMonth: getCurrentMonthKey(),
185
+ scansThisMonth: 0,
186
+ sites: [],
187
+ totalScans: 0,
188
+ history: {},
189
+ scanHistory: [],
190
+ };
191
+ saveUsage(usage);
192
+ return usage;
193
+ }
194
+
195
+ /**
196
+ * Get usage file path (for debugging)
197
+ */
198
+ function getUsageFilePath() {
199
+ return USAGE_FILE;
200
+ }
201
+
202
+ module.exports = {
203
+ recordScan,
204
+ getUsageStats,
205
+ getScansRemaining,
206
+ canPerformScan,
207
+ canAddSite,
208
+ resetUsage,
209
+ getUsageFilePath,
210
+ };