fboxrec 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.
@@ -0,0 +1,2194 @@
1
+ // src/index.ts
2
+ import { isMainThread } from "worker_threads";
3
+ import * as fs6 from "fs";
4
+ import * as path7 from "path";
5
+ import * as zlib from "zlib";
6
+
7
+ // src/config.ts
8
+ import * as path from "path";
9
+ var DEFAULTS = {
10
+ bufferMb: 64,
11
+ dir: ".flightbox",
12
+ slowRequestMs: 5e3,
13
+ cooldownMs: 6e4,
14
+ heapPct: 0.9,
15
+ stallMs: 1e3,
16
+ shedLagMs: 50,
17
+ maxStageMb: 500,
18
+ minDiskFreePct: 5,
19
+ viewerOrigin: "https://viewer.flightbox.dev"
20
+ };
21
+ function envNumber(name) {
22
+ const raw = process.env[name];
23
+ if (raw === void 0 || raw === "") return void 0;
24
+ const n = Number(raw);
25
+ return Number.isFinite(n) ? n : void 0;
26
+ }
27
+ function envBool(name) {
28
+ return process.env[name] === "1" || process.env[name] === "true";
29
+ }
30
+ function envS3Sink() {
31
+ const bucket = process.env.FLIGHTBOX_S3_BUCKET;
32
+ if (!bucket) return null;
33
+ return {
34
+ type: "s3",
35
+ bucket,
36
+ prefix: process.env.FLIGHTBOX_S3_PREFIX ?? "",
37
+ endpoint: process.env.FLIGHTBOX_S3_ENDPOINT,
38
+ region: process.env.FLIGHTBOX_S3_REGION,
39
+ presign: { enabled: true }
40
+ };
41
+ }
42
+ function validateSink(sink) {
43
+ switch (sink.type) {
44
+ case "s3": {
45
+ if (!sink.bucket || typeof sink.bucket !== "string") {
46
+ throw new Error('flightbox: s3 sink requires a non-empty "bucket"');
47
+ }
48
+ if (sink.endpoint !== void 0 && !/^https?:\/\//.test(sink.endpoint)) {
49
+ throw new Error(`flightbox: s3 sink "endpoint" must be an http(s) URL, got ${sink.endpoint}`);
50
+ }
51
+ if (sink.prefix !== void 0 && typeof sink.prefix !== "string") {
52
+ throw new Error('flightbox: s3 sink "prefix" must be a string');
53
+ }
54
+ const hrs = sink.presign?.expiresHours;
55
+ if (hrs !== void 0 && (!Number.isFinite(hrs) || hrs <= 0 || hrs > 168)) {
56
+ throw new Error(`flightbox: s3 presign.expiresHours must be in (0, 168], got ${hrs}`);
57
+ }
58
+ break;
59
+ }
60
+ case "http": {
61
+ if (!/^https?:\/\//.test(sink.url ?? "")) {
62
+ throw new Error(`flightbox: http sink "url" must be an http(s) URL, got ${sink.url}`);
63
+ }
64
+ break;
65
+ }
66
+ case "disk": {
67
+ if (!sink.dir || typeof sink.dir !== "string") {
68
+ throw new Error('flightbox: disk sink requires a non-empty "dir"');
69
+ }
70
+ if (sink.maxMb !== void 0 && (!Number.isFinite(sink.maxMb) || sink.maxMb < 1)) {
71
+ throw new Error(`flightbox: disk sink "maxMb" must be >= 1, got ${sink.maxMb}`);
72
+ }
73
+ break;
74
+ }
75
+ default:
76
+ throw new Error(`flightbox: unknown sink type "${sink.type}"`);
77
+ }
78
+ }
79
+ function resolveConfig(user = {}) {
80
+ const bufferMb = envNumber("FLIGHTBOX_BUFFER_MB") ?? user.bufferMb ?? DEFAULTS.bufferMb;
81
+ const dir = process.env.FLIGHTBOX_DIR || user.dir || DEFAULTS.dir;
82
+ const service = process.env.FLIGHTBOX_SERVICE || user.service || path.basename(process.cwd());
83
+ const slowRequestMs = envNumber("FLIGHTBOX_TRIGGER_SLOW_MS") ?? user.triggers?.slowRequestMs ?? DEFAULTS.slowRequestMs;
84
+ const cooldownMs = user.triggers?.cooldownMs ?? DEFAULTS.cooldownMs;
85
+ const heapPct = envNumber("FLIGHTBOX_TRIGGER_HEAP_PCT") ?? user.triggers?.heapPct ?? DEFAULTS.heapPct;
86
+ const stallMs = envNumber("FLIGHTBOX_TRIGGER_STALL_MS") ?? user.triggers?.stallMs ?? DEFAULTS.stallMs;
87
+ const shedLagMs = envNumber("FLIGHTBOX_SHED_LAG_MS") ?? user.shedding?.shedLagMs ?? DEFAULTS.shedLagMs;
88
+ const maxStageMb = envNumber("FLIGHTBOX_MAX_STAGE_MB") ?? user.staging?.maxStageMb ?? DEFAULTS.maxStageMb;
89
+ const minDiskFreePct = user.staging?.minDiskFreePct ?? DEFAULTS.minDiskFreePct;
90
+ if (!Number.isFinite(bufferMb) || bufferMb < 1 || bufferMb > 4096) {
91
+ throw new RangeError(`flightbox: bufferMb must be between 1 and 4096, got ${bufferMb}`);
92
+ }
93
+ if (!Number.isFinite(slowRequestMs) || slowRequestMs < 50) {
94
+ throw new RangeError(`flightbox: triggers.slowRequestMs must be >= 50, got ${slowRequestMs}`);
95
+ }
96
+ if (!Number.isFinite(cooldownMs) || cooldownMs < 0) {
97
+ throw new RangeError(`flightbox: triggers.cooldownMs must be >= 0, got ${cooldownMs}`);
98
+ }
99
+ if (!Number.isFinite(heapPct) || heapPct <= 0 || heapPct >= 1) {
100
+ throw new RangeError(`flightbox: triggers.heapPct must be in (0,1), got ${heapPct}`);
101
+ }
102
+ if (!Number.isFinite(stallMs) || stallMs < 50) {
103
+ throw new RangeError(`flightbox: triggers.stallMs must be >= 50, got ${stallMs}`);
104
+ }
105
+ if (!Number.isFinite(shedLagMs) || shedLagMs < 1) {
106
+ throw new RangeError(`flightbox: shedding.shedLagMs must be >= 1, got ${shedLagMs}`);
107
+ }
108
+ if (!Number.isFinite(maxStageMb) || maxStageMb < 1) {
109
+ throw new RangeError(`flightbox: staging.maxStageMb must be >= 1, got ${maxStageMb}`);
110
+ }
111
+ if (!Number.isFinite(minDiskFreePct) || minDiskFreePct < 0 || minDiskFreePct > 50) {
112
+ throw new RangeError(`flightbox: staging.minDiskFreePct must be 0-50, got ${minDiskFreePct}`);
113
+ }
114
+ const sinks = [...user.sinks ?? []];
115
+ const s3FromEnv = envS3Sink();
116
+ if (s3FromEnv && !sinks.some((s) => s.type === "s3")) sinks.push(s3FromEnv);
117
+ sinks.forEach(validateSink);
118
+ return {
119
+ service,
120
+ bufferMb,
121
+ dir: path.resolve(dir),
122
+ triggers: { slowRequestMs, cooldownMs, heapPct, stallMs },
123
+ staging: { maxStageMb, minDiskFreePct },
124
+ shedding: { shedLagMs },
125
+ token: process.env.FLIGHTBOX_TOKEN || user.token,
126
+ viewerOrigin: process.env.FLIGHTBOX_VIEWER_ORIGIN || user.viewerOrigin || DEFAULTS.viewerOrigin,
127
+ sinks,
128
+ allowWorkerThreads: envBool("FLIGHTBOX_ALLOW_WORKER_THREADS") || (user.allowWorkerThreads ?? false),
129
+ log: user.log ?? ((msg) => process.stderr.write(`flightbox: ${msg}
130
+ `))
131
+ };
132
+ }
133
+
134
+ // src/clock.ts
135
+ var anchor = null;
136
+ function anchorClock() {
137
+ anchor = { wallMs: Date.now(), monoNs: process.hrtime.bigint() };
138
+ return anchor;
139
+ }
140
+ function getAnchor() {
141
+ if (anchor === null) anchor = anchorClock();
142
+ return anchor;
143
+ }
144
+ function nowMono() {
145
+ return process.hrtime.bigint();
146
+ }
147
+
148
+ // src/ring-buffer.ts
149
+ var LEN_BYTES = 4;
150
+ var WRAP_MARKER = 4294967295;
151
+ var RingBuffer = class _RingBuffer {
152
+ capacity;
153
+ buf;
154
+ writePos = 0;
155
+ /** Offset of the oldest valid record. Meaningless when count === 0. */
156
+ oldest = 0;
157
+ count = 0;
158
+ droppedCount = 0;
159
+ constructor(capacityBytes) {
160
+ if (!Number.isInteger(capacityBytes) || capacityBytes < 16) {
161
+ throw new RangeError(`ring buffer capacity must be an integer >= 16 bytes, got ${capacityBytes}`);
162
+ }
163
+ this.capacity = capacityBytes;
164
+ this.buf = Buffer.allocUnsafe(capacityBytes);
165
+ }
166
+ /** Number of records currently held. */
167
+ get size() {
168
+ return this.count;
169
+ }
170
+ /** Records rejected because they exceed total capacity. */
171
+ get dropped() {
172
+ return this.droppedCount;
173
+ }
174
+ /**
175
+ * Appends one record. The optional second part lets the hot path write
176
+ * header + body as one frame without concatenating them first (ADR 015).
177
+ */
178
+ write(payload, payload2) {
179
+ const recordLen = payload.length + (payload2 !== void 0 ? payload2.length : 0);
180
+ const needed = LEN_BYTES + recordLen;
181
+ if (needed > this.capacity) {
182
+ this.droppedCount++;
183
+ return false;
184
+ }
185
+ if (this.writePos + needed > this.capacity) {
186
+ this.evictRange(this.writePos, this.capacity);
187
+ if (this.capacity - this.writePos >= LEN_BYTES) {
188
+ this.buf.writeUInt32LE(WRAP_MARKER, this.writePos);
189
+ }
190
+ this.writePos = 0;
191
+ }
192
+ this.evictRange(this.writePos, this.writePos + needed);
193
+ if (this.count === 0) this.oldest = this.writePos;
194
+ this.buf.writeUInt32LE(recordLen, this.writePos);
195
+ this.buf.set(payload, this.writePos + LEN_BYTES);
196
+ if (payload2 !== void 0) {
197
+ this.buf.set(payload2, this.writePos + LEN_BYTES + payload.length);
198
+ }
199
+ this.writePos += needed;
200
+ this.count++;
201
+ return true;
202
+ }
203
+ /**
204
+ * Copies all records out in chronological order. Recording can continue
205
+ * afterwards; the returned buffers are independent copies.
206
+ */
207
+ snapshot() {
208
+ return _RingBuffer.readRecords(this.buf, this.capacity, this.oldest, this.count);
209
+ }
210
+ /**
211
+ * ADR 009 panic path ONLY: the live underlying buffer and pointers, for a
212
+ * raw zero-allocation dump. Everything else must go through snapshot().
213
+ */
214
+ get rawState() {
215
+ return { buf: this.buf, oldest: this.oldest, count: this.count };
216
+ }
217
+ /**
218
+ * Frame-walks a ring image. Shared by live snapshots and panic-file
219
+ * recovery — the latter reads bytes from a crashed process, so lengths are
220
+ * bounds-checked and the walk bails on the first corrupt frame.
221
+ */
222
+ static readRecords(buf, capacity, oldest, count) {
223
+ const out = [];
224
+ let pos = oldest;
225
+ for (let i = 0; i < count; i++) {
226
+ if (pos < 0 || pos > capacity) break;
227
+ if (capacity - pos < LEN_BYTES || buf.readUInt32LE(pos) === WRAP_MARKER) pos = 0;
228
+ const len = buf.readUInt32LE(pos);
229
+ if (len > capacity - pos - LEN_BYTES) break;
230
+ out.push(Buffer.from(buf.subarray(pos + LEN_BYTES, pos + LEN_BYTES + len)));
231
+ pos += LEN_BYTES + len;
232
+ }
233
+ return out;
234
+ }
235
+ clear() {
236
+ this.writePos = 0;
237
+ this.oldest = 0;
238
+ this.count = 0;
239
+ }
240
+ /** Skip implicit (tail gap < 4B) and explicit wrap markers. */
241
+ normalize(pos) {
242
+ if (this.capacity - pos < LEN_BYTES || this.buf.readUInt32LE(pos) === WRAP_MARKER) {
243
+ return 0;
244
+ }
245
+ return pos;
246
+ }
247
+ /** Evict oldest records while they start inside [start, end). */
248
+ evictRange(start2, end) {
249
+ while (this.count > 0) {
250
+ this.oldest = this.normalize(this.oldest);
251
+ if (this.oldest < start2 || this.oldest >= end) return;
252
+ const len = this.buf.readUInt32LE(this.oldest);
253
+ this.oldest += LEN_BYTES + len;
254
+ this.count--;
255
+ }
256
+ }
257
+ };
258
+
259
+ // src/recorder.ts
260
+ import { pack as pack2 } from "msgpackr";
261
+
262
+ // src/encoder.ts
263
+ import { pack, unpack } from "msgpackr";
264
+ var HEADER_SIZE = 29;
265
+ var EVENT_TYPE_NAMES = {
266
+ [1 /* HttpServerStart */]: "http.server.start",
267
+ [2 /* HttpServerEnd */]: "http.server.end",
268
+ [3 /* HttpClientStart */]: "http.client.start",
269
+ [4 /* HttpClientEnd */]: "http.client.end",
270
+ [5 /* PgQueryStart */]: "pg.query.start",
271
+ [6 /* PgQueryEnd */]: "pg.query.end",
272
+ [7 /* PgPoolWait */]: "pg.pool.wait",
273
+ [8 /* Log */]: "log",
274
+ [9 /* Vitals */]: "vitals",
275
+ [10 /* Trigger */]: "trigger",
276
+ [11 /* Custom */]: "custom"
277
+ };
278
+ var LIMITS = {
279
+ path: 512,
280
+ query: 2048,
281
+ log: 1024
282
+ };
283
+ function truncate(s, max) {
284
+ if (s === void 0 || s === null) return void 0;
285
+ return s.length > max ? s.slice(0, max) : s;
286
+ }
287
+ function encodeHeaderInto(target, seq, tMonoNs, type, requestId, spanId) {
288
+ target.writeUInt32LE(seq >>> 0, 0);
289
+ target.writeBigUInt64LE(tMonoNs, 4);
290
+ target.writeUInt8(type & 255, 12);
291
+ target.writeBigUInt64LE(requestId, 13);
292
+ target.writeBigUInt64LE(spanId, 21);
293
+ }
294
+ function decodeEvent(buf) {
295
+ if (buf.length < HEADER_SIZE) {
296
+ throw new RangeError(`event record too short: ${buf.length} bytes`);
297
+ }
298
+ return {
299
+ header: {
300
+ seq: buf.readUInt32LE(0),
301
+ tMonoNs: buf.readBigUInt64LE(4),
302
+ type: buf.readUInt8(12),
303
+ requestId: buf.readBigUInt64LE(13),
304
+ spanId: buf.readBigUInt64LE(21)
305
+ },
306
+ payload: unpack(buf.subarray(HEADER_SIZE))
307
+ };
308
+ }
309
+
310
+ // src/context.ts
311
+ import { AsyncLocalStorage } from "async_hooks";
312
+ var requestStorage = new AsyncLocalStorage();
313
+ var nextRequestId = 1n;
314
+ var nextSpanId = 1n;
315
+ var U64_MASK = 0xffffffffffffffffn;
316
+ function newRequestId() {
317
+ const id = nextRequestId;
318
+ nextRequestId = nextRequestId + 1n & U64_MASK;
319
+ if (nextRequestId === 0n) nextRequestId = 1n;
320
+ return id;
321
+ }
322
+ function newSpanId() {
323
+ const id = nextSpanId;
324
+ nextSpanId = nextSpanId + 1n & U64_MASK;
325
+ if (nextSpanId === 0n) nextSpanId = 1n;
326
+ return id;
327
+ }
328
+ function currentRequestId() {
329
+ return requestStorage.getStore()?.requestId ?? 0n;
330
+ }
331
+
332
+ // src/recorder.ts
333
+ var scratchHeader = Buffer.allocUnsafe(HEADER_SIZE);
334
+ var MAX_EVENT_PAYLOAD_BYTES = 64 * 1024;
335
+ var Recorder = class {
336
+ constructor(ring) {
337
+ this.ring = ring;
338
+ }
339
+ ring;
340
+ armed = false;
341
+ seq = 0;
342
+ record(type, payload, opts) {
343
+ if (!this.armed) return;
344
+ try {
345
+ encodeHeaderInto(
346
+ scratchHeader,
347
+ this.seq++ >>> 0,
348
+ opts?.tMonoNs ?? nowMono(),
349
+ type,
350
+ opts?.requestId ?? currentRequestId(),
351
+ opts?.spanId ?? 0n
352
+ );
353
+ let body = pack2(payload);
354
+ if (body.length > MAX_EVENT_PAYLOAD_BYTES) {
355
+ body = pack2({ __truncated: true, originalBytes: body.length });
356
+ }
357
+ this.ring.write(scratchHeader, body);
358
+ } catch {
359
+ }
360
+ }
361
+ /**
362
+ * ADR 005 — zero-lock freeze: swap in a fresh ring for active writes and
363
+ * return the frozen one. Incoming traffic keeps recording into the new
364
+ * buffer while the frozen segment is serialized; the frozen bytes can
365
+ * never be overwritten mid-dump.
366
+ *
367
+ * `replacementBytes` lets memory-pressure dumps swap in a SMALL emergency
368
+ * ring instead of another full-size allocation — allocating 64MB (or up
369
+ * to 4GB) at the exact moment the heap trigger fired would deepen the
370
+ * incident being recorded. The next normal-path dump restores full size.
371
+ */
372
+ freezeAndSwap(replacementBytes) {
373
+ const frozen = this.ring;
374
+ this.ring = new RingBuffer(
375
+ Math.min(replacementBytes ?? frozen.capacity, frozen.capacity)
376
+ );
377
+ return frozen;
378
+ }
379
+ /** Decode a chronological copy of the active buffer. Recording continues. */
380
+ snapshot() {
381
+ return decodeRing(this.ring);
382
+ }
383
+ get eventCount() {
384
+ return this.ring.size;
385
+ }
386
+ /** ADR 009 panic path ONLY — raw access to the live ring. */
387
+ get activeRing() {
388
+ return this.ring;
389
+ }
390
+ };
391
+ function decodeRing(ring) {
392
+ const out = [];
393
+ for (const rec of ring.snapshot()) {
394
+ try {
395
+ out.push(decodeEvent(rec));
396
+ } catch {
397
+ }
398
+ }
399
+ return out;
400
+ }
401
+
402
+ // src/load-shedding.ts
403
+ var active = false;
404
+ function isShedding() {
405
+ return active;
406
+ }
407
+ function setShedding(on) {
408
+ active = on;
409
+ }
410
+
411
+ // src/dump/stage.ts
412
+ import * as fs from "fs";
413
+ import * as path2 from "path";
414
+ var DiskFullError = class extends Error {
415
+ constructor(freePct, minFreePct) {
416
+ super(
417
+ `disk has ${freePct.toFixed(1)}% free, below the ${minFreePct}% safety floor`
418
+ );
419
+ this.name = "DiskFullError";
420
+ }
421
+ };
422
+ var PID_DIR_RE = /^pid-(\d+)$/;
423
+ var CLAIMED_DIR_RE = /^claimed-(\d+)-(.+)$/;
424
+ var Staging = class {
425
+ constructor(baseDir, limits, log = () => {
426
+ }) {
427
+ this.limits = limits;
428
+ this.log = log;
429
+ this.baseStagingDir = path2.join(baseDir, "staging");
430
+ this.stagingDir = path2.join(this.baseStagingDir, `pid-${process.pid}`);
431
+ this.deliveredDir = path2.join(baseDir, "delivered");
432
+ fs.mkdirSync(this.stagingDir, { recursive: true });
433
+ fs.mkdirSync(this.deliveredDir, { recursive: true });
434
+ }
435
+ limits;
436
+ log;
437
+ /** Shared root: <dir>/staging/ */
438
+ baseStagingDir;
439
+ /** This process's isolated dir: <dir>/staging/pid-<pid>/ (ADR 010). */
440
+ stagingDir;
441
+ deliveredDir;
442
+ /**
443
+ * Synchronous, crash-safe write: tmp file + fsync + rename (+ best-effort
444
+ * parent-dir fsync on POSIX), so staging never holds a torn .fbox and a
445
+ * power loss right after return cannot un-persist it. Throws
446
+ * DiskFullError when the disk is critically full — the caller must
447
+ * disable the agent (ADR 006) — and rejects any single dump larger than
448
+ * the quota outright: "evict everything, then write the oversized file
449
+ * anyway" would make the cap a fiction.
450
+ */
451
+ stageSync(fileName, data) {
452
+ const maxBytes = this.limits.maxStageMb * 1024 * 1024;
453
+ if (data.length > maxBytes) {
454
+ throw new Error(
455
+ `dump is ${(data.length / 1048576).toFixed(1)}MB, larger than the ${this.limits.maxStageMb}MB staging quota \u2014 refusing to stage it`
456
+ );
457
+ }
458
+ const freePct = this.diskFreePct();
459
+ if (freePct !== null && freePct < this.limits.minDiskFreePct) {
460
+ throw new DiskFullError(freePct, this.limits.minDiskFreePct);
461
+ }
462
+ this.evictOverQuota(data.length);
463
+ const finalPath = path2.join(this.stagingDir, fileName);
464
+ const tmpPath = finalPath + ".part";
465
+ const fd = fs.openSync(tmpPath, "w");
466
+ try {
467
+ fs.writeSync(fd, data);
468
+ fs.fsyncSync(fd);
469
+ } finally {
470
+ fs.closeSync(fd);
471
+ }
472
+ fs.renameSync(tmpPath, finalPath);
473
+ try {
474
+ const dirFd = fs.openSync(this.stagingDir, "r");
475
+ fs.fsyncSync(dirFd);
476
+ fs.closeSync(dirFd);
477
+ } catch {
478
+ }
479
+ return finalPath;
480
+ }
481
+ /** Own staged .fbox files, oldest first (names embed timestamps). */
482
+ listStagedSync() {
483
+ return this.listOwn(".fbox");
484
+ }
485
+ /** Own raw panic dumps awaiting conversion (ADR 009). */
486
+ listPanicSync() {
487
+ return this.listOwn(".fboxpanic");
488
+ }
489
+ /**
490
+ * Move a staged file to delivered/ after successful sink delivery.
491
+ * delivered/ shares the ADR 006 quota — local retention must not outlive
492
+ * the disk either.
493
+ */
494
+ markDeliveredSync(filePath) {
495
+ let incoming = 0;
496
+ try {
497
+ incoming = fs.statSync(filePath).size;
498
+ } catch {
499
+ }
500
+ this.evictFilesOverQuota(this.listDir(this.deliveredDir, ".fbox"), incoming);
501
+ const dest = path2.join(this.deliveredDir, path2.basename(filePath));
502
+ fs.renameSync(filePath, dest);
503
+ return dest;
504
+ }
505
+ /**
506
+ * ADR 010 adoption pass: absorb staging dirs left by DEAD sibling
507
+ * processes. renameSync is the atomicity lock — when several new workers
508
+ * race for the same orphan, exactly one rename succeeds; losers get
509
+ * ENOENT and move on. Also re-adopts `claimed-*` dirs whose claimant died
510
+ * mid-adoption (audit M4) — without this, a crash between the claim
511
+ * rename and the file moves would strand incidents invisibly forever.
512
+ * Returns how many files were adopted.
513
+ *
514
+ * Known limitation (audit L2): PID reuse. If a dead worker's pid was
515
+ * recycled by an unrelated live process, its dir looks "alive" and waits
516
+ * until that process exits. Solving this needs process identity beyond
517
+ * the pid (start-time comparison), which has no portable API — accepted
518
+ * for v0.1; the files are preserved, only delayed.
519
+ */
520
+ claimOrphanedSync() {
521
+ let adopted = 0;
522
+ let entries;
523
+ try {
524
+ entries = fs.readdirSync(this.baseStagingDir);
525
+ } catch {
526
+ return 0;
527
+ }
528
+ for (const entry of entries) {
529
+ let ownerPid;
530
+ let originalName = entry;
531
+ const orphan = PID_DIR_RE.exec(entry);
532
+ const halfClaimed = orphan ? null : CLAIMED_DIR_RE.exec(entry);
533
+ if (orphan) {
534
+ ownerPid = Number(orphan[1]);
535
+ } else if (halfClaimed) {
536
+ ownerPid = Number(halfClaimed[1]);
537
+ originalName = halfClaimed[2];
538
+ } else {
539
+ continue;
540
+ }
541
+ if (ownerPid === process.pid || isProcessAlive(ownerPid)) continue;
542
+ const orphanDir = path2.join(this.baseStagingDir, entry);
543
+ const claimedDir = path2.join(this.baseStagingDir, `claimed-${process.pid}-${originalName}`);
544
+ try {
545
+ fs.renameSync(orphanDir, claimedDir);
546
+ } catch {
547
+ continue;
548
+ }
549
+ try {
550
+ for (const file of fs.readdirSync(claimedDir)) {
551
+ if (!file.endsWith(".fbox") && !file.endsWith(".fboxpanic")) {
552
+ try {
553
+ fs.unlinkSync(path2.join(claimedDir, file));
554
+ } catch {
555
+ }
556
+ continue;
557
+ }
558
+ try {
559
+ fs.renameSync(path2.join(claimedDir, file), path2.join(this.stagingDir, file));
560
+ adopted++;
561
+ } catch {
562
+ }
563
+ }
564
+ fs.rmdirSync(claimedDir);
565
+ } catch {
566
+ }
567
+ }
568
+ if (adopted > 0) {
569
+ this.log(`adopted ${adopted} file(s) from dead worker staging dirs`);
570
+ }
571
+ return adopted;
572
+ }
573
+ listOwn(ext) {
574
+ return this.listDir(this.stagingDir, ext);
575
+ }
576
+ listDir(dir, ext) {
577
+ try {
578
+ return fs.readdirSync(dir).filter((f) => f.endsWith(ext)).sort().map((f) => path2.join(dir, f));
579
+ } catch {
580
+ return [];
581
+ }
582
+ }
583
+ /** null = cannot determine (treat as healthy rather than disable the agent). */
584
+ diskFreePct() {
585
+ const statfsSync2 = fs.statfsSync;
586
+ if (typeof statfsSync2 !== "function") return null;
587
+ try {
588
+ const s = statfsSync2(this.stagingDir);
589
+ const blocks = Number(s.blocks);
590
+ if (!Number.isFinite(blocks) || blocks <= 0) return null;
591
+ return Number(s.bavail) / blocks * 100;
592
+ } catch {
593
+ return null;
594
+ }
595
+ }
596
+ /** ADR 006 FIFO eviction: silently drop oldest staged dumps to fit the quota. */
597
+ evictOverQuota(incomingBytes) {
598
+ this.evictFilesOverQuota(this.listStagedSync(), incomingBytes);
599
+ }
600
+ evictFilesOverQuota(candidates, incomingBytes) {
601
+ const maxBytes = this.limits.maxStageMb * 1024 * 1024;
602
+ const files = [];
603
+ let total = incomingBytes;
604
+ for (const file of candidates) {
605
+ try {
606
+ const size = fs.statSync(file).size;
607
+ files.push({ file, size });
608
+ total += size;
609
+ } catch {
610
+ }
611
+ }
612
+ for (const { file, size } of files) {
613
+ if (total <= maxBytes) break;
614
+ try {
615
+ fs.unlinkSync(file);
616
+ total -= size;
617
+ this.log(`staging quota (${this.limits.maxStageMb}MB) exceeded \u2014 evicted oldest: ${path2.basename(file)}`);
618
+ } catch {
619
+ }
620
+ }
621
+ }
622
+ };
623
+ function isProcessAlive(pid) {
624
+ try {
625
+ process.kill(pid, 0);
626
+ return true;
627
+ } catch (err) {
628
+ return err.code === "EPERM";
629
+ }
630
+ }
631
+ function recoverOnBoot(staging, log) {
632
+ staging.claimOrphanedSync();
633
+ const files = staging.listStagedSync();
634
+ if (files.length > 0) {
635
+ log(
636
+ `found ${files.length} unsent incident(s) from a previous run in ${staging.stagingDir}`
637
+ );
638
+ }
639
+ return files;
640
+ }
641
+
642
+ // src/dump/panic.ts
643
+ import * as fs2 from "fs";
644
+ import * as path3 from "path";
645
+
646
+ // src/dump/serializer.ts
647
+ import { gzipSync } from "zlib";
648
+
649
+ // ../format/src/types.ts
650
+ var FORMAT_VERSION = 1;
651
+
652
+ // ../format/src/parse.ts
653
+ var MAX_DECOMPRESSED_BYTES = 1024 * 1024 * 1024;
654
+
655
+ // src/dump/serializer.ts
656
+ var FLIGHTBOX_VERSION = "0.1.0";
657
+ var DUMP_GZIP_LEVEL = 1;
658
+ var dumpCounter = 0;
659
+ function buildIncident(opts) {
660
+ const anchor2 = opts.anchor ?? getAnchor();
661
+ const events = opts.events.map((e) => ({
662
+ seq: e.header.seq,
663
+ wallMs: anchor2.wallMs + Number(e.header.tMonoNs - anchor2.monoNs) / 1e6,
664
+ tMonoNs: e.header.tMonoNs.toString(),
665
+ type: EVENT_TYPE_NAMES[e.header.type] ?? `unknown.${e.header.type}`,
666
+ // ADR 004: context-less events are "orphan", never an error.
667
+ requestId: e.header.requestId === 0n ? "orphan" : e.header.requestId.toString(),
668
+ spanId: e.header.spanId.toString(),
669
+ data: e.payload
670
+ }));
671
+ const first = events[0];
672
+ const last = events[events.length - 1];
673
+ const windowMs = first && last ? last.wallMs - first.wallMs : 0;
674
+ const meta = {
675
+ service: opts.service,
676
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
677
+ wallAnchor: { wallMs: anchor2.wallMs, monoNs: anchor2.monoNs.toString() },
678
+ trigger: opts.trigger,
679
+ eventCount: events.length,
680
+ windowMs,
681
+ flightboxVersion: FLIGHTBOX_VERSION
682
+ };
683
+ const incident = { formatVersion: FORMAT_VERSION, meta, events };
684
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
685
+ const fileName = `incident-${stamp}-p${process.pid}-${(dumpCounter++).toString(36)}.fbox`;
686
+ return {
687
+ fileName,
688
+ json: JSON.stringify(incident),
689
+ meta,
690
+ eventCount: events.length,
691
+ windowMs
692
+ };
693
+ }
694
+ function serializeIncident(opts) {
695
+ const prepared = buildIncident(opts);
696
+ return {
697
+ fileName: prepared.fileName,
698
+ data: gzipSync(prepared.json, { level: DUMP_GZIP_LEVEL }),
699
+ meta: prepared.meta,
700
+ eventCount: prepared.eventCount,
701
+ windowMs: prepared.windowMs
702
+ };
703
+ }
704
+
705
+ // src/dump/panic.ts
706
+ var MAGIC = Buffer.from("FBOXPANC", "ascii");
707
+ var PANIC_VERSION = 1;
708
+ var PANIC_HEADER_LEN = 44;
709
+ var PanicWriter = class {
710
+ fd = null;
711
+ /** Preallocated so panic-time writes allocate (nearly) nothing. */
712
+ header = Buffer.alloc(PANIC_HEADER_LEN);
713
+ filePath;
714
+ constructor(stagingDir) {
715
+ this.filePath = path3.join(stagingDir, `panic-${process.pid}.fboxpanic`);
716
+ }
717
+ /** Preopen the fd while memory is plentiful. Failure is non-fatal. */
718
+ arm() {
719
+ try {
720
+ this.fd = fs2.openSync(this.filePath, "w");
721
+ } catch {
722
+ this.fd = null;
723
+ }
724
+ }
725
+ /**
726
+ * The panic dump: no gzip, no decode, no envelope — raw ring bytes onto
727
+ * the preopened fd. Safe to call with V8 near the heap limit.
728
+ */
729
+ writeSync(ring) {
730
+ if (this.fd === null) return false;
731
+ try {
732
+ const state = ring.rawState;
733
+ const anchor2 = getAnchor();
734
+ MAGIC.copy(this.header, 0);
735
+ this.header.writeUInt32LE(PANIC_VERSION, 8);
736
+ this.header.writeUInt32LE(ring.capacity, 12);
737
+ this.header.writeUInt32LE(state.oldest, 16);
738
+ this.header.writeUInt32LE(state.count, 20);
739
+ this.header.writeDoubleLE(anchor2.wallMs, 24);
740
+ this.header.writeBigUInt64LE(anchor2.monoNs, 32);
741
+ fs2.writeSync(this.fd, this.header, 0, PANIC_HEADER_LEN, 0);
742
+ fs2.writeSync(this.fd, state.buf, 0, state.buf.length, PANIC_HEADER_LEN);
743
+ fs2.fsyncSync(this.fd);
744
+ return true;
745
+ } catch {
746
+ return false;
747
+ }
748
+ }
749
+ /** Clean shutdown: close the fd and sweep the empty placeholder. */
750
+ disarm() {
751
+ try {
752
+ if (this.fd !== null) fs2.closeSync(this.fd);
753
+ this.fd = null;
754
+ if (fs2.existsSync(this.filePath) && fs2.statSync(this.filePath).size === 0) {
755
+ fs2.unlinkSync(this.filePath);
756
+ }
757
+ } catch {
758
+ this.fd = null;
759
+ }
760
+ }
761
+ };
762
+ function parsePanicFile(raw) {
763
+ if (raw.length < PANIC_HEADER_LEN || !raw.subarray(0, 8).equals(MAGIC)) return null;
764
+ if (raw.readUInt32LE(8) !== PANIC_VERSION) return null;
765
+ const capacity = raw.readUInt32LE(12);
766
+ const oldest = raw.readUInt32LE(16);
767
+ const count = raw.readUInt32LE(20);
768
+ const wallMs = raw.readDoubleLE(24);
769
+ const monoNs = raw.readBigUInt64LE(32);
770
+ const body = raw.subarray(PANIC_HEADER_LEN);
771
+ if (body.length !== capacity || oldest >= capacity) return null;
772
+ const events = [];
773
+ for (const rec of RingBuffer.readRecords(body, capacity, oldest, count)) {
774
+ try {
775
+ events.push(decodeEvent(rec));
776
+ } catch {
777
+ }
778
+ }
779
+ events.sort(
780
+ (a, b) => a.header.tMonoNs < b.header.tMonoNs ? -1 : a.header.tMonoNs > b.header.tMonoNs ? 1 : 0
781
+ );
782
+ return { anchor: { wallMs, monoNs }, events };
783
+ }
784
+ function recoverPanicFiles(staging, service, log) {
785
+ let recovered = 0;
786
+ for (const file of staging.listPanicSync()) {
787
+ try {
788
+ if (fs2.statSync(file).size < PANIC_HEADER_LEN) {
789
+ fs2.unlinkSync(file);
790
+ continue;
791
+ }
792
+ const parsed = parsePanicFile(fs2.readFileSync(file));
793
+ if (parsed === null) {
794
+ fs2.renameSync(file, file + ".corrupt");
795
+ log(`panic dump ${path3.basename(file)} is unparseable \u2014 kept as .corrupt`);
796
+ continue;
797
+ }
798
+ const { fileName, data, eventCount } = serializeIncident({
799
+ service,
800
+ trigger: { type: "panic", reason: "recovered from crash-time panic dump" },
801
+ events: parsed.events,
802
+ anchor: parsed.anchor
803
+ });
804
+ staging.stageSync(fileName, data);
805
+ fs2.unlinkSync(file);
806
+ recovered++;
807
+ log(`recovered panic dump \u2192 ${fileName} (${eventCount} events)`);
808
+ } catch (err) {
809
+ log(`panic recovery failed for ${path3.basename(file)}: ${err.message}`);
810
+ }
811
+ }
812
+ return recovered;
813
+ }
814
+
815
+ // src/dump/snapshot.ts
816
+ function takeFrozenSnapshot(recorder, replacementBytes) {
817
+ return decodeRing(recorder.freezeAndSwap(replacementBytes));
818
+ }
819
+
820
+ // src/dump/sinks/disk.ts
821
+ import * as fs3 from "fs";
822
+ import * as path4 from "path";
823
+ function createDiskSink(cfg) {
824
+ const maxBytes = (cfg.maxMb ?? 500) * 1024 * 1024;
825
+ return {
826
+ name: "disk",
827
+ async deliver(filePath) {
828
+ const dir = path4.resolve(cfg.dir);
829
+ await fs3.promises.mkdir(dir, { recursive: true });
830
+ const dest = path4.join(dir, path4.basename(filePath));
831
+ await fs3.promises.copyFile(filePath, dest);
832
+ evictOverQuota(dir, maxBytes);
833
+ return { ok: true, location: dest };
834
+ }
835
+ };
836
+ }
837
+ function evictOverQuota(dir, maxBytes) {
838
+ try {
839
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".fbox")).sort().map((f) => path4.join(dir, f));
840
+ let total = 0;
841
+ const sized = files.map((file) => {
842
+ const size = fs3.statSync(file).size;
843
+ total += size;
844
+ return { file, size };
845
+ });
846
+ for (const { file, size } of sized) {
847
+ if (total <= maxBytes) break;
848
+ fs3.unlinkSync(file);
849
+ total -= size;
850
+ }
851
+ } catch {
852
+ }
853
+ }
854
+
855
+ // src/dump/sinks/s3.ts
856
+ import * as fs4 from "fs";
857
+ import * as path5 from "path";
858
+
859
+ // src/dump/sinks/sigv4.ts
860
+ import { createHash, createHmac } from "crypto";
861
+ function sha256Hex(data) {
862
+ return createHash("sha256").update(data).digest("hex");
863
+ }
864
+ function hmac(key, data) {
865
+ return createHmac("sha256", key).update(data, "utf8").digest();
866
+ }
867
+ function uriEncode(str, encodeSlash) {
868
+ let out = "";
869
+ for (const ch of str) {
870
+ if (/[A-Za-z0-9\-._~]/.test(ch) || ch === "/" && !encodeSlash) {
871
+ out += ch;
872
+ } else {
873
+ for (const byte of Buffer.from(ch, "utf8")) {
874
+ out += "%" + byte.toString(16).toUpperCase().padStart(2, "0");
875
+ }
876
+ }
877
+ }
878
+ return out;
879
+ }
880
+ function timestamps(now) {
881
+ const iso = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
882
+ return { amzDate: iso, dateStamp: iso.slice(0, 8) };
883
+ }
884
+ function canonicalUri(url) {
885
+ return url.pathname.split("/").map((seg) => uriEncode(safeDecode(seg), true)).join("/") || "/";
886
+ }
887
+ function safeDecode(seg) {
888
+ try {
889
+ return decodeURIComponent(seg);
890
+ } catch {
891
+ return seg;
892
+ }
893
+ }
894
+ function byCodePoint(a, b) {
895
+ return a < b ? -1 : a > b ? 1 : 0;
896
+ }
897
+ function canonicalQuery(params) {
898
+ return params.map(([k, v]) => [uriEncode(k, true), uriEncode(v, true)]).sort((a, b) => a[0] === b[0] ? byCodePoint(a[1], b[1]) : byCodePoint(a[0], b[0])).map(([k, v]) => `${k}=${v}`).join("&");
899
+ }
900
+ function signingKey(secret, dateStamp, region, service) {
901
+ return hmac(hmac(hmac(hmac(`AWS4${secret}`, dateStamp), region), service), "aws4_request");
902
+ }
903
+ function signHeaders(input) {
904
+ const service = input.service ?? "s3";
905
+ const { amzDate, dateStamp } = timestamps(input.now ?? /* @__PURE__ */ new Date());
906
+ const headers = {
907
+ host: input.url.host,
908
+ "x-amz-content-sha256": input.payloadHash,
909
+ "x-amz-date": amzDate,
910
+ ...input.creds.sessionToken ? { "x-amz-security-token": input.creds.sessionToken } : {},
911
+ ...Object.fromEntries(
912
+ Object.entries(input.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v])
913
+ )
914
+ };
915
+ const names = Object.keys(headers).sort();
916
+ const canonicalHeaders = names.map((n) => `${n}:${headers[n].trim()}
917
+ `).join("");
918
+ const signedHeaders = names.join(";");
919
+ const canonicalRequest = [
920
+ input.method.toUpperCase(),
921
+ canonicalUri(input.url),
922
+ canonicalQuery([...input.url.searchParams.entries()]),
923
+ canonicalHeaders,
924
+ signedHeaders,
925
+ input.payloadHash
926
+ ].join("\n");
927
+ const scope = `${dateStamp}/${input.region}/${service}/aws4_request`;
928
+ const stringToSign = [
929
+ "AWS4-HMAC-SHA256",
930
+ amzDate,
931
+ scope,
932
+ sha256Hex(canonicalRequest)
933
+ ].join("\n");
934
+ const signature = createHmac(
935
+ "sha256",
936
+ signingKey(input.creds.secretAccessKey, dateStamp, input.region, service)
937
+ ).update(stringToSign, "utf8").digest("hex");
938
+ headers.authorization = `AWS4-HMAC-SHA256 Credential=${input.creds.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
939
+ return headers;
940
+ }
941
+ function presignUrl(input) {
942
+ const service = input.service ?? "s3";
943
+ const method = input.method ?? "GET";
944
+ const { amzDate, dateStamp } = timestamps(input.now ?? /* @__PURE__ */ new Date());
945
+ const scope = `${dateStamp}/${input.region}/${service}/aws4_request`;
946
+ const params = [
947
+ ...input.url.searchParams.entries(),
948
+ ["X-Amz-Algorithm", "AWS4-HMAC-SHA256"],
949
+ ["X-Amz-Credential", `${input.creds.accessKeyId}/${scope}`],
950
+ ["X-Amz-Date", amzDate],
951
+ ["X-Amz-Expires", String(Math.floor(input.expiresSec))],
952
+ ["X-Amz-SignedHeaders", "host"]
953
+ ];
954
+ if (input.creds.sessionToken) {
955
+ params.push(["X-Amz-Security-Token", input.creds.sessionToken]);
956
+ }
957
+ const query = canonicalQuery(params);
958
+ const canonicalRequest = [
959
+ method.toUpperCase(),
960
+ canonicalUri(input.url),
961
+ query,
962
+ `host:${input.url.host}
963
+ `,
964
+ "host",
965
+ "UNSIGNED-PAYLOAD"
966
+ ].join("\n");
967
+ const stringToSign = [
968
+ "AWS4-HMAC-SHA256",
969
+ amzDate,
970
+ scope,
971
+ sha256Hex(canonicalRequest)
972
+ ].join("\n");
973
+ const signature = createHmac(
974
+ "sha256",
975
+ signingKey(input.creds.secretAccessKey, dateStamp, input.region, service)
976
+ ).update(stringToSign, "utf8").digest("hex");
977
+ return `${input.url.origin}${canonicalUri(input.url)}?${query}&X-Amz-Signature=${signature}`;
978
+ }
979
+
980
+ // src/dump/sinks/credentials.ts
981
+ var REFRESH_MARGIN_MS = 5 * 6e4;
982
+ var METADATA_TIMEOUT_MS = 1e3;
983
+ var cache = null;
984
+ function invalidateCredentials() {
985
+ cache = null;
986
+ }
987
+ function fromStatic(cfg) {
988
+ const key = cfg.accessKeyId ?? process.env.FLIGHTBOX_S3_KEY;
989
+ const secret = cfg.secretAccessKey ?? process.env.FLIGHTBOX_S3_SECRET;
990
+ if (!key || !secret) return null;
991
+ return {
992
+ accessKeyId: key,
993
+ secretAccessKey: secret,
994
+ sessionToken: cfg.sessionToken ?? process.env.FLIGHTBOX_S3_TOKEN,
995
+ source: "static"
996
+ };
997
+ }
998
+ function fromAwsEnv() {
999
+ const key = process.env.AWS_ACCESS_KEY_ID;
1000
+ const secret = process.env.AWS_SECRET_ACCESS_KEY;
1001
+ if (!key || !secret) return null;
1002
+ return {
1003
+ accessKeyId: key,
1004
+ secretAccessKey: secret,
1005
+ sessionToken: process.env.AWS_SESSION_TOKEN,
1006
+ source: "aws-env"
1007
+ };
1008
+ }
1009
+ async function fetchJson(url, init) {
1010
+ try {
1011
+ const res = await fetch(url, {
1012
+ ...init,
1013
+ signal: AbortSignal.timeout(METADATA_TIMEOUT_MS)
1014
+ });
1015
+ if (!res.ok) return null;
1016
+ return await res.json();
1017
+ } catch {
1018
+ return null;
1019
+ }
1020
+ }
1021
+ async function fromEcs() {
1022
+ const fullUri = process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI;
1023
+ const relativeUri = process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI;
1024
+ if (!fullUri && !relativeUri) return null;
1025
+ const url = fullUri ?? `http://169.254.170.2${relativeUri}`;
1026
+ const body = await fetchJson(url);
1027
+ if (!body?.AccessKeyId || !body?.SecretAccessKey) return null;
1028
+ return {
1029
+ accessKeyId: body.AccessKeyId,
1030
+ secretAccessKey: body.SecretAccessKey,
1031
+ sessionToken: body.Token,
1032
+ expiresAtMs: body.Expiration ? Date.parse(body.Expiration) : void 0,
1033
+ source: "ecs-task-role"
1034
+ };
1035
+ }
1036
+ async function fromImdsV2() {
1037
+ const base = process.env.AWS_EC2_METADATA_SERVICE_ENDPOINT?.replace(/\/$/, "") ?? "http://169.254.169.254";
1038
+ let token;
1039
+ try {
1040
+ const res = await fetch(`${base}/latest/api/token`, {
1041
+ method: "PUT",
1042
+ headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
1043
+ signal: AbortSignal.timeout(METADATA_TIMEOUT_MS)
1044
+ });
1045
+ if (!res.ok) return null;
1046
+ token = await res.text();
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ const tokenHeader = { "x-aws-ec2-metadata-token": token };
1051
+ let role;
1052
+ try {
1053
+ const res = await fetch(`${base}/latest/meta-data/iam/security-credentials/`, {
1054
+ headers: tokenHeader,
1055
+ signal: AbortSignal.timeout(METADATA_TIMEOUT_MS)
1056
+ });
1057
+ if (!res.ok) return null;
1058
+ role = (await res.text()).split("\n")[0].trim();
1059
+ if (!role) return null;
1060
+ } catch {
1061
+ return null;
1062
+ }
1063
+ const body = await fetchJson(
1064
+ `${base}/latest/meta-data/iam/security-credentials/${role}`,
1065
+ { headers: tokenHeader }
1066
+ );
1067
+ if (!body?.AccessKeyId || !body?.SecretAccessKey) return null;
1068
+ return {
1069
+ accessKeyId: body.AccessKeyId,
1070
+ secretAccessKey: body.SecretAccessKey,
1071
+ sessionToken: body.Token,
1072
+ expiresAtMs: body.Expiration ? Date.parse(body.Expiration) : void 0,
1073
+ source: "ec2-imdsv2"
1074
+ };
1075
+ }
1076
+ async function resolveCredentials(cfg = {}) {
1077
+ const staticCreds = fromStatic(cfg);
1078
+ if (staticCreds) return staticCreds;
1079
+ const envCreds = fromAwsEnv();
1080
+ if (envCreds) return envCreds;
1081
+ if (cache && (cache.expiresAtMs === void 0 || cache.expiresAtMs - Date.now() > REFRESH_MARGIN_MS)) {
1082
+ return cache;
1083
+ }
1084
+ cache = await fromEcs() ?? await fromImdsV2();
1085
+ return cache;
1086
+ }
1087
+
1088
+ // src/dump/sinks/s3.ts
1089
+ var PUT_TIMEOUT_MS = 3e4;
1090
+ var MAX_PRESIGN_SEC = 7 * 24 * 3600;
1091
+ function createS3Sink(cfg, defaultViewerOrigin) {
1092
+ const region = cfg.region || (cfg.endpoint ? "auto" : "us-east-1");
1093
+ function objectUrl(key) {
1094
+ if (cfg.endpoint) {
1095
+ return new URL(`${cfg.endpoint.replace(/\/$/, "")}/${cfg.bucket}/${key}`);
1096
+ }
1097
+ return new URL(`https://${cfg.bucket}.s3.${region}.amazonaws.com/${key}`);
1098
+ }
1099
+ return {
1100
+ name: "s3",
1101
+ async deliver(filePath, meta) {
1102
+ const body = await fs4.promises.readFile(filePath);
1103
+ const key = `${cfg.prefix ?? ""}${path5.basename(filePath)}`;
1104
+ let creds = await resolveCredentials(cfg);
1105
+ if (!creds) {
1106
+ return {
1107
+ ok: false,
1108
+ detail: "no AWS credentials resolved (chain: static -> AWS_* env -> ECS task role -> IMDSv2)"
1109
+ };
1110
+ }
1111
+ const put = async (c) => {
1112
+ const url = objectUrl(key);
1113
+ const headers = signHeaders({
1114
+ method: "PUT",
1115
+ url,
1116
+ payloadHash: sha256Hex(body),
1117
+ creds: c,
1118
+ region,
1119
+ headers: {
1120
+ "content-type": "application/gzip",
1121
+ "x-amz-meta-flightbox-trigger": meta.trigger.type
1122
+ }
1123
+ });
1124
+ return fetch(url, {
1125
+ method: "PUT",
1126
+ headers,
1127
+ body,
1128
+ // A hung connection must not leave the delivery promise pending
1129
+ // forever; the file stays staged and the retry pass gets it.
1130
+ signal: AbortSignal.timeout(PUT_TIMEOUT_MS)
1131
+ });
1132
+ };
1133
+ let res = await put(creds);
1134
+ if (res.status === 403) {
1135
+ invalidateCredentials();
1136
+ const fresh = await resolveCredentials(cfg);
1137
+ if (fresh) {
1138
+ creds = fresh;
1139
+ res = await put(fresh);
1140
+ }
1141
+ }
1142
+ if (!res.ok) {
1143
+ const text = await res.text().catch(() => "");
1144
+ return { ok: false, detail: `S3 PUT ${res.status}: ${text.slice(0, 200)}` };
1145
+ }
1146
+ const location = `s3://${cfg.bucket}/${key}`;
1147
+ let viewerUrl;
1148
+ if (cfg.presign?.enabled !== false) {
1149
+ const expiresSec = Math.min(
1150
+ (cfg.presign?.expiresHours ?? 24) * 3600,
1151
+ MAX_PRESIGN_SEC
1152
+ );
1153
+ const presigned = presignUrl({ url: objectUrl(key), creds, region, expiresSec });
1154
+ const origin = (cfg.presign?.viewerOrigin ?? defaultViewerOrigin).replace(/\/$/, "");
1155
+ viewerUrl = `${origin}/?src=${encodeURIComponent(presigned)}`;
1156
+ }
1157
+ return { ok: true, location, viewerUrl };
1158
+ }
1159
+ };
1160
+ }
1161
+
1162
+ // src/dump/sinks/http.ts
1163
+ import * as fs5 from "fs";
1164
+ var MAX_ATTEMPTS = 3;
1165
+ var POST_TIMEOUT_MS = 3e4;
1166
+ function createHttpSink(cfg) {
1167
+ return {
1168
+ name: "http",
1169
+ async deliver(filePath, meta) {
1170
+ const body = await fs5.promises.readFile(filePath);
1171
+ const metaHeader = JSON.stringify(meta).replace(/[\r\n]/g, " ");
1172
+ let lastDetail = "";
1173
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
1174
+ if (attempt > 0) {
1175
+ await new Promise((r) => setTimeout(r, 500 * 2 ** (attempt - 1)));
1176
+ }
1177
+ try {
1178
+ const res = await fetch(cfg.url, {
1179
+ method: "POST",
1180
+ headers: {
1181
+ "content-type": "application/gzip",
1182
+ "x-flightbox-meta": metaHeader,
1183
+ ...cfg.headers
1184
+ },
1185
+ body,
1186
+ // A stuck connection must never hang delivery forever.
1187
+ signal: AbortSignal.timeout(POST_TIMEOUT_MS)
1188
+ });
1189
+ if (res.ok) return { ok: true, location: cfg.url };
1190
+ lastDetail = `HTTP ${res.status}`;
1191
+ if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
1192
+ break;
1193
+ }
1194
+ } catch (err) {
1195
+ lastDetail = err.message;
1196
+ }
1197
+ }
1198
+ return { ok: false, detail: `${lastDetail} after retries \u2014 file kept in staging` };
1199
+ }
1200
+ };
1201
+ }
1202
+
1203
+ // src/dump/sinks/index.ts
1204
+ function createSink(cfg, defaultViewerOrigin) {
1205
+ switch (cfg.type) {
1206
+ case "disk":
1207
+ return createDiskSink(cfg);
1208
+ case "s3":
1209
+ return createS3Sink(cfg, defaultViewerOrigin);
1210
+ case "http":
1211
+ return createHttpSink(cfg);
1212
+ }
1213
+ }
1214
+
1215
+ // src/triggers/engine.ts
1216
+ var CRASH_DEDUPE_MS = 2e3;
1217
+ var TriggerEngine = class {
1218
+ constructor(cooldownMs, onFire, onSuppressed) {
1219
+ this.cooldownMs = cooldownMs;
1220
+ this.onFire = onFire;
1221
+ this.onSuppressed = onSuppressed;
1222
+ }
1223
+ cooldownMs;
1224
+ onFire;
1225
+ onSuppressed;
1226
+ lastFireMono = null;
1227
+ lastExemptFireMono = null;
1228
+ fire(info) {
1229
+ const now = nowMono();
1230
+ const windowMs = info.exemptFromCooldown ? CRASH_DEDUPE_MS : this.cooldownMs;
1231
+ const last = info.exemptFromCooldown ? this.lastExemptFireMono : this.lastFireMono;
1232
+ if (last !== null && Number(now - last) / 1e6 < windowMs) {
1233
+ try {
1234
+ this.onSuppressed(info);
1235
+ } catch {
1236
+ }
1237
+ return null;
1238
+ }
1239
+ this.lastFireMono = now;
1240
+ if (info.exemptFromCooldown) this.lastExemptFireMono = now;
1241
+ return this.onFire(info);
1242
+ }
1243
+ };
1244
+
1245
+ // src/triggers/manual.ts
1246
+ function createManualTrigger(engine) {
1247
+ return (reason = "manual trigger") => (
1248
+ // Sync mode: dev-invoked, must return a path to a file that exists.
1249
+ engine.fire({ type: "manual", reason, mode: "sync" })
1250
+ );
1251
+ }
1252
+
1253
+ // src/triggers/uncaught.ts
1254
+ import * as v8 from "v8";
1255
+
1256
+ // src/redaction.ts
1257
+ var SENSITIVE_MARKERS = [
1258
+ "password",
1259
+ "passwd",
1260
+ "secret",
1261
+ "token",
1262
+ "authorization",
1263
+ "auth",
1264
+ "apikey",
1265
+ "api_key",
1266
+ "api-key",
1267
+ "session",
1268
+ "cookie",
1269
+ "otp",
1270
+ "pin",
1271
+ "credential",
1272
+ "private_key",
1273
+ "bearer"
1274
+ ];
1275
+ var JWT_RE = /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{2,}\b/g;
1276
+ var KV_RE = /\b(password|passwd|secret|token|authorization|auth|api[_-]?key|apikey|session|cookie|otp|pin|credential|bearer)\b(["']?\s*[=:]\s*)("[^"]*"|'[^']*'|[^\s&,;)}\]]+)/gi;
1277
+ var PAN_CANDIDATE_RE = /\b(?:\d[ -]?){13,19}\b/g;
1278
+ function needsScrub(s) {
1279
+ const lower = s.toLowerCase();
1280
+ for (const m of SENSITIVE_MARKERS) {
1281
+ if (lower.includes(m)) return true;
1282
+ }
1283
+ if (lower.includes("eyj")) return true;
1284
+ return hasLongDigitRun(s);
1285
+ }
1286
+ function hasLongDigitRun(s) {
1287
+ let run = 0;
1288
+ for (let i = 0; i < s.length; i++) {
1289
+ const c = s.charCodeAt(i);
1290
+ if (c >= 48 && c <= 57) {
1291
+ if (++run >= 13) return true;
1292
+ } else if (c !== 32 && c !== 45) {
1293
+ run = 0;
1294
+ }
1295
+ }
1296
+ return false;
1297
+ }
1298
+ function luhnValid(digits) {
1299
+ let sum = 0;
1300
+ let alt = false;
1301
+ for (let i = digits.length - 1; i >= 0; i--) {
1302
+ let d = digits.charCodeAt(i) - 48;
1303
+ if (d < 0 || d > 9) return false;
1304
+ if (alt) {
1305
+ d *= 2;
1306
+ if (d > 9) d -= 9;
1307
+ }
1308
+ sum += d;
1309
+ alt = !alt;
1310
+ }
1311
+ return sum % 10 === 0;
1312
+ }
1313
+ function scrubText(s) {
1314
+ if (!needsScrub(s)) return s;
1315
+ let out = s;
1316
+ if (out.toLowerCase().includes("eyj")) {
1317
+ out = out.replace(JWT_RE, "[REDACTED:jwt]");
1318
+ }
1319
+ out = out.replace(KV_RE, "$1$2[REDACTED]");
1320
+ if (hasLongDigitRun(out)) {
1321
+ out = out.replace(PAN_CANDIDATE_RE, (m) => {
1322
+ const digits = m.replace(/[ -]/g, "");
1323
+ return digits.length >= 13 && digits.length <= 19 && luhnValid(digits) ? "[REDACTED:pan]" : m;
1324
+ });
1325
+ }
1326
+ return out;
1327
+ }
1328
+ function capScrub(s, limit) {
1329
+ const preCapped = s.length > limit * 4 ? s.slice(0, limit * 4) : s;
1330
+ const scrubbed = scrubText(preCapped);
1331
+ return scrubbed.length > limit ? scrubbed.slice(0, limit) : scrubbed;
1332
+ }
1333
+ var HEADER_ALLOWLIST = /* @__PURE__ */ new Set([
1334
+ "content-type",
1335
+ "content-length",
1336
+ "user-agent",
1337
+ "accept",
1338
+ "x-request-id"
1339
+ ]);
1340
+ function allowlistHeaders(headers) {
1341
+ if (!headers) return void 0;
1342
+ let out;
1343
+ for (const key of Object.keys(headers)) {
1344
+ if (HEADER_ALLOWLIST.has(key.toLowerCase())) {
1345
+ (out ??= {})[key.toLowerCase()] = String(headers[key]);
1346
+ }
1347
+ }
1348
+ return out;
1349
+ }
1350
+
1351
+ // src/triggers/uncaught.ts
1352
+ var OOM_HEAP_FRACTION = 0.94;
1353
+ function nearHeapLimit() {
1354
+ try {
1355
+ const stats = v8.getHeapStatistics();
1356
+ return stats.used_heap_size / stats.heap_size_limit > OOM_HEAP_FRACTION;
1357
+ } catch {
1358
+ return false;
1359
+ }
1360
+ }
1361
+ function crashDump(deps, type, err) {
1362
+ const message = capScrub(String(err?.message ?? err), 512);
1363
+ try {
1364
+ deps.agent.recorder.record(11 /* Custom */, {
1365
+ name: type,
1366
+ message,
1367
+ stack: capScrub(String(err?.stack ?? ""), 2048)
1368
+ });
1369
+ } catch {
1370
+ }
1371
+ if (nearHeapLimit()) {
1372
+ const ok = deps.panicDump();
1373
+ deps.agent.config.log(
1374
+ ok ? `OOM panic path: raw ring dumped; will be converted to .fbox on next boot` : `OOM panic path unavailable (no preopened fd)`
1375
+ );
1376
+ return;
1377
+ }
1378
+ deps.syncDump({ type, reason: message });
1379
+ }
1380
+ function installCrashTriggers(deps) {
1381
+ const onUncaught = (err) => {
1382
+ try {
1383
+ crashDump(deps, "uncaughtException", err);
1384
+ } catch {
1385
+ }
1386
+ };
1387
+ const onRejection = (reason) => {
1388
+ try {
1389
+ crashDump(deps, "unhandledRejection", reason);
1390
+ } catch {
1391
+ }
1392
+ if (process.listenerCount("unhandledRejection") === 1) {
1393
+ throw reason;
1394
+ }
1395
+ };
1396
+ process.on("uncaughtExceptionMonitor", onUncaught);
1397
+ process.on("unhandledRejection", onRejection);
1398
+ return () => {
1399
+ process.removeListener("uncaughtExceptionMonitor", onUncaught);
1400
+ process.removeListener("unhandledRejection", onRejection);
1401
+ };
1402
+ }
1403
+
1404
+ // src/triggers/slow-request.ts
1405
+ var inFlight = /* @__PURE__ */ new Map();
1406
+ function trackRequestStart(requestId, method, path8) {
1407
+ inFlight.set(requestId, { method, path: path8, startedAtMs: Date.now(), fired: false });
1408
+ }
1409
+ function trackRequestEnd(requestId) {
1410
+ inFlight.delete(requestId);
1411
+ }
1412
+ function maybeFireSlow(agent, method, path8, durMs) {
1413
+ if (durMs > agent.config.triggers.slowRequestMs) {
1414
+ agent.fire({
1415
+ type: "slowRequest",
1416
+ reason: `${method} ${path8} took ${durMs.toFixed(0)}ms (threshold ${agent.config.triggers.slowRequestMs}ms)`
1417
+ });
1418
+ }
1419
+ }
1420
+ function startWatchdog(agent) {
1421
+ const interval = setInterval(() => {
1422
+ try {
1423
+ const now = Date.now();
1424
+ const threshold = agent.config.triggers.slowRequestMs;
1425
+ for (const entry of inFlight.values()) {
1426
+ if (!entry.fired && now - entry.startedAtMs > threshold) {
1427
+ entry.fired = true;
1428
+ agent.fire({
1429
+ type: "slowRequest",
1430
+ reason: `${entry.method} ${entry.path} still in flight after ${(now - entry.startedAtMs).toFixed(0)}ms (watchdog)`
1431
+ });
1432
+ }
1433
+ }
1434
+ } catch {
1435
+ }
1436
+ }, 1e3);
1437
+ interval.unref();
1438
+ return () => {
1439
+ clearInterval(interval);
1440
+ inFlight.clear();
1441
+ };
1442
+ }
1443
+
1444
+ // src/instrumentations/http-server.ts
1445
+ import * as http from "http";
1446
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
1447
+ var patched = false;
1448
+ var agentRef;
1449
+ function tokenMatches(supplied, expected) {
1450
+ const a = createHash2("sha256").update(supplied).digest();
1451
+ const b = createHash2("sha256").update(expected).digest();
1452
+ return timingSafeEqual(a, b);
1453
+ }
1454
+ function handleFlightboxEndpoint(agent, req, res) {
1455
+ if (!agent.config.token || !req.url?.startsWith("/__flightbox/dump")) return false;
1456
+ try {
1457
+ const url = new URL(req.url, "http://localhost");
1458
+ if (url.pathname !== "/__flightbox/dump") return false;
1459
+ if (req.method !== "GET") {
1460
+ res.writeHead(405, { allow: "GET" }).end();
1461
+ return true;
1462
+ }
1463
+ const supplied = String(req.headers["x-flightbox-token"] ?? "");
1464
+ if (!supplied || !tokenMatches(supplied, agent.config.token)) {
1465
+ res.writeHead(401, { "content-type": "application/json" });
1466
+ res.end('{"error":"invalid token"}');
1467
+ return true;
1468
+ }
1469
+ const rawReason = url.searchParams.get("reason") ?? "no reason given";
1470
+ const reason = truncate(scrubText(truncate(rawReason, 512)), 120);
1471
+ const staged = agent.fire({
1472
+ type: "manual",
1473
+ reason: `http endpoint (${reason})`,
1474
+ mode: "sync"
1475
+ // interactive call: the returned path must exist
1476
+ });
1477
+ res.writeHead(staged ? 200 : 429, { "content-type": "application/json" });
1478
+ res.end(JSON.stringify({ staged: staged ?? null, suppressed: staged === null }));
1479
+ } catch {
1480
+ try {
1481
+ res.writeHead(500);
1482
+ res.end();
1483
+ } catch {
1484
+ }
1485
+ }
1486
+ return true;
1487
+ }
1488
+ function instrumentHttpServer(agent) {
1489
+ agentRef = agent;
1490
+ if (patched) return;
1491
+ patched = true;
1492
+ const orig = http.Server.prototype.emit;
1493
+ const flightboxEmit = function(event) {
1494
+ const args = arguments;
1495
+ if (event !== "request") {
1496
+ return orig.apply(this, args);
1497
+ }
1498
+ const agent2 = agentRef;
1499
+ try {
1500
+ if (handleFlightboxEndpoint(agent2, args[1], args[2])) {
1501
+ return true;
1502
+ }
1503
+ } catch {
1504
+ }
1505
+ if (isShedding()) {
1506
+ return orig.apply(this, args);
1507
+ }
1508
+ let ctx;
1509
+ try {
1510
+ const req = args[1];
1511
+ const res = args[2];
1512
+ const requestId = newRequestId();
1513
+ const tStart = nowMono();
1514
+ const method = req.method ?? "GET";
1515
+ const path8 = capScrub(req.url ?? "", LIMITS.path);
1516
+ ctx = { requestId };
1517
+ agent2.recorder.record(
1518
+ 1 /* HttpServerStart */,
1519
+ // Bible §7: headers are allowlisted (content-type, user-agent, ...)
1520
+ // — never captured wholesale.
1521
+ { method, path: path8, headers: allowlistHeaders(req.headers) },
1522
+ { requestId, tMonoNs: tStart }
1523
+ );
1524
+ trackRequestStart(requestId, method, path8);
1525
+ const finish = (aborted) => {
1526
+ try {
1527
+ trackRequestEnd(requestId);
1528
+ const durMs = Number(nowMono() - tStart) / 1e6;
1529
+ agent2.recorder.record(
1530
+ 2 /* HttpServerEnd */,
1531
+ aborted ? { method, path: path8, aborted: true, durMs } : { method, path: path8, status: res.statusCode, durMs },
1532
+ { requestId }
1533
+ );
1534
+ maybeFireSlow(agent2, method, path8, durMs);
1535
+ } catch {
1536
+ }
1537
+ ctx.requestId = 0n;
1538
+ };
1539
+ res.once("finish", () => finish(false));
1540
+ res.once("close", () => {
1541
+ if (!res.writableFinished) finish(true);
1542
+ });
1543
+ } catch {
1544
+ return orig.apply(this, args);
1545
+ }
1546
+ return requestStorage.run(ctx, () => orig.apply(this, args));
1547
+ };
1548
+ http.Server.prototype.emit = flightboxEmit;
1549
+ }
1550
+
1551
+ // src/instrumentations/http-client.ts
1552
+ import { createRequire } from "module";
1553
+ var patched2 = false;
1554
+ var agentRef2;
1555
+ function extractTarget(args) {
1556
+ let method = "GET";
1557
+ let host = "";
1558
+ let path8 = "";
1559
+ const a0 = args[0];
1560
+ if (typeof a0 === "string") {
1561
+ try {
1562
+ const u = new URL(a0);
1563
+ host = u.host;
1564
+ path8 = u.pathname + u.search;
1565
+ } catch {
1566
+ path8 = a0;
1567
+ }
1568
+ } else if (a0 instanceof URL) {
1569
+ host = a0.host;
1570
+ path8 = a0.pathname + a0.search;
1571
+ } else if (a0 && typeof a0 === "object") {
1572
+ const o = a0;
1573
+ host = o.host ?? o.hostname ?? "";
1574
+ path8 = o.path ?? "/";
1575
+ method = o.method ?? "GET";
1576
+ }
1577
+ const a1 = args[1];
1578
+ if (a1 && typeof a1 === "object" && !(a1 instanceof URL) && typeof a1 !== "function") {
1579
+ const o = a1;
1580
+ if (o.method) method = o.method;
1581
+ if (o.host || o.hostname) host = o.host ?? o.hostname;
1582
+ }
1583
+ return { method: String(method).toUpperCase(), host: String(host), path: String(path8) };
1584
+ }
1585
+ var nodeRequire = createRequire(process.cwd() + "/noop.js");
1586
+ function wrapModule(mod) {
1587
+ const origRequest = mod.request.bind(mod);
1588
+ const tracedRequest = function(...args) {
1589
+ if (isShedding()) return origRequest(...args);
1590
+ const agent = agentRef2;
1591
+ let spanId = 0n;
1592
+ let requestId = 0n;
1593
+ let tStart = 0n;
1594
+ let recordedStart = false;
1595
+ try {
1596
+ const target = extractTarget(args);
1597
+ spanId = newSpanId();
1598
+ requestId = currentRequestId();
1599
+ tStart = nowMono();
1600
+ agent.recorder.record(
1601
+ 3 /* HttpClientStart */,
1602
+ {
1603
+ method: target.method,
1604
+ host: truncate(target.host, 256),
1605
+ path: capScrub(target.path, LIMITS.path)
1606
+ },
1607
+ { spanId, requestId, tMonoNs: tStart }
1608
+ );
1609
+ recordedStart = true;
1610
+ } catch {
1611
+ }
1612
+ const req = origRequest(...args);
1613
+ if (recordedStart) {
1614
+ const done = (status, error) => {
1615
+ try {
1616
+ const payload = {
1617
+ durMs: Number(nowMono() - tStart) / 1e6
1618
+ };
1619
+ if (status !== void 0) payload.status = status;
1620
+ if (error !== void 0) payload.error = capScrub(error, 256);
1621
+ agent.recorder.record(4 /* HttpClientEnd */, payload, { spanId, requestId });
1622
+ } catch {
1623
+ }
1624
+ };
1625
+ try {
1626
+ req.once("response", (res) => done(res.statusCode));
1627
+ req.once("error", (err) => {
1628
+ done(void 0, err.message);
1629
+ if (req.listenerCount("error") === 0) throw err;
1630
+ });
1631
+ } catch {
1632
+ }
1633
+ }
1634
+ return req;
1635
+ };
1636
+ mod.request = tracedRequest;
1637
+ mod.get = function(...args) {
1638
+ const req = tracedRequest(...args);
1639
+ req.end();
1640
+ return req;
1641
+ };
1642
+ }
1643
+ function wrapFetch() {
1644
+ const origFetch = globalThis.fetch;
1645
+ if (typeof origFetch !== "function") return;
1646
+ globalThis.fetch = async function flightboxFetch(input, init) {
1647
+ if (isShedding()) return origFetch(input, init);
1648
+ const agent = agentRef2;
1649
+ let spanId = 0n;
1650
+ let requestId = 0n;
1651
+ let tStart = 0n;
1652
+ let recordedStart = false;
1653
+ try {
1654
+ const url = typeof input === "string" ? input : input?.url ?? String(input);
1655
+ let host = "";
1656
+ let path8 = String(url);
1657
+ try {
1658
+ const u = new URL(url);
1659
+ host = u.host;
1660
+ path8 = u.pathname + u.search;
1661
+ } catch {
1662
+ }
1663
+ spanId = newSpanId();
1664
+ requestId = currentRequestId();
1665
+ tStart = nowMono();
1666
+ agent.recorder.record(
1667
+ 3 /* HttpClientStart */,
1668
+ {
1669
+ method: String(
1670
+ init?.method ?? (typeof input === "object" ? input?.method : void 0) ?? "GET"
1671
+ ).toUpperCase(),
1672
+ host: truncate(host, 256),
1673
+ path: capScrub(path8, LIMITS.path),
1674
+ via: "fetch"
1675
+ },
1676
+ { spanId, requestId, tMonoNs: tStart }
1677
+ );
1678
+ recordedStart = true;
1679
+ } catch {
1680
+ }
1681
+ const done = (status, error) => {
1682
+ if (!recordedStart) return;
1683
+ try {
1684
+ const payload = { durMs: Number(nowMono() - tStart) / 1e6 };
1685
+ if (status !== void 0) payload.status = status;
1686
+ if (error !== void 0) payload.error = capScrub(error, 256);
1687
+ agent.recorder.record(4 /* HttpClientEnd */, payload, { spanId, requestId });
1688
+ } catch {
1689
+ }
1690
+ };
1691
+ try {
1692
+ const res = await origFetch(input, init);
1693
+ done(res.status);
1694
+ return res;
1695
+ } catch (err) {
1696
+ done(void 0, err?.message ?? String(err));
1697
+ throw err;
1698
+ }
1699
+ };
1700
+ }
1701
+ function instrumentHttpClient(agent) {
1702
+ agentRef2 = agent;
1703
+ if (patched2) return;
1704
+ patched2 = true;
1705
+ try {
1706
+ wrapModule(nodeRequire("node:http"));
1707
+ } catch {
1708
+ }
1709
+ try {
1710
+ wrapModule(nodeRequire("node:https"));
1711
+ } catch {
1712
+ }
1713
+ try {
1714
+ wrapFetch();
1715
+ } catch {
1716
+ }
1717
+ }
1718
+
1719
+ // src/instrumentations/pg.ts
1720
+ import { createRequire as createRequire2 } from "module";
1721
+ import * as path6 from "path";
1722
+ function loadPg() {
1723
+ try {
1724
+ const req = createRequire2(path6.join(process.cwd(), "noop.js"));
1725
+ return req("pg");
1726
+ } catch {
1727
+ return null;
1728
+ }
1729
+ }
1730
+ function paramShapes(values) {
1731
+ if (!Array.isArray(values)) return void 0;
1732
+ return values.slice(0, 32).map((v) => v === null ? "null" : Array.isArray(v) ? "array" : typeof v);
1733
+ }
1734
+ var agentRef3;
1735
+ function instrumentPg(agent) {
1736
+ agentRef3 = agent;
1737
+ const recorder = {
1738
+ record: ((...args) => agentRef3.recorder.record(...args))
1739
+ };
1740
+ const pg = loadPg();
1741
+ if (!pg?.Client?.prototype?.query) return false;
1742
+ if (!pg.Client.prototype.query.__flightbox) {
1743
+ const origQuery = pg.Client.prototype.query;
1744
+ const wrappedQuery = function(...args) {
1745
+ if (isShedding()) return origQuery.apply(this, args);
1746
+ let spanId = 0n;
1747
+ let requestId = 0n;
1748
+ let tStart = 0n;
1749
+ let recordedStart = false;
1750
+ try {
1751
+ const first = args[0];
1752
+ const text = typeof first === "string" ? first : first?.text;
1753
+ const values = Array.isArray(args[1]) ? args[1] : Array.isArray(first?.values) ? first.values : void 0;
1754
+ spanId = newSpanId();
1755
+ requestId = currentRequestId();
1756
+ tStart = nowMono();
1757
+ recorder.record(
1758
+ 5 /* PgQueryStart */,
1759
+ {
1760
+ text: capScrub(typeof text === "string" ? text : "<unknown>", LIMITS.query),
1761
+ params: paramShapes(values)
1762
+ },
1763
+ { spanId, requestId, tMonoNs: tStart }
1764
+ );
1765
+ recordedStart = true;
1766
+ } catch {
1767
+ }
1768
+ const done = (err) => {
1769
+ if (!recordedStart) return;
1770
+ try {
1771
+ const payload = {
1772
+ durMs: Number(nowMono() - tStart) / 1e6
1773
+ };
1774
+ if (err) {
1775
+ payload.error = truncate(String(err?.message ?? err), 256);
1776
+ }
1777
+ recorder.record(6 /* PgQueryEnd */, payload, { spanId, requestId });
1778
+ } catch {
1779
+ }
1780
+ };
1781
+ try {
1782
+ const last = args[args.length - 1];
1783
+ if (typeof last === "function") {
1784
+ args[args.length - 1] = function(err) {
1785
+ done(err);
1786
+ return last.apply(this, arguments);
1787
+ };
1788
+ return origQuery.apply(this, args);
1789
+ }
1790
+ } catch {
1791
+ }
1792
+ const out = origQuery.apply(this, args);
1793
+ try {
1794
+ if (out && typeof out.then === "function") {
1795
+ return out.then(
1796
+ (v) => {
1797
+ done();
1798
+ return v;
1799
+ },
1800
+ (e) => {
1801
+ done(e);
1802
+ throw e;
1803
+ }
1804
+ );
1805
+ }
1806
+ if (out && typeof out.on === "function") {
1807
+ out.once("end", () => done());
1808
+ out.once("error", (e) => {
1809
+ done(e);
1810
+ if (out.listenerCount("error") === 0) throw e;
1811
+ });
1812
+ }
1813
+ } catch {
1814
+ }
1815
+ return out;
1816
+ };
1817
+ wrappedQuery.__flightbox = true;
1818
+ pg.Client.prototype.query = wrappedQuery;
1819
+ }
1820
+ const Pool = pg.Pool;
1821
+ if (Pool?.prototype?.connect && !Pool.prototype.connect.__flightbox) {
1822
+ const origConnect = Pool.prototype.connect;
1823
+ const wrappedConnect = function(cb) {
1824
+ if (isShedding()) return origConnect.call(this, cb);
1825
+ const tStart = nowMono();
1826
+ const requestId = currentRequestId();
1827
+ const record = () => {
1828
+ try {
1829
+ recorder.record(
1830
+ 7 /* PgPoolWait */,
1831
+ { waitMs: Number(nowMono() - tStart) / 1e6 },
1832
+ { requestId }
1833
+ );
1834
+ } catch {
1835
+ }
1836
+ };
1837
+ if (typeof cb === "function") {
1838
+ return origConnect.call(this, function(...cbArgs) {
1839
+ record();
1840
+ return cb.apply(this, cbArgs);
1841
+ });
1842
+ }
1843
+ const out = origConnect.call(this);
1844
+ try {
1845
+ if (out && typeof out.then === "function") out.then(record, record);
1846
+ } catch {
1847
+ }
1848
+ return out;
1849
+ };
1850
+ wrappedConnect.__flightbox = true;
1851
+ Pool.prototype.connect = wrappedConnect;
1852
+ }
1853
+ return true;
1854
+ }
1855
+
1856
+ // src/instrumentations/console.ts
1857
+ import { formatWithOptions } from "util";
1858
+ var LEVELS = ["log", "info", "warn", "error", "debug"];
1859
+ var FORMAT_OPTS = { depth: 2, maxArrayLength: 20, maxStringLength: LIMITS.log };
1860
+ var patched3 = false;
1861
+ var agentRef4;
1862
+ function instrumentConsole(agent) {
1863
+ agentRef4 = agent;
1864
+ if (patched3) return;
1865
+ patched3 = true;
1866
+ for (const level of LEVELS) {
1867
+ const orig = console[level].bind(console);
1868
+ console[level] = function flightboxConsole(...args) {
1869
+ try {
1870
+ if (!isShedding()) {
1871
+ const msg = capScrub(formatWithOptions(FORMAT_OPTS, ...args), LIMITS.log);
1872
+ agentRef4.recorder.record(8 /* Log */, { level, msg });
1873
+ }
1874
+ } catch {
1875
+ }
1876
+ orig(...args);
1877
+ };
1878
+ }
1879
+ }
1880
+
1881
+ // src/instrumentations/registry.ts
1882
+ var INSTRUMENTATIONS = [
1883
+ ["http-server", instrumentHttpServer],
1884
+ ["http-client", instrumentHttpClient],
1885
+ ["pg", instrumentPg],
1886
+ ["console", instrumentConsole]
1887
+ ];
1888
+ function applyInstrumentations(agent) {
1889
+ for (const [name, apply] of INSTRUMENTATIONS) {
1890
+ try {
1891
+ if (apply(agent) === false) {
1892
+ agent.config.log(`instrumentation ${name}: module not present, skipped`);
1893
+ }
1894
+ } catch (err) {
1895
+ agent.config.log(`instrumentation ${name} failed to apply: ${err.message}`);
1896
+ }
1897
+ }
1898
+ }
1899
+
1900
+ // src/instrumentations/vitals.ts
1901
+ import { monitorEventLoopDelay } from "perf_hooks";
1902
+ import * as v82 from "v8";
1903
+ function startVitals(agent) {
1904
+ const histogram = monitorEventLoopDelay({ resolution: 20 });
1905
+ histogram.enable();
1906
+ const { shedLagMs } = agent.config.shedding;
1907
+ const { heapPct, stallMs } = agent.config.triggers;
1908
+ const heapLimit = v82.getHeapStatistics().heap_size_limit;
1909
+ let memAbove = false;
1910
+ let stallAbove = false;
1911
+ const interval = setInterval(() => {
1912
+ try {
1913
+ const lagMs = histogram.mean / 1e6;
1914
+ const maxLagMs = histogram.max / 1e6;
1915
+ histogram.reset();
1916
+ const mem = process.memoryUsage();
1917
+ agent.recorder.record(9 /* Vitals */, {
1918
+ lagMs: Math.round(lagMs * 1e3) / 1e3,
1919
+ maxLagMs: Math.round(maxLagMs * 1e3) / 1e3,
1920
+ heapUsed: mem.heapUsed,
1921
+ heapTotal: mem.heapTotal,
1922
+ rss: mem.rss
1923
+ });
1924
+ if (!isShedding() && lagMs > shedLagMs) {
1925
+ setShedding(true);
1926
+ agent.recorder.record(11 /* Custom */, {
1927
+ name: "flightbox.shedding",
1928
+ on: true,
1929
+ lagMs: Math.round(lagMs)
1930
+ });
1931
+ agent.config.log(
1932
+ `event-loop lag ${lagMs.toFixed(0)}ms > ${shedLagMs}ms \u2014 load-shedding ON (ADR 012)`
1933
+ );
1934
+ } else if (isShedding() && lagMs < shedLagMs / 2) {
1935
+ setShedding(false);
1936
+ agent.recorder.record(11 /* Custom */, {
1937
+ name: "flightbox.shedding",
1938
+ on: false,
1939
+ lagMs: Math.round(lagMs)
1940
+ });
1941
+ agent.config.log("event-loop lag recovered \u2014 load-shedding OFF");
1942
+ }
1943
+ const heapFrac = mem.heapUsed / heapLimit;
1944
+ if (heapFrac > heapPct && !memAbove) {
1945
+ memAbove = true;
1946
+ agent.fire({
1947
+ type: "memory",
1948
+ reason: `heapUsed at ${(heapFrac * 100).toFixed(0)}% of V8 limit`
1949
+ });
1950
+ } else if (memAbove && heapFrac < heapPct * 0.9) {
1951
+ memAbove = false;
1952
+ }
1953
+ if (maxLagMs > stallMs && !stallAbove) {
1954
+ stallAbove = true;
1955
+ agent.fire({
1956
+ type: "eventloop-stall",
1957
+ reason: `event loop stalled ${maxLagMs.toFixed(0)}ms (threshold ${stallMs}ms)`
1958
+ });
1959
+ } else if (stallAbove && maxLagMs < stallMs / 2) {
1960
+ stallAbove = false;
1961
+ }
1962
+ } catch {
1963
+ }
1964
+ }, 100);
1965
+ interval.unref();
1966
+ return () => {
1967
+ clearInterval(interval);
1968
+ histogram.disable();
1969
+ };
1970
+ }
1971
+
1972
+ // src/index.ts
1973
+ var SINK_RETRY_INTERVAL_MS = 5 * 6e4;
1974
+ var EMERGENCY_RING_BYTES = 4 * 1024 * 1024;
1975
+ var instance = null;
1976
+ function start(userConfig = {}) {
1977
+ if (instance) return;
1978
+ const config = resolveConfig(userConfig);
1979
+ if (!isMainThread && !config.allowWorkerThreads) {
1980
+ config.log(
1981
+ "worker thread detected \u2014 agent not started (each thread would allocate its own ring buffer; see ADR 014). Set allowWorkerThreads: true to record in this thread anyway."
1982
+ );
1983
+ return;
1984
+ }
1985
+ anchorClock();
1986
+ const recorder = new Recorder(new RingBuffer(config.bufferMb * 1024 * 1024));
1987
+ const staging = new Staging(config.dir, config.staging, config.log);
1988
+ staging.claimOrphanedSync();
1989
+ recoverPanicFiles(staging, config.service, config.log);
1990
+ const unsent = recoverOnBoot(staging, config.log);
1991
+ const panicWriter = new PanicWriter(staging.stagingDir);
1992
+ panicWriter.arm();
1993
+ const sinks = config.sinks.map((cfg) => createSink(cfg, config.viewerOrigin));
1994
+ const engine = new TriggerEngine(
1995
+ config.triggers.cooldownMs,
1996
+ (info) => dump(info),
1997
+ (info) => recorder.record(10 /* Trigger */, {
1998
+ suppressed: true,
1999
+ triggerType: info.type,
2000
+ reason: info.reason
2001
+ })
2002
+ );
2003
+ const agent = {
2004
+ recorder,
2005
+ config,
2006
+ fire: (info) => engine.fire(info)
2007
+ };
2008
+ instance = {
2009
+ config,
2010
+ recorder,
2011
+ staging,
2012
+ engine,
2013
+ panicWriter,
2014
+ sinks,
2015
+ manualTrigger: createManualTrigger(engine),
2016
+ teardowns: [],
2017
+ deliveryInFlight: /* @__PURE__ */ new Set()
2018
+ };
2019
+ applyInstrumentations(agent);
2020
+ instance.teardowns.push(startWatchdog(agent));
2021
+ instance.teardowns.push(startVitals(agent));
2022
+ instance.teardowns.push(
2023
+ installCrashTriggers({
2024
+ agent,
2025
+ // Crash dumps bypass the routine cooldown (a slow-request dump minutes
2026
+ // ago must never cost us the final-moments dump); the engine's short
2027
+ // crash window still collapses rejection→rethrow→uncaught to one file.
2028
+ syncDump: (info) => engine.fire({ ...info, mode: "sync", exemptFromCooldown: true }),
2029
+ panicDump: () => panicWriter.writeSync(recorder.activeRing)
2030
+ })
2031
+ );
2032
+ recorder.armed = true;
2033
+ config.log(
2034
+ `armed \u2014 ${config.bufferMb}MB ring buffer, staging at ${staging.stagingDir}` + (sinks.length ? `, sinks: ${sinks.map((s) => s.name).join("+")}` : ", no sinks (staging only)")
2035
+ );
2036
+ if (sinks.length > 0 && unsent.length > 0) {
2037
+ void deliverRecovered(instance, unsent);
2038
+ }
2039
+ if (sinks.length > 0) {
2040
+ const inst = instance;
2041
+ const retry = setInterval(() => void retryStaged(inst), SINK_RETRY_INTERVAL_MS);
2042
+ retry.unref();
2043
+ instance.teardowns.push(() => clearInterval(retry));
2044
+ }
2045
+ }
2046
+ async function retryStaged(inst) {
2047
+ try {
2048
+ for (const file of inst.staging.listStagedSync()) {
2049
+ if (inst.deliveryInFlight.has(file)) continue;
2050
+ const meta = readMetaSync(file);
2051
+ if (!meta) continue;
2052
+ inst.config.log(`retrying delivery of ${path7.basename(file)}...`);
2053
+ await deliverFile(inst, file, meta);
2054
+ }
2055
+ } catch {
2056
+ }
2057
+ }
2058
+ function readMetaSync(filePath) {
2059
+ try {
2060
+ const parsed = JSON.parse(zlib.gunzipSync(fs6.readFileSync(filePath)).toString("utf8"));
2061
+ return parsed?.meta ?? null;
2062
+ } catch {
2063
+ return null;
2064
+ }
2065
+ }
2066
+ async function deliverRecovered(inst, files) {
2067
+ for (const file of files) {
2068
+ const meta = readMetaSync(file);
2069
+ if (!meta) continue;
2070
+ inst.config.log(`delivering recovered incident ${path7.basename(file)}...`);
2071
+ await deliverFile(inst, file, meta);
2072
+ }
2073
+ }
2074
+ async function deliverFile(inst, filePath, meta) {
2075
+ if (inst.sinks.length === 0 || inst.deliveryInFlight.has(filePath)) return;
2076
+ inst.deliveryInFlight.add(filePath);
2077
+ try {
2078
+ await deliverFileInner(inst, filePath, meta);
2079
+ } finally {
2080
+ inst.deliveryInFlight.delete(filePath);
2081
+ }
2082
+ }
2083
+ async function deliverFileInner(inst, filePath, meta) {
2084
+ let allOk = true;
2085
+ let viewerUrl;
2086
+ for (const sink of inst.sinks) {
2087
+ try {
2088
+ const result = await sink.deliver(filePath, meta);
2089
+ if (result.ok) {
2090
+ if (result.location) inst.config.log(` stored: ${result.location}`);
2091
+ viewerUrl ??= result.viewerUrl;
2092
+ } else {
2093
+ allOk = false;
2094
+ inst.config.log(` sink ${sink.name} failed: ${result.detail ?? "unknown error"}`);
2095
+ }
2096
+ } catch (err) {
2097
+ allOk = false;
2098
+ inst.config.log(` sink ${sink.name} threw: ${err.message}`);
2099
+ }
2100
+ }
2101
+ if (viewerUrl) inst.config.log(` open: ${viewerUrl}`);
2102
+ if (allOk) {
2103
+ try {
2104
+ inst.staging.markDeliveredSync(filePath);
2105
+ } catch {
2106
+ }
2107
+ }
2108
+ }
2109
+ function dump(info) {
2110
+ const inst = instance;
2111
+ if (!inst) return null;
2112
+ inst.recorder.record(10 /* Trigger */, {
2113
+ triggerType: info.type,
2114
+ reason: info.reason
2115
+ });
2116
+ const replacement = info.type === "memory" ? EMERGENCY_RING_BYTES : void 0;
2117
+ if (replacement) {
2118
+ inst.config.log(
2119
+ `memory trigger: recording continues in a ${EMERGENCY_RING_BYTES / 1048576}MB emergency ring until pressure clears`
2120
+ );
2121
+ }
2122
+ const events = takeFrozenSnapshot(inst.recorder, replacement);
2123
+ const prepared = buildIncident({
2124
+ service: inst.config.service,
2125
+ trigger: { type: info.type, reason: info.reason },
2126
+ events
2127
+ });
2128
+ const finalize = (data) => {
2129
+ let stagedPath;
2130
+ try {
2131
+ stagedPath = inst.staging.stageSync(prepared.fileName, data);
2132
+ } catch (err) {
2133
+ if (err instanceof DiskFullError) {
2134
+ inst.recorder.armed = false;
2135
+ inst.config.log(`DISK CRITICAL \u2014 ${err.message}. Agent disabled.`);
2136
+ return null;
2137
+ }
2138
+ inst.config.log(`failed to stage incident: ${err.message}`);
2139
+ return null;
2140
+ }
2141
+ inst.config.log(
2142
+ `\u{1F534} incident captured (${(data.length / 1024).toFixed(1)} KB gz, ${prepared.eventCount} events, ${(prepared.windowMs / 1e3).toFixed(1)}s window)`
2143
+ );
2144
+ inst.config.log(` trigger: ${info.type}${info.reason ? ` \u2014 ${info.reason}` : ""}`);
2145
+ inst.config.log(` staged: ${stagedPath}`);
2146
+ void deliverFile(inst, stagedPath, prepared.meta);
2147
+ return stagedPath;
2148
+ };
2149
+ if (info.mode === "sync") {
2150
+ return finalize(zlib.gzipSync(prepared.json, { level: DUMP_GZIP_LEVEL }));
2151
+ }
2152
+ const prospectivePath = path7.join(inst.staging.stagingDir, prepared.fileName);
2153
+ zlib.gzip(prepared.json, { level: DUMP_GZIP_LEVEL }, (err, data) => {
2154
+ if (err) {
2155
+ inst.config.log(`gzip failed for ${prepared.fileName}: ${err.message}`);
2156
+ return;
2157
+ }
2158
+ finalize(data);
2159
+ });
2160
+ return prospectivePath;
2161
+ }
2162
+ function trigger(reason) {
2163
+ return instance ? instance.manualTrigger(reason) : null;
2164
+ }
2165
+ function addEvent(name, data = {}) {
2166
+ instance?.recorder.record(11 /* Custom */, {
2167
+ name: truncate(name, LIMITS.path),
2168
+ ...data
2169
+ });
2170
+ }
2171
+ function stop() {
2172
+ if (instance) {
2173
+ instance.recorder.armed = false;
2174
+ for (const teardown of instance.teardowns) {
2175
+ try {
2176
+ teardown();
2177
+ } catch {
2178
+ }
2179
+ }
2180
+ instance.panicWriter.disarm();
2181
+ setShedding(false);
2182
+ instance = null;
2183
+ }
2184
+ }
2185
+ var index_default = { start, stop, trigger, addEvent };
2186
+
2187
+ export {
2188
+ start,
2189
+ trigger,
2190
+ addEvent,
2191
+ stop,
2192
+ index_default
2193
+ };
2194
+ //# sourceMappingURL=chunk-HCJF2ASP.mjs.map