guardrail-core 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 (189) hide show
  1. package/dist/__tests__/autopilot.test.d.ts +7 -0
  2. package/dist/__tests__/autopilot.test.d.ts.map +1 -0
  3. package/dist/__tests__/autopilot.test.js +156 -0
  4. package/dist/__tests__/tier-config.test.d.ts +9 -0
  5. package/dist/__tests__/tier-config.test.d.ts.map +1 -0
  6. package/dist/__tests__/tier-config.test.js +230 -0
  7. package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
  8. package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
  9. package/dist/__tests__/utils/hash-inline.test.js +62 -0
  10. package/dist/__tests__/utils/hash.test.d.ts +3 -0
  11. package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
  12. package/dist/__tests__/utils/hash.test.js +95 -0
  13. package/dist/__tests__/utils/simple.test.d.ts +1 -0
  14. package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
  15. package/dist/__tests__/utils/simple.test.js +10 -0
  16. package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
  17. package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
  18. package/dist/__tests__/utils/utils-simple.test.js +6 -0
  19. package/dist/__tests__/utils/utils.test.d.ts +15 -0
  20. package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
  21. package/dist/__tests__/utils/utils.test.js +172 -0
  22. package/dist/autopilot/autopilot-runner.d.ts +33 -0
  23. package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
  24. package/dist/autopilot/autopilot-runner.js +479 -0
  25. package/dist/autopilot/index.d.ts +6 -0
  26. package/dist/autopilot/index.d.ts.map +1 -0
  27. package/dist/autopilot/index.js +25 -0
  28. package/dist/autopilot/types.d.ts +102 -0
  29. package/dist/autopilot/types.d.ts.map +1 -0
  30. package/dist/autopilot/types.js +18 -0
  31. package/dist/cache/index.d.ts +7 -0
  32. package/dist/cache/index.d.ts.map +1 -0
  33. package/dist/cache/index.js +22 -0
  34. package/dist/cache/redis-cache.d.ts +145 -0
  35. package/dist/cache/redis-cache.d.ts.map +1 -0
  36. package/dist/cache/redis-cache.js +459 -0
  37. package/dist/ci/github-actions.d.ts +77 -0
  38. package/dist/ci/github-actions.d.ts.map +1 -0
  39. package/dist/ci/github-actions.js +277 -0
  40. package/dist/ci/index.d.ts +12 -0
  41. package/dist/ci/index.d.ts.map +1 -0
  42. package/dist/ci/index.js +27 -0
  43. package/dist/ci/pre-commit.d.ts +65 -0
  44. package/dist/ci/pre-commit.d.ts.map +1 -0
  45. package/dist/ci/pre-commit.js +286 -0
  46. package/dist/entitlements.d.ts +149 -0
  47. package/dist/entitlements.d.ts.map +1 -0
  48. package/dist/entitlements.js +464 -0
  49. package/dist/env.d.ts +113 -0
  50. package/dist/env.d.ts.map +1 -0
  51. package/dist/env.js +204 -0
  52. package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
  53. package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
  54. package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
  55. package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
  56. package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
  57. package/dist/fix-packs/generate-fix-packs.js +505 -0
  58. package/dist/fix-packs/index.d.ts +8 -0
  59. package/dist/fix-packs/index.d.ts.map +1 -0
  60. package/dist/fix-packs/index.js +23 -0
  61. package/dist/fix-packs/types.d.ts +113 -0
  62. package/dist/fix-packs/types.d.ts.map +1 -0
  63. package/dist/fix-packs/types.js +71 -0
  64. package/dist/index.d.ts +13 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +28 -0
  67. package/dist/metrics/prometheus.d.ts +99 -0
  68. package/dist/metrics/prometheus.d.ts.map +1 -0
  69. package/dist/metrics/prometheus.js +306 -0
  70. package/dist/quota-ledger.d.ts +119 -0
  71. package/dist/quota-ledger.d.ts.map +1 -0
  72. package/dist/quota-ledger.js +462 -0
  73. package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
  74. package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
  75. package/dist/rbac/__tests__/permissions.test.js +350 -0
  76. package/dist/rbac/index.d.ts +9 -0
  77. package/dist/rbac/index.d.ts.map +1 -0
  78. package/dist/rbac/index.js +32 -0
  79. package/dist/rbac/permissions.d.ts +71 -0
  80. package/dist/rbac/permissions.d.ts.map +1 -0
  81. package/dist/rbac/permissions.js +247 -0
  82. package/dist/rbac/types.d.ts +69 -0
  83. package/dist/rbac/types.d.ts.map +1 -0
  84. package/dist/rbac/types.js +213 -0
  85. package/dist/tier-config.d.ts +203 -0
  86. package/dist/tier-config.d.ts.map +1 -0
  87. package/dist/tier-config.js +675 -0
  88. package/dist/types.d.ts +365 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +5 -0
  91. package/dist/utils.d.ts +36 -0
  92. package/dist/utils.d.ts.map +1 -0
  93. package/dist/utils.js +127 -0
  94. package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
  95. package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
  96. package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
  97. package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
  98. package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
  99. package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
  100. package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
  101. package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
  102. package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
  103. package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
  104. package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
  105. package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
  106. package/dist/verified-autofix/format-validator.d.ts +101 -0
  107. package/dist/verified-autofix/format-validator.d.ts.map +1 -0
  108. package/dist/verified-autofix/format-validator.js +446 -0
  109. package/dist/verified-autofix/index.d.ts +14 -0
  110. package/dist/verified-autofix/index.d.ts.map +1 -0
  111. package/dist/verified-autofix/index.js +39 -0
  112. package/dist/verified-autofix/pipeline.d.ts +68 -0
  113. package/dist/verified-autofix/pipeline.d.ts.map +1 -0
  114. package/dist/verified-autofix/pipeline.js +330 -0
  115. package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
  116. package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
  117. package/dist/verified-autofix/repo-fingerprint.js +396 -0
  118. package/dist/verified-autofix/workspace.d.ts +83 -0
  119. package/dist/verified-autofix/workspace.d.ts.map +1 -0
  120. package/dist/verified-autofix/workspace.js +454 -0
  121. package/dist/verified-autofix.d.ts +182 -0
  122. package/dist/verified-autofix.d.ts.map +1 -0
  123. package/dist/verified-autofix.js +1021 -0
  124. package/dist/visualization/dependency-graph.d.ts +79 -0
  125. package/dist/visualization/dependency-graph.d.ts.map +1 -0
  126. package/dist/visualization/dependency-graph.js +399 -0
  127. package/dist/visualization/index.d.ts +5 -0
  128. package/dist/visualization/index.d.ts.map +1 -0
  129. package/dist/visualization/index.js +20 -0
  130. package/package.json +29 -0
  131. package/src/__tests__/autopilot.test.ts +196 -0
  132. package/src/__tests__/tier-config.test.ts +289 -0
  133. package/src/__tests__/utils/hash-inline.test.ts +76 -0
  134. package/src/__tests__/utils/hash.test.ts +119 -0
  135. package/src/__tests__/utils/simple.test.ts +10 -0
  136. package/src/__tests__/utils/utils-simple.test.ts +5 -0
  137. package/src/__tests__/utils/utils.test.ts +203 -0
  138. package/src/autopilot/autopilot-runner.ts +503 -0
  139. package/src/autopilot/index.ts +6 -0
  140. package/src/autopilot/types.ts +119 -0
  141. package/src/cache/index.ts +7 -0
  142. package/src/cache/redis-cache.d.ts +155 -0
  143. package/src/cache/redis-cache.d.ts.map +1 -0
  144. package/src/cache/redis-cache.ts +517 -0
  145. package/src/ci/github-actions.ts +335 -0
  146. package/src/ci/index.ts +12 -0
  147. package/src/ci/pre-commit.ts +338 -0
  148. package/src/db/usage-schema.prisma +114 -0
  149. package/src/entitlements.ts +570 -0
  150. package/src/env.d.ts +68 -0
  151. package/src/env.d.ts.map +1 -0
  152. package/src/env.ts +247 -0
  153. package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
  154. package/src/fix-packs/generate-fix-packs.ts +577 -0
  155. package/src/fix-packs/index.ts +8 -0
  156. package/src/fix-packs/types.ts +206 -0
  157. package/src/index.d.ts +7 -0
  158. package/src/index.d.ts.map +1 -0
  159. package/src/index.ts +12 -0
  160. package/src/metrics/prometheus.d.ts +104 -0
  161. package/src/metrics/prometheus.d.ts.map +1 -0
  162. package/src/metrics/prometheus.ts +446 -0
  163. package/src/quota-ledger.ts +548 -0
  164. package/src/rbac/__tests__/permissions.test.ts +446 -0
  165. package/src/rbac/index.ts +46 -0
  166. package/src/rbac/permissions.ts +301 -0
  167. package/src/rbac/types.ts +298 -0
  168. package/src/tier-config.json +157 -0
  169. package/src/tier-config.ts +815 -0
  170. package/src/types.d.ts +365 -0
  171. package/src/types.d.ts.map +1 -0
  172. package/src/types.ts +441 -0
  173. package/src/utils.d.ts +36 -0
  174. package/src/utils.d.ts.map +1 -0
  175. package/src/utils.ts +140 -0
  176. package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
  177. package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
  178. package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
  179. package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
  180. package/src/verified-autofix/format-validator.ts +517 -0
  181. package/src/verified-autofix/index.ts +63 -0
  182. package/src/verified-autofix/pipeline.ts +403 -0
  183. package/src/verified-autofix/repo-fingerprint.ts +459 -0
  184. package/src/verified-autofix/workspace.ts +531 -0
  185. package/src/verified-autofix.ts +1187 -0
  186. package/src/visualization/dependency-graph.d.ts +85 -0
  187. package/src/visualization/dependency-graph.d.ts.map +1 -0
  188. package/src/visualization/dependency-graph.ts +495 -0
  189. package/src/visualization/index.ts +5 -0
