guardrail-ship 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 (119) hide show
  1. package/dist/index.d.ts +7 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +7 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/mock-implementation.d.ts +1 -0
  6. package/dist/mock-implementation.d.ts.map +1 -0
  7. package/dist/mock-implementation.js +2 -0
  8. package/dist/mock-implementation.js.map +1 -0
  9. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
  10. package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
  11. package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
  12. package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
  13. package/dist/mockproof/import-graph-scanner.d.ts +93 -0
  14. package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
  15. package/dist/mockproof/import-graph-scanner.js +411 -0
  16. package/dist/mockproof/import-graph-scanner.js.map +1 -0
  17. package/dist/mockproof/index.d.ts +10 -0
  18. package/dist/mockproof/index.d.ts.map +1 -0
  19. package/dist/mockproof/index.js +10 -0
  20. package/dist/mockproof/index.js.map +1 -0
  21. package/dist/reality-mode/auth-enforcer.d.ts +13 -0
  22. package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
  23. package/dist/reality-mode/auth-enforcer.js +90 -0
  24. package/dist/reality-mode/auth-enforcer.js.map +1 -0
  25. package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
  26. package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
  27. package/dist/reality-mode/explorer/critical-flows.js +463 -0
  28. package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
  29. package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
  30. package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
  31. package/dist/reality-mode/explorer/flow-parser.js +250 -0
  32. package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
  33. package/dist/reality-mode/explorer/index.d.ts +11 -0
  34. package/dist/reality-mode/explorer/index.d.ts.map +1 -0
  35. package/dist/reality-mode/explorer/index.js +11 -0
  36. package/dist/reality-mode/explorer/index.js.map +1 -0
  37. package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
  38. package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
  39. package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
  40. package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
  41. package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
  42. package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
  43. package/dist/reality-mode/explorer/surface-discovery.js +357 -0
  44. package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
  45. package/dist/reality-mode/explorer/types.d.ts +275 -0
  46. package/dist/reality-mode/explorer/types.d.ts.map +1 -0
  47. package/dist/reality-mode/explorer/types.js +8 -0
  48. package/dist/reality-mode/explorer/types.js.map +1 -0
  49. package/dist/reality-mode/fake-success-detector.d.ts +10 -0
  50. package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
  51. package/dist/reality-mode/fake-success-detector.js +76 -0
  52. package/dist/reality-mode/fake-success-detector.js.map +1 -0
  53. package/dist/reality-mode/index.d.ts +14 -0
  54. package/dist/reality-mode/index.d.ts.map +1 -0
  55. package/dist/reality-mode/index.js +14 -0
  56. package/dist/reality-mode/index.js.map +1 -0
  57. package/dist/reality-mode/reality-scanner.d.ts +48 -0
  58. package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
  59. package/dist/reality-mode/reality-scanner.js +516 -0
  60. package/dist/reality-mode/reality-scanner.js.map +1 -0
  61. package/dist/reality-mode/report-generator.d.ts +11 -0
  62. package/dist/reality-mode/report-generator.d.ts.map +1 -0
  63. package/dist/reality-mode/report-generator.js +233 -0
  64. package/dist/reality-mode/report-generator.js.map +1 -0
  65. package/dist/reality-mode/traffic-classifier.d.ts +14 -0
  66. package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
  67. package/dist/reality-mode/traffic-classifier.js +131 -0
  68. package/dist/reality-mode/traffic-classifier.js.map +1 -0
  69. package/dist/reality-mode/types.d.ts +90 -0
  70. package/dist/reality-mode/types.d.ts.map +1 -0
  71. package/dist/reality-mode/types.js +2 -0
  72. package/dist/reality-mode/types.js.map +1 -0
  73. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
  74. package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
  75. package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
  76. package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
  77. package/dist/ship-badge/index.d.ts +9 -0
  78. package/dist/ship-badge/index.d.ts.map +1 -0
  79. package/dist/ship-badge/index.js +9 -0
  80. package/dist/ship-badge/index.js.map +1 -0
  81. package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
  82. package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
  83. package/dist/ship-badge/ship-badge-generator.js +681 -0
  84. package/dist/ship-badge/ship-badge-generator.js.map +1 -0
  85. package/package.json +20 -0
  86. package/src/index.ts +7 -0
  87. package/src/mock-implementation.ts +0 -0
  88. package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
  89. package/src/mockproof/import-graph-scanner.d.ts +93 -0
  90. package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
  91. package/src/mockproof/import-graph-scanner.js +482 -0
  92. package/src/mockproof/import-graph-scanner.ts +540 -0
  93. package/src/mockproof/index.ts +18 -0
  94. package/src/reality-mode/auth-enforcer.ts +97 -0
  95. package/src/reality-mode/explorer/critical-flows.ts +504 -0
  96. package/src/reality-mode/explorer/flow-parser.ts +293 -0
  97. package/src/reality-mode/explorer/index.ts +22 -0
  98. package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
  99. package/src/reality-mode/explorer/surface-discovery.ts +498 -0
  100. package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
  101. package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
  102. package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
  103. package/src/reality-mode/explorer/templates/github-action.yml +132 -0
  104. package/src/reality-mode/explorer/types.ts +356 -0
  105. package/src/reality-mode/fake-success-detector.ts +89 -0
  106. package/src/reality-mode/index.ts +19 -0
  107. package/src/reality-mode/reality-scanner.d.ts +123 -0
  108. package/src/reality-mode/reality-scanner.d.ts.map +1 -0
  109. package/src/reality-mode/reality-scanner.js +526 -0
  110. package/src/reality-mode/reality-scanner.ts +576 -0
  111. package/src/reality-mode/report-generator.ts +253 -0
  112. package/src/reality-mode/traffic-classifier.ts +169 -0
  113. package/src/reality-mode/types.ts +95 -0
  114. package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
  115. package/src/ship-badge/index.ts +16 -0
  116. package/src/ship-badge/ship-badge-generator.d.ts +136 -0
  117. package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
  118. package/src/ship-badge/ship-badge-generator.js +779 -0
  119. package/src/ship-badge/ship-badge-generator.ts +873 -0
