getdoorman 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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. package/src/worker.js +31 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Tree-sitter based AST analysis engine.
3
+ *
4
+ * Provides precise, structure-aware code analysis that avoids the false
5
+ * positives and false negatives inherent in regex-based scanning.
6
+ *
7
+ * Gracefully falls back when tree-sitter native modules are not installed —
8
+ * the code becomes fully functional once `npm install` is run.
9
+ */
10
+
11
+ let Parser;
12
+ let treeSitterAvailable = false;
13
+
14
+ const LANGUAGE_MODULES = {};
15
+
16
+ try {
17
+ // eslint-disable-next-line
18
+ const TreeSitter = await import('tree-sitter');
19
+ Parser = TreeSitter.default || TreeSitter;
20
+ treeSitterAvailable = true;
21
+ } catch {
22
+ // tree-sitter is not installed — AST analysis will be unavailable
23
+ }
24
+
25
+ /**
26
+ * Lazily load a tree-sitter language grammar.
27
+ */
28
+ async function loadLanguage(lang) {
29
+ if (LANGUAGE_MODULES[lang]) return LANGUAGE_MODULES[lang];
30
+
31
+ const moduleMap = {
32
+ javascript: 'tree-sitter-javascript',
33
+ python: 'tree-sitter-python',
34
+ go: 'tree-sitter-go',
35
+ ruby: 'tree-sitter-ruby',
36
+ php: 'tree-sitter-php',
37
+ };
38
+
39
+ const modName = moduleMap[lang];
40
+ if (!modName) return null;
41
+
42
+ try {
43
+ const mod = await import(modName);
44
+ LANGUAGE_MODULES[lang] = mod.default || mod;
45
+ return LANGUAGE_MODULES[lang];
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Map file extension to language name.
53
+ */
54
+ export function detectLanguage(filePath) {
55
+ const ext = filePath.slice(filePath.lastIndexOf('.'));
56
+ const map = {
57
+ '.js': 'javascript',
58
+ '.jsx': 'javascript',
59
+ '.ts': 'javascript', // tree-sitter-javascript handles TS basics
60
+ '.tsx': 'javascript',
61
+ '.mjs': 'javascript',
62
+ '.cjs': 'javascript',
63
+ '.py': 'python',
64
+ '.go': 'go',
65
+ '.rb': 'ruby',
66
+ '.php': 'php',
67
+ };
68
+ return map[ext] || null;
69
+ }
70
+
71
+ /**
72
+ * Check whether the tree-sitter AST engine is available.
73
+ */
74
+ export function isASTAvailable() {
75
+ return treeSitterAvailable;
76
+ }
77
+
78
+ /**
79
+ * Parse source code into a tree-sitter AST.
80
+ *
81
+ * @param {string} content - Source code to parse
82
+ * @param {string} language - Language name (javascript, python, go, ruby, php)
83
+ * @returns {ASTContext|null} An ASTContext wrapping the AST, or null on failure.
84
+ */
85
+ export async function parseFile(content, language) {
86
+ if (!treeSitterAvailable) return null;
87
+
88
+ const lang = await loadLanguage(language);
89
+ if (!lang) return null;
90
+
91
+ try {
92
+ const parser = new Parser();
93
+ parser.setLanguage(lang);
94
+ const tree = parser.parse(content);
95
+ return new ASTContext(tree, content, language);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Helpers: walk the tree
103
+ // ---------------------------------------------------------------------------
104
+
105
+ function* walk(node) {
106
+ yield node;
107
+ for (let i = 0; i < node.childCount; i++) {
108
+ yield* walk(node.child(i));
109
+ }
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // ASTContext — query API for AST rules
114
+ // ---------------------------------------------------------------------------
115
+
116
+ export class ASTContext {
117
+ /**
118
+ * @param {object} tree - tree-sitter Tree object
119
+ * @param {string} source - original source text
120
+ * @param {string} language - language name
121
+ */
122
+ constructor(tree, source, language) {
123
+ this.tree = tree;
124
+ this.source = source;
125
+ this.language = language;
126
+ this._root = tree.rootNode;
127
+ }
128
+
129
+ // -----------------------------------------------------------------------
130
+ // findNodes(type) — return every node whose type matches `type`.
131
+ // -----------------------------------------------------------------------
132
+ findNodes(type) {
133
+ const results = [];
134
+ for (const node of walk(this._root)) {
135
+ if (node.type === type) {
136
+ results.push(node);
137
+ }
138
+ }
139
+ return results;
140
+ }
141
+
142
+ // -----------------------------------------------------------------------
143
+ // findCalls(functionName) — find call_expression nodes for a function.
144
+ //
145
+ // Handles:
146
+ // foo() — identifier calls
147
+ // obj.method() — member expression calls
148
+ // a.b.method() — chained member expression calls
149
+ // -----------------------------------------------------------------------
150
+ findCalls(functionName) {
151
+ const callNodes = this.findNodes('call_expression');
152
+ const results = [];
153
+
154
+ for (const node of callNodes) {
155
+ const callee = node.childForFieldName('function') || node.child(0);
156
+ if (!callee) continue;
157
+
158
+ const calleeText = callee.text;
159
+
160
+ // Exact match: foo() or obj.method()
161
+ if (calleeText === functionName) {
162
+ results.push(node);
163
+ continue;
164
+ }
165
+
166
+ // Match just the method name for member expressions: *.method()
167
+ if (callee.type === 'member_expression') {
168
+ const property = callee.childForFieldName('property') || callee.child(callee.childCount - 1);
169
+ if (property && property.text === functionName) {
170
+ results.push(node);
171
+ continue;
172
+ }
173
+ }
174
+
175
+ // Match dot-separated name: e.g. "db.query"
176
+ if (functionName.includes('.') && calleeText === functionName) {
177
+ results.push(node);
178
+ }
179
+ }
180
+
181
+ return results;
182
+ }
183
+
184
+ // -----------------------------------------------------------------------
185
+ // findAssignments(variableName) — find assignments to a variable.
186
+ //
187
+ // Handles variable_declarator and assignment_expression.
188
+ // -----------------------------------------------------------------------
189
+ findAssignments(variableName) {
190
+ const results = [];
191
+
192
+ // variable_declarator: const x = ...
193
+ for (const node of this.findNodes('variable_declarator')) {
194
+ const name = node.childForFieldName('name') || node.child(0);
195
+ if (name && name.text === variableName) {
196
+ results.push(node);
197
+ }
198
+ }
199
+
200
+ // assignment_expression: x = ...
201
+ for (const node of this.findNodes('assignment_expression')) {
202
+ const left = node.childForFieldName('left') || node.child(0);
203
+ if (left && left.text === variableName) {
204
+ results.push(node);
205
+ }
206
+ }
207
+
208
+ return results;
209
+ }
210
+
211
+ // -----------------------------------------------------------------------
212
+ // getParentFunction(node) — walk up to the enclosing function node.
213
+ // -----------------------------------------------------------------------
214
+ getParentFunction(node) {
215
+ const functionTypes = new Set([
216
+ 'function_declaration',
217
+ 'function_expression',
218
+ 'arrow_function',
219
+ 'method_definition',
220
+ 'function_definition', // python
221
+ 'func_declaration', // go
222
+ ]);
223
+
224
+ let current = node.parent;
225
+ while (current) {
226
+ if (functionTypes.has(current.type)) return current;
227
+ current = current.parent;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ // -----------------------------------------------------------------------
233
+ // getScope(node) — return the innermost scope context.
234
+ // -----------------------------------------------------------------------
235
+ getScope(node) {
236
+ const scopeTypes = new Set([
237
+ 'function_declaration',
238
+ 'function_expression',
239
+ 'arrow_function',
240
+ 'method_definition',
241
+ 'class_declaration',
242
+ 'class_expression',
243
+ 'program',
244
+ 'module',
245
+ 'function_definition',
246
+ 'class_definition',
247
+ ]);
248
+
249
+ let current = node.parent;
250
+ while (current) {
251
+ if (scopeTypes.has(current.type)) {
252
+ return { type: current.type, node: current };
253
+ }
254
+ current = current.parent;
255
+ }
256
+ return { type: 'module', node: this._root };
257
+ }
258
+
259
+ // -----------------------------------------------------------------------
260
+ // isInsideTryCatch(node) — check whether node is inside a try body.
261
+ // -----------------------------------------------------------------------
262
+ isInsideTryCatch(node) {
263
+ let current = node.parent;
264
+ while (current) {
265
+ if (current.type === 'try_statement' || current.type === 'try') {
266
+ return true;
267
+ }
268
+ current = current.parent;
269
+ }
270
+ return false;
271
+ }
272
+
273
+ // -----------------------------------------------------------------------
274
+ // getArguments(callNode) — return the argument nodes of a call.
275
+ // -----------------------------------------------------------------------
276
+ getArguments(callNode) {
277
+ const args = callNode.childForFieldName('arguments');
278
+ if (!args) {
279
+ // fallback: look for an arguments node among children
280
+ for (let i = 0; i < callNode.childCount; i++) {
281
+ const child = callNode.child(i);
282
+ if (child.type === 'arguments' || child.type === 'argument_list') {
283
+ return this._extractArgChildren(child);
284
+ }
285
+ }
286
+ return [];
287
+ }
288
+ return this._extractArgChildren(args);
289
+ }
290
+
291
+ _extractArgChildren(argsNode) {
292
+ const result = [];
293
+ for (let i = 0; i < argsNode.childCount; i++) {
294
+ const child = argsNode.child(i);
295
+ // Skip punctuation (, and )
296
+ if (child.type !== ',' && child.type !== '(' && child.type !== ')') {
297
+ result.push(child);
298
+ }
299
+ }
300
+ return result;
301
+ }
302
+
303
+ // -----------------------------------------------------------------------
304
+ // isStringLiteral(node) — check if a node is a string literal.
305
+ // -----------------------------------------------------------------------
306
+ isStringLiteral(node) {
307
+ const stringTypes = new Set([
308
+ 'string',
309
+ 'string_literal',
310
+ 'template_string',
311
+ 'interpreted_string_literal',
312
+ 'raw_string_literal',
313
+ ]);
314
+ return stringTypes.has(node.type);
315
+ }
316
+
317
+ // -----------------------------------------------------------------------
318
+ // isUserInput(node) — check if node references common user-input sources.
319
+ //
320
+ // Recognises: req.query, req.body, req.params, req.headers,
321
+ // request.GET, request.POST, request.args, request.form,
322
+ // process.env, params[:...], etc.
323
+ // -----------------------------------------------------------------------
324
+ isUserInput(node) {
325
+ const text = node.text;
326
+ const patterns = [
327
+ /\breq\.(query|body|params|headers|cookies)\b/,
328
+ /\brequest\.(query|body|params|headers|cookies)\b/,
329
+ /\brequest\.(GET|POST|args|form|data|json)\b/,
330
+ /\bparams\[/,
331
+ /\bctx\.(request|query|params)\b/,
332
+ /\bc\.Query\b/, // Go gin
333
+ /\bc\.Param\b/, // Go gin
334
+ /\br\.URL\.Query\b/, // Go net/http
335
+ /\br\.FormValue\b/, // Go net/http
336
+ ];
337
+ return patterns.some((p) => p.test(text));
338
+ }
339
+
340
+ // -----------------------------------------------------------------------
341
+ // nodeText(node) — safe accessor for a node's source text.
342
+ // -----------------------------------------------------------------------
343
+ nodeText(node) {
344
+ return node ? node.text : '';
345
+ }
346
+
347
+ // -----------------------------------------------------------------------
348
+ // nodeLocation(node) — return { line, column } (1-based line).
349
+ // -----------------------------------------------------------------------
350
+ nodeLocation(node) {
351
+ return {
352
+ line: node.startPosition.row + 1,
353
+ column: node.startPosition.column,
354
+ };
355
+ }
356
+
357
+ // -----------------------------------------------------------------------
358
+ // containsConcatenation(node) — check if a node contains string
359
+ // concatenation with non-literal operands.
360
+ // -----------------------------------------------------------------------
361
+ containsConcatenation(node) {
362
+ if (node.type === 'binary_expression') {
363
+ const op = node.childForFieldName('operator');
364
+ if (op && op.text === '+') {
365
+ const left = node.childForFieldName('left');
366
+ const right = node.childForFieldName('right');
367
+ // At least one side is not a string literal
368
+ if (
369
+ (left && !this.isStringLiteral(left)) ||
370
+ (right && !this.isStringLiteral(right))
371
+ ) {
372
+ return true;
373
+ }
374
+ }
375
+ }
376
+
377
+ // Check template literals with expressions
378
+ if (node.type === 'template_string') {
379
+ for (let i = 0; i < node.childCount; i++) {
380
+ const child = node.child(i);
381
+ if (child.type === 'template_substitution') {
382
+ return true;
383
+ }
384
+ }
385
+ }
386
+
387
+ // Recurse into children
388
+ for (let i = 0; i < node.childCount; i++) {
389
+ if (this.containsConcatenation(node.child(i))) return true;
390
+ }
391
+
392
+ return false;
393
+ }
394
+
395
+ // -----------------------------------------------------------------------
396
+ // containsUserInput(node) — recursively check if any descendant is
397
+ // user input.
398
+ // -----------------------------------------------------------------------
399
+ containsUserInput(node) {
400
+ if (this.isUserInput(node)) return true;
401
+ for (let i = 0; i < node.childCount; i++) {
402
+ if (this.containsUserInput(node.child(i))) return true;
403
+ }
404
+ return false;
405
+ }
406
+
407
+ // -----------------------------------------------------------------------
408
+ // isNonLiteral(node) — check if a node is NOT a simple literal value.
409
+ // -----------------------------------------------------------------------
410
+ isNonLiteral(node) {
411
+ const literalTypes = new Set([
412
+ 'string',
413
+ 'string_literal',
414
+ 'number',
415
+ 'integer',
416
+ 'float',
417
+ 'true',
418
+ 'false',
419
+ 'null',
420
+ 'undefined',
421
+ 'template_string',
422
+ ]);
423
+
424
+ // A template_string with substitutions is non-literal
425
+ if (node.type === 'template_string') {
426
+ for (let i = 0; i < node.childCount; i++) {
427
+ if (node.child(i).type === 'template_substitution') return true;
428
+ }
429
+ return false;
430
+ }
431
+
432
+ return !literalTypes.has(node.type);
433
+ }
434
+ }
package/src/auth.js ADDED
@@ -0,0 +1,149 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const AUTH_DIR = join(homedir(), '.doorman');
6
+ const AUTH_FILE = join(AUTH_DIR, 'auth.json');
7
+
8
+ // Plans
9
+ const PLANS = {
10
+ free: {
11
+ name: 'Free', price: '$0', maxScans: 5,
12
+ categories: ['security', 'bugs'],
13
+ },
14
+ pro: {
15
+ name: 'Pro', price: '$20/mo', maxScans: Infinity,
16
+ categories: ['security', 'bugs', 'performance', 'reliability', 'cost', 'data', 'quality'],
17
+ dashboard: true, autoRun: true, cicd: true,
18
+ },
19
+ enterprise: {
20
+ name: 'Enterprise', price: '$100/mo', maxScans: Infinity,
21
+ categories: 'all',
22
+ dashboard: true, autoRun: true, cicd: true,
23
+ compliance: true, teamDashboard: true, prComments: true, slack: true, customRules: true,
24
+ },
25
+ };
26
+
27
+ const FREE_MONTHLY_SCANS = 5;
28
+
29
+ /**
30
+ * Load saved account from ~/.doorman/auth.json
31
+ */
32
+ export function loadAuth() {
33
+ if (!existsSync(AUTH_FILE)) return null;
34
+ try {
35
+ return JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Save account locally.
43
+ */
44
+ function saveAuthData(data) {
45
+ if (!existsSync(AUTH_DIR)) mkdirSync(AUTH_DIR, { recursive: true });
46
+ writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + '\n');
47
+ }
48
+
49
+ /**
50
+ * Save account by email.
51
+ */
52
+ export function saveAuth(email) {
53
+ const existing = loadAuth();
54
+ const data = {
55
+ email,
56
+ plan: existing?.plan || 'free',
57
+ scansThisMonth: existing?.scansThisMonth || 0,
58
+ monthKey: getCurrentMonthKey(),
59
+ savedAt: new Date().toISOString(),
60
+ };
61
+ saveAuthData(data);
62
+ return data;
63
+ }
64
+
65
+ /**
66
+ * Get current month key (YYYY-MM) for tracking monthly scan count.
67
+ */
68
+ function getCurrentMonthKey() {
69
+ const now = new Date();
70
+ return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
71
+ }
72
+
73
+ /**
74
+ * Record a scan and check if the user has scans remaining.
75
+ * Returns { allowed: boolean, remaining: number, plan: string }
76
+ */
77
+ export function recordScanUsage() {
78
+ const bypass = process.env.DOORMAN_BYPASS === '1';
79
+ if (bypass) return { allowed: true, remaining: Infinity, plan: 'bypass' };
80
+
81
+ let auth = loadAuth();
82
+
83
+ // No account yet — create one
84
+ if (!auth) {
85
+ auth = { plan: 'free', scansThisMonth: 0, monthKey: getCurrentMonthKey(), savedAt: new Date().toISOString() };
86
+ saveAuthData(auth);
87
+ }
88
+
89
+ const plan = PLANS[auth.plan] || PLANS.free;
90
+
91
+ // Reset count if new month
92
+ if (auth.monthKey !== getCurrentMonthKey()) {
93
+ auth.scansThisMonth = 0;
94
+ auth.monthKey = getCurrentMonthKey();
95
+ }
96
+
97
+ const maxScans = plan.maxScans ?? FREE_MONTHLY_SCANS;
98
+ const remaining = maxScans - auth.scansThisMonth;
99
+
100
+ if (remaining <= 0 && maxScans !== Infinity) {
101
+ return { allowed: false, remaining: 0, plan: plan.name };
102
+ }
103
+
104
+ // Record this scan
105
+ auth.scansThisMonth = (auth.scansThisMonth || 0) + 1;
106
+ saveAuthData(auth);
107
+
108
+ return {
109
+ allowed: true,
110
+ remaining: maxScans === Infinity ? Infinity : maxScans - auth.scansThisMonth,
111
+ plan: plan.name,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Get the user's plan and which categories they can see.
117
+ */
118
+ export function getUserPlan(email) {
119
+ const auth = loadAuth();
120
+ if (!auth || auth.email !== email) return PLANS.free;
121
+ const planId = auth.plan || 'free';
122
+ return PLANS[planId] || PLANS.free;
123
+ }
124
+
125
+ /**
126
+ * Get the user's scan usage info without recording a scan.
127
+ */
128
+ export function getScanUsage() {
129
+ const auth = loadAuth();
130
+ if (!auth) return { scansUsed: 0, maxScans: FREE_MONTHLY_SCANS, plan: 'Free' };
131
+
132
+ const plan = PLANS[auth.plan] || PLANS.free;
133
+ const monthKey = getCurrentMonthKey();
134
+ const scansUsed = auth.monthKey === monthKey ? (auth.scansThisMonth || 0) : 0;
135
+
136
+ return {
137
+ scansUsed,
138
+ maxScans: plan.maxScans ?? FREE_MONTHLY_SCANS,
139
+ plan: plan.name,
140
+ email: auth.email,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get all plan definitions.
146
+ */
147
+ export function getPlans() {
148
+ return PLANS;
149
+ }
@@ -0,0 +1,48 @@
1
+ // Baseline mode: save/load/diff findings to show only NEW issues
2
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
3
+ import { createHash } from 'crypto';
4
+
5
+ const DEFAULT_PATH = '.doorman-baseline.json';
6
+
7
+ /**
8
+ * Create a fingerprint for a finding (file + rule + line range)
9
+ */
10
+ function fingerprint(f) {
11
+ return createHash('sha256')
12
+ .update(`${f.ruleId}::${f.file}::${Math.floor((f.line || 0) / 5)}`)
13
+ .digest('hex')
14
+ .slice(0, 32);
15
+ }
16
+
17
+ /**
18
+ * Save current findings as baseline
19
+ */
20
+ export function saveBaseline(findings, path = DEFAULT_PATH) {
21
+ const baseline = {
22
+ version: 1,
23
+ createdAt: new Date().toISOString(),
24
+ count: findings.length,
25
+ fingerprints: findings.map(fingerprint),
26
+ };
27
+ writeFileSync(path, JSON.stringify(baseline, null, 2) + '\n');
28
+ return baseline;
29
+ }
30
+
31
+ /**
32
+ * Load existing baseline
33
+ */
34
+ export function loadBaseline(path = DEFAULT_PATH) {
35
+ if (!existsSync(path)) return null;
36
+ try {
37
+ return JSON.parse(readFileSync(path, 'utf-8'));
38
+ } catch { return null; }
39
+ }
40
+
41
+ /**
42
+ * Filter findings to only show NEW ones (not in baseline)
43
+ */
44
+ export function diffFindings(findings, baseline) {
45
+ if (!baseline || !baseline.fingerprints) return findings;
46
+ const known = new Set(baseline.fingerprints);
47
+ return findings.filter(f => !known.has(fingerprint(f)));
48
+ }