@equipe-tech/observability 0.2.0 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +11 -9
  2. package/dist/BrowserEvents.d.ts +89 -12
  3. package/dist/BrowserEvents.js +67 -13
  4. package/dist/Correlation.d.ts +51 -0
  5. package/dist/Correlation.js +95 -0
  6. package/dist/Metrics.d.ts +19 -3
  7. package/dist/Metrics.js +16 -0
  8. package/dist/MetricsRuntime.d.ts +45 -3
  9. package/dist/MetricsRuntime.js +478 -120
  10. package/dist/PolicyOtlpLogger.d.ts +18 -0
  11. package/dist/PolicyOtlpLogger.js +142 -0
  12. package/dist/RedactionPolicy.d.ts +2 -9
  13. package/dist/RedactionPolicy.js +2 -246
  14. package/dist/ResourceIdentity.d.ts +47 -0
  15. package/dist/ResourceIdentity.js +82 -0
  16. package/dist/Telemetry.d.ts +18 -3
  17. package/dist/Telemetry.js +43 -25
  18. package/dist/TelemetryConfig.d.ts +11 -4
  19. package/dist/TelemetryConfig.js +38 -10
  20. package/dist/audit/AuditCommitDocument.d.ts +41 -0
  21. package/dist/audit/AuditCommitDocument.js +58 -0
  22. package/dist/audit/AuditDigest.d.ts +11 -0
  23. package/dist/audit/AuditDigest.js +25 -0
  24. package/dist/audit/AuditOutbox.d.ts +75 -0
  25. package/dist/audit/AuditOutbox.js +104 -0
  26. package/dist/audit/AuditPublisher.d.ts +49 -0
  27. package/dist/audit/AuditPublisher.js +60 -0
  28. package/dist/audit/AuditRecord.d.ts +128 -0
  29. package/dist/audit/AuditRecord.js +172 -0
  30. package/dist/audit/CommittedAuditRecordInternal.d.ts +11 -0
  31. package/dist/audit/CommittedAuditRecordInternal.js +13 -0
  32. package/dist/audit/InvalidAuditRecord.d.ts +12 -0
  33. package/dist/audit/InvalidAuditRecord.js +16 -0
  34. package/dist/audit/index.d.ts +7 -0
  35. package/dist/audit/index.js +7 -0
  36. package/dist/browser/BrowserClient.d.ts +82 -3
  37. package/dist/browser/BrowserClient.js +322 -28
  38. package/dist/browser/BrowserEventLimits.d.ts +8 -0
  39. package/dist/browser/BrowserEventLimits.js +8 -0
  40. package/dist/browser/BrowserMetricRecorder.d.ts +17 -0
  41. package/dist/browser/BrowserMetricRecorder.js +35 -0
  42. package/dist/browser/BrowserTelemetry.d.ts +40 -0
  43. package/dist/browser/BrowserTelemetry.js +105 -0
  44. package/dist/browser/ClientPolicy.d.ts +3 -0
  45. package/dist/browser/ClientPolicy.js +52 -0
  46. package/dist/browser/client.d.ts +3 -2
  47. package/dist/browser/client.js +2 -1
  48. package/dist/browser/index.d.ts +4 -37
  49. package/dist/browser/index.js +3 -95
  50. package/dist/contract/ContractCompatibility.d.ts +31 -0
  51. package/dist/contract/ContractCompatibility.js +274 -0
  52. package/dist/contract/ContractIndex.d.ts +54 -0
  53. package/dist/contract/ContractIndex.js +178 -0
  54. package/dist/contract/ContractSurface.d.ts +111 -0
  55. package/dist/contract/ContractSurface.js +116 -0
  56. package/dist/contract/EventName.d.ts +6 -0
  57. package/dist/contract/EventName.js +55 -0
  58. package/dist/contract/EventProducer.d.ts +123 -0
  59. package/dist/contract/EventProducer.js +209 -0
  60. package/dist/contract/MetricContractError.d.ts +19 -0
  61. package/dist/contract/MetricContractError.js +19 -0
  62. package/dist/contract/MetricDefinition.d.ts +61 -0
  63. package/dist/contract/MetricDefinition.js +91 -0
  64. package/dist/contract/MetricProducer.d.ts +42 -0
  65. package/dist/contract/MetricProducer.js +123 -0
  66. package/dist/contract/OrganizationEvents.d.ts +165 -0
  67. package/dist/contract/OrganizationEvents.js +97 -0
  68. package/dist/contract/TelemetryContract.d.ts +95 -0
  69. package/dist/contract/TelemetryContract.js +451 -0
  70. package/dist/contract/TelemetryContractError.d.ts +44 -0
  71. package/dist/contract/TelemetryContractError.js +64 -0
  72. package/dist/contract/TelemetryEvent.d.ts +59 -0
  73. package/dist/contract/TelemetryEvent.js +54 -0
  74. package/dist/contract/index.d.ts +11 -0
  75. package/dist/contract/index.js +11 -0
  76. package/dist/effect/WideEvent.d.ts +3 -0
  77. package/dist/effect/WideEvent.js +20 -0
  78. package/dist/effect/WideEventSink.d.ts +3 -0
  79. package/dist/effect/WideEventSink.js +79 -0
  80. package/dist/effect/index.d.ts +2 -0
  81. package/dist/effect/index.js +2 -0
  82. package/dist/index.d.ts +17 -2
  83. package/dist/index.js +11 -2
  84. package/dist/metrics/InstrumentGrammar.d.ts +5 -0
  85. package/dist/metrics/InstrumentGrammar.js +12 -0
  86. package/dist/metrics/MetricScalar.d.ts +3 -0
  87. package/dist/metrics/MetricScalar.js +5 -0
  88. package/dist/node/AuditDigest.d.ts +3 -0
  89. package/dist/node/AuditDigest.js +6 -0
  90. package/dist/node/BrowserEventIngest.d.ts +7 -2
  91. package/dist/node/BrowserEventIngest.js +111 -19
  92. package/dist/node/LocalCollector.d.ts +20 -0
  93. package/dist/node/LocalCollector.js +188 -0
  94. package/dist/node/Observability.d.ts +49 -0
  95. package/dist/node/Observability.js +190 -0
  96. package/dist/node/Runtime.d.ts +4 -1
  97. package/dist/node/index.d.ts +4 -0
  98. package/dist/node/index.js +3 -0
  99. package/dist/policy/BrowserFieldPolicy.d.ts +7 -0
  100. package/dist/policy/BrowserFieldPolicy.js +298 -0
  101. package/dist/policy/DataPolicy.d.ts +19 -0
  102. package/dist/policy/DataPolicy.js +254 -0
  103. package/dist/policy/DataPolicyError.d.ts +22 -0
  104. package/dist/policy/DataPolicyError.js +25 -0
  105. package/dist/policy/Deduplication.d.ts +13 -0
  106. package/dist/policy/Deduplication.js +44 -0
  107. package/dist/policy/DefectEnvelope.d.ts +25 -0
  108. package/dist/policy/DefectEnvelope.js +52 -0
  109. package/dist/policy/MetricLabelPolicy.d.ts +5 -0
  110. package/dist/policy/MetricLabelPolicy.js +65 -0
  111. package/dist/policy/PolicyTransform.d.ts +16 -0
  112. package/dist/policy/PolicyTransform.js +136 -0
  113. package/dist/policy/PolicyVocabulary.d.ts +10 -0
  114. package/dist/policy/PolicyVocabulary.js +84 -0
  115. package/dist/policy/ResourceAttributePolicy.d.ts +9 -0
  116. package/dist/policy/ResourceAttributePolicy.js +33 -0
  117. package/dist/policy/entrypoint.d.ts +2 -0
  118. package/dist/policy/entrypoint.js +2 -0
  119. package/dist/policy/index.d.ts +17 -0
  120. package/dist/policy/index.js +9 -0
  121. package/dist/profile/DataPolicy.d.ts +2 -0
  122. package/dist/profile/DataPolicy.js +1 -0
  123. package/dist/profile/EnvironmentPolicy.d.ts +22 -0
  124. package/dist/profile/EnvironmentPolicy.js +41 -0
  125. package/dist/profile/LifecycleRegistry.d.ts +24 -0
  126. package/dist/profile/LifecycleRegistry.js +240 -0
  127. package/dist/profile/ObservabilityAdapter.d.ts +115 -0
  128. package/dist/profile/ObservabilityAdapter.js +50 -0
  129. package/dist/profile/ObservabilityConfig.d.ts +68 -0
  130. package/dist/profile/ObservabilityConfig.js +125 -0
  131. package/dist/profile/ObservabilityConfigError.d.ts +23 -0
  132. package/dist/profile/ObservabilityConfigError.js +39 -0
  133. package/dist/profile/ObservabilityProfile.d.ts +49 -0
  134. package/dist/profile/ObservabilityProfile.js +121 -0
  135. package/dist/profile/ReactWebProfile.d.ts +8 -0
  136. package/dist/profile/ReactWebProfile.js +7 -0
  137. package/dist/profile/index.d.ts +5 -0
  138. package/dist/profile/index.js +5 -0
  139. package/dist/testing/conformance/ConformanceAssertions.d.ts +5 -0
  140. package/dist/testing/conformance/ConformanceAssertions.js +31 -0
  141. package/dist/testing/conformance/ConformanceFailure.d.ts +35 -0
  142. package/dist/testing/conformance/ConformanceFailure.js +56 -0
  143. package/dist/testing/conformance/ConformanceModel.d.ts +97 -0
  144. package/dist/testing/conformance/ConformanceModel.js +8 -0
  145. package/dist/testing/conformance/ConformanceRegistry.d.ts +13 -0
  146. package/dist/testing/conformance/ConformanceRegistry.js +190 -0
  147. package/dist/testing/conformance/ConformanceRunner.d.ts +5 -0
  148. package/dist/testing/conformance/ConformanceRunner.js +198 -0
  149. package/dist/testing/conformance/TelemetryEvidence.d.ts +91 -0
  150. package/dist/testing/conformance/TelemetryEvidence.js +370 -0
  151. package/dist/testing/conformance/index.d.ts +10 -0
  152. package/dist/testing/conformance/index.js +6 -0
  153. package/dist/testing/contract.d.ts +81 -0
  154. package/dist/testing/contract.js +93 -0
  155. package/dist/testing/deployedCanary.d.ts +1 -0
  156. package/dist/testing/deployedCanary.js +1 -0
  157. package/dist/testing/index.d.ts +40 -3
  158. package/dist/testing/index.js +122 -5
  159. package/dist/trace/HttpServerOtlpTracer.d.ts +42 -0
  160. package/dist/trace/HttpServerOtlpTracer.js +301 -0
  161. package/package.json +16 -29
  162. package/dist/WideEvent.d.ts +0 -5
  163. package/dist/WideEvent.js +0 -6
  164. package/dist/nestjs/BrowserEventsController.d.ts +0 -21
  165. package/dist/nestjs/BrowserEventsController.js +0 -52
  166. package/dist/nestjs/HttpRoutePolicy.d.ts +0 -23
  167. package/dist/nestjs/HttpRoutePolicy.js +0 -179
  168. package/dist/nestjs/HttpServerOtlpTracer.d.ts +0 -15
  169. package/dist/nestjs/HttpServerOtlpTracer.js +0 -154
  170. package/dist/nestjs/RequestWideEventTraceCorrelation.d.ts +0 -15
  171. package/dist/nestjs/RequestWideEventTraceCorrelation.js +0 -13
  172. package/dist/nestjs/TelemetryInterceptor.d.ts +0 -31
  173. package/dist/nestjs/TelemetryInterceptor.js +0 -268
  174. package/dist/nestjs/TelemetryModule.d.ts +0 -53
  175. package/dist/nestjs/TelemetryModule.js +0 -428
  176. package/dist/nestjs/index.d.ts +0 -5
  177. package/dist/nestjs/index.js +0 -4
