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,253 @@
1
+ import { RealityModeResult } from "./types";
2
+
3
+ export class ReportGenerator {
4
+ generateHtml(result: RealityModeResult): string {
5
+ let verdictDisplay = "GO";
6
+ let verdictColor = "#10b981"; // Green
7
+ let verdictIcon = "✅";
8
+
9
+ if (result.verdict === "fake") {
10
+ verdictDisplay = "NO-GO";
11
+ verdictColor = "#ef4444"; // Red
12
+ verdictIcon = "🛑";
13
+ } else if (result.verdict === "suspicious") {
14
+ verdictDisplay = "WARN";
15
+ verdictColor = "#f59e0b"; // Amber
16
+ verdictIcon = "⚠️";
17
+ }
18
+
19
+ return `<!DOCTYPE html>
20
+ <html lang="en">
21
+ <head>
22
+ <meta charset="UTF-8">
23
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
+ <title>Reality Mode Report</title>
25
+ <style>
26
+ body { font-family: -apple-system, system-ui, sans-serif; margin: 0; padding: 0; background: #f9fafb; color: #1f2937; }
27
+ .container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
28
+ .header { background: white; padding: 2rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
29
+ .verdict { font-size: 2.5rem; font-weight: 800; color: ${verdictColor}; display: flex; align-items: center; gap: 0.75rem; letter-spacing: -0.025em; }
30
+ .score-badge { background: ${verdictColor}; color: white; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 1rem; font-weight: bold; }
31
+ .section { background: white; padding: 1.5rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1.5rem; }
32
+ .section-title { font-size: 1.25rem; font-weight: bold; margin-bottom: 1rem; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; }
33
+ .detection { border: 1px solid #e5e7eb; border-radius: 0.375rem; padding: 1rem; margin-bottom: 1rem; border-left: 4px solid #ef4444; }
34
+ .detection.warning { border-left-color: #f59e0b; }
35
+ .detection-title { font-weight: bold; display: flex; justify-content: space-between; align-items: center; }
36
+ .evidence { background: #f3f4f6; padding: 0.75rem; border-radius: 0.25rem; margin-top: 0.5rem; font-family: monospace; font-size: 0.875rem; overflow-x: auto; }
37
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
38
+ .summary-item { background: #f3f4f6; padding: 1rem; border-radius: 0.375rem; text-align: center; }
39
+ .summary-value { font-size: 1.5rem; font-weight: bold; }
40
+ .summary-label { color: #6b7280; font-size: 0.875rem; }
41
+ .replay-step { border-left: 2px solid #d1d5db; padding-left: 1rem; margin-bottom: 1rem; position: relative; }
42
+ .replay-step::before { content: ''; position: absolute; left: -5px; top: 0; width: 8px; height: 8px; border-radius: 50%; background: #9ca3af; }
43
+ .replay-step.request::before { background: #3b82f6; }
44
+ .replay-step.action::before { background: #10b981; }
45
+ .replay-step.detection::before { background: #ef4444; }
46
+ .timestamp { color: #9ca3af; font-size: 0.75rem; }
47
+ .failure-chip { display: inline-flex; align-items: center; background: #fee2e2; color: #991b1b; padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: bold; margin-right: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
48
+ .failure-chip.auth { background: #fef3c7; color: #92400e; } /* Amber for Auth */
49
+ .failure-chip.schema { background: #e0e7ff; color: #1e40af; } /* Blue for Schema */
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="container">
54
+ <div class="header">
55
+ <div style="display: flex; justify-content: space-between; align-items: center;">
56
+ <div>
57
+ <h1 style="margin: 0 0 0.5rem 0; font-size: 1rem; color: #6b7280; text-transform: uppercase;">Reality Check</h1>
58
+ <div class="verdict">${verdictIcon} ${verdictDisplay}</div>
59
+ </div>
60
+ <div class="score-badge">Score: ${result.score}/100</div>
61
+ </div>
62
+ <p style="margin-top: 1rem; color: #6b7280;">Generated: ${result.timestamp}</p>
63
+ </div>
64
+
65
+ <div class="section">
66
+ <h2 class="section-title">Summary</h2>
67
+ <div class="summary-grid">
68
+ <div class="summary-item">
69
+ <div class="summary-value">${result.summary.totalRequests}</div>
70
+ <div class="summary-label">Total Requests</div>
71
+ </div>
72
+ <div class="summary-item">
73
+ <div class="summary-value">${result.summary.fakeRequests}</div>
74
+ <div class="summary-label">Fake/Mock Requests</div>
75
+ </div>
76
+ <div class="summary-item">
77
+ <div class="summary-value">${result.summary.criticalIssues}</div>
78
+ <div class="summary-label">Critical Issues</div>
79
+ </div>
80
+ <div class="summary-item">
81
+ <div class="summary-value">${result.summary.warnings}</div>
82
+ <div class="summary-label">Warnings</div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ ${this.renderDetections(result)}
88
+ ${this.renderFakeSuccess(result)}
89
+ ${this.renderTrafficAnalysis(result)}
90
+ ${this.renderAuthViolations(result)}
91
+
92
+ <div class="section">
93
+ <h2 class="section-title">Flight Recorder Replay</h2>
94
+ <div class="replay-log">
95
+ ${result.replay.map((step) => this.renderReplayStep(step)).join("")}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </body>
100
+ </html>`;
101
+ }
102
+
103
+ private getBrandedChip(text: string): string {
104
+ const t = text.toLowerCase();
105
+ if (t.includes("mock backend"))
106
+ return '<span class="failure-chip">MOCK BACKEND</span>';
107
+ if (t.includes("fake success"))
108
+ return '<span class="failure-chip">FAKE SUCCESS</span>';
109
+ if (t.includes("no-wire"))
110
+ return '<span class="failure-chip">NO-WIRE UI</span>';
111
+ if (t.includes("auth mirage"))
112
+ return '<span class="failure-chip auth">AUTH MIRAGE</span>';
113
+ if (t.includes("schema drift") || t.includes("missing wiring"))
114
+ return '<span class="failure-chip schema">SCHEMA DRIFT</span>';
115
+ return "";
116
+ }
117
+
118
+ private renderDetections(result: RealityModeResult): string {
119
+ if (result.detections.length === 0) return "";
120
+
121
+ return `
122
+ <div class="section">
123
+ <h2 class="section-title">Issues Detected</h2>
124
+ ${result.detections
125
+ .map(
126
+ (d) => `
127
+ <div class="detection ${d.pattern.severity === "warning" ? "warning" : ""}">
128
+ <div class="detection-title">
129
+ <span>${this.getBrandedChip(d.pattern.name)} ${d.pattern.name}</span>
130
+ <span style="font-size: 0.75rem; text-transform: uppercase; color: #6b7280;">${d.pattern.severity}</span>
131
+ </div>
132
+ <p>${d.pattern.description}</p>
133
+ <div class="evidence">
134
+ ${d.evidence}<br>
135
+ ${d.request ? `URL: ${d.request.url}` : ""}
136
+ ${d.response ? `URL: ${d.response.url}` : ""}
137
+ </div>
138
+ </div>
139
+ `,
140
+ )
141
+ .join("")}
142
+ </div>`;
143
+ }
144
+
145
+ private renderFakeSuccess(result: RealityModeResult): string {
146
+ const fakes = result.fakeSuccessAnalysis?.filter((f) => f.isFake) || [];
147
+ if (fakes.length === 0) return "";
148
+
149
+ return `
150
+ <div class="section">
151
+ <h2 class="section-title">🚨 Fake Success Detected</h2>
152
+ <p style="color: #ef4444; margin-bottom: 1rem;">Actions appeared to succeed but triggered no backend persistence.</p>
153
+ ${fakes
154
+ .map(
155
+ (f) => `
156
+ <div class="detection">
157
+ <div class="detection-title"><span class="failure-chip">FAKE SUCCESS</span> Action Failed Persistence</div>
158
+ <div class="evidence">${f.evidence.join("<br>")}</div>
159
+ </div>
160
+ `,
161
+ )
162
+ .join("")}
163
+ </div>`;
164
+ }
165
+
166
+ private renderTrafficAnalysis(result: RealityModeResult): string {
167
+ const red =
168
+ result.trafficAnalysis?.filter((t) => t.verdict === "red") || [];
169
+ const yellow =
170
+ result.trafficAnalysis?.filter((t) => t.verdict === "yellow") || [];
171
+ const issues = [...red, ...yellow];
172
+
173
+ if (issues.length === 0) return "";
174
+
175
+ return `
176
+ <div class="section">
177
+ <h2 class="section-title">Traffic Analysis Issues</h2>
178
+ ${issues
179
+ .map(
180
+ (t) => `
181
+ <div class="detection ${t.verdict === "yellow" ? "warning" : ""}">
182
+ <div class="detection-title">
183
+ <span>${this.getBrandedChip(t.reasons.join(" "))} Traffic Analysis: ${t.verdict.toUpperCase()}</span>
184
+ </div>
185
+ <div class="evidence">${t.reasons.join("<br>")}</div>
186
+ </div>
187
+ `,
188
+ )
189
+ .join("")}
190
+ </div>`;
191
+ }
192
+
193
+ private renderAuthViolations(result: RealityModeResult): string {
194
+ if (!result.authViolations || result.authViolations.length === 0) return "";
195
+
196
+ return `
197
+ <div class="section">
198
+ <h2 class="section-title">🔐 Auth Violations</h2>
199
+ ${result.authViolations
200
+ .map(
201
+ (v) => `
202
+ <div class="detection">
203
+ <div class="detection-title">
204
+ <span><span class="failure-chip auth">AUTH MIRAGE</span> ${v.type}</span>
205
+ </div>
206
+ <div class="evidence">
207
+ Route: ${v.route}<br>
208
+ Status: ${v.status} (Expected 401/403/Redirect)
209
+ </div>
210
+ </div>
211
+ `,
212
+ )
213
+ .join("")}
214
+ </div>`;
215
+ }
216
+
217
+ private renderReplayStep(step: any): string {
218
+ let content = "";
219
+ let className = "replay-step";
220
+
221
+ if (step.type === "request") {
222
+ className += " request";
223
+ content = `<strong>Request:</strong> ${step.data.method} ${step.data.url}`;
224
+ } else if (step.type === "response") {
225
+ className += " request"; // group with request visually
226
+ content = `<strong>Response:</strong> ${step.data.status} ${step.data.url}`;
227
+ } else if (step.type === "action") {
228
+ className += " action";
229
+ content = `<strong>Action:</strong> ${step.data.type} ${step.data.selector || step.data.url}`;
230
+ } else if (step.type === "console") {
231
+ className += " console";
232
+ const isError = step.data.type === "error";
233
+ const color = isError ? "#ef4444" : "#6b7280";
234
+ content = `<strong style="color: ${color}">Console ${step.data.type}:</strong> <span style="font-family: monospace">${step.data.text}</span>`;
235
+ }
236
+
237
+ if (step.detections && step.detections.length > 0) {
238
+ className += " detection";
239
+ content += `<div style="color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;">⚠️ Issue detected here</div>`;
240
+ }
241
+
242
+ if (step.screenshot) {
243
+ content += `<div style="margin-top: 0.5rem;"><img src="./${step.screenshot}" style="max-width: 100%; border: 1px solid #ddd; border-radius: 4px; max-height: 300px; cursor: pointer;" onclick="this.style.maxHeight='none'" loading="lazy" alt="Step Screenshot"></div>`;
244
+ }
245
+
246
+ return `
247
+ <div class="${className}">
248
+ <div class="timestamp">${(new Date(step.timestamp || 0).toISOString().split("T")[1] || "").slice(0, -1)}</div>
249
+ <div>${content}</div>
250
+ </div>
251
+ `;
252
+ }
253
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ InterceptedRequest,
3
+ InterceptedResponse,
4
+ FakePattern,
5
+ TrafficClassification,
6
+ TrafficVerdict,
7
+ } from "./types";
8
+
9
+ export class TrafficClassifier {
10
+ private fakePatterns: FakePattern[];
11
+
12
+ constructor(patterns: FakePattern[]) {
13
+ this.fakePatterns = patterns;
14
+ }
15
+
16
+ /**
17
+ * Classify a single network interaction (request + response)
18
+ */
19
+ classify(
20
+ request: InterceptedRequest,
21
+ response?: InterceptedResponse,
22
+ ): TrafficClassification {
23
+ const reasons: string[] = [];
24
+ let score = 100;
25
+ let verdict: TrafficVerdict = "green";
26
+
27
+ // 1. Check for Red Flags (Fake Patterns)
28
+ for (const pattern of this.fakePatterns) {
29
+ if (pattern.detect(request)) {
30
+ score -= pattern.severity === "critical" ? 100 : 20;
31
+ reasons.push(
32
+ `Mock Backend: Request matches ${pattern.name} (${pattern.description})`,
33
+ );
34
+ }
35
+ if (response && pattern.detect(response)) {
36
+ score -= pattern.severity === "critical" ? 100 : 20;
37
+ reasons.push(
38
+ `Mock Backend: Response matches ${pattern.name} (${pattern.description})`,
39
+ );
40
+ }
41
+ }
42
+
43
+ if (score <= 50) {
44
+ return { verdict: "red", reasons, score: Math.max(0, score) };
45
+ }
46
+
47
+ // 2. Check for Yellow Flags (Suspicious)
48
+
49
+ // Suspicious: API returns "success" but no data
50
+ if (response && response.body) {
51
+ try {
52
+ const body = JSON.parse(response.body);
53
+ if (
54
+ body.success === true &&
55
+ !body.data &&
56
+ Object.keys(body).length <= 2
57
+ ) {
58
+ score -= 10;
59
+ reasons.push(
60
+ "Potential No-Wire UI: API returns generic success with no data",
61
+ );
62
+ }
63
+ } catch (e) {
64
+ // Ignore parsing errors
65
+ }
66
+ }
67
+
68
+ // Suspicious: URL looks like a dev/staging environment but we are running in "production" mode?
69
+ // (Assuming we are looking for production credibility)
70
+ if (
71
+ request.url.includes("localhost") ||
72
+ request.url.includes("127.0.0.1")
73
+ ) {
74
+ // If we are testing local, this is fine. If testing prod, it's red.
75
+ // Context matters. For now, let's treat localhost as "yellow" unless explicitly allowed.
76
+ // Actually, reality-scanner treats localhost as a fake pattern (critical) in default patterns.
77
+ // So it's already caught above.
78
+ }
79
+
80
+ // Suspicious: Non-standard status codes
81
+ if (response && response.status >= 400) {
82
+ // 404/500 on API endpoints -> Red flag (Missing Wiring)
83
+ if (
84
+ response.url.includes("/api/") ||
85
+ response.url.includes("/graphql") ||
86
+ response.url.includes("/trpc")
87
+ ) {
88
+ score -= 40;
89
+ reasons.push(
90
+ `Missing Wiring: API Error ${response.status} on ${response.url}`,
91
+ );
92
+ if (response.status === 404) verdict = "red";
93
+ } else if (response.status === 418 || response.status === 999) {
94
+ score -= 30;
95
+ reasons.push(
96
+ `Mock Backend: Suspicious HTTP status code ${response.status}`,
97
+ );
98
+ } else {
99
+ // Other errors (e.g. 401/403 are handled by AuthEnforcer, but still suspicious if unintended)
100
+ score -= 10;
101
+ reasons.push(`Schema Drift: HTTP Error ${response.status}`);
102
+ }
103
+ }
104
+
105
+ // 3. Green Signals (Real Data)
106
+ if (response && response.body) {
107
+ // IDs looking like UUIDs or MongoDB IDs or integer sequences
108
+ if (/"id":\s*["'][a-f0-9-]{36}["']/i.test(response.body)) {
109
+ // UUID found - likely real
110
+ // Boost score slightly if it was lowered by minor things
111
+ if (score < 100) score += 5;
112
+ }
113
+ if (/"created_at":\s*["']\d{4}-\d{2}-\d{2}/i.test(response.body)) {
114
+ // ISO Date found
115
+ if (score < 100) score += 5;
116
+ }
117
+ }
118
+
119
+ // Final Verdict
120
+ if (score < 60) verdict = "red";
121
+ else if (score < 90) verdict = "yellow";
122
+ else verdict = "green";
123
+
124
+ return {
125
+ verdict,
126
+ reasons,
127
+ score: Math.min(100, Math.max(0, score)),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Analyze consistency across multiple runs (stateful check)
133
+ */
134
+ classifyConsistency(
135
+ currentResponse: InterceptedResponse,
136
+ previousResponse?: InterceptedResponse,
137
+ ): TrafficClassification {
138
+ if (!previousResponse) {
139
+ return { verdict: "green", reasons: ["First run"], score: 100 };
140
+ }
141
+
142
+ // Exact body match is suspicious for dynamic data (timestamps, nonces)
143
+ if (
144
+ currentResponse.body &&
145
+ currentResponse.body === previousResponse.body
146
+ ) {
147
+ // Check if the body looks like it SHOULD have dynamic data
148
+ if (
149
+ currentResponse.body.includes("timestamp") ||
150
+ currentResponse.body.includes("created_at") ||
151
+ currentResponse.body.includes("nonce")
152
+ ) {
153
+ return {
154
+ verdict: "yellow",
155
+ reasons: [
156
+ "Response body identical across runs despite containing timestamps",
157
+ ],
158
+ score: 70,
159
+ };
160
+ }
161
+ }
162
+
163
+ return {
164
+ verdict: "green",
165
+ reasons: ["Data varies or is static static-content"],
166
+ score: 100,
167
+ };
168
+ }
169
+ }
@@ -0,0 +1,95 @@
1
+ export interface FakePattern {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ severity: "critical" | "warning" | "info";
6
+ detect: (request: InterceptedRequest | InterceptedResponse) => boolean;
7
+ }
8
+
9
+ export interface InterceptedRequest {
10
+ type: "request";
11
+ url: string;
12
+ method: string;
13
+ headers: Record<string, string>;
14
+ body?: string;
15
+ timestamp: number;
16
+ }
17
+
18
+ export interface InterceptedResponse {
19
+ type: "response";
20
+ url: string;
21
+ status: number;
22
+ headers: Record<string, string>;
23
+ body?: string;
24
+ timestamp: number;
25
+ }
26
+
27
+ export interface UserAction {
28
+ type: "click" | "input" | "navigation" | "scroll";
29
+ selector?: string;
30
+ value?: string;
31
+ url?: string;
32
+ timestamp: number;
33
+ screenshot?: string;
34
+ }
35
+
36
+ export interface FakeDetection {
37
+ pattern: FakePattern;
38
+ request?: InterceptedRequest;
39
+ response?: InterceptedResponse;
40
+ action?: UserAction;
41
+ timestamp: number;
42
+ evidence: string;
43
+ }
44
+
45
+ export interface ReplayStep {
46
+ timestamp: number;
47
+ type: "request" | "response" | "action";
48
+ data: any;
49
+ detections: FakeDetection[];
50
+ screenshot?: string;
51
+ }
52
+
53
+ export type TrafficVerdict = "green" | "yellow" | "red";
54
+
55
+ export interface TrafficClassification {
56
+ verdict: TrafficVerdict;
57
+ reasons: string[];
58
+ score: number;
59
+ }
60
+
61
+ export interface FakeSuccessResult {
62
+ isFake: boolean;
63
+ score: number;
64
+ evidence: string[];
65
+ actionStep?: ReplayStep;
66
+ }
67
+
68
+ export interface RealityModeResult {
69
+ verdict: "real" | "fake" | "suspicious";
70
+ score: number;
71
+ detections: FakeDetection[];
72
+ replay: ReplayStep[];
73
+ trafficAnalysis: TrafficClassification[];
74
+ fakeSuccessAnalysis: FakeSuccessResult[];
75
+ authViolations?: { route: string; status: number; type: string }[];
76
+ summary: {
77
+ totalRequests: number;
78
+ fakeRequests: number;
79
+ totalActions: number;
80
+ criticalIssues: number;
81
+ warnings: number;
82
+ };
83
+ timestamp: string;
84
+ duration: number;
85
+ }
86
+
87
+ export interface RealityModeConfig {
88
+ baseUrl: string;
89
+ timeout: number;
90
+ patterns: FakePattern[];
91
+ clickPaths: string[][];
92
+ screenshotOnDetection: boolean;
93
+ headless: boolean;
94
+ checkAuth: boolean;
95
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for Ship Badge Generator
3
+ */
4
+
5
+ import * as path from "path";
6
+ import { ShipBadgeGenerator } from "../ship-badge-generator";
7
+
8
+ describe("ShipBadgeGenerator", () => {
9
+ let generator: ShipBadgeGenerator;
10
+
11
+ beforeEach(() => {
12
+ generator = new ShipBadgeGenerator();
13
+ });
14
+
15
+ describe("generateShipBadge", () => {
16
+ it("should generate badges for a project", async () => {
17
+ const result = await generator.generateShipBadge({
18
+ projectPath: path.join(__dirname, "..", "..", "..", ".."),
19
+ projectName: "test-project",
20
+ });
21
+
22
+ expect(result).toHaveProperty("verdict");
23
+ expect(result).toHaveProperty("score");
24
+ expect(result).toHaveProperty("checks");
25
+ expect(result).toHaveProperty("badges");
26
+ expect(result).toHaveProperty("permalink");
27
+ expect(result).toHaveProperty("embedCode");
28
+
29
+ expect(result.checks.length).toBeGreaterThan(0);
30
+ expect(["ship", "no-ship", "review"]).toContain(result.verdict);
31
+ });
32
+
33
+ it("should include all required checks", async () => {
34
+ const result = await generator.generateShipBadge({
35
+ projectPath: __dirname,
36
+ projectName: "test",
37
+ });
38
+
39
+ const checkIds = result.checks.map((c) => c.id);
40
+
41
+ expect(checkIds).toContain("no-mock-data");
42
+ expect(checkIds).toContain("no-localhost");
43
+ expect(checkIds).toContain("env-vars");
44
+ expect(checkIds).toContain("real-billing");
45
+ expect(checkIds).toContain("real-database");
46
+ expect(checkIds).toContain("oauth-callbacks");
47
+ });
48
+ });
49
+
50
+ describe("generateReport", () => {
51
+ it("should generate readable report for passing project", () => {
52
+ const mockResult = {
53
+ projectId: "test123",
54
+ projectName: "My App",
55
+ verdict: "ship" as const,
56
+ score: 100,
57
+ checks: [
58
+ {
59
+ id: "no-mock-data",
60
+ name: "No Mock Data",
61
+ shortName: "Mock",
62
+ status: "pass" as const,
63
+ message: "Clean",
64
+ },
65
+ {
66
+ id: "no-localhost",
67
+ name: "No Localhost",
68
+ shortName: "URLs",
69
+ status: "pass" as const,
70
+ message: "Clean",
71
+ },
72
+ ],
73
+ badges: {
74
+ main: "<svg>...</svg>",
75
+ mockData: "<svg>...</svg>",
76
+ realApi: "<svg>...</svg>",
77
+ envVars: "<svg>...</svg>",
78
+ billing: "<svg>...</svg>",
79
+ database: "<svg>...</svg>",
80
+ oauth: "<svg>...</svg>",
81
+ combined: "<svg>...</svg>",
82
+ },
83
+ timestamp: new Date().toISOString(),
84
+ expiresAt: new Date().toISOString(),
85
+ permalink: "https://Guardrail.dev/badge/test123",
86
+ embedCode: "[![Badge](url)](link)",
87
+ };
88
+
89
+ const report = generator.generateReport(mockResult);
90
+
91
+ expect(report).toContain("SHIP IT!");
92
+ expect(report).toContain("My App");
93
+ expect(report).toContain("100/100");
94
+ });
95
+
96
+ it("should generate readable report for failing project", () => {
97
+ const mockResult = {
98
+ projectId: "test123",
99
+ projectName: "Bad App",
100
+ verdict: "no-ship" as const,
101
+ score: 33,
102
+ checks: [
103
+ {
104
+ id: "no-mock-data",
105
+ name: "No Mock Data",
106
+ shortName: "Mock",
107
+ status: "fail" as const,
108
+ message: "Found 5 issues",
109
+ details: ["file1.ts", "file2.ts"],
110
+ },
111
+ {
112
+ id: "no-localhost",
113
+ name: "No Localhost",
114
+ shortName: "URLs",
115
+ status: "pass" as const,
116
+ message: "Clean",
117
+ },
118
+ {
119
+ id: "env-vars",
120
+ name: "Env Vars",
121
+ shortName: "Env",
122
+ status: "fail" as const,
123
+ message: "Missing vars",
124
+ },
125
+ ],
126
+ badges: {
127
+ main: "<svg>...</svg>",
128
+ mockData: "<svg>...</svg>",
129
+ realApi: "<svg>...</svg>",
130
+ envVars: "<svg>...</svg>",
131
+ billing: "<svg>...</svg>",
132
+ database: "<svg>...</svg>",
133
+ oauth: "<svg>...</svg>",
134
+ combined: "<svg>...</svg>",
135
+ },
136
+ timestamp: new Date().toISOString(),
137
+ expiresAt: new Date().toISOString(),
138
+ permalink: "https://Guardrail.dev/badge/test123",
139
+ embedCode: "[![Badge](url)](link)",
140
+ };
141
+
142
+ const report = generator.generateReport(mockResult);
143
+
144
+ expect(report).toContain("NO SHIP");
145
+ expect(report).toContain("Bad App");
146
+ expect(report).toContain("❌");
147
+ });
148
+ });
149
+
150
+ describe("badges", () => {
151
+ it("should generate valid SVG badges", async () => {
152
+ const result = await generator.generateShipBadge({
153
+ projectPath: __dirname,
154
+ projectName: "test",
155
+ });
156
+
157
+ expect(result.badges.main).toContain("<svg");
158
+ expect(result.badges.main).toContain("</svg>");
159
+ expect(result.badges.combined).toContain("<svg");
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Ship Badge Generator
3
+ *
4
+ * "One-click shareable proof that your app is real."
5
+ *
6
+ * Social proof badges for README / landing page / Product Hunt.
7
+ */
8
+
9
+ export {
10
+ ShipBadgeGenerator,
11
+ shipBadgeGenerator,
12
+ type ShipCheck,
13
+ type ShipBadgeResult,
14
+ type ShipBadges,
15
+ type ShipBadgeConfig,
16
+ } from "./ship-badge-generator";