@nebulacomponents/citable 1.18.1 → 1.19.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 (193) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
  3. package/dist/universal/.agents/skills/citable/VERSION +1 -1
  4. package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
  5. package/dist/universal/.agents/skills/citable/commands/remediation.md +1 -1
  6. package/dist/universal/.agents/skills/citable/commands/sow.md +2 -2
  7. package/dist/universal/.agents/skills/citable/manifest.json +15 -10
  8. package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  9. package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
  10. package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  11. package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  12. package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
  13. package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
  14. package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +3 -2
  15. package/dist/universal/.claude/agents/citable/manifest.json +2 -2
  16. package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
  17. package/dist/universal/.claude/skills/citable/VERSION +1 -1
  18. package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
  19. package/dist/universal/.claude/skills/citable/commands/remediation.md +1 -1
  20. package/dist/universal/.claude/skills/citable/commands/sow.md +2 -2
  21. package/dist/universal/.claude/skills/citable/manifest.json +15 -10
  22. package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  23. package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
  24. package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  25. package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  26. package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
  27. package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
  28. package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +3 -2
  29. package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
  30. package/dist/universal/.cursor/skills/citable/VERSION +1 -1
  31. package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
  32. package/dist/universal/.cursor/skills/citable/commands/remediation.md +1 -1
  33. package/dist/universal/.cursor/skills/citable/commands/sow.md +2 -2
  34. package/dist/universal/.cursor/skills/citable/manifest.json +15 -10
  35. package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  36. package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
  37. package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  38. package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  39. package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
  40. package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
  41. package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +3 -2
  42. package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
  43. package/dist/universal/.gemini/skills/citable/VERSION +1 -1
  44. package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
  45. package/dist/universal/.gemini/skills/citable/commands/remediation.md +1 -1
  46. package/dist/universal/.gemini/skills/citable/commands/sow.md +2 -2
  47. package/dist/universal/.gemini/skills/citable/manifest.json +15 -10
  48. package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  49. package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
  50. package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  51. package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  52. package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
  53. package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
  54. package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +3 -2
  55. package/dist/universal/.github/skills/citable/SKILL.md +1 -1
  56. package/dist/universal/.github/skills/citable/VERSION +1 -1
  57. package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
  58. package/dist/universal/.github/skills/citable/commands/remediation.md +1 -1
  59. package/dist/universal/.github/skills/citable/commands/sow.md +2 -2
  60. package/dist/universal/.github/skills/citable/manifest.json +15 -10
  61. package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  62. package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
  63. package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  64. package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  65. package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
  66. package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
  67. package/dist/universal/.github/skills/citable/schemas/sow.schema.json +3 -2
  68. package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
  69. package/dist/universal/.kiro/skills/citable/VERSION +1 -1
  70. package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
  71. package/dist/universal/.kiro/skills/citable/commands/remediation.md +1 -1
  72. package/dist/universal/.kiro/skills/citable/commands/sow.md +2 -2
  73. package/dist/universal/.kiro/skills/citable/manifest.json +15 -10
  74. package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  75. package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
  76. package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  77. package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  78. package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
  79. package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
  80. package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +3 -2
  81. package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
  82. package/dist/universal/.opencode/skills/citable/VERSION +1 -1
  83. package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
  84. package/dist/universal/.opencode/skills/citable/commands/remediation.md +1 -1
  85. package/dist/universal/.opencode/skills/citable/commands/sow.md +2 -2
  86. package/dist/universal/.opencode/skills/citable/manifest.json +15 -10
  87. package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  88. package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
  89. package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  90. package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  91. package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
  92. package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
  93. package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +3 -2
  94. package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
  95. package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
  96. package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
  97. package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +1 -1
  98. package/dist/universal/.pi/agent/skills/citable/commands/sow.md +2 -2
  99. package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -10
  100. package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  101. package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
  102. package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  103. package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  104. package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
  105. package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
  106. package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +3 -2
  107. package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
  108. package/dist/universal/.qoder/skills/citable/VERSION +1 -1
  109. package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
  110. package/dist/universal/.qoder/skills/citable/commands/remediation.md +1 -1
  111. package/dist/universal/.qoder/skills/citable/commands/sow.md +2 -2
  112. package/dist/universal/.qoder/skills/citable/manifest.json +15 -10
  113. package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  114. package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
  115. package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  116. package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  117. package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
  118. package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
  119. package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +3 -2
  120. package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
  121. package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
  122. package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
  123. package/dist/universal/.rovodev/skills/citable/commands/remediation.md +1 -1
  124. package/dist/universal/.rovodev/skills/citable/commands/sow.md +2 -2
  125. package/dist/universal/.rovodev/skills/citable/manifest.json +15 -10
  126. package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  127. package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
  128. package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  129. package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  130. package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
  131. package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
  132. package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +3 -2
  133. package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
  134. package/dist/universal/.trae/skills/citable/VERSION +1 -1
  135. package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
  136. package/dist/universal/.trae/skills/citable/commands/remediation.md +1 -1
  137. package/dist/universal/.trae/skills/citable/commands/sow.md +2 -2
  138. package/dist/universal/.trae/skills/citable/manifest.json +15 -10
  139. package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  140. package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
  141. package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  142. package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  143. package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
  144. package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
  145. package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +3 -2
  146. package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
  147. package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
  148. package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
  149. package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +1 -1
  150. package/dist/universal/.trae-cn/skills/citable/commands/sow.md +2 -2
  151. package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -10
  152. package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
  153. package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
  154. package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
  155. package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -0
  156. package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
  157. package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
  158. package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +3 -2
  159. package/dist/universal/manifest.json +184 -124
  160. package/package.json +2 -1
  161. package/schemas/browser-evidence-plan.schema.json +186 -3
  162. package/schemas/browser-network-events.schema.json +142 -0
  163. package/schemas/browser-runtime-events.schema.json +112 -0
  164. package/schemas/browser-state-observations.schema.json +97 -0
  165. package/schemas/cro-report.schema.json +112 -0
  166. package/schemas/search-report.schema.json +99 -0
  167. package/schemas/sow.schema.json +3 -2
  168. package/skill/SKILL.md +1 -1
  169. package/skill/commands/artifact-interchange.md +5 -0
  170. package/skill/commands/remediation.md +1 -1
  171. package/skill/commands/sow.md +2 -2
  172. package/src/analysis/behavioral.js +5 -4
  173. package/src/analysis/eeat.js +10 -9
  174. package/src/analysis/offpage.js +19 -36
  175. package/src/analysis/readiness.js +8 -2
  176. package/src/artifacts/verifyCustomerArtifact.js +283 -0
  177. package/src/cli/index.js +8 -1
  178. package/src/observations/browser/correlation.js +83 -0
  179. package/src/observations/browser/networkCollector.js +413 -0
  180. package/src/observations/browser/runtimeEventCollector.js +337 -0
  181. package/src/observations/browser/sanitizer.js +322 -0
  182. package/src/observations/browser/stateCollector.js +386 -0
  183. package/src/observations/browserJourney.js +131 -3
  184. package/src/reporting/executiveCroReport.js +393 -229
  185. package/src/reporting/executiveExport.js +6 -2
  186. package/src/reporting/executiveSearchReport.js +519 -271
  187. package/src/shared/domainUtils.js +140 -0
  188. package/src/shared/epistemicStatus.js +111 -0
  189. package/src/shared/evidenceSourceResolver.js +286 -0
  190. package/src/shared/htmlEscape.js +65 -0
  191. package/src/shared/runPackageVerifier.js +153 -0
  192. package/src/sow/admissibilityGate.js +20 -2
  193. package/src/sow/generateSow.js +125 -159
