@traffic-twin/sdk 0.0.0-stage → 0.1.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.
- package/LICENSE +202 -0
- package/README.md +171 -2
- package/dist/cjs/agent.d.ts +143 -0
- package/dist/cjs/agent.js +927 -0
- package/dist/cjs/capture/body.d.ts +11 -0
- package/dist/cjs/capture/body.js +73 -0
- package/dist/cjs/capture/common.d.ts +75 -0
- package/dist/cjs/capture/common.js +180 -0
- package/dist/cjs/capture/context.d.ts +28 -0
- package/dist/cjs/capture/context.js +92 -0
- package/dist/cjs/capture/inbound.d.ts +12 -0
- package/dist/cjs/capture/inbound.js +195 -0
- package/dist/cjs/capture/outbound-http.d.ts +4 -0
- package/dist/cjs/capture/outbound-http.js +200 -0
- package/dist/cjs/capture/outbound-undici.d.ts +2 -0
- package/dist/cjs/capture/outbound-undici.js +242 -0
- package/dist/cjs/capture/wire.d.ts +27 -0
- package/dist/cjs/capture/wire.js +68 -0
- package/dist/cjs/export/event.d.ts +46 -0
- package/dist/cjs/export/event.js +91 -0
- package/dist/cjs/export/exporter.d.ts +50 -0
- package/dist/cjs/export/exporter.js +192 -0
- package/dist/cjs/export/pipeline.d.ts +20 -0
- package/dist/cjs/export/pipeline.js +40 -0
- package/dist/cjs/fleet/asn1.d.ts +44 -0
- package/dist/cjs/fleet/asn1.js +150 -0
- package/dist/cjs/fleet/client.d.ts +84 -0
- package/dist/cjs/fleet/client.js +216 -0
- package/dist/cjs/fleet/http.d.ts +17 -0
- package/dist/cjs/fleet/http.js +80 -0
- package/dist/cjs/fleet/store.d.ts +18 -0
- package/dist/cjs/fleet/store.js +115 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +69 -0
- package/dist/cjs/log.d.ts +4 -0
- package/dist/cjs/log.js +30 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/register.d.ts +1 -0
- package/dist/cjs/register.js +18 -0
- package/dist/cjs/sanitizer/bodies.d.ts +55 -0
- package/dist/cjs/sanitizer/bodies.js +710 -0
- package/dist/cjs/sanitizer/formats.d.ts +26 -0
- package/dist/cjs/sanitizer/formats.js +110 -0
- package/dist/cjs/sanitizer/graphql.d.ts +23 -0
- package/dist/cjs/sanitizer/graphql.js +478 -0
- package/dist/cjs/sanitizer/hostpat.d.ts +15 -0
- package/dist/cjs/sanitizer/hostpat.js +129 -0
- package/dist/cjs/sanitizer/json.d.ts +60 -0
- package/dist/cjs/sanitizer/json.js +460 -0
- package/dist/cjs/sanitizer/policy.d.ts +224 -0
- package/dist/cjs/sanitizer/policy.js +820 -0
- package/dist/cjs/sanitizer/sanitizer.d.ts +149 -0
- package/dist/cjs/sanitizer/sanitizer.js +1473 -0
- package/dist/cjs/sanitizer/utf8.d.ts +22 -0
- package/dist/cjs/sanitizer/utf8.js +180 -0
- package/dist/cjs/sanitizer/vectors.d.ts +22 -0
- package/dist/cjs/sanitizer/vectors.js +130 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/cjs/worker/dir.d.ts +1 -0
- package/dist/cjs/worker/dir.js +8 -0
- package/dist/cjs/worker/pool.d.ts +47 -0
- package/dist/cjs/worker/pool.js +179 -0
- package/dist/cjs/worker/protocol.d.ts +37 -0
- package/dist/cjs/worker/protocol.js +3 -0
- package/dist/cjs/worker/sanitizer-worker.d.ts +1 -0
- package/dist/cjs/worker/sanitizer-worker.js +53 -0
- package/dist/esm/agent.d.ts +143 -0
- package/dist/esm/agent.js +921 -0
- package/dist/esm/capture/body.d.ts +11 -0
- package/dist/esm/capture/body.js +69 -0
- package/dist/esm/capture/common.d.ts +75 -0
- package/dist/esm/capture/common.js +166 -0
- package/dist/esm/capture/context.d.ts +28 -0
- package/dist/esm/capture/context.js +84 -0
- package/dist/esm/capture/inbound.d.ts +12 -0
- package/dist/esm/capture/inbound.js +187 -0
- package/dist/esm/capture/outbound-http.d.ts +4 -0
- package/dist/esm/capture/outbound-http.js +193 -0
- package/dist/esm/capture/outbound-undici.d.ts +2 -0
- package/dist/esm/capture/outbound-undici.js +236 -0
- package/dist/esm/capture/wire.d.ts +27 -0
- package/dist/esm/capture/wire.js +65 -0
- package/dist/esm/export/event.d.ts +46 -0
- package/dist/esm/export/event.js +85 -0
- package/dist/esm/export/exporter.d.ts +50 -0
- package/dist/esm/export/exporter.js +188 -0
- package/dist/esm/export/pipeline.d.ts +20 -0
- package/dist/esm/export/pipeline.js +37 -0
- package/dist/esm/fleet/asn1.d.ts +44 -0
- package/dist/esm/fleet/asn1.js +128 -0
- package/dist/esm/fleet/client.d.ts +84 -0
- package/dist/esm/fleet/client.js +204 -0
- package/dist/esm/fleet/http.d.ts +17 -0
- package/dist/esm/fleet/http.js +73 -0
- package/dist/esm/fleet/store.d.ts +18 -0
- package/dist/esm/fleet/store.js +110 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.js +48 -0
- package/dist/esm/log.d.ts +4 -0
- package/dist/esm/log.js +25 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/register.d.ts +1 -0
- package/dist/esm/register.js +16 -0
- package/dist/esm/sanitizer/bodies.d.ts +55 -0
- package/dist/esm/sanitizer/bodies.js +694 -0
- package/dist/esm/sanitizer/formats.d.ts +26 -0
- package/dist/esm/sanitizer/formats.js +101 -0
- package/dist/esm/sanitizer/graphql.d.ts +23 -0
- package/dist/esm/sanitizer/graphql.js +472 -0
- package/dist/esm/sanitizer/hostpat.d.ts +15 -0
- package/dist/esm/sanitizer/hostpat.js +122 -0
- package/dist/esm/sanitizer/json.d.ts +60 -0
- package/dist/esm/sanitizer/json.js +447 -0
- package/dist/esm/sanitizer/policy.d.ts +224 -0
- package/dist/esm/sanitizer/policy.js +800 -0
- package/dist/esm/sanitizer/sanitizer.d.ts +149 -0
- package/dist/esm/sanitizer/sanitizer.js +1459 -0
- package/dist/esm/sanitizer/utf8.d.ts +22 -0
- package/dist/esm/sanitizer/utf8.js +166 -0
- package/dist/esm/sanitizer/vectors.d.ts +22 -0
- package/dist/esm/sanitizer/vectors.js +125 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/dist/esm/worker/dir.d.ts +1 -0
- package/dist/esm/worker/dir.js +5 -0
- package/dist/esm/worker/pool.d.ts +47 -0
- package/dist/esm/worker/pool.js +175 -0
- package/dist/esm/worker/protocol.d.ts +37 -0
- package/dist/esm/worker/protocol.js +2 -0
- package/dist/esm/worker/sanitizer-worker.d.ts +1 -0
- package/dist/esm/worker/sanitizer-worker.js +51 -0
- package/package.json +75 -4
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Decodes strictly; returns null when the bytes are not valid UTF-8. */
|
|
2
|
+
export declare function decodeUtf8(b: Uint8Array): string | null;
|
|
3
|
+
/** True when the JS string has no lone surrogates (so it has a UTF-8 encoding). */
|
|
4
|
+
export declare function isWellFormed(s: string): boolean;
|
|
5
|
+
/** UTF-8 length of a well-formed string. */
|
|
6
|
+
export declare function utf8Length(s: string): number;
|
|
7
|
+
export declare function utf8(s: string): Buffer;
|
|
8
|
+
/** ASCII-only lowercase (never touches non-ASCII characters). */
|
|
9
|
+
export declare function asciiLower(s: string): string;
|
|
10
|
+
/** ASCII-only uppercase. */
|
|
11
|
+
export declare function asciiUpper(s: string): string;
|
|
12
|
+
/** Lowercase ASCII bytes. */
|
|
13
|
+
export declare function asciiLowerBytes(b: Buffer): Buffer;
|
|
14
|
+
/** Strips OWS (space and horizontal tab) on both sides. */
|
|
15
|
+
export declare function trimOws(b: Buffer): Buffer;
|
|
16
|
+
export declare function trimOwsStr(s: string): string;
|
|
17
|
+
/** Splits bytes on a single byte value (keeps empty parts). */
|
|
18
|
+
export declare function splitBytes(b: Buffer, sep: number): Buffer[];
|
|
19
|
+
/** Percent-decoding of sanitizer.md 4.1 (optionally "+" -> space first, 8.3). */
|
|
20
|
+
export declare function pctDecode(b: Buffer, plusIsSpace: boolean): Buffer;
|
|
21
|
+
/** Byte-order comparison of two strings by their UTF-8 encoding (code point order). */
|
|
22
|
+
export declare function cmpUtf8(a: string, b: string): number;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Strict UTF-8 (RFC 3629, sanitizer.md 0.2) helpers.
|
|
3
|
+
//
|
|
4
|
+
// The WHATWG decoder in fatal mode rejects overlong forms, encoded surrogates and code
|
|
5
|
+
// points above U+10FFFF. ignoreBOM keeps a leading U+FEFF as a character instead of
|
|
6
|
+
// silently removing it.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.decodeUtf8 = decodeUtf8;
|
|
9
|
+
exports.isWellFormed = isWellFormed;
|
|
10
|
+
exports.utf8Length = utf8Length;
|
|
11
|
+
exports.utf8 = utf8;
|
|
12
|
+
exports.asciiLower = asciiLower;
|
|
13
|
+
exports.asciiUpper = asciiUpper;
|
|
14
|
+
exports.asciiLowerBytes = asciiLowerBytes;
|
|
15
|
+
exports.trimOws = trimOws;
|
|
16
|
+
exports.trimOwsStr = trimOwsStr;
|
|
17
|
+
exports.splitBytes = splitBytes;
|
|
18
|
+
exports.pctDecode = pctDecode;
|
|
19
|
+
exports.cmpUtf8 = cmpUtf8;
|
|
20
|
+
const strict = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
21
|
+
/** Decodes strictly; returns null when the bytes are not valid UTF-8. */
|
|
22
|
+
function decodeUtf8(b) {
|
|
23
|
+
if (b.length === 0)
|
|
24
|
+
return '';
|
|
25
|
+
// Fast path: pure ASCII.
|
|
26
|
+
let ascii = true;
|
|
27
|
+
for (let i = 0; i < b.length; i++) {
|
|
28
|
+
if (b[i] >= 0x80) {
|
|
29
|
+
ascii = false;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (ascii)
|
|
34
|
+
return Buffer.from(b.buffer, b.byteOffset, b.length).toString('latin1');
|
|
35
|
+
try {
|
|
36
|
+
return strict.decode(b);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const LONE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
43
|
+
/** True when the JS string has no lone surrogates (so it has a UTF-8 encoding). */
|
|
44
|
+
function isWellFormed(s) {
|
|
45
|
+
const f = s.isWellFormed;
|
|
46
|
+
if (typeof f === 'function')
|
|
47
|
+
return f.call(s);
|
|
48
|
+
return !LONE.test(s);
|
|
49
|
+
}
|
|
50
|
+
/** UTF-8 length of a well-formed string. */
|
|
51
|
+
function utf8Length(s) {
|
|
52
|
+
return Buffer.byteLength(s, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
function utf8(s) {
|
|
55
|
+
return Buffer.from(s, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
/** ASCII-only lowercase (never touches non-ASCII characters). */
|
|
58
|
+
function asciiLower(s) {
|
|
59
|
+
let out = '';
|
|
60
|
+
let changed = false;
|
|
61
|
+
for (let i = 0; i < s.length; i++) {
|
|
62
|
+
const c = s.charCodeAt(i);
|
|
63
|
+
if (c >= 0x41 && c <= 0x5a) {
|
|
64
|
+
changed = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!changed)
|
|
69
|
+
return s;
|
|
70
|
+
for (let i = 0; i < s.length; i++) {
|
|
71
|
+
const c = s.charCodeAt(i);
|
|
72
|
+
out += c >= 0x41 && c <= 0x5a ? String.fromCharCode(c + 32) : s[i];
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** ASCII-only uppercase. */
|
|
77
|
+
function asciiUpper(s) {
|
|
78
|
+
let out = '';
|
|
79
|
+
for (let i = 0; i < s.length; i++) {
|
|
80
|
+
const c = s.charCodeAt(i);
|
|
81
|
+
out += c >= 0x61 && c <= 0x7a ? String.fromCharCode(c - 32) : s[i];
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Lowercase ASCII bytes. */
|
|
86
|
+
function asciiLowerBytes(b) {
|
|
87
|
+
const out = Buffer.from(b);
|
|
88
|
+
for (let i = 0; i < out.length; i++) {
|
|
89
|
+
const c = out[i];
|
|
90
|
+
if (c >= 0x41 && c <= 0x5a)
|
|
91
|
+
out[i] = c + 32;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** Strips OWS (space and horizontal tab) on both sides. */
|
|
96
|
+
function trimOws(b) {
|
|
97
|
+
let s = 0;
|
|
98
|
+
let e = b.length;
|
|
99
|
+
while (s < e && (b[s] === 0x20 || b[s] === 0x09))
|
|
100
|
+
s++;
|
|
101
|
+
while (e > s && (b[e - 1] === 0x20 || b[e - 1] === 0x09))
|
|
102
|
+
e--;
|
|
103
|
+
return s === 0 && e === b.length ? b : b.subarray(s, e);
|
|
104
|
+
}
|
|
105
|
+
function trimOwsStr(s) {
|
|
106
|
+
let a = 0;
|
|
107
|
+
let e = s.length;
|
|
108
|
+
while (a < e && (s.charCodeAt(a) === 0x20 || s.charCodeAt(a) === 0x09))
|
|
109
|
+
a++;
|
|
110
|
+
while (e > a && (s.charCodeAt(e - 1) === 0x20 || s.charCodeAt(e - 1) === 0x09))
|
|
111
|
+
e--;
|
|
112
|
+
return s.slice(a, e);
|
|
113
|
+
}
|
|
114
|
+
/** Splits bytes on a single byte value (keeps empty parts). */
|
|
115
|
+
function splitBytes(b, sep) {
|
|
116
|
+
const out = [];
|
|
117
|
+
let start = 0;
|
|
118
|
+
for (let i = 0; i < b.length; i++) {
|
|
119
|
+
if (b[i] === sep) {
|
|
120
|
+
out.push(b.subarray(start, i));
|
|
121
|
+
start = i + 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
out.push(b.subarray(start));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function hexVal(c) {
|
|
128
|
+
if (c >= 0x30 && c <= 0x39)
|
|
129
|
+
return c - 0x30;
|
|
130
|
+
if (c >= 0x41 && c <= 0x46)
|
|
131
|
+
return c - 0x37;
|
|
132
|
+
if (c >= 0x61 && c <= 0x66)
|
|
133
|
+
return c - 0x57;
|
|
134
|
+
return -1;
|
|
135
|
+
}
|
|
136
|
+
/** Percent-decoding of sanitizer.md 4.1 (optionally "+" -> space first, 8.3). */
|
|
137
|
+
function pctDecode(b, plusIsSpace) {
|
|
138
|
+
let needs = false;
|
|
139
|
+
for (let i = 0; i < b.length; i++) {
|
|
140
|
+
if (b[i] === 0x25 || (plusIsSpace && b[i] === 0x2b)) {
|
|
141
|
+
needs = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!needs)
|
|
146
|
+
return b;
|
|
147
|
+
const out = Buffer.allocUnsafe(b.length);
|
|
148
|
+
let o = 0;
|
|
149
|
+
for (let i = 0; i < b.length; i++) {
|
|
150
|
+
const c = b[i];
|
|
151
|
+
if (c === 0x25 && i + 2 < b.length) {
|
|
152
|
+
const h = hexVal(b[i + 1]);
|
|
153
|
+
const l = hexVal(b[i + 2]);
|
|
154
|
+
if (h >= 0 && l >= 0) {
|
|
155
|
+
out[o++] = h * 16 + l;
|
|
156
|
+
i += 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
out[o++] = plusIsSpace && c === 0x2b ? 0x20 : c;
|
|
161
|
+
}
|
|
162
|
+
return out.subarray(0, o);
|
|
163
|
+
}
|
|
164
|
+
/** Byte-order comparison of two strings by their UTF-8 encoding (code point order). */
|
|
165
|
+
function cmpUtf8(a, b) {
|
|
166
|
+
const n = Math.min(a.length, b.length);
|
|
167
|
+
for (let i = 0; i < n; i++) {
|
|
168
|
+
let x = a.charCodeAt(i);
|
|
169
|
+
let y = b.charCodeAt(i);
|
|
170
|
+
if (x === y)
|
|
171
|
+
continue;
|
|
172
|
+
// Surrogates (supplementary code points) sort after U+E000..U+FFFF in UTF-8.
|
|
173
|
+
if (x >= 0xd800 && x <= 0xdfff)
|
|
174
|
+
x += 0x10000;
|
|
175
|
+
if (y >= 0xd800 && y <= 0xdfff)
|
|
176
|
+
y += 0x10000;
|
|
177
|
+
return x < y ? -1 : 1;
|
|
178
|
+
}
|
|
179
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
180
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Plain } from './json.js';
|
|
2
|
+
import { type Exchange } from './sanitizer.js';
|
|
3
|
+
export type VectorResult = {
|
|
4
|
+
outcome: 'event';
|
|
5
|
+
canonical: string;
|
|
6
|
+
} | {
|
|
7
|
+
outcome: 'not_sampled';
|
|
8
|
+
sampled_by: string;
|
|
9
|
+
} | {
|
|
10
|
+
outcome: 'refused';
|
|
11
|
+
code: string;
|
|
12
|
+
};
|
|
13
|
+
type Obj = {
|
|
14
|
+
[k: string]: Plain;
|
|
15
|
+
};
|
|
16
|
+
/** Converts the `input` object of a vector into a sanitizer exchange. */
|
|
17
|
+
export declare function vectorExchange(inp: Obj): Exchange;
|
|
18
|
+
/** Parses a vector file (bytes) with the lexeme-preserving parser. */
|
|
19
|
+
export declare function parseVector(bytes: Uint8Array): Obj;
|
|
20
|
+
/** Runs one parsed vector. `root` is the conformance/ directory (for policy_ref). */
|
|
21
|
+
export declare function runVector(vec: Obj, root: string): VectorResult;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Executes one conformance vector (conformance/format.md, "Executing a vector").
|
|
3
|
+
// This is the test function every source must expose (sanitizer.md 16).
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.vectorExchange = vectorExchange;
|
|
6
|
+
exports.parseVector = parseVector;
|
|
7
|
+
exports.runVector = runVector;
|
|
8
|
+
const node_fs_1 = require("node:fs");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const json_js_1 = require("./json.js");
|
|
11
|
+
const policy_js_1 = require("./policy.js");
|
|
12
|
+
const sanitizer_js_1 = require("./sanitizer.js");
|
|
13
|
+
function decodeBytes(v) {
|
|
14
|
+
if (v === undefined || v === null)
|
|
15
|
+
return Buffer.alloc(0);
|
|
16
|
+
if (typeof v === 'string')
|
|
17
|
+
return Buffer.from(v, 'utf8');
|
|
18
|
+
if (typeof v === 'object' && !Array.isArray(v) && !(v instanceof json_js_1.Lex) && typeof v.b64 === 'string') {
|
|
19
|
+
return Buffer.from(v.b64, 'base64');
|
|
20
|
+
}
|
|
21
|
+
throw new Error('bad bytes value');
|
|
22
|
+
}
|
|
23
|
+
function num(v) {
|
|
24
|
+
return v instanceof json_js_1.Lex ? Number(v.lex) : undefined;
|
|
25
|
+
}
|
|
26
|
+
function headers(v) {
|
|
27
|
+
if (!Array.isArray(v))
|
|
28
|
+
return [];
|
|
29
|
+
return v.map((pair) => {
|
|
30
|
+
const p = pair;
|
|
31
|
+
return [p[0], decodeBytes(p[1])];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function total(v) {
|
|
35
|
+
if (v === 'unknown')
|
|
36
|
+
return 'unknown';
|
|
37
|
+
return num(v);
|
|
38
|
+
}
|
|
39
|
+
function bool(v) {
|
|
40
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Converts the `input` object of a vector into a sanitizer exchange. */
|
|
43
|
+
function vectorExchange(inp) {
|
|
44
|
+
const ex = {
|
|
45
|
+
dir: inp.dir,
|
|
46
|
+
method: inp.method ?? '',
|
|
47
|
+
raw_path: decodeBytes(inp.raw_path),
|
|
48
|
+
raw_query: inp.raw_query === undefined ? undefined : decodeBytes(inp.raw_query),
|
|
49
|
+
req_headers: headers(inp.req_headers),
|
|
50
|
+
req_body: decodeBytes(inp.req_body),
|
|
51
|
+
req_body_total: total(inp.req_body_total),
|
|
52
|
+
req_body_consumed: bool(inp.req_body_consumed),
|
|
53
|
+
req_body_decoded: bool(inp.req_body_decoded),
|
|
54
|
+
resp_headers: headers(inp.resp_headers),
|
|
55
|
+
resp_body: decodeBytes(inp.resp_body),
|
|
56
|
+
resp_body_total: total(inp.resp_body_total),
|
|
57
|
+
resp_body_consumed: bool(inp.resp_body_consumed),
|
|
58
|
+
resp_body_decoded: bool(inp.resp_body_decoded),
|
|
59
|
+
};
|
|
60
|
+
if (typeof inp.service_name === 'string')
|
|
61
|
+
ex.service_name = inp.service_name;
|
|
62
|
+
if (typeof inp.url_base === 'string')
|
|
63
|
+
ex.url_base = inp.url_base;
|
|
64
|
+
if (typeof inp.framework_route === 'string')
|
|
65
|
+
ex.framework_route = inp.framework_route;
|
|
66
|
+
if (typeof inp.framework === 'string')
|
|
67
|
+
ex.framework = inp.framework;
|
|
68
|
+
if (typeof inp.trace_id === 'string')
|
|
69
|
+
ex.trace_id = inp.trace_id;
|
|
70
|
+
if (typeof inp.inherited === 'boolean')
|
|
71
|
+
ex.inherited = inp.inherited;
|
|
72
|
+
const st = num(inp.status);
|
|
73
|
+
if (st !== undefined)
|
|
74
|
+
ex.status = st;
|
|
75
|
+
if (typeof inp.net_error === 'string')
|
|
76
|
+
ex.net_error = inp.net_error;
|
|
77
|
+
return ex;
|
|
78
|
+
}
|
|
79
|
+
/** Parses a vector file (bytes) with the lexeme-preserving parser. */
|
|
80
|
+
function parseVector(bytes) {
|
|
81
|
+
return (0, json_js_1.nodeToPlain)((0, json_js_1.parseJson)(bytes), false);
|
|
82
|
+
}
|
|
83
|
+
/** Runs one parsed vector. `root` is the conformance/ directory (for policy_ref). */
|
|
84
|
+
function runVector(vec, root) {
|
|
85
|
+
const now = typeof vec.now === 'string' ? (0, policy_js_1.parseTime)(vec.now) : undefined;
|
|
86
|
+
let policy;
|
|
87
|
+
let sanitizer;
|
|
88
|
+
try {
|
|
89
|
+
let body;
|
|
90
|
+
if (vec.policy_envelope !== undefined) {
|
|
91
|
+
const keys = new Map();
|
|
92
|
+
const fk = vec.fleet_keys;
|
|
93
|
+
for (const kid of Object.keys(fk ?? {})) {
|
|
94
|
+
const k = (0, policy_js_1.ed25519PublicKey)(fk[kid]);
|
|
95
|
+
if (k)
|
|
96
|
+
keys.set(kid, k);
|
|
97
|
+
}
|
|
98
|
+
body = (0, policy_js_1.verifyEnvelope)(vec.policy_envelope, keys, true).body;
|
|
99
|
+
}
|
|
100
|
+
else if (typeof vec.policy_ref === 'string') {
|
|
101
|
+
body = (0, policy_js_1.validateBodySchema)((0, json_js_1.nodeToPlain)((0, json_js_1.parseJson)((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, vec.policy_ref))), true));
|
|
102
|
+
}
|
|
103
|
+
else if (vec.policy !== undefined) {
|
|
104
|
+
body = (0, policy_js_1.validateBodySchema)(vec.policy);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw new policy_js_1.Refused('no_policy');
|
|
108
|
+
}
|
|
109
|
+
policy = new policy_js_1.Policy(body);
|
|
110
|
+
(0, policy_js_1.checkAcceptance)(policy, {
|
|
111
|
+
group: typeof vec.source_group === 'string' ? vec.source_group : '',
|
|
112
|
+
now: now ?? NaN,
|
|
113
|
+
lastIssuedSeq: num(vec.last_issued_seq) ?? 0,
|
|
114
|
+
minIssuedSeq: num(vec.min_issued_seq) ?? 0,
|
|
115
|
+
});
|
|
116
|
+
const ring = (0, policy_js_1.parseKeyRing)(typeof vec.hmac_key === 'string' ? vec.hmac_key : undefined);
|
|
117
|
+
sanitizer = new sanitizer_js_1.Sanitizer(policy, (0, policy_js_1.selectKey)(ring, policy));
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
if (e instanceof policy_js_1.Refused)
|
|
121
|
+
return { outcome: 'refused', code: e.code };
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
const r = sanitizer.run(vectorExchange(vec.input), now);
|
|
125
|
+
if (r.outcome === 'not_sampled')
|
|
126
|
+
return { outcome: 'not_sampled', sampled_by: r.sampledBy };
|
|
127
|
+
if (r.outcome === 'too_large')
|
|
128
|
+
return { outcome: 'refused', code: 'event_too_large' };
|
|
129
|
+
return { outcome: 'event', canonical: r.canonical };
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function workerDir(): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Directory of the compiled worker files. This source is the CommonJS flavour; scripts/build.mjs
|
|
3
|
+
// replaces dist/esm/worker/dir.js with an import.meta.url based one (tsc cannot emit both).
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.workerDir = workerDir;
|
|
6
|
+
function workerDir() {
|
|
7
|
+
return __dirname;
|
|
8
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Sanitizer } from '../sanitizer/sanitizer.js';
|
|
2
|
+
import type { Job } from './protocol.js';
|
|
3
|
+
interface Batch {
|
|
4
|
+
n: number;
|
|
5
|
+
bytes: number;
|
|
6
|
+
}
|
|
7
|
+
export interface PoolOptions {
|
|
8
|
+
size: number;
|
|
9
|
+
/** A batch came back: `stale` when it was posted before a discard(); recs/errs are then empty. */
|
|
10
|
+
onOut: (b: Batch & {
|
|
11
|
+
recs: string[];
|
|
12
|
+
errs: string[];
|
|
13
|
+
stale: boolean;
|
|
14
|
+
}) => void;
|
|
15
|
+
/** A worker died with `lost` batches in flight. */
|
|
16
|
+
onLost: (lost: Batch) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare class SanitizerPool {
|
|
19
|
+
private readonly o;
|
|
20
|
+
private workers;
|
|
21
|
+
private epoch;
|
|
22
|
+
private seq;
|
|
23
|
+
private nextGen;
|
|
24
|
+
private readonly gens;
|
|
25
|
+
private closed;
|
|
26
|
+
constructor(o: PoolOptions);
|
|
27
|
+
/** True when at least one worker is up. */
|
|
28
|
+
get usable(): boolean;
|
|
29
|
+
/** Starts the first worker (idempotent). Never throws. */
|
|
30
|
+
warm(): void;
|
|
31
|
+
/** The policy of this sanitizer can be shipped to a worker. */
|
|
32
|
+
canHandle(san: Sanitizer): boolean;
|
|
33
|
+
private spawn;
|
|
34
|
+
private onMessage;
|
|
35
|
+
private pick;
|
|
36
|
+
/** Posts a batch of jobs. False (nothing posted) when no worker is ready. */
|
|
37
|
+
post(items: Array<{
|
|
38
|
+
san: Sanitizer;
|
|
39
|
+
job: Job;
|
|
40
|
+
}>, bytes: number, sourceId: string): boolean;
|
|
41
|
+
/** Results of everything posted so far are ignored when they arrive (kill switch, revocation). */
|
|
42
|
+
discard(): void;
|
|
43
|
+
/** Resolves when every batch posted before this call has come back. */
|
|
44
|
+
sync(): Promise<void>;
|
|
45
|
+
terminate(): void;
|
|
46
|
+
}
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Main-thread side of the sanitizer worker pool: a small number of worker threads that turn
|
|
3
|
+
// wire exchanges into OTLP records off the request path. Bounded and drop-on-pressure by
|
|
4
|
+
// construction (the agent charges every posted job to its pending limits and drops when
|
|
5
|
+
// they are exceeded); fail-open for the app: a dead worker just means "sanitize inline".
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SanitizerPool = void 0;
|
|
8
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const log_js_1 = require("../log.js");
|
|
11
|
+
const dir_js_1 = require("./dir.js");
|
|
12
|
+
class SanitizerPool {
|
|
13
|
+
o;
|
|
14
|
+
workers = [];
|
|
15
|
+
epoch = 0;
|
|
16
|
+
seq = 0;
|
|
17
|
+
nextGen = 1;
|
|
18
|
+
gens = new WeakMap();
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(o) {
|
|
21
|
+
this.o = o;
|
|
22
|
+
}
|
|
23
|
+
/** True when at least one worker is up. */
|
|
24
|
+
get usable() {
|
|
25
|
+
return !this.closed && this.workers.some((w) => w.ready && !w.dead);
|
|
26
|
+
}
|
|
27
|
+
/** Starts the first worker (idempotent). Never throws. */
|
|
28
|
+
warm() {
|
|
29
|
+
if (this.closed || this.workers.length > 0)
|
|
30
|
+
return;
|
|
31
|
+
this.spawn();
|
|
32
|
+
}
|
|
33
|
+
/** The policy of this sanitizer can be shipped to a worker. */
|
|
34
|
+
canHandle(san) {
|
|
35
|
+
if (!san.policy.bodyBytes)
|
|
36
|
+
return false;
|
|
37
|
+
const g = this.gens.get(san);
|
|
38
|
+
return g === undefined || g > this.nextGen - 12;
|
|
39
|
+
}
|
|
40
|
+
spawn() {
|
|
41
|
+
try {
|
|
42
|
+
const worker = new node_worker_threads_1.Worker((0, node_path_1.join)((0, dir_js_1.workerDir)(), 'sanitizer-worker.js'), {
|
|
43
|
+
// The parent's --import/--require preloads must not run again in the worker.
|
|
44
|
+
execArgv: [],
|
|
45
|
+
env: { TT_DISABLED: '1' },
|
|
46
|
+
workerData: { ttSanitizer: true },
|
|
47
|
+
resourceLimits: { maxOldGenerationSizeMb: 192 },
|
|
48
|
+
});
|
|
49
|
+
const w = { worker, ready: false, dead: false, gens: new Set(), batches: [], syncs: new Map() };
|
|
50
|
+
worker.unref();
|
|
51
|
+
worker.on('message', (m) => this.onMessage(w, m));
|
|
52
|
+
const die = () => {
|
|
53
|
+
if (w.dead)
|
|
54
|
+
return;
|
|
55
|
+
w.dead = true;
|
|
56
|
+
w.ready = false;
|
|
57
|
+
let n = 0;
|
|
58
|
+
let bytes = 0;
|
|
59
|
+
for (const b of w.batches) {
|
|
60
|
+
n += b.n;
|
|
61
|
+
bytes += b.bytes;
|
|
62
|
+
}
|
|
63
|
+
w.batches = [];
|
|
64
|
+
for (const r of w.syncs.values())
|
|
65
|
+
r();
|
|
66
|
+
w.syncs.clear();
|
|
67
|
+
if (n > 0)
|
|
68
|
+
this.o.onLost({ n, bytes });
|
|
69
|
+
(0, log_js_1.debug)('sanitizer_worker', 'down');
|
|
70
|
+
};
|
|
71
|
+
worker.on('error', die);
|
|
72
|
+
worker.on('exit', die);
|
|
73
|
+
this.workers.push(w);
|
|
74
|
+
return w;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
onMessage(w, m) {
|
|
81
|
+
if (m.t === 'ready') {
|
|
82
|
+
w.ready = true;
|
|
83
|
+
}
|
|
84
|
+
else if (m.t === 'out') {
|
|
85
|
+
const b = w.batches.shift();
|
|
86
|
+
if (!b)
|
|
87
|
+
return;
|
|
88
|
+
const stale = m.epoch !== this.epoch;
|
|
89
|
+
this.o.onOut({ ...b, recs: stale ? [] : m.recs, errs: stale ? [] : m.errs, stale });
|
|
90
|
+
}
|
|
91
|
+
else if (m.t === 'synced') {
|
|
92
|
+
const f = w.syncs.get(m.seq);
|
|
93
|
+
if (f) {
|
|
94
|
+
w.syncs.delete(m.seq);
|
|
95
|
+
f();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
pick() {
|
|
100
|
+
let best = null;
|
|
101
|
+
for (const w of this.workers) {
|
|
102
|
+
if (!w.ready || w.dead)
|
|
103
|
+
continue;
|
|
104
|
+
if (!best || w.batches.length < best.batches.length)
|
|
105
|
+
best = w;
|
|
106
|
+
}
|
|
107
|
+
// Grow (up to the configured size) only when the least busy worker already has a backlog.
|
|
108
|
+
if ((!best || best.batches.length >= 2) && this.workers.filter((w) => !w.dead).length < this.o.size)
|
|
109
|
+
this.spawn();
|
|
110
|
+
return best;
|
|
111
|
+
}
|
|
112
|
+
/** Posts a batch of jobs. False (nothing posted) when no worker is ready. */
|
|
113
|
+
post(items, bytes, sourceId) {
|
|
114
|
+
const w = this.pick();
|
|
115
|
+
if (!w)
|
|
116
|
+
return false;
|
|
117
|
+
try {
|
|
118
|
+
const jobs = [];
|
|
119
|
+
for (const { san, job } of items) {
|
|
120
|
+
let g = this.gens.get(san);
|
|
121
|
+
if (g === undefined) {
|
|
122
|
+
g = this.nextGen++;
|
|
123
|
+
this.gens.set(san, g);
|
|
124
|
+
}
|
|
125
|
+
job.gen = g;
|
|
126
|
+
jobs.push(job);
|
|
127
|
+
if (!w.gens.has(g)) {
|
|
128
|
+
const p = san.policy;
|
|
129
|
+
const msg = {
|
|
130
|
+
t: 'policy',
|
|
131
|
+
gen: g,
|
|
132
|
+
body: Uint8Array.from(p.bodyBytes),
|
|
133
|
+
hash: p.hash,
|
|
134
|
+
kid: san.key.kid,
|
|
135
|
+
key: Uint8Array.from(san.key.key),
|
|
136
|
+
rateOverrideBp: san.options.rateOverrideBp ?? null,
|
|
137
|
+
};
|
|
138
|
+
w.worker.postMessage(msg);
|
|
139
|
+
w.gens.add(g);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
w.worker.postMessage({ t: 'run', epoch: this.epoch, sourceId, jobs });
|
|
143
|
+
w.batches.push({ n: jobs.length, bytes });
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Results of everything posted so far are ignored when they arrive (kill switch, revocation). */
|
|
151
|
+
discard() {
|
|
152
|
+
this.epoch++;
|
|
153
|
+
}
|
|
154
|
+
/** Resolves when every batch posted before this call has come back. */
|
|
155
|
+
sync() {
|
|
156
|
+
const ps = [];
|
|
157
|
+
for (const w of this.workers) {
|
|
158
|
+
if (w.dead || !w.ready || w.batches.length === 0)
|
|
159
|
+
continue;
|
|
160
|
+
const seq = ++this.seq;
|
|
161
|
+
ps.push(new Promise((r) => {
|
|
162
|
+
w.syncs.set(seq, r);
|
|
163
|
+
w.worker.postMessage({ t: 'sync', seq });
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
return Promise.all(ps).then(() => undefined);
|
|
167
|
+
}
|
|
168
|
+
terminate() {
|
|
169
|
+
this.closed = true;
|
|
170
|
+
for (const w of this.workers) {
|
|
171
|
+
w.dead = true;
|
|
172
|
+
for (const r of w.syncs.values())
|
|
173
|
+
r();
|
|
174
|
+
void w.worker.terminate().catch(() => undefined);
|
|
175
|
+
}
|
|
176
|
+
this.workers = [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.SanitizerPool = SanitizerPool;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { WireExchange } from '../capture/wire.js';
|
|
2
|
+
import type { JobMeta } from '../export/pipeline.js';
|
|
3
|
+
import type { Decision } from '../sanitizer/sanitizer.js';
|
|
4
|
+
export interface Job {
|
|
5
|
+
gen: number;
|
|
6
|
+
decision: Decision;
|
|
7
|
+
meta: JobMeta;
|
|
8
|
+
wire: WireExchange;
|
|
9
|
+
}
|
|
10
|
+
export type ToWorker = {
|
|
11
|
+
t: 'policy';
|
|
12
|
+
gen: number;
|
|
13
|
+
body: Uint8Array;
|
|
14
|
+
hash: string;
|
|
15
|
+
kid: string;
|
|
16
|
+
key: Uint8Array;
|
|
17
|
+
rateOverrideBp: number | null;
|
|
18
|
+
} | {
|
|
19
|
+
t: 'run';
|
|
20
|
+
epoch: number;
|
|
21
|
+
sourceId: string;
|
|
22
|
+
jobs: Job[];
|
|
23
|
+
} | {
|
|
24
|
+
t: 'sync';
|
|
25
|
+
seq: number;
|
|
26
|
+
};
|
|
27
|
+
export type FromWorker = {
|
|
28
|
+
t: 'ready';
|
|
29
|
+
} | {
|
|
30
|
+
t: 'out';
|
|
31
|
+
epoch: number;
|
|
32
|
+
recs: string[];
|
|
33
|
+
errs: string[];
|
|
34
|
+
} | {
|
|
35
|
+
t: 'synced';
|
|
36
|
+
seq: number;
|
|
37
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|