@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,337 @@
1
+ import { safeJsonClone, isSensitiveKey, isSensitiveValue } from './sanitizer.js';
2
+
3
+ export class RuntimeEventCollector {
4
+ constructor(options = {}) {
5
+ const {
6
+ runtimeEventPolicy = {},
7
+ correlationTracker,
8
+ planId,
9
+ profileId,
10
+ } = options;
11
+
12
+ this.policy = runtimeEventPolicy;
13
+ this.enabled = Boolean(this.policy.enabled);
14
+ this.planId = planId || 'plan-unspecified';
15
+ this.profileId = profileId || 'profile-unspecified';
16
+ this.tracker = correlationTracker;
17
+
18
+ this.sources = this.policy.sources || [];
19
+ this.allowedEventNames = this.policy.allowed_event_names ? new Set(this.policy.allowed_event_names) : null;
20
+ this.allowedFields = this.policy.allowed_fields || null;
21
+ this.hashFields = this.policy.hash_fields || null;
22
+ this.redactFields = this.policy.redact_fields || null;
23
+ this.maxEvents = Math.min(this.policy.max_events || 200, 1000);
24
+ this.maxPayloadDepth = Math.min(this.policy.max_payload_depth || 5, 10);
25
+ this.maxFieldSize = Math.min(this.policy.max_field_size || 1024, 4096);
26
+
27
+ this.events = [];
28
+ this.eventCountsByName = {};
29
+ this.eventCountsBySource = {};
30
+ this.summary = {
31
+ total_events: 0,
32
+ filtered_events: 0,
33
+ by_source: {},
34
+ by_event_name: {},
35
+ collector_status: 'complete',
36
+ truncated: false,
37
+ truncation_reason: null,
38
+ collector_health: {
39
+ status: 'active',
40
+ queue_replaced: false,
41
+ push_replaced: false,
42
+ polling_fallback_used: false,
43
+ supported_contexts: ['top_level_main_frame'],
44
+ unsupported_contexts: ['cross_origin_iframes', 'web_workers', 'service_workers'],
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Generates initialization script to be added to browser context or page.
51
+ */
52
+ getInitScript() {
53
+ if (!this.enabled || !this.sources.length) return null;
54
+ const sourcesConfig = JSON.stringify(this.sources);
55
+
56
+ return `
57
+ (function() {
58
+ window.__citable_events = window.__citable_events || [];
59
+ window.__citable_health = window.__citable_health || {
60
+ queue_replaced: false,
61
+ push_replaced: false,
62
+ polling_fallback_used: false,
63
+ };
64
+ const sources = ${sourcesConfig};
65
+
66
+ function safeExtract(obj, depth, seen) {
67
+ if (depth <= 0) return '[DEPTH_LIMIT]';
68
+ if (obj === null || obj === undefined) return obj;
69
+ const t = typeof obj;
70
+ if (t === 'number' || t === 'boolean' || t === 'string') return obj;
71
+ if (t !== 'object') return String(obj);
72
+ if (seen.has(obj)) return '[CIRCULAR]';
73
+ seen.add(obj);
74
+
75
+ if (Array.isArray(obj)) {
76
+ return obj.map(function(item) { return safeExtract(item, depth - 1, seen); });
77
+ }
78
+
79
+ const res = {};
80
+ const keys = Object.keys(obj);
81
+ for (var i = 0; i < keys.length; i++) {
82
+ var k = keys[i];
83
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
84
+ try {
85
+ res[k] = safeExtract(obj[k], depth - 1, seen);
86
+ } catch (err) {
87
+ res[k] = '[GETTER_ERROR]';
88
+ }
89
+ }
90
+ return res;
91
+ }
92
+
93
+ for (var i = 0; i < sources.length; i++) {
94
+ var src = sources[i];
95
+ if (src.type === 'global_array' && src.global_name) {
96
+ var propName = src.global_name.replace(/^window\\./, '');
97
+
98
+ (function(sourceConfig, prop) {
99
+ var currentArray = window[prop];
100
+ if (!Array.isArray(currentArray)) {
101
+ currentArray = [];
102
+ window[prop] = currentArray;
103
+ }
104
+
105
+ for (var j = 0; j < currentArray.length; j++) {
106
+ try {
107
+ window.__citable_events.push({
108
+ source_id: sourceConfig.source_id,
109
+ payload: safeExtract(currentArray[j], 5, new WeakSet()),
110
+ ts: Date.now()
111
+ });
112
+ } catch (e) {}
113
+ }
114
+ currentArray.__citable_drained_count = currentArray.length;
115
+
116
+ function wrapPush(arr) {
117
+ var origPush = arr.push;
118
+ arr.push = function(...items) {
119
+ for (var k = 0; k < items.length; k++) {
120
+ try {
121
+ window.__citable_events.push({
122
+ source_id: sourceConfig.source_id,
123
+ payload: safeExtract(items[k], 5, new WeakSet()),
124
+ ts: Date.now()
125
+ });
126
+ } catch (e) {}
127
+ }
128
+ if (typeof origPush === 'function') {
129
+ return origPush.apply(this, items);
130
+ }
131
+ };
132
+ }
133
+
134
+ wrapPush(currentArray);
135
+
136
+ try {
137
+ var storedArray = currentArray;
138
+ Object.defineProperty(window, prop, {
139
+ configurable: true,
140
+ enumerable: true,
141
+ get: function() { return storedArray; },
142
+ set: function(newVal) {
143
+ window.__citable_health.queue_replaced = true;
144
+ storedArray = newVal;
145
+ if (Array.isArray(newVal)) {
146
+ wrapPush(newVal);
147
+ for (var m = 0; m < newVal.length; m++) {
148
+ try {
149
+ window.__citable_events.push({
150
+ source_id: sourceConfig.source_id,
151
+ payload: safeExtract(newVal[m], 5, new WeakSet()),
152
+ ts: Date.now()
153
+ });
154
+ } catch (e) {}
155
+ }
156
+ newVal.__citable_drained_count = newVal.length;
157
+ }
158
+ }
159
+ });
160
+ } catch (e) {}
161
+ })(src, propName);
162
+
163
+ } else if (src.type === 'window_event' && src.event_name) {
164
+ (function(sourceConfig) {
165
+ window.addEventListener(sourceConfig.event_name, function(e) {
166
+ try {
167
+ window.__citable_events.push({
168
+ source_id: sourceConfig.source_id,
169
+ payload: {
170
+ type: e.type,
171
+ detail: safeExtract(e.detail, 5, new WeakSet()) || null,
172
+ },
173
+ ts: Date.now()
174
+ });
175
+ } catch (err) {}
176
+ }, true);
177
+ })(src);
178
+ }
179
+ }
180
+ })();
181
+ `;
182
+ }
183
+
184
+ async installOnPage(page) {
185
+ if (!this.enabled || !this.sources.length) return;
186
+ const script = this.getInitScript();
187
+ if (!script) return;
188
+ try {
189
+ await page.evaluate(script);
190
+ } catch {
191
+ // Ignored if page is not ready yet
192
+ }
193
+ }
194
+
195
+ async drainEvents(page) {
196
+ if (!this.enabled || !this.sources.length) return;
197
+
198
+ let drainResult = null;
199
+ try {
200
+ drainResult = await page.evaluate((sources) => {
201
+ const events = window.__citable_events ? window.__citable_events.splice(0) : [];
202
+ const health = window.__citable_health || {
203
+ queue_replaced: false,
204
+ push_replaced: false,
205
+ polling_fallback_used: false,
206
+ };
207
+
208
+ // Fallback polling for arrays where push was replaced or unhooked
209
+ for (const src of sources) {
210
+ if (src.type === 'global_array' && src.global_name) {
211
+ const prop = src.global_name.replace(/^window\./, '');
212
+ const arr = window[prop];
213
+ if (Array.isArray(arr)) {
214
+ const startIdx = arr.__citable_drained_count || 0;
215
+ if (arr.length > startIdx) {
216
+ health.polling_fallback_used = true;
217
+ for (let i = startIdx; i < arr.length; i++) {
218
+ events.push({
219
+ source_id: src.source_id,
220
+ payload: arr[i],
221
+ ts: Date.now(),
222
+ });
223
+ }
224
+ arr.__citable_drained_count = arr.length;
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return { events, health };
231
+ }, this.sources);
232
+ } catch {
233
+ return;
234
+ }
235
+
236
+ const eventsList = Array.isArray(drainResult)
237
+ ? drainResult
238
+ : (drainResult && Array.isArray(drainResult.events) ? drainResult.events : []);
239
+
240
+ if (!eventsList || eventsList.length === 0) return;
241
+
242
+ if (drainResult && !Array.isArray(drainResult) && drainResult.health) {
243
+ if (drainResult.health.queue_replaced) this.summary.collector_health.queue_replaced = true;
244
+ if (drainResult.health.push_replaced) this.summary.collector_health.push_replaced = true;
245
+ if (drainResult.health.polling_fallback_used) this.summary.collector_health.polling_fallback_used = true;
246
+ }
247
+
248
+ for (const raw of eventsList) {
249
+ if (this.events.length >= this.maxEvents) {
250
+ this.summary.truncated = true;
251
+ this.summary.truncation_reason = 'max_events_exceeded';
252
+ break;
253
+ }
254
+
255
+ const sourceId = raw.source_id || 'unknown';
256
+ const sourceConfig = this.sources.find((s) => s.source_id === sourceId) || {};
257
+ const payload = raw.payload;
258
+
259
+ // Extract event name
260
+ let eventName = 'unnamed_event';
261
+ if (typeof payload === 'object' && payload !== null) {
262
+ const keyName = sourceConfig.event_name_key || 'event';
263
+ if (typeof payload[keyName] === 'string') {
264
+ eventName = payload[keyName];
265
+ } else if (typeof payload.type === 'string') {
266
+ eventName = payload.type;
267
+ } else if (sourceConfig.event_name) {
268
+ eventName = sourceConfig.event_name;
269
+ }
270
+ } else if (typeof payload === 'string') {
271
+ eventName = payload;
272
+ }
273
+
274
+ // Check allowed event names
275
+ const isAllowed = !this.allowedEventNames || this.allowedEventNames.has(eventName);
276
+ if (!isAllowed) {
277
+ this.summary.filtered_events++;
278
+ continue; // Filtered out
279
+ }
280
+
281
+ this.eventCountsByName[eventName] = (this.eventCountsByName[eventName] || 0) + 1;
282
+ this.eventCountsBySource[sourceId] = (this.eventCountsBySource[sourceId] || 0) + 1;
283
+ const occurrenceIndex = this.eventCountsByName[eventName];
284
+
285
+ const seq = this.tracker.nextSequence();
286
+ const timing = this.tracker.timing();
287
+ const correlation = this.tracker.getCorrelation();
288
+
289
+ // Sanitize payload
290
+ const stats = { redactedCount: 0, filteredFields: [] };
291
+ const sanitizedPayload = typeof payload === 'object' && payload !== null
292
+ ? safeJsonClone(payload, {
293
+ maxDepth: this.maxPayloadDepth,
294
+ maxFieldSize: this.maxFieldSize,
295
+ allowedFields: this.allowedFields,
296
+ hashFields: this.hashFields,
297
+ redactFields: this.redactFields,
298
+ stats,
299
+ })
300
+ : { value: String(payload) };
301
+
302
+ const eventRecord = {
303
+ sequence: seq,
304
+ source_id: sourceId,
305
+ event_name: eventName,
306
+ occurrence_index: occurrenceIndex,
307
+ timing,
308
+ correlation,
309
+ payload: sanitizedPayload,
310
+ disposition: {
311
+ capture_status: stats.redactedCount > 0 ? 'redacted' : 'captured',
312
+ redacted_count: stats.redactedCount,
313
+ filtered_fields: stats.filteredFields,
314
+ },
315
+ };
316
+
317
+ this.events.push(eventRecord);
318
+ this.summary.total_events++;
319
+ }
320
+
321
+ this.summary.by_source = { ...this.eventCountsBySource };
322
+ this.summary.by_event_name = { ...this.eventCountsByName };
323
+ }
324
+
325
+ toArtifact() {
326
+ this.summary.collector_status = this.summary.truncated ? 'truncated' : 'complete';
327
+ return {
328
+ schema_version: 1,
329
+ plan_id: this.planId,
330
+ profile_id: this.profileId,
331
+ collected_at: this.tracker.timing().timestamp,
332
+ capture_policy: this.policy,
333
+ summary: this.summary,
334
+ events: this.events,
335
+ };
336
+ }
337
+ }
@@ -0,0 +1,322 @@
1
+ import { sha256 } from '../../shared/io.js';
2
+
3
+ // Sensitive key patterns: headers, storage keys, query parameters, payload fields
4
+ export const SENSITIVE_KEY_PATTERN = /(?:^|[-_])(?:authorization|bearer|token|secret|apiKey|api[-_]?key|password|credential|access[-_]?token|refresh[-_]?token|jwt|session(?:[-_]?id)?|csrf|xsrf|cvv|cvc|ssn|credit[-_]?card|passwd|auth[-_]?token|oauth[-_]?token|auth[-_]?code|code)(?:$|[-_])|^(?:cookie|set[-_]?cookie)$/i;
5
+
6
+ // Sensitive value patterns: bearer tokens, common API keys, JWT tokens, card numbers
7
+ export const SENSITIVE_VALUE_PATTERN = /(?:bearer\s+[a-zA-Z0-9_\-.]{10,}|(?:ghp|gho|glpat|ya29|sk-[a-zA-Z0-9]{20,})|eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}|\b(?:\d[ -]*?){13,16}\b)/i;
8
+
9
+ export const REDACTED_MARKER = '[REDACTED_SECRET]';
10
+ export const FILTERED_MARKER = '[FILTERED]';
11
+ export const POLICY_RESTRICTED_MARKER = '[POLICY_RESTRICTED]';
12
+
13
+ /**
14
+ * Check if a field/header/cookie/param key is sensitive.
15
+ */
16
+ export function isSensitiveKey(key) {
17
+ if (typeof key !== 'string') return false;
18
+ return SENSITIVE_KEY_PATTERN.test(key);
19
+ }
20
+
21
+ /**
22
+ * Check if a string value contains sensitive tokens or patterns.
23
+ */
24
+ export function isSensitiveValue(value) {
25
+ if (typeof value !== 'string') return false;
26
+ return SENSITIVE_VALUE_PATTERN.test(value);
27
+ }
28
+
29
+ /**
30
+ * Sanitize headers against allowlist and sensitive key/value rules.
31
+ * Even if a sensitive header is allowlisted, it MUST be redacted.
32
+ */
33
+ export function sanitizeHeaders(headers, allowedNames = null) {
34
+ if (!headers || typeof headers !== 'object') {
35
+ return { headers: null, status: 'not_requested', redactedCount: 0, filteredCount: 0 };
36
+ }
37
+ const allowedSet = Array.isArray(allowedNames) ? new Set(allowedNames.map((n) => n.toLowerCase())) : null;
38
+ const result = {};
39
+ let redactedCount = 0;
40
+ let filteredCount = 0;
41
+
42
+ for (const [rawKey, rawVal] of Object.entries(headers)) {
43
+ const key = rawKey.toLowerCase();
44
+ // Sensitive keys are unconditionally redacted or omitted
45
+ if (isSensitiveKey(key)) {
46
+ if (allowedSet && allowedSet.has(key)) {
47
+ result[key] = REDACTED_MARKER;
48
+ redactedCount++;
49
+ } else {
50
+ filteredCount++;
51
+ }
52
+ continue;
53
+ }
54
+
55
+ // Non-sensitive keys: must be explicitly allowed if allowedSet is provided
56
+ if (allowedSet && !allowedSet.has(key)) {
57
+ filteredCount++;
58
+ continue;
59
+ }
60
+
61
+ // Value sanitization
62
+ const strVal = String(rawVal ?? '');
63
+ if (isSensitiveValue(strVal)) {
64
+ result[key] = REDACTED_MARKER;
65
+ redactedCount++;
66
+ } else {
67
+ result[key] = strVal;
68
+ }
69
+ }
70
+
71
+ const hasEntries = Object.keys(result).length > 0;
72
+ let status = 'captured';
73
+ if (!hasEntries) {
74
+ status = filteredCount > 0 ? 'filtered' : 'not_requested';
75
+ } else if (redactedCount > 0) {
76
+ status = 'redacted';
77
+ }
78
+
79
+ return {
80
+ headers: hasEntries ? result : null,
81
+ status,
82
+ redactedCount,
83
+ filteredCount,
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Sanitize URL query parameters according to allowlist.
89
+ */
90
+ export function sanitizeQueryParams(urlStr, allowedParams = null) {
91
+ if (!urlStr) return { query: null, status: 'none_present', redactedCount: 0, filteredFields: [] };
92
+ if (!allowedParams || !Array.isArray(allowedParams) || allowedParams.length === 0) {
93
+ return { query: null, status: 'not_requested', redactedCount: 0, filteredFields: [] };
94
+ }
95
+ let parsed;
96
+ try {
97
+ parsed = new URL(urlStr);
98
+ } catch {
99
+ return { query: null, status: 'none_present', redactedCount: 0, filteredFields: [] };
100
+ }
101
+
102
+ const searchParams = parsed.searchParams;
103
+ if (!searchParams || Array.from(searchParams.keys()).length === 0) {
104
+ return { query: null, status: 'none_present', redactedCount: 0, filteredFields: [] };
105
+ }
106
+
107
+ const allowedSet = new Set(allowedParams.map((p) => p.toLowerCase()));
108
+ const query = {};
109
+ let redactedCount = 0;
110
+ const filteredFields = [];
111
+
112
+ for (const [key, val] of searchParams.entries()) {
113
+ const lowerKey = key.toLowerCase();
114
+ if (isSensitiveKey(lowerKey)) {
115
+ if (allowedSet.has(lowerKey)) {
116
+ query[key] = REDACTED_MARKER;
117
+ redactedCount++;
118
+ } else {
119
+ filteredFields.push(key);
120
+ }
121
+ continue;
122
+ }
123
+
124
+ if (!allowedSet.has(lowerKey)) {
125
+ filteredFields.push(key);
126
+ continue;
127
+ }
128
+
129
+ if (isSensitiveValue(val)) {
130
+ query[key] = REDACTED_MARKER;
131
+ redactedCount++;
132
+ } else {
133
+ query[key] = val;
134
+ }
135
+ }
136
+
137
+ const hasEntries = Object.keys(query).length > 0;
138
+ let status = 'captured';
139
+ if (!hasEntries) {
140
+ status = filteredFields.length > 0 ? 'filtered' : 'none_present';
141
+ } else if (redactedCount > 0) {
142
+ status = 'redacted';
143
+ }
144
+
145
+ return {
146
+ query: hasEntries ? query : null,
147
+ status,
148
+ redactedCount,
149
+ filteredFields,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Safely deep-clone an object with cycle detection, depth limits, field allowlists,
155
+ * and sensitive-value redaction.
156
+ */
157
+ export function safeJsonClone(value, options = {}) {
158
+ const {
159
+ maxDepth = 5,
160
+ maxBytes = 4096,
161
+ maxFieldSize = 1024,
162
+ allowedFields = null,
163
+ hashFields = null,
164
+ redactFields = null,
165
+ seen = new WeakSet(),
166
+ depth = 0,
167
+ stats = { redactedCount: 0, filteredFields: [] },
168
+ } = options;
169
+
170
+ if (value === null || value === undefined) return value;
171
+ if (typeof value === 'boolean' || typeof value === 'number') return value;
172
+
173
+ if (typeof value === 'string') {
174
+ if (isSensitiveValue(value)) {
175
+ stats.redactedCount++;
176
+ return REDACTED_MARKER;
177
+ }
178
+ if (value.length > maxFieldSize) {
179
+ return value.slice(0, maxFieldSize) + '...[TRUNCATED]';
180
+ }
181
+ return value;
182
+ }
183
+
184
+ if (depth >= maxDepth) return '[DEPTH_LIMIT]';
185
+
186
+ if (typeof value === 'object') {
187
+ if (seen.has(value)) return '[CIRCULAR]';
188
+ seen.add(value);
189
+
190
+ if (Array.isArray(value)) {
191
+ return value.map((item) => safeJsonClone(item, { ...options, depth: depth + 1, seen, stats }));
192
+ }
193
+
194
+ const copy = {};
195
+ const allowedSet = allowedFields ? new Set(allowedFields) : null;
196
+ const hashSet = hashFields ? new Set(hashFields) : null;
197
+ const redactSet = redactFields ? new Set(redactFields) : null;
198
+
199
+ for (const k of Object.getOwnPropertyNames(value)) {
200
+ // Check allowlist first: if not allowed, filter it out
201
+ if (allowedSet && !allowedSet.has(k)) {
202
+ stats.filteredFields.push(k);
203
+ continue;
204
+ }
205
+
206
+ // If allowed (or no allowlist specified), check if k is sensitive or in redactSet
207
+ if (isSensitiveKey(k) || (redactSet && redactSet.has(k))) {
208
+ copy[k] = REDACTED_MARKER;
209
+ stats.redactedCount++;
210
+ continue;
211
+ }
212
+
213
+ let v;
214
+ try {
215
+ v = value[k];
216
+ } catch {
217
+ v = '[GETTER_ERROR]';
218
+ }
219
+
220
+ // Check hash fields
221
+ if (hashSet && hashSet.has(k)) {
222
+ copy[k] = `sha256:${sha256(typeof v === 'string' ? v : JSON.stringify(v))}`;
223
+ continue;
224
+ }
225
+
226
+ copy[k] = safeJsonClone(v, { ...options, depth: depth + 1, seen, stats });
227
+ }
228
+ return copy;
229
+ }
230
+
231
+ return String(value);
232
+ }
233
+
234
+ /**
235
+ * Sanitize request or response payloads (body).
236
+ */
237
+ export function sanitizePayload(payload, options = {}) {
238
+ const {
239
+ allowedFields = null,
240
+ hashOnly = false,
241
+ maxBytes = 4096,
242
+ maxDepth = 5,
243
+ } = options;
244
+
245
+ if (payload == null || payload === '') {
246
+ return { sanitized: null, hash: null, bytes: 0, status: 'unavailable', redactedCount: 0, filteredFields: [] };
247
+ }
248
+
249
+ // Handle binary buffers without unsafe UTF-8 string coercion
250
+ if (Buffer.isBuffer(payload)) {
251
+ const isBinary = payload.some((byte) => byte === 0);
252
+ if (isBinary) {
253
+ const hash = sha256(payload);
254
+ return {
255
+ sanitized: null,
256
+ hash,
257
+ bytes: payload.length,
258
+ status: hashOnly ? 'hash_only' : 'unavailable',
259
+ redactedCount: 0,
260
+ filteredFields: [],
261
+ };
262
+ }
263
+ }
264
+
265
+ const rawString = typeof payload === 'string' ? payload : Buffer.isBuffer(payload) ? payload.toString('utf8') : JSON.stringify(payload);
266
+ const bytes = Buffer.byteLength(rawString, 'utf8');
267
+ const hash = sha256(rawString);
268
+
269
+ if (hashOnly) {
270
+ return { sanitized: null, hash, bytes, status: 'hash_only', redactedCount: 0, filteredFields: [] };
271
+ }
272
+
273
+ // Try parsing JSON if allowedFields or structured redaction is requested
274
+ let parsedJson = null;
275
+ try {
276
+ parsedJson = JSON.parse(rawString);
277
+ } catch {
278
+ parsedJson = null;
279
+ }
280
+
281
+ const stats = { redactedCount: 0, filteredFields: [] };
282
+
283
+ if (parsedJson && typeof parsedJson === 'object') {
284
+ const cloned = safeJsonClone(parsedJson, {
285
+ maxDepth,
286
+ maxBytes,
287
+ allowedFields,
288
+ stats,
289
+ });
290
+ const hasAllowedEntries = cloned !== null && typeof cloned === 'object' && Object.keys(cloned).length > 0;
291
+ let status = 'captured';
292
+ if (!hasAllowedEntries && allowedFields && allowedFields.length > 0) {
293
+ status = 'filtered';
294
+ } else if (rawString.length > maxBytes) {
295
+ status = 'truncated';
296
+ }
297
+ return {
298
+ sanitized: cloned,
299
+ hash,
300
+ bytes,
301
+ status,
302
+ redactedCount: stats.redactedCount,
303
+ filteredFields: stats.filteredFields,
304
+ };
305
+ }
306
+
307
+ // Raw text payload
308
+ if (isSensitiveValue(rawString)) {
309
+ return { sanitized: REDACTED_MARKER, hash, bytes, status: 'captured', redactedCount: 1, filteredFields: [] };
310
+ }
311
+
312
+ const isTruncated = rawString.length > maxBytes;
313
+ const truncated = isTruncated ? rawString.slice(0, maxBytes) + '...[TRUNCATED]' : rawString;
314
+ return {
315
+ sanitized: truncated,
316
+ hash,
317
+ bytes,
318
+ status: isTruncated ? 'truncated' : 'captured',
319
+ redactedCount: 0,
320
+ filteredFields: [],
321
+ };
322
+ }