@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5

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 (195) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/bin/agentic-security.js +3508 -69
  3. package/dist/1122.index.js +779 -0
  4. package/dist/{301.index.js → 1301.index.js} +2 -2
  5. package/dist/1379.index.js +591 -0
  6. package/dist/{444.index.js → 1444.index.js} +2 -2
  7. package/dist/{660.index.js → 1660.index.js} +2 -2
  8. package/dist/{700.index.js → 1700.index.js} +2 -2
  9. package/dist/{905.index.js → 1905.index.js} +3 -3
  10. package/dist/{920.index.js → 1920.index.js} +3 -3
  11. package/dist/{238.index.js → 2238.index.js} +3 -3
  12. package/dist/{271.index.js → 2271.index.js} +2 -2
  13. package/dist/{985.index.js → 2376.index.js} +1260 -340
  14. package/dist/2432.index.js +793 -0
  15. package/dist/2659.index.js +93 -0
  16. package/dist/{826.index.js → 2826.index.js} +2 -2
  17. package/dist/{830.index.js → 2830.index.js} +2 -2
  18. package/dist/2923.index.js +298 -0
  19. package/dist/{1.index.js → 3001.index.js} +5 -5
  20. package/dist/{117.index.js → 3117.index.js} +3 -3
  21. package/dist/3180.index.js +379 -0
  22. package/dist/3276.index.js +117 -0
  23. package/dist/{415.index.js → 3415.index.js} +2 -2
  24. package/dist/{499.index.js → 3499.index.js} +2 -2
  25. package/dist/3518.index.js +450 -0
  26. package/dist/{526.index.js → 3526.index.js} +5 -5
  27. package/dist/{736.index.js → 3736.index.js} +4 -4
  28. package/dist/{839.index.js → 3839.index.js} +4 -4
  29. package/dist/{113.index.js → 4113.index.js} +5 -5
  30. package/dist/{265.index.js → 4265.index.js} +2 -2
  31. package/dist/{384.index.js → 4384.index.js} +3 -3
  32. package/dist/4547.index.js +268 -0
  33. package/dist/4863.index.js +422 -0
  34. package/dist/{970.index.js → 4970.index.js} +2 -2
  35. package/dist/5051.index.js +841 -0
  36. package/dist/{144.index.js → 5144.index.js} +5 -5
  37. package/dist/{333.index.js → 5333.index.js} +3 -3
  38. package/dist/5343.index.js +185 -0
  39. package/dist/5350.index.js +866 -0
  40. package/dist/5561.index.js +436 -0
  41. package/dist/{637.index.js → 5637.index.js} +3 -3
  42. package/dist/{449.index.js → 5830.index.js} +2 -2
  43. package/dist/6626.index.js +532 -0
  44. package/dist/6662.index.js +297 -0
  45. package/dist/{675.index.js → 6675.index.js} +5 -5
  46. package/dist/{730.index.js → 6730.index.js} +6 -6
  47. package/dist/6829.index.js +225 -0
  48. package/dist/6944.index.js +130 -0
  49. package/dist/{178.index.js → 7178.index.js} +3 -3
  50. package/dist/{227.index.js → 7227.index.js} +2 -2
  51. package/dist/7310.index.js +520 -0
  52. package/dist/{552.index.js → 7552.index.js} +4 -4
  53. package/dist/7709.index.js +78 -0
  54. package/dist/8218.index.js +160 -0
  55. package/dist/{476.index.js → 8476.index.js} +4 -4
  56. package/dist/{513.index.js → 8513.index.js} +5 -5
  57. package/dist/{520.index.js → 8520.index.js} +2 -2
  58. package/dist/{718.index.js → 8718.index.js} +2 -2
  59. package/dist/{752.index.js → 8752.index.js} +2 -2
  60. package/dist/8846.index.js +100 -0
  61. package/dist/{435.index.js → 9091.index.js} +560 -189
  62. package/dist/{207.index.js → 9207.index.js} +2 -2
  63. package/dist/{220.index.js → 9220.index.js} +2 -2
  64. package/dist/9390.index.js +163 -0
  65. package/dist/{503.index.js → 9503.index.js} +2 -2
  66. package/dist/{801.index.js → 9801.index.js} +2 -2
  67. package/dist/{824.index.js → 9824.index.js} +2 -2
  68. package/dist/agentic-security.mjs +16 -16
  69. package/dist/agentic-security.mjs.sha256 +1 -1
  70. package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
  71. package/dist/frontend/index.html +21 -0
  72. package/dist/frontend/src/app.js +176 -0
  73. package/dist/frontend/src/components/evidence-inspector.js +141 -0
  74. package/dist/frontend/src/components/filter-rail.js +119 -0
  75. package/dist/frontend/src/components/query-bar.js +126 -0
  76. package/dist/frontend/src/data/flagship-graph.js +1460 -0
  77. package/dist/frontend/src/export-entry.js +36 -0
  78. package/dist/frontend/src/lib/api-client.js +92 -0
  79. package/dist/frontend/src/lib/contrast.js +34 -0
  80. package/dist/frontend/src/lib/dom.js +24 -0
  81. package/dist/frontend/src/lib/escape-html.js +16 -0
  82. package/dist/frontend/src/lib/flow-path.js +40 -0
  83. package/dist/frontend/src/lib/focus-controls.js +149 -0
  84. package/dist/frontend/src/lib/protection-visual.js +46 -0
  85. package/dist/frontend/src/lib/query-language.js +240 -0
  86. package/dist/frontend/src/lib/row-filters.js +43 -0
  87. package/dist/frontend/src/lib/state.js +84 -0
  88. package/dist/frontend/src/main.js +83 -0
  89. package/dist/frontend/src/shell.js +184 -0
  90. package/dist/frontend/src/views/architecture-view.js +798 -0
  91. package/dist/frontend/src/views/inventory-view.js +292 -0
  92. package/dist/frontend/src/views/privacy-view.js +172 -0
  93. package/dist/frontend/src/views/trace-view.js +206 -0
  94. package/dist/frontend/styles/architecture-view.css +93 -0
  95. package/dist/frontend/styles/filter-rail.css +34 -0
  96. package/dist/frontend/styles/inspector.css +69 -0
  97. package/dist/frontend/styles/inventory-view.css +74 -0
  98. package/dist/frontend/styles/privacy-view.css +86 -0
  99. package/dist/frontend/styles/query-bar.css +107 -0
  100. package/dist/frontend/styles/shell.css +155 -0
  101. package/dist/frontend/styles/tokens.css +128 -0
  102. package/dist/frontend/styles/trace-view.css +95 -0
  103. package/package.json +13 -6
  104. package/src/dataflow/CLAUDE.md +1 -1
  105. package/src/dataflow/catalog.js +42 -0
  106. package/src/dataflow/orm-write-catalog.js +175 -0
  107. package/src/engine.js +92 -1
  108. package/src/ir/CLAUDE.md +1 -0
  109. package/src/ir/chrome-probe.mjs +150 -0
  110. package/src/ir/parser-js.js +94 -7
  111. package/src/lineage/CLAUDE.md +1203 -0
  112. package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
  113. package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
  114. package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
  115. package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
  116. package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
  117. package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
  118. package/src/lineage/DESIGN_REGISTRIES.md +880 -0
  119. package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
  120. package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
  121. package/src/lineage/classification.js +56 -0
  122. package/src/lineage/coverage.js +658 -0
  123. package/src/lineage/cross-repo-link.js +107 -0
  124. package/src/lineage/dataflow-graph.schema.json +184 -0
  125. package/src/lineage/decision-story.js +206 -0
  126. package/src/lineage/drift-policy.js +279 -0
  127. package/src/lineage/driver.js +135 -0
  128. package/src/lineage/engine.js +992 -0
  129. package/src/lineage/export-briefing.js +628 -0
  130. package/src/lineage/export-csv.js +62 -0
  131. package/src/lineage/export-json.js +238 -0
  132. package/src/lineage/export-privacy.js +258 -0
  133. package/src/lineage/federation-loader.js +111 -0
  134. package/src/lineage/field-identity.js +78 -0
  135. package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
  136. package/src/lineage/fixtures/flagship-graph.json +1453 -0
  137. package/src/lineage/flow-grade.js +221 -0
  138. package/src/lineage/governance-edit.js +169 -0
  139. package/src/lineage/graph-builder.js +1114 -0
  140. package/src/lineage/graph-diff.js +431 -0
  141. package/src/lineage/graph-snapshot.js +180 -0
  142. package/src/lineage/handling-analyzer.js +168 -0
  143. package/src/lineage/ids.js +349 -0
  144. package/src/lineage/impact-assessment.js +76 -0
  145. package/src/lineage/impact-engine.js +268 -0
  146. package/src/lineage/index.js +281 -0
  147. package/src/lineage/language-coverage-tiers.js +58 -0
  148. package/src/lineage/obligation-mapping.js +126 -0
  149. package/src/lineage/obligation-predicates.js +235 -0
  150. package/src/lineage/observation-adapters.js +282 -0
  151. package/src/lineage/observation-correlation.js +622 -0
  152. package/src/lineage/observation-store.js +497 -0
  153. package/src/lineage/path-query.js +410 -0
  154. package/src/lineage/path-store.js +400 -0
  155. package/src/lineage/protection.js +53 -0
  156. package/src/lineage/recipient-profile.js +192 -0
  157. package/src/lineage/recipient-registry.js +394 -0
  158. package/src/lineage/redact-graph.js +224 -0
  159. package/src/lineage/remediation.js +417 -0
  160. package/src/lineage/resolve-destination.js +91 -0
  161. package/src/lineage/runtime-observation.js +464 -0
  162. package/src/lineage/scenario-diff.js +84 -0
  163. package/src/lineage/scenario-engine.js +251 -0
  164. package/src/lineage/scenario.js +101 -0
  165. package/src/lineage/schema.js +167 -0
  166. package/src/lineage/sink-registry.js +427 -0
  167. package/src/lineage/source-registry.js +357 -0
  168. package/src/lineage/source-seeding.js +212 -0
  169. package/src/lineage/summaries.js +590 -0
  170. package/src/lineage/transform-catalog.js +397 -0
  171. package/src/lineage/transit-protection.js +150 -0
  172. package/src/lineage/validate.js +285 -0
  173. package/src/mcp/CLAUDE.md +7 -1
  174. package/src/mcp/dataflow-tools.js +160 -0
  175. package/src/mcp/server.js +1 -1
  176. package/src/mcp/tools.js +2 -1
  177. package/src/pipeline/scan-health.js +19 -1
  178. package/src/posture/CLAUDE.md +19 -0
  179. package/src/posture/artifact-registry.js +52 -0
  180. package/src/posture/auditor-walkthrough.js +76 -0
  181. package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
  182. package/src/posture/obligation-evidence-pack.js +202 -0
  183. package/src/posture/remediation-ledger.js +337 -0
  184. package/src/server/CLAUDE.md +47 -0
  185. package/src/server/graph-loader.js +141 -0
  186. package/src/server/http-server.js +325 -0
  187. package/src/server/routes.js +129 -0
  188. package/src/server/security.js +111 -0
  189. package/src/server/static-assets.js +144 -0
  190. package/src/shared/frontend-root.js +52 -0
  191. package/dist/11.index.js +0 -353
  192. package/dist/259.index.js +0 -975
  193. package/dist/317.index.js +0 -300
  194. package/dist/609.index.js +0 -741
  195. package/dist/838.index.js +0 -152
