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.
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mock-implementation.d.ts +1 -0
- package/dist/mock-implementation.d.ts.map +1 -0
- package/dist/mock-implementation.js +2 -0
- package/dist/mock-implementation.js.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts +5 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.d.ts.map +1 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js +92 -0
- package/dist/mockproof/__tests__/import-graph-scanner.test.js.map +1 -0
- package/dist/mockproof/import-graph-scanner.d.ts +93 -0
- package/dist/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/dist/mockproof/import-graph-scanner.js +411 -0
- package/dist/mockproof/import-graph-scanner.js.map +1 -0
- package/dist/mockproof/index.d.ts +10 -0
- package/dist/mockproof/index.d.ts.map +1 -0
- package/dist/mockproof/index.js +10 -0
- package/dist/mockproof/index.js.map +1 -0
- package/dist/reality-mode/auth-enforcer.d.ts +13 -0
- package/dist/reality-mode/auth-enforcer.d.ts.map +1 -0
- package/dist/reality-mode/auth-enforcer.js +90 -0
- package/dist/reality-mode/auth-enforcer.js.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts +71 -0
- package/dist/reality-mode/explorer/critical-flows.d.ts.map +1 -0
- package/dist/reality-mode/explorer/critical-flows.js +463 -0
- package/dist/reality-mode/explorer/critical-flows.js.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts +52 -0
- package/dist/reality-mode/explorer/flow-parser.d.ts.map +1 -0
- package/dist/reality-mode/explorer/flow-parser.js +250 -0
- package/dist/reality-mode/explorer/flow-parser.js.map +1 -0
- package/dist/reality-mode/explorer/index.d.ts +11 -0
- package/dist/reality-mode/explorer/index.d.ts.map +1 -0
- package/dist/reality-mode/explorer/index.js +11 -0
- package/dist/reality-mode/explorer/index.js.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts +35 -0
- package/dist/reality-mode/explorer/runtime-explorer.d.ts.map +1 -0
- package/dist/reality-mode/explorer/runtime-explorer.js +688 -0
- package/dist/reality-mode/explorer/runtime-explorer.js.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts +60 -0
- package/dist/reality-mode/explorer/surface-discovery.d.ts.map +1 -0
- package/dist/reality-mode/explorer/surface-discovery.js +357 -0
- package/dist/reality-mode/explorer/surface-discovery.js.map +1 -0
- package/dist/reality-mode/explorer/types.d.ts +275 -0
- package/dist/reality-mode/explorer/types.d.ts.map +1 -0
- package/dist/reality-mode/explorer/types.js +8 -0
- package/dist/reality-mode/explorer/types.js.map +1 -0
- package/dist/reality-mode/fake-success-detector.d.ts +10 -0
- package/dist/reality-mode/fake-success-detector.d.ts.map +1 -0
- package/dist/reality-mode/fake-success-detector.js +76 -0
- package/dist/reality-mode/fake-success-detector.js.map +1 -0
- package/dist/reality-mode/index.d.ts +14 -0
- package/dist/reality-mode/index.d.ts.map +1 -0
- package/dist/reality-mode/index.js +14 -0
- package/dist/reality-mode/index.js.map +1 -0
- package/dist/reality-mode/reality-scanner.d.ts +48 -0
- package/dist/reality-mode/reality-scanner.d.ts.map +1 -0
- package/dist/reality-mode/reality-scanner.js +516 -0
- package/dist/reality-mode/reality-scanner.js.map +1 -0
- package/dist/reality-mode/report-generator.d.ts +11 -0
- package/dist/reality-mode/report-generator.d.ts.map +1 -0
- package/dist/reality-mode/report-generator.js +233 -0
- package/dist/reality-mode/report-generator.js.map +1 -0
- package/dist/reality-mode/traffic-classifier.d.ts +14 -0
- package/dist/reality-mode/traffic-classifier.d.ts.map +1 -0
- package/dist/reality-mode/traffic-classifier.js +131 -0
- package/dist/reality-mode/traffic-classifier.js.map +1 -0
- package/dist/reality-mode/types.d.ts +90 -0
- package/dist/reality-mode/types.d.ts.map +1 -0
- package/dist/reality-mode/types.js +2 -0
- package/dist/reality-mode/types.js.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts +5 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.d.ts.map +1 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js +146 -0
- package/dist/ship-badge/__tests__/ship-badge-generator.test.js.map +1 -0
- package/dist/ship-badge/index.d.ts +9 -0
- package/dist/ship-badge/index.d.ts.map +1 -0
- package/dist/ship-badge/index.js +9 -0
- package/dist/ship-badge/index.js.map +1 -0
- package/dist/ship-badge/ship-badge-generator.d.ts +136 -0
- package/dist/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/dist/ship-badge/ship-badge-generator.js +681 -0
- package/dist/ship-badge/ship-badge-generator.js.map +1 -0
- package/package.json +20 -0
- package/src/index.ts +7 -0
- package/src/mock-implementation.ts +0 -0
- package/src/mockproof/__tests__/import-graph-scanner.test.ts +115 -0
- package/src/mockproof/import-graph-scanner.d.ts +93 -0
- package/src/mockproof/import-graph-scanner.d.ts.map +1 -0
- package/src/mockproof/import-graph-scanner.js +482 -0
- package/src/mockproof/import-graph-scanner.ts +540 -0
- package/src/mockproof/index.ts +18 -0
- package/src/reality-mode/auth-enforcer.ts +97 -0
- package/src/reality-mode/explorer/critical-flows.ts +504 -0
- package/src/reality-mode/explorer/flow-parser.ts +293 -0
- package/src/reality-mode/explorer/index.ts +22 -0
- package/src/reality-mode/explorer/runtime-explorer.ts +715 -0
- package/src/reality-mode/explorer/surface-discovery.ts +498 -0
- package/src/reality-mode/explorer/templates/example-flows/auth-flow.yaml +41 -0
- package/src/reality-mode/explorer/templates/example-flows/checkout-flow.yaml +66 -0
- package/src/reality-mode/explorer/templates/example-flows/contact-form.yaml +43 -0
- package/src/reality-mode/explorer/templates/github-action.yml +132 -0
- package/src/reality-mode/explorer/types.ts +356 -0
- package/src/reality-mode/fake-success-detector.ts +89 -0
- package/src/reality-mode/index.ts +19 -0
- package/src/reality-mode/reality-scanner.d.ts +123 -0
- package/src/reality-mode/reality-scanner.d.ts.map +1 -0
- package/src/reality-mode/reality-scanner.js +526 -0
- package/src/reality-mode/reality-scanner.ts +576 -0
- package/src/reality-mode/report-generator.ts +253 -0
- package/src/reality-mode/traffic-classifier.ts +169 -0
- package/src/reality-mode/types.ts +95 -0
- package/src/ship-badge/__tests__/ship-badge-generator.test.ts +162 -0
- package/src/ship-badge/index.ts +16 -0
- package/src/ship-badge/ship-badge-generator.d.ts +136 -0
- package/src/ship-badge/ship-badge-generator.d.ts.map +1 -0
- package/src/ship-badge/ship-badge-generator.js +779 -0
- package/src/ship-badge/ship-badge-generator.ts +873 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Guardrail Reality Mode - GitHub Actions Workflow
|
|
2
|
+
#
|
|
3
|
+
# This workflow runs Reality Mode tests on every PR and deployment.
|
|
4
|
+
# Copy this to .github/workflows/reality-mode.yml in your repo.
|
|
5
|
+
#
|
|
6
|
+
# Prerequisites:
|
|
7
|
+
# - Set REALITY_TEST_URL secret (your staging/preview URL)
|
|
8
|
+
# - Set REALITY_AUTH secret (optional: email:password for auth)
|
|
9
|
+
|
|
10
|
+
name: Reality Mode Tests
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [main, master]
|
|
15
|
+
deployment_status:
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
url:
|
|
19
|
+
description: "URL to test"
|
|
20
|
+
required: true
|
|
21
|
+
type: string
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
reality-test:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
timeout-minutes: 15
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- name: Checkout
|
|
30
|
+
uses: actions/checkout@v4
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
34
|
+
with:
|
|
35
|
+
node-version: "20"
|
|
36
|
+
|
|
37
|
+
- name: Install Playwright
|
|
38
|
+
run: |
|
|
39
|
+
npm install -D @playwright/test
|
|
40
|
+
npx playwright install chromium --with-deps
|
|
41
|
+
|
|
42
|
+
- name: Install Guardrail
|
|
43
|
+
run: npm install -g guardrail
|
|
44
|
+
|
|
45
|
+
- name: Determine URL
|
|
46
|
+
id: url
|
|
47
|
+
run: |
|
|
48
|
+
if [ -n "${{ github.event.inputs.url }}" ]; then
|
|
49
|
+
echo "url=${{ github.event.inputs.url }}" >> $GITHUB_OUTPUT
|
|
50
|
+
elif [ -n "${{ secrets.REALITY_TEST_URL }}" ]; then
|
|
51
|
+
echo "url=${{ secrets.REALITY_TEST_URL }}" >> $GITHUB_OUTPUT
|
|
52
|
+
elif [ "${{ github.event.deployment_status.state }}" == "success" ]; then
|
|
53
|
+
echo "url=${{ github.event.deployment_status.target_url }}" >> $GITHUB_OUTPUT
|
|
54
|
+
else
|
|
55
|
+
echo "url=" >> $GITHUB_OUTPUT
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
- name: Run Reality Mode
|
|
59
|
+
if: steps.url.outputs.url != ''
|
|
60
|
+
run: |
|
|
61
|
+
guardrail reality \
|
|
62
|
+
--url "${{ steps.url.outputs.url }}" \
|
|
63
|
+
${{ secrets.REALITY_AUTH && format('--auth "{0}"', secrets.REALITY_AUTH) || '' }} \
|
|
64
|
+
--junit \
|
|
65
|
+
--output .guardrail/reality
|
|
66
|
+
continue-on-error: true
|
|
67
|
+
|
|
68
|
+
- name: Upload Results
|
|
69
|
+
if: always() && steps.url.outputs.url != ''
|
|
70
|
+
uses: actions/upload-artifact@v4
|
|
71
|
+
with:
|
|
72
|
+
name: reality-mode-results
|
|
73
|
+
path: |
|
|
74
|
+
.guardrail/reality/reality-report.html
|
|
75
|
+
.guardrail/reality/explorer-results.json
|
|
76
|
+
.guardrail/reality/videos/
|
|
77
|
+
.guardrail/reality/trace.zip
|
|
78
|
+
retention-days: 14
|
|
79
|
+
|
|
80
|
+
- name: Upload JUnit Results
|
|
81
|
+
if: always() && steps.url.outputs.url != ''
|
|
82
|
+
uses: actions/upload-artifact@v4
|
|
83
|
+
with:
|
|
84
|
+
name: junit-results
|
|
85
|
+
path: .guardrail/reality/junit-results.xml
|
|
86
|
+
|
|
87
|
+
- name: Publish Test Results
|
|
88
|
+
if: always() && steps.url.outputs.url != ''
|
|
89
|
+
uses: dorny/test-reporter@v1
|
|
90
|
+
with:
|
|
91
|
+
name: Reality Mode Tests
|
|
92
|
+
path: .guardrail/reality/junit-results.xml
|
|
93
|
+
reporter: java-junit
|
|
94
|
+
fail-on-error: false
|
|
95
|
+
|
|
96
|
+
- name: Comment on PR
|
|
97
|
+
if: github.event_name == 'pull_request' && steps.url.outputs.url != ''
|
|
98
|
+
uses: actions/github-script@v7
|
|
99
|
+
with:
|
|
100
|
+
script: |
|
|
101
|
+
const fs = require('fs');
|
|
102
|
+
const path = '.guardrail/reality/explorer-results.json';
|
|
103
|
+
|
|
104
|
+
if (!fs.existsSync(path)) {
|
|
105
|
+
console.log('No results file found');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const results = JSON.parse(fs.readFileSync(path, 'utf8'));
|
|
110
|
+
const score = results.score || 0;
|
|
111
|
+
const emoji = score >= 80 ? '🟢' : score >= 60 ? '🟡' : '🔴';
|
|
112
|
+
|
|
113
|
+
const body = `## ${emoji} Reality Mode Score: ${score}/100
|
|
114
|
+
|
|
115
|
+
| Metric | Result |
|
|
116
|
+
|--------|--------|
|
|
117
|
+
| Routes | ${results.coverage?.routes || 0}/${results.routes?.length || 0} working |
|
|
118
|
+
| Elements | ${results.coverage?.elements || 0}/${results.elements?.length || 0} working |
|
|
119
|
+
| Forms | ${results.coverage?.forms || 0}/${results.forms?.length || 0} working |
|
|
120
|
+
| Errors | ${results.errors?.length || 0} captured |
|
|
121
|
+
|
|
122
|
+
${score >= 80 ? '✅ **Ready to ship!**' : score >= 60 ? '⚠️ **Needs some work**' : '❌ **Critical issues found**'}
|
|
123
|
+
|
|
124
|
+
📊 [View Full Report](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
github.rest.issues.createComment({
|
|
128
|
+
issue_number: context.issue.number,
|
|
129
|
+
owner: context.repo.owner,
|
|
130
|
+
repo: context.repo.repo,
|
|
131
|
+
body
|
|
132
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reality Explorer Types
|
|
3
|
+
*
|
|
4
|
+
* Types for the comprehensive app exploration system that actually
|
|
5
|
+
* tests everything - buttons, forms, modals, auth flows, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// App Surface Discovery
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface DiscoveredRoute {
|
|
13
|
+
path: string;
|
|
14
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
15
|
+
source: "link" | "router" | "api-call" | "redirect";
|
|
16
|
+
requiresAuth: boolean;
|
|
17
|
+
visited: boolean;
|
|
18
|
+
status?: number;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DiscoveredElement {
|
|
23
|
+
id: string;
|
|
24
|
+
selector: string;
|
|
25
|
+
type:
|
|
26
|
+
| "button"
|
|
27
|
+
| "link"
|
|
28
|
+
| "input"
|
|
29
|
+
| "form"
|
|
30
|
+
| "modal-trigger"
|
|
31
|
+
| "dropdown"
|
|
32
|
+
| "tab"
|
|
33
|
+
| "accordion";
|
|
34
|
+
text: string;
|
|
35
|
+
page: string;
|
|
36
|
+
isDestructive: boolean;
|
|
37
|
+
tested: boolean;
|
|
38
|
+
result?: ElementTestResult;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DiscoveredForm {
|
|
42
|
+
id: string;
|
|
43
|
+
selector: string;
|
|
44
|
+
page: string;
|
|
45
|
+
action?: string;
|
|
46
|
+
method: string;
|
|
47
|
+
fields: FormField[];
|
|
48
|
+
submitButton?: string;
|
|
49
|
+
tested: boolean;
|
|
50
|
+
result?: FormTestResult;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FormField {
|
|
54
|
+
name: string;
|
|
55
|
+
type: string;
|
|
56
|
+
required: boolean;
|
|
57
|
+
selector: string;
|
|
58
|
+
placeholder?: string;
|
|
59
|
+
pattern?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DiscoveredAPI {
|
|
63
|
+
url: string;
|
|
64
|
+
method: string;
|
|
65
|
+
calledFrom: string;
|
|
66
|
+
status?: number;
|
|
67
|
+
responseTime?: number;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AppSurface {
|
|
72
|
+
routes: DiscoveredRoute[];
|
|
73
|
+
elements: DiscoveredElement[];
|
|
74
|
+
forms: DiscoveredForm[];
|
|
75
|
+
apis: DiscoveredAPI[];
|
|
76
|
+
timestamp: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Test Results
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
export interface ElementTestResult {
|
|
84
|
+
success: boolean;
|
|
85
|
+
action: "click" | "hover" | "focus";
|
|
86
|
+
beforeState: PageState;
|
|
87
|
+
afterState: PageState;
|
|
88
|
+
changes: StateChange[];
|
|
89
|
+
errors: CapturedError[];
|
|
90
|
+
networkCalls: NetworkCall[];
|
|
91
|
+
duration: number;
|
|
92
|
+
screenshot?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface FormTestResult {
|
|
96
|
+
success: boolean;
|
|
97
|
+
fieldsFilledCount: number;
|
|
98
|
+
submitAttempted: boolean;
|
|
99
|
+
submitSucceeded: boolean;
|
|
100
|
+
validationErrors: string[];
|
|
101
|
+
networkCalls: NetworkCall[];
|
|
102
|
+
errors: CapturedError[];
|
|
103
|
+
duration: number;
|
|
104
|
+
screenshot?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface PageState {
|
|
108
|
+
url: string;
|
|
109
|
+
title: string;
|
|
110
|
+
modalsOpen: number;
|
|
111
|
+
loadingIndicators: number;
|
|
112
|
+
errorMessages: string[];
|
|
113
|
+
domHash: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface StateChange {
|
|
117
|
+
type: "url" | "modal" | "dom" | "console" | "network";
|
|
118
|
+
description: string;
|
|
119
|
+
significance: "major" | "minor" | "none";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface CapturedError {
|
|
123
|
+
type: "console" | "network" | "uncaught" | "react-boundary";
|
|
124
|
+
message: string;
|
|
125
|
+
stack?: string;
|
|
126
|
+
url?: string;
|
|
127
|
+
timestamp: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface NetworkCall {
|
|
131
|
+
url: string;
|
|
132
|
+
method: string;
|
|
133
|
+
status: number;
|
|
134
|
+
duration: number;
|
|
135
|
+
requestBody?: string;
|
|
136
|
+
responsePreview?: string;
|
|
137
|
+
error?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Critical Flows
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
export interface CriticalFlow {
|
|
145
|
+
id: string;
|
|
146
|
+
name: string;
|
|
147
|
+
description: string;
|
|
148
|
+
steps: FlowStep[];
|
|
149
|
+
assertions: FlowAssertion[];
|
|
150
|
+
required: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface FlowStep {
|
|
154
|
+
action: "navigate" | "click" | "fill" | "wait" | "assert";
|
|
155
|
+
target?: string;
|
|
156
|
+
value?: string;
|
|
157
|
+
timeout?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface FlowAssertion {
|
|
161
|
+
type:
|
|
162
|
+
| "url-contains"
|
|
163
|
+
| "element-visible"
|
|
164
|
+
| "element-hidden"
|
|
165
|
+
| "cookie-exists"
|
|
166
|
+
| "localstorage-has"
|
|
167
|
+
| "network-success"
|
|
168
|
+
| "no-errors";
|
|
169
|
+
value: string;
|
|
170
|
+
critical: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface FlowResult {
|
|
174
|
+
flow: CriticalFlow;
|
|
175
|
+
success: boolean;
|
|
176
|
+
stepsCompleted: number;
|
|
177
|
+
stepsTotal: number;
|
|
178
|
+
assertionsPassed: number;
|
|
179
|
+
assertionsTotal: number;
|
|
180
|
+
failedAt?: string;
|
|
181
|
+
errors: CapturedError[];
|
|
182
|
+
duration: number;
|
|
183
|
+
trace?: string;
|
|
184
|
+
video?: string;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// Coverage & Scoring
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
export interface CoverageMetrics {
|
|
192
|
+
routes: {
|
|
193
|
+
discovered: number;
|
|
194
|
+
visited: number;
|
|
195
|
+
successful: number;
|
|
196
|
+
blocked: number;
|
|
197
|
+
percentage: number;
|
|
198
|
+
};
|
|
199
|
+
elements: {
|
|
200
|
+
discovered: number;
|
|
201
|
+
tested: number;
|
|
202
|
+
successful: number;
|
|
203
|
+
skippedDestructive: number;
|
|
204
|
+
percentage: number;
|
|
205
|
+
};
|
|
206
|
+
forms: {
|
|
207
|
+
discovered: number;
|
|
208
|
+
tested: number;
|
|
209
|
+
successful: number;
|
|
210
|
+
blockedByAuth: number;
|
|
211
|
+
percentage: number;
|
|
212
|
+
};
|
|
213
|
+
apis: {
|
|
214
|
+
discovered: number;
|
|
215
|
+
called: number;
|
|
216
|
+
successful: number;
|
|
217
|
+
failed: number;
|
|
218
|
+
percentage: number;
|
|
219
|
+
};
|
|
220
|
+
flows: {
|
|
221
|
+
total: number;
|
|
222
|
+
passed: number;
|
|
223
|
+
failed: number;
|
|
224
|
+
skipped: number;
|
|
225
|
+
percentage: number;
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface RealityScore {
|
|
230
|
+
overall: number; // 0-100
|
|
231
|
+
breakdown: {
|
|
232
|
+
coverage: number; // 40 points max
|
|
233
|
+
functionality: number; // 35 points max
|
|
234
|
+
stability: number; // 15 points max
|
|
235
|
+
ux: number; // 10 points max
|
|
236
|
+
};
|
|
237
|
+
grade: "A" | "B" | "C" | "D" | "F";
|
|
238
|
+
verdict: "ship-it" | "needs-work" | "broken";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Explorer Config
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
export interface ExplorerConfig {
|
|
246
|
+
baseUrl: string;
|
|
247
|
+
maxPages: number;
|
|
248
|
+
maxActionsPerPage: number;
|
|
249
|
+
timeout: number;
|
|
250
|
+
headless: boolean;
|
|
251
|
+
|
|
252
|
+
// Auth config
|
|
253
|
+
auth?: {
|
|
254
|
+
loginUrl: string;
|
|
255
|
+
credentials: {
|
|
256
|
+
emailField: string;
|
|
257
|
+
passwordField: string;
|
|
258
|
+
email: string;
|
|
259
|
+
password: string;
|
|
260
|
+
};
|
|
261
|
+
successIndicator: string;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// Safety
|
|
265
|
+
allowDestructive: boolean;
|
|
266
|
+
destructivePatterns: string[];
|
|
267
|
+
|
|
268
|
+
// Output
|
|
269
|
+
outputDir: string;
|
|
270
|
+
captureVideo: boolean;
|
|
271
|
+
captureTrace: boolean;
|
|
272
|
+
captureScreenshots: boolean;
|
|
273
|
+
|
|
274
|
+
// Custom flows
|
|
275
|
+
flows?: CriticalFlow[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ============================================================================
|
|
279
|
+
// Final Report
|
|
280
|
+
// ============================================================================
|
|
281
|
+
|
|
282
|
+
export interface ExplorerReport {
|
|
283
|
+
summary: {
|
|
284
|
+
score: RealityScore;
|
|
285
|
+
coverage: CoverageMetrics;
|
|
286
|
+
duration: number;
|
|
287
|
+
timestamp: string;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
surface: AppSurface;
|
|
291
|
+
|
|
292
|
+
results: {
|
|
293
|
+
routes: RouteResult[];
|
|
294
|
+
elements: ElementResult[];
|
|
295
|
+
forms: FormResult[];
|
|
296
|
+
flows: FlowResult[];
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
failures: {
|
|
300
|
+
critical: FailureReport[];
|
|
301
|
+
warnings: FailureReport[];
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
recommendations: Recommendation[];
|
|
305
|
+
|
|
306
|
+
artifacts: {
|
|
307
|
+
traces: string[];
|
|
308
|
+
videos: string[];
|
|
309
|
+
screenshots: string[];
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface RouteResult {
|
|
314
|
+
route: DiscoveredRoute;
|
|
315
|
+
status: "success" | "error" | "blocked" | "skipped";
|
|
316
|
+
responseTime?: number;
|
|
317
|
+
errors: CapturedError[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface ElementResult {
|
|
321
|
+
element: DiscoveredElement;
|
|
322
|
+
status: "success" | "error" | "no-change" | "skipped";
|
|
323
|
+
result?: ElementTestResult;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export interface FormResult {
|
|
327
|
+
form: DiscoveredForm;
|
|
328
|
+
status:
|
|
329
|
+
| "success"
|
|
330
|
+
| "validation-error"
|
|
331
|
+
| "submit-error"
|
|
332
|
+
| "blocked"
|
|
333
|
+
| "skipped";
|
|
334
|
+
result?: FormTestResult;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface FailureReport {
|
|
338
|
+
id: string;
|
|
339
|
+
type: "route" | "element" | "form" | "flow" | "api";
|
|
340
|
+
severity: "critical" | "warning";
|
|
341
|
+
title: string;
|
|
342
|
+
description: string;
|
|
343
|
+
location: string;
|
|
344
|
+
rootCause?: string;
|
|
345
|
+
reproduction: string[];
|
|
346
|
+
screenshot?: string;
|
|
347
|
+
trace?: string;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface Recommendation {
|
|
351
|
+
priority: "high" | "medium" | "low";
|
|
352
|
+
category: "functionality" | "auth" | "ux" | "performance" | "stability";
|
|
353
|
+
title: string;
|
|
354
|
+
description: string;
|
|
355
|
+
fix?: string;
|
|
356
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ReplayStep, FakeSuccessResult } from "./types";
|
|
2
|
+
|
|
3
|
+
export class FakeSuccessDetector {
|
|
4
|
+
/**
|
|
5
|
+
* Analyze a replay to find "Fake Success" patterns
|
|
6
|
+
* i.e., User clicked "Save" -> UI showed success -> No backend write happened
|
|
7
|
+
*/
|
|
8
|
+
detect(replay: ReplayStep[]): FakeSuccessResult[] {
|
|
9
|
+
const results: FakeSuccessResult[] = [];
|
|
10
|
+
const saveActionPatterns = [
|
|
11
|
+
/save/i,
|
|
12
|
+
/update/i,
|
|
13
|
+
/create/i,
|
|
14
|
+
/submit/i,
|
|
15
|
+
/confirm/i,
|
|
16
|
+
/send/i,
|
|
17
|
+
/pay/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Iterate through replay to find "Write" actions
|
|
21
|
+
for (let i = 0; i < replay.length; i++) {
|
|
22
|
+
const step = replay[i];
|
|
23
|
+
if (!step || step.type !== "action" || !step.data?.selector) continue;
|
|
24
|
+
|
|
25
|
+
const selector = step.data.selector;
|
|
26
|
+
const isWriteAction = saveActionPatterns.some((p) => p.test(selector));
|
|
27
|
+
|
|
28
|
+
if (isWriteAction) {
|
|
29
|
+
// Look ahead for network activity (next 5 seconds or until next action)
|
|
30
|
+
const subsequentSteps = this.getSubsequentSteps(replay, i, 5000);
|
|
31
|
+
const writeRequests = subsequentSteps.filter(
|
|
32
|
+
(s) =>
|
|
33
|
+
s.type === "request" &&
|
|
34
|
+
["POST", "PUT", "PATCH", "DELETE"].includes(s.data.method),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (writeRequests.length === 0) {
|
|
38
|
+
// No write request found!
|
|
39
|
+
// But maybe it's a client-side only app?
|
|
40
|
+
// Or maybe the request happened but we missed it?
|
|
41
|
+
// Or maybe it's "Fake Success".
|
|
42
|
+
|
|
43
|
+
results.push({
|
|
44
|
+
isFake: true,
|
|
45
|
+
score: 0,
|
|
46
|
+
evidence: [
|
|
47
|
+
`Clicked "${selector}" but no POST/PUT/PATCH/DELETE request followed.`,
|
|
48
|
+
],
|
|
49
|
+
actionStep: step,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
// Write request found. Check if it looked real.
|
|
53
|
+
// (TrafficClassifier handles the quality of the request/response)
|
|
54
|
+
results.push({
|
|
55
|
+
isFake: false,
|
|
56
|
+
score: 100,
|
|
57
|
+
evidence: [
|
|
58
|
+
`Clicked "${selector}" triggered ${writeRequests.length} write request(s).`,
|
|
59
|
+
],
|
|
60
|
+
actionStep: step,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private getSubsequentSteps(
|
|
70
|
+
replay: ReplayStep[],
|
|
71
|
+
startIndex: number,
|
|
72
|
+
timeWindow: number,
|
|
73
|
+
): ReplayStep[] {
|
|
74
|
+
const steps: ReplayStep[] = [];
|
|
75
|
+
const startStep = replay[startIndex];
|
|
76
|
+
if (!startStep) return steps;
|
|
77
|
+
|
|
78
|
+
const startTime = startStep.timestamp;
|
|
79
|
+
|
|
80
|
+
for (let i = startIndex + 1; i < replay.length; i++) {
|
|
81
|
+
const step = replay[i];
|
|
82
|
+
if (!step) break;
|
|
83
|
+
if (step.timestamp - startStep.timestamp > timeWindow) break;
|
|
84
|
+
steps.push(step);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return steps;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reality Mode - Runtime Fake Detection
|
|
3
|
+
*
|
|
4
|
+
* "Stop shipping pretend features. Guardrail runs your app and catches the lies."
|
|
5
|
+
*
|
|
6
|
+
* A literal "flight recorder" for fake apps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
RealityScanner,
|
|
11
|
+
realityScanner,
|
|
12
|
+
DEFAULT_FAKE_PATTERNS,
|
|
13
|
+
} from "./reality-scanner";
|
|
14
|
+
|
|
15
|
+
export * from "./types";
|
|
16
|
+
export { ReportGenerator } from "./report-generator";
|
|
17
|
+
export { TrafficClassifier } from "./traffic-classifier";
|
|
18
|
+
export { FakeSuccessDetector } from "./fake-success-detector";
|
|
19
|
+
export { AuthEnforcer } from "./auth-enforcer";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reality Mode - Runtime Fake Detection
|
|
3
|
+
*
|
|
4
|
+
* "Stop shipping pretend features. Guardrail runs your app and catches the lies."
|
|
5
|
+
*
|
|
6
|
+
* This module spins up the app, intercepts network calls, clicks through the UI,
|
|
7
|
+
* and detects:
|
|
8
|
+
* - Calls to localhost, jsonplaceholder, staging domains, ngrok
|
|
9
|
+
* - Routes returning demo/placeholder responses
|
|
10
|
+
* - Silent fallback success patterns
|
|
11
|
+
* - Mock billing and fake invoice IDs
|
|
12
|
+
*
|
|
13
|
+
* The killer feature: a "flight recorder" replay showing exactly what happened.
|
|
14
|
+
*/
|
|
15
|
+
export interface FakePattern {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
severity: "critical" | "warning" | "info";
|
|
20
|
+
detect: (request: InterceptedRequest | InterceptedResponse) => boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface InterceptedRequest {
|
|
23
|
+
type: "request";
|
|
24
|
+
url: string;
|
|
25
|
+
method: string;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
body?: string;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
export interface InterceptedResponse {
|
|
31
|
+
type: "response";
|
|
32
|
+
url: string;
|
|
33
|
+
status: number;
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
body?: string;
|
|
36
|
+
timestamp: number;
|
|
37
|
+
}
|
|
38
|
+
export interface UserAction {
|
|
39
|
+
type: "click" | "input" | "navigation" | "scroll";
|
|
40
|
+
selector?: string;
|
|
41
|
+
value?: string;
|
|
42
|
+
url?: string;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
screenshot?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface FakeDetection {
|
|
47
|
+
pattern: FakePattern;
|
|
48
|
+
request?: InterceptedRequest;
|
|
49
|
+
response?: InterceptedResponse;
|
|
50
|
+
action?: UserAction;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
evidence: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ReplayStep {
|
|
55
|
+
timestamp: number;
|
|
56
|
+
action?: UserAction;
|
|
57
|
+
request?: InterceptedRequest;
|
|
58
|
+
response?: InterceptedResponse;
|
|
59
|
+
detections: FakeDetection[];
|
|
60
|
+
screenshot?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface RealityModeResult {
|
|
63
|
+
verdict: "real" | "fake" | "suspicious";
|
|
64
|
+
score: number;
|
|
65
|
+
detections: FakeDetection[];
|
|
66
|
+
replay: ReplayStep[];
|
|
67
|
+
summary: {
|
|
68
|
+
totalRequests: number;
|
|
69
|
+
fakeRequests: number;
|
|
70
|
+
totalActions: number;
|
|
71
|
+
criticalIssues: number;
|
|
72
|
+
warnings: number;
|
|
73
|
+
};
|
|
74
|
+
timestamp: string;
|
|
75
|
+
duration: number;
|
|
76
|
+
}
|
|
77
|
+
export interface RealityModeConfig {
|
|
78
|
+
baseUrl: string;
|
|
79
|
+
timeout: number;
|
|
80
|
+
patterns: FakePattern[];
|
|
81
|
+
clickPaths: string[][];
|
|
82
|
+
screenshotOnDetection: boolean;
|
|
83
|
+
headless: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare const DEFAULT_FAKE_PATTERNS: FakePattern[];
|
|
86
|
+
export declare class RealityScanner {
|
|
87
|
+
private config;
|
|
88
|
+
private replay;
|
|
89
|
+
private detections;
|
|
90
|
+
private requests;
|
|
91
|
+
private responses;
|
|
92
|
+
private actions;
|
|
93
|
+
constructor(config?: Partial<RealityModeConfig>);
|
|
94
|
+
/**
|
|
95
|
+
* Generate Playwright test code for Reality Mode scanning
|
|
96
|
+
*/
|
|
97
|
+
generatePlaywrightTest(config: {
|
|
98
|
+
baseUrl: string;
|
|
99
|
+
clickPaths: string[][];
|
|
100
|
+
outputDir: string;
|
|
101
|
+
}): string;
|
|
102
|
+
/**
|
|
103
|
+
* Analyze intercepted network traffic for fake patterns
|
|
104
|
+
*/
|
|
105
|
+
analyzeTraffic(
|
|
106
|
+
requests: InterceptedRequest[],
|
|
107
|
+
responses: InterceptedResponse[],
|
|
108
|
+
): FakeDetection[];
|
|
109
|
+
/**
|
|
110
|
+
* Generate evidence description for a detection
|
|
111
|
+
*/
|
|
112
|
+
private getEvidence;
|
|
113
|
+
/**
|
|
114
|
+
* Generate a human-readable Reality Mode report
|
|
115
|
+
*/
|
|
116
|
+
generateReport(result: RealityModeResult): string;
|
|
117
|
+
/**
|
|
118
|
+
* Generate common click paths for testing
|
|
119
|
+
*/
|
|
120
|
+
generateDefaultClickPaths(): string[][];
|
|
121
|
+
}
|
|
122
|
+
export declare const realityScanner: RealityScanner;
|
|
123
|
+
//# sourceMappingURL=reality-scanner.d.ts.map
|