@flareapp/node 0.1.0 → 0.2.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/README.md +20 -2
- package/dist/index.cjs +848 -0
- package/dist/index.d.cts +138 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.mjs +763 -0
- package/package.json +5 -2
- package/.oxlintrc.json +0 -7
- package/.release-it.json +0 -13
- package/CHANGELOG.md +0 -22
- package/src/Flare.ts +0 -224
- package/src/context/body.ts +0 -185
- package/src/context/collectNode.ts +0 -116
- package/src/context/headers.ts +0 -90
- package/src/context/process.ts +0 -25
- package/src/index.ts +0 -27
- package/src/process/fatal.ts +0 -54
- package/src/process/handlers.ts +0 -109
- package/src/scope/AsyncLocalStorageScopeProvider.ts +0 -86
- package/src/scope/NodeScope.ts +0 -8
- package/src/stacktrace/DiskFileReader.ts +0 -57
- package/src/types.ts +0 -37
- package/tests/asyncScopeProvider.test.ts +0 -43
- package/tests/body.test.ts +0 -129
- package/tests/diskFileReader.test.ts +0 -36
- package/tests/fatalHandlers.test.ts +0 -140
- package/tests/flush.test.ts +0 -11
- package/tests/headers.test.ts +0 -86
- package/tests/integration.test.ts +0 -63
- package/tests/lifecycle.test.ts +0 -71
- package/tests/nodeContextCollector.test.ts +0 -106
- package/tests/nodeExports.test.ts +0 -11
- package/tests/nodeScope.test.ts +0 -19
- package/tests/processAttributes.test.ts +0 -15
- package/tests/processHandlers.test.ts +0 -47
- package/tests/regexFlagSanitization.test.ts +0 -88
- package/tests/scopeIsolation.test.ts +0 -47
- package/tests/setFrameworkScope.test.ts +0 -86
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -18
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { captureBody, DEFAULT_BODY_CONTENT_TYPES } from '../src/context/body';
|
|
4
|
-
import { projectHeaders, DEFAULT_HEADER_DENYLIST } from '../src/context/headers';
|
|
5
|
-
import { NodeFlare } from '../src/Flare';
|
|
6
|
-
|
|
7
|
-
// Helpers to extract stored options from a configured NodeFlare instance.
|
|
8
|
-
// We drive the options through the real configureNode path then exercise
|
|
9
|
-
// the functions that consume them, so that we test the full sanitize path.
|
|
10
|
-
|
|
11
|
-
describe('regex flag sanitization in configureNode', () => {
|
|
12
|
-
describe('bodyKeyDenylist with g flag', () => {
|
|
13
|
-
it('redacts every matching key even when denylist has the g flag', () => {
|
|
14
|
-
// Without sanitization, /password|token/g would retain lastIndex state
|
|
15
|
-
// across calls and silently skip the second match.
|
|
16
|
-
const instance = new NodeFlare();
|
|
17
|
-
instance.configureNode({
|
|
18
|
-
bodyKeyDenylist: /password|token/g,
|
|
19
|
-
captureRequestBody: true,
|
|
20
|
-
bodyAllowedContentTypes: DEFAULT_BODY_CONTENT_TYPES,
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Extract the sanitized denylist by capturing the body with a known input.
|
|
24
|
-
// We pass the body object directly (skips content-type check).
|
|
25
|
-
const body = { user: 'alice', password: 'secret', token: 'abc' };
|
|
26
|
-
// Re-use captureBody with the sanitized regex extracted via getContext hack.
|
|
27
|
-
// Simpler: call captureBody manually with the regex that configureNode produced.
|
|
28
|
-
// We can't easily inspect private state, so we test the observable effect
|
|
29
|
-
// through captureBody called with a regex that has the g flag removed.
|
|
30
|
-
const sanitizedDenylist = new RegExp(/password|token/.source, '');
|
|
31
|
-
const out = captureBody(body, undefined, {
|
|
32
|
-
bodyAllowedContentTypes: DEFAULT_BODY_CONTENT_TYPES,
|
|
33
|
-
bodyKeyDenylist: sanitizedDenylist,
|
|
34
|
-
bodyMaxBytes: 16_384,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const parsed = JSON.parse(out!);
|
|
38
|
-
expect(parsed.user).toBe('alice');
|
|
39
|
-
expect(parsed.password).toBe('[redacted]');
|
|
40
|
-
expect(parsed.token).toBe('[redacted]');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('g flag on bodyKeyDenylist causes silent skip without sanitization (documents the bug)', () => {
|
|
44
|
-
// This test documents the broken behavior that sanitization fixes.
|
|
45
|
-
// With the g flag, the second test() call on the same regex instance
|
|
46
|
-
// may return false for a matching key (depending on lastIndex state).
|
|
47
|
-
const buggyRegex = /password|token/g;
|
|
48
|
-
const keys = ['password', 'token'];
|
|
49
|
-
const results = keys.map((k) => buggyRegex.test(k));
|
|
50
|
-
// At least one of the two tests returns false due to lastIndex advancement.
|
|
51
|
-
// (After matching 'password', lastIndex moves past it, so 'token' test resets
|
|
52
|
-
// or mis-fires depending on the JS engine state.)
|
|
53
|
-
// The exact outcome is engine-dependent, but the point is that the g flag
|
|
54
|
-
// makes behavior unreliable — sanitization removes it to guarantee correctness.
|
|
55
|
-
expect(results.includes(false)).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('headerDenylist — resolveHeaderDenylist already strips g/y (regression guard)', () => {
|
|
60
|
-
it('custom headerDenylist with g flag is combined correctly and redacts all matches', () => {
|
|
61
|
-
// resolveHeaderDenylist merges and strips g/y from the custom part.
|
|
62
|
-
const attrs = projectHeaders(
|
|
63
|
-
{ 'Authorization': 'Bearer xyz', 'X-Custom-Secret': 'val', 'X-Other': 'ok' },
|
|
64
|
-
{
|
|
65
|
-
headerDenylist: new RegExp(`(?:${DEFAULT_HEADER_DENYLIST.source})|(?:x-custom-secret)`, 'i'),
|
|
66
|
-
headerAllowlist: null,
|
|
67
|
-
},
|
|
68
|
-
);
|
|
69
|
-
expect(attrs['http.request.header.authorization']).toBe('[redacted]');
|
|
70
|
-
expect(attrs['http.request.header.x-custom-secret']).toBe('[redacted]');
|
|
71
|
-
expect(attrs['http.request.header.x-other']).toBe('ok');
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('bodyAllowedContentTypes with g flag', () => {
|
|
76
|
-
it('still matches content-type correctly after g flag is stripped', () => {
|
|
77
|
-
// A g-flagged regex for allowed content types would misfire on the second
|
|
78
|
-
// check after sanitization removes the flag, the regex becomes stateless.
|
|
79
|
-
const body = '{"a":1}';
|
|
80
|
-
const out = captureBody(body, 'application/json', {
|
|
81
|
-
bodyAllowedContentTypes: new RegExp(DEFAULT_BODY_CONTENT_TYPES.source, ''),
|
|
82
|
-
bodyKeyDenylist: /^$/,
|
|
83
|
-
bodyMaxBytes: 16_384,
|
|
84
|
-
});
|
|
85
|
-
expect(out).toBe('{"a":1}');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
-
|
|
3
|
-
import { flare } from '../src';
|
|
4
|
-
|
|
5
|
-
beforeAll(() => {
|
|
6
|
-
flare.removeProcessListeners();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
afterAll(() => {
|
|
10
|
-
flare.removeProcessListeners();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('concurrent request scope isolation', () => {
|
|
14
|
-
it('glows, attributes, user, entryPoint do not leak across requests', async () => {
|
|
15
|
-
const captured: Array<Record<string, unknown>> = [];
|
|
16
|
-
|
|
17
|
-
async function request(label: string) {
|
|
18
|
-
return flare.runWithContext({ path: `/${label}` }, async () => {
|
|
19
|
-
flare.glow(`glow-${label}`);
|
|
20
|
-
flare.addContext(`ctx-${label}`, label);
|
|
21
|
-
flare.setUser({ id: `u-${label}` });
|
|
22
|
-
flare.setEntryPoint({ identifier: `/handler/${label}`, type: 'http' });
|
|
23
|
-
await new Promise((r) => setTimeout(r, Math.random() * 20));
|
|
24
|
-
const scope = flare.getContext()!;
|
|
25
|
-
const custom = (scope.pendingAttributes['context.custom'] ?? {}) as Record<string, unknown>;
|
|
26
|
-
captured.push({
|
|
27
|
-
label,
|
|
28
|
-
glows: scope.glows.map((g) => g.name),
|
|
29
|
-
customKeys: Object.keys(custom),
|
|
30
|
-
userId: scope.user?.id,
|
|
31
|
-
entryPointId: scope.entryPoint?.identifier,
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
await Promise.all(['a', 'b', 'c'].map(request));
|
|
37
|
-
captured.sort((x, y) => String(x.label).localeCompare(String(y.label)));
|
|
38
|
-
|
|
39
|
-
for (const row of captured) {
|
|
40
|
-
const label = row.label;
|
|
41
|
-
expect(row.glows).toEqual([`glow-${label}`]);
|
|
42
|
-
expect(row.customKeys).toContain(`ctx-${label}`);
|
|
43
|
-
expect(row.userId).toBe(`u-${label}`);
|
|
44
|
-
expect(row.entryPointId).toBe(`/handler/${label}`);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Api } from '@flareapp/core';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { NodeFlare } from '../src/Flare';
|
|
5
|
-
|
|
6
|
-
function makeInstance() {
|
|
7
|
-
const sent: any[] = [];
|
|
8
|
-
const api = new Api();
|
|
9
|
-
api.report = (report: any) => {
|
|
10
|
-
sent.push(report);
|
|
11
|
-
return Promise.resolve();
|
|
12
|
-
};
|
|
13
|
-
const instance = new NodeFlare();
|
|
14
|
-
instance.api = api;
|
|
15
|
-
instance.light('test-key');
|
|
16
|
-
return { instance, sent };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('setFramework inside runWithContext', () => {
|
|
20
|
-
it('framework attrs appear in report even when called inside a request scope', async () => {
|
|
21
|
-
const { instance, sent } = makeInstance();
|
|
22
|
-
|
|
23
|
-
instance.setFramework({ name: 'Express', version: '4.0.0' });
|
|
24
|
-
|
|
25
|
-
await instance.runWithContext({ method: 'GET', path: '/test' }, async () => {
|
|
26
|
-
await instance.report(new Error('boom'));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
expect(sent.length).toBe(1);
|
|
30
|
-
const attrs = sent[0].attributes as Record<string, unknown>;
|
|
31
|
-
expect(attrs['flare.framework.name']).toBe('Express');
|
|
32
|
-
expect(attrs['flare.framework.version']).toBe('4.0.0');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('framework attrs appear when setFramework is called before runWithContext', async () => {
|
|
36
|
-
const { instance, sent } = makeInstance();
|
|
37
|
-
|
|
38
|
-
instance.setFramework({ name: 'Fastify', version: '5.0.0' });
|
|
39
|
-
|
|
40
|
-
await instance.runWithContext({ method: 'POST', path: '/submit' }, async () => {
|
|
41
|
-
await instance.report(new Error('fastify-error'));
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
expect(sent.length).toBe(1);
|
|
45
|
-
const attrs = sent[0].attributes as Record<string, unknown>;
|
|
46
|
-
expect(attrs['flare.framework.name']).toBe('Fastify');
|
|
47
|
-
expect(attrs['flare.framework.version']).toBe('5.0.0');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('framework attrs appear in report outside of runWithContext', async () => {
|
|
51
|
-
const { instance, sent } = makeInstance();
|
|
52
|
-
|
|
53
|
-
instance.setFramework({ name: 'Koa', version: '3.0.0' });
|
|
54
|
-
await instance.report(new Error('koa-error'));
|
|
55
|
-
|
|
56
|
-
expect(sent.length).toBe(1);
|
|
57
|
-
const attrs = sent[0].attributes as Record<string, unknown>;
|
|
58
|
-
expect(attrs['flare.framework.name']).toBe('Koa');
|
|
59
|
-
expect(attrs['flare.framework.version']).toBe('3.0.0');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('context.custom.framework is set inside runWithContext when setFramework was called at startup', async () => {
|
|
63
|
-
const { instance, sent } = makeInstance();
|
|
64
|
-
|
|
65
|
-
instance.setFramework({ name: 'Express', version: '4.0.0' });
|
|
66
|
-
|
|
67
|
-
await instance.runWithContext({ method: 'GET', path: '/test' }, async () => {
|
|
68
|
-
await instance.report(new Error('boom'));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(sent.length).toBe(1);
|
|
72
|
-
const custom = sent[0].attributes['context.custom'] as Record<string, unknown>;
|
|
73
|
-
expect(custom.framework).toBe('express');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('context.custom.framework is set outside runWithContext', async () => {
|
|
77
|
-
const { instance, sent } = makeInstance();
|
|
78
|
-
|
|
79
|
-
instance.setFramework({ name: 'Koa', version: '3.0.0' });
|
|
80
|
-
await instance.report(new Error('koa-error'));
|
|
81
|
-
|
|
82
|
-
expect(sent.length).toBe(1);
|
|
83
|
-
const custom = sent[0].attributes['context.custom'] as Record<string, unknown>;
|
|
84
|
-
expect(custom.framework).toBe('koa');
|
|
85
|
-
});
|
|
86
|
-
});
|
package/tsconfig.json
DELETED
package/vitest.config.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { resolve, dirname } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
|
|
4
|
-
import { defineConfig } from 'vitest/config';
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
|
|
8
|
-
export default defineConfig({
|
|
9
|
-
test: {
|
|
10
|
-
environment: 'node',
|
|
11
|
-
},
|
|
12
|
-
resolve: {
|
|
13
|
-
alias: {
|
|
14
|
-
'@flareapp/core': resolve(__dirname, '../core/src/index.ts'),
|
|
15
|
-
'@flareapp/node': resolve(__dirname, 'src/index.ts'),
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
});
|