@@ -0,0 +1,413 @@
1
+ import { sha256 } from '../../shared/io.js';
2
+ import {
3
+ sanitizeHeaders,
4
+ sanitizeQueryParams,
5
+ sanitizePayload,
6
+ isSensitiveKey,
7
+ isSensitiveValue,
8
+ REDACTED_MARKER,
9
+ } from './sanitizer.js';
10
+
11
+ function matchHost(host, pattern) {
12
+ if (pattern === '*' || host === pattern) return true;
13
+ if (pattern.startsWith('*.')) {
14
+ const root = pattern.slice(2);
15
+ return host === root || host.endsWith('.' + root);
16
+ }
17
+ return false;
18
+ }
19
+
20
+ export class NetworkCollector {
21
+ constructor(options = {}) {
22
+ const {
23
+ networkPolicy = {},
24
+ targetUrl,
25
+ correlationTracker,
26
+ planId,
27
+ profileId,
28
+ } = options;
29
+
30
+ this.policy = networkPolicy;
31
+ this.enabled = Boolean(this.policy.enabled);
32
+ this.planId = planId || 'plan-unspecified';
33
+ this.profileId = profileId || 'profile-unspecified';
34
+ this.tracker = correlationTracker;
35
+
36
+ const parsedTarget = new URL(targetUrl);
37
+ this.targetHost = parsedTarget.hostname;
38
+ this.targetOrigin = parsedTarget.origin;
39
+
40
+ this.allowedHostPatterns = this.policy.allowed_host_patterns || [this.targetHost];
41
+ this.allowedMethods = this.policy.allowed_methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
42
+ this.allowedResourceTypes = this.policy.allowed_resource_types || [
43
+ 'document', 'stylesheet', 'image', 'media', 'font', 'script', 'texttrack', 'xhr', 'fetch', 'eventsource', 'websocket', 'manifest', 'other',
44
+ ];
45
+ this.captureRequestHeaders = this.policy.capture_request_headers || null;
46
+ this.captureResponseHeaders = this.policy.capture_response_headers || null;
47
+ this.captureQueryParams = this.policy.capture_query_params || null;
48
+ this.capturePayload = this.policy.capture_payload || { request_body: false, response_body: false, hash_only: false };
49
+ this.maxEvents = Math.min(this.policy.max_events || 200, 1000);
50
+ this.maxFieldSize = Math.min(this.policy.max_field_size || 1024, 8192);
51
+
52
+ this.events = [];
53
+ this.transactionMap = new Map(); // playwright request -> { txId, initiatedCorrelation, ... }
54
+ this.summary = {
55
+ total_requests: 0,
56
+ successful_responses: 0,
57
+ failed_requests: 0,
58
+ filtered_requests: 0,
59
+ in_flight_requests: 0,
60
+ collector_status: 'complete',
61
+ truncated: false,
62
+ truncation_reason: null,
63
+ status_codes: {},
64
+ };
65
+ this.txCounter = 0;
66
+ }
67
+
68
+ isHostAllowed(hostname) {
69
+ if (!hostname) return false;
70
+ return this.allowedHostPatterns.some((pattern) => matchHost(hostname, pattern));
71
+ }
72
+
73
+ attachToPage(page) {
74
+ if (!this.enabled) return;
75
+
76
+ page.on('request', async (request) => {
77
+ await this.handleRequest(request);
78
+ });
79
+
80
+ page.on('response', async (response) => {
81
+ await this.handleResponse(response);
82
+ });
83
+
84
+ page.on('requestfailed', async (request) => {
85
+ await this.handleRequestFailed(request);
86
+ });
87
+ }
88
+
89
+ async handleRequest(request) {
90
+ const urlStr = request.url();
91
+ let parsedUrl;
92
+ try {
93
+ parsedUrl = new URL(urlStr);
94
+ } catch {
95
+ return;
96
+ }
97
+
98
+ // Filter by allowed host
99
+ const hostAllowed = this.isHostAllowed(parsedUrl.hostname);
100
+ const methodAllowed = this.allowedMethods.includes(request.method().toUpperCase());
101
+ const resourceType = request.resourceType();
102
+ const resourceTypeAllowed = this.allowedResourceTypes.includes(resourceType);
103
+
104
+ if (!hostAllowed || !methodAllowed || !resourceTypeAllowed) {
105
+ this.summary.filtered_requests++;
106
+ return;
107
+ }
108
+
109
+ this.summary.total_requests++;
110
+ this.txCounter++;
111
+ const txId = `TX-${String(this.txCounter).padStart(4, '0')}`;
112
+ const initiatedCorrelation = this.tracker.getCorrelation();
113
+ const seq = this.tracker.nextSequence();
114
+ const timing = this.tracker.timing();
115
+
116
+ this.transactionMap.set(request, {
117
+ txId,
118
+ initiatedCorrelation,
119
+ seq,
120
+ });
121
+
122
+ if (this.events.length >= this.maxEvents) {
123
+ this.summary.truncated = true;
124
+ this.summary.truncation_reason = 'max_events_exceeded';
125
+ return;
126
+ }
127
+
128
+ // Headers sanitization (deny-by-default)
129
+ const rawHeaders = request.headers();
130
+ let sanitizedHeaders = null;
131
+ let headersStatus = 'not_requested';
132
+ let headerRedacted = 0;
133
+ if (this.captureRequestHeaders && Array.isArray(this.captureRequestHeaders) && this.captureRequestHeaders.length > 0) {
134
+ const res = sanitizeHeaders(rawHeaders, this.captureRequestHeaders);
135
+ sanitizedHeaders = res.headers;
136
+ headersStatus = res.status;
137
+ headerRedacted = res.redactedCount;
138
+ }
139
+
140
+ // Query params sanitization (deny-by-default)
141
+ let sanitizedQuery = null;
142
+ let queryStatus = 'not_requested';
143
+ let queryRedacted = 0;
144
+ let queryFiltered = [];
145
+ if (this.captureQueryParams && Array.isArray(this.captureQueryParams) && this.captureQueryParams.length > 0) {
146
+ const res = sanitizeQueryParams(urlStr, this.captureQueryParams);
147
+ sanitizedQuery = res.query;
148
+ queryStatus = res.status;
149
+ queryRedacted = res.redactedCount;
150
+ queryFiltered = res.filteredFields;
151
+ }
152
+
153
+ // Request payload handling (deny-by-default)
154
+ let payloadHash = null;
155
+ let payloadBytes = null;
156
+ let payloadFields = null;
157
+ let payloadStatus = 'not_requested';
158
+ let payloadRedacted = 0;
159
+ let payloadFiltered = [];
160
+
161
+ const isBodyRequested = Boolean(this.capturePayload.request_body);
162
+ const isHashRequested = Boolean(this.capturePayload.hash_only);
163
+
164
+ if (isBodyRequested || isHashRequested) {
165
+ const postData = request.postData();
166
+ if (postData != null && postData !== '') {
167
+ const sanitized = sanitizePayload(postData, {
168
+ allowedFields: this.capturePayload.allowed_json_fields || null,
169
+ hashOnly: isHashRequested && !isBodyRequested,
170
+ maxBytes: this.capturePayload.max_bytes || 4096,
171
+ });
172
+ payloadHash = sanitized.hash;
173
+ payloadBytes = sanitized.bytes;
174
+ payloadFields = typeof sanitized.sanitized === 'object' && sanitized.sanitized !== null ? sanitized.sanitized : null;
175
+ payloadStatus = sanitized.status;
176
+ payloadRedacted = sanitized.redactedCount;
177
+ payloadFiltered = sanitized.filteredFields;
178
+ } else {
179
+ payloadStatus = 'unavailable';
180
+ }
181
+ }
182
+
183
+ const totalRedacted = headerRedacted + queryRedacted + payloadRedacted;
184
+ const allFilteredFields = [...queryFiltered, ...payloadFiltered];
185
+
186
+ const eventRecord = {
187
+ sequence: seq,
188
+ transaction_id: txId,
189
+ direction: 'request',
190
+ phase: 'request_sent',
191
+ timing,
192
+ correlation: initiatedCorrelation,
193
+ request: {
194
+ url: parsedUrl.origin + parsedUrl.pathname,
195
+ method: request.method().toUpperCase(),
196
+ scheme: parsedUrl.protocol.replace(':', ''),
197
+ host: parsedUrl.hostname,
198
+ path: parsedUrl.pathname,
199
+ resource_type: resourceType,
200
+ query: sanitizedQuery,
201
+ query_status: queryStatus,
202
+ headers: sanitizedHeaders,
203
+ headers_status: headersStatus,
204
+ payload_hash: payloadHash,
205
+ payload_bytes: payloadBytes,
206
+ payload_fields: payloadFields,
207
+ payload_status: payloadStatus,
208
+ },
209
+ response: null,
210
+ failure: null,
211
+ disposition: {
212
+ capture_status: totalRedacted > 0 ? 'redacted' : 'captured',
213
+ redacted_count: totalRedacted,
214
+ filtered_fields: allFilteredFields,
215
+ },
216
+ };
217
+
218
+ this.events.push(eventRecord);
219
+ }
220
+
221
+ async handleResponse(response) {
222
+ const request = response.request();
223
+ const tx = this.transactionMap.get(request);
224
+ if (!tx) return; // Request was filtered or not tracked
225
+
226
+ const status = response.status();
227
+ const statusKey = String(status);
228
+ this.summary.status_codes[statusKey] = (this.summary.status_codes[statusKey] || 0) + 1;
229
+
230
+ if (status >= 200 && status < 400) {
231
+ this.summary.successful_responses++;
232
+ }
233
+
234
+ if (this.events.length >= this.maxEvents) {
235
+ this.summary.truncated = true;
236
+ this.summary.truncation_reason = 'max_events_exceeded';
237
+ return;
238
+ }
239
+
240
+ const seq = this.tracker.nextSequence();
241
+ const timing = this.tracker.timing();
242
+ const completedCorrelation = this.tracker.correlateCompletion(tx.initiatedCorrelation);
243
+
244
+ // Response headers sanitization (deny-by-default)
245
+ const rawHeaders = response.headers();
246
+ let sanitizedHeaders = null;
247
+ let headersStatus = 'not_requested';
248
+ let headerRedacted = 0;
249
+ if (this.captureResponseHeaders && Array.isArray(this.captureResponseHeaders) && this.captureResponseHeaders.length > 0) {
250
+ const res = sanitizeHeaders(rawHeaders, this.captureResponseHeaders);
251
+ sanitizedHeaders = res.headers;
252
+ headersStatus = res.status;
253
+ headerRedacted = res.redactedCount;
254
+ }
255
+
256
+ let mimeType = response.headers()['content-type'] || null;
257
+ if (mimeType) {
258
+ mimeType = mimeType.split(';')[0].trim();
259
+ }
260
+
261
+ // Response body handling (deny-by-default)
262
+ let bodyHash = null;
263
+ let bodyBytes = null;
264
+ let bodyFields = null;
265
+ let bodyStatus = 'not_requested';
266
+
267
+ const isRespBodyRequested = Boolean(this.capturePayload.response_body);
268
+ const isRespHashRequested = Boolean(this.capturePayload.hash_only);
269
+
270
+ if (isRespBodyRequested || isRespHashRequested) {
271
+ try {
272
+ const buffer = await response.body();
273
+ if (buffer && buffer.length > 0) {
274
+ const sanitized = sanitizePayload(buffer, {
275
+ allowedFields: this.capturePayload.allowed_json_fields || null,
276
+ hashOnly: isRespHashRequested && !isRespBodyRequested,
277
+ maxBytes: this.capturePayload.max_bytes || 4096,
278
+ });
279
+ bodyHash = sanitized.hash;
280
+ bodyBytes = sanitized.bytes;
281
+ bodyFields = typeof sanitized.sanitized === 'object' && sanitized.sanitized !== null ? sanitized.sanitized : null;
282
+ bodyStatus = sanitized.status;
283
+ } else {
284
+ bodyStatus = 'unavailable';
285
+ }
286
+ } catch {
287
+ bodyStatus = 'unavailable';
288
+ }
289
+ }
290
+
291
+ const isRedirect = status >= 300 && status < 400;
292
+ const redirectUrl = isRedirect ? response.headers()['location'] || null : null;
293
+
294
+ const parsedUrl = new URL(request.url());
295
+
296
+ const eventRecord = {
297
+ sequence: seq,
298
+ transaction_id: tx.txId,
299
+ direction: 'response',
300
+ phase: isRedirect ? 'redirect' : 'response_received',
301
+ timing,
302
+ correlation: completedCorrelation,
303
+ request: {
304
+ url: parsedUrl.origin + parsedUrl.pathname,
305
+ method: request.method().toUpperCase(),
306
+ scheme: parsedUrl.protocol.replace(':', ''),
307
+ host: parsedUrl.hostname,
308
+ path: parsedUrl.pathname,
309
+ resource_type: request.resourceType(),
310
+ query: null,
311
+ query_status: 'not_requested',
312
+ headers: null,
313
+ headers_status: 'not_requested',
314
+ payload_hash: null,
315
+ payload_bytes: null,
316
+ payload_fields: null,
317
+ payload_status: 'not_requested',
318
+ },
319
+ response: {
320
+ status,
321
+ status_text: response.statusText(),
322
+ headers: sanitizedHeaders,
323
+ headers_status: headersStatus,
324
+ mime_type: mimeType,
325
+ body_hash: bodyHash,
326
+ body_bytes: bodyBytes,
327
+ body_fields: bodyFields,
328
+ body_status: bodyStatus,
329
+ redirect_url: redirectUrl,
330
+ from_cache: response.fromServiceWorker ? true : false,
331
+ },
332
+ failure: null,
333
+ disposition: {
334
+ capture_status: headerRedacted > 0 ? 'redacted' : 'captured',
335
+ redacted_count: headerRedacted,
336
+ filtered_fields: [],
337
+ },
338
+ };
339
+
340
+ this.events.push(eventRecord);
341
+ }
342
+
343
+ async handleRequestFailed(request) {
344
+ const tx = this.transactionMap.get(request);
345
+ this.summary.failed_requests++;
346
+
347
+ if (!tx || this.events.length >= this.maxEvents) {
348
+ if (this.events.length >= this.maxEvents) {
349
+ this.summary.truncated = true;
350
+ this.summary.truncation_reason = 'max_events_exceeded';
351
+ }
352
+ return;
353
+ }
354
+
355
+ const seq = this.tracker.nextSequence();
356
+ const timing = this.tracker.timing();
357
+ const completedCorrelation = this.tracker.correlateCompletion(tx.initiatedCorrelation);
358
+ const failureText = request.failure()?.errorText || 'unknown';
359
+ const parsedUrl = new URL(request.url());
360
+
361
+ const eventRecord = {
362
+ sequence: seq,
363
+ transaction_id: tx.txId,
364
+ direction: 'response',
365
+ phase: 'failed',
366
+ timing,
367
+ correlation: completedCorrelation,
368
+ request: {
369
+ url: parsedUrl.origin + parsedUrl.pathname,
370
+ method: request.method().toUpperCase(),
371
+ scheme: parsedUrl.protocol.replace(':', ''),
372
+ host: parsedUrl.hostname,
373
+ path: parsedUrl.pathname,
374
+ resource_type: request.resourceType(),
375
+ query: null,
376
+ query_status: 'not_requested',
377
+ headers: null,
378
+ headers_status: 'not_requested',
379
+ payload_hash: null,
380
+ payload_bytes: null,
381
+ payload_fields: null,
382
+ payload_status: 'not_requested',
383
+ },
384
+ response: null,
385
+ failure: {
386
+ error_text: failureText,
387
+ },
388
+ disposition: {
389
+ capture_status: 'error',
390
+ redacted_count: 0,
391
+ filtered_fields: [],
392
+ },
393
+ };
394
+
395
+ this.events.push(eventRecord);
396
+ }
397
+
398
+ toArtifact() {
399
+ const inFlight = Math.max(0, this.summary.total_requests - this.summary.successful_responses - this.summary.failed_requests);
400
+ this.summary.in_flight_requests = inFlight;
401
+ this.summary.collector_status = this.summary.truncated ? 'truncated' : 'complete';
402
+
403
+ return {
404
+ schema_version: 1,
405
+ plan_id: this.planId,
406
+ profile_id: this.profileId,
407
+ collected_at: this.tracker.timing().timestamp,
408
+ capture_policy: this.policy,
409
+ summary: this.summary,
410
+ events: this.events,
411
+ };
412
+ }
413
+ }