@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.0

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