@traffic-twin/sdk 0.0.0-stage → 0.1.1
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,50 @@
|
|
|
1
|
+
import type https from 'node:https';
|
|
2
|
+
import { type QueuedEvent, type Resource } from './event.js';
|
|
3
|
+
export interface ExportCounters {
|
|
4
|
+
events_sent: number;
|
|
5
|
+
events_dropped_queue: number;
|
|
6
|
+
events_rejected: number;
|
|
7
|
+
send_errors: number;
|
|
8
|
+
}
|
|
9
|
+
export interface ExporterOptions {
|
|
10
|
+
url: string;
|
|
11
|
+
agent: https.Agent;
|
|
12
|
+
resource: Resource;
|
|
13
|
+
counters: ExportCounters;
|
|
14
|
+
onError: (code: string) => void;
|
|
15
|
+
maxEvents?: number;
|
|
16
|
+
maxBytes?: number;
|
|
17
|
+
batchEvents?: number;
|
|
18
|
+
batchBytes?: number;
|
|
19
|
+
flushMs?: number;
|
|
20
|
+
maxRetries?: number;
|
|
21
|
+
baseBackoffMs?: number;
|
|
22
|
+
maxBackoffMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class Exporter {
|
|
25
|
+
private queue;
|
|
26
|
+
private head;
|
|
27
|
+
private bytes;
|
|
28
|
+
private inFlight;
|
|
29
|
+
private timer;
|
|
30
|
+
private retryTimer;
|
|
31
|
+
private stopped;
|
|
32
|
+
/** Bumped by clear() (kill switch, revocation): a batch built under an older generation is never sent. */
|
|
33
|
+
private gen;
|
|
34
|
+
private readonly o;
|
|
35
|
+
constructor(opts: ExporterOptions);
|
|
36
|
+
get size(): {
|
|
37
|
+
events: number;
|
|
38
|
+
bytes: number;
|
|
39
|
+
};
|
|
40
|
+
setTarget(url: string, agent: https.Agent): void;
|
|
41
|
+
enqueue(e: QueuedEvent): void;
|
|
42
|
+
/** Drops everything queued (kill switch). */
|
|
43
|
+
clear(): void;
|
|
44
|
+
stop(): void;
|
|
45
|
+
/** Sends what is queued; resolves when the queue is empty or a send failed. */
|
|
46
|
+
flush(): Promise<void>;
|
|
47
|
+
private kick;
|
|
48
|
+
private takeBatch;
|
|
49
|
+
private sendOne;
|
|
50
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OTLP/HTTP JSON exporter (event.md 4): bounded queue, batches of at most 512 events or
|
|
3
|
+
// 4 MiB, retry with exponential backoff and jitter on 429/502/503/504 and network errors
|
|
4
|
+
// (at most 5 times, Retry-After honoured), drop-oldest under pressure. The app never waits.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Exporter = void 0;
|
|
7
|
+
const event_js_1 = require("./event.js");
|
|
8
|
+
const http_js_1 = require("../fleet/http.js");
|
|
9
|
+
const log_js_1 = require("../log.js");
|
|
10
|
+
const sizeOf = (e) => e.size ?? e.json.length;
|
|
11
|
+
const RETRYABLE = new Set([429, 502, 503, 504]);
|
|
12
|
+
const DROP = new Set([400, 401, 403, 413]);
|
|
13
|
+
class Exporter {
|
|
14
|
+
queue = [];
|
|
15
|
+
head = 0;
|
|
16
|
+
bytes = 0;
|
|
17
|
+
inFlight = false;
|
|
18
|
+
timer;
|
|
19
|
+
retryTimer;
|
|
20
|
+
stopped = false;
|
|
21
|
+
/** Bumped by clear() (kill switch, revocation): a batch built under an older generation is never sent. */
|
|
22
|
+
gen = 0;
|
|
23
|
+
o;
|
|
24
|
+
constructor(opts) {
|
|
25
|
+
this.o = {
|
|
26
|
+
maxEvents: 10_000,
|
|
27
|
+
maxBytes: 16 << 20,
|
|
28
|
+
batchEvents: 512,
|
|
29
|
+
batchBytes: 4 << 20,
|
|
30
|
+
flushMs: 1000,
|
|
31
|
+
maxRetries: 5,
|
|
32
|
+
baseBackoffMs: 1000,
|
|
33
|
+
maxBackoffMs: 30_000,
|
|
34
|
+
...opts,
|
|
35
|
+
};
|
|
36
|
+
this.timer = setInterval(() => this.kick(), this.o.flushMs);
|
|
37
|
+
this.timer.unref();
|
|
38
|
+
}
|
|
39
|
+
get size() {
|
|
40
|
+
return { events: this.queue.length - this.head, bytes: this.bytes };
|
|
41
|
+
}
|
|
42
|
+
setTarget(url, agent) {
|
|
43
|
+
this.o.url = url;
|
|
44
|
+
this.o.agent = agent;
|
|
45
|
+
}
|
|
46
|
+
enqueue(e) {
|
|
47
|
+
if (this.stopped)
|
|
48
|
+
return;
|
|
49
|
+
this.queue.push(e);
|
|
50
|
+
this.bytes += sizeOf(e);
|
|
51
|
+
while (this.queue.length - this.head > this.o.maxEvents || this.bytes > this.o.maxBytes) {
|
|
52
|
+
const old = this.queue[this.head++];
|
|
53
|
+
this.bytes -= sizeOf(old);
|
|
54
|
+
this.o.counters.events_dropped_queue++;
|
|
55
|
+
}
|
|
56
|
+
if (this.head > 4096 && this.head * 2 > this.queue.length) {
|
|
57
|
+
this.queue = this.queue.slice(this.head);
|
|
58
|
+
this.head = 0;
|
|
59
|
+
}
|
|
60
|
+
if (this.queue.length - this.head >= this.o.batchEvents)
|
|
61
|
+
setImmediate(() => this.kick());
|
|
62
|
+
}
|
|
63
|
+
/** Drops everything queued (kill switch). */
|
|
64
|
+
clear() {
|
|
65
|
+
this.gen++;
|
|
66
|
+
this.o.counters.events_dropped_queue += this.queue.length - this.head;
|
|
67
|
+
this.queue = [];
|
|
68
|
+
this.head = 0;
|
|
69
|
+
this.bytes = 0;
|
|
70
|
+
}
|
|
71
|
+
stop() {
|
|
72
|
+
this.stopped = true;
|
|
73
|
+
if (this.timer)
|
|
74
|
+
clearInterval(this.timer);
|
|
75
|
+
if (this.retryTimer)
|
|
76
|
+
clearTimeout(this.retryTimer);
|
|
77
|
+
}
|
|
78
|
+
/** Sends what is queued; resolves when the queue is empty or a send failed. */
|
|
79
|
+
async flush() {
|
|
80
|
+
for (let i = 0; i < 1000 && this.queue.length - this.head > 0 && !this.stopped; i++) {
|
|
81
|
+
if (this.inFlight) {
|
|
82
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const ok = await this.sendOne(false);
|
|
86
|
+
if (!ok)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
kick() {
|
|
91
|
+
if (this.inFlight || this.stopped || this.retryTimer || this.queue.length - this.head === 0)
|
|
92
|
+
return;
|
|
93
|
+
void this.sendOne(true);
|
|
94
|
+
}
|
|
95
|
+
takeBatch() {
|
|
96
|
+
const batch = [];
|
|
97
|
+
let size = 0;
|
|
98
|
+
while (this.head < this.queue.length && batch.length < this.o.batchEvents) {
|
|
99
|
+
const e = this.queue[this.head];
|
|
100
|
+
const sz = sizeOf(e);
|
|
101
|
+
if (batch.length > 0 && size + sz > this.o.batchBytes)
|
|
102
|
+
break;
|
|
103
|
+
batch.push(e);
|
|
104
|
+
size += sz;
|
|
105
|
+
this.head++;
|
|
106
|
+
this.bytes -= sz;
|
|
107
|
+
}
|
|
108
|
+
return batch;
|
|
109
|
+
}
|
|
110
|
+
async sendOne(retryLater) {
|
|
111
|
+
this.inFlight = true;
|
|
112
|
+
const batch = this.takeBatch();
|
|
113
|
+
const gen = this.gen;
|
|
114
|
+
try {
|
|
115
|
+
for (let attempt = 0;; attempt++) {
|
|
116
|
+
// Kill switch or revocation while this batch was held (first attempt or between
|
|
117
|
+
// retries, Retry-After included): it is dropped, never sent.
|
|
118
|
+
if (gen !== this.gen) {
|
|
119
|
+
this.o.counters.events_dropped_queue += batch.length;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
let res;
|
|
123
|
+
let netErr = false;
|
|
124
|
+
try {
|
|
125
|
+
res = await (0, http_js_1.request)({
|
|
126
|
+
method: 'POST',
|
|
127
|
+
url: this.o.url,
|
|
128
|
+
agent: this.o.agent,
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
body: (0, event_js_1.otlpBody)(batch, this.o.resource, Date.now()),
|
|
131
|
+
timeoutMs: 15_000,
|
|
132
|
+
maxResponseBytes: 64 << 10,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
netErr = true;
|
|
137
|
+
}
|
|
138
|
+
if (res && res.status >= 200 && res.status < 300) {
|
|
139
|
+
let rejected = 0;
|
|
140
|
+
try {
|
|
141
|
+
const j = JSON.parse(res.body.toString('utf8') || '{}');
|
|
142
|
+
rejected = Number(j.partialSuccess?.rejectedLogRecords ?? 0) || 0;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
rejected = 0;
|
|
146
|
+
}
|
|
147
|
+
rejected = Math.min(Math.max(0, rejected), batch.length);
|
|
148
|
+
this.o.counters.events_sent += batch.length - rejected;
|
|
149
|
+
this.o.counters.events_rejected += rejected;
|
|
150
|
+
if (rejected)
|
|
151
|
+
this.o.onError('intake_partial_reject');
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const retryable = netErr || (res !== undefined && RETRYABLE.has(res.status));
|
|
155
|
+
if (!retryable || attempt >= this.o.maxRetries || this.stopped) {
|
|
156
|
+
if (res && DROP.has(res.status))
|
|
157
|
+
this.o.counters.events_rejected += batch.length;
|
|
158
|
+
else
|
|
159
|
+
this.o.counters.events_dropped_queue += batch.length;
|
|
160
|
+
this.o.counters.send_errors++;
|
|
161
|
+
this.o.onError(res ? 'intake_http_' + res.status : 'intake_network');
|
|
162
|
+
(0, log_js_1.debug)('export_drop', res ? res.status : 'network');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
this.o.counters.send_errors++;
|
|
166
|
+
let delay = Math.min(this.o.maxBackoffMs, this.o.baseBackoffMs * 2 ** attempt);
|
|
167
|
+
delay = delay / 2 + Math.random() * (delay / 2);
|
|
168
|
+
const ra = res?.headers['retry-after'];
|
|
169
|
+
if (typeof ra === 'string') {
|
|
170
|
+
const secs = /^[0-9]+$/.test(ra.trim()) ? Number(ra) * 1000 : Date.parse(ra) - Date.now();
|
|
171
|
+
if (Number.isFinite(secs) && secs > 0)
|
|
172
|
+
delay = Math.min(Math.max(delay, secs), 300_000);
|
|
173
|
+
}
|
|
174
|
+
await new Promise((r) => {
|
|
175
|
+
this.retryTimer = setTimeout(() => {
|
|
176
|
+
this.retryTimer = undefined;
|
|
177
|
+
r();
|
|
178
|
+
}, delay);
|
|
179
|
+
this.retryTimer.unref();
|
|
180
|
+
});
|
|
181
|
+
if (this.stopped)
|
|
182
|
+
return false;
|
|
183
|
+
void retryLater;
|
|
184
|
+
// (the generation is checked again at the top of the loop)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
this.inFlight = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
exports.Exporter = Exporter;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type WireExchange } from '../capture/wire.js';
|
|
2
|
+
import type { Decision, NetError, Sanitizer } from '../sanitizer/sanitizer.js';
|
|
3
|
+
/** Per-exchange facts the source fills in (event.md 1). */
|
|
4
|
+
export interface JobMeta {
|
|
5
|
+
dir: 'inbound' | 'outbound';
|
|
6
|
+
tsMs: number;
|
|
7
|
+
latencyMs: number;
|
|
8
|
+
status?: number;
|
|
9
|
+
netError?: NetError;
|
|
10
|
+
traceId?: string;
|
|
11
|
+
spanId?: string;
|
|
12
|
+
parentSpanId?: string;
|
|
13
|
+
}
|
|
14
|
+
export type JobResult = {
|
|
15
|
+
record: string;
|
|
16
|
+
} | {
|
|
17
|
+
err: 'event_too_large' | 'event_invalid_or_too_large' | 'sanitizer_error';
|
|
18
|
+
} | null;
|
|
19
|
+
/** Null when the exchange turned out not to be sampled. Never throws. */
|
|
20
|
+
export declare function sanitizeToRecord(san: Sanitizer, wire: WireExchange, decision: Decision, meta: JobMeta, sourceId: string, nowMs: number): JobResult;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The off-path half of one exchange: wire form -> sanitizer -> canonical event -> OTLP
|
|
3
|
+
// logRecord. Used both inline (main thread) and by the sanitizer worker threads, so the
|
|
4
|
+
// output is the same bytes either way.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sanitizeToRecord = sanitizeToRecord;
|
|
7
|
+
const wire_js_1 = require("../capture/wire.js");
|
|
8
|
+
const event_js_1 = require("./event.js");
|
|
9
|
+
/** Null when the exchange turned out not to be sampled. Never throws. */
|
|
10
|
+
function sanitizeToRecord(san, wire, decision, meta, sourceId, nowMs) {
|
|
11
|
+
try {
|
|
12
|
+
const r = san.run((0, wire_js_1.wireToExchange)(wire), nowMs, decision);
|
|
13
|
+
if (r.outcome === 'not_sampled')
|
|
14
|
+
return null;
|
|
15
|
+
if (r.outcome === 'too_large')
|
|
16
|
+
return { err: 'event_too_large' };
|
|
17
|
+
const p = san.policy;
|
|
18
|
+
const json = (0, event_js_1.buildEvent)((0, event_js_1.serializedParts)(r.parts), {
|
|
19
|
+
policyId: p.policyId,
|
|
20
|
+
policyVersion: p.version,
|
|
21
|
+
policyHash: p.hash,
|
|
22
|
+
sourceId,
|
|
23
|
+
dir: meta.dir,
|
|
24
|
+
tsMs: meta.tsMs,
|
|
25
|
+
latencyMs: meta.latencyMs,
|
|
26
|
+
status: meta.status,
|
|
27
|
+
netError: meta.netError,
|
|
28
|
+
traceId: meta.traceId,
|
|
29
|
+
spanId: meta.spanId,
|
|
30
|
+
parentSpanId: meta.parentSpanId,
|
|
31
|
+
});
|
|
32
|
+
if (json === null)
|
|
33
|
+
return { err: 'event_invalid_or_too_large' };
|
|
34
|
+
return { record: (0, event_js_1.makeRecord)({ json, tsMs: meta.tsMs, traceId: meta.traceId, spanId: meta.spanId, policyHash: p.hash }, Date.now()) };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 0: an internal error never produces an event.
|
|
38
|
+
return { err: 'sanitizer_error' };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type KeyObject } from 'node:crypto';
|
|
2
|
+
export declare function tlv(tag: number, ...content: Buffer[]): Buffer;
|
|
3
|
+
export declare const seq: (...c: Buffer[]) => Buffer<ArrayBufferLike>;
|
|
4
|
+
export declare const set: (...c: Buffer[]) => Buffer<ArrayBufferLike>;
|
|
5
|
+
export declare const ctx: (n: number, ...c: Buffer[]) => Buffer<ArrayBufferLike>;
|
|
6
|
+
export declare const octets: (b: Buffer) => Buffer<ArrayBufferLike>;
|
|
7
|
+
export declare const bitString: (b: Buffer) => Buffer<ArrayBufferLike>;
|
|
8
|
+
export declare const utf8String: (s: string) => Buffer<ArrayBufferLike>;
|
|
9
|
+
export declare const bool: (v: boolean) => Buffer<ArrayBufferLike>;
|
|
10
|
+
export declare const nul: () => Buffer<ArrayBuffer>;
|
|
11
|
+
export declare function int(v: number | Buffer): Buffer;
|
|
12
|
+
export declare function oid(dotted: string): Buffer;
|
|
13
|
+
export declare const OID: {
|
|
14
|
+
commonName: string;
|
|
15
|
+
ecdsaWithSHA256: string;
|
|
16
|
+
ed25519: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function name(cn: string): Buffer;
|
|
19
|
+
export interface DerNode {
|
|
20
|
+
tag: number;
|
|
21
|
+
start: number;
|
|
22
|
+
header: number;
|
|
23
|
+
length: number;
|
|
24
|
+
raw: Buffer;
|
|
25
|
+
content: Buffer;
|
|
26
|
+
}
|
|
27
|
+
/** Reads one TLV at `off`. */
|
|
28
|
+
export declare function readTlv(buf: Buffer, off?: number): DerNode;
|
|
29
|
+
export declare function children(node: DerNode): DerNode[];
|
|
30
|
+
export declare function pem(label: string, der: Buffer): string;
|
|
31
|
+
export declare function pemBlocks(text: string, label: string): Buffer[];
|
|
32
|
+
/** Signs DER `data` with `key` and returns the AlgorithmIdentifier and the signature bits. */
|
|
33
|
+
export declare function signDer(data: Buffer, key: KeyObject): {
|
|
34
|
+
alg: Buffer;
|
|
35
|
+
sig: Buffer;
|
|
36
|
+
};
|
|
37
|
+
export interface GeneratedCsr {
|
|
38
|
+
csrPem: string;
|
|
39
|
+
keyPem: string;
|
|
40
|
+
}
|
|
41
|
+
/** Generates a key pair (default ECDSA P-256) and a PKCS#10 request with CN = commonName. */
|
|
42
|
+
export declare function generateCsr(commonName: string, kind?: 'ec' | 'ed25519'): GeneratedCsr;
|
|
43
|
+
/** base64url(SHA-256(DER)), the CA pin format of the enrollment token. */
|
|
44
|
+
export declare function pinOf(der: Buffer): string;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tiny DER writer/reader, enough for a PKCS#10 certificate request (RFC 2986) with an
|
|
3
|
+
// ECDSA P-256 or Ed25519 key, without any dependency.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.OID = exports.nul = exports.bool = exports.utf8String = exports.bitString = exports.octets = exports.ctx = exports.set = exports.seq = void 0;
|
|
6
|
+
exports.tlv = tlv;
|
|
7
|
+
exports.int = int;
|
|
8
|
+
exports.oid = oid;
|
|
9
|
+
exports.name = name;
|
|
10
|
+
exports.readTlv = readTlv;
|
|
11
|
+
exports.children = children;
|
|
12
|
+
exports.pem = pem;
|
|
13
|
+
exports.pemBlocks = pemBlocks;
|
|
14
|
+
exports.signDer = signDer;
|
|
15
|
+
exports.generateCsr = generateCsr;
|
|
16
|
+
exports.pinOf = pinOf;
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
function len(n) {
|
|
19
|
+
if (n < 0x80)
|
|
20
|
+
return Buffer.from([n]);
|
|
21
|
+
const bytes = [];
|
|
22
|
+
while (n > 0) {
|
|
23
|
+
bytes.unshift(n & 0xff);
|
|
24
|
+
n = Math.floor(n / 256);
|
|
25
|
+
}
|
|
26
|
+
return Buffer.from([0x80 | bytes.length, ...bytes]);
|
|
27
|
+
}
|
|
28
|
+
function tlv(tag, ...content) {
|
|
29
|
+
const c = Buffer.concat(content);
|
|
30
|
+
return Buffer.concat([Buffer.from([tag]), len(c.length), c]);
|
|
31
|
+
}
|
|
32
|
+
const seq = (...c) => tlv(0x30, ...c);
|
|
33
|
+
exports.seq = seq;
|
|
34
|
+
const set = (...c) => tlv(0x31, ...c);
|
|
35
|
+
exports.set = set;
|
|
36
|
+
const ctx = (n, ...c) => tlv(0xa0 + n, ...c);
|
|
37
|
+
exports.ctx = ctx;
|
|
38
|
+
const octets = (b) => tlv(0x04, b);
|
|
39
|
+
exports.octets = octets;
|
|
40
|
+
const bitString = (b) => tlv(0x03, Buffer.from([0]), b);
|
|
41
|
+
exports.bitString = bitString;
|
|
42
|
+
const utf8String = (s) => tlv(0x0c, Buffer.from(s, 'utf8'));
|
|
43
|
+
exports.utf8String = utf8String;
|
|
44
|
+
const bool = (v) => tlv(0x01, Buffer.from([v ? 0xff : 0]));
|
|
45
|
+
exports.bool = bool;
|
|
46
|
+
const nul = () => Buffer.from([0x05, 0x00]);
|
|
47
|
+
exports.nul = nul;
|
|
48
|
+
function int(v) {
|
|
49
|
+
let b;
|
|
50
|
+
if (typeof v === 'number') {
|
|
51
|
+
let h = v.toString(16);
|
|
52
|
+
if (h.length % 2)
|
|
53
|
+
h = '0' + h;
|
|
54
|
+
b = Buffer.from(h, 'hex');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
b = v;
|
|
58
|
+
}
|
|
59
|
+
while (b.length > 1 && b[0] === 0 && b[1] < 0x80)
|
|
60
|
+
b = b.subarray(1);
|
|
61
|
+
if (b[0] >= 0x80)
|
|
62
|
+
b = Buffer.concat([Buffer.from([0]), b]);
|
|
63
|
+
return tlv(0x02, b);
|
|
64
|
+
}
|
|
65
|
+
function oid(dotted) {
|
|
66
|
+
const parts = dotted.split('.').map(Number);
|
|
67
|
+
const out = [parts[0] * 40 + parts[1]];
|
|
68
|
+
for (const p of parts.slice(2)) {
|
|
69
|
+
const stack = [p & 0x7f];
|
|
70
|
+
let v = Math.floor(p / 128);
|
|
71
|
+
while (v > 0) {
|
|
72
|
+
stack.unshift((v & 0x7f) | 0x80);
|
|
73
|
+
v = Math.floor(v / 128);
|
|
74
|
+
}
|
|
75
|
+
out.push(...stack);
|
|
76
|
+
}
|
|
77
|
+
return tlv(0x06, Buffer.from(out));
|
|
78
|
+
}
|
|
79
|
+
exports.OID = {
|
|
80
|
+
commonName: '2.5.4.3',
|
|
81
|
+
ecdsaWithSHA256: '1.2.840.10045.4.3.2',
|
|
82
|
+
ed25519: '1.3.101.112',
|
|
83
|
+
};
|
|
84
|
+
function name(cn) {
|
|
85
|
+
return (0, exports.seq)((0, exports.set)((0, exports.seq)(oid(exports.OID.commonName), (0, exports.utf8String)(cn))));
|
|
86
|
+
}
|
|
87
|
+
/** Reads one TLV at `off`. */
|
|
88
|
+
function readTlv(buf, off = 0) {
|
|
89
|
+
const tag = buf[off];
|
|
90
|
+
let l = buf[off + 1];
|
|
91
|
+
let header = 2;
|
|
92
|
+
if (l & 0x80) {
|
|
93
|
+
const n = l & 0x7f;
|
|
94
|
+
if (n === 0 || n > 4)
|
|
95
|
+
throw new Error('der length');
|
|
96
|
+
l = 0;
|
|
97
|
+
for (let i = 0; i < n; i++)
|
|
98
|
+
l = l * 256 + buf[off + 2 + i];
|
|
99
|
+
header += n;
|
|
100
|
+
}
|
|
101
|
+
if (off + header + l > buf.length)
|
|
102
|
+
throw new Error('der overflow');
|
|
103
|
+
return { tag, start: off, header, length: l, raw: buf.subarray(off, off + header + l), content: buf.subarray(off + header, off + header + l) };
|
|
104
|
+
}
|
|
105
|
+
function children(node) {
|
|
106
|
+
const out = [];
|
|
107
|
+
let off = 0;
|
|
108
|
+
while (off < node.content.length) {
|
|
109
|
+
const c = readTlv(node.content, off);
|
|
110
|
+
out.push(c);
|
|
111
|
+
off += c.header + c.length;
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function pem(label, der) {
|
|
116
|
+
const b64 = der.toString('base64').replace(/(.{64})/g, '$1\n').replace(/\n$/, '');
|
|
117
|
+
return `-----BEGIN ${label}-----\n${b64}\n-----END ${label}-----\n`;
|
|
118
|
+
}
|
|
119
|
+
function pemBlocks(text, label) {
|
|
120
|
+
const re = new RegExp(`-----BEGIN ${label}-----([\\s\\S]*?)-----END ${label}-----`, 'g');
|
|
121
|
+
const out = [];
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = re.exec(text)))
|
|
124
|
+
out.push(Buffer.from(m[1].replace(/\s+/g, ''), 'base64'));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
/** Signs DER `data` with `key` and returns the AlgorithmIdentifier and the signature bits. */
|
|
128
|
+
function signDer(data, key) {
|
|
129
|
+
if (key.asymmetricKeyType === 'ed25519')
|
|
130
|
+
return { alg: (0, exports.seq)(oid(exports.OID.ed25519)), sig: (0, node_crypto_1.sign)(null, data, key) };
|
|
131
|
+
return { alg: (0, exports.seq)(oid(exports.OID.ecdsaWithSHA256)), sig: (0, node_crypto_1.sign)('sha256', data, key) };
|
|
132
|
+
}
|
|
133
|
+
/** Generates a key pair (default ECDSA P-256) and a PKCS#10 request with CN = commonName. */
|
|
134
|
+
function generateCsr(commonName, kind = 'ec') {
|
|
135
|
+
const { publicKey, privateKey } = kind === 'ed25519'
|
|
136
|
+
? (0, node_crypto_1.generateKeyPairSync)('ed25519')
|
|
137
|
+
: (0, node_crypto_1.generateKeyPairSync)('ec', { namedCurve: 'prime256v1' });
|
|
138
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
139
|
+
const info = (0, exports.seq)(int(0), name(commonName), spki, (0, exports.ctx)(0));
|
|
140
|
+
const { alg, sig } = signDer(info, privateKey);
|
|
141
|
+
const csr = (0, exports.seq)(info, alg, (0, exports.bitString)(sig));
|
|
142
|
+
return {
|
|
143
|
+
csrPem: pem('CERTIFICATE REQUEST', csr),
|
|
144
|
+
keyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** base64url(SHA-256(DER)), the CA pin format of the enrollment token. */
|
|
148
|
+
function pinOf(der) {
|
|
149
|
+
return (0, node_crypto_1.createHash)('sha256').update(der).digest('base64url');
|
|
150
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import https from 'node:https';
|
|
2
|
+
export declare const TOKEN_RE: RegExp;
|
|
3
|
+
export interface SigningKey {
|
|
4
|
+
kid: string;
|
|
5
|
+
alg: 'Ed25519';
|
|
6
|
+
public_key: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Enrollment {
|
|
9
|
+
source_id: string;
|
|
10
|
+
group: string;
|
|
11
|
+
service_name: string;
|
|
12
|
+
client_cert: string;
|
|
13
|
+
ca_chain: string;
|
|
14
|
+
not_after?: string;
|
|
15
|
+
policy_signing_keys: SigningKey[];
|
|
16
|
+
intake_url: string;
|
|
17
|
+
fleet_url: string;
|
|
18
|
+
policy_max_age_s: number;
|
|
19
|
+
heartbeat_interval_s: number;
|
|
20
|
+
policy_poll_interval_s: number;
|
|
21
|
+
/** Local bookkeeping. */
|
|
22
|
+
enroll_url: string;
|
|
23
|
+
pin: string;
|
|
24
|
+
}
|
|
25
|
+
export interface SourceInfo {
|
|
26
|
+
kind: string;
|
|
27
|
+
version: string;
|
|
28
|
+
service_name: string;
|
|
29
|
+
instance?: string;
|
|
30
|
+
runtime?: {
|
|
31
|
+
lang: string;
|
|
32
|
+
lang_version: string;
|
|
33
|
+
os: string;
|
|
34
|
+
arch: string;
|
|
35
|
+
};
|
|
36
|
+
hmac_kids?: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare class FleetError extends Error {
|
|
39
|
+
readonly code: string;
|
|
40
|
+
readonly status?: number | undefined;
|
|
41
|
+
constructor(code: string, status?: number | undefined);
|
|
42
|
+
}
|
|
43
|
+
/** Extracts the CA pin from an enrollment token. */
|
|
44
|
+
export declare function tokenPin(token: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Opens a TLS connection without trusting anything, walks the chain the server presents
|
|
47
|
+
* to its top (the root, which tt-core must include) and checks it against the pin.
|
|
48
|
+
* Returns the root as PEM, to be used as the only trust anchor for the real request.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchPinnedRoot(url: URL, pin: string, timeoutMs?: number): Promise<string>;
|
|
51
|
+
/** Enrolls with a token. Returns the enrollment and the private key PEM (generated locally when useCsr). */
|
|
52
|
+
export declare function enroll(o: {
|
|
53
|
+
enrollUrl: string;
|
|
54
|
+
token: string;
|
|
55
|
+
source: SourceInfo;
|
|
56
|
+
useCsr: boolean;
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
enrollment: Enrollment;
|
|
60
|
+
keyPem: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** mTLS agent for the intake and Fleet calls. Trust anchors: the CA chain received over the pinned channel. */
|
|
63
|
+
export declare function mtlsAgent(e: Enrollment, keyPem: string): https.Agent;
|
|
64
|
+
export declare class FleetClient {
|
|
65
|
+
private e;
|
|
66
|
+
private agent;
|
|
67
|
+
constructor(e: Enrollment, agent: https.Agent);
|
|
68
|
+
setAgent(agent: https.Agent): void;
|
|
69
|
+
private url;
|
|
70
|
+
heartbeat(body: unknown): Promise<{
|
|
71
|
+
status: number;
|
|
72
|
+
json?: Record<string, unknown>;
|
|
73
|
+
}>;
|
|
74
|
+
getPolicy(etag?: string): Promise<{
|
|
75
|
+
status: number;
|
|
76
|
+
body?: Buffer;
|
|
77
|
+
etag?: string;
|
|
78
|
+
}>;
|
|
79
|
+
renew(useCsr: boolean, commonName: string): Promise<{
|
|
80
|
+
cert: string;
|
|
81
|
+
keyPem?: string;
|
|
82
|
+
notAfter?: string;
|
|
83
|
+
} | null>;
|
|
84
|
+
}
|