@@ -0,0 +1,841 @@
1
+ export const id = 5051;
2
+ export const ids = [5051,6944];
3
+ export const modules = {
4
+
5
+ /***/ 5051:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+
9
+ // EXPORTS
10
+ __webpack_require__.d(__webpack_exports__, {
11
+ createExploreServer: () => (/* binding */ createExploreServer)
12
+ });
13
+
14
+ // UNUSED EXPORTS: DEFAULT_IDLE_TIMEOUT_MS, MAX_REQUEST_BODY_BYTES, TOKEN_HEADER
15
+
16
+ // EXTERNAL MODULE: external "node:http"
17
+ var external_node_http_ = __webpack_require__(7067);
18
+ // EXTERNAL MODULE: external "node:fs"
19
+ var external_node_fs_ = __webpack_require__(3024);
20
+ // EXTERNAL MODULE: external "node:path"
21
+ var external_node_path_ = __webpack_require__(6760);
22
+ // EXTERNAL MODULE: ./src/server/security.js
23
+ var security = __webpack_require__(6944);
24
+ // EXTERNAL MODULE: ./src/server/routes.js
25
+ var routes = __webpack_require__(4268);
26
+ // EXTERNAL MODULE: external "node:url"
27
+ var external_node_url_ = __webpack_require__(3136);
28
+ // EXTERNAL MODULE: ./src/shared/frontend-root.js
29
+ var frontend_root = __webpack_require__(1185);
30
+ ;// CONCATENATED MODULE: ./src/server/static-assets.js
31
+ // static-assets.js — Milestone 3, sub-project Wire.
32
+ //
33
+ // Confines the `explore` server's new same-origin static-asset surface to an
34
+ // explicit ALLOWLIST of `frontend/`'s own real, servable files. This is the
35
+ // first place scanner/src/server/ ever reads arbitrary requested paths off
36
+ // disk — S1's five JSON endpoints never touched the filesystem beyond the
37
+ // one already-loaded, already-verified graph — so path confinement here is
38
+ // genuinely load-bearing, not decorative (see the M3-Wire scoping doc's
39
+ // Decision 2, and the threat-model doc's T4 entry).
40
+ //
41
+ // `resolveStaticAsset()` is a PURE function: no fs/http access anywhere in
42
+ // this file, fully unit-testable in isolation. http-server.js is the only
43
+ // caller that actually reads a resolved file off disk.
44
+
45
+
46
+
47
+
48
+
49
+ // Located relative to this module's own directory (path.dirname(
50
+ // fileURLToPath(import.meta.url)), the same pattern scanner/src/mcp/server.js
51
+ // uses), but via resolveFrontendRoot's search-upward strategy rather than a
52
+ // single hardcoded relative depth — a fixed `../../../frontend` resolved
53
+ // correctly for this file's unbundled dev location but broke (silently, at
54
+ // runtime, for every real npx/npm user) once `npm run build` split this
55
+ // module into its own dist/ chunk, whose import.meta.url reflects dist/'s
56
+ // own shallower location. See src/shared/frontend-root.js for the full story
57
+ // and scripts/copy-frontend.mjs for the build-time copy this now finds.
58
+ const _here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
59
+ const FRONTEND_ROOT = (0,frontend_root/* resolveFrontendRoot */.D)(_here);
60
+
61
+ const CONTENT_TYPE_MAP = Object.freeze({
62
+ '.html': 'text/html; charset=utf-8',
63
+ '.js': 'text/javascript; charset=utf-8',
64
+ '.css': 'text/css; charset=utf-8',
65
+ });
66
+
67
+ // Same-origin-permitting CSP for the static/HTML surface — deliberately
68
+ // DIFFERENT from security.js's CSP_HEADER_VALUE ("default-src 'none'"),
69
+ // which stays correct for the JSON-only /api/v1/* routes (unchanged). This
70
+ // one needs to let the page load its OWN same-origin script/style and make
71
+ // its OWN same-origin fetch() calls, while still denying everything
72
+ // cross-origin — no remote scripts/fonts/analytics, matching
73
+ // frontend/README.md's own "no CDN scripts, remote fonts, analytics" rule.
74
+ //
75
+ // Grepped across the whole frontend/ tree for any inline <script>/
76
+ // style="..." attribute/<style> tag beyond the one index.html inline
77
+ // <script type="module"> block this increment moves into main.js (see
78
+ // item 5 of the plan): none found (confirmed this session, 2026-09-01).
79
+ // So a strict script-src 'self'/style-src 'self' needs no 'unsafe-inline'
80
+ // exception — do not add one without re-running that grep.
81
+ const STATIC_CSP_HEADER_VALUE =
82
+ "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'";
83
+
84
+ // Deliberately restrictive character class (no backslash, no space, no
85
+ // percent sign, no null) — every real filename in frontend/'s inventory is
86
+ // plain ASCII with letters/digits/hyphen/underscore/dot/slash, and keeping
87
+ // the allowlist regex this narrow means an unexpected character in a
88
+ // request path fails allowlisting by default rather than being silently
89
+ // accepted and handed to a platform-specific path.join() later.
90
+ const SRC_JS_RE = /^src\/[A-Za-z0-9/_.-]+\.js$/;
91
+ const STYLES_CSS_RE = /^styles\/[A-Za-z0-9_.-]+\.css$/; // top-level only under styles/, no nested subdirectories — matches the real, current inventory
92
+
93
+ /**
94
+ * Resolves a request pathname (e.g. `/`, `/src/app.js`, `/styles/tokens.css`)
95
+ * against `frontend/`'s explicit servable-file allowlist. Returns
96
+ * `{ok:true, relativePath, contentType}` or `{ok:false, reason}`.
97
+ *
98
+ * Deliberately an ALLOWLIST, not merely a `../`-traversal guard: `test/`,
99
+ * `scripts/`, `package.json`, `README.md`, `CLAUDE.md`, and `.gitignore` are
100
+ * all real files that exist under FRONTEND_ROOT on disk and must NEVER be
101
+ * served — a naive "stays inside frontend/" confinement check would still
102
+ * happily serve every one of them.
103
+ */
104
+ function resolveStaticAsset(requestPath) {
105
+ if (typeof requestPath !== 'string' || requestPath.length === 0) {
106
+ return { ok: false, reason: 'invalid-path' };
107
+ }
108
+ if (requestPath.includes('\0')) {
109
+ return { ok: false, reason: 'null-byte' };
110
+ }
111
+ // Absolute-URL-looking input: a scheme (`http://...`) or a
112
+ // protocol-relative `//host/...` — neither is a valid same-origin
113
+ // pathname this server should ever try to resolve.
114
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(requestPath) || requestPath.startsWith('//')) {
115
+ return { ok: false, reason: 'absolute-url' };
116
+ }
117
+ if (!requestPath.startsWith('/')) {
118
+ return { ok: false, reason: 'invalid-path' };
119
+ }
120
+
121
+ let decoded;
122
+ try {
123
+ decoded = decodeURIComponent(requestPath);
124
+ } catch {
125
+ return { ok: false, reason: 'malformed-encoding' };
126
+ }
127
+ if (decoded.includes('\0')) {
128
+ // Catches a %00-encoded null byte the raw-string check above can't see.
129
+ return { ok: false, reason: 'null-byte' };
130
+ }
131
+
132
+ if (decoded === '/' || decoded === '') {
133
+ return { ok: true, relativePath: 'index.html', contentType: CONTENT_TYPE_MAP['.html'] };
134
+ }
135
+ if (decoded.startsWith('//')) {
136
+ return { ok: false, reason: 'absolute-url' };
137
+ }
138
+
139
+ const candidate = decoded.slice(1); // strip the single leading '/'
140
+
141
+ // Normalize using POSIX rules regardless of host OS — this is a URL path,
142
+ // never a platform filesystem path, and must be treated as forward-slash
143
+ // segments even on a host where path.normalize would use backslashes.
144
+ const normalized = external_node_path_.posix.normalize(candidate);
145
+
146
+ // Re-check for a traversal escape AFTER normalization — never trust
147
+ // normalize() alone to have removed a traversal attempt (a classic
148
+ // traversal-bypass class): explicitly reject anything that still IS, or
149
+ // still CONTAINS, a `..` segment, or that normalized to an absolute path.
150
+ if (
151
+ normalized === '..' ||
152
+ normalized === '.' ||
153
+ normalized.startsWith('../') ||
154
+ normalized.includes('/../') ||
155
+ normalized.endsWith('/..') ||
156
+ normalized.startsWith('/')
157
+ ) {
158
+ return { ok: false, reason: 'path-traversal' };
159
+ }
160
+
161
+ // --- The actual ALLOWLIST (default: deny) ---
162
+
163
+ if (normalized === 'index.html') {
164
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.html'] };
165
+ }
166
+ if (SRC_JS_RE.test(normalized)) {
167
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.js'] };
168
+ }
169
+ if (STYLES_CSS_RE.test(normalized)) {
170
+ return { ok: true, relativePath: normalized, contentType: CONTENT_TYPE_MAP['.css'] };
171
+ }
172
+
173
+ return { ok: false, reason: 'not-allowlisted' };
174
+ }
175
+
176
+ ;// CONCATENATED MODULE: ./src/server/http-server.js
177
+ // http-server.js — Milestone 3, sub-project Server, increment 1.
178
+ //
179
+ // The real node:http server for `agentic-security explore`. Binds ONLY to
180
+ // 127.0.0.1 (never 0.0.0.0, never an omitted host arg). Every request is
181
+ // validated (Host header, then session token) before any route handler
182
+ // ever runs. Every response carries CSP + Cache-Control: no-store and
183
+ // NEVER carries Access-Control-Allow-Origin (CORS stays disabled by the
184
+ // absence of that header — see security.js).
185
+ //
186
+ // This is the first node:http usage anywhere in scanner/src/.
187
+
188
+
189
+
190
+
191
+
192
+
193
+
194
+
195
+ // Idle-timeout default. No PRD-specified duration exists (confirmed by the
196
+ // scoping doc's own search) — 30 minutes is a reasoned, disclosed default:
197
+ // long enough that a human actually exploring a graph in a browser tab
198
+ // won't get cut off mid-session, short enough that a forgotten terminal
199
+ // doesn't leave a loopback server (and its session token, printed once to
200
+ // stdout/scrollback) listening indefinitely.
201
+ const DEFAULT_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
202
+
203
+ // Request-size cap, applied uniformly to every request. Milestone 5's own
204
+ // POST /api/v1/query is the first real consumer of a request body — every
205
+ // pre-M5 GET endpoint has no meaningful body of its own, so this mostly
206
+ // protects that one route today, but the cap applies to any future
207
+ // body-bearing route without retrofitting. 64KB is generous for any
208
+ // header/body this server should ever legitimately receive.
209
+ const MAX_REQUEST_BODY_BYTES = 64 * 1024;
210
+
211
+ // Session token header. A custom header (never a query param) so the
212
+ // token never ends up in server access logs, browser history, or a
213
+ // Referer header the way a query-string token could.
214
+ const TOKEN_HEADER = 'x-agentic-security-token';
215
+
216
+ function _log(method, urlPath, status, durationMs) {
217
+ // Metadata only — NEVER log request/response bodies, NEVER log the
218
+ // session token itself, even on a failed-auth attempt (T3/§17.4).
219
+ console.error(`[explore] ${method} ${urlPath} ${status} ${durationMs}ms`);
220
+ }
221
+
222
+ function _sendJson(res, status, bodyObj) {
223
+ const body = JSON.stringify(bodyObj);
224
+ if (!res.headersSent) {
225
+ res.writeHead(status, {
226
+ 'Content-Type': 'application/json; charset=utf-8',
227
+ 'Content-Security-Policy': security/* CSP_HEADER_VALUE */.Xv,
228
+ 'Cache-Control': 'no-store',
229
+ 'Content-Length': Buffer.byteLength(body),
230
+ // Access-Control-Allow-Origin is intentionally NEVER set anywhere in
231
+ // this file. CORS stays disabled by the absence of this header.
232
+ });
233
+ }
234
+ res.end(body);
235
+ }
236
+
237
+ const ROUTES = [
238
+ { method: 'GET', pattern: /^\/api\/v1\/scan\/?$/, handler: (graph) => (0,routes/* handleScan */.Yo)(graph) },
239
+ { method: 'GET', pattern: /^\/api\/v1\/graph\/?$/, handler: (graph) => (0,routes/* handleGraph */.fn)(graph) },
240
+ { method: 'GET', pattern: /^\/api\/v1\/nodes\/([^/]+)\/?$/, handler: (graph, m) => (0,routes/* handleNode */.d5)(graph, decodeURIComponent(m[1])) },
241
+ { method: 'GET', pattern: /^\/api\/v1\/edges\/([^/]+)\/?$/, handler: (graph, m) => (0,routes/* handleEdge */.Yu)(graph, decodeURIComponent(m[1])) },
242
+ { method: 'GET', pattern: /^\/api\/v1\/flows\/([^/]+)\/?$/, handler: (graph, m) => (0,routes/* handleFlow */.jg)(graph, decodeURIComponent(m[1])) },
243
+ { method: 'POST', pattern: /^\/api\/v1\/query\/?$/, handler: (graph, m, body) => (0,routes/* handleQuery */.rR)(graph, body?.filter) },
244
+ ];
245
+
246
+ /**
247
+ * Serves one resolved static asset from disk. `resolved` is a real
248
+ * `{ok:true, relativePath, contentType}` from resolveStaticAsset() —
249
+ * already vetted against the allowlist, so the only failure mode left here
250
+ * is the file genuinely missing/unreadable on disk (allowlist/inventory
251
+ * drift), which degrades to a 404 rather than a 500.
252
+ *
253
+ * `relativePath` is built entirely from static-assets.js's own restricted
254
+ * character class (no backslash, already traversal-checked) before it ever
255
+ * reaches this function, so joining it onto FRONTEND_ROOT with the platform
256
+ * path.join is safe.
257
+ */
258
+ function _serveStaticAsset(res, resolved) {
259
+ let body;
260
+ try {
261
+ body = external_node_fs_.readFileSync(external_node_path_.join(FRONTEND_ROOT, resolved.relativePath));
262
+ } catch {
263
+ _sendJson(res, 404, { error: 'not found' });
264
+ return 404;
265
+ }
266
+ if (!res.headersSent) {
267
+ res.writeHead(200, {
268
+ 'Content-Type': resolved.contentType,
269
+ // The STATIC CSP (same-origin-permitting), NOT the JSON API's
270
+ // `default-src 'none'` — this response is HTML/JS/CSS, not JSON.
271
+ 'Content-Security-Policy': STATIC_CSP_HEADER_VALUE,
272
+ 'Cache-Control': 'no-store',
273
+ 'Content-Length': body.length,
274
+ // Access-Control-Allow-Origin is intentionally NEVER set anywhere in
275
+ // this file, on any response, static or JSON alike.
276
+ });
277
+ }
278
+ res.end(body);
279
+ return 200;
280
+ }
281
+
282
+ /**
283
+ * Creates and starts the `explore` HTTP server. Returns a Promise that
284
+ * resolves once the server is actually listening, with `{server, port}`
285
+ * (the REAL bound port, resolved from the OS when `port: 0`/omitted was
286
+ * requested).
287
+ *
288
+ * @param {object} opts
289
+ * @param {object} opts.graph - the verified, in-memory lineage graph
290
+ * @param {number} [opts.port=0] - 0 lets the OS choose a random free port
291
+ * @param {string} opts.sessionToken - required on every request
292
+ * @param {number} [opts.idleTimeoutMs=DEFAULT_IDLE_TIMEOUT_MS]
293
+ * @param {boolean} [opts.keepOpen=false] - suppresses idle-timeout auto-stop
294
+ */
295
+ function createExploreServer({
296
+ graph,
297
+ port = 0,
298
+ sessionToken,
299
+ idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
300
+ keepOpen = false,
301
+ } = {}) {
302
+ if (!graph || typeof graph !== 'object') {
303
+ throw new Error('createExploreServer requires a graph object');
304
+ }
305
+ if (typeof sessionToken !== 'string' || sessionToken.length < 32) {
306
+ throw new Error('createExploreServer requires a real sessionToken (>= 32 chars)');
307
+ }
308
+
309
+ let idleTimer = null;
310
+
311
+ function _clearIdleTimer() {
312
+ if (idleTimer) {
313
+ clearTimeout(idleTimer);
314
+ idleTimer = null;
315
+ }
316
+ }
317
+
318
+ // Reset the idle-auto-stop timer. Only called for requests that pass
319
+ // BOTH the Host and session-token checks — an unauthenticated request
320
+ // (a DNS-rebinding probe, a stray scan) must never be able to keep the
321
+ // server alive indefinitely by hammering it; only genuine, authenticated
322
+ // activity extends the session.
323
+ function _resetIdleTimer() {
324
+ if (keepOpen) return;
325
+ _clearIdleTimer();
326
+ idleTimer = setTimeout(() => {
327
+ try { server.closeAllConnections?.(); } catch { /* best-effort */ }
328
+ server.close();
329
+ }, idleTimeoutMs);
330
+ // Don't let this timer alone keep the Node process alive past a clean
331
+ // shutdown path.
332
+ if (typeof idleTimer.unref === 'function') idleTimer.unref();
333
+ }
334
+
335
+ const server = external_node_http_.createServer((req, res) => {
336
+ const start = Date.now();
337
+ const method = req.method || 'GET';
338
+ let urlPath = '/';
339
+ try {
340
+ urlPath = new URL(req.url, 'http://internal').pathname;
341
+ } catch {
342
+ urlPath = req.url || '/';
343
+ }
344
+
345
+ const finish = (status) => _log(method, urlPath, status, Date.now() - start);
346
+
347
+ // The actual bound port. Cheap (no syscall — Node caches the bound
348
+ // address on the server object), so it's safe to read per-request
349
+ // rather than caching it in a closure variable that could theoretically
350
+ // race the 'listening' event.
351
+ const boundPort = server.address()?.port;
352
+
353
+ // 1. Host header validation (T2 defense) — before anything else.
354
+ const hostHeader = req.headers.host;
355
+ if (!(0,security/* isValidHost */.HO)(hostHeader, boundPort)) {
356
+ _sendJson(res, 400, { error: 'invalid Host header' });
357
+ finish(400);
358
+ req.resume(); // drain and discard any body without processing it
359
+ return;
360
+ }
361
+
362
+ // 1.5. Static-asset serving (Wire's own new surface) — the ONE
363
+ // deliberate, disclosed exception to "every request requires a session
364
+ // token" below. The token travels to the browser via a URL FRAGMENT
365
+ // (`#token=...`), which is NEVER sent to the server by the browser in
366
+ // any HTTP request — so the very first page-load request (GET /) is
367
+ // structurally incapable of carrying it. Every subsequent same-origin
368
+ // fetch() the page itself makes against /api/v1/* still requires the
369
+ // token, completely unchanged from S1.
370
+ //
371
+ // Namespaced away from /api/v1/* so there is no ambiguity: any GET
372
+ // request NOT under /api/ is resolved against the static-asset
373
+ // allowlist and NEVER reaches the token check at all, matching or
374
+ // rejecting with a 404 (never a 403 — a 403 would confirm to a prober
375
+ // that a rejected path exists). This still runs strictly AFTER the
376
+ // Host-header check above (T2 applies uniformly, auth or not — DNS
377
+ // rebinding does not care whether the resource behind it needs a
378
+ // token).
379
+ if (method === 'GET' && !urlPath.startsWith('/api/')) {
380
+ const staticResult = resolveStaticAsset(urlPath);
381
+ if (staticResult.ok) {
382
+ const status = _serveStaticAsset(res, staticResult);
383
+ finish(status);
384
+ } else {
385
+ _sendJson(res, 404, { error: 'not found' });
386
+ finish(404);
387
+ }
388
+ req.resume();
389
+ return;
390
+ }
391
+
392
+ // 2. Session-token validation (T3 defense) — required on EVERY
393
+ // request, read or write, per the threat-model doc's own §17.4
394
+ // checklist. Compared in constant time.
395
+ const provided = req.headers[TOKEN_HEADER];
396
+ if (typeof provided !== 'string' || !(0,security/* constantTimeEqual */.SK)(provided, sessionToken)) {
397
+ _sendJson(res, 401, { error: 'missing or invalid session token' });
398
+ finish(401);
399
+ req.resume();
400
+ return;
401
+ }
402
+
403
+ // Genuine, authenticated request — extend the idle-auto-stop window.
404
+ _resetIdleTimer();
405
+
406
+ // 3. Request-size cap, applied uniformly (S1's GET endpoints have no
407
+ // meaningful body; Milestone 5's own POST /api/v1/query is the first
408
+ // real consumer). Milestone 5 also starts accumulating the actual body
409
+ // BYTES (not just the size) — no earlier route needed them.
410
+ let bodySize = 0;
411
+ let bodyChunks = [];
412
+ let aborted = false;
413
+ req.on('data', (chunk) => {
414
+ if (aborted) return;
415
+ bodySize += chunk.length;
416
+ if (bodySize > MAX_REQUEST_BODY_BYTES) {
417
+ aborted = true;
418
+ _sendJson(res, 413, { error: 'request body too large' });
419
+ finish(413);
420
+ // Stop reading further body bytes only once the 413 response has
421
+ // actually been flushed to the client — destroying the request
422
+ // stream immediately can race the response write and cut it off
423
+ // before the client sees it.
424
+ res.once('finish', () => { try { req.destroy(); } catch { /* best-effort */ } });
425
+ return;
426
+ }
427
+ bodyChunks.push(chunk);
428
+ });
429
+
430
+ req.on('end', () => {
431
+ if (aborted) return;
432
+
433
+ // 4. Route.
434
+ let matched = null;
435
+ let match = null;
436
+ for (const route of ROUTES) {
437
+ if (route.method !== method) continue;
438
+ const m = route.pattern.exec(urlPath);
439
+ if (m) { matched = route; match = m; break; }
440
+ }
441
+ if (!matched) {
442
+ _sendJson(res, 404, { error: 'not found' });
443
+ finish(404);
444
+ return;
445
+ }
446
+
447
+ // Milestone 5: parse the body ONLY for a matched route, and only as
448
+ // JSON when non-empty — every pre-M5 GET route still ignores this
449
+ // 3rd argument entirely, so an empty/missing body for them is a
450
+ // no-op, not an error. Disclosed, real, low-risk behavior change
451
+ // (final whole-branch review finding): this JSON-parse pass applies
452
+ // to ANY matched route with a non-empty body, not just the new POST
453
+ // /api/v1/query — a GET request that (unusually) carries a
454
+ // non-JSON-parseable body now gets a clean 400 instead of the
455
+ // pre-M5 behavior (body silently drained, request processed
456
+ // normally). No real client sends a body on a GET here
457
+ // (frontend/src/lib/api-client.js's own fetch() calls never do), so
458
+ // this is not expected to affect any real caller — narrower
459
+ // per-method gating was judged unnecessary complexity for a case
460
+ // with no real-world traffic, but is a real, disclosed option if
461
+ // this ever needs revisiting.
462
+ let body;
463
+ if (bodyChunks.length > 0) {
464
+ try {
465
+ body = JSON.parse(Buffer.concat(bodyChunks).toString('utf8'));
466
+ } catch {
467
+ _sendJson(res, 400, { error: 'malformed JSON request body' });
468
+ finish(400);
469
+ return;
470
+ }
471
+ }
472
+
473
+ try {
474
+ const result = matched.handler(graph, match, body);
475
+ _sendJson(res, result.status, result.body);
476
+ finish(result.status);
477
+ } catch {
478
+ _sendJson(res, 500, { error: 'internal error' });
479
+ finish(500);
480
+ }
481
+ });
482
+
483
+ req.on('error', () => {
484
+ // A client that aborts mid-request — nothing to respond to.
485
+ aborted = true;
486
+ });
487
+ });
488
+
489
+ return new Promise((resolve, reject) => {
490
+ const onError = (err) => reject(err);
491
+ server.once('error', onError);
492
+ // Bind explicitly to 127.0.0.1 — never omit the host argument (that is
493
+ // NOT the same as 127.0.0.1 on every platform) and never 0.0.0.0.
494
+ server.listen(port, '127.0.0.1', () => {
495
+ server.removeListener('error', onError);
496
+ const actualPort = server.address().port;
497
+ _resetIdleTimer(); // start the idle clock even before the first request
498
+ resolve({ server, port: actualPort });
499
+ });
500
+ });
501
+ }
502
+
503
+
504
+ /***/ }),
505
+
506
+ /***/ 4268:
507
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
508
+
509
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
510
+ /* harmony export */ Yo: () => (/* binding */ handleScan),
511
+ /* harmony export */ Yu: () => (/* binding */ handleEdge),
512
+ /* harmony export */ d5: () => (/* binding */ handleNode),
513
+ /* harmony export */ fn: () => (/* binding */ handleGraph),
514
+ /* harmony export */ jg: () => (/* binding */ handleFlow),
515
+ /* harmony export */ rR: () => (/* binding */ handleQuery)
516
+ /* harmony export */ });
517
+ /* unused harmony export wrapResponse */
518
+ /* harmony import */ var _lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(859);
519
+ // routes.js — Milestone 3, sub-project Server, increment 1.
520
+ //
521
+ // Five pure GET-endpoint handlers, each `(graph, ...) -> {status, body}`.
522
+ // No req/res access anywhere in this file — that is what makes these
523
+ // handlers unit-testable without an HTTP layer at all. http-server.js is
524
+ // the only module that touches node:http and calls into these.
525
+ //
526
+ // Every response body is wrapped in `wrapResponse`, which adds the exact
527
+ // envelope fields PRD line 1326 names (quoted in the implementation plan):
528
+ // "base graph/snapshot digest, schema/extension versions, scope, coverage,
529
+ // limitations, and contributing canonical IDs."
530
+
531
+
532
+
533
+ /**
534
+ * Shared response envelope. Maps PRD line 1326's required fields onto the
535
+ * graph's own real fields:
536
+ * - digest -> graph.graphId (the base graph/snapshot digest)
537
+ * - schemaVersion -> graph.schemaVersion
538
+ * - extensions -> graph.extensions (schema/extension versions —
539
+ * today always `{}`; see schema.js)
540
+ * - scope -> graph.scope
541
+ * - coverage -> graph.coverage
542
+ * - limitations -> graph.limitations
543
+ * - canonicalIds -> see the design note below
544
+ *
545
+ * "contributing canonical IDs" design decision (disclosed per the plan):
546
+ * for `handleScan`/`handleGraph`, which describe the WHOLE graph rather
547
+ * than one entity, `canonicalIds` is `null` — the response body for
548
+ * `handleGraph` already IS the full nodes/edges/flows arrays, so echoing
549
+ * every id again here would be pure duplication with no informational
550
+ * gain, and for a large graph would materially bloat the response for
551
+ * zero benefit. For `handleNode`/`handleEdge`, `canonicalIds` is the
552
+ * single id the response is about. For `handleFlow`, `canonicalIds` is
553
+ * the flow's own id PLUS the node/edge ids that flow's evidence draws
554
+ * from (source, sink, edgeIds) — a flow is a derived record referencing
555
+ * several underlying entities, and naming all of them here is genuinely
556
+ * useful metadata a client would otherwise have to re-derive from the
557
+ * flow body itself.
558
+ */
559
+ function wrapResponse(data, graph, { canonicalIds = null } = {}) {
560
+ return {
561
+ digest: graph?.graphId ?? null,
562
+ schemaVersion: graph?.schemaVersion ?? null,
563
+ extensions: graph?.extensions ?? {},
564
+ scope: graph?.scope ?? null,
565
+ coverage: graph?.coverage ?? null,
566
+ limitations: graph?.limitations ?? [],
567
+ canonicalIds,
568
+ data,
569
+ };
570
+ }
571
+
572
+ function _findById(list, id) {
573
+ if (!Array.isArray(list)) return null;
574
+ return list.find((item) => item && item.id === id) ?? null;
575
+ }
576
+
577
+ /** Scan/graph metadata — NOT the full node/edge arrays. */
578
+ function handleScan(graph) {
579
+ const data = {
580
+ schemaVersion: graph?.schemaVersion ?? null,
581
+ graphId: graph?.graphId ?? null,
582
+ generatedAt: graph?.generatedAt ?? null,
583
+ scope: graph?.scope ?? null,
584
+ scanHealth: graph?.scanHealth ?? null,
585
+ coverage: graph?.coverage ?? null,
586
+ };
587
+ return { status: 200, body: wrapResponse(data, graph, { canonicalIds: null }) };
588
+ }
589
+
590
+ /** The full graph document, unfiltered. For a scoped/narrowed projection, use `handleQuery` (`POST /api/v1/query`, Milestone 5) below instead. */
591
+ function handleGraph(graph) {
592
+ return { status: 200, body: wrapResponse(graph, graph, { canonicalIds: null }) };
593
+ }
594
+
595
+ /**
596
+ * A deterministic typed projection query — Milestone 5's own
597
+ * `POST /api/v1/query`, the S2 endpoint `handleGraph`'s own header
598
+ * comment named and deferred. `filter` is the exact `{nodeIds, edgeIds}`
599
+ * shape `dataflow export --filter`/`exportGraphJSON` already use — reused
600
+ * via `_filterGraph`, never reimplemented. Final whole-branch review
601
+ * finding: `undefined` (filter omitted entirely) returns the WHOLE graph,
602
+ * identical to `handleGraph` — but `{}` (an empty, well-formed filter
603
+ * object) is NOT the same thing, and does NOT mean "no restriction": both
604
+ * `nodeIds`/`edgeIds` default to empty Sets inside `_filterGraph`, so `{}`
605
+ * narrows the graph down to EMPTY node/edge/flow/dataElement arrays. A
606
+ * caller that wants the whole graph must omit `filter` entirely, never
607
+ * pass `{}` meaning "everything." A malformed filter is a 400, never a
608
+ * thrown exception reaching the caller.
609
+ */
610
+ function handleQuery(graph, filter) {
611
+ const check = (0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__.validateFilterShape)(filter);
612
+ if (!check.valid) {
613
+ return { status: 400, body: { error: check.error } };
614
+ }
615
+ return { status: 200, body: wrapResponse((0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__/* ._filterGraph */ .e)(graph, filter), graph, { canonicalIds: null }) };
616
+ }
617
+
618
+ /** Look up one node by id. 404 with a clear body if not found. */
619
+ function handleNode(graph, id) {
620
+ const node = _findById(graph?.nodes, id);
621
+ if (!node) {
622
+ return { status: 404, body: wrapResponse({ error: `node not found: ${id}` }, graph, { canonicalIds: [] }) };
623
+ }
624
+ return { status: 200, body: wrapResponse(node, graph, { canonicalIds: [id] }) };
625
+ }
626
+
627
+ /** Look up one edge by id. 404 with a clear body if not found. */
628
+ function handleEdge(graph, id) {
629
+ const edge = _findById(graph?.edges, id);
630
+ if (!edge) {
631
+ return { status: 404, body: wrapResponse({ error: `edge not found: ${id}` }, graph, { canonicalIds: [] }) };
632
+ }
633
+ return { status: 200, body: wrapResponse(edge, graph, { canonicalIds: [id] }) };
634
+ }
635
+
636
+ /** Look up one flow by id. 404 with a clear body if not found. */
637
+ function handleFlow(graph, id) {
638
+ const flow = _findById(graph?.flows, id);
639
+ if (!flow) {
640
+ return { status: 404, body: wrapResponse({ error: `flow not found: ${id}` }, graph, { canonicalIds: [] }) };
641
+ }
642
+ const contributing = new Set([id]);
643
+ if (flow.source) contributing.add(flow.source);
644
+ if (flow.sink) contributing.add(flow.sink);
645
+ for (const eid of (flow.edgeIds || [])) contributing.add(eid);
646
+ return { status: 200, body: wrapResponse(flow, graph, { canonicalIds: [...contributing] }) };
647
+ }
648
+
649
+
650
+ /***/ }),
651
+
652
+ /***/ 6944:
653
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
654
+
655
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
656
+ /* harmony export */ HO: () => (/* binding */ isValidHost),
657
+ /* harmony export */ SK: () => (/* binding */ constantTimeEqual),
658
+ /* harmony export */ Xv: () => (/* binding */ CSP_HEADER_VALUE),
659
+ /* harmony export */ generateSessionToken: () => (/* binding */ generateSessionToken)
660
+ /* harmony export */ });
661
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
662
+ // security.js — Milestone 3, sub-project Server, increment 1.
663
+ //
664
+ // Security primitives for the `explore` loopback HTTP server: session-token
665
+ // generation/comparison and Host-header validation (the T2 DNS-rebinding
666
+ // defense — see docs/DATA_FLOW_EXPLORER_THREAT_MODEL.md). No node:http
667
+ // usage here — this module is pure and independently unit-testable.
668
+
669
+
670
+
671
+ /** 32 random bytes, hex-encoded (64 hex chars) — the per-session token. */
672
+ function generateSessionToken() {
673
+ return node_crypto__WEBPACK_IMPORTED_MODULE_0__.randomBytes(32).toString('hex');
674
+ }
675
+
676
+ /**
677
+ * Constant-time string comparison. Wraps crypto.timingSafeEqual, which
678
+ * THROWS on unequal-length buffers — guarded here the same way
679
+ * posture/integrity.js's verifyLastScan guards its own HMAC comparison
680
+ * (`stored.length !== expected.length`), which this function mirrors as
681
+ * its template. Never throws; returns false on any malformed input.
682
+ */
683
+ function constantTimeEqual(a, b) {
684
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
685
+ const bufA = Buffer.from(a, 'utf8');
686
+ const bufB = Buffer.from(b, 'utf8');
687
+ if (bufA.length !== bufB.length) return false;
688
+ try {
689
+ return node_crypto__WEBPACK_IMPORTED_MODULE_0__.timingSafeEqual(bufA, bufB);
690
+ } catch {
691
+ return false;
692
+ }
693
+ }
694
+
695
+ // Loopback hostnames this server ever legitimately answers to. Anything
696
+ // else in a Host header is either a stray/misconfigured client or a
697
+ // DNS-rebinding attempt from a hostile page — reject it (T2).
698
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
699
+
700
+ /**
701
+ * Validates a request's `Host` header against the port the server is
702
+ * actually listening on. Accepts only `127.0.0.1`/`localhost`/`::1`
703
+ * (`::1` written either bare or bracketed, `[::1]`), each carrying the
704
+ * expected port explicitly — this is the ENTIRE T2 (DNS rebinding)
705
+ * defense: a hostile page that points a fetch() at 127.0.0.1:<port> still
706
+ * sends the ATTACKER'S origin's Host header value (its own hostname), not
707
+ * "127.0.0.1", so this check rejects it regardless of same-origin policy
708
+ * quirks in any particular browser.
709
+ *
710
+ * A bare host with no port suffix is accepted ONLY when expectedPort is
711
+ * the default HTTP port (80) — browsers omit :80 for a default-port
712
+ * http:// request. In practice this server always listens on a
713
+ * non-default port, so that branch is effectively unreachable but is kept
714
+ * for RFC-correctness rather than special-cased away.
715
+ *
716
+ * @param {string|undefined} hostHeader
717
+ * @param {number} expectedPort
718
+ * @returns {boolean}
719
+ */
720
+ function isValidHost(hostHeader, expectedPort) {
721
+ if (typeof hostHeader !== 'string' || hostHeader.length === 0) return false;
722
+ if (typeof expectedPort !== 'number' || !Number.isInteger(expectedPort) || expectedPort <= 0) return false;
723
+
724
+ const host = hostHeader.trim();
725
+ if (host.length !== hostHeader.length) return false; // reject leading/trailing whitespace outright
726
+ if (/\s/.test(host)) return false; // no embedded whitespace/control chars in a well-formed Host header
727
+
728
+ let hostname;
729
+ let port = null;
730
+
731
+ if (host.startsWith('[')) {
732
+ // Bracketed IPv6 literal form: [::1] or [::1]:port
733
+ const closeIdx = host.indexOf(']');
734
+ if (closeIdx === -1) return false;
735
+ hostname = host.slice(1, closeIdx); // strip brackets
736
+ const rest = host.slice(closeIdx + 1);
737
+ if (rest.length) {
738
+ if (!rest.startsWith(':')) return false;
739
+ const portStr = rest.slice(1);
740
+ if (!/^\d+$/.test(portStr)) return false;
741
+ port = Number(portStr);
742
+ }
743
+ } else {
744
+ const parts = host.split(':');
745
+ if (parts.length === 1) {
746
+ hostname = parts[0];
747
+ } else if (parts.length === 2) {
748
+ hostname = parts[0];
749
+ if (!/^\d+$/.test(parts[1])) return false;
750
+ port = Number(parts[1]);
751
+ } else {
752
+ // More than one colon with no brackets: either a malformed header or
753
+ // a bare (unbracketed) IPv6 literal, which is not valid Host syntax
754
+ // (RFC 7230 §5.4 requires brackets for an IPv6 literal). Reject.
755
+ return false;
756
+ }
757
+ }
758
+
759
+ hostname = hostname.toLowerCase();
760
+ if (!LOOPBACK_HOSTS.has(hostname)) return false;
761
+
762
+ if (port === null) return expectedPort === 80;
763
+ return port === expectedPort;
764
+ }
765
+
766
+ // This server serves JSON only in this increment (no HTML, no inline
767
+ // script/style, no static assets) — `frontend/`'s own static assets are a
768
+ // later increment's (Wire's) job, not S1's. `default-src 'none'` is
769
+ // therefore the correct, maximally-restrictive policy: nothing this
770
+ // server ever returns should load or execute ANY sub-resource. Revisit
771
+ // only when a future increment starts serving HTML/JS itself.
772
+ const CSP_HEADER_VALUE = "default-src 'none'; frame-ancestors 'none'";
773
+
774
+
775
+ /***/ }),
776
+
777
+ /***/ 1185:
778
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
779
+
780
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
781
+ /* harmony export */ D: () => (/* binding */ resolveFrontendRoot)
782
+ /* harmony export */ });
783
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
784
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
785
+ // frontend-root.js — locates the Data Flow Explorer's `frontend/` assets
786
+ // (index.html/src/styles) regardless of whether this code is running:
787
+ // - unbundled, straight out of scanner/src/ or scanner/scripts/ (dev/test —
788
+ // frontend/ is a monorepo sibling of scanner/, 2-3 levels up), or
789
+ // - bundled by `npm run build` (ncc splits a dynamic import() into its own
790
+ // chunk file physically written into scanner/dist/, so import.meta.url
791
+ // inside that chunk reflects dist/'s own location, not the original
792
+ // source file's — a fixed relative-levels-up path that was correct for
793
+ // one depth breaks silently at the other), or
794
+ // - installed from the published npm package, where the build copies
795
+ // frontend/'s servable files into scanner/dist/frontend/ (a sibling of
796
+ // the chunk file itself, i.e. 0 levels up) — see scripts/copy-frontend.mjs.
797
+ //
798
+ // Rather than hardcode one of those depths (the bug this file fixes: every
799
+ // consumer used to hardcode the dev-only depth), search upward from the
800
+ // caller's own directory and take the first candidate that actually has a
801
+ // frontend/index.html on disk. Never guessed silently past that — a caller
802
+ // with no match anywhere gets a clear, actionable error instead of a
803
+ // downstream ENOENT/404 with no indication why.
804
+
805
+
806
+
807
+
808
+ const MAX_LEVELS_UP = 4;
809
+
810
+ /**
811
+ * @param {string} startDir - `path.dirname(fileURLToPath(import.meta.url))`
812
+ * of the CALLING module (not this file) — each caller's own bundled/
813
+ * unbundled location determines which candidate depth resolves.
814
+ * @returns {string} absolute path to a real `frontend/` directory containing
815
+ * `index.html`.
816
+ * @throws if no candidate directory up to MAX_LEVELS_UP contains one.
817
+ */
818
+ function resolveFrontendRoot(startDir) {
819
+ const tried = [];
820
+ for (let up = 0; up <= MAX_LEVELS_UP; up++) {
821
+ const candidate = node_path__WEBPACK_IMPORTED_MODULE_1__.resolve(startDir, ...Array(up).fill('..'), 'frontend');
822
+ tried.push(candidate);
823
+ // Every caller invokes resolveFrontendRoot() once, at module-load time,
824
+ // to compute a top-level FRONTEND_ROOT const (see static-assets.js /
825
+ // generate-html-report.mjs) — never per-request inside the explore
826
+ // server's request handler. At most MAX_LEVELS_UP+1 (5) sync stat calls
827
+ // at process startup is not a request-path DoS surface.
828
+ if (node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(node_path__WEBPACK_IMPORTED_MODULE_1__.join(candidate, 'index.html'))) return candidate; // agentic-security-ignore: dos-sync-io
829
+ }
830
+ throw new Error(
831
+ `resolveFrontendRoot: no frontend/index.html found searching up from ${startDir}. ` +
832
+ `Tried: ${tried.join(', ')}. If you're running from a source checkout, run \`npm run build\` ` +
833
+ `first (it copies frontend/ into scanner/dist/frontend/); if you're running the published ` +
834
+ `package, this indicates a packaging defect — report it.`
835
+ );
836
+ }
837
+
838
+
839
+ /***/ })
840
+
841
+ };