opensafari-mcp 0.1.1

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 (235) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/dist/115.index.js +2 -0
  4. package/dist/115.index.js.map +1 -0
  5. package/dist/67.index.js +2 -0
  6. package/dist/67.index.js.map +1 -0
  7. package/dist/679.index.js +2 -0
  8. package/dist/679.index.js.map +1 -0
  9. package/dist/681.index.js +2 -0
  10. package/dist/681.index.js.map +1 -0
  11. package/dist/838.index.js +2 -0
  12. package/dist/838.index.js.map +1 -0
  13. package/dist/auth/index.d.ts +3 -0
  14. package/dist/auth/index.d.ts.map +1 -0
  15. package/dist/auth/manager.d.ts +33 -0
  16. package/dist/auth/manager.d.ts.map +1 -0
  17. package/dist/cli/148.index.js +3 -0
  18. package/dist/cli/148.index.js.map +1 -0
  19. package/dist/cli/473.index.js +3 -0
  20. package/dist/cli/473.index.js.map +1 -0
  21. package/dist/cli/622.index.js +3 -0
  22. package/dist/cli/622.index.js.map +1 -0
  23. package/dist/cli/712.index.js +3 -0
  24. package/dist/cli/712.index.js.map +1 -0
  25. package/dist/cli/844.index.js +3 -0
  26. package/dist/cli/844.index.js.map +1 -0
  27. package/dist/cli/index.d.ts +3 -0
  28. package/dist/cli/index.d.ts.map +1 -0
  29. package/dist/cli/index.js +3 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/comparison/cross-viewport.d.ts +36 -0
  32. package/dist/comparison/cross-viewport.d.ts.map +1 -0
  33. package/dist/comparison/index.d.ts +4 -0
  34. package/dist/comparison/index.d.ts.map +1 -0
  35. package/dist/comparison/report.d.ts +10 -0
  36. package/dist/comparison/report.d.ts.map +1 -0
  37. package/dist/config/defaults.d.ts +33 -0
  38. package/dist/config/defaults.d.ts.map +1 -0
  39. package/dist/config/global.d.ts +25 -0
  40. package/dist/config/global.d.ts.map +1 -0
  41. package/dist/config/index.d.ts +5 -0
  42. package/dist/config/index.d.ts.map +1 -0
  43. package/dist/config/tool-tiers.d.ts +7 -0
  44. package/dist/config/tool-tiers.d.ts.map +1 -0
  45. package/dist/errors/codes.d.ts +20 -0
  46. package/dist/errors/codes.d.ts.map +1 -0
  47. package/dist/errors/index.d.ts +4 -0
  48. package/dist/errors/index.d.ts.map +1 -0
  49. package/dist/errors/timeout.d.ts +20 -0
  50. package/dist/errors/timeout.d.ts.map +1 -0
  51. package/dist/index.d.ts +25 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +2 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/mcp-server.d.ts +41 -0
  56. package/dist/mcp-server.d.ts.map +1 -0
  57. package/dist/metrics/collector.d.ts +43 -0
  58. package/dist/metrics/collector.d.ts.map +1 -0
  59. package/dist/orchestration/index.d.ts +3 -0
  60. package/dist/orchestration/index.d.ts.map +1 -0
  61. package/dist/orchestration/workflow-engine.d.ts +90 -0
  62. package/dist/orchestration/workflow-engine.d.ts.map +1 -0
  63. package/dist/qa/audit.d.ts +42 -0
  64. package/dist/qa/audit.d.ts.map +1 -0
  65. package/dist/qa/detectors/auto-zoom.d.ts +4 -0
  66. package/dist/qa/detectors/auto-zoom.d.ts.map +1 -0
  67. package/dist/qa/detectors/dark-mode.d.ts +5 -0
  68. package/dist/qa/detectors/dark-mode.d.ts.map +1 -0
  69. package/dist/qa/detectors/fixed-stacking.d.ts +4 -0
  70. package/dist/qa/detectors/fixed-stacking.d.ts.map +1 -0
  71. package/dist/qa/detectors/horizontal-overflow.d.ts +4 -0
  72. package/dist/qa/detectors/horizontal-overflow.d.ts.map +1 -0
  73. package/dist/qa/detectors/hover-only.d.ts +4 -0
  74. package/dist/qa/detectors/hover-only.d.ts.map +1 -0
  75. package/dist/qa/detectors/index.d.ts +14 -0
  76. package/dist/qa/detectors/index.d.ts.map +1 -0
  77. package/dist/qa/detectors/input-type.d.ts +4 -0
  78. package/dist/qa/detectors/input-type.d.ts.map +1 -0
  79. package/dist/qa/detectors/keyboard-overlap.d.ts +4 -0
  80. package/dist/qa/detectors/keyboard-overlap.d.ts.map +1 -0
  81. package/dist/qa/detectors/orientation.d.ts +5 -0
  82. package/dist/qa/detectors/orientation.d.ts.map +1 -0
  83. package/dist/qa/detectors/pwa-meta.d.ts +4 -0
  84. package/dist/qa/detectors/pwa-meta.d.ts.map +1 -0
  85. package/dist/qa/detectors/safe-area.d.ts +4 -0
  86. package/dist/qa/detectors/safe-area.d.ts.map +1 -0
  87. package/dist/qa/detectors/scroll-lock.d.ts +4 -0
  88. package/dist/qa/detectors/scroll-lock.d.ts.map +1 -0
  89. package/dist/qa/detectors/touch-targets.d.ts +4 -0
  90. package/dist/qa/detectors/touch-targets.d.ts.map +1 -0
  91. package/dist/qa/detectors/vh100.d.ts +4 -0
  92. package/dist/qa/detectors/vh100.d.ts.map +1 -0
  93. package/dist/qa/history.d.ts +44 -0
  94. package/dist/qa/history.d.ts.map +1 -0
  95. package/dist/qa/index.d.ts +8 -0
  96. package/dist/qa/index.d.ts.map +1 -0
  97. package/dist/qa/report-markdown.d.ts +3 -0
  98. package/dist/qa/report-markdown.d.ts.map +1 -0
  99. package/dist/qa/types.d.ts +29 -0
  100. package/dist/qa/types.d.ts.map +1 -0
  101. package/dist/reliability/crash-watcher.d.ts +16 -0
  102. package/dist/reliability/crash-watcher.d.ts.map +1 -0
  103. package/dist/reliability/graceful-shutdown.d.ts +3 -0
  104. package/dist/reliability/graceful-shutdown.d.ts.map +1 -0
  105. package/dist/reliability/index.d.ts +4 -0
  106. package/dist/reliability/index.d.ts.map +1 -0
  107. package/dist/reliability/zombie-cleanup.d.ts +53 -0
  108. package/dist/reliability/zombie-cleanup.d.ts.map +1 -0
  109. package/dist/security/audit-logger.d.ts +2 -0
  110. package/dist/security/audit-logger.d.ts.map +1 -0
  111. package/dist/security/content-sanitizer.d.ts +32 -0
  112. package/dist/security/content-sanitizer.d.ts.map +1 -0
  113. package/dist/security/domain-guard.d.ts +16 -0
  114. package/dist/security/domain-guard.d.ts.map +1 -0
  115. package/dist/session-manager.d.ts +54 -0
  116. package/dist/session-manager.d.ts.map +1 -0
  117. package/dist/simulator/batch.d.ts +22 -0
  118. package/dist/simulator/batch.d.ts.map +1 -0
  119. package/dist/simulator/index.d.ts +15 -0
  120. package/dist/simulator/index.d.ts.map +1 -0
  121. package/dist/simulator/manager.d.ts +66 -0
  122. package/dist/simulator/manager.d.ts.map +1 -0
  123. package/dist/simulator/pool.d.ts +52 -0
  124. package/dist/simulator/pool.d.ts.map +1 -0
  125. package/dist/simulator/presets.d.ts +4 -0
  126. package/dist/simulator/presets.d.ts.map +1 -0
  127. package/dist/simulator/proxy.d.ts +78 -0
  128. package/dist/simulator/proxy.d.ts.map +1 -0
  129. package/dist/simulator/simctl.d.ts +12 -0
  130. package/dist/simulator/simctl.d.ts.map +1 -0
  131. package/dist/simulator/socket-finder.d.ts +22 -0
  132. package/dist/simulator/socket-finder.d.ts.map +1 -0
  133. package/dist/simulator/types.d.ts +21 -0
  134. package/dist/simulator/types.d.ts.map +1 -0
  135. package/dist/simulator/xcode-check.d.ts +15 -0
  136. package/dist/simulator/xcode-check.d.ts.map +1 -0
  137. package/dist/tools/appearance-toggle.d.ts +3 -0
  138. package/dist/tools/appearance-toggle.d.ts.map +1 -0
  139. package/dist/tools/auth.d.ts +3 -0
  140. package/dist/tools/auth.d.ts.map +1 -0
  141. package/dist/tools/batch-execute.d.ts +5 -0
  142. package/dist/tools/batch-execute.d.ts.map +1 -0
  143. package/dist/tools/batch-navigate.d.ts +5 -0
  144. package/dist/tools/batch-navigate.d.ts.map +1 -0
  145. package/dist/tools/batch-screenshot.d.ts +5 -0
  146. package/dist/tools/batch-screenshot.d.ts.map +1 -0
  147. package/dist/tools/click.d.ts +3 -0
  148. package/dist/tools/click.d.ts.map +1 -0
  149. package/dist/tools/cookies.d.ts +3 -0
  150. package/dist/tools/cookies.d.ts.map +1 -0
  151. package/dist/tools/cross-viewport-compare.d.ts +4 -0
  152. package/dist/tools/cross-viewport-compare.d.ts.map +1 -0
  153. package/dist/tools/device-boot.d.ts +3 -0
  154. package/dist/tools/device-boot.d.ts.map +1 -0
  155. package/dist/tools/device-list.d.ts +3 -0
  156. package/dist/tools/device-list.d.ts.map +1 -0
  157. package/dist/tools/device-rotate.d.ts +3 -0
  158. package/dist/tools/device-rotate.d.ts.map +1 -0
  159. package/dist/tools/device-shutdown.d.ts +3 -0
  160. package/dist/tools/device-shutdown.d.ts.map +1 -0
  161. package/dist/tools/dismiss-keyboard.d.ts +3 -0
  162. package/dist/tools/dismiss-keyboard.d.ts.map +1 -0
  163. package/dist/tools/index.d.ts +8 -0
  164. package/dist/tools/index.d.ts.map +1 -0
  165. package/dist/tools/inspect.d.ts +3 -0
  166. package/dist/tools/inspect.d.ts.map +1 -0
  167. package/dist/tools/javascript.d.ts +3 -0
  168. package/dist/tools/javascript.d.ts.map +1 -0
  169. package/dist/tools/long-press.d.ts +3 -0
  170. package/dist/tools/long-press.d.ts.map +1 -0
  171. package/dist/tools/navigate.d.ts +3 -0
  172. package/dist/tools/navigate.d.ts.map +1 -0
  173. package/dist/tools/orchestration-tools.d.ts +5 -0
  174. package/dist/tools/orchestration-tools.d.ts.map +1 -0
  175. package/dist/tools/press.d.ts +3 -0
  176. package/dist/tools/press.d.ts.map +1 -0
  177. package/dist/tools/qa-audit.d.ts +3 -0
  178. package/dist/tools/qa-audit.d.ts.map +1 -0
  179. package/dist/tools/qa-detectors.d.ts +3 -0
  180. package/dist/tools/qa-detectors.d.ts.map +1 -0
  181. package/dist/tools/query-dom.d.ts +3 -0
  182. package/dist/tools/query-dom.d.ts.map +1 -0
  183. package/dist/tools/read-page.d.ts +3 -0
  184. package/dist/tools/read-page.d.ts.map +1 -0
  185. package/dist/tools/screenshot.d.ts +3 -0
  186. package/dist/tools/screenshot.d.ts.map +1 -0
  187. package/dist/tools/scroll.d.ts +3 -0
  188. package/dist/tools/scroll.d.ts.map +1 -0
  189. package/dist/tools/select-option.d.ts +3 -0
  190. package/dist/tools/select-option.d.ts.map +1 -0
  191. package/dist/tools/swipe.d.ts +3 -0
  192. package/dist/tools/swipe.d.ts.map +1 -0
  193. package/dist/tools/type.d.ts +3 -0
  194. package/dist/tools/type.d.ts.map +1 -0
  195. package/dist/tools/wait-for.d.ts +3 -0
  196. package/dist/tools/wait-for.d.ts.map +1 -0
  197. package/dist/transports/http.d.ts +57 -0
  198. package/dist/transports/http.d.ts.map +1 -0
  199. package/dist/transports/index.d.ts +38 -0
  200. package/dist/transports/index.d.ts.map +1 -0
  201. package/dist/transports/stdio.d.ts +16 -0
  202. package/dist/transports/stdio.d.ts.map +1 -0
  203. package/dist/types/browser-backend.d.ts +84 -0
  204. package/dist/types/browser-backend.d.ts.map +1 -0
  205. package/dist/types/mcp.d.ts +63 -0
  206. package/dist/types/mcp.d.ts.map +1 -0
  207. package/dist/types/tool-manifest.d.ts +52 -0
  208. package/dist/types/tool-manifest.d.ts.map +1 -0
  209. package/dist/utils/format-age.d.ts +5 -0
  210. package/dist/utils/format-age.d.ts.map +1 -0
  211. package/dist/utils/format-error.d.ts +5 -0
  212. package/dist/utils/format-error.d.ts.map +1 -0
  213. package/dist/utils/logger.d.ts +10 -0
  214. package/dist/utils/logger.d.ts.map +1 -0
  215. package/dist/utils/rate-limiter.d.ts +72 -0
  216. package/dist/utils/rate-limiter.d.ts.map +1 -0
  217. package/dist/utils/request-queue.d.ts +37 -0
  218. package/dist/utils/request-queue.d.ts.map +1 -0
  219. package/dist/utils/schema-validator.d.ts +12 -0
  220. package/dist/utils/schema-validator.d.ts.map +1 -0
  221. package/dist/utils/url-utils.d.ts +5 -0
  222. package/dist/utils/url-utils.d.ts.map +1 -0
  223. package/dist/utils/with-timeout.d.ts +5 -0
  224. package/dist/utils/with-timeout.d.ts.map +1 -0
  225. package/dist/version.d.ts +5 -0
  226. package/dist/version.d.ts.map +1 -0
  227. package/dist/watchdog/event-loop-monitor.d.ts +86 -0
  228. package/dist/watchdog/event-loop-monitor.d.ts.map +1 -0
  229. package/dist/watchdog/simulator-monitor.d.ts +16 -0
  230. package/dist/watchdog/simulator-monitor.d.ts.map +1 -0
  231. package/dist/webkit/client.d.ts +106 -0
  232. package/dist/webkit/client.d.ts.map +1 -0
  233. package/dist/webkit/index.d.ts +3 -0
  234. package/dist/webkit/index.d.ts.map +1 -0
  235. package/package.json +84 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenSafari Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ <h1 align="center">OpenSafari</h1>