package/src/env.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ declare const envSchema: z.ZodObject<
3
+ {
4
+ NODE_ENV: z.ZodDefault<z.ZodEnum<["development", "production", "test"]>>;
5
+ PORT: z.ZodDefault<z.ZodEffects<z.ZodString, number, string>>;
6
+ HOST: z.ZodDefault<z.ZodString>;
7
+ DATABASE_URL: z.ZodString;
8
+ JWT_SECRET: z.ZodString;
9
+ OPENAI_API_KEY: z.ZodOptional<z.ZodString>;
10
+ REDIS_URL: z.ZodOptional<z.ZodString>;
11
+ CORS_ORIGIN: z.ZodDefault<z.ZodString>;
12
+ RATE_LIMIT_WINDOW_MS: z.ZodDefault<
13
+ z.ZodEffects<z.ZodString, number, string>
14
+ >;
15
+ RATE_LIMIT_MAX_REQUESTS: z.ZodDefault<
16
+ z.ZodEffects<z.ZodString, number, string>
17
+ >;
18
+ GITHUB_WEBHOOK_SECRET: z.ZodOptional<z.ZodString>;
19
+ SENTRY_DSN: z.ZodOptional<z.ZodString>;
20
+ ENABLE_METRICS: z.ZodDefault<z.ZodEffects<z.ZodString, boolean, string>>;
21
+ ENABLE_AI_FEATURES: z.ZodDefault<
22
+ z.ZodEffects<z.ZodString, boolean, string>
23
+ >;
24
+ },
25
+ "strip",
26
+ z.ZodTypeAny,
27
+ {
28
+ NODE_ENV: "development" | "production" | "test";
29
+ PORT: number;
30
+ HOST: string;
31
+ DATABASE_URL: string;
32
+ JWT_SECRET: string;
33
+ CORS_ORIGIN: string;
34
+ RATE_LIMIT_WINDOW_MS: number;
35
+ RATE_LIMIT_MAX_REQUESTS: number;
36
+ ENABLE_METRICS: boolean;
37
+ ENABLE_AI_FEATURES: boolean;
38
+ OPENAI_API_KEY?: string | undefined;
39
+ REDIS_URL?: string | undefined;
40
+ GITHUB_WEBHOOK_SECRET?: string | undefined;
41
+ SENTRY_DSN?: string | undefined;
42
+ },
43
+ {
44
+ DATABASE_URL: string;
45
+ JWT_SECRET: string;
46
+ NODE_ENV?: "development" | "production" | "test" | undefined;
47
+ PORT?: string | undefined;
48
+ HOST?: string | undefined;
49
+ OPENAI_API_KEY?: string | undefined;
50
+ REDIS_URL?: string | undefined;
51
+ CORS_ORIGIN?: string | undefined;
52
+ RATE_LIMIT_WINDOW_MS?: string | undefined;
53
+ RATE_LIMIT_MAX_REQUESTS?: string | undefined;
54
+ GITHUB_WEBHOOK_SECRET?: string | undefined;
55
+ SENTRY_DSN?: string | undefined;
56
+ ENABLE_METRICS?: string | undefined;
57
+ ENABLE_AI_FEATURES?: string | undefined;
58
+ }
59
+ >;
60
+ export type Env = z.infer<typeof envSchema>;
61
+ export declare function validateEnv(): Env;
62
+ export declare function getEnv(): Env;
63
+ export declare function checkRequiredEnv(): void;
64
+ export declare function isDevelopment(): boolean;
65
+ export declare function isProduction(): boolean;
66
+ export declare function isTest(): boolean;
67
+ export {};
68
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,QAAA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCb,CAAC;AAGH,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAG5C,wBAAgB,WAAW,IAAI,GAAG,CAajC;AAID,wBAAgB,MAAM,IAAI,GAAG,CAK5B;AAGD,wBAAgB,gBAAgB,IAAI,IAAI,CASvC;AAGD,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED,wBAAgB,MAAM,IAAI,OAAO,CAEhC"}
package/src/env.ts ADDED
@@ -0,0 +1,247 @@
1
+ import { z } from 'zod';
2
+
3
+ // Build DATABASE_URL from individual PG* variables if DATABASE_URL is invalid
4
+ function getDatabaseUrl(): string {
5
+ const dbUrl = process.env['DATABASE_URL'];
6
+
7
+ // Check if DATABASE_URL is valid (starts with postgresql:// or postgres://)
8
+ if (dbUrl && (dbUrl.startsWith('postgresql://') || dbUrl.startsWith('postgres://'))) {
9
+ return dbUrl;
10
+ }
11
+
12
+ // Fallback: Build from individual PG* variables (Replit provides these)
13
+ const pgHost = process.env['PGHOST'];
14
+ const pgPort = process.env['PGPORT'] || '5432';
15
+ const pgUser = process.env['PGUSER'] || 'postgres';
16
+ const pgPassword = process.env['PGPASSWORD'] || 'password';
17
+ const pgDatabase = process.env['PGDATABASE'];
18
+
19
+ if (pgHost && pgDatabase) {
20
+ const constructedUrl = `postgresql://${pgUser}:${pgPassword}@${pgHost}:${pgPort}/${pgDatabase}`;
21
+ // Update process.env so other parts of the app can use it
22
+ process.env['DATABASE_URL'] = constructedUrl;
23
+ return constructedUrl;
24
+ }
25
+
26
+ // Return original (will fail validation but with helpful error)
27
+ return dbUrl || '';
28
+ }
29
+
30
+ // Environment variable schema
31
+ const envSchema = z.object({
32
+ // Node environment
33
+ NODE_ENV: z.enum(['development', 'production', 'test', 'staging']).default('development'),
34
+
35
+ // Server configuration
36
+ PORT: z.string().transform(Number).default('3000'),
37
+ HOST: z.string().default('localhost'),
38
+
39
+ // Database - use custom getter that falls back to PG* variables
40
+ DATABASE_URL: z.string().url('Invalid database URL format'),
41
+
42
+ // Authentication
43
+ JWT_SECRET: z.string().min(32, 'JWT secret must be at least 32 characters'),
44
+ JWT_REFRESH_SECRET: z.string().optional(),
45
+ JWT_EXPIRES_IN: z.string().default('15m'),
46
+ JWT_REFRESH_EXPIRES_IN: z.string().default('7d'),
47
+
48
+ // GitHub OAuth (required in production)
49
+ GITHUB_CLIENT_ID: z.string().optional(),
50
+ GITHUB_CLIENT_SECRET: z.string().optional(),
51
+ GITHUB_CALLBACK_URL: z.string().url().optional(),
52
+ GITHUB_WEBHOOK_SECRET: z.string().optional(),
53
+
54
+ // Stripe (required for billing)
55
+ STRIPE_SECRET_KEY: z.string().optional(),
56
+ STRIPE_PUBLISHABLE_KEY: z.string().optional(),
57
+ STRIPE_WEBHOOK_SECRET: z.string().optional(),
58
+ STRIPE_PRICE_ID_STARTER: z.string().optional(),
59
+ STRIPE_PRICE_ID_PRO: z.string().optional(),
60
+ STRIPE_PRICE_ID_ENTERPRISE: z.string().optional(),
61
+
62
+ // API URLs
63
+ API_BASE_URL: z.string().url().optional(),
64
+ NEXT_PUBLIC_API_URL: z.string().url().optional(),
65
+ NEXT_PUBLIC_APP_URL: z.string().url().optional(),
66
+
67
+ // OpenAI (optional for development)
68
+ OPENAI_API_KEY: z.string().optional(),
69
+
70
+ // Redis (optional)
71
+ REDIS_URL: z.string().optional(),
72
+
73
+ // CORS
74
+ CORS_ORIGIN: z.string().default('http://localhost:3000'),
75
+
76
+ // API Rate Limiting
77
+ RATE_LIMIT_WINDOW_MS: z.string().transform(Number).default('900000'),
78
+ RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).default('100'),
79
+
80
+ // Monitoring
81
+ SENTRY_DSN: z.string().optional(),
82
+ SENTRY_ORG: z.string().optional(),
83
+ SENTRY_PROJECT: z.string().optional(),
84
+
85
+ // Feature flags
86
+ ENABLE_METRICS: z.string().transform(val => val === 'true').default('false'),
87
+ ENABLE_AI_FEATURES: z.string().transform(val => val === 'true').default('true'),
88
+ Guardrail_DEMO_MODE: z.string().transform(val => val === 'true').default('false'),
89
+ Guardrail_POLICY_STRICT: z.string().transform(val => val === 'true').default('false'),
90
+ });
91
+
92
+ // Type for validated environment
93
+ export type Env = z.infer<typeof envSchema>;
94
+
95
+ // Validate and parse environment variables
96
+ export function validateEnv(): Env {
97
+ try {
98
+ // Fix DATABASE_URL before validation if needed
99
+ getDatabaseUrl();
100
+
101
+ const env = envSchema.parse(process.env);
102
+
103
+ // Additional production/staging checks
104
+ const isProdLike = env.NODE_ENV === 'production' || env.NODE_ENV === 'staging';
105
+
106
+ if (isProdLike) {
107
+ const productionErrors: string[] = [];
108
+ const productionWarnings: string[] = [];
109
+
110
+ // =======================================================================
111
+ // CRITICAL: Required for production
112
+ // =======================================================================
113
+
114
+ // GitHub OAuth is optional for now
115
+ // if (!process.env['GITHUB_CLIENT_ID']) {
116
+ // productionErrors.push('GITHUB_CLIENT_ID is required in production');
117
+ // }
118
+
119
+ // if (!process.env['GITHUB_CLIENT_SECRET']) {
120
+ // productionErrors.push('GITHUB_CLIENT_SECRET is required in production');
121
+ // }
122
+
123
+ // API URL must be configured
124
+ if (!process.env['API_BASE_URL'] && !process.env['NEXT_PUBLIC_API_URL']) {
125
+ productionErrors.push('API_BASE_URL or NEXT_PUBLIC_API_URL is required in production');
126
+ }
127
+
128
+ // =======================================================================
129
+ // SECURITY: No localhost in production
130
+ // =======================================================================
131
+
132
+ if (env.CORS_ORIGIN.includes('localhost')) {
133
+ productionErrors.push('CORS_ORIGIN should not include localhost in production');
134
+ }
135
+
136
+ if (env.DATABASE_URL.includes('localhost')) {
137
+ productionErrors.push('DATABASE_URL should not use localhost in production');
138
+ }
139
+
140
+ if (env.HOST === 'localhost') {
141
+ productionErrors.push('HOST should not be localhost in production (use 0.0.0.0)');
142
+ }
143
+
144
+ // Check callback URLs don't use localhost
145
+ if (process.env['GITHUB_CALLBACK_URL']?.includes('localhost')) {
146
+ productionErrors.push('GITHUB_CALLBACK_URL should not use localhost in production');
147
+ }
148
+
149
+ // =======================================================================
150
+ // WARNINGS: Recommended but not blocking
151
+ // =======================================================================
152
+
153
+ // Stripe is needed for billing
154
+ if (!process.env['STRIPE_SECRET_KEY']) {
155
+ productionWarnings.push('STRIPE_SECRET_KEY not set - billing features will be disabled');
156
+ }
157
+
158
+ if (!process.env['STRIPE_WEBHOOK_SECRET']) {
159
+ productionWarnings.push('STRIPE_WEBHOOK_SECRET not set - webhook verification disabled');
160
+ }
161
+
162
+ // Monitoring - disabled silently
163
+ // if (!process.env['SENTRY_DSN']) {
164
+ // productionWarnings.push('SENTRY_DSN not set - error monitoring disabled');
165
+ // }
166
+
167
+ // Redis for caching/sessions - disabled silently
168
+ // if (!process.env['REDIS_URL']) {
169
+ // productionWarnings.push('REDIS_URL not set - using in-memory caching');
170
+ // }
171
+
172
+ // Output warnings
173
+ if (productionWarnings.length > 0) {
174
+ console.warn('\n⚠️ Production warnings:');
175
+ productionWarnings.forEach(warning => console.warn(` - ${warning}`));
176
+ console.warn('');
177
+ }
178
+
179
+ // Exit on errors
180
+ if (productionErrors.length > 0) {
181
+ console.error('\n❌ Production environment validation failed:');
182
+ productionErrors.forEach(error => console.error(` - ${error}`));
183
+ console.error('\n🚨 Application will exit due to invalid production configuration.\n');
184
+ process.exit(1);
185
+ }
186
+ }
187
+
188
+ return env;
189
+ } catch (error) {
190
+ if (error instanceof z.ZodError) {
191
+ console.error('❌ Invalid environment variables:');
192
+ error.errors.forEach(err => {
193
+ console.error(` - ${err.path.join('.')}: ${err.message}`);
194
+ });
195
+
196
+ // Always exit on validation errors
197
+ console.error('\n🚨 Application will exit due to invalid configuration.\n');
198
+ process.exit(1);
199
+ }
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ // Get validated environment (cached)
205
+ let cachedEnv: Env | null = null;
206
+ export function getEnv(): Env {
207
+ if (!cachedEnv) {
208
+ cachedEnv = validateEnv();
209
+ }
210
+ return cachedEnv;
211
+ }
212
+
213
+ // Check if required environment variables are set
214
+ export function checkRequiredEnv(): void {
215
+ const isProd = process.env['NODE_ENV'] === 'production';
216
+ const required = ['DATABASE_URL', 'JWT_SECRET'];
217
+
218
+ // Additional requirements in production
219
+ if (isProd) {
220
+ required.push('GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET');
221
+ }
222
+
223
+ const missing = required.filter(key => !process.env[key]);
224
+
225
+ if (missing.length > 0) {
226
+ console.error('❌ Missing required environment variables:');
227
+ missing.forEach(key => console.error(` - ${key}`));
228
+
229
+ if (isProd) {
230
+ console.error('\n🚨 Production deployment requires all environment variables to be set.\n');
231
+ process.exit(1);
232
+ }
233
+ }
234
+ }
235
+
236
+ // Development environment check
237
+ export function isDevelopment(): boolean {
238
+ return getEnv().NODE_ENV === 'development';
239
+ }
240
+
241
+ export function isProduction(): boolean {
242
+ return getEnv().NODE_ENV === 'production';
243
+ }
244
+
245
+ export function isTest(): boolean {
246
+ return getEnv().NODE_ENV === 'test';
247
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Fix Packs Generator Tests
3
+ *
4
+ * Tests for deterministic grouping and fix pack generation.
5
+ */
6
+
7
+ import {
8
+ generateFixPacks,
9
+ parseFindingsFromScanOutput,
10
+ } from '../generate-fix-packs';
11
+ import {
12
+ Finding,
13
+ FindingCategory,
14
+ SeverityLevel,
15
+ compareSeverity,
16
+ sortPacksBySeverity,
17
+ generatePackId,
18
+ } from '../types';
19
+
20
+ describe('generateFixPacks', () => {
21
+ const mockRepoFingerprint = {
22
+ id: 'repo-abc123',
23
+ name: 'test-repo',
24
+ hasTypeScript: true,
25
+ hasTests: true,
26
+ packageManager: 'pnpm' as const,
27
+ hash: 'abc123def456',
28
+ };
29
+
30
+ const createFinding = (
31
+ id: string,
32
+ category: FindingCategory,
33
+ severity: SeverityLevel,
34
+ file: string
35
+ ): Finding => ({
36
+ id,
37
+ category,
38
+ severity,
39
+ title: `Test finding ${id}`,
40
+ description: `Description for ${id}`,
41
+ file,
42
+ line: 10,
43
+ });
44
+
45
+ describe('deterministic grouping', () => {
46
+ it('should produce identical output for identical input', () => {
47
+ const findings: Finding[] = [
48
+ createFinding('f1', 'secrets', 'critical', 'src/config.ts'),
49
+ createFinding('f2', 'secrets', 'high', 'src/auth.ts'),
50
+ createFinding('f3', 'routes', 'medium', 'src/routes/api.ts'),
51
+ ];
52
+
53
+ const result1 = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
54
+ const result2 = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
55
+
56
+ expect(result1.packs.length).toBe(result2.packs.length);
57
+ expect(result1.packs.map(p => p.id)).toEqual(result2.packs.map(p => p.id));
58
+ });
59
+
60
+ it('should produce stable pack IDs across runs', () => {
61
+ const findings: Finding[] = [
62
+ createFinding('f1', 'mocks', 'medium', 'src/test/mock.ts'),
63
+ createFinding('f2', 'mocks', 'medium', 'src/test/fixture.ts'),
64
+ ];
65
+
66
+ const result1 = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
67
+ const result2 = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
68
+
69
+ expect(result1.packs[0]!.id).toBe(result2.packs[0]!.id);
70
+ });
71
+
72
+ it('should maintain order independence for same findings', () => {
73
+ const findings1: Finding[] = [
74
+ createFinding('f1', 'secrets', 'critical', 'src/a.ts'),
75
+ createFinding('f2', 'secrets', 'high', 'src/b.ts'),
76
+ ];
77
+
78
+ const findings2: Finding[] = [
79
+ createFinding('f2', 'secrets', 'high', 'src/b.ts'),
80
+ createFinding('f1', 'secrets', 'critical', 'src/a.ts'),
81
+ ];
82
+
83
+ const result1 = generateFixPacks({ findings: findings1, repoFingerprint: mockRepoFingerprint });
84
+ const result2 = generateFixPacks({ findings: findings2, repoFingerprint: mockRepoFingerprint });
85
+
86
+ expect(result1.packs.length).toBe(result2.packs.length);
87
+ expect(result1.packs[0]!.findings.length).toBe(result2.packs[0]!.findings.length);
88
+ });
89
+ });
90
+
91
+ describe('category grouping', () => {
92
+ it('should group findings by category', () => {
93
+ const findings: Finding[] = [
94
+ createFinding('f1', 'secrets', 'critical', 'src/a.ts'),
95
+ createFinding('f2', 'routes', 'medium', 'src/b.ts'),
96
+ createFinding('f3', 'secrets', 'high', 'src/c.ts'),
97
+ ];
98
+
99
+ const result = generateFixPacks({
100
+ findings,
101
+ repoFingerprint: mockRepoFingerprint,
102
+ groupByCategory: true,
103
+ });
104
+
105
+ const secretsPack = result.packs.find(p => p.category === 'secrets');
106
+ const routesPack = result.packs.find(p => p.category === 'routes');
107
+
108
+ expect(secretsPack).toBeDefined();
109
+ expect(routesPack).toBeDefined();
110
+ expect(secretsPack!.findings.length).toBe(2);
111
+ expect(routesPack!.findings.length).toBe(1);
112
+ });
113
+
114
+ it('should prioritize categories correctly', () => {
115
+ const findings: Finding[] = [
116
+ createFinding('f1', 'placeholders', 'medium', 'src/a.ts'),
117
+ createFinding('f2', 'secrets', 'critical', 'src/b.ts'),
118
+ createFinding('f3', 'auth', 'high', 'src/c.ts'),
119
+ ];
120
+
121
+ const result = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
122
+
123
+ expect(result.packs[0]!.category).toBe('secrets');
124
+ expect(result.packs[1]!.category).toBe('auth');
125
+ expect(result.packs[2]!.category).toBe('placeholders');
126
+ });
127
+ });
128
+
129
+ describe('severity sorting', () => {
130
+ it('should sort packs by severity (highest first)', () => {
131
+ const findings: Finding[] = [
132
+ createFinding('f1', 'routes', 'low', 'src/a.ts'),
133
+ createFinding('f2', 'mocks', 'critical', 'src/b.ts'),
134
+ createFinding('f3', 'deps', 'medium', 'src/c.ts'),
135
+ ];
136
+
137
+ const result = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
138
+
139
+ expect(result.packs[0]!.severity).toBe('critical');
140
+ });
141
+
142
+ it('should assign pack severity based on highest finding severity', () => {
143
+ const findings: Finding[] = [
144
+ createFinding('f1', 'secrets', 'low', 'src/a.ts'),
145
+ createFinding('f2', 'secrets', 'critical', 'src/b.ts'),
146
+ createFinding('f3', 'secrets', 'medium', 'src/c.ts'),
147
+ ];
148
+
149
+ const result = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
150
+ const secretsPack = result.packs.find(p => p.category === 'secrets');
151
+
152
+ expect(secretsPack!.severity).toBe('critical');
153
+ });
154
+ });
155
+
156
+ describe('pack size limits', () => {
157
+ it('should respect maxPackSize', () => {
158
+ const findings: Finding[] = Array.from({ length: 25 }, (_, i) =>
159
+ createFinding(`f${i}`, 'secrets', 'medium', `src/file${i}.ts`)
160
+ );
161
+
162
+ const result = generateFixPacks({
163
+ findings,
164
+ repoFingerprint: mockRepoFingerprint,
165
+ maxPackSize: 10,
166
+ });
167
+
168
+ result.packs.forEach(pack => {
169
+ expect(pack.findings.length).toBeLessThanOrEqual(10);
170
+ });
171
+ });
172
+
173
+ it('should respect minPackSize', () => {
174
+ const findings: Finding[] = [
175
+ createFinding('f1', 'secrets', 'medium', 'src/a.ts'),
176
+ ];
177
+
178
+ const result = generateFixPacks({
179
+ findings,
180
+ repoFingerprint: mockRepoFingerprint,
181
+ minPackSize: 2,
182
+ });
183
+
184
+ expect(result.ungrouped.length).toBe(1);
185
+ expect(result.packs.length).toBe(0);
186
+ });
187
+ });
188
+
189
+ describe('empty input', () => {
190
+ it('should handle empty findings array', () => {
191
+ const result = generateFixPacks({
192
+ findings: [],
193
+ repoFingerprint: mockRepoFingerprint,
194
+ });
195
+
196
+ expect(result.packs).toEqual([]);
197
+ expect(result.ungrouped).toEqual([]);
198
+ expect(result.stats.totalFindings).toBe(0);
199
+ expect(result.stats.totalPacks).toBe(0);
200
+ });
201
+ });
202
+
203
+ describe('stats calculation', () => {
204
+ it('should calculate correct stats', () => {
205
+ const findings: Finding[] = [
206
+ createFinding('f1', 'secrets', 'critical', 'src/a.ts'),
207
+ createFinding('f2', 'secrets', 'high', 'src/b.ts'),
208
+ createFinding('f3', 'routes', 'medium', 'src/c.ts'),
209
+ createFinding('f4', 'mocks', 'low', 'src/d.ts'),
210
+ ];
211
+
212
+ const result = generateFixPacks({ findings, repoFingerprint: mockRepoFingerprint });
213
+
214
+ expect(result.stats.totalFindings).toBe(4);
215
+ expect(result.stats.byCategory.secrets).toBe(2);
216
+ expect(result.stats.byCategory.routes).toBe(1);
217
+ expect(result.stats.byCategory.mocks).toBe(1);
218
+ expect(result.stats.bySeverity.critical).toBe(1);
219
+ expect(result.stats.bySeverity.high).toBe(1);
220
+ expect(result.stats.bySeverity.medium).toBe(1);
221
+ expect(result.stats.bySeverity.low).toBe(1);
222
+ });
223
+ });
224
+ });
225
+
226
+ describe('helper functions', () => {
227
+ describe('compareSeverity', () => {
228
+ it('should order critical < high < medium < low < info', () => {
229
+ expect(compareSeverity('critical', 'high')).toBeLessThan(0);
230
+ expect(compareSeverity('high', 'medium')).toBeLessThan(0);
231
+ expect(compareSeverity('medium', 'low')).toBeLessThan(0);
232
+ expect(compareSeverity('low', 'info')).toBeLessThan(0);
233
+ });
234
+
235
+ it('should return 0 for equal severities', () => {
236
+ expect(compareSeverity('critical', 'critical')).toBe(0);
237
+ expect(compareSeverity('medium', 'medium')).toBe(0);
238
+ });
239
+ });
240
+
241
+ describe('generatePackId', () => {
242
+ it('should generate stable IDs', () => {
243
+ const id1 = generatePackId('secrets', 1, 'abc123');
244
+ const id2 = generatePackId('secrets', 1, 'abc123');
245
+ expect(id1).toBe(id2);
246
+ });
247
+
248
+ it('should include category prefix', () => {
249
+ const id = generatePackId('secrets', 0, 'abc123');
250
+ expect(id).toMatch(/^FP-SEC-/);
251
+ });
252
+
253
+ it('should include index', () => {
254
+ const id1 = generatePackId('routes', 0, 'abc123');
255
+ const id2 = generatePackId('routes', 1, 'abc123');
256
+ expect(id1).toContain('-000-');
257
+ expect(id2).toContain('-001-');
258
+ });
259
+ });
260
+
261
+ describe('sortPacksBySeverity', () => {
262
+ it('should sort packs by severity', () => {
263
+ const packs = [
264
+ { severity: 'low' as const },
265
+ { severity: 'critical' as const },
266
+ { severity: 'medium' as const },
267
+ ] as any[];
268
+
269
+ const sorted = sortPacksBySeverity(packs);
270
+ expect(sorted[0]!.severity).toBe('critical');
271
+ expect(sorted[1]!.severity).toBe('medium');
272
+ expect(sorted[2]!.severity).toBe('low');
273
+ });
274
+ });
275
+ });
276
+
277
+ describe('parseFindingsFromScanOutput', () => {
278
+ it('should parse JSON array format', () => {
279
+ const input = JSON.stringify([
280
+ { id: 'f1', category: 'secrets', severity: 'high', title: 'Test', file: 'a.ts' },
281
+ ]);
282
+
283
+ const findings = parseFindingsFromScanOutput(input);
284
+ expect(findings.length).toBe(1);
285
+ expect(findings[0]!.category).toBe('secrets');
286
+ });
287
+
288
+ it('should parse findings object format', () => {
289
+ const input = JSON.stringify({
290
+ findings: [
291
+ { id: 'f1', category: 'mocks', severity: 'medium', message: 'Mock detected', filePath: 'b.ts' },
292
+ ],
293
+ });
294
+
295
+ const findings = parseFindingsFromScanOutput(input);
296
+ expect(findings.length).toBe(1);
297
+ expect(findings[0]!.category).toBe('mocks');
298
+ });
299
+
300
+ it('should normalize category names', () => {
301
+ const input = JSON.stringify([
302
+ { id: 'f1', type: 'credential', severity: 'high', title: 'API key', file: 'c.ts' },
303
+ ]);
304
+
305
+ const findings = parseFindingsFromScanOutput(input);
306
+ expect(findings[0]!.category).toBe('secrets');
307
+ });
308
+
309
+ it('should normalize severity levels', () => {
310
+ const input = JSON.stringify([
311
+ { id: 'f1', category: 'security', level: 'error', title: 'XSS', file: 'd.ts' },
312
+ ]);
313
+
314
+ const findings = parseFindingsFromScanOutput(input);
315
+ expect(findings[0]!.severity).toBe('high');
316
+ });
317
+ });