@@ -0,0 +1,298 @@
1
+ import { Option, Predicate, Schema } from "effect";
2
+ import { maxEventNameLength, maxFieldKeyLength, maxFieldsPerEvent, maxFieldValueLength, } from "../BrowserEvents.js";
3
+ import { baseBlockedKeys, baseBlockedValuePatterns, isSensitiveFieldKey, replaceEmailCandidates, sensitiveFieldReplacement, sensitiveTextReplacement, } from "./PolicyVocabulary.js";
4
+ const maxOriginalFieldKeyLength = 2_048;
5
+ export const maxOriginalStringLength = 16_384;
6
+ const maxJsonDepth = 32;
7
+ const maxJsonValues = 1_024;
8
+ const SensitiveScalar = Schema.Union([
9
+ Schema.String,
10
+ Schema.Number.check(Schema.isFinite()),
11
+ Schema.Boolean,
12
+ ]);
13
+ const decodeSensitiveScalar = Schema.decodeUnknownOption(SensitiveScalar);
14
+ const decodeJsonText = Schema.decodeOption(Schema.fromJsonString(Schema.Json));
15
+ const decodeJsonObject = Schema.decodeUnknownOption(Schema.JsonObject);
16
+ const asciiCaseInsensitive = (source) => source.replace(/[A-Za-z]/g, (letter) => `[${letter.toLowerCase()}${letter.toUpperCase()}]`);
17
+ const sensitiveTerms = baseBlockedKeys.map(asciiCaseInsensitive).join("|");
18
+ const sensitiveTextTermPattern = new RegExp(`(?:${sensitiveTerms})(?=[._-]|[A-Z0-9]|[^A-Za-z0-9._-]|$)`);
19
+ const maximumAssignmentKeyLength = 256;
20
+ const isAssignmentKeyCharacter = (character) => /[A-Za-z0-9_.\-[\]"'`\\]/.test(character);
21
+ const assignmentCandidateAt = (value, start) => {
22
+ if (!isAssignmentKeyCharacter(value[start] ?? ""))
23
+ return undefined;
24
+ let index = start;
25
+ while (index < value.length &&
26
+ index - start <= maximumAssignmentKeyLength &&
27
+ isAssignmentKeyCharacter(value[index] ?? "")) {
28
+ index += 1;
29
+ }
30
+ if (index - start > maximumAssignmentKeyLength)
31
+ return undefined;
32
+ const keyEnd = index;
33
+ const first = value[start];
34
+ const escapedFirst = first === "\\" ? value[start + 1] : first;
35
+ const closingKeyQuote = escapedFirst === '"' || escapedFirst === "'" || escapedFirst === "`";
36
+ if (closingKeyQuote) {
37
+ const closingLength = first === "\\" ? 2 : 1;
38
+ const closingStart = keyEnd - closingLength;
39
+ if (value.slice(closingStart, keyEnd) !== value.slice(start, start + closingLength)) {
40
+ return undefined;
41
+ }
42
+ }
43
+ while (index < value.length && /\s/.test(value[index] ?? ""))
44
+ index += 1;
45
+ if (value[index] !== ":" && value[index] !== "=")
46
+ return undefined;
47
+ if (value[index] === "=" && value[index + 1] === ">")
48
+ index += 1;
49
+ index += 1;
50
+ while (index < value.length && /\s/.test(value[index] ?? ""))
51
+ index += 1;
52
+ return { key: value.slice(start, keyEnd), start, valueStart: index };
53
+ };
54
+ const closingDelimiterIndex = (value, start, quote, escapedQuote) => {
55
+ let escaped = false;
56
+ for (let index = start; index < value.length; index += 1) {
57
+ const character = value[index];
58
+ if (escapedQuote && character === "\\" && value[index + 1] === quote)
59
+ return index;
60
+ if (escaped) {
61
+ escaped = false;
62
+ }
63
+ else if (character === "\\") {
64
+ escaped = true;
65
+ }
66
+ else if (character === quote) {
67
+ return index;
68
+ }
69
+ }
70
+ return -1;
71
+ };
72
+ const safeValueEnd = (value, start) => {
73
+ for (let index = start; index < value.length; index += 1) {
74
+ const character = value[index];
75
+ if (character !== "&" && character !== "#")
76
+ continue;
77
+ let candidateStart = index + 1;
78
+ while (candidateStart < value.length && /\s/.test(value[candidateStart] ?? "")) {
79
+ candidateStart += 1;
80
+ }
81
+ if (assignmentCandidateAt(value, candidateStart) !== undefined)
82
+ return index;
83
+ }
84
+ return value.length;
85
+ };
86
+ const replaceCoreValues = (value) => {
87
+ let sanitized = value;
88
+ for (const pattern of baseBlockedValuePatterns) {
89
+ pattern.lastIndex = 0;
90
+ sanitized = sanitized.replace(pattern, sensitiveTextReplacement);
91
+ }
92
+ return replaceEmailCandidates(sanitized);
93
+ };
94
+ const containsCoreValue = (value) => {
95
+ if (replaceEmailCandidates(value) !== value)
96
+ return true;
97
+ for (const pattern of baseBlockedValuePatterns) {
98
+ pattern.lastIndex = 0;
99
+ if (pattern.test(value)) {
100
+ pattern.lastIndex = 0;
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ };
106
+ export const replaceStructuredAssignments = (value) => {
107
+ let sanitized = "";
108
+ let offset = 0;
109
+ let index = 0;
110
+ while (index < value.length) {
111
+ const previous = value[index - 1];
112
+ if (previous !== undefined && /[A-Za-z0-9_.-]/.test(previous)) {
113
+ index += 1;
114
+ continue;
115
+ }
116
+ const candidate = assignmentCandidateAt(value, index);
117
+ if (candidate === undefined || !isSensitiveFieldKey(candidate.key)) {
118
+ index += 1;
119
+ continue;
120
+ }
121
+ const explicitQuote = value[candidate.valueStart];
122
+ const escapedQuote = explicitQuote === "\\" &&
123
+ (value[candidate.valueStart + 1] === '"' ||
124
+ value[candidate.valueStart + 1] === "'" ||
125
+ value[candidate.valueStart + 1] === "`");
126
+ const quote = escapedQuote ? value[candidate.valueStart + 1] : explicitQuote;
127
+ const hasExplicitQuote = quote === '"' || quote === "'" || quote === "`";
128
+ const enclosingQuote = value[candidate.start - 1];
129
+ const activeQuote = hasExplicitQuote
130
+ ? quote
131
+ : enclosingQuote === '"' || enclosingQuote === "'" || enclosingQuote === "`"
132
+ ? enclosingQuote
133
+ : undefined;
134
+ const openingLength = escapedQuote ? 2 : hasExplicitQuote ? 1 : 0;
135
+ const contentStart = candidate.valueStart + openingLength;
136
+ const closingIndex = activeQuote === undefined
137
+ ? -1
138
+ : closingDelimiterIndex(value, contentStart, activeQuote, escapedQuote);
139
+ sanitized += value.slice(offset, candidate.valueStart + openingLength);
140
+ sanitized += sensitiveTextReplacement;
141
+ if (closingIndex >= 0 && activeQuote !== undefined) {
142
+ const closingLength = escapedQuote ? 2 : 1;
143
+ sanitized += value.slice(closingIndex, closingIndex + closingLength);
144
+ offset = closingIndex + closingLength;
145
+ }
146
+ else {
147
+ offset = safeValueEnd(value, candidate.valueStart);
148
+ }
149
+ index = offset;
150
+ }
151
+ return sanitized + value.slice(offset);
152
+ };
153
+ const containsStructuredAssignment = (value) => replaceStructuredAssignments(value) !== value;
154
+ const sanitizeJson = (source) => {
155
+ const root = { value: null };
156
+ const stack = [
157
+ {
158
+ source,
159
+ depth: 0,
160
+ sensitive: false,
161
+ assign: (value) => {
162
+ root.value = value;
163
+ },
164
+ },
165
+ ];
166
+ let visited = 0;
167
+ while (stack.length > 0) {
168
+ const current = stack.pop();
169
+ if (!Predicate.isNotUndefined(current)) {
170
+ continue;
171
+ }
172
+ visited += 1;
173
+ if (visited > maxJsonValues || current.depth > maxJsonDepth) {
174
+ return Option.none();
175
+ }
176
+ if (current.sensitive) {
177
+ current.assign(sensitiveTextReplacement);
178
+ continue;
179
+ }
180
+ if (current.source === null ||
181
+ Predicate.isNumber(current.source) ||
182
+ Predicate.isBoolean(current.source)) {
183
+ current.assign(current.source);
184
+ continue;
185
+ }
186
+ if (Predicate.isString(current.source)) {
187
+ current.assign(replaceStructuredAssignments(replaceCoreValues(current.source)));
188
+ continue;
189
+ }
190
+ if (Array.isArray(current.source)) {
191
+ const output = current.source.map(() => null);
192
+ current.assign(output);
193
+ for (let index = current.source.length - 1; index >= 0; index -= 1) {
194
+ const child = current.source[index];
195
+ if (!Predicate.isNotUndefined(child)) {
196
+ continue;
197
+ }
198
+ stack.push({
199
+ source: child,
200
+ depth: current.depth + 1,
201
+ sensitive: false,
202
+ assign: (value) => {
203
+ output[index] = value;
204
+ },
205
+ });
206
+ }
207
+ continue;
208
+ }
209
+ const sourceObject = decodeJsonObject(current.source);
210
+ if (Option.isNone(sourceObject)) {
211
+ return Option.none();
212
+ }
213
+ const output = Object.create(null);
214
+ const outputKeys = new Set();
215
+ current.assign(output);
216
+ const keys = Object.keys(sourceObject.value);
217
+ for (const key of keys) {
218
+ if (key.length > maxOriginalFieldKeyLength ||
219
+ containsCoreValue(key) ||
220
+ containsStructuredAssignment(key)) {
221
+ continue;
222
+ }
223
+ output[key] = null;
224
+ outputKeys.add(key);
225
+ }
226
+ for (let index = keys.length - 1; index >= 0; index -= 1) {
227
+ const key = keys[index];
228
+ if (!Predicate.isString(key) || !outputKeys.has(key)) {
229
+ continue;
230
+ }
231
+ const child = sourceObject.value[key];
232
+ if (!Predicate.isNotUndefined(child)) {
233
+ continue;
234
+ }
235
+ stack.push({
236
+ source: child,
237
+ depth: current.depth + 1,
238
+ sensitive: isSensitiveFieldKey(key),
239
+ assign: (value) => {
240
+ output[key] = value;
241
+ },
242
+ });
243
+ }
244
+ }
245
+ return Option.some(JSON.stringify(root.value));
246
+ };
247
+ export const replaceStructuredText = (value) => {
248
+ const trimmed = value.trimStart();
249
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
250
+ const parsed = decodeJsonText(value);
251
+ if (Option.isSome(parsed)) {
252
+ return Option.getOrElse(sanitizeJson(parsed.value), () => sensitiveTextReplacement);
253
+ }
254
+ if (sensitiveTextTermPattern.test(value))
255
+ return sensitiveTextReplacement;
256
+ }
257
+ return replaceStructuredAssignments(value);
258
+ };
259
+ const sanitizeString = (value, outputLimit) => {
260
+ if (value.length > maxOriginalStringLength) {
261
+ return sensitiveTextReplacement;
262
+ }
263
+ const sanitized = replaceStructuredText(replaceCoreValues(value));
264
+ const trimmed = value.trimStart();
265
+ if ((trimmed.startsWith("{") || trimmed.startsWith("[")) && sanitized.length > outputLimit) {
266
+ return sensitiveTextReplacement;
267
+ }
268
+ return sanitized;
269
+ };
270
+ const shouldDropKey = (key) => key.length > maxOriginalFieldKeyLength ||
271
+ containsCoreValue(key) ||
272
+ containsStructuredAssignment(key);
273
+ export const sanitizeBrowserFields = (fields) => {
274
+ const sanitized = [];
275
+ const boundedKeys = new Set();
276
+ for (const [key, runtimeValue] of Object.entries(fields)) {
277
+ const decoded = decodeSensitiveScalar(runtimeValue);
278
+ if (Option.isNone(decoded) || key === "" || shouldDropKey(key)) {
279
+ continue;
280
+ }
281
+ const boundedKey = key.slice(0, maxFieldKeyLength);
282
+ if (boundedKeys.has(boundedKey)) {
283
+ continue;
284
+ }
285
+ const value = isSensitiveFieldKey(key)
286
+ ? sensitiveFieldReplacement
287
+ : Predicate.isString(decoded.value)
288
+ ? sanitizeString(decoded.value, maxFieldValueLength).slice(0, maxFieldValueLength)
289
+ : decoded.value;
290
+ sanitized.push([boundedKey, value]);
291
+ boundedKeys.add(boundedKey);
292
+ if (boundedKeys.size >= maxFieldsPerEvent) {
293
+ break;
294
+ }
295
+ }
296
+ return Object.fromEntries(sanitized);
297
+ };
298
+ export const sanitizeEventName = (name) => sanitizeString(name, maxEventNameLength).slice(0, maxEventNameLength);
@@ -0,0 +1,19 @@
1
+ import { Context, Effect } from "effect";
2
+ import type { AttributeClassification, AttributeDefinition, AttributeDefinitionsInput } from "../contract/TelemetryContract.js";
3
+ import { InvalidDataPolicy } from "./DataPolicyError.js";
4
+ export type PolicySurface = "audit" | "event" | "log" | "span" | "metric" | "browser-ingest" | "defect" | "resource";
5
+ export type DataPolicyInput = {
6
+ readonly attributes: AttributeDefinitionsInput;
7
+ readonly blockedKeys: ReadonlyArray<string>;
8
+ readonly blockedValuePatterns: ReadonlyArray<string>;
9
+ };
10
+ export interface DataPolicy {
11
+ readonly attributes: ReadonlyMap<string, AttributeDefinition>;
12
+ readonly blockedKeys: ReadonlyArray<RegExp>;
13
+ readonly blockedValuePatterns: ReadonlyArray<RegExp>;
14
+ readonly classify: (key: string) => AttributeClassification;
15
+ }
16
+ export declare const definePolicy: <const Policy extends DataPolicyInput>(policy: Policy) => Policy;
17
+ export declare const parseDataPolicy: (input: DataPolicyInput, contractAttributes?: AttributeDefinitionsInput | undefined) => Effect.Effect<DataPolicy, InvalidDataPolicy, never>;
18
+ export declare const baseDataPolicy: DataPolicy;
19
+ export declare const CurrentDataPolicy: Context.Reference<DataPolicy>;
@@ -0,0 +1,254 @@
1
+ import { Context, Effect, Schema } from "effect";
2
+ import { isValidAttributeName } from "../contract/EventName.js";
3
+ import { baseBlockedKeyPatternSource, baseBlockedValuePatterns, isSensitiveFieldKey, } from "./PolicyVocabulary.js";
4
+ import { InvalidDataPolicy } from "./DataPolicyError.js";
5
+ const AttributeDocument = Schema.Struct({
6
+ classification: Schema.String,
7
+ required: Schema.Boolean,
8
+ metricLabel: Schema.Boolean,
9
+ });
10
+ const PolicyDocument = Schema.Struct({
11
+ attributes: Schema.Record(Schema.String, AttributeDocument),
12
+ blockedKeys: Schema.Array(Schema.String.check(Schema.isMaxLength(128))).check(Schema.isMaxLength(256)),
13
+ blockedValuePatterns: Schema.Array(Schema.String).check(Schema.isMaxLength(64)),
14
+ });
15
+ const decodePolicy = Schema.decodeUnknownEffect(PolicyDocument);
16
+ const classifications = new Set(["public", "internal", "sensitive", "forbidden"]);
17
+ const reservedNames = new Set(["event.name", "event.kind", "event.type", "event.severity"]);
18
+ const maximumRegexRepetition = 64;
19
+ const maximumRegexSourceLength = 256;
20
+ const maximumRegexFixedWidth = 128;
21
+ const issue = (code, message, attributeName, rule) => {
22
+ const value = {
23
+ code,
24
+ message,
25
+ };
26
+ if (attributeName !== undefined)
27
+ value.attributeName = attributeName;
28
+ if (rule !== undefined)
29
+ value.rule = rule;
30
+ return value;
31
+ };
32
+ const invalid = (issues) => new InvalidDataPolicy({
33
+ code: "OBS_POLICY_INVALID",
34
+ message: `Data policy compilation failed with ${issues.length} issue(s). Fix every reported rule and compile again.`,
35
+ issues,
36
+ });
37
+ const isAcceptedRegex = (source) => {
38
+ if (source.length === 0 || source.length > maximumRegexSourceLength)
39
+ return false;
40
+ let index = 0;
41
+ let canQuantify = false;
42
+ let variableQuantifiers = 0;
43
+ let fixedWidth = 0;
44
+ while (index < source.length) {
45
+ const character = source.charAt(index);
46
+ if (character === "^" && index === 0) {
47
+ canQuantify = false;
48
+ index += 1;
49
+ continue;
50
+ }
51
+ if (character === "$" && index === source.length - 1) {
52
+ canQuantify = false;
53
+ index += 1;
54
+ continue;
55
+ }
56
+ if (character === "\\") {
57
+ const escaped = source[index + 1];
58
+ if (escaped === undefined || /[A-Za-z0-9]/.test(escaped))
59
+ return false;
60
+ canQuantify = true;
61
+ index += 2;
62
+ continue;
63
+ }
64
+ if (character === "[") {
65
+ let classIndex = index + 1;
66
+ if (source[classIndex] === "^")
67
+ classIndex += 1;
68
+ let classValues = 0;
69
+ let closed = false;
70
+ while (classIndex < source.length) {
71
+ const classCharacter = source[classIndex];
72
+ if (classCharacter === "\\") {
73
+ if (source[classIndex + 1] === undefined)
74
+ return false;
75
+ classValues += 1;
76
+ classIndex += 2;
77
+ continue;
78
+ }
79
+ if (classCharacter === "]") {
80
+ closed = true;
81
+ classIndex += 1;
82
+ break;
83
+ }
84
+ if (classCharacter === "[")
85
+ return false;
86
+ classValues += 1;
87
+ classIndex += 1;
88
+ }
89
+ if (!closed || classValues === 0)
90
+ return false;
91
+ canQuantify = true;
92
+ index = classIndex;
93
+ continue;
94
+ }
95
+ if (character === "?" || character === "+" || character === "*") {
96
+ if (!canQuantify)
97
+ return false;
98
+ variableQuantifiers += 1;
99
+ if (variableQuantifiers > 1)
100
+ return false;
101
+ canQuantify = false;
102
+ index += 1;
103
+ continue;
104
+ }
105
+ if (character === "{") {
106
+ if (!canQuantify)
107
+ return false;
108
+ const quantifier = source.slice(index).match(/^\{\d+(?:,\d*)?\}/);
109
+ if (quantifier === null)
110
+ return false;
111
+ const repetition = quantifier[0];
112
+ const separator = repetition.indexOf(",");
113
+ const minimum = Number(repetition.slice(1, separator === -1 ? -1 : separator));
114
+ const maximumText = separator === -1 ? undefined : repetition.slice(separator + 1, -1);
115
+ const maximum = maximumText === undefined || maximumText === "" ? undefined : Number(maximumText);
116
+ if (minimum > maximumRegexRepetition ||
117
+ (maximum !== undefined && maximum > maximumRegexRepetition)) {
118
+ return false;
119
+ }
120
+ if (maximum !== undefined && minimum > maximum)
121
+ return false;
122
+ if (separator !== -1) {
123
+ variableQuantifiers += 1;
124
+ }
125
+ else {
126
+ fixedWidth += minimum;
127
+ }
128
+ if (variableQuantifiers > 1 || fixedWidth > maximumRegexFixedWidth)
129
+ return false;
130
+ canQuantify = false;
131
+ index += repetition.length;
132
+ continue;
133
+ }
134
+ if ("()|.^$}]".includes(character))
135
+ return false;
136
+ canQuantify = true;
137
+ index += 1;
138
+ }
139
+ return true;
140
+ };
141
+ const hasMalformedEscapeOrClass = (source) => {
142
+ let escaped = false;
143
+ let inCharacterClass = false;
144
+ for (const character of source) {
145
+ if (escaped) {
146
+ escaped = false;
147
+ }
148
+ else if (character === "\\") {
149
+ escaped = true;
150
+ }
151
+ else if (character === "[") {
152
+ if (inCharacterClass)
153
+ return true;
154
+ inCharacterClass = true;
155
+ }
156
+ else if (character === "]") {
157
+ if (!inCharacterClass)
158
+ return true;
159
+ inCharacterClass = false;
160
+ }
161
+ }
162
+ return escaped || inCharacterClass;
163
+ };
164
+ const compilePatterns = (sources, code, unsafeCode, global, issues) => {
165
+ const patterns = [];
166
+ for (const source of sources) {
167
+ if (!isAcceptedRegex(source)) {
168
+ const malformed = hasMalformedEscapeOrClass(source);
169
+ issues.push(issue(malformed ? code : unsafeCode, malformed
170
+ ? "A policy regular expression is malformed. Use a complete escaped literal or character class."
171
+ : "A policy regular expression uses unsupported syntax. Use literals, escaped literals, character classes, anchors, and direct quantifiers only."));
172
+ continue;
173
+ }
174
+ try {
175
+ patterns.push(new RegExp(source, global ? "gi" : "i"));
176
+ }
177
+ catch {
178
+ issues.push(issue(code, code === "OBS_POLICY_INVALID_BLOCKED_KEY"
179
+ ? "A blocked key rule is invalid. Use a bounded regular expression fragment."
180
+ : "A blocked value rule is invalid. Use a bounded regular expression."));
181
+ }
182
+ }
183
+ return patterns;
184
+ };
185
+ const rank = (classification) => {
186
+ switch (classification) {
187
+ case "public":
188
+ return 0;
189
+ case "internal":
190
+ return 1;
191
+ case "sensitive":
192
+ return 2;
193
+ case "forbidden":
194
+ return 3;
195
+ }
196
+ };
197
+ export const definePolicy = (policy) => policy;
198
+ export const parseDataPolicy = Effect.fn("parseDataPolicy")(function* (input, contractAttributes = {}) {
199
+ const parsed = yield* decodePolicy(input).pipe(Effect.mapError(() => invalid([
200
+ issue("OBS_POLICY_INVALID_DOCUMENT", "Data policy document is malformed. Provide attributes, blockedKeys, and blockedValuePatterns."),
201
+ ])));
202
+ const issues = [];
203
+ const attributes = new Map();
204
+ if (Object.keys(parsed.attributes).length > 512) {
205
+ issues.push(issue("OBS_POLICY_LIMIT_EXCEEDED", "Data policy exceeds the 512-attribute limit."));
206
+ }
207
+ for (const [name, definition] of Object.entries(parsed.attributes)) {
208
+ if (!isValidAttributeName(name)) {
209
+ issues.push(issue("OBS_POLICY_INVALID_ATTRIBUTE_NAME", "A policy attribute name is invalid. Use a dotted lowercase name no longer than 128 characters.", undefined, "dotted-name"));
210
+ continue;
211
+ }
212
+ if (reservedNames.has(name)) {
213
+ issues.push(issue("OBS_POLICY_RESERVED_ATTRIBUTE_NAME", "A policy attribute collides with a canonical signal field. Rename the application attribute.", name, "reserved-name"));
214
+ continue;
215
+ }
216
+ if (!classifications.has(definition.classification) ||
217
+ ((definition.classification === "sensitive" || definition.classification === "forbidden") &&
218
+ (definition.required || definition.metricLabel))) {
219
+ issues.push(issue("OBS_POLICY_INVALID_CLASSIFICATION", "A policy attribute has an invalid classification or incompatible flags.", name, "classification"));
220
+ continue;
221
+ }
222
+ const classification = Schema.decodeUnknownSync(Schema.Literals(["public", "internal", "sensitive", "forbidden"]))(definition.classification);
223
+ const contractClassification = contractAttributes[name]?.classification;
224
+ if (contractClassification !== undefined &&
225
+ rank(classification) < rank(contractClassification)) {
226
+ issues.push(issue("OBS_POLICY_CONTRACT_CONFLICT", "A policy attribute is less restrictive than its telemetry contract declaration.", name, "classification"));
227
+ continue;
228
+ }
229
+ attributes.set(name, { ...definition, classification });
230
+ }
231
+ const applicationKeyPatterns = compilePatterns(parsed.blockedKeys, "OBS_POLICY_INVALID_BLOCKED_KEY", "OBS_POLICY_UNSAFE_BLOCKED_KEY_PATTERN", false, issues);
232
+ const applicationValuePatterns = compilePatterns(parsed.blockedValuePatterns, "OBS_POLICY_INVALID_BLOCKED_VALUE_PATTERN", "OBS_POLICY_UNSAFE_BLOCKED_VALUE_PATTERN", true, issues);
233
+ if (issues.length > 0)
234
+ return yield* invalid(issues);
235
+ const blockedKeys = [new RegExp(baseBlockedKeyPatternSource), ...applicationKeyPatterns];
236
+ const blockedValuePatterns = [
237
+ ...baseBlockedValuePatterns.map((pattern) => new RegExp(pattern.source, pattern.flags)),
238
+ ...applicationValuePatterns,
239
+ ];
240
+ return Object.freeze({
241
+ attributes,
242
+ blockedKeys,
243
+ blockedValuePatterns,
244
+ classify: (key) => {
245
+ const declared = attributes.get(key)?.classification ?? "internal";
246
+ if (isSensitiveFieldKey(key) || blockedKeys.some((pattern) => pattern.test(key))) {
247
+ return rank(declared) >= rank("sensitive") ? declared : "sensitive";
248
+ }
249
+ return declared;
250
+ },
251
+ });
252
+ });
253
+ export const baseDataPolicy = Effect.runSync(parseDataPolicy({ attributes: {}, blockedKeys: [], blockedValuePatterns: [] }));
254
+ export const CurrentDataPolicy = Context.Reference("@equipe-tech/observability/CurrentDataPolicy", { defaultValue: () => baseDataPolicy });
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect";
2
+ export declare const PolicyIssueCode: Schema.Literals<readonly ["OBS_POLICY_INVALID_DOCUMENT", "OBS_POLICY_INVALID_ATTRIBUTE_NAME", "OBS_POLICY_RESERVED_ATTRIBUTE_NAME", "OBS_POLICY_INVALID_CLASSIFICATION", "OBS_POLICY_INVALID_BLOCKED_KEY", "OBS_POLICY_INVALID_BLOCKED_VALUE_PATTERN", "OBS_POLICY_UNSAFE_BLOCKED_KEY_PATTERN", "OBS_POLICY_UNSAFE_BLOCKED_VALUE_PATTERN", "OBS_POLICY_CONTRACT_CONFLICT", "OBS_POLICY_LIMIT_EXCEEDED", "OBS_POLICY_DUPLICATE_RESOURCE_ATTRIBUTE"]>;
3
+ export type PolicyIssueCode = typeof PolicyIssueCode.Type;
4
+ export type PolicyIssue = {
5
+ readonly code: PolicyIssueCode;
6
+ readonly message: string;
7
+ readonly attributeName?: string;
8
+ readonly rule?: string;
9
+ };
10
+ declare const InvalidDataPolicy_base: Schema.Class<InvalidDataPolicy, Schema.TaggedStruct<"InvalidDataPolicy", {
11
+ readonly code: Schema.Literal<"OBS_POLICY_INVALID">;
12
+ readonly message: Schema.String;
13
+ readonly issues: Schema.$Array<Schema.Struct<{
14
+ readonly code: Schema.Literals<readonly ["OBS_POLICY_INVALID_DOCUMENT", "OBS_POLICY_INVALID_ATTRIBUTE_NAME", "OBS_POLICY_RESERVED_ATTRIBUTE_NAME", "OBS_POLICY_INVALID_CLASSIFICATION", "OBS_POLICY_INVALID_BLOCKED_KEY", "OBS_POLICY_INVALID_BLOCKED_VALUE_PATTERN", "OBS_POLICY_UNSAFE_BLOCKED_KEY_PATTERN", "OBS_POLICY_UNSAFE_BLOCKED_VALUE_PATTERN", "OBS_POLICY_CONTRACT_CONFLICT", "OBS_POLICY_LIMIT_EXCEEDED", "OBS_POLICY_DUPLICATE_RESOURCE_ATTRIBUTE"]>;
15
+ readonly message: Schema.String;
16
+ readonly attributeName: Schema.optionalKey<Schema.String>;
17
+ readonly rule: Schema.optionalKey<Schema.String>;
18
+ }>>;
19
+ }>, import("effect/Cause").YieldableError>;
20
+ export declare class InvalidDataPolicy extends InvalidDataPolicy_base {
21
+ }
22
+ export {};
@@ -0,0 +1,25 @@
1
+ import { Schema } from "effect";
2
+ export const PolicyIssueCode = Schema.Literals([
3
+ "OBS_POLICY_INVALID_DOCUMENT",
4
+ "OBS_POLICY_INVALID_ATTRIBUTE_NAME",
5
+ "OBS_POLICY_RESERVED_ATTRIBUTE_NAME",
6
+ "OBS_POLICY_INVALID_CLASSIFICATION",
7
+ "OBS_POLICY_INVALID_BLOCKED_KEY",
8
+ "OBS_POLICY_INVALID_BLOCKED_VALUE_PATTERN",
9
+ "OBS_POLICY_UNSAFE_BLOCKED_KEY_PATTERN",
10
+ "OBS_POLICY_UNSAFE_BLOCKED_VALUE_PATTERN",
11
+ "OBS_POLICY_CONTRACT_CONFLICT",
12
+ "OBS_POLICY_LIMIT_EXCEEDED",
13
+ "OBS_POLICY_DUPLICATE_RESOURCE_ATTRIBUTE",
14
+ ]);
15
+ export class InvalidDataPolicy extends Schema.TaggedError()("InvalidDataPolicy", {
16
+ code: Schema.Literal("OBS_POLICY_INVALID"),
17
+ message: Schema.String,
18
+ issues: Schema.Array(Schema.Struct({
19
+ code: PolicyIssueCode,
20
+ message: Schema.String,
21
+ attributeName: Schema.String.pipe(Schema.optionalKey),
22
+ rule: Schema.String.pipe(Schema.optionalKey),
23
+ })),
24
+ }) {
25
+ }
@@ -0,0 +1,13 @@
1
+ import type { DefectEnvelope } from "./DefectEnvelope.js";
2
+ export type DedupeDecision = {
3
+ readonly kind: "admitted";
4
+ } | {
5
+ readonly kind: "deduplicated";
6
+ readonly reason: "identity" | "fingerprint";
7
+ };
8
+ export type DefectDeduplicator = {
9
+ readonly admit: (eventId: string, envelope: DefectEnvelope, now: number) => DedupeDecision;
10
+ readonly rollback: (eventId: string) => void;
11
+ readonly release: (eventId: string) => void;
12
+ };
13
+ export declare const defectDeduplicator: (windowMillis: number, capacity: number) => DefectDeduplicator;
@@ -0,0 +1,44 @@
1
+ const normalizedFingerprint = (envelope) => JSON.stringify(envelope.fingerprint.map((part) => part
2
+ .toLowerCase()
3
+ .replaceAll(/[?#].*$/g, "")
4
+ .replaceAll(/\b[0-9a-f]{8,}\b/g, "<hash>")
5
+ .replaceAll(/\d{4,}/g, "<number>")));
6
+ export const defectDeduplicator = (windowMillis, capacity) => {
7
+ const identities = new WeakSet();
8
+ const fingerprints = new Map();
9
+ const reservations = new Map();
10
+ return {
11
+ admit: (eventId, envelope, now) => {
12
+ if (identities.has(envelope))
13
+ return { kind: "deduplicated", reason: "identity" };
14
+ const fingerprint = normalizedFingerprint(envelope);
15
+ const previous = fingerprints.get(fingerprint);
16
+ if (previous !== undefined && now - previous.admittedAt <= windowMillis) {
17
+ return { kind: "deduplicated", reason: "fingerprint" };
18
+ }
19
+ identities.add(envelope);
20
+ fingerprints.delete(fingerprint);
21
+ fingerprints.set(fingerprint, { eventId, admittedAt: now });
22
+ reservations.set(eventId, { envelope, fingerprint });
23
+ while (fingerprints.size > capacity) {
24
+ const oldest = fingerprints.keys().next().value;
25
+ if (oldest !== undefined)
26
+ fingerprints.delete(oldest);
27
+ }
28
+ return { kind: "admitted" };
29
+ },
30
+ rollback: (eventId) => {
31
+ const reservation = reservations.get(eventId);
32
+ if (reservation === undefined)
33
+ return;
34
+ reservations.delete(eventId);
35
+ identities.delete(reservation.envelope);
36
+ if (fingerprints.get(reservation.fingerprint)?.eventId === eventId) {
37
+ fingerprints.delete(reservation.fingerprint);
38
+ }
39
+ },
40
+ release: (eventId) => {
41
+ reservations.delete(eventId);
42
+ },
43
+ };
44
+ };