2
+
3
+ <p align="center">
4
+ <b>Smart. Fast. Parallel.</b><br>
5
+ iOS Safari automation MCP server via Xcode Simulator.
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT"></a>
10
+ <a href="https://github.com/shaun0927/opensafari"><img src="https://img.shields.io/badge/status-in--development-orange" alt="Status"></a>
11
+ <a href="https://github.com/shaun0927/openchrome"><img src="https://img.shields.io/badge/sibling-OpenChrome-blue" alt="OpenChrome"></a>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ### How OpenSafari compares
17
+
18
+ | | OpenSafari | Playwright WebKit | BrowserStack | Manual Testing |
19
+ |---|:---:|:---:|:---:|:---:|
20
+ | **Engine** | **Real Safari** (Xcode Sim) | Bundled WebKit (approximation) | Real devices (cloud) | Real devices |
21
+ | **Protocol** | **WebKit Remote Debugging** (direct) | Playwright API (wrapper) | Proprietary | N/A |
22
+ | **iOS Fidelity** | **exact** | close but diverges | exact | exact |
23
+ | **Parallel sessions** | **N simulators** | N browsers | limited by plan | 1 device |
24
+ | **Login persistence** | **built-in** (real Safari cookies) | manual | manual | manual |
25
+ | **LLM integration** | **MCP native** | none | none | none |
26
+ | **Cost** | **free** (Xcode) | free | $29+/mo | device cost |
27
+ | **iOS-specific QA** | **auto-detect** (zoom, safe area, keyboard) | none | manual | manual |
28
+
29
+ > **tl;dr** — OpenSafari controls the **real Safari** inside Xcode Simulator via WebKit Remote Debugging Protocol — the same way [OpenChrome](https://github.com/shaun0927/openchrome) controls real Chrome via CDP. No middleware, no bundled browsers. Just direct protocol access to the actual Safari.app.
30
+
31
+ ---
32
+
33
+ ## What is OpenSafari?
34
+
35
+ Imagine testing your e-commerce site on **iPhone 17e, iPhone 17, iPhone 17 Pro Max, and iPad** — all at the same time, already logged in, with an AI agent that automatically finds iOS-specific bugs. That's OpenSafari.
36
+
37
+ ```
38
+ You: Check our checkout flow for mobile issues across all iPhone sizes
39
+
40
+ AI: [4 parallel simulators, all devices simultaneously]
41
+ iPhone 17e: ⚠ Credit card input triggers iOS auto-zoom (font-size: 14px)
42
+ iPhone 17: ✓ Layout OK
43
+ iPhone 17 PM: ⚠ "Place Order" button only 38×32px (below 44px touch target)
44
+ iPad: ⚠ Shipping form hidden behind keyboard when focused
45
+
46
+ Time: 8s | All screenshots captured and analyzed.
47
+ ```
48
+
49
+ | | Manual QA | OpenSafari |
50
+ |---|:---:|:---:|
51
+ | **4-device test** | ~30 min | **~10s** (parallel) |
52
+ | **Login** | Each device, each time | **Never** (persisted) |
53
+ | **iOS bug detection** | Human eye | **Automatic** (LLM vision) |
54
+ | **Consistency** | Varies by tester | **Deterministic** |
55
+
56
+ ---
57
+
58
+ ## Core Architecture
59
+
60
+ OpenSafari follows the same direct-protocol philosophy as OpenChrome:
61
+
62
+ ```
63
+ OpenChrome: CDPClient → Chrome DevTools Protocol → Real Chrome
64
+ OpenSafari: SafariClient → WebKit Remote Debugging Protocol → Real Safari in Simulator
65
+ ```
66
+
67
+ No middleware. No bundled browsers. Direct connection.
68
+
69
+ ```
70
+ Claude Code / AI Agent (MCP Client)
71
+
72
+ │ JSON-RPC (stdio / HTTP)
73
+
74
+ ┌─────────────────────────────────────┐
75
+ │ OpenSafari MCP Server │
76
+ │ │
77
+ │ ┌─────────────┐ ┌──────────────┐ │
78
+ │ │ Simulator │ │ Safari │ │
79
+ │ │ Manager │ │ Client │ │
80
+ │ │ (simctl) │ │ (WebKit │ │
81
+ │ │ │ │ Protocol) │ │
82
+ │ └──────┬──────┘ └──────┬───────┘ │
83
+ │ │ │ │
84
+ │ boot/shutdown navigate/click │
85
+ │ rotate/appear screenshot │
86
+ │ multi-device DOM/JS/cookies │
87
+ │ │ │ │
88
+ │ ┌──────▼────────────────▼───────┐ │
89
+ │ │ Xcode Simulator(s) │ │
90
+ │ │ ┌────────┐ ┌────────┐ │ │
91
+ │ │ │ iPhone │ │ iPhone │ ... │ │
92
+ │ │ │ SE │ │ 16 PM │ │ │
93
+ │ │ │ Safari │ │ Safari │ │ │
94
+ │ │ └────────┘ └────────┘ │ │
95
+ │ └───────────────────────────────┘ │
96
+ │ │
97
+ │ ┌─────────────────────────────┐ │
98
+ │ │ Shared Infrastructure │ │
99
+ │ │ (from OpenChrome) │ │
100
+ │ │ • Security (sanitizer, │ │
101
+ │ │ domain guard, audit) │ │
102
+ │ │ • Watchdog (event loop, │ │
103
+ │ │ disk, health endpoint) │ │
104
+ │ │ • Orchestration (workflow │ │
105
+ │ │ engine, parallel workers)│ │
106
+ │ │ • Session persistence │ │
107
+ │ └─────────────────────────────┘ │
108
+ └─────────────────────────────────────┘
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Key Features
114
+
115
+ ### 1. Real Safari, Real Bugs
116
+
117
+ OpenSafari controls the **actual Safari.app** inside Xcode Simulator via WebKit Remote Debugging Protocol — not a bundled approximation. Every iOS-specific quirk is faithfully reproduced:
118
+
119
+ - **iOS auto-zoom** on inputs with `font-size < 16px`
120
+ - **`position: fixed`** behavior with real software keyboard
121
+ - **`100vh`** viewport height inconsistencies with address bar
122
+ - **Safe area insets** (notch, home indicator) on real device frames
123
+ - **`color-scheme`** dark mode forced rendering
124
+ - **Touch target** minimum size requirements (44×44px)
125
+
126
+ ### 2. Parallel Multi-Device Testing
127
+
128
+ Test across multiple devices simultaneously with a single command:
129
+
130
+ ```
131
+ opensafari serve --devices "iphone-17e,iphone-17,iphone-17-pro-max,ipad-pro"
132
+
133
+ # 4 simulators boot in parallel
134
+ # Each gets its own Safari instance + WebKit Protocol connection
135
+ # All share login state via cookie injection
136
+ ```
137
+
138
+ ### 3. Persistent Login Sessions
139
+
140
+ Log in once, test forever. Cookies and localStorage are extracted directly from the real Safari session:
141
+
142
+ ```
143
+ # First time: login captured from real Safari
144
+ opensafari auth save --site myapp.com
145
+ → Exports cookies via WebKit Protocol Network.getAllCookies
146
+ → Saves to ~/.opensafari/auth/myapp.json
147
+
148
+ # Every subsequent run: auto-restored
149
+ opensafari serve
150
+ → Injects cookies via Network.setCookie into each simulator's Safari
151
+ → All simulators start already logged in
152
+ ```
153
+
154
+ ### 4. iOS-Specific Auto-Detection
155
+
156
+ Built-in QA checks that run on real Safari — no approximation:
157
+
158
+ | Check | What It Detects |
159
+ |-------|----------------|
160
+ | **Auto-Zoom Guard** | `<input>` elements with font-size < 16px |
161
+ | **Safe Area Validator** | Content hidden behind notch or home indicator |
162
+ | **Keyboard Overlap** | Fixed elements covered by real software keyboard |
163
+ | **Touch Target Audit** | Clickable elements smaller than 44×44px |
164
+ | **Dark Mode Diff** | Visual differences via `simctl ui appearance` toggle |
165
+ | **Viewport Compare** | Layout breaks across different screen sizes |
166
+ | **Scroll Lock Check** | Body scroll not restored after modal close |
167
+
168
+ ### 5. MCP Native
169
+
170
+ Works with any MCP client — Claude Code, Cursor, VS Code, or custom agents:
171
+
172
+ ```jsonc
173
+ // ~/.claude.json
174
+ {
175
+ "mcpServers": {
176
+ "opensafari": {
177
+ "command": "opensafari",
178
+ "args": ["serve"]
179
+ }
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### 6. Shared DNA with OpenChrome
185
+
186
+ OpenSafari shares battle-tested infrastructure with [OpenChrome](https://github.com/shaun0927/openchrome):
187
+
188
+ | Module | Source | Status |
189
+ |--------|--------|--------|
190
+ | MCP Server core | OpenChrome | Shared |
191
+ | Transport (stdio/HTTP) | OpenChrome | Shared |
192
+ | Security (sanitizer, guard, audit) | OpenChrome | Shared |
193
+ | Watchdog (event loop, disk, health) | OpenChrome | Shared |
194
+ | Orchestration (workflow engine) | OpenChrome | Adapted |
195
+ | Simulator Manager | **NEW** | OpenSafari |
196
+ | Safari Client (WebKit Protocol) | **NEW** | OpenSafari |
197
+ | iOS QA Engine | **NEW** | OpenSafari |
198
+
199
+ ---
200
+
201
+ ## Tools (Planned)
202
+
203
+ ### Core Tools (Phase 1)
204
+
205
+ | Tool | Description |
206
+ |------|-------------|
207
+ | `navigate` | Open URL in real Safari |
208
+ | `computer` | Touch, scroll, type — coordinate-based interaction |
209
+ | `screenshot` | Capture real Safari screen via WebKit Protocol or simctl |
210
+ | `read_page` | Extract visible text content |
211
+ | `query_dom` | CSS selector queries with element details |
212
+ | `javascript` | Execute JavaScript in page context via `Runtime.evaluate` |
213
+ | `inspect` | Element CSS, accessibility, and layout inspection |
214
+ | `cookies` | Get/set/clear real Safari cookies via `Network` domain |
215
+
216
+ ### Device Management (Phase 1)
217
+
218
+ | Tool | Description |
219
+ |------|-------------|
220
+ | `device_list` | List available simulator device types |
221
+ | `device_boot` | Boot a specific device (iPhone SE, 16, iPad, etc.) |
222
+ | `device_shutdown` | Shutdown simulator |
223
+ | `device_snapshot` | Save/restore simulator state (including login) |
224
+ | `device_rotate` | Toggle portrait/landscape |
225
+ | `appearance_toggle` | Switch light/dark mode via `simctl ui` |
226
+
227
+ ### Parallel & Orchestration (Phase 2)
228
+
229
+ | Tool | Description |
230
+ |------|-------------|
231
+ | `batch_screenshot` | Capture same URL across all active devices |
232
+ | `batch_execute` | Run JS across all simulators in parallel |
233
+ | `batch_navigate` | Open same URL on all devices simultaneously |
234
+ | `cross_viewport_compare` | Side-by-side visual comparison across devices |
235
+
236
+ ### iOS QA Engine (Phase 3)
237
+
238
+ | Tool | Description |
239
+ |------|-------------|
240
+ | `qa_auto_zoom` | Detect inputs triggering iOS auto-zoom |
241
+ | `qa_touch_targets` | Find elements below 44×44px minimum |
242
+ | `qa_safe_area` | Check content behind notch/home indicator |
243
+ | `qa_keyboard_overlap` | Detect fixed elements hidden by real keyboard |
244
+ | `qa_dark_mode` | Compare light vs dark mode rendering |
245
+ | `qa_full_audit` | Run all QA checks and generate report |
246
+
247
+ ---
248
+
249
+ ## Quick Start (Coming Soon)
250
+
251
+ ```bash
252
+ # Prerequisites: macOS + Xcode (with iOS Simulator)
253
+
254
+ # Install
255
+ npm install -g opensafari-mcp
256
+
257
+ # Run
258
+ opensafari serve
259
+
260
+ # With specific devices
261
+ opensafari serve --devices "iphone-17e,iphone-17-pro-max"
262
+
263
+ # With auth state
264
+ opensafari serve --auth ~/.opensafari/auth/mysite.json
265
+
266
+ # Health check
267
+ opensafari doctor
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Requirements
273
+
274
+ - **macOS** (Xcode Simulator is macOS only)
275
+ - **Xcode** with iOS Simulator runtime installed
276
+ - **Node.js** >= 18
277
+ - **ios-webkit-debug-proxy** — `brew install ios-webkit-debug-proxy`
278
+
279
+ ---
280
+
281
+ ## WebInspector Proxy Configuration
282
+
283
+ OpenSafari uses `ios_webkit_debug_proxy` to bridge WebKit Remote Debugging from Xcode Simulator. The proxy is **auto-started** by the `device_boot` tool — no manual setup is needed in most cases.
284
+
285
+ ### Default Ports
286
+
287
+ | Port | Purpose |
288
+ |------|---------|
289
+ | **9321** | Device list (HTML) — serves the proxy's device listing page |
290
+ | **9322** | Device connection (JSON) — WebKit debugging targets for connected simulators |
291
+
292
+ Port 9322 is deliberately offset from Chrome DevTools (9222) so OpenSafari and [OpenChrome](https://github.com/shaun0927/openchrome) can run simultaneously.
293
+
294
+ ### Custom Port
295
+
296
+ Set the `OPENSAFARI_PROXY_PORT` environment variable to use a different device port:
297
+
298
+ ```bash
299
+ # Use port 9500 instead of the default 9322
300
+ OPENSAFARI_PROXY_PORT=9500 opensafari serve
301
+ ```
302
+
303
+ Port resolution order:
304
+ 1. Explicit `port` option (programmatic use)
305
+ 2. `OPENSAFARI_PROXY_PORT` environment variable
306
+ 3. Default: **9322**
307
+
308
+ ### Multi-Session Usage
309
+
310
+ Multiple Claude Code sessions can share the same proxy. When a session detects a healthy proxy already running on its target port, it reuses it instead of starting a new one. When the owning session exits, only its own proxy is terminated — other sessions' proxies remain unaffected.
311
+
312
+ ---
313
+
314
+ ## Relationship to OpenChrome
315
+
316
+ OpenSafari is the **Safari/iOS counterpart** to [OpenChrome](https://github.com/shaun0927/openchrome). Same philosophy, same architecture — different browser.
317
+
318
+ | | OpenChrome | OpenSafari |
319
+ |---|---|---|
320
+ | **Browser** | Real Chrome | Real Safari (in Simulator) |
321
+ | **Protocol** | Chrome DevTools Protocol (CDP) | WebKit Remote Debugging Protocol |
322
+ | **Client** | CDPClient (puppeteer-core) | SafariClient (WebKit Protocol) |
323
+ | **Execution** | `chrome --remote-debugging-port` | `xcrun simctl` + WebKit debug socket |
324
+ | **Use Case** | Desktop web automation | Mobile web QA & debugging |
325
+ | **Parallel** | N tabs in 1 Chrome | N simulators, each with Safari |
326
+ | **Login** | Real Chrome sessions | Real Safari sessions |
327
+
328
+ Together, they provide **complete browser coverage** — Chrome for desktop, Safari for iOS — both controlled by AI agents through MCP with direct protocol connections. No middleware, no bundled browsers.
329
+
330
+ ---
331
+
332
+ ## License
333
+
334
+ MIT
335
+
336
+ ---
337
+
338
+ <p align="center">
339
+ <b>Built for developers who ship mobile-first products.</b><br>
340
+ <sub>By the creators of <a href="https://github.com/shaun0927/openchrome">OpenChrome</a></sub>
341
+ </p>
@@ -0,0 +1,2 @@
1
+ "use strict";exports.id=115,exports.ids=[115],exports.modules={115(s,e,t){function r(s){const e=[`## ${s.score>=90?"V":s.score>=70?"!":"X"} iOS QA Audit Report`,"",`**Score: ${s.score}/100** | Device: ${s.device} (${s.viewport.w}x${s.viewport.h}) | ${s.timestamp}`,`**URL:** ${s.url} | Duration: ${s.duration}ms`,"","| Severity | Count |","|----------|-------|",`| Critical | ${s.summary.critical} |`,`| High | ${s.summary.high} |`,`| Medium | ${s.summary.medium} |`,`| Low | ${s.summary.low} |`,`| Passed | ${s.summary.passed}/13 detectors |`],t=s.detectors.filter(s=>!s.passed);if(t.length>0){e.push("","### Issues Found","");for(const s of t){e.push(`#### [${s.severity.toUpperCase()}] ${s.detector} (${s.issueCount} issues)`);for(const t of s.issues.slice(0,5))e.push(`- \`${t.selector}\`: ${t.problem}`),e.push(` - **Fix:** ${t.fix}`);s.issues.length>5&&e.push(`- ... and ${s.issues.length-5} more`),e.push("")}}return e.join("\n")}t.d(e,{generateAuditMarkdown:()=>r})}};
2
+ //# sourceMappingURL=115.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"115.index.js","mappings":"0EAEO,SAASA,EAAsBC,GACpC,MACMC,EAAQ,CACZ,MAFYD,EAAOE,OAAS,GAAK,IAAMF,EAAOE,OAAS,GAAK,IAAM,0BAGlE,GACA,YAAYF,EAAOE,yBAAyBF,EAAOG,WAAWH,EAAOI,SAASC,KAAKL,EAAOI,SAASE,QAAQN,EAAOO,YAClH,YAAYP,EAAOQ,mBAAmBR,EAAOS,aAC7C,GACA,uBACA,uBACA,gBAAgBT,EAAOU,QAAQC,aAC/B,YAAYX,EAAOU,QAAQE,SAC3B,cAAcZ,EAAOU,QAAQG,WAC7B,WAAWb,EAAOU,QAAQI,QAC1B,cAAcd,EAAOU,QAAQK,yBAGzBC,EAAShB,EAAOiB,UAAUC,OAAOC,IAAMA,EAAEJ,QAC/C,GAAIC,EAAOI,OAAS,EAAG,CACrBnB,EAAMoB,KAAK,GAAI,mBAAoB,IACnC,IAAK,MAAMC,KAAON,EAAQ,CACxBf,EAAMoB,KAAK,SAASC,EAAIC,SAASC,kBAAkBF,EAAIG,aAAaH,EAAII,sBACxE,IAAK,MAAMC,KAASL,EAAIM,OAAOC,MAAM,EAAG,GACtC5B,EAAMoB,KAAK,OAAOM,EAAMG,eAAeH,EAAMI,WAC7C9B,EAAMoB,KAAK,gBAAgBM,EAAMK,OAE/BV,EAAIM,OAAOR,OAAS,GAAGnB,EAAMoB,KAAK,aAAaC,EAAIM,OAAOR,OAAS,UACvEnB,EAAMoB,KAAK,GACb,CACF,CAEA,OAAOpB,EAAMgC,KAAK,KACpB,C","sources":["webpack://opensafari-mcp/./src/qa/report-markdown.ts"],"sourcesContent":["import { AuditReport } from './audit';\n\nexport function generateAuditMarkdown(report: AuditReport): string {\n const emoji = report.score >= 90 ? 'V' : report.score >= 70 ? '!' : 'X';\n const lines = [\n `## ${emoji} iOS QA Audit Report`,\n '',\n `**Score: ${report.score}/100** | Device: ${report.device} (${report.viewport.w}x${report.viewport.h}) | ${report.timestamp}`,\n `**URL:** ${report.url} | Duration: ${report.duration}ms`,\n '',\n '| Severity | Count |',\n '|----------|-------|',\n `| Critical | ${report.summary.critical} |`,\n `| High | ${report.summary.high} |`,\n `| Medium | ${report.summary.medium} |`,\n `| Low | ${report.summary.low} |`,\n `| Passed | ${report.summary.passed}/13 detectors |`,\n ];\n\n const failed = report.detectors.filter(d => !d.passed);\n if (failed.length > 0) {\n lines.push('', '### Issues Found', '');\n for (const det of failed) {\n lines.push(`#### [${det.severity.toUpperCase()}] ${det.detector} (${det.issueCount} issues)`);\n for (const issue of det.issues.slice(0, 5)) {\n lines.push(`- \\`${issue.selector}\\`: ${issue.problem}`);\n lines.push(` - **Fix:** ${issue.fix}`);\n }\n if (det.issues.length > 5) lines.push(`- ... and ${det.issues.length - 5} more`);\n lines.push('');\n }\n }\n\n return lines.join('\\n');\n}\n"],"names":["generateAuditMarkdown","report","lines","score","device","viewport","w","h","timestamp","url","duration","summary","critical","high","medium","low","passed","failed","detectors","filter","d","length","push","det","severity","toUpperCase","detector","issueCount","issue","issues","slice","selector","problem","fix","join"],"sourceRoot":""}
@@ -0,0 +1,2 @@
1
+ "use strict";exports.id=67,exports.ids=[67],exports.modules={448(e,t,r){r.d(t,{QAHistory:()=>o});var i=r(943),s=r(928),n=r(857);class o{reportsDir;constructor(e){this.reportsDir=e??s.join(n.homedir(),".opensafari","reports")}async save(e){const t=s.join(this.reportsDir,this.sanitizeSite(e.url));await i.mkdir(t,{recursive:!0});const r=(new Date).toISOString().replace(/[:.]/g,"-")+".json",n=s.join(t,r);return await i.writeFile(n,JSON.stringify(e,null,2)),await this.rotate(t,30),n}async getLatest(e){const t=await this.listReportFiles(e);return 0===t.length?null:JSON.parse(await i.readFile(t[t.length-1],"utf-8"))}async getPrevious(e){const t=await this.listReportFiles(e);return t.length<2?null:JSON.parse(await i.readFile(t[t.length-2],"utf-8"))}async detectRegressions(e,t){const r=this.buildFingerprints(e),i=this.buildFingerprints(t),s=[...r.values()].filter(e=>!i.has(e.fingerprint)),n=[...i.values()].filter(e=>!r.has(e.fingerprint)),o=[...r.values()].filter(e=>i.has(e.fingerprint)),a=e.score-t.score,c=a>0?"improved":a<0?"regressed":"unchanged";return{currentScore:e.score,previousScore:t.score,scoreDelta:a,newIssues:s,fixedIssues:n,recurringIssues:o,summary:`Score ${c} ${t.score} -> ${e.score} (${a>=0?"+":""}${a}). ${n.length} fixed, ${s.length} new, ${o.length} recurring.`}}getExitCode(e,t){const r={failOnCritical:!0,failOnHigh:!1,...t};return r.failOnCritical&&e.summary.critical>0||r.failOnHigh&&e.summary.high>0||r.minScore&&e.score<r.minScore?1:0}buildFingerprints(e){const t=new Map;for(const r of e.detectors)for(const e of r.issues){const i=this.fingerprint(r.detector,e.selector);t.set(i,{detector:r.detector,selector:e.selector,problem:e.problem,fingerprint:i})}return t}fingerprint(e,t){const r=`${e}::${t}`;let i=0;for(let e=0;e<r.length;e++)i=(i<<5)-i+r.charCodeAt(e),i|=0;return i.toString(36)}sanitizeSite(e){try{return new URL(e).hostname.replace(/[^a-zA-Z0-9.-]/g,"_")}catch{return e.replace(/[^a-zA-Z0-9.-]/g,"_")}}async listReportFiles(e){const t=s.join(this.reportsDir,this.sanitizeSite(e));try{return(await i.readdir(t)).filter(e=>e.endsWith(".json")).sort().map(e=>s.join(t,e))}catch{return[]}}async rotate(e,t){try{const r=(await i.readdir(e)).filter(e=>e.endsWith(".json")).sort();if(r.length>t)for(const n of r.slice(0,r.length-t))await i.unlink(s.join(e,n)).catch(()=>{})}catch{}}}}};
2
+ //# sourceMappingURL=67.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"67.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/history.ts"],"sourcesContent":["import * as fs from 'fs/promises';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { AuditReport } from './audit';\n\nexport interface RegressionReport {\n currentScore: number;\n previousScore: number;\n scoreDelta: number;\n newIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n fixedIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n recurringIssues: Array<{ detector: string; selector: string; problem: string; fingerprint: string }>;\n summary: string;\n}\n\nexport class QAHistory {\n private reportsDir: string;\n\n constructor(reportsDir?: string) {\n this.reportsDir = reportsDir ?? path.join(os.homedir(), '.opensafari', 'reports');\n }\n\n async save(report: AuditReport): Promise<string> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(report.url));\n await fs.mkdir(siteDir, { recursive: true });\n const filename = new Date().toISOString().replace(/[:.]/g, '-') + '.json';\n const filePath = path.join(siteDir, filename);\n await fs.writeFile(filePath, JSON.stringify(report, null, 2));\n await this.rotate(siteDir, 30);\n return filePath;\n }\n\n async getLatest(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length === 0) return null;\n return JSON.parse(await fs.readFile(files[files.length - 1], 'utf-8'));\n }\n\n async getPrevious(url: string): Promise<AuditReport | null> {\n const files = await this.listReportFiles(url);\n if (files.length < 2) return null;\n return JSON.parse(await fs.readFile(files[files.length - 2], 'utf-8'));\n }\n\n async detectRegressions(current: AuditReport, previous: AuditReport): Promise<RegressionReport> {\n const currentFP = this.buildFingerprints(current);\n const previousFP = this.buildFingerprints(previous);\n\n const newIssues = [...currentFP.values()].filter(i => !previousFP.has(i.fingerprint));\n const fixedIssues = [...previousFP.values()].filter(i => !currentFP.has(i.fingerprint));\n const recurringIssues = [...currentFP.values()].filter(i => previousFP.has(i.fingerprint));\n\n const scoreDelta = current.score - previous.score;\n const trend = scoreDelta > 0 ? 'improved' : scoreDelta < 0 ? 'regressed' : 'unchanged';\n\n return {\n currentScore: current.score,\n previousScore: previous.score,\n scoreDelta,\n newIssues,\n fixedIssues,\n recurringIssues,\n summary: `Score ${trend} ${previous.score} -> ${current.score} (${scoreDelta >= 0 ? '+' : ''}${scoreDelta}). ${fixedIssues.length} fixed, ${newIssues.length} new, ${recurringIssues.length} recurring.`,\n };\n }\n\n getExitCode(report: AuditReport, options?: { failOnCritical?: boolean; failOnHigh?: boolean; minScore?: number }): number {\n const opts = { failOnCritical: true, failOnHigh: false, ...options };\n if (opts.failOnCritical && report.summary.critical > 0) return 1;\n if (opts.failOnHigh && report.summary.high > 0) return 1;\n if (opts.minScore && report.score < opts.minScore) return 1;\n return 0;\n }\n\n private buildFingerprints(report: AuditReport): Map<string, { detector: string; selector: string; problem: string; fingerprint: string }> {\n const map = new Map<string, { detector: string; selector: string; problem: string; fingerprint: string }>();\n for (const det of report.detectors) {\n for (const issue of det.issues) {\n const fp = this.fingerprint(det.detector, issue.selector);\n map.set(fp, { detector: det.detector, selector: issue.selector, problem: issue.problem, fingerprint: fp });\n }\n }\n return map;\n }\n\n private fingerprint(detector: string, selector: string): string {\n const input = `${detector}::${selector}`;\n let hash = 0;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) - hash) + input.charCodeAt(i);\n hash |= 0;\n }\n return hash.toString(36);\n }\n\n private sanitizeSite(url: string): string {\n try { return new URL(url).hostname.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n catch { return url.replace(/[^a-zA-Z0-9.-]/g, '_'); }\n }\n\n private async listReportFiles(url: string): Promise<string[]> {\n const siteDir = path.join(this.reportsDir, this.sanitizeSite(url));\n try {\n const files = await fs.readdir(siteDir);\n return files.filter(f => f.endsWith('.json')).sort().map(f => path.join(siteDir, f));\n } catch { return []; }\n }\n\n private async rotate(dir: string, maxReports: number): Promise<void> {\n try {\n const files = await fs.readdir(dir);\n const sorted = files.filter(f => f.endsWith('.json')).sort();\n if (sorted.length > maxReports) {\n for (const f of sorted.slice(0, sorted.length - maxReports)) {\n await fs.unlink(path.join(dir, f)).catch(() => {});\n }\n }\n } catch { /* */ }\n }\n}\n"],"names":["QAHistory","reportsDir","constructor","this","save","report","siteDir","sanitizeSite","url","recursive","filename","Date","toISOString","replace","filePath","JSON","stringify","rotate","getLatest","files","listReportFiles","length","parse","getPrevious","detectRegressions","current","previous","currentFP","buildFingerprints","previousFP","newIssues","values","filter","i","has","fingerprint","fixedIssues","recurringIssues","scoreDelta","score","trend","currentScore","previousScore","summary","getExitCode","options","opts","failOnCritical","failOnHigh","critical","high","minScore","map","Map","det","detectors","issue","issues","fp","detector","selector","set","problem","input","hash","charCodeAt","toString","URL","hostname","f","endsWith","sort","dir","maxReports","sorted","slice","catch"],"sourceRoot":""}
@@ -0,0 +1,2 @@
1
+ "use strict";exports.id=679,exports.ids=[679],exports.modules={679(e,s,t){t.d(s,{HTTPTransport:()=>i});var n=t(67),r=t(598),o=t(462);class i{server=null;messageHandler=null;port;sessions=new Set;sseConnections=[];sessionDeleteHandler=null;constructor(e){this.port=e}onSessionDelete(e){this.sessionDeleteHandler=e}onMessage(e){this.messageHandler=e}send(e){for(const s of this.sseConnections)try{s.res.write(`data: ${JSON.stringify(e)}\n\n`)}catch{}}start(){this.server=n.createServer((e,s)=>{this.handleHTTPRequest(e,s)}),this.server.listen(this.port,()=>{console.error(`[HTTPTransport] Listening on port ${this.port}`),console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`)}),this.server.on("error",e=>{console.error("[HTTPTransport] Server error:",e)})}async close(){for(const e of this.sseConnections)try{e.res.end()}catch{}return this.sseConnections=[],new Promise(e=>{this.server?this.server.close(()=>{this.server=null,e()}):e()})}handleHTTPRequest(e,s){const t=new URL(e.url||"/",`http://localhost:${this.port}`).pathname;if(s.setHeader("Access-Control-Allow-Origin","*"),s.setHeader("Access-Control-Allow-Methods","GET, POST, DELETE, OPTIONS"),s.setHeader("Access-Control-Allow-Headers","Content-Type, Mcp-Session-Id"),s.setHeader("Access-Control-Expose-Headers","Mcp-Session-Id"),"OPTIONS"===e.method)return s.writeHead(204),void s.end();if("/health"!==t){if("/mcp"===t)switch(e.method){case"POST":return void this.handlePost(e,s);case"GET":return void this.handleSSE(e,s);case"DELETE":return void this.handleDelete(e,s);default:return s.writeHead(405,{"Content-Type":"application/json"}),void s.end(JSON.stringify({error:"Method not allowed"}))}s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Not found"}))}else this.handleHealth(s)}handleHealth(e){e.writeHead(200,{"Content-Type":"application/json"}),e.end(JSON.stringify({status:"ok",transport:"http",activeSessions:this.sessions.size,sseConnections:this.sseConnections.length}))}handlePost(e,s){const t=[];let n=0;e.on("data",r=>{if(n+=r.length,n>10485760)return s.writeHead(413,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Request body too large"}})),void e.destroy();t.push(r)}),e.on("end",async()=>{const n=Buffer.concat(t).toString("utf-8");if(!n.trim())return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Empty request body"}}));let i;try{i=JSON.parse(n)}catch(e){return s.writeHead(400,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:e instanceof Error?e.message:"Parse error"}}))}let a=e.headers["mcp-session-id"];if(!this.messageHandler)return s.writeHead(500,{"Content-Type":"application/json"}),void s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.INTERNAL_ERROR,message:"No message handler registered"}}));if(Array.isArray(i)){const e=(await this.processBatch(i,a)).filter(e=>null!==e);return a&&s.setHeader("Mcp-Session-Id",a),void(0===e.length?(s.writeHead(202),s.end()):1===e.length?(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e[0]))):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e))))}const d=i;"initialize"!==d.method||a||(a=r.randomUUID(),this.sessions.add(a));try{const e=await this.messageHandler(d);a&&s.setHeader("Mcp-Session-Id",a),null===e?(s.writeHead(202),s.end()):(s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify(e)))}catch(e){const t=d.id??0;s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:t,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}))}}),e.on("error",e=>{console.error("[HTTPTransport] Request read error:",e),s.headersSent||(s.writeHead(400,{"Content-Type":"application/json"}),s.end(JSON.stringify({jsonrpc:"2.0",id:0,error:{code:o.D.PARSE_ERROR,message:"Request read error"}})))})}handleSSE(e,s){const t=e.headers["mcp-session-id"]||"anonymous";s.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache",Connection:"keep-alive"}),s.write(": keepalive\n\n");const n={res:s,sessionId:t};this.sseConnections.push(n),e.on("close",()=>{const e=this.sseConnections.indexOf(n);-1!==e&&this.sseConnections.splice(e,1),console.error(`[HTTPTransport] SSE client disconnected (session: ${t})`)})}handleDelete(e,s){const t=e.headers["mcp-session-id"];t&&this.sessions.has(t)?(this.sessions.delete(t),this.sessionDeleteHandler&&this.sessionDeleteHandler(t),this.sseConnections=this.sseConnections.filter(e=>{if(e.sessionId===t){try{e.res.end()}catch{}return!1}return!0}),s.writeHead(200,{"Content-Type":"application/json"}),s.end(JSON.stringify({status:"session terminated"}))):(s.writeHead(404,{"Content-Type":"application/json"}),s.end(JSON.stringify({error:"Session not found"})))}async processBatch(e,s){const t=this.messageHandler;s||e.some(e=>"object"==typeof e&&null!==e&&"initialize"===e.method)&&(s=r.randomUUID(),this.sessions.add(s));const n=e.map(async e=>{if("object"!=typeof e||null===e)return{jsonrpc:"2.0",id:0,error:{code:o.D.INVALID_REQUEST,message:"Invalid batch element: not an object"}};const s=e;try{return await t(s)}catch(e){return{jsonrpc:"2.0",id:s.id??0,error:{code:o.D.INTERNAL_ERROR,message:e instanceof Error?e.message:"Internal error"}}}});return Promise.all(n)}}}};
2
+ //# sourceMappingURL=679.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"679.index.js","mappings":"qIA0BO,MAAMA,EACHC,OAA6B,KAC7BC,eAAyF,KACzFC,KACAC,SAAwB,IAAIC,IAC5BC,eAAkC,GAClCC,qBAA6D,KAErE,WAAAC,CAAYL,GACVM,KAAKN,KAAOA,CACd,CAMA,eAAAO,CAAgBC,GACdF,KAAKF,qBAAuBI,CAC9B,CAEA,SAAAC,CAAUD,GACRF,KAAKP,eAAiBS,CACxB,CAMA,IAAAE,CAAKC,GAEH,IAAK,MAAMC,KAAQN,KAAKH,eACtB,IACES,EAAKC,IAAIC,MAAM,SAASC,KAAKC,UAAUL,SACzC,CAAE,MAEF,CAEJ,CAEA,KAAAM,GACEX,KAAKR,OAAS,eAAkB,CAACoB,EAAKL,KACpCP,KAAKa,kBAAkBD,EAAKL,KAG9BP,KAAKR,OAAOsB,OAAOd,KAAKN,KAAM,KAC5BqB,QAAQC,MAAM,qCAAqChB,KAAKN,QACxDqB,QAAQC,MAAM,kDAAkDhB,KAAKN,cAGvEM,KAAKR,OAAOyB,GAAG,QAAUC,IACvBH,QAAQC,MAAM,gCAAiCE,IAEnD,CAEA,WAAMC,GAEJ,IAAK,MAAMb,KAAQN,KAAKH,eACtB,IACES,EAAKC,IAAIa,KACX,CAAE,MAEF,CAIF,OAFApB,KAAKH,eAAiB,GAEf,IAAIwB,QAASC,IACdtB,KAAKR,OACPQ,KAAKR,OAAO2B,MAAM,KAChBnB,KAAKR,OAAS,KACd8B,MAGFA,KAGN,CAEQ,iBAAAT,CAAkBD,EAA2BL,GACnD,MACMgB,EADM,IAAIC,IAAIZ,EAAIa,KAAO,IAAK,oBAAoBzB,KAAKN,QACxC6B,SASrB,GANAhB,EAAImB,UAAU,8BAA+B,KAC7CnB,EAAImB,UAAU,+BAAgC,8BAC9CnB,EAAImB,UAAU,+BAAgC,gCAC9CnB,EAAImB,UAAU,gCAAiC,kBAG5B,YAAfd,EAAIe,OAGN,OAFApB,EAAIqB,UAAU,UACdrB,EAAIa,MAIN,GAAiB,YAAbG,EAAJ,CAKA,GAAiB,SAAbA,EACF,OAAQX,EAAIe,QACV,IAAK,OAEH,YADA3B,KAAK6B,WAAWjB,EAAKL,GAEvB,IAAK,MAEH,YADAP,KAAK8B,UAAUlB,EAAKL,GAEtB,IAAK,SAEH,YADAP,KAAK+B,aAAanB,EAAKL,GAEzB,QAGE,OAFAA,EAAIqB,UAAU,IAAK,CAAE,eAAgB,0BACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CAAEM,MAAO,wBAMtCT,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CAAEM,MAAO,cAtBhC,MAFEhB,KAAKgC,aAAazB,EAyBtB,CAKQ,YAAAyB,CAAazB,GACnBA,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrBuB,OAAQ,KACRC,UAAW,OACXC,eAAgBnC,KAAKL,SAASyC,KAC9BvC,eAAgBG,KAAKH,eAAewC,SAExC,CAKQ,UAAAR,CAAWjB,EAA2BL,GAC5C,MAAM+B,EAAmB,GACzB,IAAIC,EAAY,EAEhB3B,EAAIK,GAAG,OAASuB,IAEd,GADAD,GAAaC,EAAMH,OACfE,EAxJa,SAgKf,OAPAhC,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CAAE2B,KAAM,IAAcC,gBAAiBC,QAAS,kCAEzDjC,EAAIkC,UAGNR,EAAOS,KAAKP,KAGd5B,EAAIK,GAAG,MAAO+B,UACZ,MAAMC,EAAOC,OAAOC,OAAOb,GAAQc,SAAS,SAE5C,IAAKH,EAAKI,OAOR,OANA9C,EAAIqB,UAAU,IAAK,CAAE,eAAgB,0BACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CAAE2B,KAAM,IAAcW,YAAaT,QAAS,yBAKvD,IAAIU,EACJ,IACEA,EAAS9C,KAAK+C,MAAMP,EACtB,CAAE,MAAOjC,GAUP,OATAT,EAAIqB,UAAU,IAAK,CAAE,eAAgB,0BACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CACL2B,KAAM,IAAcW,YACpBT,QAAS7B,aAAiByC,MAAQzC,EAAM6B,QAAU,iBAIxD,CAGA,IAAIa,EAAY9C,EAAI+C,QAAQ,kBAE5B,IAAK3D,KAAKP,eAOR,OANAc,EAAIqB,UAAU,IAAK,CAAE,eAAgB,0BACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CAAE2B,KAAM,IAAciB,eAAgBf,QAAS,oCAM1D,GAAIgB,MAAMC,QAAQP,GAAS,CACzB,MAEMQ,SAFgB/D,KAAKgE,aAAaT,EAAQG,IAEtBO,OAAQC,GAA8B,OAANA,GAiB1D,OAfIR,GACFnD,EAAImB,UAAU,iBAAkBgC,QAGT,IAArBK,EAAU1B,QAEZ9B,EAAIqB,UAAU,KACdrB,EAAIa,OAC0B,IAArB2C,EAAU1B,QACnB9B,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAUqD,EAAU,OAEjCxD,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAUqD,KAG3B,CAGA,MAAMI,EAAMZ,EAGO,eAAfY,EAAIxC,QAA4B+B,IAClCA,EAAY,eACZ1D,KAAKL,SAASyE,IAAIV,IAGpB,IACE,MAAMrD,QAAiBL,KAAKP,eAAe0E,GAEvCT,GACFnD,EAAImB,UAAU,iBAAkBgC,GAGjB,OAAbrD,GAEFE,EAAIqB,UAAU,KACdrB,EAAIa,QAEJb,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAUL,IAE3B,CAAE,MAAOW,GACP,MAAM0B,EAAMyB,EAAIzB,IAA0B,EAC1CnC,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,KACA1B,MAAO,CACL2B,KAAM,IAAciB,eACpBf,QAAS7B,aAAiByC,MAAQzC,EAAM6B,QAAU,oBAGxD,IAGFjC,EAAIK,GAAG,QAAUC,IACfH,QAAQC,MAAM,sCAAuCE,GAChDX,EAAI8D,cACP9D,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CACrB+B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CAAE2B,KAAM,IAAcW,YAAaT,QAAS,2BAI3D,CAKQ,SAAAf,CAAUlB,EAA2BL,GAC3C,MAAMmD,EAAY9C,EAAI+C,QAAQ,mBAA+B,YAE7DpD,EAAIqB,UAAU,IAAK,CACjB,eAAgB,oBAChB,gBAAiB,WACjB,WAAc,eAIhBrB,EAAIC,MAAM,mBAEV,MAAMF,EAAsB,CAAEC,MAAKmD,aACnC1D,KAAKH,eAAekD,KAAKzC,GAGzBM,EAAIK,GAAG,QAAS,KACd,MAAMqD,EAAMtE,KAAKH,eAAe0E,QAAQjE,IAC3B,IAATgE,GACFtE,KAAKH,eAAe2E,OAAOF,EAAK,GAElCvD,QAAQC,MAAM,qDAAqD0C,OAEvE,CAKQ,YAAA3B,CAAanB,EAA2BL,GAC9C,MAAMmD,EAAY9C,EAAI+C,QAAQ,kBAE1BD,GAAa1D,KAAKL,SAAS8E,IAAIf,IACjC1D,KAAKL,SAAS+E,OAAOhB,GAGjB1D,KAAKF,sBACPE,KAAKF,qBAAqB4D,GAI5B1D,KAAKH,eAAiBG,KAAKH,eAAeoE,OAAQ3D,IAChD,GAAIA,EAAKoD,YAAcA,EAAW,CAChC,IACEpD,EAAKC,IAAIa,KACX,CAAE,MAEF,CACA,OAAO,CACT,CACA,OAAO,IAGTb,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CAAEuB,OAAQ,0BAEjC1B,EAAIqB,UAAU,IAAK,CAAE,eAAgB,qBACrCrB,EAAIa,IAAIX,KAAKC,UAAU,CAAEM,MAAO,uBAEpC,CAKQ,kBAAMgD,CACZW,EACAjB,GAEA,MAAMxD,EAAUF,KAAKP,eAIhBiE,GACmBiB,EAASC,KAC5BT,GAAuB,iBAARA,GAA4B,OAARA,GAA4D,eAA3CA,EAAgCxC,UAGrF+B,EAAY,eACZ1D,KAAKL,SAASyE,IAAIV,IAItB,MAAMmB,EAAWF,EAASG,IAAI9B,MAAOmB,IACnC,GAAmB,iBAARA,GAA4B,OAARA,EAC7B,MAAO,CACL1B,QAAS,MACTC,GAAI,EACJ1B,MAAO,CACL2B,KAAM,IAAcC,gBACpBC,QAAS,yCAKf,MAAMkC,EAASZ,EAEf,IACE,aAAajE,EAAQ6E,EACvB,CAAE,MAAO/D,GAEP,MAAO,CACLyB,QAAS,MACTC,GAHUqC,EAAOrC,IAA0B,EAI3C1B,MAAO,CACL2B,KAAM,IAAciB,eACpBf,QAAS7B,aAAiByC,MAAQzC,EAAM6B,QAAU,kBAGxD,IAGF,OAAOxB,QAAQ2D,IAAIH,EACrB,E","sources":["webpack://opensafari-mcp/./src/transports/http.ts"],"sourcesContent":["/**\n * Streamable HTTP transport for MCP server.\n *\n * Implements MCP Streamable HTTP transport (spec 2025-03-26):\n * - POST /mcp: receives JSON-RPC request/notification, returns JSON-RPC response\n * - GET /health: basic health check (separate from the self-healing health endpoint)\n * - DELETE /mcp: session termination\n *\n * Key difference from stdio: client disconnect does NOT kill the server.\n * The HTTP server continues to accept new connections.\n */\n\nimport * as http from 'node:http';\nimport * as crypto from 'node:crypto';\nimport { MCPResponse, MCPErrorCodes } from '../types/mcp';\nimport { MCPTransport } from './index';\n\n/** Maximum allowed HTTP request body size (10 MB) to prevent OOM from oversized requests */\nconst MAX_BODY_BYTES = 10 * 1024 * 1024;\n\n/** Active SSE connections for server-initiated notifications */\ninterface SSEConnection {\n res: http.ServerResponse;\n sessionId: string;\n}\n\nexport class HTTPTransport implements MCPTransport {\n private server: http.Server | null = null;\n private messageHandler: ((msg: Record<string, unknown>) => Promise<MCPResponse | null>) | null = null;\n private port: number;\n private sessions: Set<string> = new Set();\n private sseConnections: SSEConnection[] = [];\n private sessionDeleteHandler: ((sessionId: string) => void) | null = null;\n\n constructor(port: number) {\n this.port = port;\n }\n\n /**\n * Register a callback to be invoked whenever a session is deleted.\n * Used by MCPServer to clean up per-session state (e.g. rate-limiter buckets).\n */\n onSessionDelete(handler: (sessionId: string) => void): void {\n this.sessionDeleteHandler = handler;\n }\n\n onMessage(handler: (msg: Record<string, unknown>) => Promise<MCPResponse | null>): void {\n this.messageHandler = handler;\n }\n\n /**\n * Send a server-initiated notification to all connected SSE clients.\n * For HTTP, request-correlated responses are sent directly in handlePost.\n */\n send(response: MCPResponse): void {\n // Broadcast to all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.write(`data: ${JSON.stringify(response)}\\n\\n`);\n } catch {\n // Connection may have been closed\n }\n }\n }\n\n start(): void {\n this.server = http.createServer((req, res) => {\n this.handleHTTPRequest(req, res);\n });\n\n this.server.listen(this.port, () => {\n console.error(`[HTTPTransport] Listening on port ${this.port}`);\n console.error(`[HTTPTransport] MCP endpoint: http://localhost:${this.port}/mcp`);\n });\n\n this.server.on('error', (err) => {\n console.error(`[HTTPTransport] Server error:`, err);\n });\n }\n\n async close(): Promise<void> {\n // Close all SSE connections\n for (const conn of this.sseConnections) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n }\n this.sseConnections = [];\n\n return new Promise((resolve) => {\n if (this.server) {\n this.server.close(() => {\n this.server = null;\n resolve();\n });\n } else {\n resolve();\n }\n });\n }\n\n private handleHTTPRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n const url = new URL(req.url || '/', `http://localhost:${this.port}`);\n const pathname = url.pathname;\n\n // CORS headers for all responses\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');\n res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');\n\n // Handle CORS preflight\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (pathname === '/health') {\n this.handleHealth(res);\n return;\n }\n\n if (pathname === '/mcp') {\n switch (req.method) {\n case 'POST':\n this.handlePost(req, res);\n return;\n case 'GET':\n this.handleSSE(req, res);\n return;\n case 'DELETE':\n this.handleDelete(req, res);\n return;\n default:\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Method not allowed' }));\n return;\n }\n }\n\n // Unknown path\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n }\n\n /**\n * GET /health - basic health check\n */\n private handleHealth(res: http.ServerResponse): void {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n status: 'ok',\n transport: 'http',\n activeSessions: this.sessions.size,\n sseConnections: this.sseConnections.length,\n }));\n }\n\n /**\n * POST /mcp - handle JSON-RPC request or batch\n */\n private handlePost(req: http.IncomingMessage, res: http.ServerResponse): void {\n const chunks: Buffer[] = [];\n let bodyBytes = 0;\n\n req.on('data', (chunk: Buffer) => {\n bodyBytes += chunk.length;\n if (bodyBytes > MAX_BODY_BYTES) {\n res.writeHead(413, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INVALID_REQUEST, message: 'Request body too large' },\n }));\n req.destroy();\n return;\n }\n chunks.push(chunk);\n });\n\n req.on('end', async () => {\n const body = Buffer.concat(chunks).toString('utf-8');\n\n if (!body.trim()) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Empty request body' },\n }));\n return;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: {\n code: MCPErrorCodes.PARSE_ERROR,\n message: error instanceof Error ? error.message : 'Parse error',\n },\n }));\n return;\n }\n\n // Session tracking via Mcp-Session-Id header\n let sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (!this.messageHandler) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.INTERNAL_ERROR, message: 'No message handler registered' },\n }));\n return;\n }\n\n // Handle JSON-RPC batch (array of requests)\n if (Array.isArray(parsed)) {\n const results = await this.processBatch(parsed, sessionId);\n // Filter out null results (notifications don't produce responses)\n const responses = results.filter((r): r is MCPResponse => r !== null);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (responses.length === 0) {\n // All were notifications — respond with 202 Accepted\n res.writeHead(202);\n res.end();\n } else if (responses.length === 1) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses[0]));\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(responses));\n }\n return;\n }\n\n // Single request/notification\n const msg = parsed as Record<string, unknown>;\n\n // Check if this is an initialize request — assign session ID\n if (msg.method === 'initialize' && !sessionId) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n\n try {\n const response = await this.messageHandler(msg);\n\n if (sessionId) {\n res.setHeader('Mcp-Session-Id', sessionId);\n }\n\n if (response === null) {\n // Notification — no response body\n res.writeHead(202);\n res.end();\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(response));\n }\n } catch (error) {\n const id = (msg.id as string | number) ?? 0;\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n }));\n }\n });\n\n req.on('error', (err) => {\n console.error('[HTTPTransport] Request read error:', err);\n if (!res.headersSent) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n id: 0,\n error: { code: MCPErrorCodes.PARSE_ERROR, message: 'Request read error' },\n }));\n }\n });\n }\n\n /**\n * GET /mcp - Server-Sent Events for server-initiated notifications\n */\n private handleSSE(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string || 'anonymous';\n\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n // Send initial keepalive\n res.write(': keepalive\\n\\n');\n\n const conn: SSEConnection = { res, sessionId };\n this.sseConnections.push(conn);\n\n // Clean up on disconnect\n req.on('close', () => {\n const idx = this.sseConnections.indexOf(conn);\n if (idx !== -1) {\n this.sseConnections.splice(idx, 1);\n }\n console.error(`[HTTPTransport] SSE client disconnected (session: ${sessionId})`);\n });\n }\n\n /**\n * DELETE /mcp - Session termination\n */\n private handleDelete(req: http.IncomingMessage, res: http.ServerResponse): void {\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n if (sessionId && this.sessions.has(sessionId)) {\n this.sessions.delete(sessionId);\n\n // Notify session-delete listeners (e.g. rate-limiter cleanup)\n if (this.sessionDeleteHandler) {\n this.sessionDeleteHandler(sessionId);\n }\n\n // Close any SSE connections for this session\n this.sseConnections = this.sseConnections.filter((conn) => {\n if (conn.sessionId === sessionId) {\n try {\n conn.res.end();\n } catch {\n // Already closed\n }\n return false;\n }\n return true;\n });\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'session terminated' }));\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Session not found' }));\n }\n }\n\n /**\n * Process a batch of JSON-RPC messages\n */\n private async processBatch(\n messages: unknown[],\n sessionId: string | undefined,\n ): Promise<(MCPResponse | null)[]> {\n const handler = this.messageHandler!;\n\n // Assign sessionId once before concurrent processing to avoid data race\n // when multiple initialize requests appear in the same batch.\n if (!sessionId) {\n const hasInitialize = messages.some(\n (msg) => typeof msg === 'object' && msg !== null && (msg as Record<string, unknown>).method === 'initialize',\n );\n if (hasInitialize) {\n sessionId = crypto.randomUUID();\n this.sessions.add(sessionId);\n }\n }\n\n const promises = messages.map(async (msg) => {\n if (typeof msg !== 'object' || msg === null) {\n return {\n jsonrpc: '2.0' as const,\n id: 0,\n error: {\n code: MCPErrorCodes.INVALID_REQUEST,\n message: 'Invalid batch element: not an object',\n },\n } as MCPResponse;\n }\n\n const record = msg as Record<string, unknown>;\n\n try {\n return await handler(record);\n } catch (error) {\n const id = (record.id as string | number) ?? 0;\n return {\n jsonrpc: '2.0' as const,\n id,\n error: {\n code: MCPErrorCodes.INTERNAL_ERROR,\n message: error instanceof Error ? error.message : 'Internal error',\n },\n } as MCPResponse;\n }\n });\n\n return Promise.all(promises);\n }\n}\n"],"names":["HTTPTransport","server","messageHandler","port","sessions","Set","sseConnections","sessionDeleteHandler","constructor","this","onSessionDelete","handler","onMessage","send","response","conn","res","write","JSON","stringify","start","req","handleHTTPRequest","listen","console","error","on","err","close","end","Promise","resolve","pathname","URL","url","setHeader","method","writeHead","handlePost","handleSSE","handleDelete","handleHealth","status","transport","activeSessions","size","length","chunks","bodyBytes","chunk","jsonrpc","id","code","INVALID_REQUEST","message","destroy","push","async","body","Buffer","concat","toString","trim","PARSE_ERROR","parsed","parse","Error","sessionId","headers","INTERNAL_ERROR","Array","isArray","responses","processBatch","filter","r","msg","add","headersSent","idx","indexOf","splice","has","delete","messages","some","promises","map","record","all"],"sourceRoot":""}
@@ -0,0 +1,2 @@
1
+ "use strict";exports.id=681,exports.ids=[681],exports.modules={681(e,t,n){async function s(e){return e.evaluate("\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function i(e){return e.evaluate("\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n ")}async function o(e){return e.evaluate("\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n ")}async function r(e){return e.evaluate("\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n ")}async function a(e){return e.evaluate("\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function l(e){const t=await e.evaluate("\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n ");if(!t||0===t.length)return{detector:"keyboard_overlap",severity:"pass",issues:[],passed:!0,totalScanned:0,issueCount:0};const n=await e.evaluate("\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n "),s=[];for(const i of n||[])try{await e.click(i),await new Promise(e=>setTimeout(e,500));const n=await e.evaluate("window.visualViewport ? window.visualViewport.height : window.innerHeight");for(const e of t)e.rect.y+e.rect.height>n&&s.push({selector:e.selector,problem:`Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(e.rect.y+e.rect.height)}px, viewport with keyboard: ${Math.round(n)}px)`,fix:"Use visualViewport API to adjust position when keyboard appears",triggeredBy:i});await e.dismissKeyboard(),await new Promise(e=>setTimeout(e,300))}catch{}return{detector:"keyboard_overlap",severity:s.length>0?"critical":"pass",issues:s,passed:0===s.length,totalScanned:t.length*(n?.length??0),issueCount:s.length}}async function u(e){return e.evaluate("\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n ")}async function c(e){return e.evaluate("\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n ")}async function d(e){return e.evaluate("\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n ")}async function h(e){return e.evaluate("\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n ")}async function m(e,t,n){const s=await e.evaluate("\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n "),i=[];let o,r;if("not set"===s&&i.push({selector:"head",problem:'No <meta name="color-scheme"> tag — browser may apply forced dark mode',fix:'Add <meta name="color-scheme" content="light only"> or implement proper dark mode'}),t&&n)try{await t.setAppearance(n,"light"),await new Promise(e=>setTimeout(e,500)),o=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"dark"),await new Promise(e=>setTimeout(e,500)),r=(await e.screenshot()).toString("base64"),await t.setAppearance(n,"light")}catch{}return{detector:"dark_mode",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{colorScheme:s,...o?{lightScreenshot:o,darkScreenshot:r,note:"Compare screenshots visually"}:{}}}}async function p(e,t,n){const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })"),i=[];if(t&&n)try{await t.rotate(n),await new Promise(e=>setTimeout(e,1e3));const s=await e.evaluate("({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })");s.overflow&&i.push({selector:"document.documentElement",problem:`Horizontal overflow in landscape (scrollWidth: ${s.scrollWidth}px, viewport: ${s.innerWidth}px)`,fix:"Ensure responsive layout handles landscape orientation"}),await t.rotate(n)}catch{}return{detector:"orientation",severity:i.length>0?"medium":"pass",issues:i,passed:0===i.length,totalScanned:1,issueCount:i.length,metadata:{portrait:s}}}async function f(e){return e.evaluate("\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n ")}n.d(t,{QAAudit:()=>g});const v={critical:10,high:5,medium:2,low:1};class g{client;config;simulator;deviceId;deviceInfo;constructor(e,t={},n,s,i){this.client=e,this.config=t,this.simulator=n,this.deviceId=s,this.deviceInfo=i}async runFullAudit(e){e&&await this.client.navigate({url:e,waitUntil:"load"});const t=await this.client.evaluate("window.location.href"),n=Date.now(),v=await Promise.allSettled([s(this.client),i(this.client),o(this.client),r(this.client),a(this.client),u(this.client),c(this.client),d(this.client),h(this.client),f(this.client)]),g=[];try{g.push({status:"fulfilled",value:await l(this.client)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await m(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}try{g.push({status:"fulfilled",value:await p(this.client,this.simulator,this.deviceId)})}catch(e){g.push({status:"rejected",reason:e})}const w=[...v,...g].map((e,t)=>"fulfilled"===e.status?function(e,t){const n=t.ignore?.filter(t=>t.detector===e.detector)??[];return n.length>0&&(e.issues=e.issues.filter(e=>!n.some(t=>e.selector.includes(t.selector))),e.issueCount=e.issues.length,e.passed=0===e.issueCount,e.passed&&(e.severity="pass")),e}(e.value,this.config):{detector:"unknown",severity:"error",issues:[],passed:!1,totalScanned:0,issueCount:0,error:e.reason instanceof Error?e.reason.message:String(e.reason)}),y=this.calculateScore(w),x=this.summarize(w);return{url:t,device:this.deviceInfo?.name??"unknown",viewport:{w:this.deviceInfo?.w??0,h:this.deviceInfo?.h??0},timestamp:(new Date).toISOString(),duration:Date.now()-n,score:y,summary:x,detectors:w}}calculateScore(e){let t=0;for(const n of e)n.severity&&"pass"!==n.severity&&"error"!==n.severity&&(t+=(v[n.severity]??0)*n.issueCount);return Math.max(0,100-t)}summarize(e){return{totalIssues:e.reduce((e,t)=>e+t.issueCount,0),critical:e.filter(e=>"critical"===e.severity).reduce((e,t)=>e+t.issueCount,0),high:e.filter(e=>"high"===e.severity).reduce((e,t)=>e+t.issueCount,0),medium:e.filter(e=>"medium"===e.severity).reduce((e,t)=>e+t.issueCount,0),low:e.filter(e=>"low"===e.severity).reduce((e,t)=>e+t.issueCount,0),passed:e.filter(e=>e.passed).length,failed:e.filter(e=>!e.passed).length,errors:e.filter(e=>"error"===e.severity).length}}}}};
2
+ //# sourceMappingURL=681.index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"681.index.js","mappings":"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","sources":["webpack://opensafari-mcp/./src/qa/detectors/auto-zoom.ts","webpack://opensafari-mcp/./src/qa/detectors/touch-targets.ts","webpack://opensafari-mcp/./src/qa/detectors/hover-only.ts","webpack://opensafari-mcp/./src/qa/detectors/input-type.ts","webpack://opensafari-mcp/./src/qa/detectors/safe-area.ts","webpack://opensafari-mcp/./src/qa/detectors/keyboard-overlap.ts","webpack://opensafari-mcp/./src/qa/detectors/horizontal-overflow.ts","webpack://opensafari-mcp/./src/qa/detectors/vh100.ts","webpack://opensafari-mcp/./src/qa/detectors/fixed-stacking.ts","webpack://opensafari-mcp/./src/qa/detectors/scroll-lock.ts","webpack://opensafari-mcp/./src/qa/detectors/dark-mode.ts","webpack://opensafari-mcp/./src/qa/detectors/orientation.ts","webpack://opensafari-mcp/./src/qa/detectors/pwa-meta.ts","webpack://opensafari-mcp/./src/qa/audit.ts","webpack://opensafari-mcp/./src/qa/types.ts"],"sourcesContent":["import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectAutoZoom(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var inputs = document.querySelectorAll('input, select, textarea');\n var issues = [];\n inputs.forEach(function(el) {\n var style = window.getComputedStyle(el);\n var size = parseFloat(style.fontSize);\n if (size < 16) {\n var rect = el.getBoundingClientRect();\n issues.push({\n selector: el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : '')),\n problem: 'font-size is ' + size + 'px (< 16px minimum)',\n fix: 'Set font-size to at least 16px to prevent iOS Safari auto-zoom on focus',\n });\n }\n });\n return { detector: 'auto_zoom', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectTouchTargets(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var selectors = 'a, button, input, select, textarea, [onclick], [role=\"button\"], [role=\"link\"], [tabindex]:not([tabindex=\"-1\"])';\n var elements = document.querySelectorAll(selectors);\n var threshold = 44;\n var issues = [];\n elements.forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) return;\n if (rect.width < threshold || rect.height < threshold) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Touch target is ' + Math.round(rect.width) + 'x' + Math.round(rect.height) + 'px (minimum: ' + threshold + 'x' + threshold + 'px)',\n fix: 'Increase element size to at least 44x44px or add padding',\n size: { width: Math.round(rect.width), height: Math.round(rect.height) },\n });\n }\n });\n return { detector: 'touch_targets', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: elements.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHoverOnly(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n try {\n Array.from(document.styleSheets).forEach(function(sheet) {\n try {\n Array.from(sheet.cssRules || []).forEach(function(rule) {\n if (rule.selectorText && rule.selectorText.indexOf(':hover') !== -1) {\n var style = rule.style;\n if (style.display || style.visibility || style.opacity) {\n var baseSelector = rule.selectorText.replace(/:hover/g, '').trim();\n var el = document.querySelector(baseSelector);\n if (el) {\n issues.push({\n selector: baseSelector,\n problem: ':hover changes visibility — inaccessible on touch devices',\n fix: 'Add click/touch handler or use :focus-within as alternative',\n cssRule: rule.selectorText,\n });\n }\n }\n }\n });\n } catch(e) {}\n });\n } catch(e) {}\n return { detector: 'hover_only', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 0, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectInputType(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var inputs = document.querySelectorAll('input');\n inputs.forEach(function(el) {\n var type = el.getAttribute('type') || 'text';\n var inputMode = el.getAttribute('inputmode');\n var name = (el.name || el.id || '').toLowerCase();\n if (type === 'text' && !inputMode) {\n if (name.match(/phone|tel|zip|postal|code|pin|otp|cvv|cvc/)) {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Likely numeric field using type=\"text\" without inputmode', fix: 'Add inputmode=\"numeric\" or inputmode=\"tel\"' });\n }\n if (name.match(/email/) && type !== 'email') {\n issues.push({ selector: el.id ? '#' + el.id : 'input[name=\"' + el.name + '\"]', problem: 'Email field using type=\"text\"', fix: 'Use type=\"email\" for email keyboard' });\n }\n }\n });\n return { detector: 'input_type', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: inputs.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectSafeArea(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var viewport = document.querySelector('meta[name=\"viewport\"]');\n var hasViewportFitCover = viewport && viewport.content && viewport.content.indexOf('viewport-fit=cover') !== -1;\n if (!hasViewportFitCover) {\n return { detector: 'safe_area', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0, metadata: { note: 'viewport-fit=cover not set' } };\n }\n var all = document.querySelectorAll('*');\n all.forEach(function(el) {\n var style = window.getComputedStyle(el);\n if (style.position === 'fixed' || style.position === 'sticky') {\n var top = parseFloat(style.top);\n var bottom = parseFloat(style.bottom);\n if (top === 0 || bottom === 0) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Fixed element at ' + (top === 0 ? 'top' : 'bottom') + ' edge without safe-area-inset padding',\n fix: 'Add padding: env(safe-area-inset-' + (top === 0 ? 'top' : 'bottom') + ')',\n });\n }\n }\n });\n return { detector: 'safe_area', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectKeyboardOverlap(client: BrowserBackend): Promise<DetectorResult> {\n // Get fixed-bottom elements\n const fixedBottom = await client.evaluate<Array<{ selector: string; bottom: number; rect: { y: number; height: number } }>>(`\n (function() {\n return Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' && parseFloat(s.bottom) < 50;\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n bottom: parseFloat(window.getComputedStyle(el).bottom),\n rect: { y: el.getBoundingClientRect().y, height: el.getBoundingClientRect().height }\n };\n });\n })()\n `);\n\n if (!fixedBottom || fixedBottom.length === 0) {\n return { detector: 'keyboard_overlap', severity: 'pass', issues: [], passed: true, totalScanned: 0, issueCount: 0 };\n }\n\n // Get inputs\n const inputs = await client.evaluate<string[]>(`\n Array.from(document.querySelectorAll('input:not([type=\"hidden\"]), textarea, select')).slice(0, 5).map(function(el) {\n return el.id ? '#' + el.id : (el.name ? el.tagName.toLowerCase() + '[name=\"' + el.name + '\"]' : el.tagName.toLowerCase());\n })\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string; triggeredBy?: string }> = [];\n\n for (const inputSelector of (inputs || [])) {\n try {\n await client.click(inputSelector);\n await new Promise(r => setTimeout(r, 500));\n\n const viewportWithKeyboard = await client.evaluate<number>('window.visualViewport ? window.visualViewport.height : window.innerHeight');\n\n for (const fixed of fixedBottom) {\n if (fixed.rect.y + fixed.rect.height > viewportWithKeyboard) {\n issues.push({\n selector: fixed.selector,\n problem: `Fixed bottom element hidden behind keyboard (element bottom: ${Math.round(fixed.rect.y + fixed.rect.height)}px, viewport with keyboard: ${Math.round(viewportWithKeyboard)}px)`,\n fix: 'Use visualViewport API to adjust position when keyboard appears',\n triggeredBy: inputSelector,\n });\n }\n }\n\n await client.dismissKeyboard();\n await new Promise(r => setTimeout(r, 300));\n } catch {\n // Input may not be focusable\n }\n }\n\n return {\n detector: 'keyboard_overlap',\n severity: issues.length > 0 ? 'critical' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: fixedBottom.length * (inputs?.length ?? 0),\n issueCount: issues.length,\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectHorizontalOverflow(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var docWidth = document.documentElement.scrollWidth;\n var vpWidth = window.innerWidth;\n if (docWidth <= vpWidth) {\n return { detector: 'horizontal_overflow', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0 };\n }\n var issues = [];\n function findCulprits(parent) {\n Array.from(parent.children).forEach(function(el) {\n var rect = el.getBoundingClientRect();\n if (rect.right > vpWidth + 1) {\n issues.push({\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n problem: 'Element extends to ' + Math.round(rect.right) + 'px (viewport: ' + vpWidth + 'px)',\n fix: 'Add overflow-x: hidden or max-width: 100%',\n overflow: Math.round(rect.right - vpWidth) + 'px',\n });\n }\n });\n }\n findCulprits(document.body);\n return { detector: 'horizontal_overflow', severity: 'high', issues: issues.slice(0, 20), passed: false, totalScanned: 1, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detect100vh(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var temp = document.createElement('div');\n temp.style.cssText = 'position:fixed;top:0;height:100vh;width:1px;pointer-events:none;visibility:hidden';\n document.body.appendChild(temp);\n var vh100 = temp.offsetHeight;\n document.body.removeChild(temp);\n var innerH = window.innerHeight;\n var diff = vh100 - innerH;\n if (Math.abs(diff) < 10) {\n return { detector: '100vh', severity: 'pass', issues: [], passed: true, totalScanned: 1, issueCount: 0, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n }\n var issues = [];\n issues.push({\n selector: 'viewport',\n problem: '100vh = ' + vh100 + 'px but visible viewport = ' + innerH + 'px (diff: ' + diff + 'px)',\n fix: 'Use 100dvh or calc(var(--vh, 1vh) * 100) with JS viewport listener',\n });\n return { detector: '100vh', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 1, issueCount: issues.length, metadata: { vh100: vh100, innerHeight: innerH, difference: diff } };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectFixedStacking(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var fixedEls = Array.from(document.querySelectorAll('*')).filter(function(el) {\n var s = window.getComputedStyle(el);\n return s.position === 'fixed' || s.position === 'sticky';\n }).map(function(el) {\n return {\n selector: el.id ? '#' + el.id : el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ')[0] : ''),\n rect: el.getBoundingClientRect(),\n zIndex: parseInt(window.getComputedStyle(el).zIndex) || 0,\n };\n });\n var issues = [];\n for (var i = 0; i < fixedEls.length; i++) {\n for (var j = i + 1; j < fixedEls.length; j++) {\n var a = fixedEls[i], b = fixedEls[j];\n var overlap = !(a.rect.right < b.rect.left || a.rect.left > b.rect.right || a.rect.bottom < b.rect.top || a.rect.top > b.rect.bottom);\n if (overlap && a.zIndex === b.zIndex) {\n issues.push({\n selector: a.selector + ' <-> ' + b.selector,\n problem: 'Overlapping fixed elements with same z-index (' + a.zIndex + ')',\n fix: 'Set distinct z-index values',\n });\n }\n }\n }\n return { detector: 'fixed_stacking', severity: issues.length > 0 ? 'medium' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: fixedEls.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectScrollLock(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var issues = [];\n var bodyOverflow = document.body.style.overflow;\n var htmlOverflow = document.documentElement.style.overflow;\n var hasVisibleModal = document.querySelector('[role=\"dialog\"]:not([aria-hidden=\"true\"]), .modal:not(.hidden), [data-modal]:not([hidden])');\n if ((bodyOverflow === 'hidden' || htmlOverflow === 'hidden') && !hasVisibleModal) {\n issues.push({ selector: bodyOverflow === 'hidden' ? 'document.body' : 'document.documentElement', problem: 'overflow: hidden set but no visible modal found', fix: 'Ensure modal close handlers restore overflow' });\n }\n if (bodyOverflow === 'unset') {\n issues.push({ selector: 'document.body', problem: 'overflow set to \"unset\" — not a proper reset', fix: 'Use document.body.style.overflow = \"\" (empty string)' });\n }\n return { detector: 'scroll_lock', severity: issues.length > 0 ? 'high' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: 2, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectDarkMode(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const colorScheme = await client.evaluate<string>(`\n (document.querySelector('meta[name=\"color-scheme\"]') || {}).content || 'not set'\n `);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n if (colorScheme === 'not set') {\n issues.push({\n selector: 'head',\n problem: 'No <meta name=\"color-scheme\"> tag — browser may apply forced dark mode',\n fix: 'Add <meta name=\"color-scheme\" content=\"light only\"> or implement proper dark mode',\n });\n }\n\n // Toggle dark mode if simulator available\n let lightScreenshot: string | undefined;\n let darkScreenshot: string | undefined;\n\n if (simulator && deviceId) {\n try {\n await simulator.setAppearance(deviceId, 'light');\n await new Promise(r => setTimeout(r, 500));\n const lightBuf = await client.screenshot();\n lightScreenshot = lightBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'dark');\n await new Promise(r => setTimeout(r, 500));\n const darkBuf = await client.screenshot();\n darkScreenshot = darkBuf.toString('base64');\n\n await simulator.setAppearance(deviceId, 'light');\n } catch {\n // Simulator not available\n }\n }\n\n return {\n detector: 'dark_mode',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: {\n colorScheme,\n ...(lightScreenshot ? { lightScreenshot, darkScreenshot, note: 'Compare screenshots visually' } : {}),\n },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\nimport { SimulatorManager } from '../../simulator/manager';\n\nexport async function detectOrientation(client: BrowserBackend, simulator?: SimulatorManager, deviceId?: string): Promise<DetectorResult> {\n const portraitMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n const issues: Array<{ selector: string; problem: string; fix: string }> = [];\n\n // Try rotation\n if (simulator && deviceId) {\n try {\n await simulator.rotate(deviceId);\n await new Promise(r => setTimeout(r, 1000));\n\n const landscapeMeta = await client.evaluate<{ scrollWidth: number; innerWidth: number; overflow: boolean }>(`({\n scrollWidth: document.documentElement.scrollWidth,\n innerWidth: window.innerWidth,\n overflow: document.documentElement.scrollWidth > window.innerWidth\n })`);\n\n if (landscapeMeta.overflow) {\n issues.push({\n selector: 'document.documentElement',\n problem: `Horizontal overflow in landscape (scrollWidth: ${landscapeMeta.scrollWidth}px, viewport: ${landscapeMeta.innerWidth}px)`,\n fix: 'Ensure responsive layout handles landscape orientation',\n });\n }\n\n await simulator.rotate(deviceId); // rotate back\n } catch {\n // Rotation not available\n }\n }\n\n return {\n detector: 'orientation',\n severity: issues.length > 0 ? 'medium' : 'pass',\n issues,\n passed: issues.length === 0,\n totalScanned: 1,\n issueCount: issues.length,\n metadata: { portrait: portraitMeta },\n };\n}\n","import { BrowserBackend } from '../../types/browser-backend';\nimport { DetectorResult } from '../types';\n\nexport async function detectPwaMeta(client: BrowserBackend): Promise<DetectorResult> {\n return client.evaluate<DetectorResult>(`\n (function() {\n var checks = [\n { name: 'viewport', selector: 'meta[name=\"viewport\"]', required: true, fix: 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">' },\n { name: 'theme-color', selector: 'meta[name=\"theme-color\"]', required: false, fix: 'Add <meta name=\"theme-color\" content=\"#yourColor\">' },\n { name: 'color-scheme', selector: 'meta[name=\"color-scheme\"]', required: false, fix: 'Add <meta name=\"color-scheme\" content=\"light only\">' },\n { name: 'apple-touch-icon', selector: 'link[rel=\"apple-touch-icon\"]', required: false, fix: 'Add <link rel=\"apple-touch-icon\" href=\"/icon-180.png\">' },\n { name: 'manifest', selector: 'link[rel=\"manifest\"]', required: false, fix: 'Add <link rel=\"manifest\" href=\"/manifest.json\">' },\n ];\n var issues = [];\n checks.forEach(function(check) {\n if (!document.querySelector(check.selector)) {\n issues.push({ selector: 'head', problem: 'Missing ' + check.name + (check.required ? ' (required)' : ' (recommended)'), fix: check.fix });\n }\n });\n return { detector: 'pwa_meta', severity: issues.some(function(i) { return i.problem.indexOf('required') !== -1; }) ? 'high' : issues.length > 0 ? 'low' : 'pass', issues: issues, passed: issues.length === 0, totalScanned: checks.length, issueCount: issues.length };\n })()\n `);\n}\n","import { BrowserBackend } from '../types/browser-backend';\nimport { DetectorResult, QAConfig, applyIgnoreRules } from './types';\nimport { detectAutoZoom } from './detectors/auto-zoom';\nimport { detectTouchTargets } from './detectors/touch-targets';\nimport { detectHoverOnly } from './detectors/hover-only';\nimport { detectInputType } from './detectors/input-type';\nimport { detectSafeArea } from './detectors/safe-area';\nimport { detectKeyboardOverlap } from './detectors/keyboard-overlap';\nimport { detectHorizontalOverflow } from './detectors/horizontal-overflow';\nimport { detect100vh } from './detectors/vh100';\nimport { detectFixedStacking } from './detectors/fixed-stacking';\nimport { detectScrollLock } from './detectors/scroll-lock';\nimport { detectDarkMode } from './detectors/dark-mode';\nimport { detectOrientation } from './detectors/orientation';\nimport { detectPwaMeta } from './detectors/pwa-meta';\nimport { SimulatorManager } from '../simulator/manager';\n\nexport interface AuditSummary {\n totalIssues: number;\n critical: number;\n high: number;\n medium: number;\n low: number;\n passed: number;\n failed: number;\n errors: number;\n}\n\nexport interface AuditReport {\n url: string;\n device: string;\n viewport: { w: number; h: number };\n timestamp: string;\n duration: number;\n score: number;\n summary: AuditSummary;\n detectors: DetectorResult[];\n}\n\nconst SEVERITY_WEIGHTS: Record<string, number> = {\n critical: 10,\n high: 5,\n medium: 2,\n low: 1,\n};\n\nexport class QAAudit {\n constructor(\n private client: BrowserBackend,\n private config: QAConfig = {},\n private simulator?: SimulatorManager,\n private deviceId?: string,\n private deviceInfo?: { name: string; w: number; h: number },\n ) {}\n\n async runFullAudit(url?: string): Promise<AuditReport> {\n if (url) await this.client.navigate({ url, waitUntil: 'load' });\n\n const currentUrl = await this.client.evaluate<string>('window.location.href');\n const startTime = Date.now();\n\n // Parallel: stateless detectors (10)\n const parallelResults = await Promise.allSettled([\n detectAutoZoom(this.client),\n detectTouchTargets(this.client),\n detectHoverOnly(this.client),\n detectInputType(this.client),\n detectSafeArea(this.client),\n detectHorizontalOverflow(this.client),\n detect100vh(this.client),\n detectFixedStacking(this.client),\n detectScrollLock(this.client),\n detectPwaMeta(this.client),\n ]);\n\n // Sequential: stateful detectors (3)\n const sequentialResults: PromiseSettledResult<DetectorResult>[] = [];\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectKeyboardOverlap(this.client) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectDarkMode(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n try {\n sequentialResults.push({ status: 'fulfilled', value: await detectOrientation(this.client, this.simulator, this.deviceId) });\n } catch (e) {\n sequentialResults.push({ status: 'rejected', reason: e });\n }\n\n // Combine + apply ignore rules\n const allSettled = [...parallelResults, ...sequentialResults];\n const allResults: DetectorResult[] = allSettled.map((r, _i) => {\n if (r.status === 'fulfilled') {\n return applyIgnoreRules(r.value, this.config);\n }\n return {\n detector: 'unknown',\n severity: 'error' as const,\n issues: [],\n passed: false,\n totalScanned: 0,\n issueCount: 0,\n error: r.reason instanceof Error ? r.reason.message : String(r.reason),\n };\n });\n\n const score = this.calculateScore(allResults);\n const summary = this.summarize(allResults);\n\n return {\n url: currentUrl,\n device: this.deviceInfo?.name ?? 'unknown',\n viewport: { w: this.deviceInfo?.w ?? 0, h: this.deviceInfo?.h ?? 0 },\n timestamp: new Date().toISOString(),\n duration: Date.now() - startTime,\n score,\n summary,\n detectors: allResults,\n };\n }\n\n private calculateScore(results: DetectorResult[]): number {\n let penalty = 0;\n for (const result of results) {\n if (result.severity && result.severity !== 'pass' && result.severity !== 'error') {\n penalty += (SEVERITY_WEIGHTS[result.severity] ?? 0) * result.issueCount;\n }\n }\n return Math.max(0, 100 - penalty);\n }\n\n private summarize(results: DetectorResult[]): AuditSummary {\n return {\n totalIssues: results.reduce((s, r) => s + r.issueCount, 0),\n critical: results.filter(r => r.severity === 'critical').reduce((s, r) => s + r.issueCount, 0),\n high: results.filter(r => r.severity === 'high').reduce((s, r) => s + r.issueCount, 0),\n medium: results.filter(r => r.severity === 'medium').reduce((s, r) => s + r.issueCount, 0),\n low: results.filter(r => r.severity === 'low').reduce((s, r) => s + r.issueCount, 0),\n passed: results.filter(r => r.passed).length,\n failed: results.filter(r => !r.passed).length,\n errors: results.filter(r => r.severity === 'error').length,\n };\n }\n}\n","export interface DetectorIssue {\n selector: string;\n element?: string;\n problem: string;\n fix: string;\n [key: string]: unknown;\n}\n\nexport interface DetectorResult {\n detector: string;\n severity: 'critical' | 'high' | 'medium' | 'low' | 'pass' | 'error';\n issues: DetectorIssue[];\n passed: boolean;\n totalScanned: number;\n issueCount: number;\n metadata?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface QAConfig {\n thresholds?: {\n touchTargetMinSize?: number;\n inputMinFontSize?: number;\n };\n ignore?: Array<{\n detector: string;\n selector: string;\n }>;\n}\n\nexport function applyIgnoreRules(result: DetectorResult, config: QAConfig): DetectorResult {\n const ignores = config.ignore?.filter(r => r.detector === result.detector) ?? [];\n if (ignores.length > 0) {\n result.issues = result.issues.filter(issue =>\n !ignores.some(ign => issue.selector.includes(ign.selector))\n );\n result.issueCount = result.issues.length;\n result.passed = result.issueCount === 0;\n if (result.passed) result.severity = 'pass';\n }\n return result;\n}\n"],"names":["async","detectAutoZoom","client","evaluate","detectTouchTargets","detectHoverOnly","detectInputType","detectSafeArea","detectKeyboardOverlap","fixedBottom","length","detector","severity","issues","passed","totalScanned","issueCount","inputs","inputSelector","click","Promise","r","setTimeout","viewportWithKeyboard","fixed","rect","y","height","push","selector","problem","Math","round","fix","triggeredBy","dismissKeyboard","detectHorizontalOverflow","detect100vh","detectFixedStacking","detectScrollLock","detectDarkMode","simulator","deviceId","colorScheme","lightScreenshot","darkScreenshot","setAppearance","screenshot","toString","metadata","note","detectOrientation","portraitMeta","rotate","landscapeMeta","overflow","scrollWidth","innerWidth","portrait","detectPwaMeta","SEVERITY_WEIGHTS","critical","high","medium","low","QAAudit","config","deviceInfo","constructor","runFullAudit","url","this","navigate","waitUntil","currentUrl","startTime","Date","now","parallelResults","allSettled","sequentialResults","status","value","e","reason","allResults","map","_i","result","ignores","ignore","filter","issue","some","ign","includes","applyIgnoreRules","error","Error","message","String","score","calculateScore","summary","summarize","device","name","viewport","w","h","timestamp","toISOString","duration","detectors","results","penalty","max","totalIssues","reduce","s","failed","errors"],"sourceRoot":""}
@@ -0,0 +1,2 @@
1
+ "use strict";exports.id=838,exports.ids=[838],exports.modules={838(s,e,r){r.d(e,{StdioTransport:()=>o});var t=r(785),n=r(462);class o{rl=null;messageHandler=null;onMessage(s){this.messageHandler=s}send(s){process.stdout.write(JSON.stringify(s)+"\n")}start(){this.rl=t.createInterface({input:process.stdin,terminal:!1}),this.rl.on("line",s=>{if(!s.trim())return;let e;try{e=JSON.parse(s)}catch(s){const e={jsonrpc:"2.0",id:0,error:{code:n.D.PARSE_ERROR,message:s instanceof Error?s.message:"Parse error"}};return void this.send(e)}this.messageHandler?this.messageHandler(e).then(s=>{s&&this.send(s)}).catch(s=>{const r={jsonrpc:"2.0",id:e.id??0,error:{code:n.D.INTERNAL_ERROR,message:s instanceof Error?s.message:"Internal error"}};this.send(r)}):console.error("[StdioTransport] No message handler registered, dropping message")}),this.rl.on("close",()=>{console.error("[StdioTransport] stdin closed, shutting down..."),process.exit(0)})}async close(){this.rl&&(this.rl.close(),this.rl=null)}}}};
2
+ //# sourceMappingURL=838.index.js.map