@flareapp/node 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,24 +34,8 @@ let node_fs_promises = require("node:fs/promises");
34
34
  let node_url = require("node:url");
35
35
 
36
36
  //#region src/context/body.ts
37
- /** `^` plus `\b` accepts a `; charset=utf-8` suffix while still rejecting `...-urlencoded-foo`. */
38
37
  const DEFAULT_BODY_CONTENT_TYPES = /^application\/(json|x-www-form-urlencoded)\b/i;
39
- /** Reuses core's URL denylist, so credentials are caught by the same regex everywhere. */
40
38
  const DEFAULT_BODY_KEY_DENYLIST = _flareapp_core.DEFAULT_URL_DENYLIST;
41
- /**
42
- * Normalize, redact, serialize, and size-cap a request body for a Flare report. Returns the JSON string,
43
- * or `null` when the body should not be reported (unknown shape, content-type miss, serialization fail).
44
- *
45
- * Accepts four runtime shapes:
46
- * - `string`: must match `contentType` per `bodyAllowedContentTypes`, else dropped.
47
- * - `Buffer`: decoded UTF-8 then treated as a string.
48
- * - `URLSearchParams`: flattened to a plain record. No content-type gate (shape is unambiguous).
49
- * - Other `object`/array: used as-is, no gate. The common middleware path (Express/Fastify `req.body`).
50
- * Anything else returns `null`.
51
- *
52
- * Then: redact (denylisted keys become `'[redacted]'`, cycles become `'[Circular]'`), `JSON.stringify`
53
- * (drop body if it throws on BigInt/Symbol/etc), and truncate to `bodyMaxBytes` including the suffix.
54
- */
55
39
  function captureBody(body, contentType, opts) {
56
40
  if (body === void 0 || body === null) return null;
57
41
  let parsed;
@@ -77,7 +61,6 @@ function captureBody(body, contentType, opts) {
77
61
  }
78
62
  const TRUNCATION_SUFFIX = "…[truncated]";
79
63
  const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
80
- /** Walks back over continuation bytes (`10xxxxxx`) to a codepoint boundary, so the result still decodes. */
81
64
  function truncateToByteLimit(serialized, maxBytes) {
82
65
  const buf = Buffer.from(serialized, "utf8");
83
66
  if (buf.length <= maxBytes) return serialized;
@@ -91,15 +74,12 @@ function truncateToByteLimit(serialized, maxBytes) {
91
74
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
92
75
  return buf.subarray(0, cut).toString("utf8") + TRUNCATION_SUFFIX;
93
76
  }
94
- /** Normalizes to the bare media type first, so a strict custom regex like `/^application\/json$/` still
95
- * matches `application/json; charset=utf-8`. */
96
77
  function matchesContentType(ct, allowed) {
97
78
  if (!ct) return false;
98
79
  const mediaType = ct.split(";")[0].trim().toLowerCase();
99
80
  if (!mediaType) return false;
100
81
  return allowed.test(mediaType);
101
82
  }
102
- /** Returns `undefined` rather than `null` on failure, since `null` is itself a valid JSON value. */
103
83
  function parseString(text, contentType) {
104
84
  if (contentType && /x-www-form-urlencoded/i.test(contentType)) return Object.fromEntries(new URLSearchParams(text).entries());
105
85
  try {
@@ -108,13 +88,11 @@ function parseString(text, contentType) {
108
88
  return;
109
89
  }
110
90
  }
111
- /** Excludes class instances, streams, FormData, ArrayBuffer views, Buffer and URLSearchParams. */
112
91
  function isPlainObject(value) {
113
92
  if (value === null || typeof value !== "object") return false;
114
93
  const proto = Object.getPrototypeOf(value);
115
94
  return proto === null || proto === Object.prototype;
116
95
  }
117
- /** `seen` is a parameter rather than a closure to avoid allocating a WeakSet per recursion. */
118
96
  function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
119
97
  if (value === null || typeof value !== "object") return value;
120
98
  if (seen.has(value)) return "[Circular]";
@@ -127,7 +105,6 @@ function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
127
105
 
128
106
  //#endregion
129
107
  //#region src/context/deviceInfo.ts
130
- /** Node os + runtime as normalised device info. */
131
108
  var NodeDeviceInfoProvider = class {
132
109
  collect() {
133
110
  return {
@@ -146,7 +123,6 @@ const nodeDeviceInfoProvider = new NodeDeviceInfoProvider();
146
123
 
147
124
  //#endregion
148
125
  //#region src/context/headers.ts
149
- /** Case-insensitive. An array value collapses to its first element; callers here want a single value. */
150
126
  function findHeader(headers, name) {
151
127
  if (!headers) return;
152
128
  const target = name.toLowerCase();
@@ -156,20 +132,12 @@ function findHeader(headers, name) {
156
132
  return Array.isArray(value) ? value[0] : value;
157
133
  }
158
134
  }
159
- /** The `^` and `$` matter: without them, `cookie` would also match a header like `X-Some-Cookie-Hint`. */
160
135
  const DEFAULT_HEADER_DENYLIST = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-csrf-token|x-xsrf-token|x-auth-token|forwarded|x-forwarded-(?:for|user))$/i;
161
- /** `g`/`y` are stripped from a custom pattern: those carry lastIndex, which makes `.test()` stateful. */
162
136
  function resolveHeaderDenylist(custom, replaceDefault = false) {
163
137
  if (!custom) return DEFAULT_HEADER_DENYLIST;
164
138
  if (replaceDefault) return new RegExp(custom.source, custom.flags.replace(/[gy]/g, ""));
165
139
  return new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:${custom.source})`, "i");
166
140
  }
167
- /**
168
- * Turns headers into `http.request.header.<name>` attributes. The two lists differ on purpose: an
169
- * allowlist drops a header entirely, for apps that may only send named headers, while the denylist
170
- * keeps the name and replaces the value, so you can still see the header was there. `undefined` is
171
- * how `node:http` says "not sent", so those are dropped.
172
- */
173
141
  function projectHeaders(headers, options) {
174
142
  const out = {};
175
143
  if (!headers) return out;
@@ -185,10 +153,6 @@ function projectHeaders(headers, options) {
185
153
 
186
154
  //#endregion
187
155
  //#region src/context/process.ts
188
- /**
189
- * Host + process attributes, read per report so `process.uptime()` and `os.hostname()` stay current.
190
- * os and runtime come from `NodeDeviceInfoProvider`.
191
- */
192
156
  function collectProcessAttributes() {
193
157
  return {
194
158
  "process.pid": process.pid,
@@ -200,13 +164,6 @@ function collectProcessAttributes() {
200
164
 
201
165
  //#endregion
202
166
  //#region src/context/collectNode.ts
203
- /**
204
- * Turns process info (always) and the active request scope (only inside `runWithContext`) into
205
- * OTel-style attributes. User identity is not handled here: `Flare.setUser` writes straight to
206
- * `pendingAttributes`.
207
- *
208
- * `getOptions` is a getter so `configureNode(...)` shows up on later reports without rebuilding this.
209
- */
210
167
  function makeNodeContextCollector(provider, getOptions) {
211
168
  return (config) => {
212
169
  const attrs = {
@@ -283,23 +240,12 @@ function buildFatalCallbacks(flare, getOpts, exit = process.exit.bind(process))
283
240
 
284
241
  //#endregion
285
242
  //#region src/process/handlers.ts
286
- /**
287
- * Owns the `uncaughtException` and `unhandledRejection` listeners that feed fatal failures to Flare.
288
- * Separate from `NodeFlare` because it is pure `process`-event plumbing with no Flare semantics, which
289
- * keeps it trivially testable.
290
- *
291
- * Deliberately kept separate from electron's ProcessHandlerManager, not a shared module. @flareapp/electron
292
- * does not depend on @flareapp/node, and the only package both import is @flareapp/core, which ships in
293
- * every browser bundle and touches `process` only behind a typeof guard. A shared manager belongs in a new
294
- * package, not in core, and one method does not pay for one.
295
- */
296
243
  var ProcessHandlerManager = class {
297
244
  uncaughtHandler = null;
298
245
  rejectionHandler = null;
299
246
  constructor(cbs) {
300
247
  this.cbs = cbs;
301
248
  }
302
- /** Idempotent: a no-op when the attached listeners already match the supplied modes. */
303
249
  reconcile(opts) {
304
250
  this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
305
251
  this.uncaughtHandler = h;
@@ -308,7 +254,6 @@ var ProcessHandlerManager = class {
308
254
  this.rejectionHandler = h;
309
255
  }, (reason) => this.cbs.onRejection(reason));
310
256
  }
311
- /** Remove both listeners regardless of intent. Safe when nothing is attached. */
312
257
  detach() {
313
258
  if (this.uncaughtHandler) {
314
259
  process.off("uncaughtException", this.uncaughtHandler);
@@ -319,11 +264,6 @@ var ProcessHandlerManager = class {
319
264
  this.rejectionHandler = null;
320
265
  }
321
266
  }
322
- /**
323
- * Generic attach/detach for one event. The `get`/`set` closures share this body across both events
324
- * while mutating distinct fields (`uncaughtHandler` vs `rejectionHandler`). Attaches when wanted and
325
- * absent, detaches when unwanted and present, else no-op.
326
- */
327
267
  reconcileOne(event, mode, get, set, impl) {
328
268
  const current = get();
329
269
  const wants = mode !== "off";
@@ -346,25 +286,15 @@ var NodeScope = class extends _flareapp_core.Scope {
346
286
 
347
287
  //#endregion
348
288
  //#region src/scope/AsyncLocalStorageScopeProvider.ts
349
- /**
350
- * Gives every in-flight request its own `NodeScope`, isolated from concurrent requests.
351
- *
352
- * The `fallback` scope catches work outside any request (process-level reports, startup errors,
353
- * scheduled jobs). It is per-instance rather than fresh per read, so outside-scope writes persist for a
354
- * later outside report.
355
- */
356
289
  var AsyncLocalStorageScopeProvider = class {
357
290
  als = new node_async_hooks.AsyncLocalStorage();
358
291
  fallback = new NodeScope();
359
- /** Never null: falls back to the shared scope outside `runWithContext`. */
360
292
  active() {
361
293
  return this.als.getStore() ?? this.fallback;
362
294
  }
363
- /** Null outside `runWithContext`, so callers can tell "inside a request" from "not". */
364
295
  getContext() {
365
296
  return this.als.getStore() ?? null;
366
297
  }
367
- /** `request` is shallow-cloned so later edits to the caller's object do not leak into the scope. */
368
298
  runWithContext(request, fn) {
369
299
  const scope = new NodeScope();
370
300
  scope.request = { ...request };
@@ -381,11 +311,6 @@ var AsyncLocalStorageScopeProvider = class {
381
311
 
382
312
  //#endregion
383
313
  //#region src/stacktrace/DiskFileReader.ts
384
- /**
385
- * Reads snippet sources off disk: on the server a frame's "URL" is usually a local path or a `file://`
386
- * URL from `import.meta.url`. Only unambiguously local paths are read, which rules out traversal,
387
- * following an http frame in a server build, and cwd-relative ambiguity. Never throws.
388
- */
389
314
  var DiskFileReader = class {
390
315
  async read(url) {
391
316
  if (!isLocalFileUrl(url)) return null;
@@ -396,7 +321,6 @@ var DiskFileReader = class {
396
321
  }
397
322
  }
398
323
  };
399
- /** `file://` (any casing), POSIX absolute, Windows drive-letter (`C:\foo`), Windows UNC (`\\`). */
400
324
  function isLocalFileUrl(url) {
401
325
  return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
402
326
  }
@@ -404,9 +328,7 @@ function isLocalFileUrl(url) {
404
328
  //#endregion
405
329
  //#region src/Flare.ts
406
330
  const NODE_SDK_NAME = "@flareapp/node";
407
- const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.11.0" : "?";
408
- /** `g`/`y` make `.test()` keep `lastIndex` state, so reusing the regex across keys skips every other
409
- * match. All other flags are preserved. */
331
+ const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.12.0" : "?";
410
332
  function sanitizeRegex(re) {
411
333
  const safeFlags = re.flags.replace(/[gy]/g, "");
412
334
  return new RegExp(re.source, safeFlags);
@@ -424,18 +346,12 @@ const DEFAULT_NODE_OPTIONS = {
424
346
  bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST
425
347
  };
426
348
  /**
427
- * Node.js-specific `Flare` singleton, exposed from `@flareapp/node` as `flare`.
428
- *
429
- * Subclasses core's `Flare` and wires the Node-only seams in its constructor:
430
- * - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
431
- * isolated from concurrent requests.
432
- * - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
433
- * - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
434
- * - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
349
+ * Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
435
350
  *
436
- * Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
437
- * `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
438
- * type and `configureNode(...)` stays callable mid-chain.
351
+ * Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
352
+ * `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
353
+ * and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
354
+ * `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
439
355
  */
440
356
  var NodeFlare = class extends _flareapp_core.Flare {
441
357
  nodeOptions = { ...DEFAULT_NODE_OPTIONS };
package/dist/index.d.cts CHANGED
@@ -29,18 +29,12 @@ declare class NodeScope extends Scope$1 {
29
29
  //#endregion
30
30
  //#region src/Flare.d.ts
31
31
  /**
32
- * Node.js-specific `Flare` singleton, exposed from `@flareapp/node` as `flare`.
32
+ * Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
33
33
  *
34
- * Subclasses core's `Flare` and wires the Node-only seams in its constructor:
35
- * - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
36
- * isolated from concurrent requests.
37
- * - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
38
- * - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
39
- * - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
40
- *
41
- * Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
42
- * `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
43
- * type and `configureNode(...)` stays callable mid-chain.
34
+ * Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
35
+ * `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
36
+ * and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
37
+ * `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
44
38
  */
45
39
  declare class NodeFlare extends Flare$1 {
46
40
  private nodeOptions;
package/dist/index.d.mts CHANGED
@@ -29,18 +29,12 @@ declare class NodeScope extends Scope$1 {
29
29
  //#endregion
30
30
  //#region src/Flare.d.ts
31
31
  /**
32
- * Node.js-specific `Flare` singleton, exposed from `@flareapp/node` as `flare`.
32
+ * Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
33
33
  *
34
- * Subclasses core's `Flare` and wires the Node-only seams in its constructor:
35
- * - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
36
- * isolated from concurrent requests.
37
- * - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
38
- * - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
39
- * - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
40
- *
41
- * Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
42
- * `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
43
- * type and `configureNode(...)` stays callable mid-chain.
34
+ * Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
35
+ * `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
36
+ * and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
37
+ * `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
44
38
  */
45
39
  declare class NodeFlare extends Flare$1 {
46
40
  private nodeOptions;
package/dist/index.mjs CHANGED
@@ -5,24 +5,8 @@ import { readFile } from "node:fs/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  //#region src/context/body.ts
8
- /** `^` plus `\b` accepts a `; charset=utf-8` suffix while still rejecting `...-urlencoded-foo`. */
9
8
  const DEFAULT_BODY_CONTENT_TYPES = /^application\/(json|x-www-form-urlencoded)\b/i;
10
- /** Reuses core's URL denylist, so credentials are caught by the same regex everywhere. */
11
9
  const DEFAULT_BODY_KEY_DENYLIST = DEFAULT_URL_DENYLIST$1;
12
- /**
13
- * Normalize, redact, serialize, and size-cap a request body for a Flare report. Returns the JSON string,
14
- * or `null` when the body should not be reported (unknown shape, content-type miss, serialization fail).
15
- *
16
- * Accepts four runtime shapes:
17
- * - `string`: must match `contentType` per `bodyAllowedContentTypes`, else dropped.
18
- * - `Buffer`: decoded UTF-8 then treated as a string.
19
- * - `URLSearchParams`: flattened to a plain record. No content-type gate (shape is unambiguous).
20
- * - Other `object`/array: used as-is, no gate. The common middleware path (Express/Fastify `req.body`).
21
- * Anything else returns `null`.
22
- *
23
- * Then: redact (denylisted keys become `'[redacted]'`, cycles become `'[Circular]'`), `JSON.stringify`
24
- * (drop body if it throws on BigInt/Symbol/etc), and truncate to `bodyMaxBytes` including the suffix.
25
- */
26
10
  function captureBody(body, contentType, opts) {
27
11
  if (body === void 0 || body === null) return null;
28
12
  let parsed;
@@ -48,7 +32,6 @@ function captureBody(body, contentType, opts) {
48
32
  }
49
33
  const TRUNCATION_SUFFIX = "…[truncated]";
50
34
  const TRUNCATION_SUFFIX_BYTES = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
51
- /** Walks back over continuation bytes (`10xxxxxx`) to a codepoint boundary, so the result still decodes. */
52
35
  function truncateToByteLimit(serialized, maxBytes) {
53
36
  const buf = Buffer.from(serialized, "utf8");
54
37
  if (buf.length <= maxBytes) return serialized;
@@ -62,15 +45,12 @@ function truncateToByteLimit(serialized, maxBytes) {
62
45
  while (cut > 0 && (buf[cut] & 192) === 128) cut--;
63
46
  return buf.subarray(0, cut).toString("utf8") + TRUNCATION_SUFFIX;
64
47
  }
65
- /** Normalizes to the bare media type first, so a strict custom regex like `/^application\/json$/` still
66
- * matches `application/json; charset=utf-8`. */
67
48
  function matchesContentType(ct, allowed) {
68
49
  if (!ct) return false;
69
50
  const mediaType = ct.split(";")[0].trim().toLowerCase();
70
51
  if (!mediaType) return false;
71
52
  return allowed.test(mediaType);
72
53
  }
73
- /** Returns `undefined` rather than `null` on failure, since `null` is itself a valid JSON value. */
74
54
  function parseString(text, contentType) {
75
55
  if (contentType && /x-www-form-urlencoded/i.test(contentType)) return Object.fromEntries(new URLSearchParams(text).entries());
76
56
  try {
@@ -79,13 +59,11 @@ function parseString(text, contentType) {
79
59
  return;
80
60
  }
81
61
  }
82
- /** Excludes class instances, streams, FormData, ArrayBuffer views, Buffer and URLSearchParams. */
83
62
  function isPlainObject(value) {
84
63
  if (value === null || typeof value !== "object") return false;
85
64
  const proto = Object.getPrototypeOf(value);
86
65
  return proto === null || proto === Object.prototype;
87
66
  }
88
- /** `seen` is a parameter rather than a closure to avoid allocating a WeakSet per recursion. */
89
67
  function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
90
68
  if (value === null || typeof value !== "object") return value;
91
69
  if (seen.has(value)) return "[Circular]";
@@ -98,7 +76,6 @@ function redact(value, denylist, seen = /* @__PURE__ */ new WeakSet()) {
98
76
 
99
77
  //#endregion
100
78
  //#region src/context/deviceInfo.ts
101
- /** Node os + runtime as normalised device info. */
102
79
  var NodeDeviceInfoProvider = class {
103
80
  collect() {
104
81
  return {
@@ -117,7 +94,6 @@ const nodeDeviceInfoProvider = new NodeDeviceInfoProvider();
117
94
 
118
95
  //#endregion
119
96
  //#region src/context/headers.ts
120
- /** Case-insensitive. An array value collapses to its first element; callers here want a single value. */
121
97
  function findHeader(headers, name) {
122
98
  if (!headers) return;
123
99
  const target = name.toLowerCase();
@@ -127,20 +103,12 @@ function findHeader(headers, name) {
127
103
  return Array.isArray(value) ? value[0] : value;
128
104
  }
129
105
  }
130
- /** The `^` and `$` matter: without them, `cookie` would also match a header like `X-Some-Cookie-Hint`. */
131
106
  const DEFAULT_HEADER_DENYLIST = /^(authorization|proxy-authorization|cookie|set-cookie|x-api-key|x-csrf-token|x-xsrf-token|x-auth-token|forwarded|x-forwarded-(?:for|user))$/i;
132
- /** `g`/`y` are stripped from a custom pattern: those carry lastIndex, which makes `.test()` stateful. */
133
107
  function resolveHeaderDenylist(custom, replaceDefault = false) {
134
108
  if (!custom) return DEFAULT_HEADER_DENYLIST;
135
109
  if (replaceDefault) return new RegExp(custom.source, custom.flags.replace(/[gy]/g, ""));
136
110
  return new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:${custom.source})`, "i");
137
111
  }
138
- /**
139
- * Turns headers into `http.request.header.<name>` attributes. The two lists differ on purpose: an
140
- * allowlist drops a header entirely, for apps that may only send named headers, while the denylist
141
- * keeps the name and replaces the value, so you can still see the header was there. `undefined` is
142
- * how `node:http` says "not sent", so those are dropped.
143
- */
144
112
  function projectHeaders(headers, options) {
145
113
  const out = {};
146
114
  if (!headers) return out;
@@ -156,10 +124,6 @@ function projectHeaders(headers, options) {
156
124
 
157
125
  //#endregion
158
126
  //#region src/context/process.ts
159
- /**
160
- * Host + process attributes, read per report so `process.uptime()` and `os.hostname()` stay current.
161
- * os and runtime come from `NodeDeviceInfoProvider`.
162
- */
163
127
  function collectProcessAttributes() {
164
128
  return {
165
129
  "process.pid": process.pid,
@@ -171,13 +135,6 @@ function collectProcessAttributes() {
171
135
 
172
136
  //#endregion
173
137
  //#region src/context/collectNode.ts
174
- /**
175
- * Turns process info (always) and the active request scope (only inside `runWithContext`) into
176
- * OTel-style attributes. User identity is not handled here: `Flare.setUser` writes straight to
177
- * `pendingAttributes`.
178
- *
179
- * `getOptions` is a getter so `configureNode(...)` shows up on later reports without rebuilding this.
180
- */
181
138
  function makeNodeContextCollector(provider, getOptions) {
182
139
  return (config) => {
183
140
  const attrs = {
@@ -254,23 +211,12 @@ function buildFatalCallbacks(flare, getOpts, exit = process.exit.bind(process))
254
211
 
255
212
  //#endregion
256
213
  //#region src/process/handlers.ts
257
- /**
258
- * Owns the `uncaughtException` and `unhandledRejection` listeners that feed fatal failures to Flare.
259
- * Separate from `NodeFlare` because it is pure `process`-event plumbing with no Flare semantics, which
260
- * keeps it trivially testable.
261
- *
262
- * Deliberately kept separate from electron's ProcessHandlerManager, not a shared module. @flareapp/electron
263
- * does not depend on @flareapp/node, and the only package both import is @flareapp/core, which ships in
264
- * every browser bundle and touches `process` only behind a typeof guard. A shared manager belongs in a new
265
- * package, not in core, and one method does not pay for one.
266
- */
267
214
  var ProcessHandlerManager = class {
268
215
  uncaughtHandler = null;
269
216
  rejectionHandler = null;
270
217
  constructor(cbs) {
271
218
  this.cbs = cbs;
272
219
  }
273
- /** Idempotent: a no-op when the attached listeners already match the supplied modes. */
274
220
  reconcile(opts) {
275
221
  this.reconcileOne("uncaughtException", opts.uncaughtExceptionMode, () => this.uncaughtHandler, (h) => {
276
222
  this.uncaughtHandler = h;
@@ -279,7 +225,6 @@ var ProcessHandlerManager = class {
279
225
  this.rejectionHandler = h;
280
226
  }, (reason) => this.cbs.onRejection(reason));
281
227
  }
282
- /** Remove both listeners regardless of intent. Safe when nothing is attached. */
283
228
  detach() {
284
229
  if (this.uncaughtHandler) {
285
230
  process.off("uncaughtException", this.uncaughtHandler);
@@ -290,11 +235,6 @@ var ProcessHandlerManager = class {
290
235
  this.rejectionHandler = null;
291
236
  }
292
237
  }
293
- /**
294
- * Generic attach/detach for one event. The `get`/`set` closures share this body across both events
295
- * while mutating distinct fields (`uncaughtHandler` vs `rejectionHandler`). Attaches when wanted and
296
- * absent, detaches when unwanted and present, else no-op.
297
- */
298
238
  reconcileOne(event, mode, get, set, impl) {
299
239
  const current = get();
300
240
  const wants = mode !== "off";
@@ -317,25 +257,15 @@ var NodeScope = class extends Scope$1 {
317
257
 
318
258
  //#endregion
319
259
  //#region src/scope/AsyncLocalStorageScopeProvider.ts
320
- /**
321
- * Gives every in-flight request its own `NodeScope`, isolated from concurrent requests.
322
- *
323
- * The `fallback` scope catches work outside any request (process-level reports, startup errors,
324
- * scheduled jobs). It is per-instance rather than fresh per read, so outside-scope writes persist for a
325
- * later outside report.
326
- */
327
260
  var AsyncLocalStorageScopeProvider = class {
328
261
  als = new AsyncLocalStorage();
329
262
  fallback = new NodeScope();
330
- /** Never null: falls back to the shared scope outside `runWithContext`. */
331
263
  active() {
332
264
  return this.als.getStore() ?? this.fallback;
333
265
  }
334
- /** Null outside `runWithContext`, so callers can tell "inside a request" from "not". */
335
266
  getContext() {
336
267
  return this.als.getStore() ?? null;
337
268
  }
338
- /** `request` is shallow-cloned so later edits to the caller's object do not leak into the scope. */
339
269
  runWithContext(request, fn) {
340
270
  const scope = new NodeScope();
341
271
  scope.request = { ...request };
@@ -352,11 +282,6 @@ var AsyncLocalStorageScopeProvider = class {
352
282
 
353
283
  //#endregion
354
284
  //#region src/stacktrace/DiskFileReader.ts
355
- /**
356
- * Reads snippet sources off disk: on the server a frame's "URL" is usually a local path or a `file://`
357
- * URL from `import.meta.url`. Only unambiguously local paths are read, which rules out traversal,
358
- * following an http frame in a server build, and cwd-relative ambiguity. Never throws.
359
- */
360
285
  var DiskFileReader = class {
361
286
  async read(url) {
362
287
  if (!isLocalFileUrl(url)) return null;
@@ -367,7 +292,6 @@ var DiskFileReader = class {
367
292
  }
368
293
  }
369
294
  };
370
- /** `file://` (any casing), POSIX absolute, Windows drive-letter (`C:\foo`), Windows UNC (`\\`). */
371
295
  function isLocalFileUrl(url) {
372
296
  return /^file:\/\//i.test(url) || url.startsWith("/") || /^[a-z]:[\\/]/i.test(url) || url.startsWith("\\\\");
373
297
  }
@@ -375,9 +299,7 @@ function isLocalFileUrl(url) {
375
299
  //#endregion
376
300
  //#region src/Flare.ts
377
301
  const NODE_SDK_NAME = "@flareapp/node";
378
- const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.11.0" : "?";
379
- /** `g`/`y` make `.test()` keep `lastIndex` state, so reusing the regex across keys skips every other
380
- * match. All other flags are preserved. */
302
+ const NODE_SDK_VERSION = typeof process !== "undefined" && true ? "0.12.0" : "?";
381
303
  function sanitizeRegex(re) {
382
304
  const safeFlags = re.flags.replace(/[gy]/g, "");
383
305
  return new RegExp(re.source, safeFlags);
@@ -395,18 +317,12 @@ const DEFAULT_NODE_OPTIONS = {
395
317
  bodyKeyDenylist: DEFAULT_BODY_KEY_DENYLIST
396
318
  };
397
319
  /**
398
- * Node.js-specific `Flare` singleton, exposed from `@flareapp/node` as `flare`.
399
- *
400
- * Subclasses core's `Flare` and wires the Node-only seams in its constructor:
401
- * - `AsyncLocalStorageScopeProvider` so each `runWithContext(...)` callback gets its own `NodeScope`,
402
- * isolated from concurrent requests.
403
- * - `makeNodeContextCollector(...)` turns the current `NodeScope` + process info into report attributes.
404
- * - `DiskFileReader` reads source for stack-trace snippets via `node:fs/promises`, not `fetch`.
405
- * - `ProcessHandlerManager` attaches/detaches the fatal process listeners per `NodeOptions`.
320
+ * Node.js `Flare` singleton, exported from `@flareapp/node` as `flare`.
406
321
  *
407
- * Adds Node-only API on top of core: `configureNode`, `runWithContext`, `mergeContext`, `getContext`,
408
- * `removeProcessListeners`. Inherited core methods return `this`, so chaining keeps the `NodeFlare`
409
- * type and `configureNode(...)` stays callable mid-chain.
322
+ * Subclasses core's `Flare` and wires the Node-only seams: per-request scope via
323
+ * `AsyncLocalStorageScopeProvider`, a Node context collector, `DiskFileReader` for stack snippets,
324
+ * and `ProcessHandlerManager` for the fatal listeners. Adds `configureNode`, `runWithContext`,
325
+ * `mergeContext`, `getContext`, and `removeProcessListeners` on top of the core API.
410
326
  */
411
327
  var NodeFlare = class extends Flare$1 {
412
328
  nodeOptions = { ...DEFAULT_NODE_OPTIONS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flareapp/node",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Node.js SDK for flareapp.io",
5
5
  "homepage": "https://flareapp.io",
6
6
  "bugs": {
@@ -44,7 +44,7 @@
44
44
  "release": "release-it"
45
45
  },
46
46
  "dependencies": {
47
- "@flareapp/core": "2.11.0"
47
+ "@flareapp/core": "2.12.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@flareapp/test-helpers": "*",