@@ -0,0 +1,779 @@
1
+ "use strict";
2
+ /**
3
+ * Ship Badge Generator
4
+ *
5
+ * "One-click shareable proof that your app is real."
6
+ *
7
+ * Generates badges + hosted permalinks for:
8
+ * ✅ No Mock Data Detected
9
+ * ✅ No Localhost/Ngrok
10
+ * ✅ All required env vars present
11
+ * ✅ Billing not simulated
12
+ * ✅ DB is real
13
+ * ✅ OAuth callbacks not localhost
14
+ *
15
+ * Vibecoders slap this on README / landing page / Product Hunt for social proof.
16
+ */
17
+ var __createBinding =
18
+ (this && this.__createBinding) ||
19
+ (Object.create
20
+ ? function (o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (
24
+ !desc ||
25
+ ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)
26
+ ) {
27
+ desc = {
28
+ enumerable: true,
29
+ get: function () {
30
+ return m[k];
31
+ },
32
+ };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }
36
+ : function (o, m, k, k2) {
37
+ if (k2 === undefined) k2 = k;
38
+ o[k2] = m[k];
39
+ });
40
+ var __setModuleDefault =
41
+ (this && this.__setModuleDefault) ||
42
+ (Object.create
43
+ ? function (o, v) {
44
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
45
+ }
46
+ : function (o, v) {
47
+ o["default"] = v;
48
+ });
49
+ var __importStar =
50
+ (this && this.__importStar) ||
51
+ (function () {
52
+ var ownKeys = function (o) {
53
+ ownKeys =
54
+ Object.getOwnPropertyNames ||
55
+ function (o) {
56
+ var ar = [];
57
+ for (var k in o)
58
+ if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
+ return ar;
60
+ };
61
+ return ownKeys(o);
62
+ };
63
+ return function (mod) {
64
+ if (mod && mod.__esModule) return mod;
65
+ var result = {};
66
+ if (mod != null)
67
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++)
68
+ if (k[i] !== "default") __createBinding(result, mod, k[i]);
69
+ __setModuleDefault(result, mod);
70
+ return result;
71
+ };
72
+ })();
73
+ Object.defineProperty(exports, "__esModule", { value: true });
74
+ exports.shipBadgeGenerator = exports.ShipBadgeGenerator = void 0;
75
+ const fs = __importStar(require("fs"));
76
+ const path = __importStar(require("path"));
77
+ const crypto = __importStar(require("crypto"));
78
+ const BADGE_COLORS = {
79
+ pass: "#4ade80", // Green
80
+ fail: "#f87171", // Red
81
+ warning: "#fbbf24", // Yellow
82
+ skip: "#9ca3af", // Gray
83
+ ship: "#22c55e", // Bright green
84
+ noship: "#ef4444", // Bright red
85
+ };
86
+ class ShipBadgeGenerator {
87
+ /**
88
+ * Run all ship checks and generate badges
89
+ */
90
+ async generateShipBadge(config) {
91
+ const projectName = config.projectName || path.basename(config.projectPath);
92
+ const projectId = this.generateProjectId(config.projectPath);
93
+ // Run all checks
94
+ const checks = await this.runAllChecks(config.projectPath);
95
+ // Calculate verdict
96
+ const { verdict, score } = this.calculateVerdict(checks);
97
+ // Generate badges
98
+ const badges = this.generateAllBadges(checks, verdict, score);
99
+ // Generate permalink (would be hosted on Guardrail servers in production)
100
+ const permalink = `https://Guardrail.dev/badge/${projectId}`;
101
+ const embedCode = this.generateEmbedCode(projectId, verdict, projectName);
102
+ // Save badges if output dir specified
103
+ if (config.outputDir) {
104
+ await this.saveBadges(badges, config.outputDir);
105
+ }
106
+ const result = {
107
+ projectId,
108
+ projectName,
109
+ verdict,
110
+ score,
111
+ checks,
112
+ badges,
113
+ timestamp: new Date().toISOString(),
114
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days
115
+ permalink,
116
+ embedCode,
117
+ };
118
+ // Save result JSON
119
+ if (config.outputDir) {
120
+ await fs.promises.writeFile(
121
+ path.join(config.outputDir, "ship-badge-result.json"),
122
+ JSON.stringify(result, null, 2),
123
+ );
124
+ }
125
+ return result;
126
+ }
127
+ /**
128
+ * Run all ship-worthiness checks
129
+ */
130
+ async runAllChecks(projectPath) {
131
+ const checks = [];
132
+ // 1. No Mock Data
133
+ checks.push(await this.checkNoMockData(projectPath));
134
+ // 2. No Localhost/Ngrok
135
+ checks.push(await this.checkNoLocalhost(projectPath));
136
+ // 3. Env Vars Present
137
+ checks.push(await this.checkEnvVars(projectPath));
138
+ // 4. Real Billing (not simulated)
139
+ checks.push(await this.checkRealBilling(projectPath));
140
+ // 5. Real Database
141
+ checks.push(await this.checkRealDatabase(projectPath));
142
+ // 6. OAuth Callbacks
143
+ checks.push(await this.checkOAuthCallbacks(projectPath));
144
+ return checks;
145
+ }
146
+ /**
147
+ * Check for mock data patterns
148
+ */
149
+ async checkNoMockData(projectPath) {
150
+ const patterns = [
151
+ /MockProvider/g,
152
+ /useMock\(/g,
153
+ /mock-context/g,
154
+ /const\s+mock\w*\s*=/gi,
155
+ /lorem\s+ipsum/gi,
156
+ /john\.doe|jane\.doe/gi,
157
+ /user@example\.com/gi,
158
+ ];
159
+ const issues = [];
160
+ const files = await this.findSourceFiles(projectPath);
161
+ for (const file of files.slice(0, 100)) {
162
+ // Limit for performance
163
+ try {
164
+ const content = await fs.promises.readFile(file, "utf-8");
165
+ const relativePath = path.relative(projectPath, file);
166
+ // Skip test files
167
+ if (this.isTestFile(relativePath)) continue;
168
+ for (const pattern of patterns) {
169
+ pattern.lastIndex = 0;
170
+ if (pattern.test(content)) {
171
+ issues.push(`${relativePath}: ${pattern.source}`);
172
+ break;
173
+ }
174
+ }
175
+ } catch (e) {
176
+ // Skip unreadable files
177
+ }
178
+ }
179
+ return {
180
+ id: "no-mock-data",
181
+ name: "No Mock Data Detected",
182
+ shortName: "Mock Data",
183
+ status: issues.length === 0 ? "pass" : "fail",
184
+ message:
185
+ issues.length === 0
186
+ ? "No mock data patterns found in production code"
187
+ : `Found ${issues.length} mock data patterns`,
188
+ details: issues.slice(0, 5),
189
+ };
190
+ }
191
+ /**
192
+ * Check for localhost/ngrok URLs
193
+ */
194
+ async checkNoLocalhost(projectPath) {
195
+ const patterns = [
196
+ /localhost:\d+/g,
197
+ /127\.0\.0\.1:\d+/g,
198
+ /\.ngrok\.io/g,
199
+ /\.ngrok-free\.app/g,
200
+ /jsonplaceholder\.typicode\.com/g,
201
+ ];
202
+ const issues = [];
203
+ const configFiles = [
204
+ ".env",
205
+ ".env.production",
206
+ "next.config.js",
207
+ "next.config.mjs",
208
+ "vite.config.ts",
209
+ "vite.config.js",
210
+ "src/config/api.ts",
211
+ "src/lib/api.ts",
212
+ ];
213
+ for (const configFile of configFiles) {
214
+ const filePath = path.join(projectPath, configFile);
215
+ if (fs.existsSync(filePath)) {
216
+ try {
217
+ const content = await fs.promises.readFile(filePath, "utf-8");
218
+ for (const pattern of patterns) {
219
+ pattern.lastIndex = 0;
220
+ const matches = content.match(pattern);
221
+ if (matches) {
222
+ issues.push(`${configFile}: ${matches[0]}`);
223
+ }
224
+ }
225
+ } catch (e) {
226
+ // Skip
227
+ }
228
+ }
229
+ }
230
+ return {
231
+ id: "no-localhost",
232
+ name: "No Localhost/Ngrok",
233
+ shortName: "Real URLs",
234
+ status: issues.length === 0 ? "pass" : "fail",
235
+ message:
236
+ issues.length === 0
237
+ ? "No localhost or temporary URLs in config"
238
+ : `Found ${issues.length} localhost/ngrok URLs`,
239
+ details: issues,
240
+ };
241
+ }
242
+ /**
243
+ * Check for required environment variables
244
+ */
245
+ async checkEnvVars(projectPath) {
246
+ const requiredVars = [
247
+ "DATABASE_URL",
248
+ "API_URL",
249
+ "NEXTAUTH_URL",
250
+ "NEXTAUTH_SECRET",
251
+ ];
252
+ const envPath = path.join(projectPath, ".env");
253
+ const envProdPath = path.join(projectPath, ".env.production");
254
+ let envContent = "";
255
+ if (fs.existsSync(envProdPath)) {
256
+ envContent = await fs.promises.readFile(envProdPath, "utf-8");
257
+ } else if (fs.existsSync(envPath)) {
258
+ envContent = await fs.promises.readFile(envPath, "utf-8");
259
+ }
260
+ // Also check .env.example to see what's expected
261
+ const examplePath = path.join(projectPath, ".env.example");
262
+ let expectedVars = [];
263
+ if (fs.existsSync(examplePath)) {
264
+ const exampleContent = await fs.promises.readFile(examplePath, "utf-8");
265
+ expectedVars = exampleContent
266
+ .split("\n")
267
+ .filter((line) => line.includes("=") && !line.startsWith("#"))
268
+ .map((line) => line.split("=")[0].trim());
269
+ }
270
+ const missing = [];
271
+ const varsToCheck = expectedVars.length > 0 ? expectedVars : requiredVars;
272
+ for (const varName of varsToCheck) {
273
+ const regex = new RegExp(`^${varName}=.+`, "m");
274
+ if (!regex.test(envContent)) {
275
+ missing.push(varName);
276
+ }
277
+ }
278
+ const hasEnvFile = fs.existsSync(envPath) || fs.existsSync(envProdPath);
279
+ return {
280
+ id: "env-vars",
281
+ name: "Environment Variables Present",
282
+ shortName: "Env Vars",
283
+ status: !hasEnvFile ? "skip" : missing.length === 0 ? "pass" : "warning",
284
+ message: !hasEnvFile
285
+ ? "No .env file found"
286
+ : missing.length === 0
287
+ ? "All expected environment variables are set"
288
+ : `Missing ${missing.length} environment variables`,
289
+ details: missing.slice(0, 5),
290
+ };
291
+ }
292
+ /**
293
+ * Check for real billing (not demo/test)
294
+ */
295
+ async checkRealBilling(projectPath) {
296
+ const testKeyPatterns = [
297
+ /sk_test_/g,
298
+ /pk_test_/g,
299
+ /STRIPE_TEST/g,
300
+ /demo_billing/gi,
301
+ /simulate.*payment/gi,
302
+ /fake.*billing/gi,
303
+ ];
304
+ const issues = [];
305
+ const files = await this.findSourceFiles(projectPath);
306
+ // Check for Stripe/billing related files
307
+ const billingFiles = files.filter((f) =>
308
+ /stripe|billing|payment|checkout/i.test(f),
309
+ );
310
+ if (billingFiles.length === 0) {
311
+ return {
312
+ id: "real-billing",
313
+ name: "Billing Not Simulated",
314
+ shortName: "Billing",
315
+ status: "skip",
316
+ message: "No billing code detected",
317
+ };
318
+ }
319
+ for (const file of billingFiles) {
320
+ try {
321
+ const content = await fs.promises.readFile(file, "utf-8");
322
+ const relativePath = path.relative(projectPath, file);
323
+ if (this.isTestFile(relativePath)) continue;
324
+ for (const pattern of testKeyPatterns) {
325
+ pattern.lastIndex = 0;
326
+ if (pattern.test(content)) {
327
+ issues.push(`${relativePath}: ${pattern.source}`);
328
+ }
329
+ }
330
+ } catch (e) {
331
+ // Skip
332
+ }
333
+ }
334
+ return {
335
+ id: "real-billing",
336
+ name: "Billing Not Simulated",
337
+ shortName: "Billing",
338
+ status: issues.length === 0 ? "pass" : "fail",
339
+ message:
340
+ issues.length === 0
341
+ ? "No test billing keys or demo billing code found"
342
+ : `Found ${issues.length} test/demo billing patterns`,
343
+ details: issues.slice(0, 5),
344
+ };
345
+ }
346
+ /**
347
+ * Check for real database connection
348
+ */
349
+ async checkRealDatabase(projectPath) {
350
+ const fakeDbPatterns = [
351
+ /sqlite:memory/gi,
352
+ /\.sqlite$/gi,
353
+ /mockdb/gi,
354
+ /fake.*database/gi,
355
+ /in-memory.*db/gi,
356
+ ];
357
+ const envPath = path.join(projectPath, ".env");
358
+ const envProdPath = path.join(projectPath, ".env.production");
359
+ let dbUrl = "";
360
+ for (const p of [envProdPath, envPath]) {
361
+ if (fs.existsSync(p)) {
362
+ const content = await fs.promises.readFile(p, "utf-8");
363
+ const match = content.match(/DATABASE_URL=(.+)/);
364
+ if (match) {
365
+ dbUrl = match[1];
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ if (!dbUrl) {
371
+ return {
372
+ id: "real-database",
373
+ name: "Database Is Real",
374
+ shortName: "Database",
375
+ status: "skip",
376
+ message: "No DATABASE_URL found",
377
+ };
378
+ }
379
+ const isFake =
380
+ fakeDbPatterns.some((p) => p.test(dbUrl)) || /localhost/.test(dbUrl);
381
+ return {
382
+ id: "real-database",
383
+ name: "Database Is Real",
384
+ shortName: "Database",
385
+ status: isFake ? "warning" : "pass",
386
+ message: isFake
387
+ ? "Database URL points to local/fake database"
388
+ : "Database URL appears to be a real hosted database",
389
+ };
390
+ }
391
+ /**
392
+ * Check OAuth callback URLs
393
+ */
394
+ async checkOAuthCallbacks(projectPath) {
395
+ const authFiles = [
396
+ "src/app/api/auth/[...nextauth]/route.ts",
397
+ "src/pages/api/auth/[...nextauth].ts",
398
+ "src/lib/auth.ts",
399
+ "src/config/auth.ts",
400
+ ];
401
+ const issues = [];
402
+ for (const authFile of authFiles) {
403
+ const filePath = path.join(projectPath, authFile);
404
+ if (fs.existsSync(filePath)) {
405
+ try {
406
+ const content = await fs.promises.readFile(filePath, "utf-8");
407
+ if (/callbackUrl.*localhost/i.test(content)) {
408
+ issues.push(`${authFile}: localhost callback URL`);
409
+ }
410
+ if (/redirect.*localhost/i.test(content)) {
411
+ issues.push(`${authFile}: localhost redirect`);
412
+ }
413
+ } catch (e) {
414
+ // Skip
415
+ }
416
+ }
417
+ }
418
+ // Also check NEXTAUTH_URL
419
+ const envPath = path.join(projectPath, ".env");
420
+ if (fs.existsSync(envPath)) {
421
+ const content = await fs.promises.readFile(envPath, "utf-8");
422
+ const match = content.match(/NEXTAUTH_URL=(.+)/);
423
+ if (match && /localhost/i.test(match[1])) {
424
+ issues.push(".env: NEXTAUTH_URL points to localhost");
425
+ }
426
+ }
427
+ const hasAuthCode = authFiles.some((f) =>
428
+ fs.existsSync(path.join(projectPath, f)),
429
+ );
430
+ return {
431
+ id: "oauth-callbacks",
432
+ name: "OAuth Callbacks Not Localhost",
433
+ shortName: "OAuth",
434
+ status: !hasAuthCode ? "skip" : issues.length === 0 ? "pass" : "fail",
435
+ message: !hasAuthCode
436
+ ? "No OAuth/auth code detected"
437
+ : issues.length === 0
438
+ ? "OAuth callbacks configured for production"
439
+ : `Found ${issues.length} localhost OAuth issues`,
440
+ details: issues,
441
+ };
442
+ }
443
+ /**
444
+ * Calculate overall verdict
445
+ */
446
+ calculateVerdict(checks) {
447
+ const activeChecks = checks.filter((c) => c.status !== "skip");
448
+ const passed = activeChecks.filter((c) => c.status === "pass").length;
449
+ const failed = activeChecks.filter((c) => c.status === "fail").length;
450
+ const warnings = activeChecks.filter((c) => c.status === "warning").length;
451
+ const score =
452
+ activeChecks.length > 0
453
+ ? Math.round((passed / activeChecks.length) * 100)
454
+ : 100;
455
+ let verdict;
456
+ if (failed > 0) {
457
+ verdict = "no-ship";
458
+ } else if (warnings > 0) {
459
+ verdict = "review";
460
+ } else {
461
+ verdict = "ship";
462
+ }
463
+ return { verdict, score };
464
+ }
465
+ /**
466
+ * Generate all badge SVGs
467
+ */
468
+ generateAllBadges(checks, verdict, score) {
469
+ const mainColor =
470
+ verdict === "ship"
471
+ ? BADGE_COLORS.ship
472
+ : verdict === "no-ship"
473
+ ? BADGE_COLORS.noship
474
+ : BADGE_COLORS.warning;
475
+ return {
476
+ main: this.createBadge(
477
+ "Guardrail",
478
+ verdict === "ship"
479
+ ? "🚀 SHIP IT"
480
+ : verdict === "no-ship"
481
+ ? "🛑 NO SHIP"
482
+ : "⚠️ REVIEW",
483
+ mainColor,
484
+ ),
485
+ mockData: this.createCheckBadge(
486
+ checks.find((c) => c.id === "no-mock-data"),
487
+ ),
488
+ realApi: this.createCheckBadge(
489
+ checks.find((c) => c.id === "no-localhost"),
490
+ ),
491
+ envVars: this.createCheckBadge(checks.find((c) => c.id === "env-vars")),
492
+ billing: this.createCheckBadge(
493
+ checks.find((c) => c.id === "real-billing"),
494
+ ),
495
+ database: this.createCheckBadge(
496
+ checks.find((c) => c.id === "real-database"),
497
+ ),
498
+ oauth: this.createCheckBadge(
499
+ checks.find((c) => c.id === "oauth-callbacks"),
500
+ ),
501
+ combined: this.createCombinedBadge(checks, score),
502
+ };
503
+ }
504
+ /**
505
+ * Create a single check badge
506
+ */
507
+ createCheckBadge(check) {
508
+ const icon =
509
+ check.status === "pass"
510
+ ? "✅"
511
+ : check.status === "fail"
512
+ ? "❌"
513
+ : check.status === "warning"
514
+ ? "⚠️"
515
+ : "⏭️";
516
+ const color = BADGE_COLORS[check.status];
517
+ const label = check.shortName;
518
+ const value =
519
+ check.status === "pass"
520
+ ? "Pass"
521
+ : check.status === "fail"
522
+ ? "Fail"
523
+ : check.status === "warning"
524
+ ? "Warning"
525
+ : "Skip";
526
+ return this.createBadge(label, `${icon} ${value}`, color);
527
+ }
528
+ /**
529
+ * Create a combined badge strip
530
+ */
531
+ createCombinedBadge(checks, score) {
532
+ const passed = checks.filter((c) => c.status === "pass").length;
533
+ const total = checks.filter((c) => c.status !== "skip").length;
534
+ const color =
535
+ score >= 80
536
+ ? BADGE_COLORS.pass
537
+ : score >= 50
538
+ ? BADGE_COLORS.warning
539
+ : BADGE_COLORS.fail;
540
+ return this.createBadge(
541
+ "Ship Score",
542
+ `${passed}/${total} (${score}%)`,
543
+ color,
544
+ "for-the-badge",
545
+ );
546
+ }
547
+ /**
548
+ * Create SVG badge
549
+ */
550
+ createBadge(label, value, color, style = "flat") {
551
+ const labelWidth = label.length * 7 + 10;
552
+ const valueWidth = value.length * 7 + 10;
553
+ const totalWidth = labelWidth + valueWidth;
554
+ const height = style === "for-the-badge" ? 28 : 20;
555
+ const fontSize = 11;
556
+ const labelBg = "#555";
557
+ if (style === "for-the-badge") {
558
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}">
559
+ <linearGradient id="smooth" x2="0" y2="100%">
560
+ <stop offset="0" stop-color="#fff" stop-opacity=".7"/>
561
+ <stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
562
+ <stop offset=".9" stop-color="#000" stop-opacity=".3"/>
563
+ <stop offset="1" stop-color="#000" stop-opacity=".5"/>
564
+ </linearGradient>
565
+ <rect rx="4" width="${totalWidth}" height="${height}" fill="${labelBg}"/>
566
+ <rect rx="4" x="${labelWidth}" width="${valueWidth}" height="${height}" fill="${color}"/>
567
+ <rect rx="4" width="${totalWidth}" height="${height}" fill="url(#smooth)"/>
568
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="${fontSize}" font-weight="bold">
569
+ <text x="${labelWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(label)}</text>
570
+ <text x="${labelWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(label)}</text>
571
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(value)}</text>
572
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(value)}</text>
573
+ </g>
574
+ </svg>`;
575
+ }
576
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${height}">
577
+ <linearGradient id="smooth" x2="0" y2="100%">
578
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
579
+ <stop offset="1" stop-opacity=".1"/>
580
+ </linearGradient>
581
+ <clipPath id="round">
582
+ <rect width="${totalWidth}" height="${height}" rx="3" fill="#fff"/>
583
+ </clipPath>
584
+ <g clip-path="url(#round)">
585
+ <rect width="${labelWidth}" height="${height}" fill="${labelBg}"/>
586
+ <rect x="${labelWidth}" width="${valueWidth}" height="${height}" fill="${color}"/>
587
+ <rect width="${totalWidth}" height="${height}" fill="url(#smooth)"/>
588
+ </g>
589
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="${fontSize}">
590
+ <text x="${labelWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(label)}</text>
591
+ <text x="${labelWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(label)}</text>
592
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 4}" fill="#010101" fill-opacity=".3">${this.escapeHtml(value)}</text>
593
+ <text x="${labelWidth + valueWidth / 2}" y="${height / 2 + 3}">${this.escapeHtml(value)}</text>
594
+ </g>
595
+ </svg>`;
596
+ }
597
+ /**
598
+ * Generate embed code for README
599
+ */
600
+ generateEmbedCode(projectId, verdict, projectName) {
601
+ return `<!-- Guardrail Ship Badge -->
602
+ [![Guardrail Ship Status](https://Guardrail.dev/api/badge/${projectId}/main.svg)](https://Guardrail.dev/badge/${projectId})
603
+ [![Mock Data](https://Guardrail.dev/api/badge/${projectId}/mock-data.svg)](https://Guardrail.dev/badge/${projectId})
604
+ [![Real APIs](https://Guardrail.dev/api/badge/${projectId}/real-api.svg)](https://Guardrail.dev/badge/${projectId})
605
+ <!-- End Guardrail Ship Badge -->
606
+
607
+ ---
608
+
609
+ **${projectName}** verified by [Guardrail](https://Guardrail.dev) - Stop shipping pretend features.`;
610
+ }
611
+ /**
612
+ * Save badges to directory
613
+ */
614
+ async saveBadges(badges, outputDir) {
615
+ await fs.promises.mkdir(outputDir, { recursive: true });
616
+ const files = [
617
+ ["main", "ship-status.svg"],
618
+ ["mockData", "mock-data.svg"],
619
+ ["realApi", "real-api.svg"],
620
+ ["envVars", "env-vars.svg"],
621
+ ["billing", "billing.svg"],
622
+ ["database", "database.svg"],
623
+ ["oauth", "oauth.svg"],
624
+ ["combined", "ship-score.svg"],
625
+ ];
626
+ for (const [key, filename] of files) {
627
+ await fs.promises.writeFile(
628
+ path.join(outputDir, filename),
629
+ badges[key],
630
+ "utf-8",
631
+ );
632
+ }
633
+ }
634
+ /**
635
+ * Generate project ID from path
636
+ */
637
+ generateProjectId(projectPath) {
638
+ const hash = crypto
639
+ .createHash("sha256")
640
+ .update(projectPath)
641
+ .digest("hex")
642
+ .slice(0, 12);
643
+ return hash;
644
+ }
645
+ /**
646
+ * Find source files
647
+ */
648
+ async findSourceFiles(projectPath) {
649
+ const files = [];
650
+ const extensions = [".ts", ".tsx", ".js", ".jsx"];
651
+ const excludeDirs = [
652
+ "node_modules",
653
+ ".git",
654
+ ".next",
655
+ "dist",
656
+ "build",
657
+ "coverage",
658
+ ];
659
+ const walk = async (dir) => {
660
+ try {
661
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
662
+ for (const entry of entries) {
663
+ const fullPath = path.join(dir, entry.name);
664
+ if (entry.isDirectory()) {
665
+ if (
666
+ !excludeDirs.includes(entry.name) &&
667
+ !entry.name.startsWith(".")
668
+ ) {
669
+ await walk(fullPath);
670
+ }
671
+ } else if (entry.isFile()) {
672
+ const ext = path.extname(entry.name);
673
+ if (extensions.includes(ext)) {
674
+ files.push(fullPath);
675
+ }
676
+ }
677
+ }
678
+ } catch (e) {
679
+ // Skip
680
+ }
681
+ };
682
+ await walk(projectPath);
683
+ return files;
684
+ }
685
+ /**
686
+ * Check if file is a test file
687
+ */
688
+ isTestFile(filePath) {
689
+ const testPatterns = [
690
+ /__tests__/,
691
+ /\.test\./,
692
+ /\.spec\./,
693
+ /test\//,
694
+ /tests\//,
695
+ /e2e\//,
696
+ /__mocks__/,
697
+ /stories\//,
698
+ ];
699
+ return testPatterns.some((p) => p.test(filePath));
700
+ }
701
+ /**
702
+ * Escape HTML entities
703
+ */
704
+ escapeHtml(text) {
705
+ return text
706
+ .replace(/&/g, "&amp;")
707
+ .replace(/</g, "&lt;")
708
+ .replace(/>/g, "&gt;")
709
+ .replace(/"/g, "&quot;")
710
+ .replace(/'/g, "&#039;");
711
+ }
712
+ /**
713
+ * Generate human-readable report
714
+ */
715
+ generateReport(result) {
716
+ const lines = [];
717
+ lines.push(
718
+ "╔══════════════════════════════════════════════════════════════╗",
719
+ );
720
+ lines.push(
721
+ "║ 🚀 Guardrail Ship Badge Report 🚀 ║",
722
+ );
723
+ lines.push(
724
+ "╚══════════════════════════════════════════════════════════════╝",
725
+ );
726
+ lines.push("");
727
+ const verdictEmoji =
728
+ result.verdict === "ship"
729
+ ? "🚀"
730
+ : result.verdict === "no-ship"
731
+ ? "🛑"
732
+ : "⚠️";
733
+ const verdictText =
734
+ result.verdict === "ship"
735
+ ? "SHIP IT!"
736
+ : result.verdict === "no-ship"
737
+ ? "NO SHIP"
738
+ : "NEEDS REVIEW";
739
+ lines.push(`${verdictEmoji} VERDICT: ${verdictText}`);
740
+ lines.push(` Ship Score: ${result.score}/100`);
741
+ lines.push(` Project: ${result.projectName}`);
742
+ lines.push("");
743
+ lines.push("─".repeat(64));
744
+ lines.push("");
745
+ lines.push("CHECKS:");
746
+ lines.push("");
747
+ for (const check of result.checks) {
748
+ const icon =
749
+ check.status === "pass"
750
+ ? "✅"
751
+ : check.status === "fail"
752
+ ? "❌"
753
+ : check.status === "warning"
754
+ ? "⚠️"
755
+ : "⏭️";
756
+ lines.push(`${icon} ${check.name}`);
757
+ lines.push(` ${check.message}`);
758
+ if (check.details && check.details.length > 0) {
759
+ for (const detail of check.details) {
760
+ lines.push(` • ${detail}`);
761
+ }
762
+ }
763
+ lines.push("");
764
+ }
765
+ lines.push("─".repeat(64));
766
+ lines.push("");
767
+ lines.push("ADD TO YOUR README:");
768
+ lines.push("");
769
+ lines.push(result.embedCode);
770
+ lines.push("");
771
+ lines.push("─".repeat(64));
772
+ lines.push(`Permalink: ${result.permalink}`);
773
+ lines.push(`Generated: ${result.timestamp}`);
774
+ lines.push(`Expires: ${result.expiresAt}`);
775
+ return lines.join("\n");
776
+ }
777
+ }
778
+ exports.ShipBadgeGenerator = ShipBadgeGenerator;
779
+ exports.shipBadgeGenerator = new ShipBadgeGenerator();